ステップごとにイベント駆動型Pythonバックテストを行います。 パート3





以前の記事では、イベント指向のバックテストシステムとは何かについて説明し、そのために開発する必要があるクラス階層を整理し、そのようなシステムが過去のテストと取引所での「ライブ」作業において市場データをどのように使用するかについて説明しました。



今日は、ポートフォリオ内のポジションの追跡を担当し、着信信号に基づいて限られた数の株式で注文を生成するNaivePortfolioオブジェクトについて説明します。



位置の追跡と注文の操作



注文管理システムは、イベント指向のバックテスターの最も複雑なコンポーネントの1つです。 その役割は、現在の市場ポジションとその市場価値を追跡することです。 したがって、バックテスターの対応するコンポーネントから取得したデータに基づいて、位置の残差値が計算されます。



ポートフォリオコンポーネントは、ポジションの分析に加えて、リスク要因を考慮し、ポジションのサイズを決定する手法を適用できる必要があります。これは、ブローカーのトレーディングシステムを通じて市場に送信される注文を最適化するために必要です。



Portfolio



オブジェクトは、 SignalEvent



オブジェクトを処理し、 SignalEvent



オブジェクトを生成し、 OrderEvent



オブジェクトを解釈して位置を更新できる必要があります。 したがって、Portfolioオブジェクトが通常、コード行の点でバックテストシステムの最も膨大な要素であることは驚くことではありません。



実装



新しいportfolio.pyファイルを作成し、必要なライブラリをインポートしましょう-以前に使用した抽象基本クラスの同じ実装です。 整数次数を生成するには、数学ライブラリからフロア関数をインポートする必要があります。 FillEvent



OrderEvent



も必要ですOrderEvent



オブジェクトはそれぞれを処理します。



 # portfolio.py import datetime import numpy as np import pandas as pd import Queue from abc import ABCMeta, abstractmethod from math import floor from event import FillEvent, OrderEvent
      
      





Portfolioの抽象基本クラスと、2つの抽象メソッドupdate_signal



およびupdate_fill



ます。 前者はイベントキューから取得された新しい取引シグナルを処理し、後者は処理オブジェクトのエンジンから受信した実行済み注文に関する情報を処理します。



 # portfolio.py class Portfolio(object): """  Portfolio          : , , 5 , 30 , 60   . """ __metaclass__ = ABCMeta @abstractmethod def update_signal(self, event): """  SignalEvent         . """ raise NotImplementedError("Should implement update_signal()") @abstractmethod def update_fill(self, event): """           FillEvent. """ raise NotImplementedError("Should implement update_fill()")
      
      





この記事の主なオブジェクトは、 NaivePortfolio



クラスです。 これは、ポジションと予約済みファンドのサイズを計算し、取引注文を簡単な方法で処理するために作成されました-特定の株式数でそれらを証券取引システムに送信するだけです。 現実の世界では、すべてがより複雑ですが、このような単純化は、ポートフォリオ注文処理システムがイベント指向製品でどのように機能するかを理解するのに役立ちます。



NaivePortfolio



は初期資本が必要です-例では、100,000ドルに設定されています。 また、仕事を開始する日時を設定する必要があります。



ポートフォリオには、 all_positions



およびcurrent_positions



が含まれcurrent_positions



。 最初の要素は、市場イベントのタイムスタンプによって記録された以前のすべてのポジションのリストを保存します。 ポジションは、単に金融商品の額です。 マイナスのポジションは、株式が「ショート」で売却されることを意味します。 2番目の要素には、バーの最終更新の現在の位置を含む辞書が格納されます。



ポートフォリオは、ポジションを担当する要素に加えて、オープンポジション(保有)の現在の市場価値に関する情報を保存します。 この場合の「現在の市場価値」とは、現在のバーから得られる終値を意味します。これは近似値ですが、現時点では非常に妥当です。 all_holdings



要素には、すべてのポジションのコストの履歴リストが格納され、 current_holdings



は値の最新の辞書が格納されます。



 # portfolio.py class NaivePortfolio(Portfolio): """  NaivePortfolio    (..   -)    /   ,   .       BuyAndHoldStrategy. """ def __init__(self, bars, events, start_date, initial_capital=100000.0): """          .           ( ,     ). 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メソッドは、各金融商品の辞書を作成し、各値をゼロに設定してから、日付と時刻のキーを追加します。 Python辞書ジェネレーターを使用しました。



 # portfolio.py def construct_all_positions(self): """   ,  start_date   ,      . """ d = dict( (k,v) for k, v in [(s, 0) for s in self.symbol_list] ) d['datetime'] = self.start_date return [d]
      
      





construct_all_hldings



メソッドは上記のメソッドと似ていますが、利用可能な資金、手数料、取引後の口座の残高、支払った手数料の合計、利用可能な資産の合計額(オープンポジションとマネー)の追加キーを追加します。 ショートポジションは「マイナス」とみなされます。 starting cash



total account



の値は、初期資本と同じです



 # portfolio.py def construct_all_holdings(self): """      ,  start_date   ,      . """ 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): """  ,         . """ 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



オブジェクトからの市場データの要求ごとに、ポートフォリオは保有ポジションの現在の市場価値を更新する必要があります。 実際の取引シナリオでは、この情報は仲介システムから直接ダウンロードして解析できますが、バックテストシステムの場合、これらの値は個別に計算する必要があります。



残念ながら、ビッド/アスクのスプレッドと流動性により、「現在の市場価値」などは存在しません。 したがって、保有する資産の量に「価格」を乗じて評価する必要があります。 この例では、以前に受け取ったバーの終値が使用されます。日中の取引戦略では、これはかなり現実的なアプローチですが、1日以上の期間の取引では、始値が次のバーの始値と大きく異なる可能性があるため、すべてがそれほど妥当ではありません。



update_timeindex



メソッドupdate_timeindex



、新しいポジションの現在の値を処理します。 彼は市場データプロセッサから最新の価格を取得し、現在のポジションを表す新しいディクショナリのディクショナリを作成し、「新しい」ポジションを「現在の」ポジションと同一視します。 このスキーマは、 FillEvent



ときにのみ変更されます。 その後、メソッドall_positions



現在の位置all_positions



セットをall_positions



リストにall_positions



ます。 次に、現在値の値が同様の方法で更新されますが、市場価値は、現在のポジションの数に最後のバーの終値を掛けることによって計算されます( self.current_positions[s] * bars[s][0][5]



)。 取得された新しい値は、 all_holdings



リストに追加されます。



 # portfolio.py def update_timeindex(self, event): """           .   , ..        (OLHCVI).  MarketEvent   . """ 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



何でFillEvent



(買いまたは売り)を正確に判別し、対応する数の共有を追加または削除することでcurrent_positions辞書を更新します。



 # portfolio.py def update_positions_from_fill(self, fill): """   FillEvent     ,     . 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 #     self.current_positions[fill.symbol] += fill_dir*fill.quantity
      
      





対応するupdate_holdings_from_fill



メソッドupdate_holdings_from_fill



上記のメソッドupdate_holdings_from_fill



似ていますが、 保有値を更新します。 実行コストをシミュレートするために、このメソッドはFillEvent



関連付けられた価格を使用しません。 これはなぜですか? バックテスト環境では、執行価格は実際には不明であるため、想定する必要があります。 したがって、行使価格は「現在の市場価格」(最後のバーの終値)として設定されます。 特定の商品の現在のポジションの値は、執行価格に注文の有価証券の数を乗じたものと等しくなります。



行使価格を決定した後、保有の現在価値、利用可能な資金、および合計額を更新できます。 一般委員会も更新されます。



 # portfolio.py def update_holdings_from_fill(self, fill): """   FillEvent    holdings   . : fill -  FillEvent,    . """ # 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



の抽象update_fill



メソッドが実装されます。 前の2つのメソッドupdate_positions_from_fill



update_holdings_from_fill



実行するだけupdate_holdings_from_fill







 # portfolio.py def update_fill(self, event): """            FillEvent. """ if event.type == 'FILL': self.update_positions_from_fill(event) self.update_holdings_from_fill(event)
      
      





Portfolioオブジェクトは、FillEventイベントを発生させるだけでなく、 SignalEvent



シグナルイベントを受信したときにOrderEvent



生成するOrderEvent



があります。 generate_naive_orderメソッドは、シグナルを使用してロングまたはショートポジション、ターゲット金融商品を開き、目的の資産の100株で対応する注文を送信します。 ここで100は任意の値です。 実際の取引の過程で、リスク管理システムまたはポジション値を計算するモジュールによって決定されます。 ただし、 NaivePortfolio



では、リスク管理なしで信号を受信した直後に「 NaivePortfolio



」注文を送信できます。



このメソッドは、ロングポジションとショートポジションのオープン、および現在の数量と特定の金融商品に基づいたそれらの終了を処理します。 次に、対応するOrderEvent



オブジェクトがOrderEvent



ます。



 # portfolio.py def generate_naive_order(self, signal): """   OrderEvent           . : signal -   SignalEvent. """ 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): """   SignalEvent        . """ 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): """  pandas DataFrame    all_holdings. """ 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
      
      





Portfolio



オブジェクトは、イベント指向のバックテスター全体の最も複雑な側面です。 複雑さにもかかわらず、ここでのポジションの処理は非常に単純なレベルで実装されています。



次の記事では、イベント指向の歴史的テストシステムの最後の部分である、 OrderEvent



オブジェクトを使用してOrderEvent



を作成するExecutionHandler



オブジェクトについてFillEvent



します。



継続するには...



PSHabréのブログで既に、トレーディングシステムの開発のさまざまな段階を検討しましたITinvestとパートナーは、このトピックに関するオンラインコースを実施しています。



All Articles