機械学習による仮想通貨価格予測 第3部:バックテストと自動売買システム統合【完全ガイド】
機械学習による仮想通貨価格予測 第3部:バックテストと自動売買システム統合|プロダクション実装の完全ガイド
バックテストで年率300%叩き出したのに、本番で即死した経験はあるかネ?🤔 キミだけじゃナイ、ぽんたぬきもやったヨ!!
夜中の2時にシャープレシオ3.8を見て「天才じゃん俺!!」ってガッツポーズしてたら、翌朝起きたら口座が20%溶けてたヨ……😭 やらかしちゃいましたヨ……😭 ログ見たら、バックテストの特徴量に思いっきり未来データが混入してたネ。あの夜の興奮はどこへやら、コーヒーが苦く感じたヨ……。
さて、気を取り直して!!この記事は「機械学習による仮想通貨価格予測」シリーズの第3部だヨ。第1部でデータ取得とモデル構築の基礎を、第2部で特徴量エンジニアリングと精度向上を扱ったネ。今回はいよいよプロダクション実装の核心部分に踏み込むヨ。
今回キミが答えを得られる3つの問いはコレだヨ:
- バックテストの信頼性をどう担保するか? — 3大バイアスを徹底排除する方法
- FreqAIで自動売買システムにどう統合するか? — 設定から戦略クラス実装まで
- モデル劣化にどう対処するか? — 検出から自動再学習トリガーまで
この記事は、Python・機械学習の基礎知識があって、FreqtradeとDockerを触ったことがあるキミに向けて書いているネ。初心者には少し難しいかもしれないけど、コードは全部動くから、手を動かせばわかってくるヨ!!
導入:なぜ「バックテスト優秀・本番惨敗」が起きるのか
正直に言うヨ。最先端のAIでも、仮想通貨の価格方向性の正解率は55〜65%程度がせいぜいなんだヨネ。なのにバックテストで80%超えの正解率が出たりするのは、ほぼ確実に何かのバイアスが混入してるネ。
市場規模の話をすると、仮想通貨×AI(アルゴトレード含む)の市場は2025年時点で約51億ドル、2035年には552億ドルに達するとの予測がある(CAGR 26.8%)ヨ。要するにこの分野は激熱で、参入者が増えれば増えるほど「バックテストでうまくいく戦略」のエッジは薄れていくヨ。
バックテストを破壊する犯人は主に三兄弟だヨ:
- ルックアヘッドバイアス(先読みバイアス):未来のデータがこっそり特徴量に混入
- サバイバーシップバイアス:現存している(生き残った)コインだけで検証
- オーバーフィッティング:過去データへの過最適化で汎化性能ゼロ
この三兄弟を全員倒さないと、本番での惨敗は避けられないネ。順番に潰していこうネ!!
第1章:バックテストを破壊する「3大バイアス」完全解説
1-1. ルックアヘッドバイアス:未来データ混入を防ぐ実装
ルックアヘッドバイアスとは、「予測する時点では知りえないはずの未来の情報」が特徴量に混ざってしまうことだヨ。😱
典型的な発生パターンを見てほしいネ:
| パターン | 具体例 | 結果 |
|---|---|---|
shift() 忘れ |
df['ma_20'] = df['close'].rolling(20).mean() |
現在バーの終値が含まれる |
| ターゲットのリーク | df['target'] = df['close'].pct_change(1) |
予測対象が特徴量に |
| 将来の出来高使用 | df['vol_ratio'] = df['volume'] / df['volume'].rolling(24).mean() |
右端未確定バーの出来高参照 |
fillna(method='bfill') |
欠損値を後ろの値で埋める | 未来データで穴埋め |
防ぐための大原則は「すべての特徴量に shift(1) を徹底適用する」こと。コレ見てよ!!スゴくナイ!?✨
import pandas as pd
import numpy as np
def create_features_no_lookahead(df: pd.DataFrame) -> pd.DataFrame:
"""
先読みバイアスを排除した特徴量生成。
すべての特徴量は予測時点では既知の値のみを使用する。
"""
df = df.copy()
# shift(1)でバーが確定した後の値のみ使用
df['ma_20'] = df['close'].shift(1).rolling(20).mean()
df['ma_50'] = df['close'].shift(1).rolling(50).mean()
close_shifted = df['close'].shift(1)
delta = close_shifted.diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.rolling(14).mean()
avg_loss = loss.rolling(14).mean()
rs = avg_gain / avg_loss.replace(0, 1e-10)
df['rsi_14'] = 100 - (100 / (1 + rs))
rolling_std = close_shifted.rolling(20).std()
df['bb_upper'] = df['ma_20'] + 2 * rolling_std
df['bb_lower'] = df['ma_20'] - 2 * rolling_std
bb_range = (df['bb_upper'] - df['bb_lower']).replace(0, 1e-10)
df['bb_position'] = (close_shifted - df['bb_lower']) / bb_range
df['return_1'] = close_shifted.pct_change(1)
df['return_5'] = close_shifted.pct_change(5)
df['return_24'] = close_shifted.pct_change(24)
df['target'] = df['close'].pct_change(1).shift(-1)
return df.dropna()ポイントは close_shifted = df['close'].shift(1) を一度変数に入れて、以降は全部そこから派生させてることだヨ!!ターゲットだけは例外で .shift(-1) で1本先の未来を使うネ(それが予測したいものだから)。
1-2. サバイバーシップバイアス:消えたコインの損失も計算に入れる
「現在Binanceに上場しているコインだけ」でバックテストすると、どうなると思うネ?🤔
答えは単純だヨ。上場廃止になったり、99%下落して実質死亡したコインは最初から除外されてるから、バックテストの成績が自動的に良く見えるヨ。研究によると、この方法だと年率15〜30%程度の過大評価が発生するヨ。
対策としては:
- KaikoやCoin Metricsなどの有料データプロバイダーを使う(廃止銘柄のデータが含まれてる)
- 自分でシミュレーションする場合は、「ポートフォリオに含まれていた銘柄が突然-90%になって取引停止になる」というシナリオを強制的に追加する
- バックテスト期間内に廃止された全銘柄のリストを取得して、その損失を成績に反映させる
ぽんたぬきの経験だとこの対策はコスト面でもハードルが高いから、最低限「現存している銘柄でも2020〜2021年のアルトコインシーズン終焉のような局面をどう処理したか確認する」のが現実的なネ。
ところで最近ちょっと困ったことあったんだけどサ、データ収集サーバーのディスクが満杯になってて、気づかずに6時間ぶんまるごとデータが空白になってたヨ!!OHLCV取得スクリプトがエラーも吐かずに黙って止まってたヨ、恐ろしい……。モニタリングはちゃんとしないとダメだネって反省したヨ。😅
1-3. オーバーフィッティング:過去への過最適化を見抜く
モデルが訓練データの「ノイズ」まで完璧に覚えてしまい、未見データでボロボロになる現象だヨ。
鉄則はコレだヨ:
- 最低100トレード以上のサンプルを確保してからモデル評価する
- ハイパーパラメータ調整は必ず**OOSデータ(アウトオブサンプル)**で最終評価する
- パラメータ数が多いほど過学習リスクが上がるから、特徴量はVIF(分散膨張係数)や特徴量重要度で削減する
時系列データの正しいクロス検証、コレ見てよ!!スゴくナイ!?✨
import pandas as pd
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_squared_error
import numpy as np
def walk_forward_validation(model, X: pd.DataFrame, y: pd.Series,
n_splits: int = 5) -> dict:
"""
時系列データの正しいクロス検証。
TimeSeriesSplitで未来データの混入を防ぐ。
"""
tscv = TimeSeriesSplit(n_splits=n_splits)
is_scores = []
oos_scores = []
for fold, (train_idx, test_idx) in enumerate(tscv.split(X)):
X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
model.fit(X_train, y_train)
is_pred = model.predict(X_train)
is_rmse = np.sqrt(mean_squared_error(y_train, is_pred))
is_scores.append(is_rmse)
oos_pred = model.predict(X_test)
oos_rmse = np.sqrt(mean_squared_error(y_test, oos_pred))
oos_scores.append(oos_rmse)
print(f'Fold {fold+1}: IS RMSE={is_rmse:.4f}, OOS RMSE={oos_rmse:.4f}')
return {
'is_mean': np.mean(is_scores),
'oos_mean': np.mean(oos_scores),
'overfit_ratio': np.mean(oos_scores) / np.mean(is_scores),
'is_scores': is_scores,
'oos_scores': oos_scores
}overfit_ratio が1.5を超えたら過学習の可能性大だヨ。IS(インサンプル)の精度に比べてOOSが大幅に悪化してたら、モデルは「過去を暗記」してるだけダヨ。🚨
💡 コラム:SVRが草コインで壊滅する理由
SVR(サポートベクター回帰)は理論的にはエレガントだけど、草コインのボラティリティが高い局面で使うとボロボロになることがあるヨ。理由はカーネルのパラメータ(C・γ・ε)のチューニングが非常にシビアで、小さなデータ分布の変化に激弱なんだヨネ。特に仮想通貨のような非定常時系列では、分布が数週間で別物になることがあるから、SVRはむしろ致命的になるネ。XGBoostやLightGBMの方が非定常性への頑健性が高いし、特徴量重要度も出るから、草コインには断然こっちを使うべきだヨ!!
第2章:ウォークフォワード最適化(WFO)で「本当に使える戦略」を選ぶ
2-1. WFOの基本概念:IS期間とOOS期間の分割ロジック
ウォークフォワード最適化(WFO)は、「時間をスライドさせながら学習と検証を繰り返す」手法だヨ。
時間軸 ─────────────────────────────────→
ウィンドウ1: [────IS期間(学習)────][OOS]
ウィンドウ2: [────IS期間(学習)────][OOS]
ウィンドウ3: [────IS期間(学習)────][OOS]
ウィンドウ4: [────IS期間(学習)────][OOS]
IS = インサンプル(最適化に使う)
OOS = アウトオブサンプル(検証のみ、パラメータ調整に使わない)
各窓でIS期間に最適化→OOS期間で検証→を繰り返して、OOSの成績を合計したものが「本当の戦略の実力」になるヨ。
評価指標として使うのが WFE(ウォークフォワード効率) だヨ:
WFE = OOS成績 ÷ IS成績
これが1に近いほど「バックテストと本番の乖離が小さい」優秀なモデルだヨ。WFEが0.3以下なら過学習が疑われるし、0.8以上なら本番稼働の信頼性が高いネ!!
2-2. ウォークフォワード最適化の完全実装:XGBoostで動かす
コレ見てよ!!スゴくナイ!?✨ XGBoostを使ったWFOの完全実装だヨ:
import pandas as pd
import numpy as np
import xgboost as xgb
from dataclasses import dataclass
from typing import List, Tuple
@dataclass
class WFOConfig:
lookback_period: int = 252
retrain_frequency: int = 30
prediction_horizon: int = 1
@dataclass
class WFOResult:
is_return: float
oos_return: float
is_sharpe: float
oos_sharpe: float
is_max_dd: float
oos_max_dd: float
wfe: float
n_trades: int
def compute_sharpe(returns: np.ndarray, freq: int = 252) -> float:
if len(returns) < 2:
return 0.0
mean = np.mean(returns) * freq
std = np.std(returns, ddof=1) * np.sqrt(freq)
return mean / std if std != 0 else 0.0
def compute_max_drawdown(returns: np.ndarray) -> float:
cumulative = np.cumprod(1 + returns)
peak = np.maximum.accumulate(cumulative)
drawdown = (cumulative - peak) / peak
return float(np.min(drawdown))
def run_walk_forward_optimization(
df: pd.DataFrame,
feature_cols: List[str],
target_col: str,
config: WFOConfig
) -> Tuple[List[WFOResult], pd.Series]:
"""
ウォークフォワード最適化のメインループ。
非定常な時系列変化に追従するため、窓をスライドさせながら再学習する。
"""
results: List[WFOResult] = []
all_oos_returns = []
total_len = len(df)
start_idx = config.lookback_period
while start_idx + config.retrain_frequency <= total_len:
is_end = start_idx
is_start = max(0, is_end - config.lookback_period)
is_data = df.iloc[is_start:is_end]
oos_start = is_end
oos_end = min(total_len, oos_start + config.retrain_frequency)
oos_data = df.iloc[oos_start:oos_end]
if len(is_data) < 50 or len(oos_data) < 5:
start_idx += config.retrain_frequency
continue
X_is = is_data[feature_cols]
y_is = is_data[target_col]
X_oos = oos_data[feature_cols]
y_oos = oos_data[target_col]
model = xgb.XGBRegressor(
n_estimators=100, max_depth=4,
learning_rate=0.05, subsample=0.8,
random_state=42, verbosity=0
)
model.fit(X_is, y_is)
is_preds = model.predict(X_is)
is_signals = np.sign(is_preds)
is_rets = is_signals * y_is.values
is_sharpe = compute_sharpe(is_rets)
is_mdd = compute_max_drawdown(is_rets)
is_ret = float(np.sum(is_rets))
oos_preds = model.predict(X_oos)
oos_signals = np.sign(oos_preds)
oos_rets = oos_signals * y_oos.values
oos_sharpe = compute_sharpe(oos_rets)
oos_mdd = compute_max_drawdown(oos_rets)
oos_ret = float(np.sum(oos_rets))
wfe = (oos_sharpe / is_sharpe) if is_sharpe != 0 else 0.0
results.append(WFOResult(
is_return=is_ret, oos_return=oos_ret,
is_sharpe=is_sharpe, oos_sharpe=oos_sharpe,
is_max_dd=is_mdd, oos_max_dd=oos_mdd,
wfe=wfe, n_trades=len(oos_data)
))
all_oos_returns.extend(oos_rets.tolist())
start_idx += config.retrain_frequency
return results, pd.Series(all_oos_returns)lookback_period=252 は日足換算で約1年分だヨ。1時間足なら 252*24=6048 に変えるサ。retrain_frequency=30 は30バーごとに再学習するって意味ネ。このバランスが実運用の鍵だヨ!!
2-3. WFE評価目安テーブル:モデルの本番適性を判定する
WFEの値をどう解釈するか、判定基準はコレだヨ:
| WFE値 | 評価 | 本番可否 | 推奨アクション |
|---|---|---|---|
| 0.8〜1.0 | 優秀 ✅ | 即時稼働推奨 | 定期モニタリングのみ |
| 0.6〜0.8 | 良好 🟡 | 条件付き可 | ポジションサイズを抑えてテスト |
| 0.4〜0.6 | 要注意 ⚠️ | 非推奨 | 特徴量削減・正則化を強化 |
| 0.0〜0.4 | 危険 🔴 | 不可 | 戦略の根本的な見直しが必要 |
| 0.0以下 | 論外 💀 | 絶対不可 | 廃棄してゼロから再設計 |
WFEが0.0以下、つまりOOSでIS以上に損失を出してるモデルは「過去を逆張りで覚えてる」状態だから即廃棄だヨ!!そのモデルを本番に出したら笑えない金額が消えるヨ。😱
第3章:FreqAIで自動売買システムと統合する
3-1. FreqAIとは何か:MLをFreqtradeに組み込む仕組み
FreqAIはFreqtradeに組み込まれたMLパイプラインだヨ。アーキテクチャはこんな感じサだヨ:
┌─────────────────────────────────────────────┐
│ FreqAI │
│ │
│ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ Feature │ → │ Train │ → │ Model │ │
│ │Engineering│ │(XGBoost)│ │ Store │ │
│ └──────────┘ └──────────┘ └────┬───┘ │
│ │ │
│ ┌──────────┐ ┌──────────┐ ┌────▼───┐ │
│ │Live OHLCV│ → │ Predict │ ← │Retrain │ │
│ │ Data │ │ (signal) │ │Trigger │ │
│ └──────────┘ └────┬─────┘ └────────┘ │
│ │ │
└────────────────────────┼─────────────────────┘
↓
Freqtrade Bot (売買執行)
train → predict → retrain のループがFreqAIの核心だヨ:
- train:設定した
train_period_days分の過去データで初回学習 - predict:学習済みモデルを使ってリアルタイムでシグナル生成
- retrain:
live_retrain_hours経過したら新データを追加して自動再学習
このループのおかげで、モデルが市場変化に自動追従するネ!!FreqtradeのDockerコンテナの中でこれが全部動くから、一度設定すれば手放しに近い状態を作れるヨ。🔥
3-2. FreqAI設定ファイルの完全解説
コレ見てよ!!スゴくナイ!?✨ 実際に動く config.json の freqai セクションはコレだヨ:
{
"freqai": {
"enabled": true,
"purge_old_models": true,
"train_period_days": 60,
"backtest_period_days": 7,
"live_retrain_hours": 24,
"identifier": "btc_xgb_v1",
"feature_parameters": {
"include_timeframes": ["1h", "4h", "1d"],
"include_corr_pairlist": ["BTC/USDT", "ETH/USDT"],
"label_period_candles": 24,
"include_shifted_candles": 3,
"DI_threshold": 0.9,
"weight_factor": 0.9,
"indicator_periods_candles": [10, 20, 50]
},
"data_split_parameters": {
"test_size": 0.15,
"random_state": 42,
"shuffle": false
},
"model_training_parameters": {
"n_estimators": 200,
"max_depth": 6,
"learning_rate": 0.05,
"subsample": 0.8,
"colsample_bytree": 0.8
}
}
}重要ポイントを解説するヨ:
train_period_days: 60:直近60日分で学習ヨ。長すぎると古いレジームのデータを引きずるネlive_retrain_hours: 24:24時間ごとに再学習。市場が荒れてる時は12時間にしてもいいヨDI_threshold: 0.9:Dissimilarity Index(非類似度指数)の閾値。学習データと似てないデータではシグナルを出さないようにするヨ。これがモデル劣化への最初の防衛線だネ!!shuffle: false:時系列データだからシャッフルは絶対NGだヨ。ここをtrueにするとルックアヘッドバイアスの温床になるヨ
3-3. FreqAI戦略クラスの実装
戦略クラスのコードはコレだヨ。コレ見てよ!!スゴくナイ!?✨
from freqtrade.strategy import IStrategy
import pandas as pd
import talib.abstract as ta
class FreqAIXGBStrategy(IStrategy):
"""
FreqAI + XGBoostによる仮想通貨自動売買戦略。
24時間ごとに自動再学習し、モデル劣化に対応する。
"""
minimal_roi = {"0": 0.05}
stoploss = -0.03
trailing_stop = True
trailing_stop_positive = 0.01
timeframe = '1h'
can_short = True
def feature_engineering_expand_all(self, dataframe, period, **kwargs):
dataframe["%-rsi"] = ta.RSI(dataframe, timeperiod=period)
dataframe["%-mfi"] = ta.MFI(dataframe, timeperiod=period)
dataframe["%-adx"] = ta.ADX(dataframe, timeperiod=period)
dataframe["%-cci"] = ta.CCI(dataframe, timeperiod=period)
return dataframe
def feature_engineering_standard(self, dataframe, **kwargs):
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
dataframe["%-volume_mean_ratio"] = (
dataframe["volume"] / dataframe["volume"].rolling(24).mean()
)
return dataframe
def set_freqai_targets(self, dataframe, **kwargs):
dataframe["&-return_24h"] = (
dataframe["close"].shift(-24) / dataframe["close"] - 1
)
return dataframe
def populate_entry_trend(self, df, metadata):
df.loc[
(df["freqai.target_mean"] > 0.01) &
(df["freqai.DI_values"] < df["freqai.DI_cutoff"]),
"enter_long"
] = 1
df.loc[
(df["freqai.target_mean"] < -0.01) &
(df["freqai.DI_values"] < df["freqai.DI_cutoff"]),
"enter_short"
] = 1
return df
def populate_exit_trend(self, df, metadata):
df.loc[df["freqai.target_mean"] < 0, "exit_long"] = 1
df.loc[df["freqai.target_mean"] > 0, "exit_short"] = 1
return dffreqai.DI_values < freqai.DI_cutoff の条件が重要だヨ!!これは「学習データと似ていない相場環境ではエントリーしない」というフィルターだヨ。FTX崩壊みたいな「前代未聞のイベント」では、このフィルターがモデルを守ってくれるネ。🛡️
3-4. Dockerによる本番デプロイ
本番環境はDockerで管理するのが一番安全だヨ!!
バックテスト実行:
docker compose run --rm freqtrade backtesting \
--strategy FreqAIXGBStrategy \
--config config.json \
--timerange 20240101-20241231 \
--freqaimodel XGBoostRegressorライブトレード起動:
docker compose run -d --name freqtrade_live freqtrade trade \
--strategy FreqAIXGBStrategy \
--config config.json \
--freqaimodel XGBoostRegressorログ確認:
docker logs -f freqtrade_liveDockerを使う理由は「環境の再現性」だヨ。ローカルのPython環境が壊れても、docker compose up 一発で同じ環境が再現できるヨ!!本番環境をVPSに乗せるときも同じコマンドで動くから、環境差異による謎バグを防げるネ。
第4章:モデル劣化の検出と自動再学習トリガー
4-1. モデル劣化とは何か:仮想通貨市場固有のリスク
仮想通貨市場は**レジームチェンジ(市場構造の急変)**が頻繁に起きるヨ。🌪️
典型例を挙げると:
- 2020〜2021年:DeFiブームで草コインが数週間で100倍→99%下落
- 2021年後半:NFTバブルの生成と崩壊が数ヶ月で完結
- 2022年11月:FTX崩壊による市場全体の相関1・ボラティリティ爆発
- 2023〜2024年:ビットコインETF承認期待で機関投資家主導の相場構造へ
これらのイベントで「過去6ヶ月のデータで学習したモデル」は事実上使えなくなるヨ。
モデル劣化の典型的なサインはコレだヨ:
| 症状 | 閾値の目安 | 対処 |
|---|---|---|
| シャープレシオの急低下 | ベースラインの50%以下 | 即時再学習トリガー |
| 最大ドローダウンの拡大 | -15%超 | 取引停止+緊急再学習 |
| 正解率の低下 | ランダムウォーク水準(50%±2%)に接近 | 特徴量見直し |
| DI値の上昇 | freqai.DI_cutoffを継続的に超過 | 学習期間の延長 |
| スリッページの拡大 | 期待値の2倍以上 | 板の薄い銘柄から撤退 |
4-2. モデル劣化の自動検出:パフォーマンス監視システム
コレ見てよ!!スゴくナイ!?✨ リアルタイム監視システムのコードだヨ:
import numpy as np
from collections import deque
from dataclasses import dataclass, field
from typing import Optional
@dataclass
class PerformanceMonitor:
"""
リアルタイムでモデルのパフォーマンスを監視し、
劣化を検出したら再学習トリガーを発火させる。
"""
window_size: int = 100
sharpe_threshold: float = 0.5
drawdown_limit: float = -0.15
recent_returns: deque = field(default_factory=lambda: deque(maxlen=100))
baseline_sharpe: Optional[float] = None
is_trading_halted: bool = False
def update(self, trade_return: float) -> dict:
self.recent_returns.append(trade_return)
if len(self.recent_returns) < 30:
return {"status": "warming_up", "action": "none"}
rets = np.array(self.recent_returns)
current_sharpe = self._compute_sharpe(rets)
current_mdd = self._compute_max_drawdown(rets)
if self.baseline_sharpe is None and len(self.recent_returns) >= 100:
self.baseline_sharpe = current_sharpe
action = "continue"
status = "healthy"
if current_mdd < self.drawdown_limit:
action = "halt_trading"
status = "critical"
self.is_trading_halted = True
elif (self.baseline_sharpe is not None and
current_sharpe < self.baseline_sharpe * 0.5):
action = "trigger_retrain"
status = "degraded"
elif current_sharpe < self.sharpe_threshold:
action = "increase_monitoring"
status = "warning"
return {
"status": status,
"action": action,
"current_sharpe": round(current_sharpe, 3),
"current_mdd": round(current_mdd, 3),
"n_trades": len(self.recent_returns)
}
def _compute_sharpe(self, returns: np.ndarray) -> float:
if np.std(returns) == 0:
return 0.0
return (np.mean(returns) / np.std(returns)) * np.sqrt(252)
def _compute_max_drawdown(self, returns: np.ndarray) -> float:
cumulative = np.cumprod(1 + returns)
peak = np.maximum.accumulate(cumulative)
return float(np.min((cumulative - peak) / peak))このクラスはトレードのたびに monitor.update(trade_return) を呼ぶだけで使えるヨ!!返り値の action が "halt_trading" になったら即座に取引を停止する処理を繋げるサ。"trigger_retrain" なら FreqAI の再学習 API を叩く処理を書けばいいネ。
実際の運用では status と action をSlackやDiscordに飛ばしておくと、朝起きてすぐ状況がわかって便利だヨ!!夜中2時に爆損してるのに気づかないのは精神衛生上よくないヨ……経験者は語るネ。😅
4-3. リスク管理:ケリー基準によるポジションサイジング
「どれだけ賭けるか」がリターンとリスクの両方を決定するヨ。ケリー基準は数学的に最適なポジションサイズを計算する公式だヨ:
def kelly_position_size(
win_rate: float,
avg_win: float,
avg_loss: float,
max_kelly_fraction: float = 0.25
) -> float:
"""
ケリー基準によるポジションサイズ計算。
フルケリーは破産リスクが高いため、最大25%にキャップする。
"""
if avg_loss == 0:
return 0.0
b = avg_win / abs(avg_loss)
q = 1 - win_rate
kelly = (b * win_rate - q) / b
return min(max(kelly * 0.5, 0.0), max_kelly_fraction)フルケリーは理論上最適だけど、実際にはハーフケリー(kelly * 0.5)が推奨されるヨ。理由は三つ:
- 勝率・期待値の推定に必ず誤差がある
- 仮想通貨は極端な動きがあり「ルイン(全損)」リスクが実在する
- ハーフケリーでもフルケリーの約75%のリターンを得られる
さらに max_kelly_fraction=0.25 で25%にキャップしてるヨ。これで口座の75%は常に保護されるヨ。「生き残ること」が自動売買の第一原則だネ!!💪
まとめ:プロダクション実装の完全チェックリスト
長い旅だったけど、キミは3大バイアス・WFO・FreqAI統合・モデル劣化対策まで全部学んだヨ!!🎉 本番稼働前に以下のチェックリストで確認するヨ:
① バイアス排除
- すべての特徴量に
shift(1)を適用し、ルックアヘッドバイアスがないか確認した - 廃止銘柄・上場廃止コインのデータを含めてバックテストを実施した(またはリスクを認識した)
- バックテスト期間に最低100トレード以上のサンプルが含まれている
-
fillna(method='bfill')など未来方向の補完を使っていない
② ウォークフォワード検証
- WFEが0.6以上であることを確認した
-
TimeSeriesSplitを使ってOOSで最終評価を実施した -
overfit_ratio(OOS RMSE / IS RMSE)が1.5未満に収まっている
③ FreqAI統合
-
shuffle: falseが設定されている(時系列データのシャッフル禁止) -
DI_thresholdを設定して異常相場でのシグナル抑制を有効化した -
live_retrain_hoursを設定して自動再学習ループを有効化した
④ モデル劣化対策
-
PerformanceMonitorをトレードループに組み込んだ - ドローダウン -15% で取引停止するサーキットブレーカーを実装した
- 再学習トリガーをアラート通知(Slack等)と連携させた
⑤ リスク管理
- ポジションサイズにハーフケリー(最大25%キャップ)を適用した
- ストップロスを設定した(FreqAIXGBStrategy の場合
stoploss = -0.03) -
trailing_stopを有効にして利益を守る仕組みを入れた
チェックリストを全部埋めたキミは、もう「バックテストで喜んで本番で爆死」するパターンから抜け出せるヨ!!
ぽんたぬきはこの記事を書きながら、昨夜また live_retrain_hours の設定を間違えて12時間再学習が走り続け、VPSのCPUが100%になってBotが止まってたのを今朝発見したヨ……またやらかしちゃいましたヨ……😭 それでも諦めずに設定直して、また動かしてるヨ!!
キミも失敗を恐れず、でも同じ失敗は繰り返さないように、チェックリストを守ってプロダクション実装に挑戦してみてネ!!一緒に溶かさずに稼ごうネ!!💪🔥
関連記事
プロンプトエンジニアリングの"次" — コンテキストエンジニアリングが変えるAIエージェント設計の常識
プロンプトを書き直しても改善しないAIエージェント設計に悩んでいませんか?コンテキストエンジニアリングの定義からLLMの失敗パターン4選・RAG・メモリ設計・マルチエージェント構成まで、今日から使える設計パターンを実体験とコード付きで解説します。
RAGFlowで構築する高精度なオープンソースRAG:40代エンジニアが実践してみた完全ガイド
オープンソースのRAGFlowを使って高精度な検索拡張生成システムを構築する方法を、40代エンジニアが実際に試した経験をもとに詳しく解説します。