第 06 課:データエンジニアリングの厳しい現実
データ問題はモデル問題よりも多くの戦略を殺します。
悪夢の週末
金曜日の夜11時、あるエンジニアが慎重に開発した米国株クオンツ戦略をデプロイしました。バックテストで年間80%リターン、シャープレシオ2.5、すべてが完璧に見えました。
月曜日の朝9時、彼はアラートメッセージで起こされました:「戦略エラー、データを取得できません。」
彼は起きて確認すると、Alpha Vantage APIが429 Too Many Requestsを返していました。彼のプログラムは1分間に10回リクエストしており、無料アカウントのAPIレート制限(1分間に5回)を超えていました。
それを修正した後、戦略はついに動き始めました。
月曜日の午後2時、別のアラート:「戦略ポジション異常。」
今回はデータ問題でした:APIが欠損した分足バーを返し、MACDがNaNを計算し、プログラムはNaNを売りシグナルとして扱い、すべてのポジションをクリアしました。
それを修正した後、彼は包括的なデータチェックを行うことにしました。すると、さらに多くの問題を発見しました:
- 一部のタイムスタンプはUTC、他はET(東部時間)
- 一部のバーは0出来高(プレマーケット/アフターアワーセッション)
- 履歴データとリアルタイムデータは異なるフィールド名(
closevsClose)
1週間まるまる、彼は戦略コードを1行も書かず - すべてデータ問題の修正に費やしました。
これがデータエンジニアリングの厳しい現実です。
6.1 データソース選択
無料データソース
| データソース | 市場カバレッジ | メリット | デメリット |
|---|---|---|---|
| Yahoo Finance (yfinance) | 米国株、ETF | 無料、長い履歴 | 最近の安定性問題が報告されている(API変更/レート制限/欠損データ);堅牢なフォールバックを構築 |
| Alpaca Markets | 米国株 | 無料IEXデータ、開発者フレンドリーAPI | 無料ティアはデータカバレッジが制限 |
| Binance API | 暗号通貨 | リアルタイム、無料 | Binanceデータのみ |
| Alpha Vantage | 株式、外国為替、暗号通貨 | 無料ティアあり | 無料ティアのクォータは非常に低いことが多い(制限は変更される;公式サイトで確認) |
| CCXT | 暗号通貨 | 統一インターフェース | 取引所APIに依存 |
注意:データベンダーは製品、API、クォータ、価格を変更したり、サービスを終了したりする可能性があります。データソースには「上場廃止リスク」があるものとして扱い、常にバックアップを用意してください。
有料データソース(2024-2025参考価格)
| データソース | 市場カバレッジ | 月額コスト | 機能 |
|---|---|---|---|
| Bloomberg | グローバル | $2,500+ | 機関標準、最高品質 |
| LSEG Workspace (旧Refinitiv) | グローバル | 見積ベース価格 | LSEG子会社、旧Reutersデータ |
| Polygon.io (現在Massiveにリブランド) | 米国株 | $99+ | リアルタイム + 履歴、開発者フレンドリー |
| Nasdaq Data Link (旧Quandl) | 代替データ | データセットごとの価格 | 衛星、消費者データなど |
どう選ぶ?
個人学習/研究:
-> 無料データソース + データ問題を許容
小規模ファンド(< $1M AUM):
-> 有料データ(基本ティア) + データ検証構築
機関(> $10M AUM):
-> Bloomberg/Refinitiv + マルチソースクロス検証
核心原則:データ品質は戦略の上限を決定。ゴミを入れれば、ゴミが出る。
データソース進化パス
段階 推奨ソース 備考 学習/プロトタイピング Yahoo Finance (yfinance) 無料日次バー、演習に十分 小規模ライブ Polygon.io / Alpaca リアルタイムクォート、開発者フレンドリーAPI 機関本番 Databento / 取引所直接フィード L1/L2リアルタイムデータ、低レイテンシ このレッスンの演習はYahoo Financeで完璧に動作します。ライブ取引の準備ができたら、プロフェッショナルグレードのデータソースにアップグレードする必要があります — 詳細はレッスン19を参照してください。
6.2 APIの辛い現実
書くと思うコード vs 現実
あなたが思うこと:
data = api.get_history("AAPL", days=365)
現実:
疑似コードリファレンス(yfinance / alpaca / ccxtなどの特定のSDKに置き換える)
# 注意:これは堅牢なデータ取得設計パターンを示す疑似コードです
# api、log、EmptyDataError、RateLimitErrorは特定のライブラリに置き換える必要があります
import time
from typing import Optional
import pandas as pd
def get_history_robust(
symbol: str,
days: int,
max_retries: int = 5,
backoff_base: float = 2.0
) -> Optional[pd.DataFrame]:
"""
堅牢なデータ取得関数
処理される問題:
- Rate Limiting(指数バックオフリトライ)
- 空データ(検証 + アラート)
- 接続エラー(自動再接続)
- タイムアウト(合理的なタイムアウト設定)
"""
for attempt in range(max_retries):
try:
# タイムアウトを設定(APIコールに置き換える)
data = api.get_history(
symbol,
days=days,
timeout=30
)
# データを検証(汎用ロジック)
if data is None:
raise EmptyDataError(f"No data for {symbol}")
if len(data) == 0:
raise EmptyDataError(f"Empty data for {symbol}")
if len(data) < days * 0.9: # 期待データの90%未満
log.warning(f"Data incomplete: got {len(data)}, expected ~{days}")
# 異常をチェック
if data['close'].isnull().any():
log.warning("NaN values detected, filling...")
data['close'] = data['close'].ffill()
return data
except RateLimitError:
wait_time = backoff_base ** attempt
log.info(f"Rate limited, waiting {wait_time}s...")
time.sleep(wait_time)
except ConnectionError:
log.warning("Connection lost, reconnecting...")
api.reconnect()
time.sleep(1)
except TimeoutError:
log.warning("Request timeout, retrying...")
except Exception as e:
log.error(f"Unexpected error: {e}")
if attempt == max_retries - 1:
raise
return None一般的なAPI問題
| 問題 | 症状 | 解決策 |
|---|---|---|
| Rate Limiting | 429エラー | 指数バックオフ + リクエストキュー |
| 欠損データ | 空のレスポンスまたは部分データ | マルチソースバックアップ + 欠損検出 |
| データ遅延 | タイムスタンプの遅れ | 遅延を記録 + 戦略を調整 |
| フォーマット不一致 | フィールド名が変わる | 統一データアダプタ層 |
| 不安定な接続 | 切断、タイムアウト | ハートビート検出 + 自動再接続 |
本番ノート:正規データモデル 異なる市場とデータソースは異なるシンボル形式を使用します(
600519.SH、AAPL、0700.HK、BTC/USDT)。本番システムには、外部形式を内部標準に変換する統一正規化層(Normalizer)が必要です(例:SYMBOL.EXCHANGE:AAPL.NASDAQ、0700.HKEX)。これはマルチマーケット取引の前提条件です。一般的なシンボル解決の落とし穴に注意してください:上場廃止ティッカーが静かに失敗、企業行動がシンボルを変更、マルチパートティッカー(例:BRK.B)など。詳細はレッスン19を参照してください。
Rate Limitingの実際のコスト
100シンボルの分足バーが必要だとします:
| API制限 | 完了時間 |
|---|---|
| 1リクエスト/秒 | 100秒 |
| 10リクエスト/秒 | 10秒 |
| 無制限 | < 1秒 |
高頻度戦略には高いAPIクォータが必要 - これが有料データソースの核心価値です。
6.3 時間調整問題
タイムゾーンの混乱
異なるデータソースは異なるタイムゾーンを使用:
| データソース | デフォルトタイムゾーン |
|---|---|
| Binance | UTC |
| Yahoo Finance | 取引所現地時間 |
| 中国A株 | UTC+8 |
| 米国株 | ET(東部時間) |
処理しないと:
- 9:30 A株データ + 9:30米国株データ -> 実際には12時間離れている
- 戦略は「同じ瞬間」のデータを使用するが、実際には異なる瞬間
解決策:すべてをUTCに変換、UTCで保存して計算。
import pandas as pd
def normalize_timezone(df, source_tz='UTC'):
"""UTCに標準化"""
if df.index.tz is None:
df.index = df.index.tz_localize(source_tz)
df.index = df.index.tz_convert('UTC')
return df
ティックからローソク足への集計
ティックデータをローソク足に集計する際の注意:
| 問題 | 説明 |
|---|---|
| 始値 | その期間の最初の取引価格 |
| 終値 | その期間の最後の取引価格 |
| 高値 | その期間の最高取引価格 |
| 安値 | その期間の最低取引価格 |
| 出来高 | その期間のすべての取引の合計 |
罠:特定の分に取引がない場合は?
オプション1:OHLCを前の分の終値で埋める
オプション2:欠損としてマーク、後で処理
オプション3:その時点を集計から除外
異なるアプローチは異なる指標計算につながります。処理ルールを標準化する必要があります。
クロスアセットデータ調整
株式、先物、外国為替は異なる取引時間:
| 資産 | 取引時間 |
|---|---|
| 中国A株 | 9:30-11:30、13:00-15:00 |
| 米国株 | 9:30-16:00 ET |
| 外国為替 | ほぼ24/7 |
| 暗号通貨 | 24/7 |
戦略がクロスアセットシグナルを必要とする場合:
- A株がクローズするとき、米国はまだオープンしていない
- A株クローズ + 米国オープンを使用?時間ギャップがある
- 慎重な「同期ポイント」設計が必要
6.4 データ品質問題
異常検出と処理
| 異常タイプ | 検出方法 | 処理 |
|---|---|---|
| 価格ジャンプ | 価格変動 > 20% | 実際か確認(株式分割?) |
| 出来高異常 | 0または極端な値 | 取引所ステータスを確認 |
| 欠損値 | NaN | 前方埋めまたは削除 |
| 重複データ | 重複タイムスタンプ | 最初または最後を保持 |
def detect_anomalies(df, price_col='close', volume_col='volume'):
"""データ異常を検出"""
issues = []
# 価格ジャンプ
returns = df[price_col].pct_change()
jumps = returns.abs() > 0.20
if jumps.any():
issues.append(f"Price jumps detected: {jumps.sum()} times")
# ゼロ出来高
zero_volume = df[volume_col] == 0
if zero_volume.any():
issues.append(f"Zero volume: {zero_volume.sum()} bars")
# 欠損値
nulls = df.isnull().sum()
if nulls.any():
issues.append(f"Null values: {nulls.to_dict()}")
# 重複タイムスタンプ
duplicates = df.index.duplicated()
if duplicates.any():
issues.append(f"Duplicate timestamps: {duplicates.sum()}")
return issues
株式配当と分割調整
株式の履歴価格には「調整」が必要:
| イベント | 実際の変更 | 未調整データ | 調整データ |
|---|---|---|---|
| 2対1分割 | 株式2倍、価格半分 | 前後で価格の崖 | 滑らかで連続 |
| 配当 | 権利落ち控除 | 権利落ち日のギャップ | 履歴価格が調整される |
調整しない結果:
- バックテストで、戦略が権利落ち日に偽シグナルを生成
- リターン計算が極端な値を示す
先物ロール処理
先物契約は満期になり「ロール」が必要:
2024年1月:IF2401(1月契約)を保有
1月満期前:IF2402(2月契約)に切り替え
問題:IF2401は4000でクローズ、IF2402は4050でクローズ
実際の価格変動ではなく、異なる契約
解決策:
- 接続時にスプレッドを計算、履歴価格を調整
- または「連続主契約」データを使用(データベンダーが事前処理)
6.5 サバイバーシップバイアス
サバイバーシップバイアスとは?
「今日も存在する株式」のみでバックテストすると:
2010年株式プール:1000株
2024年にまだ存在:800株
上場廃止/破産:200株
バックテストは800のみを使用、失敗した200社を無視
-> バックテストリターンが膨張
サバイバーシップバイアスはどれほど深刻?
| 研究 | 結論 |
|---|---|
| 学術研究 | 年間リターンが1-3%膨張 |
| バリュー投資戦略 | バイアスがより深刻(安い株式は上場廃止の可能性が高い) |
| 小型株戦略 | バイアスが最大 |
どう避ける?
| 方法 | 難易度 | 有効性 |
|---|---|---|
| 上場廃止株を含むデータベースを使用 | 高 | 最も正確 |
| 履歴インデックス構成銘柄を使用 | 中 | かなり正確 |
| 結論でバイアスを認める | 低 | 少なくとも正直 |
有料データソースは通常「サバイバーシップバイアスフリー」データセットを提供 - 支払うもう一つの価値。
6.6 代替データ入門
代替データとは?
従来のデータ:価格、出来高、財務諸表 代替データ:その他すべて
| タイプ | データソース | 応用 |
|---|---|---|
| 衛星データ | 駐車場の車数、オイルタンクレベル | 小売収益を予測、石油在庫 |
| テキストデータ | ニュース、ソーシャルメディア、決算電話 | センチメント分析、イベント駆動 |
| クレジットカードデータ | 支出統計 | 企業収益を予測 |
| Webトラフィック | Webサイト訪問 | eコマースパフォーマンスを予測 |
| GPSデータ | 電話位置 | フットトラフィック分析 |
代替データの課題
| 課題 | 説明 |
|---|---|
| 高ノイズ | 信号/ノイズ比が価格データよりはるかに低い |
| コンプライアンスリスク | プライバシー問題、データソースの合法性 |
| 速いAlpha減衰 | 広く使用されると、利点が消える |
| 高コスト | データ自体が高価 + 処理コスト |
マルチAgent視点
6.7 データパイプライン設計原則
3つの原則
| 原則 | 説明 |
|---|---|
| 不変性 | 生データを決して変更しない、処理後に新しいファイルを生成 |
| 追跡可能性 | 各データポイントのソース、処理時間、処理バージョンを記録 |
| 冗長バックアップ | 少なくとも2つのデータソースでクロス検証 |
データパイプラインアーキテクチャ
データ収集層
|
|-> 生データストレージ(不変)
| |
| v
|-> データクリーニング層
| - 異常処理
| - 欠損値埋め
| - タイムゾーン標準化
| |
| v
|-> 特徴量エンジニアリング層
| - テクニカル指標計算
| - ラベル生成
| |
| v
--> 準備完了データストレージ -> 戦略使用
監視とアラート
| 監視項目 | 推奨しきい値 | アラートアクション |
|---|---|---|
| データ遅延 | > 1分 | 警告 |
| 欠損データ率 | > 1% | 警告 |
| 異常比率 | > 0.1% | 調査 |
| APIエラー率 | > 5% | 戦略一時停止 |
コード実装(オプション)
データ品質チェックフレームワーク
import pandas as pd
from dataclasses import dataclass
from typing import List
@dataclass
class DataQualityReport:
symbol: str
start_date: str
end_date: str
total_rows: int
missing_rows: int
null_values: dict
anomalies: List[str]
is_valid: bool
def check_data_quality(df: pd.DataFrame, symbol: str) -> DataQualityReport:
"""包括的データ品質チェック"""
anomalies = []
# 時間連続性をチェック
# 注意:この例は*日次*バーを想定し、営業日を大まかな期待値として使用します。
# 分/ティックデータの場合、取引所の取引セッション、休日、ランチブレーク(ある場合)などに基づいて
# expected_indexを生成する必要があります。
idx = pd.DatetimeIndex(df.index)
expected_index = pd.bdate_range(start=idx.min().normalize(), end=idx.max().normalize())
expected_rows = len(expected_index)
actual_rows = len(df)
missing_rows = expected_rows - actual_rows
if missing_rows > expected_rows * 0.05:
anomalies.append(f"High missing rate: {missing_rows/expected_rows:.1%}")
# NULL値をチェック
null_counts = df.isnull().sum().to_dict()
# 価格ジャンプをチェック
if 'close' in df.columns:
returns = df['close'].pct_change()
extreme_moves = (returns.abs() > 0.2).sum()
if extreme_moves > 0:
anomalies.append(f"Extreme price moves: {extreme_moves}")
# 出来高をチェック
if 'volume' in df.columns:
zero_volume = (df['volume'] == 0).sum()
if zero_volume > 0:
anomalies.append(f"Zero volume periods: {zero_volume}")
# タイムスタンプ重複をチェック
duplicates = df.index.duplicated().sum()
if duplicates > 0:
anomalies.append(f"Duplicate timestamps: {duplicates}")
return DataQualityReport(
symbol=symbol,
start_date=str(df.index[0]),
end_date=str(df.index[-1]),
total_rows=actual_rows,
missing_rows=missing_rows,
null_values=null_counts,
anomalies=anomalies,
is_valid=len(anomalies) == 0
)
レッスン成果物
このレッスンを完了すると、以下が得られます:
- データ問題の深い理解 - データエンジニアリングがクオンツの主戦場であることを知る
- APIベストプラクティス - 堅牢なデータ取得コードフレームワーク
- データ品質チェック能力 - 一般的なデータ問題を識別して処理できる
- データパイプライン設計思考 - 本番グレードのデータシステムアーキテクチャを理解
重要ポイント
- 無料と有料データソースのトレードオフを理解
- APIレート制限、欠損データなどの処理方法をマスター
- タイムゾーンの混乱、サバイバーシップバイアスなどの隠れた問題を認識
- 代替データの価値と課題を理解
- データパイプライン設計原則を理解
拡張読書
- 背景知識:取引所と注文簿メカニズム - ティックデータのソース
- 背景知識:暗号通貨取引の特性 - 24/7データの課題
次のレッスンプレビュー
レッスン 07: バックテストシステムの落とし穴
データが問題なくても、バックテストはあなたを欺く可能性があります。Look-Ahead Bias、過剰適合、取引コストの無視...これらの罠は、見た目には完璧な戦略を無数にライブ取引で惨めに失敗させました。次のレッスンでバックテストの真実を明らかにします。