パンダとPythonでバックテスト環境を研究する

作者: リン・ハーン優しさ作成日:2019年3月16日 11:58:20 更新日:

バックテスト (Backtesting) は,過去のパフォーマンスを確認するために,取引戦略のアイデアを歴史的データに適用する研究プロセスである.特に,バックテストは,戦略の将来のパフォーマンスについて保証するものではない.しかし,戦略パイプライン研究プロセスの不可欠な要素であり,戦略を生産に入れる前にフィルタリングできるようにする.

この記事 (およびそれ以降の記事) は,Pythonで書かれた基本的なオブジェクト指向バックテストシステムについて説明します.この初期のシステムは主に"教材"であり,バックテストシステムのさまざまなコンポーネントを実証するために使用されます.記事を通して進むにつれて,より洗練された機能が追加されます.

バックテストの概要

強力なバックテストシステムを設計するプロセスは極めて困難である.アルゴリズム取引システムのパフォーマンスに影響を与えるすべてのコンポーネントを効果的にシミュレーションすることは困難である.データ粒度が悪いこと,ブローカーでのオーダールーティングの不透明性,オーダーレイテンシー,および多数の他の要因は,バックテストされたパフォーマンスと比較して戦略の"真の"パフォーマンスを変化させる.

バックテストシステムを開発する際には,パフォーマンスを評価する上で重要な要素が多く見られるため,常に"ゼロから書き直す"ことが誘惑的です.バックテストシステムは決して完成せず,システムによって十分な要素が捉えられたかどうかを判断する必要があります.

これらの懸念を考慮して,ここで提示されたバックテストは,ある程度単純化されます.さらに問題を (ポートフォリオ最適化,リスク管理,取引コスト管理) 調べると,バックテストはより堅牢になります.

バックテスト システムの種類

バックテストシステムには,一般的に2種類の種類があります.最初のものは研究ベースのもので,主に初期段階で使用され,より深刻な評価のために選択するために多くの戦略がテストされます.これらの研究バックテストシステムは,この段階で開発速度が実行速度よりも重要であるため,Python,RまたはMatLabで書かれています.

2番目のタイプのバックテストシステムはイベントベースのものである.つまり,バックテストプロセスは,取引実行システム自体と同様の (同一でない場合でも) 実行ループで実行される.戦略のより厳格な評価を提供するために,市場データとオーダー実行プロセスを現実的にモデル化する.

後者のシステムは,実行速度が不可欠なC++やJavaなどの高性能言語で書かれることが多い.低周波戦略 (それでも日中) では,この文脈で使用するにはPythonが十分である.

Python でのオブジェクト指向研究バックテスト

現在,オブジェクト指向研究ベースのバックテスト環境の設計と実装について議論します.オブジェクト指向は以下の理由からソフトウェア設計パラダイムとして選択されています.

  • 各コンポーネントのインターフェースは事前に指定できるが,各コンポーネントの内部はプロジェクトが進むにつれて変更 (または交換) することができる.
  • インターフェースを事前に指定することで,各コンポーネントの動作を効果的にテストすることができます (ユニットテストを通じて)
  • システムを拡張すると,新しいコンポーネントが他のコンポーネントの上に構築され,または他のコンポーネントに追加され,継承または組成によって作られる.

この段階では,バックテストは,実際の市場精度を犠牲にして,実装の容易さと合理的な柔軟性のために設計されています.特に,このバックテストは,単一の楽器で動作する戦略のみを処理することができます.後で,バックテストは,一連の楽器を処理するために変更されます.初期バックテストには,以下のコンポーネントが必要です:

  • 戦略 - 戦略クラスは,特定の周波数でオープン・ハイ・ロー・クローズ・ボリューム (OHLCV) データポイントのリストであるバーのパンダスデータフレームを受け取ります. 戦略は,それぞれロング,ホール,ショート信号を示すタイムスタンプとセットの要素からなるシグナルの一覧を生成します.
  • ポートフォリオ - バックテストの大部分はポートフォリオクラスで行われる.それはシグナルの一組 (上記のように) を受信し,キャッシュコンポーネントに対して配分された一連のポジションを作成する.ポートフォリオオブジェクトの仕事は,株式曲線を作成し,基本的な取引コストを組み込み,取引を追跡することである.
  • パフォーマンス - パフォーマンスオブジェクトは,ポートフォリオを取り,そのパフォーマンスに関する統計のセットを作成します.特に,リスク/リターン特性 (シャープ,ソルティーノおよび情報比),取引/利益指標および引き上げ情報を出力します.

欠けているのは何?

このバックテストには,ポートフォリオ/リスク管理,実行処理 (すなわち制限オーダーなし) に言及していないし,トランザクションコストの洗練されたモデリングも提供していない.この段階ではあまり問題ではありません.オブジェクト指向バックテストとPandas/NumPyライブラリを作成するプロセスを熟知できるようにします.時間とともに改善されます.

実施

各オブジェクトの実装を概要します.

戦略

戦略オブジェクトは,予測,平均逆転,モメンタム,変動戦略を扱うため,この段階ではかなり汎用的である必要があります.ここで検討されている戦略は,常に時間系列に基づきます.このバックテストの初期要件は,派生戦略クラスは,ティック (取引ごとに価格) またはオーダーブックデータではなく,入力としてバーリスト (OHLCV) を受け入れることです.したがって,ここで検討されている最も細かな粒子は1秒バーになります.

戦略クラスは常に信号の推奨も生成します.これは,ポートフォリオインスタンスをロング/ショートまたはポジションを保持するという意味で助言することを意味します.この柔軟性により,複数の戦略アドバイザーを作成することができ,より高度なポートフォリオクラスが実際のポジションを決定するために受け入れることができる一連の信号を提供します.

クラスのインターフェースは,抽象的なベースクラスの方法論を利用して強制される.抽象的なベースクラスはインスタンスタイズできないオブジェクトであり,したがって派生クラスのみを作成することができる. Python コードは,以下にファイルで示されている.backtest.py. 戦略クラスは,どのサブクラスが generate_signals メソッドを実装することを要求します.

クラスのプロパティを設定します.このプロパティは,abc モジュールからABCMetaとabstractmethodオブジェクトを使用する必要があります.メタクラス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()!")

上記のインターフェースはシンプルですが,このクラスがそれぞれの特定のタイプの戦略に継承された場合,より複雑になります.この設定における戦略クラスの最終目標は,ポートフォリオに送信される各ツールのロング/ショート/ホールド信号のリストを提供することです.

ポートフォリオ

ポートフォリオクラスは,取引論理の大部分を占める.この調査バックテストでは,ポートフォリオは,ポジションサイズ,リスク分析,取引コスト管理,実行処理 (すなわち市場開設,市場閉じるオーダー) を決定する責任を負います.後期には,これらのタスクは別々のコンポーネントに分割されます.今のところ,それらは1つのクラスにロールされます.

このクラスはパンダを広く利用し,ライブラリが膨大な時間を節約できる素晴らしい例を提供します.特に"ボイラープレート"データ争いに関して.また,パンダとNumPyの主なトリックは,for d in...構文を使用して任意のデータセットを繰り返さないことです.これはNumPy (パンダの基礎となる) がベクトル化操作によってループを最適化するためです.したがって,パンダを使用するときに直接的な繰り返しはほとんどありません.

ポートフォリオクラスの目的は,最終的に取引の順序と株式曲線を生成することであり,これはパフォーマンスクラスによって分析される.これを達成するためには,戦略オブジェクトからの取引勧告のリストが提供されなければならない.後で,これは戦略オブジェクトのグループになります.

ポートフォリオクラスは,特定の一連の取引信号に対して資本がどのように配置され,取引コストをどのように処理し,どの形式のオーダーが利用されるかを伝えなければならない. 戦略オブジェクトはデータバーで動作しており,したがって,オーダーの実行時に達成された価格に関して仮定する必要があります.任意のバーの高値/低値が事前に知られていないため,取引のために開閉価格のみを使用することが可能です.実際には,市場オーダーを使用するときに,この特定の価格の1つでオーダーが満たされると保証することは不可能です.したがって,最良の場合は,近似になります.

このバックテストは,オーダーが満たされるという仮定に加えて,すべてのマージン/ブローカージー制約の概念を無視し,流動性制約なしに任意の楽器で自由にロングとショートに行くことが可能であると仮定します.これは明らかに非常に非現実的な仮定ですが,後で緩和することができます.

次のリストは続きます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()!")

この段階では,戦略とポートフォリオ抽象的なベースクラスが導入されました.現在,機能する"おもちゃ戦略"を作成するために,これらのクラスの具体的な派生実装を生成することができます.

ランダム・フォレックス・ストラテジーと呼ばれる戦略のサブクラスを生成して始めます.その唯一の仕事はランダムに選択されたロング/ショート・シグナルを生成することです! これは明らかに無意味な取引戦略ですが,オブジェクト指向バックテストフレームワークを実証することで私たちのニーズを満たします.したがって,ランダム_フォレックス・ストラテジーと呼ばれる新しいファイルを開始します.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

現在,我々は"具体的"予測システムを持っているので,ポートフォリオオブジェクトの実装を作成する必要があります.このオブジェクトはバックテストコードの大部分をカバーします.これは,2つの別々のデータフレームを作成するように設計されています.そのうちの1つはポジションフレームで,特定のバーで保持されている各ツールの量を保存するために使用されます.第2のポートフォリオは,実際に各バーのすべての保有品の市場価格,および初期資本を想定した現金総額を含みます.これは最終的に戦略のパフォーマンスを評価するための株式曲線を提供します.

ポートフォリオオブジェクトは,そのインターフェースでは非常に柔軟ですが,取引コスト,マーケットオーダーなどに対処する方法に関して特定の選択を必要とします.この基本的な例では,制限や手間なしで簡単に楽器をロング/ショートしたり,バーのオープン価格で直接購入または販売したり,ゼロの取引コスト (スリップ,手数料,市場影響を含む) を指定し,各取引で直接購入するストック量を指定しました.

これは 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

この例では,戦略はお金を失った.予測者のストーカスティックな性質を考えると驚くことではありません.次のステップは,ポートフォリオインスタンスを受け入れ,戦略をフィルタリングするか否かの決定をベースとするパフォーマンスメトリックのリストを提供するパフォーマンスオブジェクトを作成することです.

Portfolio オブジェクトを改良することで,トランザクションコスト (Interactive Brokers の佣金やスリップなど) をより現実的に処理できます.また,予測エンジンを戦略 オブジェクトに直接組み込むこともできます.これは (希望) より良い結果をもたらすでしょう.次の記事では,これらのコンセプトをより深く調べます.


もっと