Skip to content

유튜브에서 발견한 backtest 전략 추가하기 #4

사이드 프로젝트 링크 : https://www.github.com/fromitive/backtest

따라하지 마세요!

2023-07-24 Update : 해당 전략은 compare_movement로 승률을 올리고 있지만, 코드 상 compare_movement는 사실, 트레이딩 시 이미 미래의 결과를 반영했기 때문에, 수익율이 잘나온 것임을 분석하였습니다. 좀더 확실하게 투자를 할 수 있게 추 후 연구중이니, 과거의 데이터는 과거의 결과일 뿐 현실과 차이가 있으므로 절대 따라하지 마시길 부탁드립니다 🙏

코드 작성 전 고려해야 할 사항

지난시간에는 유튜브 에서 나온 전략이 아쉽게도 수익을 내지 못했다. 그 이유는 상승장에서만 통하는 전략이기 때문이다.

유튜브에서는 롱 포지션 전략을 하이킨아시(Heikin Ashi) 차트1로 변환하여 상승장 일때 이 전략을 사용하고 있다.

Image title

그림 1 - 하이킨아시 차트

유튜브자료 참조

따라서, Heikin ashi는 어떻게 양봉과, 음봉을 구별하는지 확인해야 하고, 상승장을 파악할 때 양봉이 연속적으로 나올 경우에만 거래를 하도록 기능을 추가한다.

요구사항 분석

오늘의 요구사항은 아래와 같이 작성했으며, 요구사항대로 코드를 작성한 후 실제 백테스팅 결과가 비슷하게 나오는지 확인해보겠다.

요구사항
1. StockData가 주어지면, 하이킨아시 차트에서 양봉인지 음봉인지 얻을 수 있는 함수를 만들어야 함.
2. 하이킨아시 일봉 차트를 30분 봉으로 쪼개어 적용
3. 하이킨아시 차트에서 연속적으로 양봉임을 검증하는 기능 추가.

하이킨아시 차트를 구하는법

Heikin Ashi 차트를 구하는 소스코드는 아래와 같으며, 'Movment' 열을 통해 현재 시간의 봉이 양봉인지 음봉인지 확인할 수 있다.

heikin_ashi.py
def calculate_heikin_ashi(df,open,high,low,close):
    """
    df    : 주식 DataFrame
    open  : 시가의 열 이름
    high  : 고가의 열 이름
    low   : 저가의 열 이름
    close : 종가의 열 이름
    """

    # 하이킨 아시 캔들 계산
    ha_close = (df[open] + df[high] + df[low] + df[close]) / 4
    ha_open = (df[open].shift(1) + df[close].shift(1)) / 2
    ha_high = df[[high,open,close]].max(axis=1)
    ha_low = df[[low,open,close]].min(axis=1)

    df['HA_Close'] = ha_close
    df['HA_Open'] = ha_open
    df['HA_High'] = ha_high
    df['HA_Low'] = ha_low

    # 양봉인지 음봉인지 확인
    df['Movement'] = np.where(df['HA_Close'] > df['HA_Open'], 'Up', 'Down')

양봉 음봉은 일반 캔들차트와 동일하게 종가시가보다 크면 양봉 그 반대면 음봉이다. 다만, 종가시가를 하이킨 아시 종가, 시가로 계산하여 비교한다는 점에서 차이가 있다.

이로써 요구사항 1번은 해결되었다.

  • StockData가 주어지면, 하이킨아시 차트에서 양봉인지 음봉인지 얻을 수 있는 함수를 만들어야 함.
  • 하이킨아시 일봉 차트를 30분 봉으로 쪼개어 적용
  • 하이킨아시 차트에서 연속적으로 양봉임을 검증하는 기능 추가.

하이킨아시 차트를 일봉으로 봐야 하는 이유

일봉으로 봐야 하는 이유는, 하루 상승장은 수익을 낼 확률이 높다고 생각하기 때문이다.

그리고, 연속적으로 상승장을 향하고 있는 추세를 발견하면, 오늘의 거래도 상승장일 확률이 높아진다. 즉, 관성을 따른다.

따라서 30분봉 차트로 백테스팅 자료를 만들 때 같은 종목의 하이킨아시 일봉 차트를 가져와서, 30분 봉에 대입해야 한다.

어떻게 1일 봉을 30분 봉으로 쪼갤 수 있을까? pandas는 이미 그런 기능을 가지고 있어서 아주 간단하게 해결된다.

코드는 아래와 같다.

day_2_minute.py
1
2
3
    day_df.index = pd.to_datetime(day_df.index)
    day_df = day_df.resample('T').fillna(method='bfill')  # 뒤의 NaN값을 현재 값으로 채운다. 
    day_df = day_df.reindex(minute_df.index, method='ffill') # 앞의 NaN값을 현재 값으로 채운다. 결국엔 모든 값이 NaN이 아니도록 된다.

resample을 하게 되면, day_df의 일단위 index가 1분단위로 확장되고, 이를 minute_df의 index를 세팅하면, 일단위 데이터를 30분봉 데이터에 대입하여 사용할 수 있게 된다.

이를 이용해 day_df에는 하이킨 아시 1일봉 차트, minute_df에는 우리가 백테스팅 돌릴 30분봉 차트를 대입해주자.

이로써 요구사항 2번도 해결되었다.

  • StockData가 주어지면, 하이킨아시 차트에서 양봉인지 음봉인지 얻을 수 있는 함수를 만들어야 함.
  • 하이킨아시 일봉 차트를 30분 봉으로 쪼개어 적용
  • 하이킨아시 차트에서 연속적으로 양봉임을 검증하는 기능 추가.

연속적으로 양봉임을 확인하는 방법

오류 수정

수정 날짜 : 2023-07-14 수정 이유 : 1일 차트로 N일 연속 양봉인지 확인할 때, 쪼개어진 하이킨아시 차트를 대상으로 하는게 아님

30분 봉으로 하이킨아시 차트를 쪼개었다면 매수 신호를 만들기 전에 연속적으로 양봉인지 확인해야 한다.

이럴 땐, 이전 값을 앞으로 당길 수 있는 .shift()를 이용하면 된다.

n일 연속 양봉임을 확인하기 위해 여러개의 shift 열을 추가하는 반복문을 추가한다.

양봉인지 음봉인지 확인하는 열은 Movement로 이를 대상으로 추가한다.

1일 봉으로 만들어진 하이킨 아시 차트에서 N일 연속 양봉을 검증한 후 그 결과값을 tradable 열에 저장하려면 아래와 같은 작업이 필요하다.

일단 N일 까지의 열을 .shift() 함수를 통해 현재 날짜까지 앞당긴 후, 해당 결과값이 전부 Up 인지 확인하면 된다.

continuos_up_detect.py
for i in range(continue_day):
    heikin_ashi_df['Movement_{}'.format(i)] = heikin_ashi_df.shift(i)

def _is_tradeable(r:pd.Series):
    for i in range(continue_day):
        if r['Movement_{}'.format(i)] == 'Down':
            return False
    return True

heikin_ash_df['tradable'] = heikinn_ash_df.apply(lambda r: _is_tradeable(r), axis=1)

이로서 마지막의 요구사항도 깔끔하게 적용할 수 있었다.

  • StockData가 주어지면, 하이킨아시 차트에서 양봉인지 음봉인지 얻을 수 있는 함수를 만들어야 함.
  • 하이킨아시 일봉 차트를 30분 봉으로 쪼개어 적용
  • 하이킨아시 차트에서 연속적으로 양봉임을 검증하는 기능 추가.

코드 구현 및 검증

코드 구현

유튜브 전략의 최종 함수는 아래와 같으며, 하이라이트 된 부분이 위의 요구사항 을 반영한 것이다.

stocastic_rsi_ema_mix_function_final.py
def stocastic_rsi_ema_mix_function(data: StockData, weight: int, name: str, timeperiod: int = 200, rsi_period: int = 14, fastk_period=3, fastd_period=3, fastd_matype=0,
                                buy_rate: float = 25.0, sell_profit: float = 0.02, sell_lose: float = -0.015, heikin_ashi: dict = {}, compare_movement: int = 3):
    heikin_ashi_df = None
    if data.symbol in heikin_ashi.keys():
        heikin_ashi_stockdata = heikin_ashi[data.symbol]
        heikin_ashi_df = heikin_ashi_stockdata.data

    for i in range(compare_movement):
        heikin_ashi_df['Movement_{}'.format(i)] = heikin_ashi_df['Movement'].shift(i)

    def _is_tradeable(r: pd.Series):
        for i in range(compare_movement):
            if r['Movement_{}'.format(i)] == 'Down':
                return False
        return True

    # resample heikin_ashi_df
    if heikin_ashi_stockdata.unit == 'D' and data.unit == 'M':
        heikin_ashi_df.index = pd.to_datetime(heikin_ashi_df.index)
        heikin_ashi_df = heikin_ashi_df.resample('T').fillna(method='bfill')
        heikin_ashi_df = heikin_ashi_df.reindex(data.data.index, method='ffill')

    df = data.data
    df['tradeable'] = heikin_ashi_df['tradeable']

    # ...(중략)...

    def _buy_signal(r: pd.Series):
        if r.tradeable:
            if r.close > r.ema:  # buy condition 1
                # buy condition 2
                if r.fastk < buy_rate and r.fastd < buy_rate:
                    # buy_condition 3
                    if r.before_fastk < r.fastk and r.before_fastd < r.fastd:
                        if r.fastk > r.fastd:
                            return (StrategyResultColumnType.BUY, weight)
        return (StrategyResultColumnType.KEEP, 0)

    df['result'] = df.apply(lambda r: _buy_signal(r), axis=1)

    # ...(생략)...

백테스팅 결과

vectorbt를 이용해 어제와 같은 데이터로 백테스팅을 확인한 결과, 아래와 같이 수익을 벌어들였다. 승률이 50퍼센트라도, 한번 이기면 손절하는 것 보다 더욱 수익을 벌어서 결국엔 이득이다.

백테스팅 결과
Start                         2023-06-19 09:00:00
End                           2023-07-13 18:00:00
Period                                       1164
Start Value                                 100.0
End Value                              102.866105
Total Return [%]                         2.866105
Benchmark Return [%]                     8.012395
Max Gross Exposure [%]                      100.0
Total Fees Paid                               0.0
Max Drawdown [%]                         6.122818
Max Drawdown Duration                       354.0
Total Trades                                   10
Total Closed Trades                            10
Total Open Trades                               0
Open Trade PnL                                0.0
Win Rate [%]                                 50.0
Best Trade [%]                           2.734218
Worst Trade [%]                         -1.610954
Avg Winning Trade [%]                    1.856504
Avg Losing Trade [%]                    -1.261333
Avg Winning Trade Duration                   29.0
Avg Losing Trade Duration                    20.8
Profit Factor                            1.456966
Expectancy                               0.286611
dtype: object

그래프를 확인하면, 하락장에선 매수를 하지 않도록 제어를 하고 있는 것으로 보인다.

Image title

그림 2 - backtest 시각화 결과

다음은?

왜 100만 조회수가 넘은 유튜브인지 알겠다. 승률이 높고, 지속적으로 수익을 가져다 주기 때문일 거라 생각한다.

다음은, 실제 거래소 시스템과 연동하는 방법을 생각하여 실제로 이 전략을가지고 투자를 해볼 예정이다.



  1. 하이킨아시(Heikin Ashi) 차트는 양봉과 음봉을 시가, 고가, 저가, 종가의 평균값들을 활용하여 만들어지는 차트이다. 

Comments