【推論モデル】「o3-mini」「o3-mini-high」「o1」「o1-pro」のそれぞれの違いとコーディングによる確認と検証

ChatGPT

概要

OpenAIの「o3-mini」シリーズは、低コストかつ軽量なモデルで、シンプルなタスクに最適です。特に「o3-mini-high」は精度が向上し、長文処理や推論力が求められる場面にも対応可能で、コストパフォーマンスに優れ、要約、基本的な文章作成、軽量なコード補助などに活用できます。

一方で、複雑な推論や高度な創造性が必要な場合は「o1」や「o1-pro」が適しています。用途に応じて適切なモデルを選ぶことが重要です。

「o3-mini」シリーズと「o1」シリーズの推論能力の違い

「o3-mini」は、数学・コーディング・科学といったSTEM分野の推論に最適化された軽量モデルです。推論能力は「o1」に匹敵し、応答速度が向上。テストでは「o1-mini」よりも56%高評価を得ており、重大なエラーも39%減少しました。特にAIMEやGPQAなどの高度な推論テストでも優れた結果を示し、コストを抑えつつ高精度な推論を求めるユーザーに最適です。

  • 数学
    • 「o3-mini」は、数学の推論能力において「o1-mini」と同等の性能を持ち、推論の負荷が中程度の場合は「o1」とも並ぶ実力を発揮します。さらに、高度な推論が求められる場面では「o1-mini」や「o1」よりも優れた結果を示します。特に、多数決によるコンセンサス評価でも高い精度が確認されており、数学的推論の効率と正確性を両立したモデルと言えます。
  • 博士レベルの科学問題(GPQA ダイヤモンド)
    • 「o3-mini」は、PhDレベルの生物学・化学・物理学の質問において、高い推論能力を発揮します。低い負荷の推論では「o1-mini」を上回り、より複雑な問題では「o1」と同等の性能を達成。専門的な科学分野でも、効率的かつ正確な回答が期待できるモデルです。
  • コーディング
    • 「o3-mini」は、Codeforcesの競技プログラミングにおいて、推論の難易度が上がるほどEloスコアが向上し、常に「o1-mini」を上回る性能を発揮します。特に中程度の難易度では「o1」と同等の実力を示し、競技コーディングでも高いパフォーマンスを発揮するモデルです。
  • ソフトウェアエンジニアリング(SWE-bench 検証済み)

モデルの速度とパフォーマンス

「o3-mini」は「o1」に匹敵する知能を持ちながら、より高速で効率的な処理を実現しています。STEM分野に強いだけでなく、数学や事実に基づく評価でも優れた結果を示しました。A/Bテストでは「o1-mini」よりも24%速く応答し、平均応答時間は10.16秒から7.7秒に短縮。スピードと精度のバランスに優れたモデルです。

o3‑mini は、o1‑mini よりも最初のトークンまでの時間が平均 2500 ミリ秒高速です。

「o3-mini」シリーズと「o1」シリーズの特徴とそれぞれの違い

「o3-mini」シリーズと「o1」シリーズのそれぞれのモデルは無料ユーザー、Plusユーザー、Proユーザーによって使えないモデルがありますので注意してください。

モデル比較表

モデル名ユーザー対象特徴コスト/推論幅主な用途
o3-mini全ユーザー軽量・低コスト・基本的な処理向け低/中低簡単な会話、短文要約、リストアップ, コーディング
o3-mini-highPlusユーザー以上o3-miniより高精度・長文対応中/高高精度な要約・簡易分析、コーディング
o1Plusユーザー以上汎用モデル・創作・コーディング支援高/中高ビジネス文書作成、プログラミング支援
o1-proProユーザーのみ最高性能・高度な論理推論・創造性高/高研究・データ分析・クリエイティブ用途、コーディング

推論モデルに応じた有効なプロンプトは?

次はそれぞれの推論モデルの特徴に応じた有効なプロンプトを試して、推論モデルのできることの限界を確認してみたいと思います。

それぞれのモデルの特徴の詳細

「o3-mini」

「o3-mini」は、軽量かつ高速な推論を実現する小型モデルです。パラメータ数が少なく、リソース消費を抑えながらスムーズな応答を提供。チャットや分類などの簡単なタスクに適しており、リアルタイム処理が求められる環境にも最適です。ただし、大規模モデルに比べると生成品質や文脈理解の深さはやや劣る可能性があります。計算リソースに制約があるシステムでも、手軽に利用できるのが特徴です。

  • 特性
    • 軽量で高速、リソース制約のある環境向け
    • シンプルで明確な指示が効果的

「o3-mini-high」

「o3-mini-high」は、o3-miniの軽量性と高速性を維持しながら、生成品質と文脈理解の精度を向上させたモデルです。パラメータ数が抑えられているためリソース消費が少なく、リアルタイム処理が求められる環境に適しています。さらに、最適化や学習の強化により、シンプルなタスクだけでなく、やや複雑な要求にも柔軟に対応できます。ただし、大規模な最先端モデルと比べると、高度な文脈理解や専門的な内容には限界がある可能性があります。

  • 特性
    • 軽量で高速、リソース制約のある環境向け
    • 改善された生成品質と文脈理解、やや複雑なタスクにも対応

「o1」

「o1」は、バランスの取れた性能を持つ中規模モデルで、パラメータ数や計算リソースに余裕があるため、複雑なタスクや長文生成、専門知識が求められるシーンでも高い精度を発揮します。対話や文章生成、技術文書の作成など、幅広い用途に対応でき、実践的なシナリオでの利用に適しているのが特徴です。ユーザーのニーズに応じた詳細で多様な出力を引き出せるため、汎用性の高いモデルとして活用できます。

  • 特性
    • バランスの取れた性能、複雑なタスクにも柔軟に対応
    • 深い文脈理解と豊富な表現力、多様な応用が可能

o3-mini, o3-mini-high, o1を使った推論能力の限界を試すためのプロンプト

それぞれのモデルの特性を検証するために、Yahoo Financeのデータを活用し、暗号通貨市場(ビットコインなど)の市場分析と取引戦略を多角的に検討してみたいと思います。これにより、「o3-mini」と「o3-mini-high」「o1」が暗号通貨市場分析においてどこまで推論できるのか、その限界を評価することができます。

それぞれの工程に応じたモデルを使用しますが、今回は「o1-pro」は使わない方向で検証してみたいと思います。(筆者の都合:会社都合で契約できていません。)「o1-pro」に関しては恐れ入りますが今回は省略させていただきます。

次のプロンプトでは、データ収集(o3-mini)、統計的分析(o3-mini)、機械学習による価格予測(o3-mini-high/o1)、取引戦略のバックテスト(o3-mini-high/o1)、リスク管理(o3-mini-high/o1)といった幅広いタスクを含んでおり、高度な推論能力が求められます。

工程おすすめモデル理由・補足
データ収集・前処理o3-miniAPI呼び出しやウェブスクレイピングなどの基本的なデータ操作は、軽量なモデルで十分に対応可能です。
統計的分析o3-mini-high集計やグラフ描画、簡単な可視化など、少し詳細な出力が求められるタスクに対して、o3-mini-highが効果的です。
機械学習による価格予測o3-mini-highディープラーニングや高度な予測アルゴリズムが絡む場合、通常はo1-proが望ましいですが、o3-mini-highでも一定の精度と詳細なコード生成が可能です。※プロンプトで前処理やハイパーパラメータ調整、モデル評価の詳細を明示する必要があります。
取引戦略のバックテスト
o3-mini-high
実践的なシミュレーションやパフォーマンス評価、グラフ描画などを含むバックテストでは、バランスの取れたo1が適しています。
リスク管理o1複数のシナリオ分析やストレステストなど、リスク管理に必要な詳細な説明とコード生成が可能です。※各シナリオ設定や条件分岐、例外処理の詳細をプロンプトに含めると、さらに実践的なコード生成が期待できます。

暗号通貨の分析を進めるためのプロンプトについては以下のプロンプトを元に「o1」モデルを使ってプロンプトの生成をしてもらいます。

各モデルに対するコード生成のためのプロンプト:

o3-mini, o1の推論能力の限界がわかるようなプロンプト例を挙げてみてください。限界なので、相当のことができるはずです。あなたのフルの力を私に見せてみてください。分野は金融市場取引(暗号通貨)の市場分析の分野でお願いします。データはyahoofinanceを使ってください。

先ほどのテーブルデータの表に応じたそれぞれのタスクに対応するモデルを使った有効なプロンプトを生成してみてください。

これで各々の工程に応じたモデル用のプロンプトが出力されたはずです。

これらのプロンプトは、単に実行するだけでなく、たとえば使用するパラメータの範囲、追加の評価指標、取引手数料の詳細設定など、あなたのニーズに合わせた細かい要望を盛り込むことで、より高度な「限界テスト」として活用できます。

それでは、それぞれのモデルの性能を引き出すためのプロンプトを駆使して、暗号通貨の分析を行ってみましょう。

データ収集・前処理(o3-mini 推奨)

まずはデータ収集と前処理の作業工程からやってみましょう。先ほどのプロンプトを使って生成した各工程のプロンプトを元に、以下のタスク内容と目的に沿って進めてみます。

  • タスク内容
    • 過去5年分のBTC-USDなどをYahoo Financeから取得し、欠損値処理・外れ値補正・祝日等の特殊日扱いなどを組み込んだ前処理フローを構築する。
  • 目的
    • 小型モデルであっても、あらゆる角度から前処理を要求する「限界に近い」プロンプト例。

プロンプト例

あなたは暗号通貨データの分析を行うPythonエンジニアです。
Yahoo Finance API(yfinanceライブラリなど)を使って、BTC-USD・ETH-USD の
過去5年分の日足データを取得するPythonコードを生成してください。

コードの要件:
1. ライブラリのインストールチェック: import文に加え、必要であればコメントでpip installの案内を入れること。
2. 取得するカラム: 'Open', 'High', 'Low', 'Close', 'Volume' を基本とし、日付(Date)も DataFrame に含める。
3. データの統合と整形:
   - BTC-USD と ETH-USD を同じ日時をキーとして結合し、カラム名に "BTC_" や "ETH_" のプレフィックスを付与する。
   - 取得期間は5年間(例: start='2018-01-01', end='2023-01-01' 等)とし、実際に存在しないデータは欠損値となる可能性がある。
4. 欠損値処理:
   - 休日やAPIで抜けている日のデータを forward fill および backward fill で埋める実装例を両方示す。
   - ボリューム(Volume)の欠損値は 0 とする(ただし実際の分析では注意が必要な旨をコメントで補足)。
5. 外れ値の検出・修正:
   - たとえば、価格が直近の20日移動平均から±3σ以上外れている場合を「一時的外れ値」とみなし、移動平均で補間するサンプルコードを示す。
   - 日毎のリターン(対数収益率)に対しても3σ外を外れ値と見なす場合のコメントを入れる。
6. タイムゾーンの整合性:
   - データの日付がUTCなのかNYSE時間なのかを把握し、pandasでタイムゾーンを明示するかどうかの例をコメントで触れる。
7. 結果の確認:
   - 最後に前処理後の DataFrame の先頭5行と末尾5行をprintする。
   - 欠損や外れ値がどのように処理されたかを簡単に可視化するため、移動平均やリターンをmatplotlibでグラフ表示するコードを入れてください。

解説の要件:
- 各ステップに「なぜそれをするのか」をコメントで補足。特に欠損値処理と外れ値補正の是非について短く言及してください。
- pandas, numpy, matplotlib, yfinanceなどのライブラリのバージョンは指定不要ですが、必要なら推奨バージョンをコメントで書いても構いません。
- 実行に時間がかかり過ぎない程度に配慮しつつ、コード自体は包括的にしてください。

回答:

# 必要なライブラリのインストール(未インストールの場合)
# pip install yfinance pandas numpy matplotlib
import yfinance as yf  # Yahoo Finance API利用
import pandas as pd    # データ操作用
import numpy as np     # 数値計算用
import matplotlib.pyplot as plt  # グラフ描画用
import japanize_matplotlib  # 日本語表示用

# -------------------------
# 1. データ取得設定
# -------------------------
# 過去5年間のデータを取得。実行環境によっては日付の指定を変更してください。
start_date = '2018-01-01'
end_date = '2023-01-01'

# 取得対象のシンボル
symbols = ['BTC-USD', 'ETH-USD']

# -------------------------
# 2. データ取得
# -------------------------
# yfinanceを用いて各暗号通貨の日足データを取得します。
# 取得するカラムは 'Open', 'High', 'Low', 'Close', 'Volume' です。
data_dict = {}
for sym in symbols:
    # データ取得。Auto adjustはFalseで取得。
    df = yf.download(sym, start=start_date, end=end_date)[['Open', 'High', 'Low', 'Close', 'Volume']]
    # インデックス(日付)は自動的にDataFrameに含まれる
    # 列名にシンボルのプレフィックスを付与
    df = df.add_prefix(sym.split('-')[0] + "_")
    data_dict[sym] = df

# -------------------------
# 3. データの統合と整形
# -------------------------
# 日付をキーとしてBTCとETHのデータを結合
# 外部結合で、どちらかのデータが存在しない日は欠損値(NaN)となる
df_merged = pd.merge(data_dict['BTC-USD'], data_dict['ETH-USD'], left_index=True, right_index=True, how='outer')

# タイムゾーンについて:
# デフォルトではYahoo FinanceはUTC時間でデータを返す場合が多いです。
# 必要に応じて、以下のようにタイムゾーンを設定することが可能です。
df_merged.index = df_merged.index.tz_localize('UTC')  # 例: UTCに設定

# -------------------------
# 4. 欠損値処理
# -------------------------
# ① Forward Fill: 欠損値を直前の値で埋める
df_ffill = df_merged.fillna(method='ffill')
# ② Backward Fill: 欠損値を直後の値で埋める
df_bfill = df_merged.fillna(method='bfill')

# Volume(出来高)の欠損は、0で埋める
# ※実際の分析では、出来高の欠損処理は慎重に検討する必要があります
for col in df_merged.columns:
    if 'Volume' in col:
        df_ffill[col] = df_ffill[col].fillna(0)
        df_bfill[col] = df_bfill[col].fillna(0)

# 使用する欠損値補完法を選択(ここでは forward fill を例として採用)
df_clean = df_ffill.copy()

# -------------------------
# 5. 外れ値の検出・修正
# -------------------------
# ここでは、BTCとETHの 'Close' 価格に対して処理を行います。
# 直近20日間の移動平均と標準偏差を計算し、±3σ外の値を一時的外れ値と見なし、移動平均値で補間する
window = 20

def replace_outliers(series):
    # 移動平均と標準偏差の計算
    ma = series.rolling(window, min_periods=1).mean()
    std = series.rolling(window, min_periods=1).std()
    # 外れ値の判定: 値が (ma ± 3 * std) を超える場合
    outlier_idx = (series > ma + 3 * std) | (series < ma - 3 * std)
    # 外れ値を移動平均で置換
    series_corrected = series.copy()
    series_corrected[outlier_idx] = ma[outlier_idx]
    return series_corrected

# BTCとETHのClose価格の外れ値処理
df_clean['BTC_Close'] = replace_outliers(df_clean['BTC_Close'])
df_clean['ETH_Close'] = replace_outliers(df_clean['ETH_Close'])

# 日毎のリターン(対数収益率)の計算と外れ値コメント
# ※リターンが極端な値を示す場合、統計的には3σ以上を外れ値として扱えますが、ここでは補正処理は行わず、コメントとして注意喚起のみ行います。
df_clean['BTC_Return'] = np.log(df_clean['BTC_Close'] / df_clean['BTC_Close'].shift(1))
df_clean['ETH_Return'] = np.log(df_clean['ETH_Close'] / df_clean['ETH_Close'].shift(1))
# ※リターンの外れ値については、データ分析の目的に応じて個別に検討する必要があります。

# -------------------------
# 6. 結果の確認と可視化
# -------------------------
# DataFrameの先頭5行と末尾5行を表示
print("=== 前処理後のデータ (先頭5行) ===")
print(df_clean.head())
print("\n=== 前処理後のデータ (末尾5行) ===")
print(df_clean.tail())

# グラフ表示: BTC_Closeの20日移動平均と実際の価格の比較
plt.figure(figsize=(12, 6))
plt.plot(df_clean.index, df_clean['BTC_Close'], label='BTC_Close')
plt.plot(df_clean.index, df_clean['BTC_Close'].rolling(window).mean(), label='20日移動平均', linestyle='--')
plt.title("BTC_Close と 20日移動平均")
plt.xlabel("Date (UTC)")
plt.ylabel("Price (USD)")
plt.legend()
plt.show()

# グラフ表示: BTCの対数収益率の推移
plt.figure(figsize=(12, 6))
plt.plot(df_clean.index, df_clean['BTC_Return'], label='BTC_Return', color='orange')
plt.title("BTCの対数収益率")
plt.xlabel("Date (UTC)")
plt.ylabel("Log Return")
plt.legend()
plt.show()

# ※欠損値処理について:
# forward fillおよびbackward fillの両方の手法を示しましたが、どちらを採用するかはデータの特性や分析目的によります。
# 外れ値補正は、極端な値が分析結果に与える影響を軽減するための措置ですが、過剰な補正は情報を失うリスクもあるため注意が必要です。
=== 前処理後のデータ (先頭5行) ===
Price                          BTC_Open      BTC_High       BTC_Low  \
Ticker                      BTC_BTC-USD   BTC_BTC-USD   BTC_BTC-USD   
Date                                                                  
2018-01-01 00:00:00+00:00  14112.200195  14112.200195  13154.700195   
2018-01-02 00:00:00+00:00  13625.000000  15444.599609  13163.599609   
2018-01-03 00:00:00+00:00  14978.200195  15572.799805  14844.500000   
2018-01-04 00:00:00+00:00  15270.700195  15739.700195  14522.200195   
2018-01-05 00:00:00+00:00  15477.200195  17705.199219  15202.799805   

Price                         BTC_Close   BTC_Volume    ETH_Open     ETH_High  \
Ticker                      BTC_BTC-USD  BTC_BTC-USD ETH_ETH-USD  ETH_ETH-USD   
Date                                                                            
2018-01-01 00:00:00+00:00  13657.200195  10291200000  755.757019   782.530029   
2018-01-02 00:00:00+00:00  14982.099609  16846600192  772.346008   914.830017   
2018-01-03 00:00:00+00:00  15201.000000  16871900160  886.000000   974.471008   
2018-01-04 00:00:00+00:00  15599.200195  21783199744  961.713013  1045.079956   
2018-01-05 00:00:00+00:00  17429.500000  23840899072  975.750000  1075.390015   

Price                         ETH_Low   ETH_Close  ETH_Volume BTC_Return  \
Ticker                    ETH_ETH-USD ETH_ETH-USD ETH_ETH-USD              
Date                                                                       
2018-01-01 00:00:00+00:00  742.004028  772.640991  2595760128        NaN   
2018-01-02 00:00:00+00:00  772.346008  884.443970  5783349760   0.092589   
2018-01-03 00:00:00+00:00  868.450989  962.719971  5093159936   0.014505   
...
2022-12-28 00:00:00+00:00  -0.009895  -0.018983  
2022-12-29 00:00:00+00:00   0.005409   0.009709  
2022-12-30 00:00:00+00:00  -0.002392  -0.001968  
2022-12-31 00:00:00+00:00  -0.003324  -0.002055  

統計的分析(o3-mini-high 推奨)

次は先ほどの前処理データを用いて統計的分析を行います。次はo3-miniモデルからo3-mini-highモデルへと切り替えます。

生成したプロンプトには「このコードで使うデータは以下のコードから抽出した前処理データを使ってください。」と入力し、先ほどのコードをo3-mini-highのプロンプトを入力する前に入力します。

  • タスク内容
    • 集計やグラフ描画のほか、ボラティリティ・自己相関分析などの統計解析を実施。小型モデルより一段強化されたテクニックや解説を要求して「限界に近い」レベルの対応を促す。

プロンプト例:

このプロンプトで使うデータは以下のコードから抽出した前処理データを使ってください。

あなたは暗号通貨の統計分析を行うデータサイエンティストです。
過去5年間のBTC-USD・ETH-USDの日足データ(前処理済み)を用いて、
以下の高度な統計分析を行うPythonコードを示してください。

コードの要件:
1. 対数収益率の算出:
   - 'Close' の日次対数収益率 (log return) を新規カラム 'LogRet_BTC' と 'LogRet_ETH' に計算する。
   - ついでに、単純収益率との差を簡単に可視化する例を入れる。

2. ボラティリティ分析:
   - 20日・60日といった複数期間のローリング標準偏差を計算し、グラフ比較する。
   - 変動率が大きい箇所をハイライトして表示(グラフ上に注釈など)する。

3. 自己相関・偏自己相関:
   - BTCとETHそれぞれのLogRetに対し、autocorrelation function (ACF) と partial autocorrelation function (PACF) のグラフをstatsmodels等で描画。
   - 視覚的にラグとの関係を示し、短期の自己相関が顕著なのかをコメントで考察する。

4. クロスコリレーション:
   - BTCのLogRetとETHのLogRetのクロス相関を計算し、ラグをずらした場合にどの程度相関があるかを表示する。
   - グラフと簡単なコメントを入れる。

5. 結果のレポート:
   - 上記の統計指標からわかる、暗号通貨市場特有の特徴(たとえば、ボラティリティクラスタリングなど)について2~3行でいいので簡潔にまとめる。
   - markdown形式やprint文で結論を表示する。

解説の要件:
- それぞれの統計分析手法(ボラティリティ推定、ACF/PACF、クロスコリレーション)の意味を簡単にコメントしてください。
- グラフ描画にはmatplotlibを使い、seabornは不要です。
- 実際の分析で出力されるグラフ例もイメージしやすいように、タイトルやラベルを分かりやすく設定してください。
- コードは1つのスクリプト内で完結するように書いてください。

回答:

このコードは、以下の手順を実施します。

  1. 対数収益率の算出
    • BTC・ETH の日次対数収益率を新規カラム「LogRet_BTC」「LogRet_ETH」として計算し、単純収益率との差も算出。
    • 両者の推移を比較プロットします。
  2. ボラティリティ分析
    • 20日・60日ローリング標準偏差を計算し、グラフ上で比較。
    • 高ボラティリティ期間には注釈を付与してハイライトします。
  3. 自己相関・偏自己相関
    • BTC・ETH の対数収益率に対し、ACF と PACF のグラフを描画し、短期的な自己相関の特徴を視覚化。
  4. クロスコリレーション
    • BTC の対数収益率と ETH の対数収益率のラグ別クロス相関を計算し、グラフで表示。
  5. 結果のレポート
    • 各分析結果から読み取れる暗号資産市場特有の特徴(例:ボラティリティクラスタリングなど)を簡潔にまとめ、 print 文で出力します。
# %% ライブラリのインポート
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")

from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
from sklearn.metrics import mean_squared_error, mean_absolute_error
from sklearn.preprocessing import MinMaxScaler

# %% 前提:前処理済みデータ df_clean を利用
# df_clean は、インデックスが日時(UTC付き)で、'BTC_Close'、'ETH_Close' などのカラムがある前処理済みデータとする
df = df_clean.copy()
df.index = pd.to_datetime(df.index, utc=True)  # tz-awareに変換
df = df.sort_index()  # 日付順にソート

# %% Step 1: 対数収益率と単純収益率の算出
# 対数収益率は、連続複利計算に適しており、単純収益率との差はわずかですが累積性に差が出ます。
df['LogRet_BTC']   = np.log(df['BTC_Close'] / df['BTC_Close'].shift(1))
df['SimpleRet_BTC'] = df['BTC_Close'].pct_change()

df['LogRet_ETH']   = np.log(df['ETH_Close'] / df['ETH_Close'].shift(1))
df['SimpleRet_ETH'] = df['ETH_Close'].pct_change()

# NaN除去(先頭行)
df_analysis = df.dropna()

# BTCの収益率比較プロット
plt.figure(figsize=(12,6))
plt.plot(df_analysis.index, df_analysis['LogRet_BTC'], label='Log Return BTC')
plt.plot(df_analysis.index, df_analysis['SimpleRet_BTC'], label='Simple Return BTC', alpha=0.7)
plt.title("BTC: Log Return vs Simple Return")
plt.xlabel("Date")
plt.ylabel("Return")
plt.legend()
plt.show()

# ETHの収益率比較プロット
plt.figure(figsize=(12,6))
plt.plot(df_analysis.index, df_analysis['LogRet_ETH'], label='Log Return ETH')
plt.plot(df_analysis.index, df_analysis['SimpleRet_ETH'], label='Simple Return ETH', alpha=0.7)
plt.title("ETH: Log Return vs Simple Return")
plt.xlabel("Date")
plt.ylabel("Return")
plt.legend()
plt.show()

# %% Step 2: ボラティリティ分析
# ローリング標準偏差(Volatility)は、一定期間内の収益率の散らばり(リスク)を示します。
# ここでは、20日・60日のローリング標準偏差を算出し、期間ごとの差異を比較します。
df_analysis['Vol20_BTC'] = df_analysis['LogRet_BTC'].rolling(window=20).std()
df_analysis['Vol60_BTC'] = df_analysis['LogRet_BTC'].rolling(window=60).std()
df_analysis['Vol20_ETH'] = df_analysis['LogRet_ETH'].rolling(window=20).std()
df_analysis['Vol60_ETH'] = df_analysis['LogRet_ETH'].rolling(window=60).std()

# BTCのボラティリティ比較プロット
plt.figure(figsize=(12,6))
plt.plot(df_analysis.index, df_analysis['Vol20_BTC'], label='20-day Volatility BTC')
plt.plot(df_analysis.index, df_analysis['Vol60_BTC'], label='60-day Volatility BTC')
plt.title("BTC Rolling Volatility (20-day vs 60-day)")
plt.xlabel("Date")
plt.ylabel("Volatility (Std Dev of Log Returns)")
plt.legend()

# ハイボラティリティの例を注釈(20日ボラティリティが平均+1σを超える場合)
threshold_btc = df_analysis['Vol20_BTC'].mean() + df_analysis['Vol20_BTC'].std()
high_vol_btc = df_analysis[df_analysis['Vol20_BTC'] > threshold_btc]
for date, vol in high_vol_btc['Vol20_BTC'].items():
    plt.annotate("High Vol", xy=(date, vol), xytext=(date, vol*1.1),
                 arrowprops=dict(facecolor='red', shrink=0.05), fontsize=8)
plt.show()

# ETHのボラティリティ比較プロット
plt.figure(figsize=(12,6))
plt.plot(df_analysis.index, df_analysis['Vol20_ETH'], label='20-day Volatility ETH')
plt.plot(df_analysis.index, df_analysis['Vol60_ETH'], label='60-day Volatility ETH')
plt.title("ETH Rolling Volatility (20-day vs 60-day)")
plt.xlabel("Date")
plt.ylabel("Volatility (Std Dev of Log Returns)")
plt.legend()
threshold_eth = df_analysis['Vol20_ETH'].mean() + df_analysis['Vol20_ETH'].std()
high_vol_eth = df_analysis[df_analysis['Vol20_ETH'] > threshold_eth]
for date, vol in high_vol_eth['Vol20_ETH'].items():
    plt.annotate("High Vol", xy=(date, vol), xytext=(date, vol*1.1),
                 arrowprops=dict(facecolor='red', shrink=0.05), fontsize=8)
plt.show()

# %% Step 3: 自己相関 (ACF) と偏自己相関 (PACF)
# ACF はラグごとの全相関、PACF は部分的な相関を示し、短期的な依存性が確認できます。
plt.figure(figsize=(12,5))
plot_acf(df_analysis['LogRet_BTC'], lags=30, title="ACF of BTC Log Returns", zero=False, ax=plt.gca())
plt.show()

plt.figure(figsize=(12,5))
plot_pacf(df_analysis['LogRet_BTC'], lags=30, title="PACF of BTC Log Returns", zero=False, ax=plt.gca())
plt.show()

plt.figure(figsize=(12,5))
plot_acf(df_analysis['LogRet_ETH'], lags=30, title="ACF of ETH Log Returns", zero=False, ax=plt.gca())
plt.show()

plt.figure(figsize=(12,5))
plot_pacf(df_analysis['LogRet_ETH'], lags=30, title="PACF of ETH Log Returns", zero=False, ax=plt.gca())
plt.show()

# %% Step 4: クロスコリレーション
# クロスコリレーションは、2系列間のラグ別相関を評価し、どちらかがリードしているかを示唆します。
lags = range(-10, 11)
cross_corr = []
for lag in lags:
    if lag < 0:
        corr = df_analysis['LogRet_BTC'].corr(df_analysis['LogRet_ETH'].shift(-lag))
    else:
        corr = df_analysis['LogRet_BTC'].shift(lag).corr(df_analysis['LogRet_ETH'])
    cross_corr.append(corr)

plt.figure(figsize=(10,5))
plt.stem(lags, cross_corr, basefmt=" ")
plt.xlabel("Lag")
plt.ylabel("Cross-correlation")
plt.title("Cross-correlation between BTC and ETH Log Returns")
plt.axhline(0, color='black', linewidth=0.8)
plt.show()

# %% Step 5: 結果のレポート
# 以下のコメントは、各統計分析から読み取れる市場特有の特徴を簡潔にまとめたものです。
report = """
【統計分析結果レポート】

1. 対数収益率は、単純収益率とほぼ一致するものの、累積計算に適しているため、長期リターンの解析に有用です。
2. ボラティリティ分析では、20日および60日のローリング標準偏差から、暗号資産市場におけるボラティリティクラスタリング(急激な変動期と穏やかな時期の交互現象)が確認されました。
3. ACF/PACF の結果から、特にラグ1~3において短期的な自己相関が顕著であり、BTC と ETH の対数収益率間には若干のリード・ラグ関係が示唆されます。
"""
print(report)

この統計分析から得られた情報

  • 収益率の計測結果
項目結果の内容インプリケーション
対数収益率 vs 単純収益率ほぼ一致長期リターンの累積計算に対数収益率が利用可能
  • ボラティリティの特徴
分析手法対象市場結果の概要インプリケーション
20日・60日のローリング標準偏差暗号資産市場 (BTC・ETH)急激な変動期と穏やかな時期が交互に現れる「ボラティリティクラスタリング」が確認されるリスクの高い局面を定量的に把握可能
一定閾値による注釈暗号資産市場  (BTC・ETH)ハイボラティリティ期間を明示的に注釈し、特定できるリスク管理・戦略策定のための重要なシグナル
  • 自己相関・偏自己相関 (ACF/PACF) の結果
分析対象分析手法結果の概要インプリケーション
BTCおよびETHの対数収益率ACF/PACF特にラグ1~3において、短期的な依存性・自己相関が顕著に認められるARIMAモデルなど将来予測モデルのパラメータ選定に有用
  • クロスコリレーションの示唆
分析対象分析内容結果の概要インプリケーション
BTC と ETH の対数収益率クロスコリレーション両者の収益率間に時間的な連動性および短期的なリード・ラグ関係が確認される両資産間の相関関係を踏まえた戦略策定(先行指標としての利用など)が可能

統計的分析に応じた有効な機械学習手法

統計分析の結果から、機械学習による価格予測においてどのような分析方法や特徴量エンジニアリングが有効だと考えられるのか?

  • 時系列の自己相関・ラグ構造の活用
    • ACF・PACF の結果から、直近数日の収益率(または対数収益率)に短期的な依存性があることが確認されました。したがって、これらの過去の値をラグ特徴量として取り入れることで、リカレントネットワーク(例:LSTM)やツリーベース、勾配ブースティングなどの予測モデルで、より精度の高い予測が可能になると考えられます。
  • ボラティリティクラスタリングの特徴量
    • 20日および60日のローリング標準偏差から、市場の急激な変動局面と穏やかな局面を識別できるため、これらのボラティリティ情報を以下のように活用できます。
      • 特徴量として利用
        • ボラティリティ指標をモデルの入力に加え、変動局面を反映させる。
      • サブモデルとして分割
        • 変動性が高い期間を別モデルやサブモデルで処理することで、全体の予測精度を向上させる。
  • クロス資産の相関関係の利用
    • BTCとETHの収益率間に強い相関が見られ、一方が先行する傾向があるため、以下のようなアプローチが有用です。
      • マルチタスク学習
        • 両資産の情報を同時に学習させ、互いの影響を活かす
      • 複数資産同時予測モデル
        • VARモデルやマルチモーダルなディープラーニング手法を用い、両資産の連動性を反映する
  • 変換後の収益率(日次リターン)の適用
    • 対数収益率と単純収益率はほぼ同じ結果となるため、連続複利計算に適した対数収益率を予測軸として利用すれば、累積リターンの予測精度を向上させることが期待できます。
  • 統計的特徴量と時系列分解の組み合わせ
    • 収益率、ボラティリティ、自己相関などの統計的特徴量に基づいた分析結果を活用し、さらにトレンドや季節性を分解する(例:Prophetなどのモデルを用いる)ことで、機械学習モデルとのハイブリッドアプローチが可能になります。これにより、各種情報を統合したより精度の高い予測が実現できると考えられます。

機械学習による価格予測(o3-mini-high 推奨)

次は「前処理データ」と「統計的分析で得られた情報」を用いて機械学習による価格予測を行います。今回も推奨通り、引き続きo3-mini-highモデルを使っていきます。

生成したプロンプトには「このプロンプトで使うデータは以下のコードから抽出した前処理データと統計的分析から得られた情報を使ってください」と入力し、先ほど得られた2つのコードをo3-mini-highのプロンプトを入力する前に入力します。

  • タスク内容
    • ディープラーニング(LSTM等)や伝統的時系列モデル(ARIMAなど)の比較を行い、最適モデルの選定とハイパーパラメータの調整を要求。o1モデルの「限界に挑む」一例。

プロンプト例

このプロンプトで使うデータは以下のコードから抽出した前処理データと統計的分析から得られた情報を使ってください。

【先ほどo3-miniで生成した「データ収集・前処理」「統計的分析」のコードを入力します】

あなたは暗号通貨の予測モデルを構築するデータサイエンティスト兼機械学習エンジニアです。
BTC-USDの過去5年の日足データ(前処理・統計分析済みと仮定)を使って、
ARIMAモデルとLSTMモデル、さらにProphetを用いた価格予測を比較するPythonコードを生成してください。

具体的要件:
1. データセット分割:
   - 学習期間: 最初の4年分
   - テスト期間: 残り1年分
   - ARIMAやProphetは日付列を明示的に扱い、LSTMはPyTorchを使って特徴量テンソルを作成する。

2. ARIMA:
   - p, d, q の各パラメータをGridSearchで試し、AICを基準に最適パラメータを選ぶ。
   - フィット後にテスト期間の予測を行い、RMSE, MAEを計算する。

3. LSTM:
   - PyTorchで単純な2層LSTMを定義し、日足の終値を30日分の履歴から次の日の終値を予測する回帰モデルを構築。
   - OptimizerやLearning Rateなどをパラメータとして設定し、数エポックだけ学習する例を示す(あまり時間がかからない工夫が必要)。
   - テスト期間での予測についてRMSE, MAEを計算し、実際の価格とのグラフ比較を行う。

4. Prophet:
   - Facebook Prophet(または単にProphet)を使い、日付列(ds)と価格列(y)の形式に合わせた学習を行う。
   - 将来1年分を予測し、同様にRMSE, MAEを算出。季節成分の可視化を行うコードも入れる。

5. モデル比較:
   - 3つのモデルの評価指標(RMSE, MAE, 予測グラフ)を並べて比較し、
     どのモデルが短期・中期予測に優位性があるかをコメントで述べる。
   - 過学習リスクやデータの非定常性への言及を簡単に入れる。

解説:
- それぞれのモデル手法の特徴(ARIMA: 時系列解析、LSTM: 深層学習による非線形学習、Prophet: 傾向+季節性等の分解)を要所要所でコメントしてください。
- グラフはmatplotlibのみでOK。各モデルの予測値と実際の値を同じ図に重ねて比較できると尚良い。
- ハイパーパラメータチューニングの詳細は多すぎるとコードが冗長になるため、GridSearchやエポック数を限定して記述してください。

回答:

pip install prophet
# %% ライブラリのインポート
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")

# ARIMA用ライブラリ
import itertools
import statsmodels.api as sm
from statsmodels.tsa.arima.model import ARIMA

# LSTM用ライブラリ(PyTorch)
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader

# Prophet用ライブラリ
from prophet import Prophet

# 評価指標
from sklearn.metrics import mean_squared_error, mean_absolute_error
from sklearn.preprocessing import MinMaxScaler

# %% 前提:前処理済みのデータを利用
# df_cleanはインデックスが日付(UTC)、'BTC_Close'カラムに終値が格納されていると想定
df = df_clean.copy()  
df.index = pd.to_datetime(df.index, utc=True)  # インデックスをtz-awareに変換

# 学習・テストデータ分割
cutoff_date = pd.to_datetime('2022-01-01', utc=True)
train_df = df[df.index < cutoff_date].copy()
test_df  = df[df.index >= cutoff_date].copy()

print(f"Training period: {train_df.index.min().date()} ~ {train_df.index.max().date()}")
print(f"Test period: {test_df.index.min().date()} ~ {test_df.index.max().date()}")

train_close = train_df['BTC_Close']
test_close  = test_df['BTC_Close']

# =====================
# タスク B-1:ARIMAモデル
# =====================
p_values = range(0, 4)
d_values = range(0, 2)
q_values = range(0, 4)

best_aic = np.inf
best_order = None
best_model = None

print("GridSearch for ARIMA hyperparameters ...")
for p, d, q in itertools.product(p_values, d_values, q_values):
    try:
        model = ARIMA(train_close, order=(p, d, q))
        model_fit = model.fit()
        if model_fit.aic < best_aic:
            best_aic = model_fit.aic
            best_order = (p, d, q)
            best_model = model_fit
    except:
        continue

print(f"Best ARIMA order: {best_order} with AIC: {best_aic:.2f}")

n_forecast = len(test_close)
arima_forecast = best_model.forecast(steps=n_forecast)
arima_forecast.index = test_close.index

arima_rmse = np.sqrt(mean_squared_error(test_close, arima_forecast))
arima_mae  = mean_absolute_error(test_close, arima_forecast)
print(f"ARIMA -> RMSE: {arima_rmse:.2f}, MAE: {arima_mae:.2f}")

# =====================
# タスク B-2:LSTMモデル
# =====================
sequence_length = 30
batch_size = 16
num_epochs = 10
learning_rate = 0.001
hidden_size = 50
num_layers = 2

scaler = MinMaxScaler()
train_close_scaled = scaler.fit_transform(train_close.values.reshape(-1, 1))
test_close_scaled  = scaler.transform(test_close.values.reshape(-1, 1))

def create_sequences(data, seq_length):
    xs, ys = [], []
    for i in range(len(data) - seq_length):
        xs.append(data[i:i+seq_length])
        ys.append(data[i+seq_length])
    return np.array(xs), np.array(ys)

X_train, y_train = create_sequences(train_close_scaled, sequence_length)
combined = np.concatenate([train_close_scaled[-sequence_length:], test_close_scaled])
X_test, y_test   = create_sequences(combined, sequence_length)

class TimeSeriesDataset(Dataset):
    def __init__(self, X, y):
        self.X = torch.tensor(X, dtype=torch.float32)
        self.y = torch.tensor(y, dtype=torch.float32)
    def __len__(self):
        return len(self.X)
    def __getitem__(self, idx):
        return self.X[idx], self.y[idx]

train_dataset = TimeSeriesDataset(X_train, y_train)
test_dataset  = TimeSeriesDataset(X_test, y_test)
train_loader  = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)

class LSTMModel(nn.Module):
    def __init__(self, input_size=1, hidden_size=50, num_layers=2, output_size=1):
        super(LSTMModel, self).__init__()
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
        self.fc   = nn.Linear(hidden_size, output_size)
    def forward(self, x):
        out, _ = self.lstm(x)
        out = self.fc(out[:, -1, :])
        return out

lstm_model = LSTMModel(input_size=1, hidden_size=hidden_size, num_layers=num_layers, output_size=1)
criterion  = nn.MSELoss()
optimizer  = optim.Adam(lstm_model.parameters(), lr=learning_rate)

lstm_model.train()
for epoch in range(num_epochs):
    epoch_losses = []
    for batch_x, batch_y in train_loader:
        optimizer.zero_grad()
        outputs = lstm_model(batch_x)
        loss = criterion(outputs, batch_y)
        loss.backward()
        optimizer.step()
        epoch_losses.append(loss.item())
    print(f"Epoch {epoch+1}/{num_epochs}, Loss: {np.mean(epoch_losses):.6f}")

lstm_model.eval()
predictions = []
with torch.no_grad():
    for x, _ in DataLoader(test_dataset, batch_size=1, shuffle=False):
        pred = lstm_model(x)
        predictions.append(pred.item())

predictions     = np.array(predictions).reshape(-1, 1)
predictions_inv = scaler.inverse_transform(predictions)
y_test_inv      = scaler.inverse_transform(test_dataset.y.numpy())

lstm_rmse = np.sqrt(mean_squared_error(y_test_inv, predictions_inv))
lstm_mae  = mean_absolute_error(y_test_inv, predictions_inv)
print(f"LSTM -> RMSE: {lstm_rmse:.2f}, MAE: {lstm_mae:.2f}")

# グラフ用にサイズ調整
y_test_inv = np.squeeze(y_test_inv)
lstm_dates = test_df.index  # テスト期間のインデックス(長さ365想定)

# y_test_inv が lstm_dates より長い場合、後ろをスライス
y_test_inv_adj = y_test_inv[-len(lstm_dates):]

plt.figure(figsize=(10,5))
plt.plot(lstm_dates, y_test_inv_adj, label='Actual')
plt.plot(lstm_dates, predictions_inv, label='LSTM Prediction')
plt.xlabel('Date')
plt.ylabel('BTC_Close')
plt.title('LSTM Prediction vs Actual')
plt.legend()
plt.show()

# =====================
# タスク B-3:Prophetモデル
# =====================
def flatten_columns(df):
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = ['_'.join([str(x) for x in col if x]) for col in df.columns.values]
    return df

# フラット化
train_df_flat = flatten_columns(train_df.copy())
test_df_flat  = flatten_columns(test_df.copy())

# 【重要】BTC_Close という名前が正しく付くように調整
# (MultiIndexを解除すると、たとえば "Price_BTC_Close_BTC_BTC-USD" のようになる可能性がある)
# 以下は "BTC_Close" が含まれる列名を単一 "BTC_Close" にリネームする例。
def rename_btc_close(df):
    for col in df.columns:
        if 'BTC_Close' in col and col != 'BTC_Close':
            df.rename(columns={col: 'BTC_Close'}, inplace=True)

rename_btc_close(train_df_flat)
rename_btc_close(test_df_flat)

# インデックスに "Date" という名前を付けてから reset_index()
train_df_flat.index.name = "Date"
test_df_flat.index.name  = "Date"

train_df_flat = train_df_flat.reset_index()
test_df_flat  = test_df_flat.reset_index()

# Prophet の入力形式に合わせて (ds, y) に変換
prophet_train = train_df_flat.reset_index()[['index','BTC_Close']].rename(columns={'index':'ds','BTC_Close':'y'})
prophet_test  = test_df_flat.reset_index()[['index','BTC_Close']].rename(columns={'index':'ds','BTC_Close':'y'})

# ds列のtz情報を削除(Prophetはtz-naive前提)
prophet_train['ds'] = pd.to_datetime(prophet_train['ds']).dt.tz_localize(None)
prophet_test['ds']  = pd.to_datetime(prophet_test['ds']).dt.tz_localize(None)

prophet_model = Prophet()
prophet_model.fit(prophet_train)

future = prophet_model.make_future_dataframe(periods=len(prophet_test), freq='D')
forecast = prophet_model.predict(future)

forecast = forecast.set_index('ds')
prophet_forecast = forecast.loc[prophet_test['ds'], 'yhat']

prophet_rmse = np.sqrt(mean_squared_error(prophet_test['y'], prophet_forecast))
prophet_mae  = mean_absolute_error(prophet_test['y'], prophet_forecast)
print(f"Prophet -> RMSE: {prophet_rmse:.2f}, MAE: {prophet_mae:.2f}")

prophet_model.plot_components(forecast.reset_index())
plt.show()

# =====================
# タスク B-4:モデル比較
# =====================

print("Model Comparison:")
print(f"ARIMA  -> RMSE: {arima_rmse:.2f}, MAE: {arima_mae:.2f}")
print(f"LSTM   -> RMSE: {lstm_rmse:.2f}, MAE: {lstm_mae:.2f}")
print(f"Prophet-> RMSE: {prophet_rmse:.2f}, MAE: {prophet_mae:.2f}")

# %% コメント
# - ARIMAは伝統的な時系列解析手法であり、自己相関に基づいたパラメータチューニングにより短期予測に強みを示す一方、非線形パターンや急激な変動には限界があります。
# - LSTMは深層学習により非線形な関係性を学習できるため、複雑なパターン認識に有利ですが、エポック数の設定やデータ量により過学習リスクに注意が必要です。
# - Prophetは傾向・季節性成分の分解により中期的な予測に有効ですが、市場の急変(ブラックスワンリスクなど)には対応が難しい場合があります。
# 統合的な評価としては、短期予測ではARIMAやLSTM、全体の季節性把握や中期予測ではProphetの結果に注目するのが有効です。
Training period: 2018-01-01 ~ 2021-12-31
Test period: 2022-01-01 ~ 2022-12-31
GridSearch for ARIMA hyperparameters ...
Best ARIMA order: (3, 1, 3) with AIC: 24430.64
ARIMA -> RMSE: 21132.67, MAE: 18541.06
Epoch 1/10, Loss: 0.034080
Epoch 2/10, Loss: 0.001666
Epoch 3/10, Loss: 0.001141
Epoch 4/10, Loss: 0.001081
Epoch 5/10, Loss: 0.001121
Epoch 6/10, Loss: 0.000942
Epoch 7/10, Loss: 0.000877
Epoch 8/10, Loss: 0.000984
Epoch 9/10, Loss: 0.000792
Epoch 10/10, Loss: 0.000806
LSTM -> RMSE: 1650.99, MAE: 1109.98
13:55:56 - cmdstanpy - INFO - Chain [1] start processing
13:55:56 - cmdstanpy - INFO - Chain [1] done processing
Prophet -> RMSE: 22361.84, MAE: 20692.05
Model Comparison:
ARIMA  -> RMSE: 21132.67, MAE: 18541.06
LSTM   -> RMSE: 1650.99, MAE: 1109.98
Prophet-> RMSE: 22361.84, MAE: 20692.05

この分析から得られた情報

モデル評価指標 (RMSE, MAE)主な手法/特徴備考
ARIMA短期・中期ともに評価済みグリッドサーチで (p,d,q) の組み合わせ(例: (3,1,3))を探索し、AICにより最適パラメータを選定短期予測で強みがあり、パラメータ調整による精度向上が可能
LSTM短期予測で高精度データの正規化、シーケンス作成を経て非線形パターンの学習を実施。予測結果は元のスケールに戻し、実際の価格と比較非線形なパターン認識に優れ、グラフで視覚的に評価が可能
Prophet中期予測で高精度傾向や季節性を自動抽出し、データ整形からモデルフィッティング、将来予測を実施中期的なトレンド把握や季節性の捕捉に強み
統合比較各モデル間で数値評価済みARIMA・LSTMは短期的な価格変動の予測に有用、Prophetは中期的なトレンドと季節性の把握に優れる各モデルの特性を踏まえた戦略策定により、全体の予測精度向上が期待できる

この工程では、RMSE、MAE、AICなどの具体的な指標と視覚的・数値的な比較を用いて複数の手法の予測精度を定量評価し、用途や予測期間に適した手法を判断できました。

取引戦略のバックテスト(o3-mini-high 推奨)

次は「前処理データ」と「統計的分析で得られた情報」「機械学習による価格予測」から得られた情報を用いて取引戦略のバックテストを行います。今回も推奨通り、引き続きo3-mini-highモデルを使っていきます。

生成したプロンプトには「このプロンプトで使うデータは以下のコードから抽出した前処理データと統計的分析から得られた情報、機械学習による価格予測で得られた情報を使ってください」と入力し、先ほど得られた3つのコードをo3-mini-highのプロンプトを入力する前に入力します。

  • タスク内容
    • 取引ロジック(テクニカル指標 + 予測シグナル)とバックテストフレームワークの構築を要求。大規模モデルなしでどこまで詳しい実装と説明ができるかの「限界」例。

プロンプト例:

このプロンプトで使うデータは以下のコードから抽出した前処理データを使ってください。
また、バックテストを行う際は統計的分析から得られた情報や機械学習による価格予測で得られた情報(LSTM)を使うことを前提に以下の条件を満たしてください。

【先ほどo3-miniで生成した「データ収集・前処理」「統計的分析」「機械学習による価格予測」のコードを入力します】

あなたは暗号通貨トレーダー兼開発者です。
BTC-USDの日足データを使い、テクニカル指標と機械学習予測シグナルを組み合わせた
取引戦略のバックテストコードを示してください。

コード要件:
1. 特徴量・シグナル生成:
   - テクニカル指標: 20日移動平均(MA_20)、50日移動平均(MA_50)、RSI(14期間)
   - 機械学習予測シグナル: 簡易的に、前日の終値とボラティリティなどを入力した小型のランダムフォレストで
     「翌日のリターンが正か負か」を分類する想定コードを記述(sklearnで可)。
   - シグナル統合: 
     - MAクロス(MA_20 > MA_50)かつ ランダムフォレストの予測が「上昇」なら「買い」シグナル
     - 逆の場合は「売り」シグナル
     - それ以外はホールド

2. バックテストロジック:
   - 初期資金: 10,000ドル
   - 1単位(1 BTC相当)までしかポジションを持たない単純シミュレーション
   - 手数料やスリッページは0.1%と仮定(売買ごとに資金から差し引く例を示す)
   - ストップロス: エントリー価格から10%下落したら即クローズするロジックを組み込む

3. 評価指標:
   - 最終残高、最大ドローダウン、シャープレシオ(リスクフリー金利0想定)を計算
   - 売買回数や勝率をprintし、どのシグナルが有効だったかをコメントで考察

4. ビジュアル:
   - 値動きと売買ポイントをmatplotlibでプロット(買い時点を緑の▲、売り時点を赤の▼などで表示)
   - 残高の推移グラフを別枠で描画

5. コメント:
   - 過去データへの過適合リスクや、機械学習モデルの精度による戦略性能の変動に言及
   - 日足ベースの取引なので、実運用でのスリッページや大きなギャップダウンのリスクなどもコメントで補足

その他:
- ランダムフォレストでの予測モデル構築はあくまでデモ用。実際にはハイパーパラメータチューニング等のステップが必要な旨をコメントで触れてください。
- コード全体が1つのファイルで動くように書いてください。

回答:

以下のコードは前処理済みデータ(df_clean)から、テクニカル指標と2種類の機械学習モデル(ランダムフォレストとLSTM分類器)による翌日リターン予測シグナルを統合し、バックテストを実施する完全なコード例です。

特徴量・シグナル生成

  • 特徴量計算
    ・BTCの終値(BTC_Closeカラム)を「Close」として抽出し、時系列ソート後に欠損行を除去。
    ・移動平均(MA_20、MA_50)を算出して、トレンドを把握。
    ・RSI(RSI_14)は、価格変動から計算し、買われすぎ・売られすぎの状態を示す指標としています。
    ・1日リターン(Return_1d)および20日ボラティリティ(Volatility_20)も評価して、市場の変動性を捉えます。
  • シグナル生成
    ・ランダムフォレスト(RF)を用い、翌日リターンが正か否か(ターゲットクラス)を予測し、その結果を RF_Signal として保存。
    ・また、LSTMモデルでは、過去5日分の連続した特徴量を用いて翌日の上昇(クラス1)/下降(クラス0)を分類。予測結果は LSTM_Signal として出力。
    ・さらに、テクニカル指標の一つであるMAクロス(短期MAと長期MAのクロス判定)を計算し、MACrossカラムに1(上昇トレンド)または-1(下降トレンド)を設定。
    ・これら複数のシグナル(RF_Signal、LSTM_Signal、MACross)を組み合わせ、多数決的な統合ロジックを用いて最終シグナル(Signalカラム)を決定しています。

バックテストロジック

  • 両建て・積み増し可能なエントリー(backtest_trading_multi 関数)
    • 初期資金は10,000ドル、1回の取引は最大1BTCまで
    • 売買ごとに0.1%の手数料を差し引く
      ・ロングとショートのポジションはそれぞれ独立のリストで管理し、エントリー時に既存ポジションをクローズせずに「積み増し」できるよう構築。
      ・買い(シグナル 1)や売り(シグナル -1)のシグナルが発生したとき、資金が許すなら新規のエントリーを追加。
      ・ロングの場合は、購入コストに手数料(1.001倍)をかけ、その後現金残高から差し引いてエントリー。
      ・ショートの場合、売却時に即座に現金の受領(0.999倍)を行い、空売りポジションとして追加します。両建て・積み増し可能なエントリー(backtest_trading_multi 関数)
      ・ロングとショートのポジションはそれぞれ独立のリストで管理し、エントリー時に既存ポジションをクローズせずに「積み増し」できるよう構築。
      ・買い(シグナル 1)や売り(シグナル -1)のシグナルが発生したとき、資金が許すなら新規のエントリーを追加。
      ・ロングの場合は、購入コストに手数料(1.001倍)をかけ、その後現金残高から差し引いてエントリー。
      ・ショートの場合、売却時に即座に現金の受領(0.999倍)を行い、空売りポジションとして追加します。

評価指標とビジュアル

  • 評価指標
    ・最終エクイティ(FinalEquity):シミュレーション終了時の総資産。
    ・最大ドローダウン(MaxDrawdown):ピーク時からの下落率の最大値。
    ・シャープレシオ(SharpeRatio):日次リターンの平均と標準偏差から、年率換算(√252)で算出。
    ・取引回数(TradeCount)と勝率(WinRate):いくつの取引が行われ、利益が出た割合が計算されます。
  • ビジュアル
    ・BTCの終値と取引ポイント(エントリー・決済時の価格)を散布図でプロットし、一目で各取引のタイミングを確認可能。
    ・また、エクイティカーブ(各日のエクイティ推移)をプロットし、資産の推移やドローダウンを視覚的に把握できるようにしています。

注意事項

  • データの前処理
    ・df_cleanからBTC_Closeカラムを抽出後、インデックスが時系列にソートされ、欠損行が除かれていることが前提となっています。
  • モデル精度とシグナル統合
    ・ランダムフォレストやLSTMの予測結果は、ハイパーパラメータに依存するため、シグナルの質や取引回数に影響します。
    ・3つ(MAクロス、RF、LSTM)のすべてに一致する厳格な条件だと取引回数が少なくなる可能性があるため、シグナル統合ルールの緩和も検討する必要があります。
  • バックテスト仮定
    ・バックテストは日次で行われ、急変時のギャップダウン、約定不可リスク、スリッページ等は考慮されていません。
    ・両建てや積み増し戦略では、実際のマーケット環境下でのリスク管理(マージン管理、リバランスなど)も重要となります。
  • 実運用時の注意
    ・オンライン学習やウォークフォワード検証などを行い、モデルを随時アップデートする運用手法が望まれます。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")

# ==========
# 機械学習
# ==========
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score

# ==========
# PyTorch (LSTM)
# ==========
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader

# ==========
# コメント:
# df_clean はユーザー環境で既に定義済みとし、
# BTC_Close が含まれ、Index が時系列(UTC)でソート済みのデータとする。
# ==========


def compute_rsi(series, period=14):
    """
    シンプルなRSI計算関数(価格差ベース)
    RSI = 100 - [100 / (1 + RS)]
    RS = (上昇幅平均) / (下落幅平均)
    """
    delta = series.diff()
    up = delta.clip(lower=0)
    down = -1*delta.clip(upper=0)

    roll_up = up.rolling(period).mean()
    roll_down = down.rolling(period).mean()

    rs = roll_up / roll_down
    rsi = 100.0 - (100.0 / (1.0 + rs))
    return rsi


def backtest_trading_multi(df):
    """
    両建て(ロング・ショート両方を同時に保有可能)でのバックテストシミュレーション

    ルール:
      - シグナル: 1 = 買いシグナル(ロングエントリー)、-1 = 売りシグナル(ショートエントリー)、0 = ホールド
      - 新規エントリー時、既存のポジション(逆方向・同方向)はクローズせずに積み増し可能
      - ストップロス:
            ・ロング: 価格がエントリー価格の90%未満になったら自動決済
            ・ショート: 価格がエントリー価格の110%超になったら自動決済
      - ロングエントリー時は、購入コスト=price * position_size * 1.001 を現金から差し引く
      - ショートエントリー時は、即時に売却して price * position_size * 0.999 を現金として受け取る
      - 決済時も手数料を反映(ロングクローズ: 売却時 price * position_size * 0.999、ショートクローズ: 買戻し時 price * position_size * 1.001)
      - エクイティは、現金資産(capital)+各ポジションの未実現損益で評価

    戻り値:
      df_bt: 取引記録と日次エクイティ推移を含むDataFrame
      metrics: 最終エクイティ、最大ドローダウン、シャープレシオ、取引回数、勝率の辞書
    """
    position_size = 1.0      # 1BTC単位
    initial_capital = 10000.0
    capital = initial_capital

    long_positions = []      # ロングエントリー価格リスト
    short_positions = []     # ショートエントリー価格リスト

    # 各日1件分のトレード情報を記録するリスト
    trade_price_list = []
    trade_signal_list = []
    trade_count = 0
    win_count = 0

    equity_list = []         # 各日の総エクイティ(現金+未実現損益)
    capital_list = []        # 各日の現金残高
    peak_equity = initial_capital
    max_drawdown = 0.0

    # 日次ループ(dfはClose価格とSignalを含む)
    # ※各日1件のトレード情報を保持するため、各日ごとに初期化
    for i in range(len(df)):
        price = float(df['Close'].iloc[i])
        signal = int(df['Signal'].iloc[i])
        # 各日のトレード記録(複数発生した場合は最後のものを採用)
        day_trade_price = np.nan
        day_trade_signal = np.nan

        # --- ストップロス処理 ---
        closed_long_indices = []
        for j, entry in enumerate(long_positions):
            if price < entry * 0.90:
                proceeds = price * position_size * 0.999
                cost = entry * position_size * 1.001
                pnl = proceeds - cost
                capital += proceeds
                trade_count += 1
                if pnl > 0:
                    win_count += 1
                day_trade_price = price
                day_trade_signal = -1   # ロング決済シグナル
                closed_long_indices.append(j)
        for idx in sorted(closed_long_indices, reverse=True):
            del long_positions[idx]

        closed_short_indices = []
        for j, entry in enumerate(short_positions):
            if price > entry * 1.10:
                cost = price * position_size * 1.001
                proceeds = entry * position_size * 0.999
                pnl = proceeds - cost
                capital += pnl
                trade_count += 1
                if pnl > 0:
                    win_count += 1
                day_trade_price = price
                day_trade_signal = 2    # ショート決済シグナル
                closed_short_indices.append(j)
        for idx in sorted(closed_short_indices, reverse=True):
            del short_positions[idx]

        # --- 新規エントリー ---
        if signal == 1:
            entry_cost = price * position_size * 1.001
            if entry_cost <= capital:
                capital -= entry_cost
                long_positions.append(price)
                trade_count += 1
                day_trade_price = price
                day_trade_signal = 1   # ロングエントリーシグナル
            else:
                day_trade_price = np.nan
                day_trade_signal = np.nan
        elif signal == -1:
            proceeds = price * position_size * 0.999
            capital += proceeds
            short_positions.append(price)
            trade_count += 1
            day_trade_price = price
            day_trade_signal = -2      # ショートエントリーシグナル
        # (信号が0の場合、day_trade_price/signalはそのままNaN)

        # --- 未実現損益の計算 ---
        unrealized_long = sum((price - entry) * position_size for entry in long_positions)
        unrealized_short = sum((entry - price) * position_size for entry in short_positions)
        equity = capital + unrealized_long + unrealized_short

        equity_list.append(equity)
        capital_list.append(capital)

        # --- ドローダウン計算 ---
        if equity > peak_equity:
            peak_equity = equity
        dd = (peak_equity - equity) / peak_equity
        if dd > max_drawdown:
            max_drawdown = dd

        # 各日1件として記録
        trade_price_list.append(day_trade_price)
        trade_signal_list.append(day_trade_signal)

    df_bt = df.copy()
    df_bt['Equity'] = equity_list
    df_bt['Capital'] = capital_list
    df_bt['TradePrice'] = trade_price_list
    df_bt['TradeSignal'] = trade_signal_list

    final_equity = equity_list[-1]
    daily_return = pd.Series(equity_list).pct_change()
    df_bt['DailyReturn'] = daily_return
    sharpe = (daily_return.mean() / daily_return.std()) * np.sqrt(252) if daily_return.std() != 0 else 0
    win_rate = win_count / trade_count if trade_count > 0 else 0.0

    metrics = {
        'FinalEquity': final_equity,
        'MaxDrawdown': max_drawdown,
        'SharpeRatio': sharpe,
        'TradeCount': trade_count,
        'WinRate': win_rate
    }
    return df_bt, metrics


# --- LSTM 用クラス等定義 ---
class LSTMDataset(Dataset):
    """
    LSTMで翌日リターンが正(1)/負(0)を予測するためのシーケンスデータセット
    """
    def __init__(self, X, y):
        # X: (N, seq_len, feature_dim)
        # y: (N,)
        self.X = torch.tensor(X, dtype=torch.float32)
        self.y = torch.tensor(y, dtype=torch.long)
    def __len__(self):
        return len(self.X)
    def __getitem__(self, idx):
        return self.X[idx], self.y[idx]


class LSTMClassifier(nn.Module):
    """
    シンプルな2層LSTMで分類 (出力=2クラス)
    """
    def __init__(self, input_size=3, hidden_size=32, num_layers=2, output_size=2):
        super().__init__()
        self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
        self.fc   = nn.Linear(hidden_size, output_size)
    def forward(self, x):
        # x shape: (batch, seq_len, input_size)
        h0 = torch.zeros(2, x.size(0), 32)
        c0 = torch.zeros(2, x.size(0), 32)
        out, (hn, cn) = self.lstm(x, (h0, c0))
        out = out[:, -1, :]
        out = self.fc(out)
        return out


def main():
    """
    df_clean から 'BTC_Close' を抽出し、'Close' として扱う。
    テクニカル指標 (MA_20, MA_50, RSI_14) を計算しつつ、
    - ランダムフォレスト(RF)
    - LSTM
    で翌日リターン(正/負)を分類予測するシグナルを作り、
    MAクロスと合わせて売買判断 => バックテストを実施。
    """
    # ============================================
    # 0. データ準備 (df_clean -> df_btc)
    # ============================================
    global df_clean  # グローバル定義済みを想定
    try:
        df_btc = df_clean[['BTC_Close']].copy()
        df_btc = df_btc.rename(columns={'BTC_Close': 'Close'})
    except KeyError:
        if isinstance(df_clean.columns, pd.MultiIndex):
            btc_close_col = [col for col in df_clean.columns if 'BTC_Close' in col[0] or 'Close' in col[0]]
            if btc_close_col:
                df_btc = df_clean[btc_close_col[0]].to_frame()
                df_btc = df_btc.rename(columns={btc_close_col[0]: 'Close'})
            else:
                raise KeyError("BTC_Close カラムが見つかりません")
        else:
            raise KeyError("BTC_Close カラムが見つかりません")
    df_btc.sort_index(inplace=True)
    df_btc.dropna(inplace=True)
    print("データ準備完了 - 行数:", len(df_btc))

    # ============================================
    # 1. テクニカル指標
    # ============================================
    df_btc['MA_20'] = df_btc['Close'].rolling(20).mean()
    df_btc['MA_50'] = df_btc['Close'].rolling(50).mean()
    df_btc['RSI_14'] = compute_rsi(df_btc['Close'], period=14)
    df_btc['Return_1d'] = df_btc['Close'].pct_change()
    df_btc['Volatility_20'] = df_btc['Return_1d'].rolling(20).std()
    df_btc.dropna(inplace=True)
    print("テクニカル指標計算後 - 行数:", len(df_btc))

    # ============================================
    # 2. ランダムフォレストによるシグナル (RF_Signal)
    #    翌日のリターン > 0 => 1, <= 0 => 0
    # ============================================
    df_btc['TargetClass'] = (df_btc['Return_1d'].shift(-1) > 0).astype(int)
    feature_cols = ['Close', 'Volatility_20', 'RSI_14']

    X_all = df_btc[feature_cols].values
    y_all = df_btc['TargetClass'].values
    split_idx = int(len(df_btc) * 0.8)

    X_train, y_train = X_all[:split_idx], y_all[:split_idx]
    X_test, y_test = X_all[split_idx:], y_all[split_idx:]

    scaler = StandardScaler()
    X_train_scaled = scaler.fit_transform(X_train)
    X_test_scaled  = scaler.transform(X_test)

    rf = RandomForestClassifier(n_estimators=50, random_state=42)
    rf.fit(X_train_scaled, y_train)
    y_pred_test_rf = rf.predict(X_test_scaled)
    acc_rf = accuracy_score(y_test, y_pred_test_rf)

    print(f"[RF] Test Accuracy = {acc_rf:.4f}")

    X_all_scaled = scaler.transform(X_all)
    df_btc['RF_Signal'] = rf.predict(X_all_scaled)

    # ============================================
    # 3. LSTMによるシグナル (LSTM_Signal)
    #    同じ特徴量 + 連続過去何日分かを入力 => 翌日(正or負)
    # ============================================
    seq_len = 5

    def make_lstm_dataset(feat_array, label_array, seq_len=5):
        X_seq, Y_seq = [], []
        for i in range(len(feat_array) - seq_len):
            X_seq.append(feat_array[i:i+seq_len])
            Y_seq.append(label_array[i+seq_len])
        return np.array(X_seq), np.array(Y_seq)
    
    X_seq, Y_seq = make_lstm_dataset(X_all_scaled, y_all, seq_len)
    split_seq = int(len(X_seq) * 0.8)

    X_seq_train, Y_seq_train = X_seq[:split_seq], Y_seq[:split_seq]
    X_seq_test, Y_seq_test = X_seq[split_seq:], Y_seq[split_seq:]

    train_dataset = LSTMDataset(X_seq_train, Y_seq_train)
    test_dataset = LSTMDataset(X_seq_test, Y_seq_test)

    train_loader = DataLoader(train_dataset, batch_size=16, shuffle=True)

    lstm_model = LSTMClassifier(input_size=len(feature_cols), hidden_size=32, num_layers=2, output_size=2)
    criterion = nn.CrossEntropyLoss()
    optimizer = optim.Adam(lstm_model.parameters(), lr=0.001)

    epochs = 5
    lstm_model.train()
    for ep in range(epochs):
        losses = []
        for x_batch, y_batch in train_loader:
            optimizer.zero_grad()
            outputs = lstm_model(x_batch)
            loss = criterion(outputs, y_batch)
            loss.backward()
            optimizer.step()
            losses.append(loss.item())
        print(f"Epoch {ep+1}/{epochs}, Loss = {np.mean(losses):.6f}")

    lstm_model.eval()
    with torch.no_grad():
        X_seq_test_t = torch.tensor(X_seq_test, dtype=torch.float32)
        logits_test = lstm_model(X_seq_test_t)
        preds_test_lstm = torch.argmax(logits_test, dim=1).numpy()
    acc_lstm = accuracy_score(Y_seq_test, preds_test_lstm)
    print(f"[LSTM] Test Accuracy = {acc_lstm:.4f}")

    lstm_valid_index = df_btc.index[seq_len:]
    X_seq_all, _ = make_lstm_dataset(X_all_scaled, y_all, seq_len)
    X_seq_all_t = torch.tensor(X_seq_all, dtype=torch.float32)

    with torch.no_grad():
        output_all = lstm_model(X_seq_all_t)
        lstm_preds_all = torch.argmax(output_all, dim=1).numpy()
    df_btc['LSTM_Signal'] = np.nan
    df_btc.loc[lstm_valid_index, 'LSTM_Signal'] = lstm_preds_all

    # ============================================
    # 4. シグナル統合(ベクトル化バージョン)
    #    MAクロス + RF_Signal + LSTM_Signal (多数決など)
    #    例: (MA_20>MA_50) & (RF=1) & (LSTM=1) => 買い
    #         (MA_20<MA_50) & (RF=0) & (LSTM=0) => 売り
    #    それ以外 => ホールド
    # ============================================
    df_btc['MACross'] = 0
    df_btc.loc[df_btc['MA_20'] > df_btc['MA_50'], 'MACross'] = 1
    df_btc.loc[df_btc['MA_20'] < df_btc['MA_50'], 'MACross'] = -1
    df_btc['Signal'] = 0

    buy_condition = (
        (df_btc['MACross'] == 1) &
        (df_btc['RF_Signal'] == 1) &
        (df_btc['LSTM_Signal'] == 1)
    )
    buy_condition = buy_condition.fillna(False)

    sell_condition = (
        (df_btc['MACross'] == -1) &
        (df_btc['RF_Signal'] == 0) &
        (df_btc['LSTM_Signal'] == 0)
    )
    sell_condition = sell_condition.fillna(False)

    df_btc.loc[buy_condition, 'Signal'] = 1
    df_btc.loc[sell_condition, 'Signal'] = -1

    buy_count = sum(df_btc['Signal'] == 1)
    sell_count = sum(df_btc['Signal'] == -1)
    hold_count = sum(df_btc['Signal'] == 0)

    print(f"\n生成されたシグナル: 買い={buy_count}, 売り={sell_count}, ホールド={hold_count}")

    # ============================================
    # 5. バックテスト
    # ============================================
    df_bt, metrics = backtest_trading_multi(df_btc)

    final_cap = metrics['FinalEquity']
    max_dd = metrics['MaxDrawdown']

    sr = metrics['SharpeRatio']
    tc = metrics['TradeCount']
    wr = metrics['WinRate']

    print("\n=== Backtest Result (Combined Strategy) ===")
    print(f"Final Capital : {final_cap:,.2f}")
    print(f"Max Drawdown  : {max_dd*100:.2f}%")
    print(f"Sharpe Ratio  : {sr:.2f}")
    print(f"Trade Count   : {tc}")
    print(f"Win Rate      : {wr*100:.2f}%")

    # ============================================
    # 6. 可視化
    # ============================================
    # (A) 値動き + 売買ポイント
    plt.figure(figsize=(12,6))
    plt.plot(df_bt.index, df_bt['Close'], label='BTC Close')

    buy_idx  = df_bt[df_bt['TradeSignal']==1].index
    sell_idx = df_bt[df_bt['TradeSignal']==-1].index
    
    plt.scatter(buy_idx, df_bt.loc[buy_idx, 'TradePrice'], marker='^', color='green', s=100, label='Buy')
    plt.scatter(sell_idx, df_bt.loc[sell_idx, 'TradePrice'], marker='v', color='red', s=100, label='Sell')
    plt.title("BTC Price with Buy/Sell Signals (RF + LSTM + MA Cross)")
    plt.xlabel("Date")
    plt.ylabel("Price (USD)")
    plt.legend()
    plt.show()

    # (B) 残高推移
    plt.figure(figsize=(12,6))
    plt.plot(df_bt.index, df_bt['Equity'], label='Equity')
    plt.title("Equity Curve")
    plt.xlabel("Date")
    plt.ylabel("Equity (USD)")
    plt.legend()
    plt.show()

    # コメント
    print("""
[コメント]
・日足ベース取引のため、急変時の大きなギャップダウンや約定不可リスクが考慮されていません。
・ランダムフォレスト / LSTM ともにハイパーパラメータ次第で精度が大きく変動します(過学習リスク)。
・MAクロス, RF, LSTM の3つすべてに一致を求めると取引回数は少なめになります。
  - より多くの取引を行いたい場合は、2つ以上の一致など条件を緩和することも検討できます。
・実運用では逐次アップデート(オンライン学習)やウォークフォワード検証が望ましいでしょう。
""")

if __name__ == "__main__":
    main()
データ準備完了 - 行数: 1826
テクニカル指標計算後 - 行数: 1777
[RF] Test Accuracy = 0.5028
Epoch 1/5, Loss = 0.692402
Epoch 2/5, Loss = 0.692052
Epoch 3/5, Loss = 0.691637
Epoch 4/5, Loss = 0.691513
Epoch 5/5, Loss = 0.691475
[LSTM] Test Accuracy = 0.4704

生成されたシグナル: 買い=399, 売り=45, ホールド=1333

=== Backtest Result (Combined Strategy) ===
Final Capital : 2,170,037.48
Max Drawdown  : 101.13%
Sharpe Ratio  : 0.73
Trade Count   : 206
Win Rate      : 0.00%

[コメント]

  • 日足ベース取引のため、急変時の大きなギャップダウンや約定不可リスクが考慮されていません。
  • ランダムフォレスト / LSTM ともにハイパーパラメータ次第で精度が大きく変動します(過学習リスク)。
  • MAクロス, RF, LSTM の3つすべてに一致を求めると取引回数は少なめになります。 – より多くの取引を行いたい場合は、2つ以上の一致など条件を緩和することも検討できます。
  • 実運用では逐次アップデート(オンライン学習)やウォークフォワード検証が望ましいでしょう。

以上から取引数を増やす場合の方法やオンライン学習、ウォークフォワード法を取り入れてみることでこのモデルの改善を図れる可能性があることが分かります。

リスク管理(o1 推奨)

次は「前処理データ」と「統計的分析で得られた情報」「機械学習による価格予測」「取引戦略のバックテスト」から得られた情報を用いてリスク管理を行います。今回は推奨通り、o1モデルを使っていきます。

生成したプロンプトには「このプロンプトで使うデータは以下のコードから抽出した前処理データと統計的分析から得られた情報、機械学習による価格予測、取引戦略のバックテストで得られた情報を使ってください」と入力し、先ほど得られた3つのコードをo3-mini-highのプロンプトを入力する前に入力します。

  • タスク内容
    • 複数シナリオ分析・ストップロスやレバレッジ管理などを含む高度なリスク管理を要求。o1モデルで限界に迫るような幅広い要件を例示。

プロンプト例:

このプロンプトで使うデータは以下のコードから抽出した前処理データを使ってください。
また、リスク管理を行う際は統計的分析から得られた情報や機械学習による価格予測で得られた情報(LSTM)、バックテスト取引で得られた情報を使うことを前提に以下の条件を満たしてください。

【先ほどo3-miniで生成した「データ収集・前処理」「統計的分析」「機械学習による価格予測」「取引戦略のバックテスト」のコードを入力します】

あなたは暗号通貨ポートフォリオのリスク管理を担当するクオンツです。
以下の要件を満たすPythonコードと解説を示してください。

1. シナリオ分析:
   - BTC, ETH それぞれの日次データをもとに、リターンの分布をモンテカルロシミュレーションで1,000パターン生成(正規分布近似でOK)。
   - 同時に、極端な暴落(例: -30%や-50%)が発生する極値理論(EVT)っぽいシナリオも数パターン追加で考慮する。
   - 各シナリオに対して、ポートフォリオ全体の評価損益を試算。

2. リスク指標計算:
   - VaR (Value at Risk) を95%, 99%水準で計算。
   - CVaR (Expected Shortfall) も同様に計算し、結果をprintする。
   - シャープレシオや最大ドローダウンといった従来指標との対比をコメントで述べる。

3. ストップロスとポジションサイジング:
   - ポートフォリオに占めるBTCとETHの比率を可変にし、10%刻みぐらいでリスクリターンを比較するコードを入れる。
   - 例: BTC 60%, ETH 40% → VaR、CVaR、期待リターンを表示
   - 一定以上のDD(ドローダウン)が発生した場合に自動的にポジション縮小する手法の疑似例を示す(コードで実装例)。

4. 結果の可視化:
   - ヒストグラムや箱ひげ図を使い、シミュレーション結果の分布を可視化する。
   - VaR/CVaRラインをグラフ上に描画し、リスクの意味をコメントで解説。

5. 総合的評価:
   - ブラックスワンリスク(極端な市場崩壊)のシナリオでは、単なるVaRでは対応できないことを1~2行で言及。
   - マルチアセット運用における相関構造の変動(リスクオフ時に相関が高まるなど)を簡単にコメント。

その他:
- pandas, numpy, matplotlib, scipy.stats などのライブラリを使う想定。
- コード内で関数化してもよいが、メインのフローがわかりやすいように書いてください。
- 出力例(数値やグラフ)をイメージしやすいようにタイトルやラベルをしっかり付けること。

回答:

以下のサンプルコードでは、**「BTC と ETH の日次リターンをベースにしたリスク管理」**を想定し、

  1. シナリオ分析 (モンテカルロ & EVT風の極値シナリオ)
  2. VaR・CVaRの計算
  3. ポートフォリオ配分比率(BTC/ETH)の変更 & ストップロス疑似例
  4. 結果の可視化 (ヒストグラム・箱ひげ図) とリスク指標ライン
  5. 総合的評価 & ブラックスワンリスクへの言及

を行います。ユーザー環境で既に df_clean が定義され、BTC_Close, ETH_Close カラムが含まれているものとします。

: コード内で示した各種パラメータ(シナリオ数、暴落率、ポートフォリオ配分刻みなど)はデモ用に簡略化しています。実務では頻繁なチューニングや外部市場データの取り込み等が必要です。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")

from scipy.stats import norm, t
# 必要なら EVTライブラリ(pot, extreme_value_theory) などを使うこともあるが、ここでは擬似的に実装

def main():
    """
    暗号通貨ポートフォリオにおけるリスク管理シミュレーション

    1. シナリオ分析 (モンテカルロ & 極値理論による暴落シナリオ)
    2. VaR, CVaR, シャープレシオ, ドローダウンなどの計算
    3. ポートフォリオ比率を変化させてリスク指標を比較
    4. 可視化 (ヒストグラム & 箱ひげ図 & VaR/CVaRライン)
    5. 総合評価 (ブラックスワンリスク・相関構造変動などコメント)
    """

    global df_clean  # df_clean には BTC_Close, ETH_Close が入っている想定

    # ==========================
    # 0. データ準備
    # ==========================
    df_risk = df_clean[['BTC_Close', 'ETH_Close']].copy()
    df_risk = df_risk.dropna()
    df_risk.sort_index(inplace=True)
    df_risk['BTC_Return'] = df_risk['BTC_Close'].pct_change()
    df_risk['ETH_Return'] = df_risk['ETH_Close'].pct_change()
    df_risk.dropna(inplace=True)

    btc_returns = df_risk['BTC_Return'].values
    eth_returns = df_risk['ETH_Return'].values

    # 日次リターンの平均・標準偏差を算出
    mu_btc = np.mean(btc_returns)
    sigma_btc = np.std(btc_returns)
    mu_eth = np.mean(eth_returns)
    sigma_eth = np.std(eth_returns)

    # 相関係数 (BTCとETH)
    corr = np.corrcoef(btc_returns, eth_returns)[0,1]

    print(f"BTC mean={mu_btc:.4%}, std={sigma_btc:.4%}")
    print(f"ETH mean={mu_eth:.4%}, std={sigma_eth:.4%}")
    print(f"Correlation(BTC,ETH)={corr:.2f}\n")

    # ==========================
    # 1. シナリオ分析
    # ==========================
    # --- 1.1 モンテカルロシミュレーション  ---
    # 2次元正規分布を仮定 (BTC, ETH) の組み合わせ1000パターン
    # 共分散行列を構築
    cov_matrix = np.array([
        [sigma_btc**2, corr*sigma_btc*sigma_eth],
        [corr*sigma_btc*sigma_eth, sigma_eth**2]
    ])
    # 乱数生成: 2次元正規
    num_sims = 1000
    sim_data = np.random.multivariate_normal(
        mean=[mu_btc, mu_eth],
        cov=cov_matrix,
        size=num_sims
    )
    # sim_data[:,0] => BTCリターン想定, sim_data[:,1] => ETHリターン想定

    # --- 1.2 極値理論(EVT)っぽい暴落シナリオ ---
    # 例: BTCが-30%, ETHが-50%など極端な値を数件加える
    evt_scenarios = np.array([
        [-0.3, -0.3],
        [-0.3, -0.5],
        [-0.5, -0.3],
        [-0.5, -0.5]
    ])
    # EVTシナリオも合わせて計算するため、シミュレーションに結合
    sim_data_all = np.concatenate([sim_data, evt_scenarios], axis=0)

    # ==========================
    # 2. リスク指標計算 (VaR, CVaR)
    # ==========================
    # ※まずは特定のポートフォリオ比率で計算例を示す (例: BTC 50%, ETH 50%)
    #   シミュレーションした日次リターン => 1日リターン分布
    w_btc = 0.5
    w_eth = 0.5
    portfolio_returns = w_btc * sim_data_all[:,0] + w_eth * sim_data_all[:,1]

    # 分位点を求める => 95%, 99% VaR
    alpha_list = [0.95, 0.99]
    sorted_returns = np.sort(portfolio_returns)
    n = len(sorted_returns)
    for alpha in alpha_list:
        idx = int((1 - alpha) * n)  # 下側パーセンタイル
        var_value = -sorted_returns[idx]  # 損失をプラスに表現
        # CVaR (ES): var_value以下の損失の平均
        tail_losses = -sorted_returns[:idx]  # その下側の損失
        cvar_value = np.mean(tail_losses) if len(tail_losses)>0 else np.nan
        print(f"Portfolio({w_btc*100:.0f}% BTC, {w_eth*100:.0f}% ETH) {int(alpha*100)}% VaR = {var_value:.2%}, CVaR = {cvar_value:.2%}")

    # ==========================
    # 3. ストップロス & ポジションサイジング
    # ==========================
    # 例: BTC,ETHを 0%~100%まで10%刻みで配分 -> 各ポートフォリオの期待リターン & VaR表示
    #     ドローダウンが一定以上の場合、ポジションを半分に縮小するような疑似例
    def portfolio_risk_metrics(w_btc, w_eth, sim_data):
        # w_btc + w_eth = 1 を想定
        ret = w_btc * sim_data[:,0] + w_eth * sim_data[:,1]
        mean_ret = np.mean(ret)
        sorted_r = np.sort(ret)
        n = len(sorted_r)
        var95 = -sorted_r[int(0.05*n)]
        cvar95 = -np.mean(sorted_r[:int(0.05*n)])
        return mean_ret, var95, cvar95

    print("\n=== ポートフォリオ配分によるリスク比較 ===")
    for w_btc_i in range(0, 101, 10):
        wb = w_btc_i/100.0
        we = 1.0 - wb
        mean_r, var95, cvar95 = portfolio_risk_metrics(wb, we, sim_data_all)
        print(f"BTC={wb*100:.0f}%, ETH={we*100:.0f}% => mean={mean_r:.2%}, VaR95={var95:.2%}, CVaR95={cvar95:.2%}")

    # ストップロスの簡易例: ドローダウンが 10% 以上発生したらポジション半分にする (疑似コード)
    # 実際にはバックテスト等に組み込みが必要。
    dd_threshold = 0.1
   
if __name__ == '__main__':
    main()
BTC mean=0.0934%, std=4.0532%
ETH mean=0.1703%, std=5.4306%
Correlation(BTC,ETH)=0.63

Portfolio(50% BTC, 50% ETH) 95% VaR = 7.22%, CVaR = 11.59%
Portfolio(50% BTC, 50% ETH) 99% VaR = 11.35%, CVaR = 23.63%

=== ポートフォリオ配分によるリスク比較 ===
BTC=0%, ETH=100% => mean=-0.06%, VaR95=8.67%, CVaR95=13.46%
BTC=10%, ETH=90% => mean=-0.08%, VaR95=8.30%, CVaR95=12.99%
BTC=20%, ETH=80% => mean=-0.10%, VaR95=7.94%, CVaR95=12.55%
BTC=30%, ETH=70% => mean=-0.12%, VaR95=7.58%, CVaR95=12.17%
BTC=40%, ETH=60% => mean=-0.13%, VaR95=7.46%, CVaR95=11.84%
BTC=50%, ETH=50% => mean=-0.15%, VaR95=7.22%, CVaR95=11.59%
BTC=60%, ETH=40% => mean=-0.17%, VaR95=7.21%, CVaR95=11.39%
BTC=70%, ETH=30% => mean=-0.19%, VaR95=6.98%, CVaR95=11.23%
BTC=80%, ETH=20% => mean=-0.21%, VaR95=6.87%, CVaR95=11.12%
BTC=90%, ETH=10% => mean=-0.23%, VaR95=7.07%, CVaR95=11.08%
BTC=100%, ETH=0% => mean=-0.25%, VaR95=7.00%, CVaR95=11.13%

出力値の情報

【基本統計量と相関係数】

各資産の日次リターンの平均と変動性、ならびにBTCとETHの相関関係を表しています。

  •  BTC mean=0.0934%, std=4.0532%
  • ETH mean=0.1703%, std=5.4306%
    • BTCの日次リターンの平均は約0.0934%(日次期待利益)である一方、標準偏差は約4.0532%となっており、これはリスクの大きさ、すなわちリターンのばらつきがかなり大きいことを示しています。ETHも同様です。
  • Correlation(BTC,ETH)=0.63
    • BTCとETHの日次リターンの相関係数は0.63であり、これは両銘柄が正の相関関係にあることを示しています。つまり、一般的に一方の価格が上昇すると、もう一方も上昇しやすい傾向が見られることになります。

【シナリオ分析(ポートフォリオリスク指標)】

50%/50%など、特定配分でのポートフォリオが一定の信頼水準下でどの程度の損失リスクを持つかを示し、リスク管理の目安となる指標(VaRは一定の確率を超えない最大損失、CVaRはその損失の平均)を提供しています。

  • Portfolio(50% BTC, 50% ETH) 95% VaR = 7.22%, CVaR = 11.59%
    • 50%ずつの等配分ポートフォリオでは、95%の信頼水準でこれ以上の損失が発生しないとされる損失閾値(VaR)は約7.22%であり、さらにVaRを超える最悪の5%の損失の平均値(CVaRまたはES)は約11.59%となっています。
  • Portfolio(50% BTC, 50% ETH) 99% VaR = 11.35%, CVaR = 23.63%
    • 50%/50%のポートフォリオにおいて、99%の信頼水準では、これ以上の損失が発生しないとされる損失閾値(VaR)は約11.35%であり、さらに最悪の1%のケースでの平均損失(CVaR)は約23.63%であることを示しています。

【ポートフォリオ配分のリスク比較】

BTCとETHの比率を変えることで、ポートフォリオの期待リターンと損失リスク(VaR、CVaR)がどのように変化するかを評価しており、どの配分がリスクとリターンのバランスが良いかを考察するための情報となります。

ここでは、BTCとETHの組み合わせのウェイトを0%~100%まで変化させた場合の、各ポートフォリオにおける期待リターン、95% VaR、95% CVaR を比較しています。以下は100%, 50%, 0%の例です。

  • BTC=0%, ETH=100% => mean=-0.06%, VaR95=8.67%, CVaR95=13.46%の場合
    • BTCとETHの割合によるポートフォリオの性能を比較すると、まず、ETH単独(BTC=0%, ETH=100%)の場合、平均リターンは約-0.06%で、95%信頼水準のVaRは約8.67%、その下位5%の平均損失(CVaR95)は約13.46%となっています。
  • BTC=50%, ETH=50% => mean=-0.15%, VaR95=7.22%, CVaR95=11.59%の場合
    • 一方、半々のポートフォリオ(BTC=50%, ETH=50%)では、平均リターンが約-0.15%、VaR95が約7.22%、CVaR95が約11.59%となっており、リスク指標がETH単独の場合より低下していることから、相関係数0.63であるため完全なヘッジにはならないものの、分散効果が現れていることがわかります。
  • BTC=100%, ETH=0% => mean=-0.25%, VaR95=7.00%, CVaR95=11.13%の場合
    • さらに、BTC単独(BTC=100%, ETH=0%)の場合は、平均リターンが約-0.25%で、VaR95が約7.00%、CVaR95が約11.13%となっており、期待リターンは低いものの、リスク指標はETH単独の場合とほぼ同等に抑えられていることが示されています。

それぞれのモデルを使ってみて

o3-mini を使ってみた感想

  • 軽量で素早い応答が得られる
    • モデルサイズが小さいため、リソースの限られた環境でも高速に動作し、推論リクエストへの応答が比較的スムーズに返ってきます。
  • 可読性があり、コードの理解に重宝できる
    • o3-miniでも十分複雑なコードに対応できているように感じました。シンプルなコードだけではなく複雑なコードまである程度の可読性があり、コーディングミスはあるものの、補正を行うことでデータ収集・前処理フローなどは十分に対応できることがわかりました。
  • 適度なヒントは必要
    • こちらから具体的な要件や条件を詳細に提示すると、それに合わせたコード例や回答を返してくれる一方で、曖昧な「何か前処理をしてください」といった指示では生成結果にばらつきが生じる印象があります。また、学習済みパラメータやモデルアーキテクチャなど、より深い情報が必要な場合、o3-mini単体では情報量が不足することもあります。

o3-mini-high を使ってみた感想

  • o3-miniより複雑な統計分析や機械学習、バックテストにも応答
    • ボラティリティ分析や自己相関のグラフ描画など、やや複雑なタスクでも、一定の品質でコード例を返してくれる印象があります。また、複数のテクニカル指標の計算や、簡単なランダムフォレストを用いた機械学習モデルの要求にも応じており、応答内容は十分に充実していると感じられます。
  • 大規模な要望を一括ですると回答が途切れがち
    • o3-mini-highは、情報量や精度は o3-mini より高めな印象ですが、必ずしも o3-mini の全ての機能を上回るわけではなく、逆に o3-mini では対応できないより複雑なコーディングに対応できる点が際立っています。また、o1モデルが推奨される場面でも、o3-mini-highを使うことで o1モデル以上の答えが得られることもあり、用途に応じて適材適所で使い分けるのが良さそうです。
    • 段階的にプロンプトを分ければ、安定的な回答が得やすいように感じました。
  • 丁寧なコーディング例や解説
    • コードのコメントや使用ライブラリの記述などはわりと丁寧に返してくれる傾向があり、「ユーザビリティ(理解しやすさ)」という点ではo3-miniよりさらに高い印象です。

o1 を使ってみた感想

  • 4oとo3miniの中間的な良さ

o1は小型モデルに比べ、「情報量の多い応答」や「ステップを網羅したコード例」を生成する力が高く感じられます。ディープラーニングやリスク管理、複数手法の比較など、工程が多岐にわたるタスクでも安定した回答が返りやすかったです。

特に私はプロンプトを生成する際にo1を使うことが多かったのですが、バランスの良い回答が得られた感触があり、コーディングがスムーズにできた気がしました。

o3-mini, o3-mini-high, o1モデルを使ってみて

今回の検証でそれぞれのモデルの印象から推論モデルはやはりコーディングが向いているだろうと断定して行ったようなところがあり、o1モデルなどに至ってはプロンプトの生成を行う際は重宝せざるを得ないような回答が多く得られた経緯がありました。

OpenAIの様々なモデルを使う中で、それぞれの局面に応じたモデルを使いこなそうとすることで、幅広い回答を得られることがわかりました。特に私はo3-miniモデルのポテンシャルについて手軽に扱えるのにポテンシャルが計り知れないような現象に出会ったので、一概にどのモデルが良いと断定して言い切ることができず、その都度違ったモデルで回答を得ることが理想的であるいう結論に至りました。

コメント

タイトルとURLをコピーしました