이전글에 이어서 백테스팅을 진행해보겠습니다.
이 포스팅에서는 백테스팅 패키지(zipline, backtrader 등)을 사용하지 않고 간단하게나마 직접 구현해보겠습니다.
먼저 BackTest 클래스에 필요한 변수들을 보겠습니다.
class VolatilityBackTest(object):
def __init__(self, hourly_data, daily_data, start_cash):
self.hourly_data = hourly_data # 시간단위 데이터
self.daily_data = daily_data # 일단위 데이터
self.position = None # 진입 포지션
self.start_cash = start_cash # 초기 현금 자산
self.cash = self.start_cash # 현재 현금 자산
self.highest_cash = self.cash # 최고점 현금 자산
self.lowest_cash = self.cash # 최저점 현금자산
self.last_highest_cash = self.cash # 최근 최고점 현금 자산
self.last_lowest_cash = self.cash # 최근 최저점 현금자산
self.ror = 1 # rate of return
self.mdd = 0 # maximum drawdown
self.accumulate_ror = 1 # 누적 rate of return
self.trades_log = dict() # 거래 기록
self.trades_count = 0 # 총거래 횟수
self.win_count = 0 # 승리 횟수(매도가/매수가 > 1 이면 승리 )
self.mdd_logs = dict() # mdd 기록용
다음은 백테스팅의 중심부분 run_test 함수입니다.
단순 반복문이며, VolatilityBackTest객체 생성시 전달할 hourly_data(한시간봉)를 기준으로 시간순으로 수행합니다.
무슨 말이냐면 전 포스팅에서 준비했던 테스트 시작시점부터 한시간봉 단위 체크를 수행합니다.
def run_test(self):
for yyyy_mm_dd_hour, row in self.hourly_data.iterrows(): # 한시간봉 반복, hourly_data loop
yyyy_mm_dd = yyyy_mm_dd_hour[:10] # hourly_data의 yyyy-mm-dd format
# 기준이 되는 한시간봉의 날짜가 일봉에 없으면 제외
if yyyy_mm_dd not in self.daily_data.index:
continue
준비한 데이터가 두 벌(일봉, 한시간봉)인데 전 포스팅에서 알아보았듯이 거래량이 없는 잘못된 데이터가 있으므로 이를 걸러주는 작업이 필요합니다.
예를들어 기준이 되는 한시간봉(2020-01-01 09:00:00)의 날짜(2020-01-01)가 일봉에 없으면
거래량이 없는 데이터라 제외되었기에 패스합니다.
(이 때문에 대략적인 백테스팅이 가능합니다. 이런 문제 없이 코인 한시간봉 어디서 갖고 오는지 좀 알려주세요)
다음은 매수 진입 체크입니다.
변동성 돌파 전략이므로 현재가격이 목표가(당일 시가 + Range * K) 보다 높으면 매수합니다.
이에 대한 체크는 목표가가 한시간 봉의 고점 보다 낮거나 저점 보다 높으면 조건에 만족한다고 판단했습니다.
def run_test(self):
for yyyy_mm_dd_hour, row in self.hourly_data.iterrows(): # 한시간봉 반복, hourly_data loop
yyyy_mm_dd = yyyy_mm_dd_hour[:10] # hourly_data의 yyyy-mm-dd format
# 기준이 되는 한시간봉의 날짜가 일봉에 없으면 제외
if yyyy_mm_dd not in self.daily_data.index:
continue
target_price = self.daily_data.loc[yyyy_mm_dd]['Target_Price'] # 목표가
# 진입한 포지션이 없다면 진입 체크
if not self.position:
# 목표가가 한시간봉 안에 있다면 매수
if row['Low'] <= target_price < row['High']:
print('Got buy Signal!')
self.position = {'created_at': yyyy_mm_dd_hour,
'volume': round(self.cash * 0.5 / target_price, 7),
'bid_price': target_price}
# 매수 체결가 계산
# 주문가격 * 주문수량 + 주문가격 * 주문수량 * 거래수수료율 = 주문가격 * 주문수량 + (1+ 거래수수료율)
contract_price = self.position['bid_price'] * self.position['volume'] * 1.00139
# 현재 잔액 - 매수 체결가
self.cash -= contract_price
print('매수체결 금액: %s, 체결 금액 : %s , 개수 : %s' % (self.position['bid_price'], contract_price, self.position['volume']))
self.trades_count += 1
위 코드에서 살펴볼 부분은 크게 세가지입니다.
먼저 self.postion의 volume 부분입니다.
해당 코인 주문 수량 계산은 (가진 현금 / 매수 호가)이지만 목표가(target_price)에 도달하면
바로 주문을 넣어 체결된다는 가정으로 매수 호가 = 목표가로 계산했습니다.
또 가진 현금을 다 투입하지 않고 50%만 트레이딩하도록 조절하여 주문 수량을 다음과 같이 계산하였습니다.
round(self.cash * 0.5 / target_price, 7)
다음은 매수 체결가 계산입니다.
여기서 고려하지 않은 부분은 시장가 매수로 인한 슬리피지입니다. 지정가 주문으로만 체결된다고 가정했으며
사용한 거래수수료율은 업비트 기준입니다.
거래수수료율 계산은 다음에 근거해 계산했습니다.
데이터는 빗썸에서 갖고 오고 거래수수료율은 업비트 것을 사용하고..,
뒤죽 박죽 입맛대로라 반성하는 차원에서 거래수수료율은 할인하지 않은 0.139%를 적용했습니다.
마지막으로 살펴볼 부분은 제일 중요한 부분인 매수 조건입니다.
# 목표가가 한시간봉 안에 있다면 매수
if row['Low'] <= target_price < row['High']:
# do buy
row['High'] > target_price로 나타나는 조건을 살펴보면
제일 높은 가격이 한 번이라도 목표가인 target_price를 넘으면 매수한다는 뜻입니다.
이렇게 하면 돌파선을 확실히 넘는 상승 뿐만 아니라 돌파선을 잠깐 터치하고 내려오는 경우도 매수 타이밍으로 계산하게 되는 현상이 있어 추후 약간의 개선이 필요한 부분입니다.
다음은 매도 체결 부분 구현입니다.
매도 시점은 다음날 09:00이며 매수 부분을 포함한 전체 run_test 함수를 첨부합니다.
def run_test(self):
for yyyy_mm_dd_hour, row in self.hourly_data.iterrows(): # 한시간봉 반복, hourly_data loop
yyyy_mm_dd = yyyy_mm_dd_hour[:10] # hourly_data의 yyyy-mm-dd format
# 기준이 되는 한시간봉의 날짜가 일봉에 없으면 제외
if yyyy_mm_dd not in self.daily_data.index:
continue
target_price = self.daily_data.loc[yyyy_mm_dd]['Target_Price'] # 목표가
# 진입한 포지션이 없다면 진입 체크
if not self.position:
# 목표가가 한시간봉 안에 있다면 매수
if row['Low'] <= target_price <= row['High']:
self.position = {'created_at': yyyy_mm_dd_hour,
'volume': round(self.cash * 0.5 / target_price, 7),
'bid_price': target_price}
# 매수 체결가 계산
# 주문가격 * 주문수량 + 주문가격 * 주문수량 * 거래수수료율 = 주문가격 * 주문수량 + (1+ 거래수수료율)
contract_price = self.position['bid_price'] * self.position['volume'] * 1.00139
# contract_price = self.position['bid_price'] * self.position['volume']
# 현재 잔액 = 현재 잔액 - 매수 체결가
self.cash -= contract_price
print('매수호가 금액: %s, 체결 금액 : %s , 주문개수 : %s, 현재 잔액 : %s' % (
self.position['bid_price'], contract_price, self.position['volume'], self.cash))
self.trades_count += 1
# 진입해있다면
else:
# time_to_sell, 팔아야하는 시점 = 다음날 9시
time_to_sell = parse(self.position['created_at'][:10]).replace(hour=9) + datetime.timedelta(days=1)
# 현재 시각
current_time = parse(yyyy_mm_dd_hour)
# 팔아야할 시각이 됐으면 매도
if time_to_sell <= current_time:
ask_price = self.daily_data.loc[yyyy_mm_dd]['Open'] # 다음날 시가에 매도
self.ror = ask_price / self.position['bid_price'] # 매도 / 매수
self.win_count = self.win_count + 1 if self.ror > 1 else self.win_count
self.accumulate_ror *= self.ror
# 매도 체결가 계산
# 주문가격 * 주문수량 - 주문가격 * 주문수량 * 거래수수료율 = 주문가격 * 주문수량 + (1-거래수수료율)
contract_price = ask_price * self.position['volume'] * (1 - 0.00139)
# 현재 잔액 = 현재 잔액 + 매도 체결가
self.cash += contract_price
# 최고 자산 계산
self.highest_cash = max(self.highest_cash, self.cash)
# 최저 자산 계산
self.lowest_cash = min(self.lowest_cash, self.cash)
# 최근 최고점(MDD 계산용)
self.last_highest_cash = max(self.last_highest_cash, self.cash)
# 최근 최저점(MDD 계산용)
self.last_lowest_cash = min(self.last_lowest_cash, self.cash)
dd = -1
# 최근 신고 갱신하면 저점 초기화
if self.last_highest_cash == self.cash:
self.last_lowest_cash = self.cash
# 신저점 갱신하면 MDD 계산
if self.last_lowest_cash == self.cash:
# Drawdown 계산, 전고점 - 저점 / 전고점
dd = (self.last_highest_cash - self.last_lowest_cash) / self.last_highest_cash * 100
# MDD 비교
self.mdd = max(self.mdd, dd)
# MDD 갱신하면
if self.mdd == dd:
self.mdd_logs[yyyy_mm_dd] = {'mdd': self.mdd, '전고점': self.last_highest_cash, '현저점': self.last_lowest_cash }
# 거래 기록, 이겼으면 profit_ratio에 기록, 졌으면 loss_ratio에 기록
self.trades_log[yyyy_mm_dd] = {'profit_ratio': (self.ror - 1) * 100 if self.ror > 1 else 0,
'loss_ratio': (self.ror - 1) * 100 if self.ror < 1 else 0}
print(
'[%s] bid price(매수 금액): %s / ask price(매도 금액): %s / 수익률: %s / 매수시각: %s / 매도시각: %s / 현재 보유 자산: %s \n' % (
self.trades_count, self.position['bid_price'], ask_price, (self.ror - 1) * 100,
self.position['created_at'], yyyy_mm_dd_hour, self.cash))
# 진입 포지션 삭제
self.position = None
self.end() # 종료
테스팅이 끝나면 거래 결과를 간략히 보여주는 end 함수를 만들어보겠습니다.
def end(self):
print('End backtest')
print('총거래횟수 : %s' % self.trades_count)
print('승리횟수 : %s, 승률 : %s' % (self.win_count, self.win_count / self.trades_count * 100))
print('ROR : %s' % self.ror)
print('ACCUM_ROR : %s , 현재 잔액 : %s' % (self.accumulate_ror, self.cash))
print('최고잔액 : %s' % self.highest_cash)
print('최저잔액 : %s' % self.lowest_cash)
print('MDD : %s' % self.mdd)
df = pd.DataFrame.from_dict(self.trades_log)
df = df.T
avg_profit = df[df['profit_ratio'] > 0]['profit_ratio'].mean()
avg_loss = df[df['loss_ratio'] < 0]['loss_ratio'].mean()
print('HPR : %s' % ((self.cash - self.start_cash) / self.start_cash * 100))
print('avg profit ratio : %s' % avg_profit)
print('avg loss ratio : %s' % avg_loss)
print('손익비 : %s' % abs(avg_profit / avg_loss))
위 함수를 클래스에 추가해주고 실행해보겠습니다.
if __name__ == '__main__':
backtest = VolatilityBackTest(df_hourly, df_daily, 1000000)
backtest.run_test()
백테스팅 기간은 2017-07-01부터 2020-02-25이며 변동성 돌파 전략의 결과는 다음과 같습니다.
296번 거래하여 승률(매도가/매수가 > 1, 매수-매도로 이익이 난 횟수 / 전체거래)은 55%이며
손익비는 2.1이나 됩니다. 먹을 때 왕창 먹고 잃을 때는 크게 잃지는 않았습니다.
결과적으로 100만원을 넣어 2년 반만에 260만원이 되었습니다.
MDD는 18프로로 2019-03-15일에 발생했고 2019-06-28일에 매도하면서 회복했습니다.
그런데 너무 큰 이익으로 회복하여 뭔가 이상하다고 생각했습니다.
(=> 2019-06-28에 수익이 너무 크게 뛰어서 데이터를 확인해보니 https://www.cryptodatadownload.com/data/bithumb/에서 얻어온 06-27일자 코인 시/고/저/종가가 다른 거래소 및 빗썸 사이트를 통해 확인한 금액과 다릅니다. 이 때문에 수익이 뻥튀기 됐습니다....)
마지막에 와서 이런 사실을 확인하게 되어 굉장히 찝찝하기 때문에 백테스팅 시작일을 조금 조정해보겠습니다.
2017년 상승장을 제외하고 2019-06에 있던 잘못된 데이터를 제외하고 짧지만 올해 2020년부터 돌려보겠습니다.
한시간봉을 계산할 때 아래 코드를 추가합니다.
df_hourly = df_hourly[(df_hourly.index > '2020-01-01')]
결과를 확인해보겠습니다.
100만원으로 2달 내내 매매 돌려서 거래세 제외하고 2만5천원 벌었습니다.
수익률이 적금 정도이긴 합니다. 시장가로 매매하는 경우에는 본전치기할 수준이긴 합니다.
하지만 이 변동성 돌파 전략 자체가 1980년대에 개발되었기 때문에 최신 전략은 아님에도 아직 나름 시장에서 사용해볼만하다고 판단하는 이유는 MDD를 줄일 수 있는 방법을 적용해보면 괜찮다고 생각하기 때문입니다.
다음 포스팅에서는 조금 더 승률을 높이거나 손익비를 높일 수 있는 장치(K 조절, 상승장 체크, 변동성 조절 등)들을 적용시켜 보겠습니다. 역시 같은 데이터를 사용해서 여전히 오차가 꽤 심할 수 있음을 감안해야하겠습니다.
같이 읽어보면 좋은 글
2022.12.27 - [파이썬/가상화폐] - [전자책] 바이낸스 코인선물자동매매 시스템 개발 방법을 담은 책이 출시되었습니다.
2022.11.05 - [파이썬/가상화폐] - [공지] 코인거래소별 프리미엄 체크봇 개발 가이드와 풀소스 전자책 | binance bybit | 업비트 김치프리미엄
댓글