판다와 함께 파이썬에서 백테스팅 환경을 연구

저자:선함, 2019-03-16 11:58:20에서 제작, 업데이트:

백테스팅 (Backtesting) 은 과거 성과를 확인하기 위해 역사적인 데이터에 거래 전략 아이디어를 적용하는 연구 과정이다. 특히 백테스터는 전략의 미래 성과에 대해 보증하지 않습니다. 그러나 전략 파이프라인 연구 과정의 필수 요소이며, 전략이 생산에 들어가기 전에 필터링 될 수 있습니다.

이 기사 (그리고 그 다음 기사) 에서는 파이썬으로 작성된 기본적인 객체 지향 백테스팅 시스템을 설명할 것입니다. 이 초기 시스템은 주로 교육 보조제로, 백테스팅 시스템의 다양한 구성 요소를 보여주기 위해 사용됩니다. 기사를 진행함에 따라 더 정교한 기능이 추가 될 것입니다.

백테스팅 개요

탄탄한 백테스팅 시스템을 설계하는 과정은 매우 어렵습니다. 알고리즘 거래 시스템의 성능에 영향을 미치는 모든 구성 요소를 효과적으로 시뮬레이션하는 것은 어려운 일입니다. 데이터의 불량, 브로커의 오더 라우팅의 불투명성, 오더 레이텐시 및 수많은 다른 요소는 백테스트 성능에 비해 전략의 "진실한" 성능을 변경하는 음모를 꾸미고 있습니다.

백테스팅 시스템을 개발할 때 성능을 평가하는 데 결정적인 요소가 더 많기 때문에 끊임없이 "초로부터 다시 작성"하려는 유혹이 있습니다. 백테스팅 시스템은 결코 완료되지 않으며 시스템에서 충분한 요소가 캡처되었는지 개발 과정에서 판단해야합니다.

이러한 우려를 염두에 두고 여기서 제시된 백테스터는 다소 단순화 될 것입니다. 우리가 더 많은 문제를 탐구 할 때 (포트폴리오 최적화, 위험 관리, 거래 비용 처리) 백테스터는 더 강력해질 것입니다.

백테스트 시스템 종류

일반적으로 두 가지 유형의 백테스팅 시스템이 흥미로울 수 있습니다. 첫 번째는 연구 기반이며, 주로 초기 단계에서 사용되며, 더 심각한 평가를위한 전략을 선택하기 위해 많은 전략이 테스트됩니다. 이러한 연구 백테스팅 시스템은 종종 파이썬, R 또는 MatLab에서 작성됩니다. 개발 속도가 이 단계에서 실행 속도보다 더 중요하기 때문에.

두 번째 유형의 백테스팅 시스템은 이벤트 기반입니다. 즉, 거래 실행 시스템 자체와 유사한 (만약 동일하지 않다면) 실행 루프에서 백테스팅 프로세스를 수행합니다. 전략에 대한 더 엄격한 평가를 제공하기 위해 시장 데이터와 주문 실행 프로세스를 현실적으로 모델링합니다.

후자의 시스템은 종종 실행 속도가 필수적인 C ++ 또는 Java와 같은 고성능 언어로 작성됩니다. 낮은 주파수 전략 (아직도 내일에도 불구하고), 파이썬은이 맥락에서 사용하기에 충분합니다.

파이썬에서 객체 지향 연구 백테스터

이제 객체 지향 연구 기반 백테스팅 환경의 설계 및 구현에 대해 논의 할 것입니다. 객체 지향은 다음과 같은 이유로 소프트웨어 설계 패러다임으로 선택되었습니다.

  • 각 구성 요소의 인터페이스는 미리 지정 될 수 있으며, 각 구성 요소의 내부는 프로젝트 진행에 따라 수정 (또는 교체) 될 수 있습니다.
  • 인터페이스를 미리 지정함으로써 각 구성 요소의 동작을 효과적으로 테스트 할 수 있습니다 (단위 테스트를 통해)
  • 시스템을 확장할 때 새로운 구성 요소는 다른 구성 요소 위에 또는 추가로 생성될 수 있습니다.

이 단계에서 백테스터는 실제 시장 정확성의 희생으로 구현 용이성과 합리적인 수준의 유연성을 위해 설계되었습니다. 특히, 이 백테스터는 단일 도구에 작용하는 전략을 처리 할 수 있습니다. 나중에 백테스터는 일련의 도구를 처리하도록 수정됩니다. 초기 백테스터에 대해 다음과 같은 구성 요소가 필요합니다.

  • 전략 - 전략 클래스는 바의 판다스 데이터 프레임, 즉 특정 주파수에서 오픈-하이-로-클로즈 볼륨 (OHLCV) 데이터 포인트 목록을 수신합니다. 전략은 시간표와 세트 {1,0,−1}의 요소로 구성된 신호 목록을 생성합니다. 각각 긴 신호, 홀드 신호 또는 짧은 신호를 나타냅니다.
  • 포트폴리오 - 백테스팅 작업의 대부분은 포트폴리오 클래스에서 이루어집니다. 그것은 신호의 세트를 수신하고 현금 구성 요소에 대해 할당 된 일련의 포지션을 생성합니다. 포트폴리오 객체의 작업은 주식 곡선을 생성하고 기본 거래 비용을 통합하고 거래를 추적하는 것입니다.
  • 성능 - 성능 객체는 포트폴리오를 가지고 성능에 대한 통계 집합을 생성합니다. 특히 위험/수익 특성을 (샤르페, 소르티노 및 정보 비율), 무역/이익 메트릭 및 유출 정보를 출력합니다.

뭐가 빠졌어?

볼 수 있듯이, 이 백테스터는 포트폴리오/위험 관리, 실행 처리 (즉, 제한 주문) 에 대한 언급을 포함하지 않으며, 거래 비용의 정교한 모델링도 제공하지 않습니다. 이 단계에서는 큰 문제가 아닙니다. 객체 지향 백테스터와 Pandas/NumPy 라이브러리를 만드는 과정에 익숙해질 수 있습니다. 시간이 지남에 따라 개선 될 것입니다.

시행

이제 각 객체에 대한 구현을 설명합니다.

전략

전략 객체는 예측, 평균 역전, 추진력 및 변동성 전략을 처리하기 때문에 이 단계에서 상당히 일반적이어야합니다. 여기서 고려되는 전략은 항상 시간 계열에 기반을두고, 즉 가격 구동입니다. 이 백테스터의 초기 요구 사항은 파생된 전략 클래스가 틱 (거래별 가격) 또는 오더북 데이터보다는 입력으로 바 목록 (OHLCV) 을 받아들이는 것입니다. 따라서 여기서 고려되는 가장 세밀한 곡선은 1초 바입니다.

전략 클래스는 또한 항상 신호 추천을 생성할 것입니다. 이것은 포트폴리오 인스턴스에 장기/단순 또는 포지션을 보유하는 의미에서 조언을 할 것이라는 것을 의미합니다. 이러한 유연성은 더 고급 포트폴리오 클래스가 입력되는 실제 포지션을 결정하기 위해 받아들일 수있는 신호 세트를 제공하는 여러 전략 상담자를 만들 수 있습니다.

클래스의 인터페이스는 추상적 기본 클래스 방법론을 사용하여 강제됩니다. 추상적 기본 클래스는 인스턴스가 될 수없는 객체이며 따라서 파생 클래스만 만들 수 있습니다. 파이썬 코드는 아래에서 파일로 제공됩니다.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()!")

위의 인터페이스는 간단하지만, 이 클래스가 각 특정 유형의 전략에 대해 상속될 때 더 복잡해질 것입니다. 궁극적으로 이 설정에서 전략 클래스의 목표는 포트폴리오에 전송되는 각 도구에 대한 긴 / 짧은 / 보유 신호 목록을 제공하는 것입니다.

포트폴리오

포트폴리오 클래스는 거래 논리의 대부분을 차지합니다. 이 연구 백테스터를 위해 포트폴리오는 포지션 사이즈, 리스크 분석, 거래 비용 관리 및 실행 처리 (즉 시장 개시, 시장 폐쇄 주문) 를 결정하는 역할을 합니다. 나중에 이러한 작업은 별도의 구성 요소로 분해됩니다. 지금 그들은 하나의 클래스로 롤링됩니다.

이 클래스는 판다를 광범위하게 사용하며, 특히 보일러플레이트 데이터 링과 관련하여 라이브러리가 많은 시간을 절약할 수 있다는 훌륭한 예를 제공합니다. 한편으로, 판다와 NumPy의 주요 트릭은 for d in... 문법을 사용하여 모든 데이터 세트에서 반복을 피하는 것입니다. 이것은 NumPy (판다를 기본으로하는) 가 벡터화 된 연산으로 루핑을 최적화하기 때문입니다. 따라서 판다를 사용할 때 직접 반복을 거의 (혹은 전혀!) 볼 수 없습니다.

포트폴리오 클래스의 목표는 궁극적으로 거래의 순서와 주식 곡선을 생성하는 것입니다. 성과 클래스가 분석 할 것입니다. 이를 달성하기 위해서는 전략 객체에서 거래 권고 목록을 제공해야합니다. 나중에 이것은 전략 객체의 그룹이 될 것입니다.

포트폴리오 클래스는 특정 거래 신호 세트에 자본이 어떻게 배치되어야하는지, 거래 비용을 처리하는 방법과 어떤 형태의 오더가 사용될지 알려져야합니다. 전략 객체는 데이터 바에 작동하고 따라서 오더 실행에 달성 된 가격에 대해 가정해야합니다. 모든 바의 높은 / 낮은 가격이 사전에 알려지지 않기 때문에 거래에 대한 오픈 및 클로즈 가격만을 사용할 수 있습니다. 실제로 시장 오더를 사용할 때 이러한 특정 가격 중 하나에서 오더가 채워질 것을 보장하는 것은 불가능하므로 가장 좋은 경우에는 근사일 것입니다.

명령어 채용에 대한 가정 외에도, 이 백테스터는 마진/브로커리지 제약에 대한 모든 개념을 무시하고 유동성 제약 없이 자유롭게 모든 도구에 대해 길고 짧게 갈 수 있다고 가정합니다. 이것은 분명히 매우 비현실적인 가정이지만 나중에 완화 될 수 있습니다.

다음 목록은 계속됩니다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

이 경우 전략은 돈을 잃었습니다. 예측자의 스토카스틱 성격을 감안할 때 놀라운 일이 아닙니다. 다음 단계는 포트폴리오 인스턴스를 받아들이고 전략을 필터링 할 것인지 아닌지 결정하는 데 기반을 둔 성능 메트릭의 목록을 제공하는 성능 객체를 만드는 것입니다.

우리는 또한 포트폴리오 객체를 개선하여 트랜잭션 비용 (인터랙티브 브로커 수수료 및 미끄러짐과 같은) 을 보다 현실적으로 처리 할 수 있습니다. 우리는 또한 전략 객체에 예측 엔진을 간단하게 포함 할 수 있습니다. 이는 (희망) 더 나은 결과를 가져올 것입니다. 다음 기사에서는 이러한 개념을 더 깊이 탐구 할 것입니다.


더 많은