백테스팅 (Backtesting) 은 과거 성과를 확인하기 위해 역사적인 데이터에 거래 전략 아이디어를 적용하는 연구 과정이다. 특히 백테스터는 전략의 미래 성과에 대해 보증하지 않습니다. 그러나 전략 파이프라인 연구 과정의 필수 요소이며, 전략이 생산에 들어가기 전에 필터링 될 수 있습니다.
이 기사 (그리고 그 다음 기사) 에서는 파이썬으로 작성된 기본적인 객체 지향 백테스팅 시스템을 설명할 것입니다. 이 초기 시스템은 주로
탄탄한 백테스팅 시스템을 설계하는 과정은 매우 어렵습니다. 알고리즘 거래 시스템의 성능에 영향을 미치는 모든 구성 요소를 효과적으로 시뮬레이션하는 것은 어려운 일입니다. 데이터의 불량, 브로커의 오더 라우팅의 불투명성, 오더 레이텐시 및 수많은 다른 요소는 백테스트 성능에 비해 전략의 "진실한" 성능을 변경하는 음모를 꾸미고 있습니다.
백테스팅 시스템을 개발할 때 성능을 평가하는 데 결정적인 요소가 더 많기 때문에 끊임없이 "초로부터 다시 작성"하려는 유혹이 있습니다. 백테스팅 시스템은 결코 완료되지 않으며 시스템에서 충분한 요소가 캡처되었는지 개발 과정에서 판단해야합니다.
이러한 우려를 염두에 두고 여기서 제시된 백테스터는 다소 단순화 될 것입니다. 우리가 더 많은 문제를 탐구 할 때 (포트폴리오 최적화, 위험 관리, 거래 비용 처리) 백테스터는 더 강력해질 것입니다.
일반적으로 두 가지 유형의 백테스팅 시스템이 흥미로울 수 있습니다. 첫 번째는 연구 기반이며, 주로 초기 단계에서 사용되며, 더 심각한 평가를위한 전략을 선택하기 위해 많은 전략이 테스트됩니다. 이러한 연구 백테스팅 시스템은 종종 파이썬, R 또는 MatLab에서 작성됩니다. 개발 속도가 이 단계에서 실행 속도보다 더 중요하기 때문에.
두 번째 유형의 백테스팅 시스템은 이벤트 기반입니다. 즉, 거래 실행 시스템 자체와 유사한 (만약 동일하지 않다면) 실행 루프에서 백테스팅 프로세스를 수행합니다. 전략에 대한 더 엄격한 평가를 제공하기 위해 시장 데이터와 주문 실행 프로세스를 현실적으로 모델링합니다.
후자의 시스템은 종종 실행 속도가 필수적인 C ++ 또는 Java와 같은 고성능 언어로 작성됩니다. 낮은 주파수 전략 (아직도 내일에도 불구하고), 파이썬은이 맥락에서 사용하기에 충분합니다.
이제 객체 지향 연구 기반 백테스팅 환경의 설계 및 구현에 대해 논의 할 것입니다. 객체 지향은 다음과 같은 이유로 소프트웨어 설계 패러다임으로 선택되었습니다.
이 단계에서 백테스터는 실제 시장 정확성의 희생으로 구현 용이성과 합리적인 수준의 유연성을 위해 설계되었습니다. 특히, 이 백테스터는 단일 도구에 작용하는 전략을 처리 할 수 있습니다. 나중에 백테스터는 일련의 도구를 처리하도록 수정됩니다. 초기 백테스터에 대해 다음과 같은 구성 요소가 필요합니다.
볼 수 있듯이, 이 백테스터는 포트폴리오/위험 관리, 실행 처리 (즉, 제한 주문) 에 대한 언급을 포함하지 않으며, 거래 비용의 정교한 모델링도 제공하지 않습니다. 이 단계에서는 큰 문제가 아닙니다. 객체 지향 백테스터와 Pandas/NumPy 라이브러리를 만드는 과정에 익숙해질 수 있습니다. 시간이 지남에 따라 개선 될 것입니다.
이제 각 객체에 대한 구현을 설명합니다.
전략 객체는 예측, 평균 역전, 추진력 및 변동성 전략을 처리하기 때문에 이 단계에서 상당히 일반적이어야합니다. 여기서 고려되는 전략은 항상 시간 계열에 기반을두고, 즉
전략 클래스는 또한 항상 신호 추천을 생성할 것입니다. 이것은 포트폴리오 인스턴스에 장기/단순 또는 포지션을 보유하는 의미에서 조언을 할 것이라는 것을 의미합니다. 이러한 유연성은 더 고급 포트폴리오 클래스가 입력되는 실제 포지션을 결정하기 위해 받아들일 수있는 신호 세트를 제공하는 여러 전략
클래스의 인터페이스는 추상적 기본 클래스 방법론을 사용하여 강제됩니다. 추상적 기본 클래스는 인스턴스가 될 수없는 객체이며 따라서 파생 클래스만 만들 수 있습니다. 파이썬 코드는 아래에서 파일로 제공됩니다.backtest.py. 전략 클래스는 모든 하위 클래스가 generate_signals 메소드를 구현해야 합니다.
전략 클래스가 직접 인스턴스화되는 것을 막기 위해 (그것이 추상적이기 때문에!) ABCMeta와 abstractmethod 객체를 abc 모듈에서 사용해야합니다. 우리는 클래스의 속성을 설정합니다.메타클래스ABCMeta와 같고 abstractmethod decorator로 generate_signals 메소드를 장식합니다.
# backtest.py
from abc import ABCMeta, abstractmethod
class Strategy(object):
"""Strategy is an abstract base class providing an interface for
all subsequent (inherited) trading strategies.
The goal of a (derived) Strategy object is to output a list of signals,
which has the form of a time series indexed pandas DataFrame.
In this instance only a single symbol/instrument is supported."""
__metaclass__ = ABCMeta
@abstractmethod
def generate_signals(self):
"""An implementation is required to return the DataFrame of symbols
containing the signals to go long, short or hold (1, -1 or 0)."""
raise NotImplementedError("Should implement generate_signals()!")
위의 인터페이스는 간단하지만, 이 클래스가 각 특정 유형의 전략에 대해 상속될 때 더 복잡해질 것입니다. 궁극적으로 이 설정에서 전략 클래스의 목표는 포트폴리오에 전송되는 각 도구에 대한 긴 / 짧은 / 보유 신호 목록을 제공하는 것입니다.
포트폴리오 클래스는 거래 논리의 대부분을 차지합니다. 이 연구 백테스터를 위해 포트폴리오는 포지션 사이즈, 리스크 분석, 거래 비용 관리 및 실행 처리 (즉 시장 개시, 시장 폐쇄 주문) 를 결정하는 역할을 합니다. 나중에 이러한 작업은 별도의 구성 요소로 분해됩니다. 지금 그들은 하나의 클래스로 롤링됩니다.
이 클래스는 판다를 광범위하게 사용하며, 특히
포트폴리오 클래스의 목표는 궁극적으로 거래의 순서와 주식 곡선을 생성하는 것입니다. 성과 클래스가 분석 할 것입니다. 이를 달성하기 위해서는 전략 객체에서 거래 권고 목록을 제공해야합니다. 나중에 이것은 전략 객체의 그룹이 될 것입니다.
포트폴리오 클래스는 특정 거래 신호 세트에 자본이 어떻게 배치되어야하는지, 거래 비용을 처리하는 방법과 어떤 형태의 오더가 사용될지 알려져야합니다. 전략 객체는 데이터 바에 작동하고 따라서 오더 실행에 달성 된 가격에 대해 가정해야합니다. 모든 바의 높은 / 낮은 가격이 사전에 알려지지 않기 때문에 거래에 대한 오픈 및 클로즈 가격만을 사용할 수 있습니다. 실제로 시장 오더를 사용할 때 이러한 특정 가격 중 하나에서 오더가 채워질 것을 보장하는 것은 불가능하므로 가장 좋은 경우에는 근사일 것입니다.
명령어 채용에 대한 가정 외에도, 이 백테스터는 마진/브로커리지 제약에 대한 모든 개념을 무시하고 유동성 제약 없이 자유롭게 모든 도구에 대해 길고 짧게 갈 수 있다고 가정합니다. 이것은 분명히 매우 비현실적인 가정이지만 나중에 완화 될 수 있습니다.
다음 목록은 계속됩니다backtest.py:
# backtest.py
class Portfolio(object):
"""An abstract base class representing a portfolio of
positions (including both instruments and cash), determined
on the basis of a set of signals provided by a Strategy."""
__metaclass__ = ABCMeta
@abstractmethod
def generate_positions(self):
"""Provides the logic to determine how the portfolio
positions are allocated on the basis of forecasting
signals and available cash."""
raise NotImplementedError("Should implement generate_positions()!")
@abstractmethod
def backtest_portfolio(self):
"""Provides the logic to generate the trading orders
and subsequent equity curve (i.e. growth of total equity),
as a sum of holdings and cash, and the bar-period returns
associated with this curve based on the 'positions' DataFrame.
Produces a portfolio object that can be examined by
other classes/functions."""
raise NotImplementedError("Should implement backtest_portfolio()!")
이 단계에서 전략과 포트폴리오 추상 기본 클래스가 도입되었습니다. 우리는 이제 작동하는 "놀이 전략"을 생산하기 위해 이러한 클래스의 구체적인 파생 구현을 생성 할 수있는 위치에 있습니다.
우리는 RandomForecastStrategy라는 전략의 하위 클래스를 생성하여 시작할 것입니다. 유일한 임무는 무작위로 선택한 긴 / 짧은 신호를 생성하는 것입니다! 이것은 분명히 무의미한 거래 전략이지만 객체 지향 백테스팅 프레임워크를 보여줌으로써 우리의 필요를 충족시킬 것입니다. 따라서 random_forecast.py라는 새로운 파일을 시작하여 무작위 예측자에 대한 목록은 다음과 같습니다.
# random_forecast.py
import numpy as np
import pandas as pd
import Quandl # Necessary for obtaining financial data easily
from backtest import Strategy, Portfolio
class RandomForecastingStrategy(Strategy):
"""Derives from Strategy to produce a set of signals that
are randomly generated long/shorts. Clearly a nonsensical
strategy, but perfectly acceptable for demonstrating the
backtesting infrastructure!"""
def __init__(self, symbol, bars):
"""Requires the symbol ticker and the pandas DataFrame of bars"""
self.symbol = symbol
self.bars = bars
def generate_signals(self):
"""Creates a pandas DataFrame of random signals."""
signals = pd.DataFrame(index=self.bars.index)
signals['signal'] = np.sign(np.random.randn(len(signals)))
# The first five elements are set to zero in order to minimise
# upstream NaN errors in the forecaster.
signals['signal'][0:5] = 0.0
return signals
이제 우리는
포트폴리오 객체는 인터페이스에서 매우 유연하지만 거래 비용, 시장 주문 등을 처리하는 방법에 대한 특정 선택이 필요합니다. 이 기본 예제에서 저는 제한이나 마진 없이 쉽게 인스트루먼트를 길게 / 짧게 할 수 있고, 바리의 오픈 가격에 직접 구매하거나 판매 할 수 있으며, 거래 비용 (슬리퍼, 수수료 및 시장 영향을 포함) 은 0이며 각 거래에 직접 구매 할 수 있는 주식의 양을 지정했습니다.
여기 random_forecast.py 목록의 연속입니다:
# random_forecast.py
class MarketOnOpenPortfolio(Portfolio):
"""Inherits Portfolio to create a system that purchases 100 units of
a particular symbol upon a long/short signal, assuming the market
open price of a bar.
In addition, there are zero transaction costs and cash can be immediately
borrowed for shorting (no margin posting or interest requirements).
Requires:
symbol - A stock symbol which forms the basis of the portfolio.
bars - A DataFrame of bars for a symbol set.
signals - A pandas DataFrame of signals (1, 0, -1) for each symbol.
initial_capital - The amount in cash at the start of the portfolio."""
def __init__(self, symbol, bars, signals, initial_capital=100000.0):
self.symbol = symbol
self.bars = bars
self.signals = signals
self.initial_capital = float(initial_capital)
self.positions = self.generate_positions()
def generate_positions(self):
"""Creates a 'positions' DataFrame that simply longs or shorts
100 of the particular symbol based on the forecast signals of
{1, 0, -1} from the signals DataFrame."""
positions = pd.DataFrame(index=signals.index).fillna(0.0)
positions[self.symbol] = 100*signals['signal']
return positions
def backtest_portfolio(self):
"""Constructs a portfolio from the positions DataFrame by
assuming the ability to trade at the precise market open price
of each bar (an unrealistic assumption!).
Calculates the total of cash and the holdings (market price of
each position per bar), in order to generate an equity curve
('total') and a set of bar-based returns ('returns').
Returns the portfolio object to be used elsewhere."""
# Construct the portfolio DataFrame to use the same index
# as 'positions' and with a set of 'trading orders' in the
# 'pos_diff' object, assuming market open prices.
portfolio = self.positions*self.bars['Open']
pos_diff = self.positions.diff()
# Create the 'holdings' and 'cash' series by running through
# the trades and adding/subtracting the relevant quantity from
# each column
portfolio['holdings'] = (self.positions*self.bars['Open']).sum(axis=1)
portfolio['cash'] = self.initial_capital - (pos_diff*self.bars['Open']).sum(axis=1).cumsum()
# Finalise the total and bar-based returns based on the 'cash'
# and 'holdings' figures for the portfolio
portfolio['total'] = portfolio['cash'] + portfolio['holdings']
portfolio['returns'] = portfolio['total'].pct_change()
return portfolio
이것은 우리에게 필요한 모든 것을 제공합니다. 이러한 시스템에 기반한 주식 곡선을 생성합니다. 마지막 단계는주요기능:
if __name__ == "__main__":
# Obtain daily bars of SPY (ETF that generally
# follows the S&P500) from Quandl (requires 'pip install Quandl'
# on the command line)
symbol = 'SPY'
bars = Quandl.get("GOOG/NYSE_%s" % symbol, collapse="daily")
# Create a set of random forecasting signals for SPY
rfs = RandomForecastingStrategy(symbol, bars)
signals = rfs.generate_signals()
# Create a portfolio of SPY
portfolio = MarketOnOpenPortfolio(symbol, bars, signals, initial_capital=100000.0)
returns = portfolio.backtest_portfolio()
print returns.tail(10)
프로그램의 출력은 다음과 같습니다. 당신의 선택 날짜 범위와 사용 된 무작위 씨앗에 따라 아래 출력에서 다를 것입니다:
SPY holdings cash total returns
날짜
2014-01-02 -18398 -18398 111486 93088 0.000097
2014-01-03 18321 18321 74844 93165 0.000827
2014-01-06 18347 18347 74844 93191 0.000279
2014-01-07 18309 18309 74844 93153 -0.000408
2014-01-08 -18345 -18345 111534 93189 0.000386
2014-01-09 -18410 -18410 111534 93124 -0.000698
2014-01-10 -18395 -18395 111534 93139 0.000161
2014-01-13 -18371 -18371 111534 93163 0.000258
2014-01-14 -18228 -18228 111534 93306 0.001535
2014-01-15 18410 18410 74714 93124 -0.001951
이 경우 전략은 돈을 잃었습니다. 예측자의 스토카스틱 성격을 감안할 때 놀라운 일이 아닙니다. 다음 단계는 포트폴리오 인스턴스를 받아들이고 전략을 필터링 할 것인지 아닌지 결정하는 데 기반을 둔 성능 메트릭의 목록을 제공하는 성능 객체를 만드는 것입니다.
우리는 또한 포트폴리오 객체를 개선하여 트랜잭션 비용 (인터랙티브 브로커 수수료 및 미끄러짐과 같은) 을 보다 현실적으로 처리 할 수 있습니다. 우리는 또한 전략 객체에 예측 엔진을 간단하게 포함 할 수 있습니다. 이는 (희망) 더 나은 결과를 가져올 것입니다. 다음 기사에서는 이러한 개념을 더 깊이 탐구 할 것입니다.