機械学習による仮想通貨価格予測(第1部):データ前処理&テクニカル指標の完全解説
機械学習による仮想通貨価格予測(第1部):データ前処理&テクニカル指標の完全解説
1. はじめに:なぜ「データ品質」が予測精度を決めるのか
1-1. 仮想通貨価格予測の現状と可能性
機械学習による仮想通貨価格予測は、2025〜2026年にかけて大きな転換点を迎えています。かつての主流であったLSTM単体モデルから、LSTM-GRUハイブリッドやTransformerベースのアーキテクチャへと研究の重心が移り、予測精度は着実に向上しています。
特筆すべきは、GRUニューラルネットワークがビットコインの価格予測でMAPE(平均絶対パーセント誤差)=0.09%という驚異的な精度を達成した事例が報告されていることです。また、高次元テクニカル指標を統合したMLモデルを Bitcoin・Ethereum・Ripple に適用する研究も活発化しており、方向性予測(上昇か下落か)においては55〜65%の正解率が現実的な目標値として研究標準となっています。
55〜65%という数字は一見低く感じるかもしれません。しかし仮想通貨市場は株式市場よりもランダムウォーク的な価格変動が激しく、この水準を継続的に達成できれば統計的に有意なエッジ(優位性)があると判断されます。
1-2. この連載の全体像と第1部の位置づけ
本連載は全3部構成で、機械学習を用いた仮想通貨価格予測システムをゼロから構築することを目標としています。
| 回 | テーマ |
|---|---|
| 第1部(本記事) | データ前処理 & テクニカル指標 |
| 第2部 | モデル構築(LSTM・GRU・Transformer) |
| 第3部 | バックテスト・リアルタイム予測 |
実際の開発現場では「予測モデルを複雑にするより、データを丁寧に前処理する方が精度向上に直結する」というのはベストプラクティスとして広く認識されています。第1部をマスターするだけで、予測精度の8割が決まるといっても過言ではありません。
1-3. 対象読者・前提知識
- Python の基本操作ができる方(pandas・NumPy 経験があれば尚良)
- 機械学習の基本概念(学習・検証・テストの概念)を理解している方
- 実装経験は問いません。コードを動かしながら段階的に理解を深めていきましょう
2. OHLCVデータの基礎知識
2-1. OHLCVとは何か
OHLCVとは、Open(始値)・High(高値)・Low(安値)・Close(終値)・Volume(出来高)の5つの情報を1本のローソク足にまとめたデータ形式です。
ローソク足1本 = {
open: その時間足の最初の取引価格
high: その時間足の最高価格
low: その時間足の最安価格
close: その時間足の最後の取引価格
volume: その時間足の総取引量
}
「終値だけを使えばいいのでは?」と思う方もいるかもしれませんが、終値のみでは価格のボラティリティ(high-low の差)や売買圧力(volume)といった重要な市場情報が失われます。OHLCVを揃えることで、RSIやボリンジャーバンドといったテクニカル指標を正確に計算できるようになります。
なお、ティックデータ(1取引ごとの約定データ)は最もきめ細かい情報を持ちますが、データ量が膨大になるため、一般的な価格予測には日足・時間足・15分足などのOHLCVが使われます。
2-2. データ取得APIの選び方
| API | 強み | 注意点 |
|---|---|---|
| Binance API | 高頻度データ(分足〜)に強い。無料枠が充実 | レート制限あり。海外取引所 |
| CoinAPI | マルチ銘柄・長期履歴データが充実 | 無料プランは1日100リクエスト |
| Yahoo Finance API | 手軽さと実績。30銘柄以上・2021〜2025年のデータが取得可能 | 高頻度データには不向き |
選定基準としては、①コスト(無料枠の範囲)、②レート制限(1日あたりのリクエスト数)、③データ品質(欠損率・遅延)の3点を確認することをお勧めします。学習・プロトタイピング段階では yfinance が最も手軽です。
2-3. Pythonでデータを取得する(実装例)
import yfinance as yf
import pandas as pd
def fetch_ohlcv(ticker: str, start: str, end: str, interval: str = "1d") -> pd.DataFrame:
"""
Yahoo Finance から OHLCV データを取得する
ticker : ティッカーシンボル(例: "BTC-USD")
start : 開始日("YYYY-MM-DD")
end : 終了日("YYYY-MM-DD")
interval: 時間足("1d", "1h", "15m" など)
"""
df = yf.download(ticker, start=start, end=end, interval=interval, progress=False)
df.columns = ["open", "high", "low", "close", "volume"] # カラムを小文字統一
df.index.name = "datetime"
return df
# 取得例:BTCの日足データ(2021〜2025年)
btc = fetch_ohlcv("BTC-USD", start="2021-01-01", end="2025-12-31")
# 取得確認
print(f"データ件数 : {len(btc)} 行")
print(f"期間 : {btc.index[0]} 〜 {btc.index[-1]}")
print(f"欠損値の合計 : {btc.isnull().sum().sum()} 件")
print(btc.head())ccxt を使えば Binance などの取引所から直接取得することも可能です。複数銘柄を扱う場合は for ループで取得し、辞書や dict にまとめて管理するとコードが整理しやすくなります。
3. データ前処理パイプラインの構築
データ前処理は「つまらない作業」と思われがちですが、実際の開発現場ではここに最も多くの時間と注意を払います。各ステップを順番に見ていきましょう。
3-1. ステップ①:欠損値の検出と補完
仮想通貨データに欠損が生じる主な原因は、取引所のメンテナンス停止・APIの一時障害・祝日対応の不備などです。特に24時間超の欠損は指標計算に致命的な影響を与えます。例えばRSIは14期間の連続データを必要とするため、長期欠損があると計算結果が歪みます。
def handle_missing(df: pd.DataFrame, gap_threshold_hours: int = 24) -> pd.DataFrame:
"""
欠損値を補完する。長期欠損(gap_threshold_hours 超)は警告を出す。
"""
# 欠損の連続長を確認
missing_mask = df["close"].isnull()
if missing_mask.any():
# 連続欠損の最大幅を計算
max_gap = missing_mask.astype(int).groupby(
missing_mask.ne(missing_mask.shift()).cumsum()
).sum().max()
if max_gap > gap_threshold_hours:
print(f"[警告] {max_gap} 時間超の連続欠損を検出。該当期間の削除を推奨します。")
# 短期欠損は前方補完(前の値で埋める)
df = df.ffill()
# 残った欠損(先頭部分)は後方補完
df = df.bfill()
return df補完手法の選び方は以下の通りです。
| 手法 | 適用場面 |
|---|---|
| 前方補完(ffill) | 短期欠損(数時間以内)に有効。市場が止まっている間の価格を維持 |
| 線形補間 | 数値が滑らかに変化すると仮定できる場合 |
| 削除 | 24時間超の長期欠損。補完しても精度が落ちるため |
3-2. ステップ②:外れ値の検出とフラグ処理
仮想通貨市場では1日で20%以上価格が動くことがあります。これを単純に外れ値として削除すると、フラッシュクラッシュや急騰といった重要な市場イベントの情報が失われてしまいます。ベストプラクティスとして、削除ではなくフラグを立てて保持するアプローチが推奨されます。
import numpy as np
def detect_outliers(df: pd.DataFrame, z_threshold: float = 3.0) -> pd.DataFrame:
"""
Zスコアで外れ値を検出し、フラグ列を追加する
"""
log_returns = np.log(df["close"] / df["close"].shift(1))
z_scores = (log_returns - log_returns.mean()) / log_returns.std()
df["outlier_flag"] = (z_scores.abs() > z_threshold).astype(int)
# 価格変動20%超もフラグ
price_change = df["close"].pct_change().abs()
df["outlier_flag"] = df["outlier_flag"] | (price_change > 0.20).astype(int)
print(f"外れ値検出数: {df['outlier_flag'].sum()} 件 ({df['outlier_flag'].mean():.2%})")
return df3-3. ステップ③:対数変換とリターン化
仮想通貨の価格系列は非定常です。つまり、平均や分散が時間とともに変化するため、そのままモデルに入力すると学習が不安定になります。対数収益率(Log Return)に変換することで、この問題を緩和できます。
$$r_t = \ln\left(\frac{P_t}{P_{t-1}}\right)$$
def add_log_return(df: pd.DataFrame) -> pd.DataFrame:
"""対数収益率を計算して列に追加する"""
df["log_return"] = np.log(df["close"] / df["close"].shift(1))
df.dropna(subset=["log_return"], inplace=True)
return df変換後の分布は元の価格系列よりも正規分布に近づき、モデルの学習が安定しやすくなります。
3-4. ステップ④:StandardScalerによる正規化
各特徴量のスケールが大きく異なると(例:価格は数万ドル、RSIは0〜100)、ニューラルネットワークの勾配計算が不安定になり、収束が遅くなります。正規化は必須の工程です。
from sklearn.preprocessing import StandardScaler
# ⚠️ 重要:スケーラーは訓練データのみで fit する
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
# テストデータには訓練データで fit したスケーラーを適用する(fit は不要)
X_test_scaled = scaler.transform(X_test)StandardScaler vs MinMaxScaler の使い分け:
- StandardScaler(平均0・標準偏差1):外れ値に対してロバスト。LSTMやGRUに推奨
- MinMaxScaler(0〜1の範囲):シグモイド活性化関数を使う場合に適合しやすい
⚠️ 最も多い落とし穴:テストデータに対して
fit_transformを呼ぶと、テストデータの統計量がスケーラーに染み込み、未来情報が訓練に漏れ出します(データリーク)。必ずtransformのみを使用してください。
3-5. ステップ⑤:クラス不均衡への対策(分類タスクの場合)
「価格が上がるか下がるか」を予測する分類タスクでは、上昇と下落の件数が均等でないことが多く、モデルが多数派クラスばかりを予測するようになります。
from imblearn.over_sampling import SMOTE
smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)
print(f"リサンプリング前: {dict(zip(*np.unique(y_train, return_counts=True)))}")
print(f"リサンプリング後: {dict(zip(*np.unique(y_resampled, return_counts=True)))}")SMOTEは少数派クラスの合成データを生成することで不均衡を解消します。ただし、回帰タスク(価格の具体的な値を予測する場合)では不要です。
3-6. データ分割の標準:80 / 10 / 10 ルール
研究標準として定着しているのが「訓練80%・検証10%・テスト10%」の分割比率です。
def split_time_series(df: pd.DataFrame, train_ratio=0.8, val_ratio=0.1):
"""時系列データを順序を保って分割する"""
n = len(df)
train_end = int(n * train_ratio)
val_end = int(n * (train_ratio + val_ratio))
train = df.iloc[:train_end]
val = df.iloc[train_end:val_end]
test = df.iloc[val_end:]
print(f"訓練: {len(train)} 件 / 検証: {len(val)} 件 / テスト: {len(test)} 件")
return train, val, test⚠️ 時系列データでは無作為分割(シャッフル)は厳禁です。未来のデータが訓練データに混入し、評価結果が実態より大幅に良く見えてしまいます。必ず時系列順を保持したまま分割してください。
ウォークフォワード検証と組み合わせることで、より実態に近い汎化性能を評価できます。
4. 主要テクニカル指標の実装
4-1. RSI(相対力指数)
RSIは、一定期間の「上昇幅の平均」と「下落幅の平均」の比から、価格のモメンタム(勢い)を0〜100のスケールで表します。
$$RSI = 100 - \frac{100}{1 + RS}, \quad RS = \frac{\text{平均上昇幅}}{\text{平均下落幅}}$$
- 30以下:売られすぎ(買いシグナル候補)
- 70以上:買われすぎ(売りシグナル候補)
def calc_rsi(df: pd.DataFrame, period: int = 14) -> pd.DataFrame:
delta = df["close"].diff()
gain = delta.clip(lower=0)
loss = -delta.clip(upper=0)
avg_gain = gain.ewm(com=period - 1, min_periods=period).mean()
avg_loss = loss.ewm(com=period - 1, min_periods=period).mean()
rs = avg_gain / avg_loss
df[f"rsi_{period}"] = 100 - (100 / (1 + rs))
return df特徴量として使う場合、RSI値そのものだけでなく「RSIが30を下回った直後」「RSIが70を超えた後の反転」といった閾値クロスのフラグを追加することで、モデルの判断材料が豊かになります。
4-2. MACD(移動平均収束拡散法)
MACDは2本のEMAの差からトレンドの転換点を検出します。
$$\text{MACD線} = EMA(12) - EMA(26)$$ $$\text{シグナル線} = EMA(\text{MACD}, 9)$$ $$\text{ヒストグラム} = \text{MACD線} - \text{シグナル線}$$
def calc_macd(df: pd.DataFrame, fast=12, slow=26, signal=9) -> pd.DataFrame:
ema_fast = df["close"].ewm(span=fast, adjust=False).mean()
ema_slow = df["close"].ewm(span=slow, adjust=False).mean()
df["macd"] = ema_fast - ema_slow
df["macd_signal"] = df["macd"].ewm(span=signal, adjust=False).mean()
df["macd_hist"] = df["macd"] - df["macd_signal"]
return dfヒストグラムが0をクロスする瞬間(ゼロクロス)がトレンド転換のシグナルとして機能します。この値をそのまま特徴量として入力することで、モデルがトレンドの強弱を学習できます。
4-3. ボリンジャーバンド
ボリンジャーバンドは移動平均線を中心に、標準偏差の幅でバンドを描きます。
$$\text{上限バンド} = SMA(20) + 2\sigma, \quad \text{下限バンド} = SMA(20) - 2\sigma$$
def calc_bollinger(df: pd.DataFrame, period: int = 20, num_std: float = 2.0) -> pd.DataFrame:
sma = df["close"].rolling(window=period).mean()
std = df["close"].rolling(window=period).std()
df["bb_upper"] = sma + num_std * std
df["bb_lower"] = sma - num_std * std
df["bb_middle"] = sma
# %B:現在価格がバンド内のどの位置にあるか(0〜1)
df["bb_pct_b"] = (df["close"] - df["bb_lower"]) / (df["bb_upper"] - df["bb_lower"])
# バンド幅:ボラティリティの代理指標
df["bb_width"] = (df["bb_upper"] - df["bb_lower"]) / sma
return dfバンド幅(bb_width)が急激に縮小する「スクイーズ」状態は、大きな価格変動の前兆として知られています。このパターンをフラグとして追加することが、予測精度の向上につながることがあります。
4-4. EMA(指数移動平均)
EMAはSMA(単純移動平均)と異なり、直近のデータにより大きな重みを置きます。これにより、価格変動へのレスポンスが速くなります。
def calc_ema(df: pd.DataFrame, periods: list = [9, 21, 50]) -> pd.DataFrame:
for p in periods:
df[f"ema_{p}"] = df["close"].ewm(span=p, adjust=False).mean()
# クロスオーバーシグナル(短期EMAが長期EMAを上抜け)
df["ema_cross_9_21"] = (df["ema_9"] > df["ema_21"]).astype(int)
df["ema_cross_21_50"] = (df["ema_21"] > df["ema_50"]).astype(int)
return df短期(9)・中期(21)・長期(50)の三本EMAを組み合わせたクロス戦略は、多くのトレーダーに使われており、機械学習モデルへの入力特徴量としても実績があります。
4-5. 指標の組み合わせ戦略と注意点
実際の研究事例では、**RSI+MACDを組み合わせたBTCバックテストで勝率77%**を達成した報告があります。しかし指標を増やせば増やすほど良いわけではありません。
注意すべき点は多重共線性です。例えばEMA(9)とEMA(21)は高い相関を持つため、両方を独立した特徴量として投入するとモデルが混乱することがあります。相関行列で指標間の関係を確認することをお勧めします。
初心者には「まず3〜5指標から始める」ことを強く推奨します。RSI・MACD・ボリンジャーバンドの3つだけでも、十分な情報量を確保できます。
5. 特徴量エンジニアリングのベストプラクティス
5-1. ラグ特徴量(過去N期間の価格情報)
LSTM・GRUはシーケンスデータを扱えますが、RandomForestやXGBoostを使う場合は「過去何日前の価格か」を明示的に特徴量として追加する必要があります。
def add_lag_features(df: pd.DataFrame, target_col: str = "close",
lags: list = [1, 3, 7]) -> pd.DataFrame:
for lag in lags:
df[f"{target_col}_lag_{lag}"] = df[target_col].shift(lag)
df.dropna(inplace=True)
return dfラグ期間が長いほど過去の情報を参照できますが、ウィンドウサイズを増やしすぎると過学習のリスクが高まります。7日・14日・30日など複数試して、検証データでの性能を見ながら決定してください。
5-2. マルチ銘柄の相関係数を特徴量に加える
BTCが上昇するとき、ETHやXRPも連動して上昇する傾向があります。この相関関係を特徴量として活用することで、予測精度が向上することがあります。
def add_correlation_features(dfs: dict, window: int = 30) -> pd.DataFrame:
"""
dfs: {"BTC": df_btc, "ETH": df_eth, "XRP": df_xrp} のような辞書
各銘柄の終値を結合し、ローリング相関係数を計算する
"""
closes = pd.DataFrame({k: v["close"] for k, v in dfs.items()})
corr_btc_eth = closes["BTC"].rolling(window).corr(closes["ETH"])
corr_btc_xrp = closes["BTC"].rolling(window).corr(closes["XRP"])
dfs["BTC"]["corr_btc_eth"] = corr_btc_eth
dfs["BTC"]["corr_btc_xrp"] = corr_btc_xrp
return dfs["BTC"]ただし、相関が高い銘柄を多数追加しても情報の冗長性が増すだけです。相関係数が0.8を超える組み合わせは1つに絞ることを検討してください。
5-3. 特徴量の重要度評価と選択
全特徴量を投入するのではなく、RandomForestの feature_importances_ を使って重要度を評価し、貢献度の低い特徴量を除去します。
from sklearn.ensemble import RandomForestClassifier
import matplotlib.pyplot as plt
model_rf = RandomForestClassifier(n_estimators=100, random_state=42)
model_rf.fit(X_train, y_train)
importances = pd.Series(model_rf.feature_importances_, index=X_train.columns)
importances.sort_values(ascending=False).head(15).plot(kind="bar", figsize=(12, 5))
plt.title("特徴量の重要度 Top 15")
plt.tight_layout()
plt.show()
# 重要度が0.01未満の特徴量を除外
low_importance = importances[importances < 0.01].index.tolist()
print(f"除外する特徴量: {low_importance}")
X_train_selected = X_train.drop(columns=low_importance)5-4. 完成パイプラインのまとめコード
ここまでの処理をまとめた実装テンプレートです。
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
class CryptoPipeline:
def __init__(self, ticker: str = "BTC-USD", random_seed: int = 42):
self.ticker = ticker
self.random_seed = random_seed
np.random.seed(random_seed)
self.scaler = StandardScaler()
def run(self, df: pd.DataFrame) -> dict:
df = handle_missing(df)
df = detect_outliers(df)
df = add_log_return(df)
df = calc_rsi(df)
df = calc_macd(df)
df = calc_bollinger(df)
df = calc_ema(df)
df = add_lag_features(df)
df.dropna(inplace=True)
feature_cols = [c for c in df.columns if c not in ["open", "high", "low", "close", "volume"]]
X = df[feature_cols]
y = (df["close"].shift(-1) > df["close"]).astype(int) # 翌日上昇=1
train, val, test = split_time_series(df)
X_train = train[feature_cols]
X_val = val[feature_cols]
X_test = test[feature_cols]
X_train_scaled = self.scaler.fit_transform(X_train)
X_val_scaled = self.scaler.transform(X_val)
X_test_scaled = self.scaler.transform(X_test)
return {
"X_train": X_train_scaled, "X_val": X_val_scaled, "X_test": X_test_scaled,
"y_train": train["log_return"].shift(-1).dropna(),
"scaler": self.scaler,
"feature_names": feature_cols,
}6. よくある疑問と落とし穴
6-1. 「テクニカル指標は多いほど良い?」への答え
結論は「No」です。指標を増やすほど特徴量空間が広がり、**過学習(Overfitting)**のリスクが高まります。訓練データでは高精度に見えても、テストデータでは全く機能しないモデルが出来上がります。
また、高度に相関した指標を多数投入すると、各指標が持つシグナルのノイズが増幅されてしまいます。推奨は「まず3指標でベースラインを作り、精度を見ながら段階的に追加する」アプローチです。
6-2. 正規化のリーク問題はなぜ起きるのか
「データリーク(Information Leakage)」は、予測時には本来知りえないはずの未来の情報がモデルの学習に混入してしまう問題です。
❌ 間違った手順:
全データ → fit_transform → 訓練/検証/テストに分割
✅ 正しい手順:
全データを分割 → 訓練データのみで fit → 各データセットに transform
全データに fit_transform を適用してから分割すると、テストデータの統計量(平均・標準偏差)がスケーラーに含まれてしまいます。これにより「未来のデータの情報」が訓練に漏れ出し、評価指標が実態より楽観的に見えてしまいます。
6-3. 仮想通貨は株より予測が難しい理由
以下の要因が、仮想通貨の価格予測を株式市場より困難にしています。
- 24時間365日取引:週末・祝日がなく、データが途切れない分ノイズも多い
- 規制ニュースの突発的影響:政府の規制発表で数時間に数十%動くことがある
- クジラ(大口保有者)の操作:少数の大口投資家が市場を動かせる
- 極めて高いボラティリティ:ビットコインの年率ボラティリティは株式の3〜5倍
それでも機械学習が有効な場面はあります。短期のモメンタム継続性やボラティリティの予測、そして特定のパターン(スクイーズからのブレイクアウト等)の検出においては、統計的に有意なシグナルを捉えられることが複数の研究で示されています。ただし「常に機能する魔法のモデルは存在しない」という事実は、正直にお伝えしておく必要があります。
7. まとめと次のステップ
7-1. 第1部のポイント整理
本記事で学んだ内容を、実装チェックリストとして整理します。
データ取得
- yfinance/ccxt でOHLCVデータを取得した
- データ件数・期間・欠損の有無を確認した
前処理パイプライン
- 欠損値を検出・補完した(24時間超の欠損に注意)
- 外れ値を検出してフラグ列を追加した(削除は非推奨)
- 対数収益率(Log Return)に変換した
- StandardScaler を 訓練データのみで fit した
- 時系列順を保ったまま 80/10/10 で分割した
テクニカル指標
- RSI(14期間)を実装した
- MACD(12/26/9)を実装した
- ボリンジャーバンド(20期間・2σ)を実装した
- EMA(9/21/50)を実装した
特徴量エンジニアリング
- ラグ特徴量を追加した
- RandomForest で特徴量重要度を評価した
- 重要度の低い特徴量を除外した
7-2. 第2部予告:モデル構築編
次回の第2部では、いよいよモデルの構築に入ります。
- LSTM・GRU・Transformerの選び方:それぞれのアーキテクチャの長所と適用シーン
- Kerasによる実装:モデル定義・学習・ハイパーパラメータチューニング
- バックテストと評価指標:RMSE・MAPE・方向性精度による多角的な評価方法
第1部で構築した前処理パイプラインがそのまま第2部のモデルへの入力になります。今回のコードをしっかり動かしておくことが、次回への最善の準備です。
参考文献・リソース
- SpringerLink:Revolutionizing Cryptocurrency Price Prediction: Advanced Insights from Machine Learning — 機械学習による仮想通貨予測の最新研究
- MDPI FinTech:高次元テクニカル指標統合モデル(Bitcoin・Ethereum・Ripple対象)
- PMC(PubMed Central):LSTM・GRU・RandomForestの評価比較研究
- CoinAPI Blog:OHLCVデータ取得のQ&Aと実践ガイド
- Gate Wiki:2026年版テクニカル指標活用ガイド
関連記事
GRUで仮想通貨価格を予測する方法|初心者からわかる実装ガイド2026
GRU(Gated Recurrent Unit)を使った仮想通貨価格予測の実装ガイド。LSTMより速く・軽く・高精度なGRUの仕組みをゼロから解説し、BTC・ETH予測の実績データも紹介します。
機械学習による仮想通貨価格予測(第4部):強化学習で収益を最大化するトレードAIの実装ガイド
強化学習(PPO・DQN・SAC)を使った仮想通貨トレードAIの実装を解説。カスタムGym環境の構築からstable-baselines3による学習まで、Pythonコードで段階的に説明します。
機械学習による仮想通貨価格予測(第3部):予測モデルの検証と自動売買システムへの統合完全ガイド
バックテスト成功でも本番失敗する原因を解説。ウォークフォワード検証・CPCVなどプロの検証手法と、自動売買システムへの統合・運用まで実装コード付きで完全解説。