CH08 ML4T 작업 흐름: 모델에서 전략 백테스트까지¶
목표: ML 알고리듬에 의해 구동되는 트레이딩 전략을 설계, 시뮬레이션, 평가하는 프로세스에 대한 엔드투엔드 관점을 제시하는 것
ML4T 워크플로의 목표: 과거 데이터에서 증거 수집
- 다양한 데이터 세트 소스로 작업해 정보력 있는 팩터를 창출
- 트레이딩 전략의 정보력을 향상시키는 예측 신호를 생성하는 머신러닝 모델 설계
- 리스크 수익률 관점에서 최종 포트폴리오 최적화
8장에서 다루는 내용
- 엔드투엔드 전략 백테스트 계획과 구현
- 백테스트 구현 시 중요한 함정의 이해와 방지
- 벡터화된 백테스트 엔진과 이벤트 기반 백테스트 엔진의 장점과 단점에 대한 설명
- 이벤트 기반 백테스터의 주요 구성 요소 식별과 평가
- 별도로 훈련하거나 백테스트의 일환으로 훈련된 ML 모델을 이용해 분당 및 일일 빈도의 데이터 소스를 사용해 ML4T 워크플로 설계와 실행
- 집라인과 백트레이더 사용
ML 기반 전략의 백테스트 방법¶
- ML4T 워크플로: 머신러닝을 활용해 트레이딩 신호를 생성하고 포지션을 선택하고 크기를 조정하거나 거래 집행을 최적화하는 트레이딩 전략을 백테스트하는 것
- 시장, 기본적 및 대체 데이터 출처와 준비
- 예측 알파 팩터와 특성 공학
- ML 모델을 설계, 튜닝, 평가해 트레이딩 신호 생성
- 규칙을 적용해 이러한 신호에 따라 거래 결정
- 포트폴리오 맥락에서 개별 포지션의 크기 조정
- 과거 시장 데이터를 사용해 발생한 트레이딩 시뮬레이션
- 최종 포지션이 어떻게 수행됐는지 평가
- 전략의 (상대적) 백테스트 성과는 미래 시장 성과를 나타내야 한다.
백테스트의 단점과 이를 회피하는 방법¶
백테스트는 새로운 시장 상황을 일반화하는 성과 결과를 산출하고자 과거 데이터를 기반으로 알고리듬 전략을 시뮬레이션한다.
- 변화하는 시자으이 맥락에서 예측에 대한 일반적인 불확실성 외에도 몇 가지 구현 측면은 결과를 편향시킬 수 잇으며, 표본 내 성과를 표본 외에서 성립하는 패턴으로 오인할 리스크를 증가시킬 수 있다.
잘못된 백테스트 발견의 리스크는 증가하는 계산 능력, 더 큰 데이터 세트, 잡음이 많은 표본에서 명백한 신호의 오식별을 용이하게 하는 더 복잡한 알고리듬과 함께 증가한다.
축소 샤프 비율: 동일한 금융 데이터 세트를 사용할 때 반복된 시행으로 인해 발생하는 척도를 조정하는 방법을 보여줌
데이터를 올바르게 얻기¶
- 선견 편향: 현재 시점의 데이터만을 사용
- 과거 정보를 사용해 트레이딩 규칙을 개발하거나 평가할 때 나타난다.
- 원인: 최초 발표 후 보고된 재무제표의 수정이나 재작성을 설명하지 못한다.
- 해결책: 백테스트에 들어가는 모든 데이터의 타임스탬프를 신중하게 검증하는 것이다.
- 생존 편향: 과거 유니버스의 추적
- 백테스트 데이터에 시간이 지남에 따라 사라진 자산(예. 파산, 상장 폐지 또는 인수)을 생략하고 현재 활성 상태인 증권만 포함되어 있을 때 발생한다.
- 원인: 더 이상 투자 유니버스에 속하지 않는 증권들은 종종 좋은 성과를 거두지 못했으며, 이러한 사례들을 포함시키지 않으면 백테스트 결과가 긍정적으로 왜곡된다.
- 해결책: 테스트를 실행할 때 여전히 사용할 수 있는 증권만 포함하는 것이 아니라 당연히 데이터 세트가 시간이 지남에 따라 사용 가능한 모든 증권을 포함하는지 확인하는 것이다.
- 특이값 제어: 현실적인 극단치를 제거하지 말자
- 데이터 준비에는 일반적으로 원저화나 클리핑과 같은 특이값 처리가 포함되어 나타난다.
- 원인: 그 당시 시장 환경의 필수 요소인 극단치와 달리 분석 대상 기간을 진정을 대표하지 않는 특이값을 식별하는 것이다.
- 해결책: 극단치 발생 확률을 기반으로 특이값을 주의 깊게 분석하고 이러한 현실에 맞게 전략 파라미터를 조정하는 것이다.
- 표본 기간: 관련된 미래 시나리오를 대표하게 하자
- 표본 데이터가 현재(그리고 미래의 가능성이 있는) 환경응 반영하지 않는 경우 백테스트는 미래로 일반화하는 대표적인 결과를 산출하지 않는다.
- 원인: 잘못 선택된 표본 데이터는 변동성과 거래량 측면에서 관련 시장 국면이 부족하거나 데이터 포인트가 충분히 포함되지 않거나, 또는 극단적인 과거 사건들이 너무 많게 또는 적게 포함할 수 있다.
- 해결책: 중요한 시장 현상을 포함하는 표본 기간을 사용하거나 관련 시장의 특성을 반영하는 합성 데이터를 생성하는 것이다.
시뮬레이션을 올바르게 수행¶
과거 시뮬레이션의 구현과 관련된 실제 문제는 다음과 같다.
- 시가 평가 성과: 시간에 걸친 리스크 추적
- 시장 가격과 계정 또는 인출을 정확하게 반영하는 시가 평가의 실패
- 해결책: 시간 경과에 따른 성과를 그러가나 VaR(Value at Risk) 또는 소리티노 비율과 같은 (롤링) 리스크 척도를 계산하는 작업이 연관된다.
- 거래 비용: 현실적인 트레이딩 환경 가정
- 거래의 가용성, 비용이나 시장 충격에 대한 비현실적인 가정
- 해결책: 유동성 유니버스에 대한 제한이나 트레이딩 및 슬리피지 비용에 대한 현실적인 파라미터 가정이 포함하는 것이다.
- 의사결정의 타이밍: 적절한 신호와 거래 시퀀스
- 신호와 거래 집행의 부정확한 타이밍
- 해결책: 신호 도착, 거래 실행, 성과 평가의 순서를 신중하게 조정하는 것이다.
통계량을 올바르게 얻기¶
- 동일한 데이터에 대해 서로 다른 후보의 테스트를 기반으로 전략을 선택하는 것을 선택을 편향하게 된다.
- 이 전략은 테스트 표본을 과대적합하며, 실제 거래 중에 발생하는 미래 데이터에 일반화될 가능성이 거의 없는 매우 긍정적인 결과를 산출한다.
- 백테스트 기간의 최소 길이와 축소 샤프 비율
축소 SR을 도출해 다중 테스트, 비정규 분호 수익률, 짧은 표본 길이의 증복 효과를 제어하면서 SR이 통계적으로 유의할 확률을 계산했다.
#!/usr/bin/env python # On 20140607 by lopezdeprado@lbl.gov from itertools import product import numpy as np import pandas as pd import scipy.stats as ss def get_analytical_max_sr(mu, sigma, num_trials): """Compute the expected maximum Sharpe ratio (Analytically)""" # Euler-Mascheroni constant emc = 0.5772156649 maxZ = (1 - emc) * ss.norm.ppf(1 - 1. / num_trials) + emc * ss.norm.ppf(1 - 1 / (num_trials * np.e)) return mu + sigma * maxZ def get_numerical_max_sr(mu, sigma, num_trials, n_iter): """Compute the expected maximum Sharpe ratio (Numerically)""" max_sr, count = [], 0 while count < n_iter: count += 1 series = np.random.normal(mu, sigma, num_trials) max_sr.append(max(series)) return np.mean(max_sr), np.std(max_sr) def simulate(mu, sigma, num_trials, n_iter): """Get analytical and numerical solutions""" expected_max_sr = get_analytical_max_sr(mu, sigma, num_trials) mean_max_sr, stdmean_max_sr = get_numerical_max_sr(mu, sigma, num_trials, n_iter) return expected_max_sr, mean_max_sr, stdmean_max_sr def main(): n_iter, sigma, output, count = 1e4, 1, [], 0 for i, prod_ in enumerate(product(np.linspace(-100, 100, 101), range(10, 1001, 10)), 1): if i % 1000 == 0: print(i, end=' ', flush=True) mu, num_trials = prod_[0], prod_[1] expected_max_sr, mean_max_sr, std_max_sr = simulate(mu, sigma, num_trials, n_iter) err = expected_max_sr - mean_max_sr output.append([mu, sigma, num_trials, n_iter, expected_max_sr, mean_max_sr, std_max_sr, err]) output = pd.DataFrame(output, columns=['mu', 'sigma', 'num_trials', 'n_iter', 'expected_max_sr', 'mean_max_sr', 'std_max_sr', 'err']) print(output.info()) output.to_csv('DSR.csv') # df = pd.read_csv('DSR.csv') # print(df.info()) # print(df.head()) if __name__ == '__main__': main()
- 백테스트의 최적 종료
- 최적 정지이론으로부터 비서 문제에 대한 해에 의존한다.
- 합리적인 전략의 1/e(대략 37%)의 무작위 표본을 테스트하고 성능을 기록한다.
- 전략이 이전에 테스트한 것보다 성능이 우수할 때까지 테스트를 계속한다.
백테스트 엔진 작동법¶
백테스트 엔진은 과거 가격(및 기타 데이터)에 대해 반복 실행하면서 현재 값을 알고리듬에 전달하며, 그 대가로 주문을 받고 결과 포지션들과 이들의 값을 추적한다.
백터화 대 이벤트 기반 백테스트¶
백터화 백테스트: 목표 포지션 크기를 나타내는 신호 벡터에 투자 기간의 수익률 벡터를 곱해 기간 성과를 계산한다.
02_vectorized_backtest.ipynb
Vectorized Backtest¶
import warnings
warnings.filterwarnings('ignore')
from pathlib import Path
from time import time
import datetime
import numpy as np
import pandas as pd
import pandas_datareader.data as web
from scipy.stats import spearmanr
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
import seaborn as sns
sns.set_style('whitegrid')
np.random.seed(42)
Load Data¶
Return Predictions¶
from google.colab import drive
drive.mount('/content/drive') # 구글 드라이브를 사용하는 경우
Mounted at /content/drive
!unzip /content/drive/MyDrive/스터디/금융공학_퀀트_스터디/data/WIKI_PRICES.zip
!mv WIKI_PRICES_212b326a081eacca455e13140d7bb9db.csv wiki_prices.csv
Archive: /content/drive/MyDrive/스터디/금융공학_퀀트_스터디/data/WIKI_PRICES.zip inflating: WIKI_PRICES_212b326a081eacca455e13140d7bb9db.csv
DATA_STORE = 'assets.h5'
df = (pd.read_csv('wiki_prices.csv',
parse_dates=['date'],
index_col=['date', 'ticker'],
infer_datetime_format=True)
.sort_index())
print(df.info(null_counts=True))
with pd.HDFStore(DATA_STORE) as store:
store.put('quandl/wiki/prices', df)
<class 'pandas.core.frame.DataFrame'> MultiIndex: 15389314 entries, (Timestamp('1962-01-02 00:00:00'), 'ARNC') to (Timestamp('2018-03-27 00:00:00'), 'ZUMZ') Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 open 15388776 non-null float64 1 high 15389259 non-null float64 2 low 15389259 non-null float64 3 close 15389313 non-null float64 4 volume 15389314 non-null float64 5 ex-dividend 15389314 non-null float64 6 split_ratio 15389313 non-null float64 7 adj_open 15388776 non-null float64 8 adj_high 15389259 non-null float64 9 adj_low 15389259 non-null float64 10 adj_close 15389313 non-null float64 11 adj_volume 15389314 non-null float64 dtypes: float64(12) memory usage: 1.4+ GB None
!cp /content/drive/MyDrive/스터디/금융공학_퀀트_스터디/07_linear_models/data.h5 .
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = 'Stefan Jansen'
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import spearmanr
pd.set_option('display.expand_frame_repr', False)
np.random.seed(42)
DATA_DIR = Path('.', '.')
def get_backtest_data(predictions='lasso/predictions'):
"""Combine chapter 7 lr/lasso/ridge regression predictions
with adjusted OHLCV Quandl Wiki data"""
with pd.HDFStore(DATA_DIR / 'assets.h5') as store:
prices = (store['quandl/wiki/prices']
.filter(like='adj')
.rename(columns=lambda x: x.replace('adj_', ''))
.swaplevel(axis=0))
with pd.HDFStore(DATA_DIR / 'data.h5') as store:
print(store.info())
predictions = store[predictions]
best_alpha = predictions.groupby('alpha').apply(lambda x: spearmanr(x.actuals, x.predicted)[0]).idxmax()
predictions = predictions[predictions.alpha == best_alpha]
predictions.index.names = ['ticker', 'date']
tickers = predictions.index.get_level_values('ticker').unique()
start = predictions.index.get_level_values('date').min().strftime('%Y-%m-%d')
stop = (predictions.index.get_level_values('date').max() + pd.DateOffset(1)).strftime('%Y-%m-%d')
idx = pd.IndexSlice
prices = prices.sort_index().loc[idx[tickers, start:stop], :]
predictions = predictions.loc[predictions.alpha == best_alpha, ['predicted']]
return predictions.join(prices, how='right')
df = get_backtest_data('lasso/predictions')
print(df.info())
df.to_hdf('backtest.h5', 'data')
<class 'pandas.io.pytables.HDFStore'> File path: data.h5 /lasso/coeffs frame (shape->[8,33]) /lasso/predictions frame (shape->[592432,3]) /lasso/scores frame (shape->[6000,3]) /lr/predictions frame (shape->[74054,2]) /lr/scores frame (shape->[750,2]) /model_data frame (shape->[2904233,69]) /ridge/coeffs frame (shape->[18,33]) /ridge/predictions frame (shape->[1332972,3]) /ridge/scores frame (shape->[13500,3]) <class 'pandas.core.frame.DataFrame'> MultiIndex: 190451 entries, ('AAPL', Timestamp('2014-12-09 00:00:00')) to ('LNG', Timestamp('2017-11-30 00:00:00')) Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 predicted 74054 non-null float64 1 open 190451 non-null float64 2 high 190451 non-null float64 3 low 190451 non-null float64 4 close 190451 non-null float64 5 volume 190451 non-null float64 dtypes: float64(6) memory usage: 14.2+ MB None
DATA_DIR = Path('.', '.')
data = pd.read_hdf('backtest.h5', 'data')
data.info()
<class 'pandas.core.frame.DataFrame'> MultiIndex: 190451 entries, ('AAPL', Timestamp('2014-12-09 00:00:00')) to ('LNG', Timestamp('2017-11-30 00:00:00')) Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 predicted 74054 non-null float64 1 open 190451 non-null float64 2 high 190451 non-null float64 3 low 190451 non-null float64 4 close 190451 non-null float64 5 volume 190451 non-null float64 dtypes: float64(6) memory usage: 10.2+ MB
SP500 Benchmark¶
sp500 = web.DataReader('SP500', 'fred', '2014', '2018').pct_change()
sp500.info()
<class 'pandas.core.frame.DataFrame'> DatetimeIndex: 1044 entries, 2014-01-01 to 2018-01-01 Data columns (total 1 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 SP500 1042 non-null float64 dtypes: float64(1) memory usage: 16.3 KB
Compute Forward Returns¶
daily_returns = data.open.unstack('ticker').sort_index().pct_change()
daily_returns.info()
<class 'pandas.core.frame.DataFrame'> DatetimeIndex: 751 entries, 2014-12-09 to 2017-11-30 Columns: 257 entries, AAPL to LNG dtypes: float64(257) memory usage: 1.5 MB
fwd_returns = daily_returns.shift(-1)
Generate Signals¶
predictions = data.predicted.unstack('ticker')
predictions.info()
<class 'pandas.core.frame.DataFrame'> DatetimeIndex: 751 entries, 2014-12-09 to 2017-04-14 Columns: 257 entries, AAPL to LNG dtypes: float64(257) memory usage: 1.5 MB
N_LONG = N_SHORT = 15
long_signals = ((predictions
.where(predictions > 0)
.rank(axis=1, ascending=False) > N_LONG)
.astype(int))
short_signals = ((predictions
.where(predictions < 0)
.rank(axis=1) > N_SHORT)
.astype(int))
Compute Portfolio Returns¶
long_returns = long_signals.mul(fwd_returns).mean(axis=1)
short_returns = short_signals.mul(-fwd_returns).mean(axis=1)
strategy = long_returns.add(short_returns).to_frame('Strategy')
Plot results¶
fig, axes = plt.subplots(ncols=2, figsize=(14,5))
strategy.join(sp500).add(1).cumprod().sub(1).plot(ax=axes[0], title='Cumulative Return')
sns.distplot(strategy.dropna(), ax=axes[1], hist=False, label='Strategy')
sns.distplot(sp500, ax=axes[1], hist=False, label='SP500')
axes[1].set_title('Daily Standard Deviation')
axes[0].yaxis.set_major_formatter(FuncFormatter(lambda y, _: '{:.0%}'.format(y)))
axes[1].xaxis.set_major_formatter(FuncFormatter(lambda y, _: '{:.0%}'.format(y)))
sns.despine()
fig.tight_layout();
res = strategy.join(sp500).dropna()
res.std()
Strategy 0.001979 SP500 0.007923 dtype: float64
res.corr()
Strategy | SP500 | |
---|---|---|
Strategy | 1.000000 | -0.101723 |
SP500 | -0.101723 | 1.000000 |
백터화 기반 백테스트는 신속한 back-of-the-envelope 평가를 허용하지만 강력하고 현실적이며 사용자 친화적인 백테스트 엔진의 중요한 특성을 놓친다.
- 예측과 수익률의 타임스탬프를 수동으로 정렬해야하며, 의도치 않은 선견 편향에 대한 안전장치가 없다.
- 명시적인 포지션 조정도 없고 비용 및 기타 시장 현실을 설명하는 트레이딩 프로세스나 포지션 및 성과를 추적하는 회계 시스템의 명시적인 표현도 없다.
- 또한 사후적으로 계산하는 것 이외의 다른 성과 측정은 없으며, 손절매와 같은 리스크 관리 규칙을 시뮬레이션하기 어렵다.
이벤트 기반 벡티스트 엔진
- 트레이딩의 시간 차원을 명시적으로 시뮬레이션하고 시뮬레이션에 훨씬 많은 구조를 부과한다.
- 타임스탬프를 적용하여 선견 평향과 기타 구현 오류를 방지하는 데 도움이 된다.
주요 구현 측면¶
현실적인 시뮬레이션 요건은 엔드투엔드 방식으로 프로세스의 모든 스텝을 지원하는 단일 플랫폼이나 각각 다른 측면에 전문화된 여러 도구를 통해 충족될 수 있다.
프로세스를 실행하기 위해 해결해야할 주요 항목과 구현 세부 정보는 다음과 같다.
데이터 인제스트: 형태, 빈도, 타이밍¶
- 형태
- 얼마나 많은 다양한 저장형식과 데이터 유형 지원할 것인지
- 독점적 또는 사용자 지정 형식을 사용할 것인지
- 제3자 또는 오픈소스 형식에 의존할 것인지
- 빈도
- 데이터 소스의 빈도와 상이한 빈도의 소스를 결합할 수 있는지
- 타이밍
- 데이터 인제스트는 선견 편향을 피하고자 시점 제약 조건도 해결해야 한다.
팩터 공학: 내장 팩터 대 라이브러리¶
내장 팩터 공학의 주요 이점은 백테스트 파이프라인을 입력 데이터와 동일한 계산을 적용하는 실시간 트레이딩 엔진으로 쉽게 변환한다는 것이다.
수치 파이썬 라이브러리는 팩터를 미리 계산할 수 있는 대안이다.
ML 모델, 예측, 신호¶
- 모델 설계와 평가 부분을 백테스트 프로세스에 통합하는 엔드투엔트 플랫폼에 포함될 수 있다.
트레이딩 규칙과 실행¶
- 현실적인 전략 시뮬레이션은 거래 환경을 충실히 표현해야한다.
성과 평가¶
- 거래 계정에서 도출된 표준 성과 척도 또는 이러한 목적에 적합화된 파이폴리오와 같은 라이브러리로 사용할 수 있는 성과 척도의 출력을 제공할 수 있다.
백트레이더: 로컬 백테스트용 유연한 도구¶
- 백트레이더: 로컬 백테스트용 파이썬 라이브러리
백트레더의 세리브로 구조의 주요 개념¶
- 백트레이더의 세레브로 구조는 파이썬 객체로서 백테스트 워크플로의 주요 구성 요소를 나타낸다.
데이터 피드, 행, 지표¶
- 데이터 피드: 전략의 기본 자료이며 각 관측에 대한 타임스탬프가 있는 OHLCV 시장 데이터와 같은 개별 증권에 대한 정보를 포함하지만, 사용 가능한 필드를 사용자가 지정할 수 있다.
일단 로딩되면 데이터 피드를 세레브로 인스턴스에 추가하며, 그 결과 수신된 순서에 따라 하나 이상의 전략에 데이터 피드를 사용할 수 있게 된다.
전략의 트레이딩 논리는 각 데이터 피드에 종목명(예. 티커) 또는 시퀀스 번호로 접근하고 데이터 피드의 모든 필드의 현재 및 과거 값을 검색할 수 있다. 각 필드를 라인이라고 한다.
백트레이더는 130개 이상의 일반적인 기술적 지표와 함께 제공되므로 각 데이터 피드에 대한 라인이나 기타 지표로부터 새로운 값을 계산해 전략을 구동할 수 있다. (표준 파이썬 연산을 사용해 새 값을 도출할 수도 있다.)
데이터와 신호로부터 거래로: 전략¶
- 전략 객체에는 백테스트 실행 중 모든 바에서 세레브로 인스턴스가 제공하는 데이터 피드 정보를 기반으로 주문을 내는 트레이딩 논리가 포함돼 있다.
- 전략 인스턴스를 세레브로에 추가할 때 정의한 임의의 파라미터를 허용하도록 전략을 구성해 변형을 쉽게 테스트할 수 있다.
수수료 체계 대신 수수료¶
당신의 전략이 각 바에서 현재 및 과거 데이터 포인트를 평가하면 어떤 주문을 낼지 결정해야 한다. 백트레이더를 사용하면 세레브로가 실행을 위해 브로커 인스턴스로 전달하는 여러 표준 주문 유형을 생성하고 각 바에서 결과에 대한 통지를 제공할 수 있다.
- 전략 메서드를 사용한 주문 실행 작동
- 시장가 주문: 다음 시가 바에서 체결
- 종가 주문: 다음 종가 바에서 체결
- 지정가 주문: 유효 기간(선택 사항) 동안 가격 임곗값(예, 특정 가격까지만 매수)이 충족되는 경우에만 실행
- 스탑 주문: 가격이 지정된 임곗값에 도달할 경우 시장가 주문이 된다.
- 스탑 지정가 주문: 스탑이 발동된 후 지정가 주문이 된다.
이 모든 것을 실현: 세레브로¶
- 세레브로 제어 시스템은 타임스탬프로 표시되는 바를 기반으로 데이터 피드를 동기화하고 이에 따라 이벤트별로 거래 로직과 브로커 행동을 실행한다.
실제에서 백트레이더를 사용하는 방법¶
세레브로 인스턴트를 만들고, 데이터를 로딩하고, 전략을 공식화 및 추가하고, 백테스트를 실행하고, 결과를 검토한다.
03_backtesting_with_backtrader.ipynb
백트레이더 요약과 다음 단계¶
- 로컬 백테스트를 위한 간단하고 유연하며 성과 좋은 백테스트 엔진
- 판다스 호환성으로 광범위한 소스에서 원하는 빈도로 데이터 세트 로딩 가능
- 전략을 통해 임의의 트레이딩 로직을 정의 가능
- 파이폴리오와 잘 통합돼 빠르고 포괄적인 성과 평가 가능
집라인: 퀀토피안이 만든 확장 가능한 백테스트¶
- 백테스팅 엔진 집라인은 퀀토피안의 온라인 연구, 백테스팅, 실시간(모의) 거래 플랫폼을 지원한다.
- 헤지 편도로서 퀀토피안은 리스크 관리 기준에 따라 성능이 우수한 강력한 알고리듬을 식별하는 것을 목표로 한다.
강건한 시뮬레이션을 위한 캘린더와 파이프라인¶
- 확장성과 신뢰성의 목표에 기여하는 주요 특성은 분할 및 배당에 대한 즉각적인 조정으로 OHLCV 시장 데이터를 저장하는 데이터 번들, 전 세계 거래소 운영 시간을 반영하는 거래 캘린더, 강력한 Pipline API다.
번들: 즉각적인 조정이 가능한 시점 데이터¶
- 주 데이터 저장소는 SQLite 데이터베이스에 저장된 메타데이터와 함께 효율적인 검색을 위해 디스크에 있는 압축된 열 형태의 bcolz 형식으로 지정된 번들이다.
- 번들은 OHLCV 데이터만 포함되게 설계됐으며 일과 분 빈도로 제한된다.
- 번들이 분할 및 배당 정보를 저장하고, 집라인은 백테스트에 대해 선택한 기간에 따라 시점 조정을 계산한다.
알고리듬 API: 일정에 따른 백테스트¶
- 초기 설정 후 백테스트는 지정된 기간 동안 실행되며 특정 이벤트가 발생할 때 해당 트레이딩 로직을 실행한다.
- 이러한 이벤트는 일별 또는 분별 거래 빈도에 따라 발생하지만 신호를 평가하고 주문을 내고, 포트폴리오를 리밸런싱하거나 진행 중인 시뮬레이션에 대한 정보를 기록하기 위해 임의의 함수를 예약할 수도 있다.
알려진 문제¶
- 현재 재무성 채권 곡선과 S&P 500 벤치마킹 수익률을 필요로 한다. 후자는 IEX API에 의존하며, 이제 키를 얻을 때 등록이 필요하다.
분 데이터를 사용해 자체 번들 인제스트¶
- OHCLV 데이터를 티커당 하나의 파일로 나누고 메타데이터를 저장하고 분할 및 배당 조정 작업을 수행한다.
- 결과를 ingest() 함수에 전달하기 위한 스크립트를 작성해 번들을 bcolz 및 SQLite 형식으로 작성한다.
- 홈 폴더의 .zipline 디렉터리에 있는 extension.py 스크립트에 번들을 등록하고 데이터 소스를 symlink한다.
- AlgoSeek 데이터의 경우 NYSE 시장 시간 이외의 거래 활동을 포함하므로 사용자 정의 거래 일정도 제공한다.
# algoseek_1min_trades.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = 'Stefan Jansen'
from pathlib import Path
from os import getenv
import numpy as np
import pandas as pd
pd.set_option('display.expand_frame_repr', False)
np.random.seed(42)
"""
This code is based on Algoseek's NASDAQ100 minute-bar trade data.
Please refer to the README for a brief summary on how you could adapt this code for your purposes.
"""
ZIPLINE_ROOT = getenv('ZIPLINE_ROOT')
if not ZIPLINE_ROOT:
custom_data_path = Path('~/.zipline/custom_data').expanduser()
else:
custom_data_path = Path(ZIPLINE_ROOT, 'custom_data')
def load_equities():
return pd.read_hdf(custom_data_path / 'algoseek.h5', 'equities')
def ticker_generator():
"""
Lazily return (sid, ticker) tuple
"""
return (v for v in load_equities().values)
def data_generator():
for sid, symbol, asset_name in ticker_generator():
df = (pd.read_hdf(custom_data_path / 'algoseek.h5', str(sid))
.tz_localize('US/Eastern')
.tz_convert('UTC'))
start_date = df.index[0]
end_date = df.index[-1]
first_traded = start_date.date()
auto_close_date = end_date + pd.Timedelta(days=1)
exchange = 'AlgoSeek'
yield (sid, df), symbol, asset_name, start_date, end_date, first_traded, auto_close_date, exchange
def metadata_frame():
dtype = [
('symbol', 'object'),
('asset_name', 'object'),
('start_date', 'datetime64[ns]'),
('end_date', 'datetime64[ns]'),
('first_traded', 'datetime64[ns]'),
('auto_close_date', 'datetime64[ns]'),
('exchange', 'object'), ]
return pd.DataFrame(np.empty(len(load_equities()), dtype=dtype))
def algoseek_to_bundle(interval='1m'):
def ingest(environ,
asset_db_writer,
minute_bar_writer,
daily_bar_writer,
adjustment_writer,
calendar,
start_session,
end_session,
cache,
show_progress,
output_dir
):
metadata = metadata_frame()
def minute_data_generator():
return (sid_df for (sid_df, *metadata.iloc[sid_df[0]]) in data_generator())
minute_bar_writer.write(minute_data_generator(), show_progress=True)
metadata.dropna(inplace=True)
asset_db_writer.write(equities=metadata)
adjustment_writer.write(splits=pd.read_hdf(custom_data_path / 'algoseek.h5', 'splits'))
# dividends do not work
# adjustment_writer.write(dividends=pd.read_hdf(custom_data_path / 'algoseek.h5', 'dividends'))
return ingest
# algoseek_preprocessing.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
__author__ = 'Stefan Jansen'
import sqlite3
from pathlib import Path
from os import getenv
import numpy as np
import pandas as pd
from pandas_datareader.nasdaq_trader import get_nasdaq_symbols
np.random.seed(42)
idx = pd.IndexSlice
"""
This code is based on Algoseek's NASDAQ100 minute-bar trade data.
Please refer to the README for a brief summary on how you could adapt this code for your purposes.
"""
PROJECT_DIR = Path('..', '..')
data_path = PROJECT_DIR / 'data' / 'nasdaq100'
ZIPLINE_ROOT = getenv('ZIPLINE_ROOT')
if not ZIPLINE_ROOT:
quandl_path = Path('~', '.zipline', 'data', 'quandl').expanduser()
else:
quandl_path = Path(ZIPLINE_ROOT, 'data', 'quandl')
downloads = sorted([f.name for f in quandl_path.iterdir() if f.is_dir()])
if not downloads:
print('Need to run "zipline ingest" first')
exit()
download_timestamp = downloads[-1]
adj_db_path = quandl_path / 'adjustments.sqlite'
equities_db_path = quandl_path / 'assets-7.sqlite'
def read_sqlite(table, con):
return pd.read_sql("SELECT * FROM " + table, con=con).dropna(how='all', axis=1)
def get_equities():
nasdaq100 = pd.read_hdf(data_path / 'data.h5', '1min_trades')
equities_con = sqlite3.connect(equities_db_path.as_posix())
equities = read_sqlite('equity_symbol_mappings', equities_con)
all_tickers = nasdaq100.index.get_level_values('ticker').unique()
tickers_with_meta = np.sort(all_tickers.intersection(pd.Index(equities.symbol)))
nasdaq_info = (get_nasdaq_symbols()
.reset_index()
.rename(columns=lambda x: x.lower().replace(' ', '_'))
.loc[:, ['symbol', 'security_name']]
.rename(columns={'security_name': 'asset_name'}))
nasdaq_tickers = pd.DataFrame({'symbol': tickers_with_meta}).merge(nasdaq_info, how='left')
nasdaq_sids = (equities.loc[equities.symbol.isin(nasdaq_tickers.symbol),
['symbol', 'sid']])
nasdaq_tickers = (nasdaq_tickers.merge(nasdaq_sids, how='left')
.reset_index()
.rename(columns={'sid': 'quandl_sid', 'index': 'sid'}))
nasdaq_tickers.to_hdf('algoseek.h5', 'equities')
def get_dividends():
equities = pd.read_hdf('algoseek.h5', 'equities')
adjustments_con = sqlite3.connect(adj_db_path.as_posix())
div_cols = ['sid', 'ex_date', 'declared_date', 'pay_date', 'record_date', 'amount']
dividends = read_sqlite('dividend_payouts', adjustments_con)[['sid', 'ex_date', 'amount']]
dividends = (dividends.rename(columns={'sid': 'quandl_sid'})
.merge(equities[['quandl_sid', 'sid']])
.drop('quandl_sid', axis=1))
print(dividends.loc[:, div_cols].info())
dividends.reindex(div_cols, axis=1).to_hdf('algoseek.h5', 'dividends')
def get_splits():
split_cols = ['sid', 'effective_date', 'ratio']
equities = pd.read_hdf('algoseek.h5', 'equities')
adjustments_con = sqlite3.connect(adj_db_path.as_posix())
splits = read_sqlite('splits', adjustments_con)[split_cols]
splits = (splits.rename(columns={'sid': 'quandl_sid'})
.merge(equities[['quandl_sid', 'sid']])
.drop('quandl_sid', axis=1)
)
print(splits.loc[:, split_cols].info())
splits.loc[:, split_cols].to_hdf('algoseek.h5', 'splits')
def get_ohlcv_by_ticker():
equities = pd.read_hdf('algoseek.h5', 'equities')
col_dict = {'first': 'open', 'last': 'close'}
nasdaq100 = (pd.read_hdf(data_path / 'data.h5', '1min_trades')
.loc[idx[equities.symbol, :], :]
.rename(columns=col_dict))
print(nasdaq100.info())
symbol_dict = equities.set_index('symbol').sid.to_dict()
for symbol, data in nasdaq100.groupby(level='ticker'):
print(symbol)
data.reset_index('ticker', drop=True).to_hdf('algoseek.h5', '{}'.format(symbol_dict[symbol]))
equities.drop('quandl_sid', axis=1).to_hdf('algoseek.h5', 'equities')
# extension.py
import sys
from pathlib import Path
sys.path.append(Path('~', '.zipline').expanduser().as_posix())
from zipline.data.bundles import register
from algoseek_1min_trades import algoseek_to_bundle
from datetime import time
from pytz import timezone
from trading_calendars import register_calendar
from trading_calendars.exchange_calendar_xnys import XNYSExchangeCalendar
"""
This code is based on Algoseek's NASDAQ100 minute-bar trade data.
Please refer to the README for a brief summary on how you could adapt this code for your purposes.
"""
class AlgoSeekCalendar(XNYSExchangeCalendar):
"""
A calendar for trading assets before and after market hours
Open Time: 4AM, US/Eastern
Close Time: 19:59PM, US/Eastern
"""
@property
def name(self):
"""
The name of the exchange that zipline
looks for when we run our algorithm
"""
return "AlgoSeek"
@property
def tz(self):
return timezone("US/Eastern")
open_times = (
(None, time(4, 1)),
)
close_times = (
(None, time(19, 59)),
)
register_calendar(
'AlgoSeek',
AlgoSeekCalendar()
)
register('algoseek',
algoseek_to_bundle(),
calendar_name='AlgoSeek',
minutes_per_day=960
)
파이프라인 API: ML 신호 백테스트¶
- Pieline API는 과거 데이터로부터 유가 증권의 횡단면에 대한 알파 팩터의 정의와 계산을 용이하게 한다.
- 이벤트 기반 구조를 계속 따르지만 가능한 경우 팩터의 계산을 벡터와 한다.
- 02_backtesting_with_zipline.ipynb
백테스트 중 모델을 훈련하는 방법¶
- 모델 훈련을 백테스트에 통합할 수 있다.
- 03_ml4t_with_zipline.ipynb
'공부 > ML4T' 카테고리의 다른 글
CH07 선형 모델: 리스크 팩터에서 수익률 예측까지 (0) | 2023.06.30 |
---|---|
CH06 머신러닝 프로세스 (0) | 2023.06.30 |
CH05 포트폴리오 최적화와 성과 평가 (0) | 2023.06.24 |
CH04 금융 특성 공학: 알파 팩터 리서치 (0) | 2023.06.24 |
CH03 금융을 위한 대체 데이터: 범주와 사용 사례 (0) | 2023.06.17 |