Backtraderの内部アーキテクチャとコアロジックの深掘り

Backtraderの概要と特徴

Backtraderは2015年にオープンソースとしてリリースされたPythonベースのアルゴリズム取引およびバックテストフレームワークです。バックテストだけでなく、実際のリアルタイム取引(ライブトレード)にも対応しており、その柔軟性と拡張性の高さから多くの開発者に採用されています。このフレームワークは、コードの簡潔さと美しさを重視した設計になっており、以下のような多様な機能を備えています。

  • 対応資産の多様性: 株式、先物、オプション、外国為替、仮想通貨など、幅広い金融商品をサポートしています。
  • タイムフレームの網羅: ティックデータや秒足といった高頻度データから、日足、週足、月足、年足まで、あらゆる時間軸での分析が可能です。
  • 高速な処理性能: Pandasによるベクトル演算やマルチストラテジー並列処理を活用し、高速なバックテストを実現します。
  • 豊富な組み込みコンポーネント: Ta-libベースのテクニカル指標、PyFlioを用いたポートフォリオ分析、描画機能、パラメータ最適化ツールなどが標準装備されています。
  • 柔軟なカスタマイズ性: 既存のコンポーネントを組み合わせるだけでなく、独自の機能や指標を拡張して実装することが容易です。

コアコンポーネントの動作フロー

Backtraderの中核を担うのは「DataFeed」と「Strategy」という2つのコンポーネントです。DataFeedは市場データ(価格情報など)を継続的に取り込み、Strategyはそのデータに基づいて指標計算を行い、売買シグナル(発注)を生成します。これら全体を統括し、実行サイクルを制御するのが「Cerebro」です。Cerebroによる処理の流れを擬似コードで表現すると以下のようになります。

class CerebroEngine:
    def run_cycle(self):
        while True:
            # 全データフィードの更新処理
            for feed in self.data_feeds:
                feed.update()  # 次のOHLCバー(ローソク足)を取得

            # 各ストラテジーのロジック実行
            for strategy in self.active_strategies:
                if strategy.is_data_ready():
                    strategy.next()

ソースコードレベルでの実行制御

実際のソースコードにおいて、Cerebroの`run`メソッド内ではストラテジーのイテレータが作成され、実行可能なストラテジーが順次処理されます。以下に、その内部的な処理の流れを簡略化したコード例を示します。

def execute_strategies(self):
    # ストラテジーの初期化とイテレータの生成
    strategy_queue = self.prepare_strategies()

    for instance in strategy_queue:
        self.run_strategy(instance)

    # ...(中略)...

def run_strategy(self, instance):
    # 各ストラテジーの次のステップへ進む処理
    instance.advance_stage()

ストラテジーの`advance_stage`メソッド(ソースコード内では`_next`に相当)では、現在のデータ量と計算に必要な最小期間を比較し、実際のロジック部分である`next`メソッドを呼び出すかどうかを判定します。

def advance_stage(self):
    # 必要なデータ量が揃っているか確認
    if self._check_minimum_period() >= 0:
        self.next()

各ストラテジーインスタンスは、参照している各銘柄ごとに最小期間(`minperiod`)を持っています。銘柄ごとに蓄積されたデータ長がこの最小期間を超えて初めて、`next`メソッド内に記述された売買ロジックが実行されます。これにより、指標計算のために十分なデータが存在しない状態での誤作動を防いでいます。

LineBufferと時系列データの管理

Backtraderにおけるデータ管理では、1つの銘柄(オブジェクト)が複数の「Line」を保持します。「Line」とは、時間軸上のデータ点(値)を保持する配列であり、内部的には`LineBuffer`オブジェクトとして実装されています。`LineBuffer`は配列操作や比較演算子などのオーバーロードを実装しており、`line[0]`(現在の値)や`line[-1]`(1つ前の値)といった直感的なアクセスを可能にしています。

以下に、LineBufferの挙動を確認するための実装例を示します。

import backtrader as bt
import random

class CustomDataSet(bt.DataSeries):
    # 3つのライン(バッファ)を定義
    lines = ('signal_a', 'signal_b', 'metric',)

def demonstrate_linebuffer_usage():
    # データ系列のインスタンス化
    dataset = CustomDataSet()

    # データの生成とバッファへの格納
    for i in range(10):
        dataset.signal_a.advance()  # 時間を1つ進める (forwardと同義の概念)
        dataset.signal_a[0] = round(random.uniform(7, 10), 2)

    # 特定の値の設定とバックワード操作
    dataset.signal_a.advance()
    dataset.signal_a[0] = 100.0
    dataset.signal_a[-1] = dataset.signal_a[0]  # 前回の値を更新
    dataset.signal_a.rewind()  # ポインタを戻す (backwards相当)

    # 個別のLineBuffer操作
    buffer = bt.LineBuffer()
    buffer.advance()
    buffer[0] = 1.21
    buffer.advance()
    buffer[0] = 1.22

このように、データオブジェクトは複数のLineBufferを持ち、データが供給されるたびに内部的なポインタを進めながら値を格納していきます。ライブトレードにおいては、Feedから継続的にデータが配信される限り、このサイクルが無限に繰り返されます。LineBuffer、Pandas、Ta-libを組み合わせることで、独自の簡易的なトレーディングシステムを構築することも可能です。

ライブトレード時の注意点

Backtraderは、バックテスト時もライブトレード時も基本的にシングルコア(シングルスレッド)で動作します(パラメータ最適化時を除く)。CPUの処理能力には限界があるため、シグナルが発生した際に即座に注文を出し、取引所への通信遅延やスリッページを最小限に抑えるためには、ストラテジーの`next`メソッド内での計算処理を可能な限り軽くする必要があります。

例えば、複数のティックデータからの足(バー)合成処理などをストラテジー内で行うと、計算負荷が高まりレスポンスが悪化するため、そのようなデータ加工は事前に済ませておくか、DataFeed側で処理することが推奨されます。また、ストラテジーの`next`メソッドが呼び出されない場合、多くの場合は必要な最小期間データが溜まっていないか、データフィードの接続に問題があります。以下のようなロジックで、データの準備状況が確認されています。

def check_period_status(self):
    # 各データの必要最小期間と実際の長さの差分を計算
    required_lengths = [len(d) - self.min_periods[i] for i, d in enumerate(self.datas)]
    
    # 全てのデータ準備が整っているかを判定
    self.ready_status = max(required_lengths)

タグ: Python Backtrader AlgorithmicTrading Cerebro TechnicalAnalysis

6月6日 16:44 投稿