전략 백테스팅(Event-Driven)

게으른직장인·2022년 3월 13일
0
post-thumbnail

간단한 전략을 정하고 백테스팅 해보자.

전략

import matplotlib.pyplot as plt
import mplfinance as mpf
import numpy as np

# df = 이전 포스팅 참조...

df['EMA12'] = df['close'].ewm(12, adjust=False).mean()
df['EMA26'] = df['close'].ewm(26, adjust=False).mean()

df['MACD'] = df['EMA12'] - df['EMA26']

df['MACD_Signal'] = df['MACD'].ewm(9, adjust=False).mean()
df['MACD_DIFF'] = df['MACD'] - df['MACD_Signal']

df_draw = df[-400:]

adps = []
adps.append(
    mpf.make_addplot(df_draw['EMA12'],panel=0,type='line'))
adps.append(
    mpf.make_addplot(df_draw['EMA26'],panel=0,type='line'))
adps.append(
    mpf.make_addplot(df_draw['MACD'],panel=1,type='line', ylabel='MACD'))
adps.append(
    mpf.make_addplot(df_draw['MACD_Signal'],panel=1,type='line', color='orange',secondary_y=False))
adps.append(
    mpf.make_addplot(np.zeros((len(df_draw))), panel=1,type='line', color='red', linestyle='dotted',secondary_y=False))
adps.append(
    mpf.make_addplot(df_draw['MACD_DIFF'],panel=1,type='bar', color='green'))

fig, axs = mpf.plot(df_draw, style='charles', figratio=(4,3),figscale=1.5, addplot=adps, returnfig=True)

axs[0].legend(['EMA12', 'EMA26'])
axs[2].legend(['MACD', 'MACD_SIGNAL'])

거래 발생 조건

앞서 배웠던 기술적 지표 중 MACD가 MACD Signal을 상회화면 선물 매수, 하회하면 선물 매도

거래 규모

이벤트 발생시 올인 올아웃 으로 거래.(몰빵 투자)

거래 발생 표시 차트 그리기

import numpy as np

def macd_diff_up(macd_diff, prc):
    signal   = []
    previous = 0
    for date,value in macd_diff.iteritems():
        if previous < 0 and value > 0:
            signal.append(prc[date])
        else:
            signal.append(np.nan)
        previous = value
    return signal

def macd_diff_down(macd_diff, prc):
    signal   = []
    previous = 0
    for date,value in macd_diff.iteritems():
        if previous > 0 and value < 0:
            signal.append(prc[date])
        else:
            signal.append(np.nan)
        previous = value
    return signal


df_draw = df[:]

low_signal = macd_diff_up(df_draw['MACD_DIFF'], df_draw['close'] )
high_signal = macd_diff_down(df_draw['MACD_DIFF'], df_draw['close'])

adps = []
adps.append(
    mpf.make_addplot(low_signal,type='scatter',markersize=50,marker='^'))
adps.append(
    mpf.make_addplot(high_signal,type='scatter',markersize=50,marker='v'))
adps.append(
    mpf.make_addplot(df_draw['MACD'],panel=1,type='line', ylabel=''))
adps.append(
    mpf.make_addplot(df_draw['MACD_Signal'],panel=1,type='line', color='orange',secondary_y=False))
adps.append(
    mpf.make_addplot(np.zeros((len(df_draw))), panel=1,type='line', color='red', linestyle='dotted',secondary_y=False))
adps.append(
    mpf.make_addplot(df_draw['MACD_DIFF'],panel=1,type='bar', color='green'))

fig, axs = mpf.plot(df_draw, style='charles', figratio=(4,3),figscale=1.5, addplot=adps, returnfig=True)

푸른색 화살표는 저점이라 생각되어 매수, 붉은색 화살표는 고점이라 생각되어 매도 표시이다. 차트에 직접 이벤트를 표시하니 직관적이다. 하지만 수익이 발생하는지, 발생했다면 얼마나 발생하는지와 이 전략이 결국 좋은전략인지 안좋은 전략인지 판단하기에는 힘들다.

백테스팅

백테스팅 코드 작성

acc = {
    'CASH': 1,
    'F_BTC': { 
        'LONG': {'QTY': 0, 'MARGIN': 0, 'PRC': 0, 'DATETIME': None},
        'SHORT': {'QTY': 0, 'MARGIN': 0, 'PRC': 0, 'DATETIME': None}
    }
}

eval_amt_hist = []
td_hist = []

pre_prc = 0

# 백터연산 X, 교육용 FOR 문 활용 백테스팅
for i, (date, row) in zip(range(len(df)), df.iterrows()):   
    long = acc['F_BTC']['LONG']
    short = acc['F_BTC']['SHORT']  
    
    if i == 0: # 최초
        pre_macd_diff = 0
        long['DATETIME'] = date
        short['DATETIME'] = date
        print(acc)
    
    elif i + 1 == len(df): # 마지막 
        ''
    
    else: # 나머지
        pre_macd_diff = df.iloc[i-1]['MACD_DIFF']
        now_macd_diff = row['MACD_DIFF']
        prc = df.iloc[i+1]['open'] # 매수, 매도시 가격은 이벤트 발생 다음 봉의 시가
        
        
        if pre_macd_diff < 0 and now_macd_diff > 0:    
            # SHORT 청산
            if short['QTY'] > 0:
                acc['CASH'] += (short['PRC'] - prc) * short['QTY'] + short['MARGIN']
                rt = short['PRC']/prc - 1
                term = (date - short['DATETIME']).total_seconds()/60/60/24
                td_hist.append({'RETURN': rt, 'TERM': term})
                acc['F_BTC']['SHORT']  = {'QTY': 0, 'MARGIN': 0, 'PRC': 0, 'DATETIME': None}
                short = acc['F_BTC']['SHORT']
            
            # LONG 진입
            cash = acc['CASH']
            qty = cash / prc
            long['QTY'] = qty
            long['PRC'] = prc
            long['MARGIN'] = cash
            long['DATETIME'] = date
            
            acc['CASH'] -= cash
            
        elif pre_macd_diff > 0 and now_macd_diff < 0:    
            # LONG 청산
            if long['QTY'] > 0:
                acc['CASH'] += (prc - long['PRC']) * long['QTY'] + long['MARGIN']
                rt = prc/long['PRC'] - 1
                term = (date - long['DATETIME']).total_seconds()/60/60/24
                td_hist.append({'RETURN': rt, 'TERM': term})
                acc['F_BTC']['LONG'] = {'QTY': 0, 'MARGIN': 0, 'PRC': 0, 'DATETIME': None}
                long = acc['F_BTC']['LONG']
            
            #SHORT 진입
            cash = acc['CASH']
            qty = cash / prc
            short['QTY'] = qty
            short['PRC'] = prc
            short['MARGIN'] = cash   
            short['DATETIME'] = date
            
            acc['CASH'] -= cash
    
    eval_amt =  acc['CASH']
    eval_amt += (row['close'] - long['PRC']) * long['QTY'] + long['MARGIN']
    eval_amt += (short['PRC'] - row['close']) * short['QTY'] + short['MARGIN']
        
    eval_amt_hist.append(eval_amt)
    pre_macd_diff = row['MACD_DIFF']

백테스팅을 통한 전략 평가

위 전략으로 투자했을경우 수익이 얼마나 났을까? 평가 수익을 차트로 나타내어 보자.


df['return'] = eval_amt_hist
bt = df[['return']].copy()

bt[['return']].plot()

수익률이 오르락 내리락 하다 결국 첫 진입대비 6%정도의 수익을 기록했다. 실제 투자자산인 비트코인의 가격이 벤치마크 지수이다. 벤치마크 지수와 위 전략의 수익률을 비교해보자

bt['BM'] = df['close'] / df.iloc[0]['close']

bt.plot(rot=45)

return이 전략 수익률이고 BM이 벤치마크인 비트코인 수익률이다. 전략을보니 결국 비트코인에 몰빵 후 존버하는것이 열심히 전략대로 거래한것보다 수익률이 좋다. 즉, 알파가 없는 전략이므로 해당 전략이 좋은전략이라고 보기 힘들다.(다만, 연습을 위해 백테스팅 기간을 하루만 줬지만 실제로는 훨씬 긴 기간으로 해야한다.)

이전 포스팅에서 배운 백테스팅 관련 개념용어들을 좀더 활용하여 전략을 평가해보자.

mdd = min((bt['return'] - bt['return'].cummax()) / bt['return'].cummax())
bm_mdd = min((bt['BM'] - bt['BM'].cummax()) / bt['BM'].cummax())

days = (bt.index[-1] - bt.index[0]).total_seconds()/60/60/24
cagr = (bt['return'][-1] / bt['return'][0])**(1/(days/365)) - 1
bm_cagr = (bt['BM'][-1] / bt['BM'][0])**(1/(days/365)) - 1
sharpe_ratio = (bt['return'][-1] / bt['return'][0] - 1)/bt['return'].std()

win_rate = sum([1 if v['RETURN'] > 0 else 0 for v in td_hist])/len(td_hist)
hit_rate = sum([1 if v['RETURN']-cagr*term > 0 else 0 for v in td_hist])/len(td_hist)

summary = {
    'MDD': round(mdd * 100, 2),
    'CAGR/1000': round(cagr * 100/1000, 2),
    'ALPHA/1000': round((cagr-bm_cagr) * 100/1000, 2),
    'SHARPE_RATIO': round(sharpe_ratio, 2),
    '-CAGR/MDD': round(cagr/-mdd*100,2),
    'WIN_RATE': round(win_rate*100, 2),
    'HIT_RATE': round(hit_rate*100, 2),
    'TOT_TRADE_CNT': len(td_hist)
}

s_df = pd.DataFrame.from_dict(summary, orient='index').rename(columns={0:'Summary'})
s_df

  • MDD가 -8.72% 해당전략은 내 투자자산이 최고점대비 -8.72% 하락하는 가장 마음아픈 구간이 있다는 것이다. 하루 치곤 꽤나 높은 수치이다.
  • CAGR이 73만%이다. 하루 6%이상 수익이므로, 연복리인 CAGR로 환산하면 어마어마한 수익률이다. 하지만 너무 단기간의 백테스팅의 수익률로 1년치를 퉁치기엔 무리가 있다. 그냥 개념만 짚고 넘어가자.
  • ALPHA는 벤치마크지수를 하회하니 연복리로 환산했을경우 어마어마하게 안좋게 산출된다. 즉, 하루에 비트코인이 9%정도 올랐으므로, 하루치만 가지고 산출하면 벤치마크지수의 수익률은 매일 9프로씩 365일 즉, 1.09 의 365승 만큼으로 산출되어 어마어마하게 높게 산출된다. 그러니 다시한번 강조하지만 이렇게 짧은기간의 백테스팅을 통해 수익률 산출은 사실상 현실적으로 크게 의미는 없다.
  • 샤프 지수는 2.12이다. 변동성 대비 수익률이 저정도 나온다는것인데, 백테스팅을 여러번 해보면 2.12란 수치가 어느정도의 느낌인지 감으로 느낄 수 있을것이다. 아직은 내공이 부족하니 일단 이전포스팅의 계산식을 참조하여 한번 음미만 해보자.
  • -CAGR/MDD는 수익률/최대낙폭 이므로 수익률대비 낙폭이 낮을수록, 즉 최대로 꼴아봤짜 얼마 안꼴면서 수익률이 높을수록 해당 지표는 높게 산출된다. 퀀트전략에서 많이 쓰이는 지표로 Carmal Ratio라고 부른다.
  • WIN_RATE와 HIT_RATE는 전체 거래(TOT_TRADE_CNT)인 42번 거래중 28.57% 수익이 있었다(WIN_RATE) 또는 알파가 있었다(HIT_RATE)라는 의미이다. 즉 승률이 28.75%.... 쓰레기 전략이다.

이외 참고사항

위 백테스팅은 앞에서 말했듯이 기간이 하루이므로 매우 편향될 수 있고 노이즈가 클 수 있다.

또한 거래시에 수수료 및 슬리피지를 전혀 염두에 두지 않았다. 슬리피지라 거래를 할때 발생하는 손실을 의미한다. 즉 현재 가격이 10달러인 종목이 있는데 내가 해당 종목을 매수할때 10달러로 주문을 넣는다고 100% 주문이 성사 되지 않는다. 마지막 거래가 10달러였지만 매도호가는 11달러고 아무도 10달러에 팔아주지않으면 10달러로 거래를 하지 못하는것이다. 즉, 내가 거래하기 위해서는 현재가 이상의 손실이 발생하는것을 감안 해야한다.

이외에도 손실 발생시 청산 로직을 구현하지 않는등 다양한 문제점을 내포하고 있다. 하지만 백테스팅의 개념과 의미를 이해하기위해 위 코드는 반드시 직접 자신의 차트로 실습해보길 바란다.

전략 튜닝

백테스팅을 해보고 미흡한점을 보완하여 수식을 수정하는 일련의 행위를 튜닝이라고 한다. 위 MACD지수에서 매수 매도 이벤트에 특정 변수를 추가 변형하면 아래와 같이 더 좋은 전략을 만들 수 있다.

profile
취미로 퀀트 투자를 도전하는 직장인

0개의 댓글