機械学習による仮想通貨価格予測 第2部:Prophet/LSTMによる価格予測モデル構築【実装コード付き】

約41分で読めます by ぽんたぬき
機械学習による仮想通貨価格予測 第2部:Prophet/LSTMによる価格予測モデル構築【実装コード付き】

機械学習による仮想通貨価格予測 第2部:Prophet/LSTMによる価格予測モデル構築【実装コード付き】


はじめに:この記事で実装する「時系列予測パイプライン」の全体像

キミ、「機械学習で仮想通貨の価格予測をやってみたい」と思ってコレにたどり着いたなら、正解の記事に来たヨ!!✨

この第2部では、Facebook ProphetBidirectional LSTMという2つのモデルをアンサンブルした、時系列予測パイプラインを実装していきマスヨ。精度が高いモデルを作ることと、それで実際に儲けることは全くイコールじゃない——これは僕が4万円溶かして身体で覚えた教訓デスネ😭 その失敗話は各章の合間に差し込むから、反面教師にしてネ!!

今回キミと一緒に作るのはコレだヨ:

  • Facebook Prophet(長期トレンド・季節性の専門家)
  • Bidirectional LSTM(短期の非線形パターンハンター)
  • この2枚看板をアンサンブルした予測パイプライン
  • backtesting.pyによる実戦想定のバックテスト

この記事でできるようになること

  • ccxtでBinanceからOHLCVデータを取得する
  • taライブラリでテクニカル指標を一括計算する
  • ProphetにRSI・MACDをadd_regressor()で組み込む
  • Bi-LSTMのモデルアーキテクチャをディープラーニングで実装する
  • Optunaでベイズ最適化をかける
  • 2モデルをアンサンブルして予測精度を検証する
  • backtesting.pyでバックテストを走らせる

対象読者:Pythonの基礎がある人・第1部を読んだ人・僕みたいに一回溶かした人😅

動作確認済み環境

Python 3.11.x
prophet==1.1.5
tensorflow==2.15.0
keras==2.15.0
optuna==3.5.0
ccxt==4.2.x
ta==0.11.0
backtesting==0.3.3
pandas==2.1.x
numpy==1.26.x
scikit-learn==1.4.x
plotly==5.18.x

第1章:なぜ Prophet × Bi-LSTM のアンサンブルなのか

1-1. 単独モデルの限界を理解する

正直に言うヨ、キミ。ProphetもLSTMも、単体だとマジで弱点あるんデスヨ😅

Prophetの弱点:加法モデルの構造上、「今から3時間以内に急騰する」みたいな非線形な短期変動を捉えるのが苦手デス。長期の大局観は神なんだケド、局所的なパターン認識は正直ショボいんデスネ……。

LSTM単体の弱点:こっちは逆で、直近100時間のローソク足パターンは強いんだケド、「4年周期の半減期サイクル」とか「週末の流動性低下」みたいなマクロな周期性の把握が弱い。僕が4万円溶かしたのはコレが原因デスヨ(笑)🦝

比較表で整理するとこんな感じデスネ♪

モデル 精度 実装コスト 解釈性
ARIMA
Prophet単独
LSTM単独
GRU単独
Prophet × Bi-LSTM

1-2. アンサンブルで弱点を補完する仕組み

コレが今日の核心デスヨ!!✨

役割分担をざっくり言うと:

  • Prophet = 「森を見る目」。年単位・月単位のトレンドと周期性を担当
  • Bi-LSTM = 「木を見る目」。直近数十〜数百時間の細かいパターンを担当
  • アンサンブル = 両者の予測を合体させて誤差を相殺

これ、学術的にもちゃんと裏付けられてるんデスヨ💡 PeerJやPMCに掲載された研究では、Prophet+LSTMのハイブリッドモデルが単独モデル比でRMSEを5〜20%改善(R²=0.98〜0.99)を達成したと報告されてマスネ🎵 エネルギー消費予測での実績だとRMSE=65.34、MAPE=7.3%、R²=0.98という数字も出てる。時系列予測の分野では、この手の組み合わせが王道になりつつあるんデスヨ!!

統合方法は大きく2つ:

  • 加重平均:シンプルで解釈しやすい。重みは検証期間のRMSE逆数で自動計算
  • Stacking(メタモデル):Ridge回帰などでさらに精度を上げられる。実装コストは高め

今回は加重平均ベースで実装して、最後にStackingのコードも紹介しますヨ!!

1-3. Bidirectional LSTMを選ぶ理由

「なんで普通のLSTMじゃなくてBidirectional(双方向)なの?」ってキミ思ったよネ!!センスあるネ、その疑問!!😆

通常のLSTMは「過去→未来」の一方向だけど、Bidirectional LSTMは前から読むLSTMと後ろから読むLSTMを並列で走らせて、両方の文脈を同時に学習するんデスヨ。ディープラーニングの中でも特に時系列データとの相性が良い構造デスネ💡

仮想通貨の価格系列での実測値として、BTCに対してMAPC=0.036(通常LSTM・GRUより優秀)という結果が出てるんデスネ💡 特にRSI・MACD・ボリンジャーバンドなどのテクニカル指標を入力に加えると検証精度が最大80%に達した事例もある。

デメリットは、計算コストが通常LSTMの約2倍になること。でもGPUがあれば全然許容範囲デスヨ!!CPU環境の人は後ほどOptunaでユニット数を絞る最適化を解説するから安心してネ🎵


第2章:データ取得と前処理【実装コード】

2-1. ccxtでBinanceからOHLCVデータを取得する

ccxtは100以上の取引所に対応した神ライブラリデスヨ🔥 BinanceもBybitも同じAPIで叩けちゃう。まずインストールから!!

pip3 install ccxt ta prophet tensorflow optuna backtesting plotly scikit-learn

コレ見てよ!!OHLCVデータ取得コード、スゴくナイ!?✨

import ccxt
import pandas as pd
import numpy as np
from datetime import datetime, timezone

def fetch_ohlcv_data(
    symbol: str = 'BTC/USDT',
    timeframe: str = '1d',
    since_str: str = '2020-01-01',
    exchange_id: str = 'binance'
) -> pd.DataFrame:
    """
    ccxtでOHLCVデータを取得してDataFrameに変換する

    Args:
        symbol: 取引ペア(例: 'BTC/USDT')
        timeframe: 時間軸('1d', '4h', '1h' など)
        since_str: 取得開始日('YYYY-MM-DD' 形式)
        exchange_id: 取引所ID

    Returns:
        OHLCV DataFrameに変換したデータ
    """
    exchange = getattr(ccxt, exchange_id)({'enableRateLimit': True})
    since = exchange.parse8601(f'{since_str}T00:00:00Z')

    all_ohlcv = []
    limit = 1000

    while True:
        ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=since, limit=limit)
        if not ohlcv:
            break
        all_ohlcv.extend(ohlcv)
        since = ohlcv[-1][0] + 1
        if len(ohlcv) < limit:
            break

    df = pd.DataFrame(
        all_ohlcv,
        columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']
    )

    df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True)
    df.set_index('timestamp', inplace=True)
    df = df[~df.index.duplicated(keep='first')]
    df.sort_index(inplace=True)
    df.ffill(inplace=True)
    df.dropna(inplace=True)

    print(f"取得完了!レコード数: {len(df)} 件 ({df.index[0]}{df.index[-1]})")
    return df

動いた時、思わず「よっしゃ!!」って声出ちゃいましたヨ😆 妻に「また独り言言ってる」って怒られたケド!!(笑)

2-2. taライブラリでテクニカル指標を一括計算する

キミ、RSIとかMACDって手計算したことある?僕あるんだケド、マジで苦行だからネ(笑)taライブラリ使えば一発デスヨ🎵

なぜこの指標を選んだか:

  • RSI:買われすぎ・売られすぎを示す。Prophet/LSTM双方の入力として有効
  • MACD:短期・長期移動平均の乖離でトレンド転換を捉える
  • ボリンジャーバンド:ボラティリティの拡大・収縮を定量化
  • Stochastic RSI:RSIをさらに滑らかにしたモメンタム指標

コレも見てよ!!テクニカル指標の一括追加コードデスヨ!!✨

import ta
from ta.momentum import RSIIndicator, StochasticOscillator
from ta.trend import MACD
from ta.volatility import BollingerBands

def add_technical_indicators(df: pd.DataFrame) -> pd.DataFrame:
    """
    OHLCVデータにテクニカル指標を追加する

    Args:
        df: OHLCVのDataFrame(open/high/low/close/volumeカラム必須)

    Returns:
        テクニカル指標追加後のDataFrame
    """
    df = df.copy()

    rsi = RSIIndicator(close=df['close'], window=14)
    df['rsi'] = rsi.rsi()

    macd = MACD(close=df['close'], window_slow=26, window_fast=12, window_sign=9)
    df['macd'] = macd.macd()
    df['macd_signal'] = macd.macd_signal()
    df['macd_diff'] = macd.macd_diff()

    bb = BollingerBands(close=df['close'], window=20, window_dev=2)
    df['bb_upper'] = bb.bollinger_hband()
    df['bb_lower'] = bb.bollinger_lband()
    df['bb_width'] = bb.bollinger_wband()
    df['bb_pband'] = bb.bollinger_pband()

    stoch_rsi = StochasticOscillator(
        high=df['high'], low=df['low'], close=df['close'],
        window=14, smooth_window=3
    )
    df['stoch_rsi'] = stoch_rsi.stoch()
    df['stoch_rsi_signal'] = stoch_rsi.stoch_signal()

    df.dropna(inplace=True)
    print(f"テクニカル指標追加完了!特徴量数: {len(df.columns)} カラム")
    return df

2-3. 時系列データの正規化と分割戦略

ここ超重要デスヨ!!キミ、k-fold交差検証って知ってるよネ?時系列データにアレをそのまま使うと未来のデータで過去を予測するという地獄みたいなリーク(データ漏洩)が起きちゃうんデスヨ😭 時系列予測ではWalk-forward Validationが必須デスネ💡

from sklearn.preprocessing import MinMaxScaler
from typing import Tuple

def normalize_and_split(
    df: pd.DataFrame,
    train_ratio: float = 0.7,
    val_ratio: float = 0.15,
) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, MinMaxScaler]:
    """
    時間軸に沿ってtrain/validation/testに分割し、正規化する

    MinMaxScaler: 値域が決まっている特徴量(RSI: 0-100など)に適している
    → 今回はOHLCV + テクニカル指標の混在なのでMinMaxを採用
    """
    n = len(df)
    train_end = int(n * train_ratio)
    val_end = int(n * (train_ratio + val_ratio))

    train_df = df.iloc[:train_end].copy()
    val_df   = df.iloc[train_end:val_end].copy()
    test_df  = df.iloc[val_end:].copy()

    feature_cols = [
        'open', 'high', 'low', 'close', 'volume',
        'rsi', 'macd', 'macd_signal', 'macd_diff',
        'bb_width', 'bb_pband', 'stoch_rsi', 'stoch_rsi_signal'
    ]

    scaler = MinMaxScaler(feature_range=(0, 1))
    train_df[feature_cols] = scaler.fit_transform(train_df[feature_cols])
    val_df[feature_cols]   = scaler.transform(val_df[feature_cols])
    test_df[feature_cols]  = scaler.transform(test_df[feature_cols])

    print(f"Train: {len(train_df)} 件 | Val: {len(val_df)} 件 | Test: {len(test_df)} 件")
    return train_df, val_df, test_df, scaler

第3章:Facebook Prophet モデルの構築【実装コード】

3-1. Prophetの加法モデルを理解する

Prophetの数式はこんな感じデスネ♪

$$y(t) = g(t) + s(t) + h(t) + \varepsilon_t$$

意味 仮想通貨での役割
g(t) トレンド成分 長期の価格方向性
s(t) 季節性成分 週次・月次・年次サイクル
h(t) 祝日・イベント成分 半減期・フォーク等
ε_t 誤差項 モデルで説明できない残差

仮想通貨の場合、seasonality_mode='multiplicative'(乗法モデル)を選ぶのがポイントデスヨ!!価格のスケールが大きく変動するときは加法より乗法の方が実態に合うんデスネ💡

3-2. Prophetにテクニカル指標を組み込む

コレがProphetの真骨頂デスヨ!!add_regressor()で外部変数(RSI・MACDなどのテクニカル指標)を追加できるんデス!!✨

from prophet import Prophet
import pandas as pd

def build_prophet_model(
    train_df: pd.DataFrame,
    regressor_cols: list = ['rsi', 'macd', 'macd_diff']
) -> Prophet:
    """
    Facebook Prophetモデルを構築・学習する

    Args:
        train_df: 学習用DataFrame(timestampインデックス・closeカラム必須)
        regressor_cols: 外部変数として追加するカラム名のリスト

    Returns:
        学習済みProphetモデル
    """
    prophet_df = train_df.reset_index().rename(
        columns={'timestamp': 'ds', 'close': 'y'}
    )

    model = Prophet(
        changepoint_prior_scale=0.05,
        seasonality_prior_scale=10.0,
        seasonality_mode='multiplicative',
        daily_seasonality=False,
        weekly_seasonality=True,
        yearly_seasonality=True,
    )

    # 仮想通貨特有の4年半減期サイクルを季節性として追加
    model.add_seasonality(
        name='halving_cycle',
        period=365.25 * 4,
        fourier_order=5
    )

    # RSI・MACDなどのテクニカル指標を外部変数(regressors)として追加
    for col in regressor_cols:
        model.add_regressor(col)

    model.fit(prophet_df[['ds', 'y'] + regressor_cols])
    return model

def predict_prophet(
    model: Prophet,
    future_df: pd.DataFrame,
    regressor_cols: list = ['rsi', 'macd', 'macd_diff']
) -> pd.DataFrame:
    """
    Prophetで将来予測を実行する

    Args:
        model: 学習済みProphetモデル
        future_df: 予測対象のDataFrame(dsカラムと各regressor必須)
        regressor_cols: 学習時と同じ外部変数カラム名

    Returns:
        予測結果DataFrame(yhat・yhat_lower・yhat_upper)
    """
    forecast = model.predict(future_df[['ds'] + regressor_cols])
    return forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']]

Prophetはfitとpredictを実行するだけでサクッと予測してくれるんデスヨ!!これだけで長期トレンドと季節性を同時にモデリングできちゃうんだから、ホントにスゴいライブラリデスネ✨

ただ一回やらかしちゃいましたヨ……😭 future_dfを作る時にtestデータのregressorカラムを渡し忘れて、KeyErrorが出まくる地獄を30分くらい味わいました(笑)。学習時と予測時でregressorのカラムを必ず揃えること、これ絶対忘れないようにネ!!


第4章:Bidirectional LSTM モデルの構築【実装コード】

4-1. モデルアーキテクチャを設計する

いよいよディープラーニングの本丸デスヨ!!キミ、興奮してきたよネ!?僕はしてるヨ!!🔥

Bi-LSTMのアーキテクチャ設計でポイントになるのはコレだヨ:

  • 2層構成:1層目はシーケンスを出力(return_sequences=True)、2層目はベクトルを出力
  • BatchNormalization:勾配爆発・消失を防ぐ(時系列モデルに特に有効)
  • Dropout:過学習を抑制。0.2〜0.4の範囲が実用的
  • Huber損失:外れ値に頑健。仮想通貨の急騰・急落に強いんデスヨ!!

コレ見てよ!!Bi-LSTMのモデル定義、超スッキリしてナイ!?✨

import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import (
    Bidirectional, LSTM, Dense, Dropout, BatchNormalization
)
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
import numpy as np

def create_sequences(
    df: pd.DataFrame,
    feature_cols: list,
    target_col: str = 'close',
    seq_len: int = 60
) -> tuple:
    """
    時系列DataFrameをLSTM用の3Dシーケンスに変換する

    Args:
        df: 正規化済みDataFrame
        feature_cols: 入力特徴量カラムのリスト
        target_col: 予測対象カラム
        seq_len: 過去何ステップを入力とするか

    Returns:
        X: shape (samples, seq_len, features)
        y: shape (samples,)
    """
    X, y = [], []
    data = df[feature_cols + [target_col]].values

    for i in range(seq_len, len(data)):
        X.append(data[i - seq_len:i, :-1])
        y.append(data[i, -1])

    return np.array(X), np.array(y)

def build_bilstm_model(
    input_shape: tuple,
    units_1: int = 128,
    units_2: int = 64,
    dropout_rate: float = 0.3,
    learning_rate: float = 0.001
) -> tf.keras.Model:
    """
    Bidirectional LSTMモデルを構築する

    Args:
        input_shape: (seq_len, n_features)
        units_1: 第1層のユニット数
        units_2: 第2層のユニット数
        dropout_rate: ドロップアウト率
        learning_rate: Adam最適化の学習率

    Returns:
        コンパイル済みKerasモデル
    """
    model = Sequential([
        Bidirectional(
            LSTM(units_1, return_sequences=True),
            input_shape=input_shape
        ),
        BatchNormalization(),
        Dropout(dropout_rate),

        Bidirectional(LSTM(units_2, return_sequences=False)),
        BatchNormalization(),
        Dropout(dropout_rate),

        Dense(32, activation='relu'),
        Dense(1)
    ])

    model.compile(
        optimizer=tf.keras.optimizers.Adam(learning_rate=learning_rate),
        loss='huber',
        metrics=['mae']
    )

    return model

def train_bilstm(
    model: tf.keras.Model,
    X_train: np.ndarray,
    y_train: np.ndarray,
    X_val: np.ndarray,
    y_val: np.ndarray,
    epochs: int = 100,
    batch_size: int = 32
) -> tf.keras.callbacks.History:
    """Bi-LSTMモデルを訓練する"""
    callbacks = [
        EarlyStopping(
            monitor='val_loss', patience=15, restore_best_weights=True
        ),
        ReduceLROnPlateau(
            monitor='val_loss', factor=0.5, patience=7, min_lr=1e-6
        )
    ]

    history = model.fit(
        X_train, y_train,
        validation_data=(X_val, y_val),
        epochs=epochs,
        batch_size=batch_size,
        callbacks=callbacks,
        verbose=1
    )

    return history

4-2. Optunaでハイパーパラメータを自動最適化する

「units_1って128で本当にいいの?」ってキミ思ったよネ!!そこはOptunaに任せちゃおうヨ!!✨ ベイズ最適化でグリッドサーチより賢く探索してくれるんデスヨ😆

コレ見てよ!!Optunaの目的関数、カッコよくナイ!?✨

import optuna
optuna.logging.set_verbosity(optuna.logging.WARNING)

def objective(
    trial: optuna.Trial,
    X_train: np.ndarray,
    y_train: np.ndarray,
    X_val: np.ndarray,
    y_val: np.ndarray,
    input_shape: tuple
) -> float:
    """
    Optunaの目的関数:検証損失を最小化するハイパーパラメータを探索する
    """
    units_1 = trial.suggest_int('units_1', 64, 256, step=32)
    units_2 = trial.suggest_int('units_2', 32, 128, step=32)
    dropout_rate = trial.suggest_float('dropout_rate', 0.1, 0.5)
    learning_rate = trial.suggest_float('learning_rate', 1e-4, 1e-2, log=True)
    batch_size = trial.suggest_categorical('batch_size', [16, 32, 64])

    model = build_bilstm_model(
        input_shape=input_shape,
        units_1=units_1,
        units_2=units_2,
        dropout_rate=dropout_rate,
        learning_rate=learning_rate
    )

    history = train_bilstm(
        model, X_train, y_train, X_val, y_val,
        epochs=50, batch_size=batch_size
    )

    return min(history.history['val_loss'])

def run_optuna_optimization(
    X_train: np.ndarray,
    y_train: np.ndarray,
    X_val: np.ndarray,
    y_val: np.ndarray,
    input_shape: tuple,
    n_trials: int = 50
) -> dict:
    """
    Optunaでベイズ最適化を実行し、最適なハイパーパラメータを返す
    """
    study = optuna.create_study(
        direction='minimize',
        sampler=optuna.samplers.TPESampler(seed=42)
    )

    study.optimize(
        lambda trial: objective(
            trial, X_train, y_train, X_val, y_val, input_shape
        ),
        n_trials=n_trials,
        show_progress_bar=True
    )

    print(f"最適パラメータ: {study.best_params}")
    print(f"最良検証損失: {study.best_value:.6f}")

    return study.best_params

CPU環境の人は、n_trials=20units_1の上限を128に絞ると現実的な時間で終わるヨ!!GPU環境ならそのままでどうぞ🎵


第5章:アンサンブル予測と精度検証【実装コード】

5-1. RMSE逆数による加重平均アンサンブル

ProphetとBi-LSTMの予測を合体させるヨ!!コレが一番の見せ場デスヨ!!🔥 RMSE(二乗平均平方根誤差)が小さいほど重みを大きく設定する、シンプルで理にかなった方法デスネ💡

コレ見てよ!!アンサンブル関数と評価コード、一気に見せちゃうヨ!!✨

import numpy as np
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

def weighted_ensemble(
    prophet_preds: np.ndarray,
    lstm_preds: np.ndarray,
    prophet_rmse: float,
    lstm_rmse: float
) -> np.ndarray:
    """
    RMSE逆数による加重平均アンサンブル

    RMSEが小さいモデルほど重みを大きく設定する
    """
    w_prophet = 1.0 / prophet_rmse
    w_lstm = 1.0 / lstm_rmse
    total_weight = w_prophet + w_lstm

    ensemble_preds = (
        w_prophet * prophet_preds + w_lstm * lstm_preds
    ) / total_weight

    return ensemble_preds

def evaluate_models(
    y_true: np.ndarray,
    prophet_preds: np.ndarray,
    lstm_preds: np.ndarray,
    ensemble_preds: np.ndarray
) -> dict:
    """
    各モデルのRMSE・MAE・R²を計算して比較する
    """
    results = {}
    for name, preds in [
        ('Prophet', prophet_preds),
        ('Bi-LSTM', lstm_preds),
        ('Ensemble', ensemble_preds)
    ]:
        results[name] = {
            'RMSE': np.sqrt(mean_squared_error(y_true, preds)),
            'MAE':  mean_absolute_error(y_true, preds),
            'R²':   r2_score(y_true, preds)
        }

    for name, metrics in results.items():
        print(f"[{name}] RMSE: {metrics['RMSE']:.4f} | "
              f"MAE: {metrics['MAE']:.4f} | R²: {metrics['R²']:.4f}")

    return results

実際に動かすとEnsembleのR²がProphet単独・Bi-LSTM単独を上回るのが見えるんデスヨ!!コレが出た瞬間、思わず「動いたヨ!!スゴいヨ!!」って叫んでしまいましたネ😆

5-2. Stacking(メタモデル)でさらに精度を上げる

加重平均より一歩進んだStackingの実装もサクッと見せておくヨ!!✨ Ridge回帰をメタモデルとして使うデスヨ💡

from sklearn.linear_model import Ridge
import numpy as np

def stacking_ensemble(
    val_prophet_preds: np.ndarray,
    val_lstm_preds: np.ndarray,
    y_val: np.ndarray,
    test_prophet_preds: np.ndarray,
    test_lstm_preds: np.ndarray
) -> np.ndarray:
    """
    Ridgeメタモデルによるスタッキングアンサンブル

    検証データで2モデルの予測を「特徴量」としてメタモデルを学習し、
    テストデータの最終予測を生成する
    """
    X_val_meta  = np.column_stack([val_prophet_preds,  val_lstm_preds])
    X_test_meta = np.column_stack([test_prophet_preds, test_lstm_preds])

    meta_model = Ridge(alpha=1.0)
    meta_model.fit(X_val_meta, y_val)

    final_preds = meta_model.predict(X_test_meta)
    print(f"Stackingの係数: {meta_model.coef_}")
    return final_preds

第6章:backtesting.pyでバックテストを走らせる【実装コード】

6-1. バックテストで「予測の良さ」と「儲けの良さ」を分離する

キミ、ここが一番大事なとこデスヨ!!僕が4万円溶かした根本原因はここにあるんデスネ😭

R²=0.97の予測モデルを作っても、それで自動的に儲かるわけじゃない。予測モデルはあくまで「未来の価格の見積もり」であって、どのタイミングで何をどれだけ買って売るかという売買戦略とは別物デスヨ!! バックテストはこの2つを切り離して検証するために絶対に必要なんデスネ💡

コレ見てよ!!backtesting.pyの実装、一気に見せちゃうヨ!!✨

from backtesting import Backtest, Strategy
import pandas as pd
import numpy as np

class EnsemblePredictionStrategy(Strategy):
    """
    アンサンブル予測に基づくロングオンリー戦略

    エントリー条件:予測価格が現在価格より threshold% 以上高い
    エグジット条件:予測価格が現在価格以下 or ストップロス発動
    """
    threshold     = 0.02   # エントリー閾値(2%)
    stop_loss_pct = 0.05   # ストップロス(5%)

    def init(self):
        self.pred = self.I(lambda: self.data.Predicted, name='Predicted')

    def next(self):
        current_price   = self.data.Close[-1]
        predicted_price = self.pred[-1]
        expected_return = (predicted_price - current_price) / current_price

        if not self.position:
            if expected_return > self.threshold:
                sl_price = current_price * (1 - self.stop_loss_pct)
                self.buy(sl=sl_price)
        else:
            if expected_return <= 0:
                self.position.close()

def run_backtest(
    df: pd.DataFrame,
    ensemble_preds: np.ndarray,
    initial_cash: float = 1_000_000,
    commission: float = 0.001
) -> object:
    """
    backtesting.pyでバックテストを実行する

    Args:
        df: OHLCVのDataFrame(インデックスはdatetime)
        ensemble_preds: アンサンブル予測値の配列
        initial_cash: 初期資金(円)
        commission: 手数料率(0.001 = 0.1%)

    Returns:
        backtesting.pyのstatsオブジェクト
    """
    bt_df = df[['open', 'high', 'low', 'close', 'volume']].copy()
    bt_df.columns = ['Open', 'High', 'Low', 'Close', 'Volume']
    bt_df['Predicted'] = ensemble_preds

    bt = Backtest(
        bt_df,
        EnsemblePredictionStrategy,
        cash=initial_cash,
        commission=commission,
        exclusive_orders=True
    )

    stats = bt.run()
    bt.plot(filename='backtest_result.html')

    print(stats)
    return stats

6-2. バックテスト結果の読み方

バックテスト結果で特に注目すべき指標はコレデスヨ!!💡

指標 意味 目安
Return [%] 総収益率 Buy&Holdと比較
Max. Drawdown [%] 最大ドローダウン -20%以内が望ましい
Sharpe Ratio リスク調整後リターン 1.0以上が目安
Win Rate [%] 勝率 単独では判断不可
# Trades 総取引回数 少なすぎは過学習の疑い

ちなみに。実は今週、会社の同僚にこのバックテストコードを見せたら「え、これPythonで書いたの?天才じゃん」って言われたんだよネ!!ちょっとしたドヤ顔しちゃいましたヨ(笑)😏 でも僕が最初にやった時のバックテストはMax. Drawdown -47%という絶望的な数字が出て、「やらかしちゃいましたヨ……😭」ってなったのは内緒デス……。


まとめ:予測モデルは「道具」であって「魔法」じゃないヨ!!

キミ、最後まで読んでくれてありがとうネ!!✨ 今回実装したパイプラインをサクッと振り返るとサ:

  1. 第2章ccxt + ta でデータ取得・テクニカル指標計算・Walk-forward分割
  2. 第3章:Prophetにadd_regressor()でRSI・MACDを組み込んで時系列予測
  3. 第4章:Bi-LSTMのアーキテクチャ構築 + Optunaでハイパーパラメータ自動最適化
  4. 第5章:RMSE逆数加重平均アンサンブル + Stackingオプション
  5. 第6章backtesting.pyで売買戦略のバックテスト

この記事で実装したアンサンブルモデルは、単独のProphetやBi-LSTMより時系列予測の精度が高い。でも最後にもう一度だけ言わせてネ!!💡

機械学習による価格予測モデルの性能と、実際の売買収益は別物デスヨ!! R²=0.99のモデルを作っても、バックテストでSharpe Ratioが0.3だったら実運用には向かないんデスネ。予測モデルはあくまで意思決定の「道具」として使うこと。それを忘れて実弾投入すると、僕みたいに「やらかしちゃいましたヨ……😭」ってなるんデスネ……(笑)

次の第3部では、Plotlyを使った予測結果の可視化ダッシュボードと、Streamlitでのリアルタイム予測アプリ化を一緒に実装しますヨ!!お楽しみにネ!!🔥


⚠️ 免責事項:本記事はあくまで機械学習・ディープラーニングの学習目的で作成したものです。実際の投資判断・売買に利用する場合は、自己責任のもとで行ってください。仮想通貨取引には元本割れのリスクがあります。

関連記事

機械学習による仮想通貨価格予測 第3部:バックテストと自動売買システム統合【完全ガイド】
AI・機械学習

機械学習による仮想通貨価格予測 第3部:バックテストと自動売買システム統合【完全ガイド】

機械学習・仮想通貨・バックテストの落とし穴を徹底解説。自動売買・ウォークフォワード最適化・FreqAI・モデル劣化対策まで、プロダクション実装の全手順をPythonコード付きで解説するヨ!

機械学習による仮想通貨価格予測 第1部:時系列データの前処理とテクニカル指標【実践ガイド】
AI・機械学習

機械学習による仮想通貨価格予測 第1部:時系列データの前処理とテクニカル指標【実践ガイド】

LSTMで仮想通貨価格を予測するための前処理パイプラインを徹底解説。CCXTによるOHLCVデータ取得・欠損値の線形補間・IQRクリッピング・RSI/MACD/ATR/ボリンジャーバンドの追加・RobustScalerによる正規化とデータリーク防止まで、実装コード付きで紹介します。

プロンプトエンジニアリングの"次" — コンテキストエンジニアリングが変えるAIエージェント設計の常識
AI・機械学習

プロンプトエンジニアリングの"次" — コンテキストエンジニアリングが変えるAIエージェント設計の常識

プロンプトを書き直しても改善しないAIエージェント設計に悩んでいませんか?コンテキストエンジニアリングの定義からLLMの失敗パターン4選・RAG・メモリ設計・マルチエージェント構成まで、今日から使える設計パターンを実体験とコード付きで解説します。

コメント

0/2000