イベント駆動バックテスト Python - Part V

作者: リン・ハーン優しさ作成日:2019年3月25日 15:54:16 更新日:

イベント駆動バックテストに関する前の記事では,戦略クラス階層をどのように構築するかについて検討しました.ここで定義されている戦略は,ポートフォリオオブジェクトが注文を送信するかどうかを決定するために使用するシグナルを生成するために使用されます.以前のように,すべての次のサブクラスが継承するポートフォリオ抽象ベースクラス (ABC) を作成することは自然です.

この記事では,ポートフォリオ内のポジションを追跡し,シグナルに基づいて固定量の株のオーダーを生成するNaivePortfolioオブジェクトについて説明します.後のポートフォリオオブジェクトには,より洗練されたリスク管理ツールが含まれ,後の記事のテーマになります.

位置追跡と注文管理

ポートフォリオオーダー管理システムは,おそらくイベント駆動バックテストの最も複雑な構成要素である.その役割は,すべての現在の市場ポジションおよびポジションの市場価値 (ホールディングスとして知られる) を追跡することである.これは単にポジションの清算価値の推定であり,部分的にバックテストのデータ処理施設から派生する.

ポートフォリオは,ポジション管理や保有管理に加えて,リスク要因やポジションサイズのテクニックも把握し,ブローカリングや他の形態の市場アクセスに送信されるオーダーを最適化する必要があります.

イベントクラス等級階層に沿って,ポートフォリオオブジェクトは,SignalEventオブジェクトを処理し,OrderEventオブジェクトを生成し,FillEventオブジェクトを解釈し,ポジションを更新する必要があります.したがって,ポートフォリオオブジェクトがしばしばイベント駆動システムの最大の構成要素であることに驚きはありません.

実施

新しいファイルを作成します.portfolio.pyそして必要なライブラリをインポートします. これらは他の抽象的なベースクラス実装のほとんどと同じです. 整数値のオーダーサイズを生成するために,数学ライブラリからフロア関数をインポートする必要があります. ポートフォリオが両方の処理を行うため,FillEventとOrderEventオブジェクトも必要です.

# portfolio.py

import datetime
import numpy as np
import pandas as pd
import Queue

ABCから輸入 ABCMeta,抽象方法 数学インポートフロアから

イベントインポートから FillEvent, OrderEvent ポートフォリオのABCを作成し,純粋な仮想メソッド update_signalと update_fillを2つ用意します.前者はイベントキューから捕らえた新しい取引信号を処理し,後者は実行ハンドラーオブジェクトから受信されたフィールを処理します.

# portfolio.py

class Portfolio(object):
    """
    The Portfolio class handles the positions and market
    value of all instruments at a resolution of a "bar",
    i.e. secondly, minutely, 5-min, 30-min, 60 min or EOD.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def update_signal(self, event):
        """
        Acts on a SignalEvent to generate new orders 
        based on the portfolio logic.
        """
        raise NotImplementedError("Should implement update_signal()")

    @abstractmethod
    def update_fill(self, event):
        """
        Updates the portfolio current positions and holdings 
        from a FillEvent.
        """
        raise NotImplementedError("Should implement update_fill()")

この記事の主なテーマはNaivePortfolioクラスである.これはポジションサイズと現在の保有を処理するように設計されているが,預金量に関係なく,既定の固定量サイズで直接ブローカージに送信することによって,取引注文を"愚かな"方法で実行する.これらはすべて不現実的な仮定であるが,ポートフォリオオーダー管理システム (OMS) がイベント主導の方法で機能する方法を概要するのに役立ちます.

NaivePortfolioには初期資本額が必要で 標準額は100,000ドルです また 開始日時も必要です

ポートフォリオには,all_positionsとcurrent_positionsメンバーが含まれます.前者は,市場データイベントのタイムスタンプで記録されたすべての以前のポジションのリストを格納します.ポジションは単に資産の量です.負のポジションは資産がショートカットされたことを意味します.後者は,最後の市場バー更新の現在のポジションを含む辞書を格納します.

ポートフォリオは,ポジションメンバーに加えて,保有するポジションの現在の市場価値を記述する保有品を保管します. 現在の市場価値は,現行の市場バーから得られた閉店価格を意味します.これは明らかに近似ですが,現時点では十分合理的です. all_holdingsは,すべてのシンボル保有品の歴史的なリストを保存し,current_holdingsは,すべてのシンボル保有品の最新値の辞書を保存します.

# portfolio.py

class NaivePortfolio(Portfolio):
    """
    The NaivePortfolio object is designed to send orders to
    a brokerage object with a constant quantity size blindly,
    i.e. without any risk management or position sizing. It is
    used to test simpler strategies such as BuyAndHoldStrategy.
    """
    
    def __init__(self, bars, events, start_date, initial_capital=100000.0):
        """
        Initialises the portfolio with bars and an event queue. 
        Also includes a starting datetime index and initial capital 
        (USD unless otherwise stated).

        Parameters:
        bars - The DataHandler object with current market data.
        events - The Event Queue object.
        start_date - The start date (bar) of the portfolio.
        initial_capital - The starting capital in USD.
        """
        self.bars = bars
        self.events = events
        self.symbol_list = self.bars.symbol_list
        self.start_date = start_date
        self.initial_capital = initial_capital
        
        self.all_positions = self.construct_all_positions()
        self.current_positions = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )

        self.all_holdings = self.construct_all_holdings()
        self.current_holdings = self.construct_current_holdings()

次の方法,construct_all_positionsは,単に各シンボルの辞書を作成し,それぞれの値をゼロに設定し,その後日付時間キーを追加し,最終的にリストに追加します.辞書理解を使用し,リスト理解の精神に類似しています:

# portfolio.py

    def construct_all_positions(self):
        """
        Constructs the positions list using the start_date
        to determine when the time index will begin.
        """
        d = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        d['datetime'] = self.start_date
        return [d]

コンストラクット_オール_ホールディングス方法は,上記と類似しているが,現金,手数料,総額の追加キーを追加し,それぞれ,購入後の口座の余剰現金,累積された手数料,現金および開いたポジションを含む総口座資本を表す.ショートポジションは負とみなされる.スタート現金と総口座資本は両方が初期資本に設定される:

# portfolio.py

    def construct_all_holdings(self):
        """
        Constructs the holdings list using the start_date
        to determine when the time index will begin.
        """
        d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
        d['datetime'] = self.start_date
        d['cash'] = self.initial_capital
        d['commission'] = 0.0
        d['total'] = self.initial_capital
        return [d]

次の方法,construct_current_holdingsは,辞書をリストに包み込むこと以外は,上記の方法とほぼ同じです.

# portfolio.py

    def construct_current_holdings(self):
        """
        This constructs the dictionary which will hold the instantaneous
        value of the portfolio across all symbols.
        """
        d = dict( (k,v) for k, v in [(s, 0.0) for s in self.symbol_list] )
        d['cash'] = self.initial_capital
        d['commission'] = 0.0
        d['total'] = self.initial_capital
        return d

DataHandler オブジェクトから新しい市場データが要求されるたびに,ポートフォリオは所有するすべてのポジションの現在の市場価値を更新する必要があります.ライブ取引シナリオでは,この情報は直接ブローカージからダウンロードして解析できますが,バックテスト実装のために,これらの値を手動で計算する必要があります.

不幸にも,Bid/Askのスプレッドと流動性の問題により,現在の市場価値など存在しない.したがって,保有資産の量を"価格"で倍して推定することが必要である.ここで私が採用したアプローチは,受信した最後のバーの閉値を使用することです.日中の戦略では,これは比較的現実的です.日々の戦略では,開盤価格が閉盤価格と大幅に異なるので,これは現実的ではありません.

メソッド update_timeindexは,新しいホールディングスの追跡を処理する.まずは,市場データハンドラーから最新の価格を取得し,現在のポジションを表現するための新しいシンボルの辞書を作成し,newポジションをcurrentポジションに等しく設定します.これらのものは,FillEventが得られ,ポートフォリオで後で処理されるときにのみ変更されます.メソッドは,このセットの現在のポジションをall_positionsリストに追加します.次に,現在のポジションを最新のバーの閉値 (self.current_positions[s] * bars[s][0][5]) と倍数することによって市場価値を再計算する例外を除いて,同様の方法でホールディングスを更新します.最後に,新しいホールディングスはall_holdingsに追加されます:

# portfolio.py

    def update_timeindex(self, event):
        """
        Adds a new record to the positions matrix for the current 
        market data bar. This reflects the PREVIOUS bar, i.e. all
        current market data at this stage is known (OLHCVI).

        Makes use of a MarketEvent from the events queue.
        """
        bars = {}
        for sym in self.symbol_list:
            bars[sym] = self.bars.get_latest_bars(sym, N=1)

        # Update positions
        dp = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        dp['datetime'] = bars[self.symbol_list[0]][0][1]

        for s in self.symbol_list:
            dp[s] = self.current_positions[s]

        # Append the current positions
        self.all_positions.append(dp)

        # Update holdings
        dh = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] )
        dh['datetime'] = bars[self.symbol_list[0]][0][1]
        dh['cash'] = self.current_holdings['cash']
        dh['commission'] = self.current_holdings['commission']
        dh['total'] = self.current_holdings['cash']

        for s in self.symbol_list:
            # Approximation to the real value
            market_value = self.current_positions[s] * bars[s][0][5]
            dh[s] = market_value
            dh['total'] += market_value

        # Append the current holdings
        self.all_holdings.append(dh)

メソッド update_positions_from_fill は FillEvent が Buy か Sell かを決定し,正しい数のシェアを足し/引いて current_positions 辞書をそれに応じて更新します.

# portfolio.py

    def update_positions_from_fill(self, fill):
        """
        Takes a FilltEvent object and updates the position matrix
        to reflect the new position.

        Parameters:
        fill - The FillEvent object to update the positions with.
        """
        # Check whether the fill is a buy or sell
        fill_dir = 0
        if fill.direction == 'BUY':
            fill_dir = 1
        if fill.direction == 'SELL':
            fill_dir = -1

        # Update positions list with new quantities
        self.current_positions[fill.symbol] += fill_dir*fill.quantity

対応する update_holdings_from_fill は,上記の方法と類似しているが,その代わりにホールディング値を更新する. FillEvent のコストをシミュレートするために,次の方法では FillEvent に関連するコストを使用しない.なぜか.単純に言えば,バックテスト環境では,フィルティングコストは実際には不明であり,したがって推定されなければならない.したがって,フィルティングコストは 現在の市場価格 (最後のバーの終了価格) に設定される.特定のシンボルのホールディングは,フィルティングコストをトランザクション量に掛けるものに等しいと設定される.

充填コストがわかると,現在の保有量,現金,総額がすべて更新できます.累積手数料も更新されます:

# portfolio.py

    def update_holdings_from_fill(self, fill):
        """
        Takes a FillEvent object and updates the holdings matrix
        to reflect the holdings value.

        Parameters:
        fill - The FillEvent object to update the holdings with.
        """
        # Check whether the fill is a buy or sell
        fill_dir = 0
        if fill.direction == 'BUY':
            fill_dir = 1
        if fill.direction == 'SELL':
            fill_dir = -1

        # Update holdings list with new quantities
        fill_cost = self.bars.get_latest_bars(fill.symbol)[0][5]  # Close price
        cost = fill_dir * fill_cost * fill.quantity
        self.current_holdings[fill.symbol] += cost
        self.current_holdings['commission'] += fill.commission
        self.current_holdings['cash'] -= (cost + fill.commission)
        self.current_holdings['total'] -= (cost + fill.commission)

Portfolio ABC からの純粋仮想更新_fill メソッドがここで実装されています.それは,上記で既に議論されている,前述の2つのメソッド,update_positions_from_fill と update_holdings_from_fill を実行するだけです:

# portfolio.py

    def update_fill(self, event):
        """
        Updates the portfolio current positions and holdings 
        from a FillEvent.
        """
        if event.type == 'FILL':
            self.update_positions_from_fill(event)
            self.update_holdings_from_fill(event)

ポートフォリオオブジェクトは,FillEventsを処理しなければならないが,SignalEventsの1つまたは複数の受信時にOrderEventsを生成することにも注意しなければならない. generate_naive_orderメソッドは,単に資産をロングまたはショートにシグナルを取って,そのような資産の100株に対してそうするように命令を送信する.明らかに100は任意の値である.現実的な実装では,この値はリスク管理またはポジションサイズオーバーレイによって決定される.しかし,これはNaivePortfolioであり,リスクシステムなしでシグナルから直接すべてのオーダーを送信する.

この方法は,現在の量と特定のシンボルに基づいて,ポジションの待ち望,ショート,終了を処理します.それに対応する OrderEvent オブジェクトが生成されます:

# portfolio.py

    def generate_naive_order(self, signal):
        """
        Simply transacts an OrderEvent object as a constant quantity
        sizing of the signal object, without risk management or
        position sizing considerations.

        Parameters:
        signal - The SignalEvent signal information.
        """
        order = None

        symbol = signal.symbol
        direction = signal.signal_type
        strength = signal.strength

        mkt_quantity = floor(100 * strength)
        cur_quantity = self.current_positions[symbol]
        order_type = 'MKT'

        if direction == 'LONG' and cur_quantity == 0:
            order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY')
        if direction == 'SHORT' and cur_quantity == 0:
            order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL')   
    
        if direction == 'EXIT' and cur_quantity > 0:
            order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL')
        if direction == 'EXIT' and cur_quantity < 0:
            order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY')
        return order

update_signal メソッドは上記のメソッドを呼び出し,生成された順序をイベントキューに追加します.

# portfolio.py

    def update_signal(self, event):
        """
        Acts on a SignalEvent to generate new orders 
        based on the portfolio logic.
        """
        if event.type == 'SIGNAL':
            order_event = self.generate_naive_order(event)
            self.events.put(order_event)

NaivePortfolioの最終方法は,株式曲線を生成することです.これは単に業績計算に有用なリターンストリームを作成し,その後,株式曲線をパーセントベースに正規化します.したがって,アカウントの初期サイズは1.0に等しいです:

# portfolio.py

    def create_equity_curve_dataframe(self):
        """
        Creates a pandas DataFrame from the all_holdings
        list of dictionaries.
        """
        curve = pd.DataFrame(self.all_holdings)
        curve.set_index('datetime', inplace=True)
        curve['returns'] = curve['total'].pct_change()
        curve['equity_curve'] = (1.0+curve['returns']).cumprod()
        self.equity_curve = curve

ポートフォリオオブジェクトは,イベント駆動バックテストシステム全体の最も複雑な側面である.この実装は複雑であるが,ポジションの処理においては比較的基本的なものである.後のバージョンではリスク管理とポジションサイズ化が考慮され,戦略パフォーマンスのより現実的な考えにつながります.

次の記事では,イベント駆動バックテストの最終的な部分,すなわち OrderEvent オブジェクトを取り,そこから FillEvent オブジェクトを作成するために使用される ExecutionHandler オブジェクトを検討します.


もっと