Bot

開発記録#145(2025/3/25)「論文ベースのbot開発フローpart.7 バックテストの結果分析」

2025年3月25日

前回の記事に引き続き、今回も仮想通貨botの開発状況をまとめていきます。

本記事では「暗号通貨のパンプ&ダンプスキームの検出」に関する論文をベースにbot開発の過程をまとめていきます。

パフォーマンス評価とパラメータ調整

次のステップでは、バックテストの結果を分析し、最適なパラメータを見つけるための評価システムを構築します。
具体的には、以下の内容を実装します:


1. 評価とパラメータ調整のアプローチ

🔎 評価指標の選定

以下の評価指標を基に、最適な取引戦略のパラメータを調整します。

勝率 (Win Rate):全トレードのうち、勝ちトレードの割合
損益 (PnL: Profit and Loss):最終的な利益と損失
最大ドローダウン (Max Drawdown):過去最高残高からの最大下落率
リスク・リワード比率 (Risk-Reward Ratio):1回のトレードでのリスクと利益のバランス
シャープレシオ (Sharpe Ratio):リスク調整後のリターン評価


⚙️ パラメータの最適化対象

以下の3つのパラメータを最適化します。

STOP_LOSS_PERCENT (損切り幅)
TAKE_PROFIT_PERCENT (利益確定幅)
TRADE_AMOUNT (1回のトレード額)


2. コード実装 (parameter_optimizer.py)

import pandas as pd
import numpy as np
from scipy.optimize import minimize
import matplotlib.pyplot as plt
from backtest_engine import backtest, evaluate_performance, load_data  # 既存のバックテスト関数を活用

# ==============================
# 評価指標の定義
# ==============================
def calculate_sharpe_ratio(pnl_series, risk_free_rate=0.01):
    daily_return = pnl_series.pct_change().dropna()
    excess_return = daily_return - risk_free_rate / 252 #金融市場が年間で平均的に営業している日数で割る
    sharpe_ratio = np.mean(excess_return) / np.std(excess_return)
    return sharpe_ratio

# ==============================
# 最適化関数
# ==============================
def optimize_parameters(df, pnd_events):
    def objective(params):
        stop_loss, take_profit, trade_amount = params
        trades_df, final_balance = backtest(df, pnd_events, 
                                            stop_loss=stop_loss, 
                                            take_profit=take_profit, 
                                            trade_amount=trade_amount)
        
        # シャープレシオを最大化
        sharpe_ratio = calculate_sharpe_ratio(trades_df['PnL'].cumsum())
        return -sharpe_ratio  # minimize()を使用するため符号を反転

    # パラメータの初期値と範囲
    initial_params = [0.03, 0.05, 100]  # [STOP_LOSS, TAKE_PROFIT, TRADE_AMOUNT]
    bounds = [(0.01, 0.1), (0.03, 0.1), (50, 500)]

    result = minimize(objective, initial_params, bounds=bounds, method='L-BFGS-B')

    return result.x  # 最適なパラメータを返却

# ==============================
# メイン処理
# ==============================
def main():
    df = load_data('./data/BTCUSDT_data.csv')
    pnd_events = pd.read_csv('./data/pnd_events.csv')

    # 最適化
    best_params = optimize_parameters(df, pnd_events)
    stop_loss, take_profit, trade_amount = best_params

    print("\n=== 最適パラメータ ===")
    print(f"損切り (STOP LOSS): {stop_loss:.2%}")
    print(f"利益確定 (TAKE PROFIT): {take_profit:.2%}")
    print(f"1回のトレード額 (TRADE AMOUNT): ${trade_amount:.2f}")

    # 最適パラメータでの再評価
    trades_df, final_balance = backtest(df, pnd_events,
                                        stop_loss=stop_loss,
                                        take_profit=take_profit,
                                        trade_amount=trade_amount)
    
    evaluate_performance(trades_df, 10000)  # 初期残高 = 10000ドル

if __name__ == "__main__":
    main()

補足

補足

仮想通貨市場の場合は、確かに通常の株式市場と異なり、年中無休で24時間取引が行われています。したがって、年間の営業日数を252日とするのは仮想通貨市場には適さないため、365日を基準にするのがより適切です。仮想通貨の取引データを分析する際にリスクフリーレートを日割りで適用する場合は、年率のリスクフリーレートを365で割ることによって、1日あたりのリスクフリーレートを求めるべきです。これにより、リアルタイムで運営される仮想通貨市場の実態に合ったリスク調整リターンの計算が可能となります。

# ==============================
# 評価指標の定義
# ==============================
def calculate_sharpe_ratio(pnl_series, risk_free_rate=0.01):
    daily_return = pnl_series.pct_change().dropna()
    excess_return = daily_return - risk_free_rate / 252 # 252で割る
    sharpe_ratio = np.mean(excess_return) / np.std(excess_return)
    return sharpe_ratio

したがって、仮想通貨のバックテストやパフォーマンス評価を行う際には、上記のコードで行っているシャープレシオの計算式を以下のように調整することが推奨されます。

def calculate_sharpe_ratio(pnl_series, risk_free_rate=0.01):
    daily_return = pnl_series.pct_change().dropna()
    excess_return = daily_return - risk_free_rate / 365  # 365で割る
    sharpe_ratio = np.mean(excess_return) / np.std(excess_return)
    return sharpe_ratio

この変更により、仮想通貨市場の実際の運用環境をより正確に反映したリスク調整リターンの評価が可能になります。

3. 実行方法

1.scipyのインストール

pip install scipy

2.スクリプトの実行

python parameter_optimizer.py

4. 出力結果の例

=== 最適パラメータ ===
損切り (STOP LOSS): 4.20%
利益確定 (TAKE PROFIT): 6.80%
1回のトレード額 (TRADE AMOUNT): $150.00

=== バックテスト結果 ===
総トレード数: 30
勝率: 65.00%
最終残高: $12800.00
最大ドローダウン: 6.00%
総損益 (PnL): $2800.00

5. 機能のポイント

自動パラメータ最適化scipyminimize()を活用し、シャープレシオを最大化
複数の評価指標:PnL、勝率、ドローダウン、シャープレシオを併用
柔軟なパラメータ範囲設定:リスクとリターンのバランス調整が可能
再評価シミュレーション:最適化されたパラメータで再度バックテストを実行


6.コードの解説

Yodaka

それでは、コードを順を追って説明していきましょう。

まず、このコードの最初にいくつかの必要なライブラリがインポートされています。pandasnumpyはデータの操作や数値計算に非常に便利です。scipyminimize関数は、数値最適化のために用いられています。そしてmatplotlib.pyplotはデータの可視化に使われることが多いですね。最後に、backtest_engineからはバックテストのための関数がインポートされています。

評価指標の定義

このコードでは、シャープレシオというリスク調整後リターンを計算する関数calculate_sharpe_ratioが定義されています。シャープレシオは投資の効率を測るためによく使用される指標で、平均リターンから無リスクリターンを引いた値を標準偏差で割ることで計算されます。ここで、無リスクリターンは年間1%を日割りで適用しています。

最適化関数

次に、optimize_parameters関数が定義されており、トレードのパラメータを最適化するためにこの関数が使われています。具体的には、損切り、利益確定、トレード量の3つのパラメータを最適化しています。minimize関数を使ってシャープレシオを最大にするパラメータの組み合わせを求めています。ここでの目的関数では、シャープレシオの符号を反転させています。これはminimize関数が最小値を探す関数であるため、最大化問題を解くには符号を反転させる必要があるからです。

メイン処理

最後に、main関数ではデータの読み込みから始まり、最適化されたパラメータでのバックテスト、そしてそのパフォーマンスの評価までを行っています。ここで最適なパラメータを実際のデータに適用し、どのようにパフォーマンスが改善されるかを確認しています。

7. 処理フロー

Yodaka

このスクリプトがどのように機能するか、各ステップを詳しく見ていきましょう。

ステップ1: ライブラリのインポート

import pandas as pd
import numpy as np
from scipy.optimize import minimize
import matplotlib.pyplot as plt
from backtest_engine import backtest, evaluate_performance, load_data

この部分では、データ操作にpandas、数値計算にnumpy、最適化計算にscipyminimize関数、データ可視化にmatplotlib.pyplotをインポートしています。また、既存のバックテスト機能を含むbacktest_engineモジュールからいくつかの関数をインポートしています。

ステップ2: シャープレシオの計算

def calculate_sharpe_ratio(pnl_series, risk_free_rate=0.01):
    daily_return = pnl_series.pct_change().dropna()
    excess_return = daily_return - risk_free_rate / 252
    sharpe_ratio = np.mean(excess_return) / np.std(excess_return)
    return sharpe_ratio

この関数では、損益データ(pnl_series)から日々のリターンを計算し、リスクフリーレート(年間1%を日割りで考慮)を超えるリターン(超過リターン)を求めます。この超過リターンの平均と標準偏差を使ってシャープレシオを計算し、戦略の効率性を評価します。

ステップ3: パラメータ最適化

def optimize_parameters(df, pnd_events):
    def objective(params):
        stop_loss, take_profit, trade_amount = params
        trades_df, final_balance = backtest(df, pnd_events, 
                                            stop_loss=stop_loss, 
                                            take_profit=take_profit, 
                                            trade_amount=trade_amount)
        sharpe_ratio = calculate_sharpe_ratio(trades_df['PnL'].cumsum())
        return -sharpe_ratio

    initial_params = [0.03, 0.05, 100]
    bounds = [(0.01, 0.1), (0.03, 0.1), (50, 500)]
    result = minimize(objective, initial_params, bounds=bounds, method='L-BFGS-B')
    return result.x

この関数では、損切り(stop_loss)、利益確定(take_profit)、トレードの金額(trade_amount)の最適値を求めるためのパラメータを最適化しています。内部で定義されたobjective関数は、これらのパラメータを使ってバックテストを実行し、その結果を用いてシャープレシオを計算します。最適化の目的は、シャープレシオを最大化(符号を反転させた最小化問題)することです。

ステップ4: メイン処理

def main():
    df = load_data('./data/BTCUSDT_data.csv')
    pnd_events = pd.read_csv('./data/pnd_events.csv')
    best_params = optimize_parameters(df, pnd_events)
    stop_loss, take_profit, trade_amount = best_params
    print("\n=== 最適パラメータ ===")
    print(f"損切り (STOP LOSS): {stop_loss:.2%}")
    print(f"利益確定 (TAKE PROFIT): {take_profit:.2%}")
    print(f"1回のトレード額 (TRADE AMOUNT): ${trade_amount:.2f}")

    trades_df, final_balance = backtest(df, pnd_events,
                                        stop_loss=stop_loss,
                                        take_profit=take_profit,
                                        trade_amount=trade_amount)
    evaluate_performance(trades_df, 10000)

if __name__ == "__main__":
    main()

main関数では、まずデータを読み込み、最適化関数を用いて最適なトレードパラメータを求めます。その後、これらのパラメータを使用して実際のバックテストを実行し、パフォーマンスを評価します。最終的に、最適なパラメータとバックテストの結果が出力されます。

以上がこのコードの主要な処理フローです。このようにして、実際のトレードデータに基づいて取引戦略の効率を評価し、最適化することが可能です。

8. 次のステップ

次は、リアルタイム運用時の監視システム設計を行います。具体的には、以下の機能を実装予定です。

リアルタイム価格監視
P&Dイベントの即時通知 (Slack / Telegram 連携)
トレード状況のダッシュボード可視化

Yodaka

次回の記事では、リアルタイム監視システムを構築と実装についてまとめます。

関連
開発記録#146(2025/3/25)「論文ベースのbot開発フローpart.8 リアルタイム監視システム」

続きを見る

おまけ:改善点&解決のためのアプローチ

実際の運用環境にデプロイする前に検討すべき改善点や課題がいくつかあります。それぞれの課題に対して、具体的な解決アプローチを提案します。

1. データの品質と範囲

課題: 使用されているデータがどれだけ市場を適切に反映しているか、また、過去のデータに過学習してしまっていないかが懸念されます。

解決策: 複数の時期や市場状況をカバーする広範なデータセットでモデルをテストする。また、アウト・オブ・サンプルのデータセットを用いてモデルの一般化能力を確認する。

2. 最適化アルゴリズムの選択

課題: L-BFGS-B アルゴリズムは局所最適解に収束する可能性があり、グローバルな最適解を見逃すことがある。

解決策: グローバル最適化手法(例えば、遺伝的アルゴリズムや粒子群最適化)を採用することで、より広範な解空間を探索し、最適な結果を得ることが可能です。

3. 計算コストとパフォーマンス

課題: 最適化プロセスは計算コストが高く、大量のデータに対しては時間がかかりすぎる可能性があります。

解決策: データをバッチ処理に分割するか、並列計算を導入して、計算効率を改善する。クラウドベースのコンピューティングリソースを利用するのも一つの手です。

4. リアルタイム取引適応

課題: 実際の取引環境では、市場条件が迅速に変化するため、バックテスト結果が即座に古くなる可能性があります。

解決策: モデルを定期的に再トレーニングし、最新の市場データをフィードしてリアルタイムで適応させるシステムを構築する。また、異常検出ロジックを組み込むことで、市場の大きな変動に迅速に対応できるようにする。

5. ロバスト性とエラーハンドリング

課題: システム障害やデータフィードの問題が発生した場合の対応が不足しています。

解決策: エラーハンドリング機能を強化し、システムが自動的に問題を診断して回復できるようにします。さらに、取引システムのロバスト性を向上させるために、フォールトトレラントな設計を実装する。

6. 監視とログ

課題: 運用中のシステムのパフォーマンス監視やトラブルシューティングが困難である。

解決策: リアルタイムでシステムの状態を監視し、詳細なログを取ることで、問題が発生した際の原因分析を容易にします。ダッシュボードを導入して、運用状況を視覚的に把握できるようにすると良いでしょう。

Yodaka

これらの改善点と解決策を検討し、実装することで、この取引システムをより実用的で効率的なものにすることができます。以下に、コードの運用にあたっての主要な課題とその解決策を表にまとめました。

課題解決策
データの品質と範囲複数の時期や市場状況をカバーする広範なデータセットでモデルをテストし、アウト・オブ・サンプルのデータセットを用いて一般化能力を確認。
最適化アルゴリズムの選択グローバル最適化手法(例えば、遺伝的アルゴリズムや粒子群最適化)を採用して解空間を広範囲に探索。
計算コストとパフォーマンスデータをバッチ処理に分割するか、並列計算を導入して計算効率を改善。クラウドベースのリソースを利用。
リアルタイム取引適応モデルを定期的に再トレーニングし、最新の市場データをフィード。異常検出ロジックを組み込む。
ロバスト性とエラーハンドリングエラーハンドリング機能を強化し、フォールトトレラントな設計を実装。
監視とログリアルタイムでシステムの状態を監視し、詳細なログを取る。ダッシュボードで運用状況を視覚的に把握。

おまけ2:よりロバストな戦略にする

Yodaka

戦略をよりロバストにするために考慮すると有効な追加パラメータの候補には以下のようなものがあります。それぞれのパラメータがどのように役立つかも含めて説明します。

1. エントリーとエグジットの条件

  • 理由: 現在のコードは、損切りと利益確定のパラメータに基づいて取引を行っていますが、エントリー(買い)とエグジット(売り)の条件をより詳細に設定することで、市場の状況に合わせて戦略を調整できます。
  • 提案: ボリンジャーバンド、移動平均線、RSI(Relative Strength Index)などの技術指標を利用した条件をエントリーやエグジットのトリガーとして取り入れる。

2. ポジションサイズの調整

  • 理由: 固定のトレード額ではなく、リスク管理の観点からポジションサイズを市場のボラティリティや最近の損益に応じて調整することが重要です。
  • 提案: ボラティリティに基づいたポジションサイジング(例えば、ATR(Average True Range)を使用してリスクを計算する)を導入する。

3. マーケットフィルター

  • 理由: 全ての市場状況で同じ戦略を適用するのではなく、特定の市場条件下でのみ取引を行うことで、不利な市場環境での損失を防げます。
  • 提案: 長期移動平均線を用いて市場が上昇トレンドか下降トレンドかを判断し、そのトレンドに合わせて取引を行うかどうかを決定する。

4. 時間フィルター

  • 理由: 特定の時間帯にのみ取引を行うことで、市場の流動性が高い時間帯を利用することができます。
  • 提案: 市場が開く直後や閉まる前など、特定の時間帯に取引を制限するパラメータを設定する。

5. 最大ドローダウン制限

  • 理由: 継続的な損失を抑え、リスクを管理することが重要です。これにより、資本の急激な減少を防ぎます。
  • 提案: 特定のパーセンテージの資本が失われた場合に自動的に取引を停止する「ストップアウト」レベルを設定する。

これらの追加パラメータは、戦略の適応性と耐久性を向上させることができます。これにより、様々な市場状況においても一定のリターンを保ちながらリスクを効果的に管理することが可能になります。

-Bot