Sysmetic trading

[트레이딩] Optuna로 변동성 돌파 전략 Foward walk testing 구현하기

quantoasis 2023. 5. 22. 16:06
반응형

 
안녕하세요.
 
최근 많은 블로거들이 백테스트 결과와 전략을 공유하고 있어 많은 시스템 트레이더 분들이 관심을 갖고 계실 것입니다. 그중에서도 백석꾼님의 '직장인 전략'과 퀀스택스님의 '슈퍼 ETF 전략'은 특히 유명한 전략 중 하나입니다.
 
 

 

ETF 시스템 트레이딩 연구소 : 네이버 블로그

ETF 트레이딩 연구 결과 기록을 위한 블로그입니다. 공지의 네비게이션 페이지를 이용하면 보다 쉽게 연구결과를 찾을 수 있습니다. since 2022-08-08

blog.naver.com

 

 

Get the money : 네이버 블로그

Bill Staxx through Quant Quant + Bill Staxx = Quanstaxx

blog.naver.com

 
오늘은 이러한 ETF 전략의 효과를 극대화하기 위해 활용할 수 있는 Walk Forward Test에 대해 이야기해보려고 합니다.  Walk Forward Test는 백테스트에서 전략을 더욱 견고하고 신뢰성 있게 만들기 위한 방법입니다.
 
 
Walk Forward Test는 어떻게 이루어지는지 간단히 설명드리겠습니다. 먼저, 전체 기간을 일정한 구간으로 나눕니다. 예를 들어, 3년의 데이터가 있다면 1년씩 구간을 나눌 수 있습니다. 그런 다음, 첫 번째 구간에서는 백테스트를 진행하여 최적의 파라미터를 찾습니다. 그 다음, 두 번째 구간에서는 이전 구간에서 찾은 파라미터를 그대로 적용하여 백테스트를 수행합니다. 이를 계속해서 구간을 이동하며 백테스트를 진행합니다.
 

https://www.amibroker.com/guide/h_walkforward.html



Walk Forward Test를 통해 변수를 동적으로 변경하는 이유는 시장의 동태, 즉 시장 상황이 변화하기 때문입니다. 시장은 우리가 생각하는 것처럼 정적이지 않습니다. 따라서 전략도 변화에 적응하고 유연하게 대응해야 합니다. Walk Forward Test를 통해 전략의 파라미터를 동적으로 조정하면서 시장 상황에 더 잘 적응할 수 있습니다.

예를 들어, 변동성 돌파 전략에서 k 값을 최적화하는 경우를 생각해봅시다. Walk Forward Test를 활용하면 각 구간에서 최적의 k 값을 찾을 수 있습니다. 그리고 이렇게 찾은 최적의 k 값을 다음 구간에서 적용하여 계속해서 테스트를 진행합니다. 이렇게 하면 시장 상황에 따라 최적의 k 값을 유지하면서 전략을 평가할 수 있습니다.
 
아래는 변동성돌파에 Walk Foward Test를 적용한 코드입니다
 
 

import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
import optuna
import quantstats as qs
# 종목 코드와 기간 설정
symbol = '233740.KS'
start = '2016-08-01'
end = '2023-05-19'

fulldata = yf.download(symbol, start=start, end=end)

우선 데이터를 받아왔구요,

def backtest(df, params):
    # 초기 자본금 설정
    initial_capital = 10000000

    final_capital = initial_capital

    price_flag = False
    price_buy = 0
    price_sell = 0

    # 전일 고가, 저가 중간값 계산
    df['prev_high_low_avg'] = df['High'].shift(1) - df['Low'].shift(1)

    # 매수/매도 조건 설정
    buy_condition = df['High'] > df['Open'] + params['rng'] * df['prev_high_low_avg']
    sell_condition = True  # 다음날 시가에 판매하기 때문에 항상 참으로 설정

    # 매수/매도 시점 설정
    for i in range(1, len(df) - 1):  # 마지막 인덱스 이전까지만 반복
        if price_flag == False and buy_condition[i]:
            price_flag = True
            price_buy = df['Open'][i] + params['rng'] * df['prev_high_low_avg'][i]

        elif price_flag == True and sell_condition:
            # 매도 조건은 항상 참이며, 다음날 시가에 매도
            price_sell = df['Open'][i + 1]
            profit = (price_sell - price_buy) * 100 / price_buy - 0.03

            profit_money = initial_capital * profit / 100
            final_capital += profit_money

            price_flag = False

    # 남은 자산 계산
    profit = final_capital - initial_capital

    # 수익률 계산
    roi = profit / initial_capital * 100

    return roi


def objective(trial):
    # Hyperparameter 범위 설정
    rng = trial.suggest_float('rng', 0.1, 0.9,step=0.01)
    
    params = {'rng': rng
             }

    # 전략 수익률 계산
    roi = backtest(df, params)

    # 목적 함수 값 계산 (수익률 최대화)
    return roi

Optuna와 backtest 함수를 만들어서 최적 k를 구하도록 했습니다.
 


def get_dailyreturn(df,params,start_date,end_date):
    initial_capital = 10000000

    final_capital = initial_capital
    df['return'] = 0
    price_flag = False
    price_buy = 0
    price_sell = 0

    # 전일 고가, 저가 중간값 계산
    df['prev_high_low_avg'] = df['High'].shift(1) - df['Low'].shift(1)

    # 매수/매도 조건 설정
    buy_condition = df['High'] > df['Open'] + params['rng'] * df['prev_high_low_avg']
    sell_condition = True  # 다음날 시가에 판매하기 때문에 항상 참으로 설정
    daily_returns = [0] * len(df)  # Initialize with zeros
    
    # 매수/매도 시점 설정
    for i in range(1, len(df) - 1):  # 마지막 인덱스 이전까지만 반복
        if price_flag == False and buy_condition[i]:
            price_flag = True
            price_buy = df['Open'][i] + params['rng'] * df['prev_high_low_avg'][i]

        elif price_flag == True and sell_condition:
            # 매도 조건은 항상 참이며, 다음날 시가에 매도
            price_sell = df['Open'][i + 1]
            profit = (price_sell - price_buy) * 100 / price_buy - 0.03

            profit_money = initial_capital * profit / 100
            final_capital += profit_money
            daily_return = profit_money / initial_capital
            df['return'][i]=daily_return
            price_flag = False

    # 남은 자산 계산
    profit = final_capital - initial_capital
    mask = (df.index >= start_date) & (df.index <= end_date)
    df = df.loc[mask]
    
    return df['return']

최적화된 파라미터로 한달 뒤의 매매 결과를 일별수익률로 리턴하는 함수입니다.
 

# 1년동안의 데이터로 최적화하고 그 다음 달에 매매를 진행하는 Rolling backtest
backtest_result = [] # 일별 수익률을 저장할 리스트

start_date = pd.to_datetime(start)

set_lookbackdate =pd.DateOffset(months=12)
start_date =start_date+set_lookbackdate
end_date = pd.to_datetime(end)


while start_date < end_date:
    print('start_date : ',start_date)
    start_test_date=start_date - set_lookbackdate
    end_test_date = start_date - pd.DateOffset(days=1)
    
    
    df = fulldata.loc[slice(start_test_date,end_test_date)]

    
    # Optuna를 이용한 하이퍼파라미터 최적화
    study = optuna.create_study(direction='maximize')
    study.optimize(objective, n_trials=10)

    print(f'최적 값: {study.best_value:.2f}%')
    print(f'최적 하이퍼파라미터: {study.best_params}') 

    # 최적화된 하이퍼파라미터로 다음 달 매매 진행
    
    start_trade_date=end_test_date + pd.DateOffset(days=1)
    end_trade_date = end_test_date + pd.DateOffset(months=1,days=-1)
    
    
    print('test ' ,start_test_date,end_test_date)
    print('trade' ,start_trade_date,end_trade_date)
    # daily_mean_earnings를 저장
    
    backtest_result.append(get_dailyreturn(fulldata, study.best_params,start_trade_date,end_trade_date))
    
    # 다음 1년 후로 날짜 업데이트
    start_date += pd.DateOffset(months=1)
    

    
combined_series = pd.concat(backtest_result)
qs.reports.plots(combined_series,mode='full')

이 코드가 위에 함수들을 동작하여 Walk Foward테스트를 하는 코드입니다. 
 
작동 과정은 다음과 같습니다:

먼저, lookbackdate를 설정하여 과거 1년의 데이터를 가져옵니다. 이를 통해 start_test_date와 end_test_date를 설정합니다.

그 다음, Optuna를 이용하여 파라미터 k를 최적화합니다. Optuna는 주어진 목적 함수(objective)를 최대화하는 최적의 파라미터를 찾는 데 사용됩니다. n_trials은 목적 함수를 평가하는 횟수로, 10번으로 설정되어 있습니다.

최적화된 k 값을 다음 달에 적용하여 수익률을 계산하고, 이를 backtest_result에 쌓습니다. 수익률은 get_dailyreturn 함수를 통해 계산되며, 해당 함수에는 fulldata, 최적화된 파라미터 k, 그리고 start_trade_date와 end_trade_date이 전달됩니다.

이와 같은 구조를 가진 코드를 통해 Walk Forward 테스트를 수행하면, 과거 1년의 데이터로 최적화된 파라미터를 찾고, 다음 달에 해당 파라미터를 적용하여 수익률을 계산하며, 이를 backtest_result에 쌓게 됩니다. 이를 반복하면서 전체 매매 기간에 대한 수익률을 평가할 수 있습니다.
 
아래는 테스트 결과입니다.

 
백테스트 결과에 실망하셨나요 ^^
 
Walk Foward test가 꼭 좋은 성과를 담보하는 것은 아닙니다.

 

그래도 다양한 Test를 통해 과거 성과를 검증하고, 변화하는 시장 상황에 적응할 수 있는 전략을 구축하는 것이 중요하겠죠? ㅎㅎ
 
 
여러분도 Walk Forward Test를 통해 ETF 전략의 성과를 향상시키고, 안정적인 투자를 이루어보시길 바랍니다. 
 
감사합니다.
 
 
 

반응형