Skip to content
Go back

経済指標フィルターを自動売買に組み込んだ設計と実装:3層フォールバックで止まらないシステムを作る

Edit page

FX自動売買システムで「米国雇用統計の発表直後にエントリーして大損した」という経験はないだろうか。テクニカル指標ベースのシステムは、経済指標発表前後のボラティリティ急変に対して本質的に脆弱だ。

ADX(Average Directional Index)のようなテクニカル指標でトレンド強度を測っても、14期間の遅延がある。雇用統計の発表は13:30 UTC(22:30 JST)に始まり、その数秒後には相場が急変している。遅延のあるフィルターでは間に合わない。

本記事では、経済指標カレンダーAPIと連動して「発表前から事前にエントリーをブロック」するフィルターを、Finnhub API + FRED API + ハードコードカレンダーの3層フォールバック構造で実装した設計と、実運用で直面した課題について記録する。


なぜテクニカルフィルターだけでは不十分なのか

自動売買システムには既にADX(Average Directional Index)ベースの市場レジームフィルターを実装していた。ADXとは、トレンドの強さを0〜100の数値で表す指標で、値が高いほどトレンドが強いことを示す。これで「トレンドが出ていない局面ではエントリーしない」という制御を行っている。

しかしADXには構造的な問題がある。

ADXの計算: 過去14本のローソク足(H4足なら14×4=56時間分)から算出
→ 相場が急変してからADXに反映されるまで最大56時間のタイムラグ

経済指標の発表は「事前にスケジュールが確定している」イベントだ。米国雇用統計は毎月第1金曜日の13:30 UTC、FOMCは年8回の決まった日程で発表される。この情報を使えば、発表の2時間前からエントリーを止めることができる。

つまり2つのフィルターは補完関係にある。

フィルタートリガー有効タイミング
ADX市場レジームテクニカル指標の値相場変化後、14期間の遅延あり
経済指標カレンダー事前確定スケジュール発表X時間前から即時ブロック

設計のコア:重要度別ブロッキング

すべての経済指標を同じように扱うわけにはいかない。米国雇用統計とISM製造業PMIでは相場への影響度が全く異なる。そこで経済指標を3段階の重要度に分類し、それぞれに異なるブロッキングルールを設定した。

重要度の分類

HIGH(高重要度) — エントリー完全ブロック

指標名解説
米国雇用統計(NFP)Non-Farm Payrolls。毎月第1金曜発表。FX市場最大のイベント
米国CPI(消費者物価指数)インフレ率の主要指標。金利政策に直結
米国FOMC金利決定米連邦準備理事会の政策金利決定。年8回
米国GDP速報値経済成長率。四半期ごと
日本日銀金融政策決定会合日本の金利政策決定。年8回
EUECB金利決定欧州中央銀行の政策金利
英国BOE金利決定イングランド銀行の政策金利

MEDIUM(中重要度) — 閾値を50%引き上げ

ISM製造業PMI、小売売上高、耐久財受注など。相場は動くが、HIGH指標ほどの急変は少ない。

LOW(低重要度) — フィルター対象外

ブロッキングロジック

IMPACT_CONFIGS = {
    "HIGH": {
        "pre_hours": 2,          # 発表2時間前からブロック
        "post_hours": 2,         # 発表2時間後までブロック
        "score_multiplier": 999.9,  # 事実上エントリー不可
    },
    "MEDIUM": {
        "pre_hours": 1,          # 発表1時間前から
        "post_hours": 1,         # 発表1時間後まで
        "score_multiplier": 1.5, # 閾値50%引き上げ
    },
    "LOW": {
        "pre_hours": 0,
        "post_hours": 0,
        "score_multiplier": 1.0, # フィルターなし
    }
}

score_multiplierはエントリー判定の閾値に掛ける乗数だ。通常の閾値が1.9の場合:

「エントリーを完全に禁止する」のではなく「閾値を引き上げる」設計にしたのがポイントだ。MEDIUM指標では本当に強いシグナルなら取引を許可する柔軟性を持たせている。


3層フォールバック:止まらないシステムの設計

自動売買システムで最も避けるべきは「外部APIの障害でシステムが停止すること」だ。経済指標カレンダーが取得できないからといってエントリー処理を止めるわけにはいかない。

そこで3層のフォールバック構造を設計した。

Layer 1: Finnhub API(プライマリ)
  ↓ 失敗
Layer 2: FRED API(米国主要指標のみ)
  ↓ 失敗
Layer 3: ハードコードカレンダー(静的内蔵)
  ↓ 失敗(通常ありえないが)
フィルター無効化(multiplier=1.0)で通常動作継続

Layer 1: Finnhub API

Finnhubは金融データAPIサービスで、無料枠で経済カレンダーのエンドポイントを提供している。

# Finnhub APIのレスポンス例
{
  "economicCalendar": [
    {
      "country": "US",
      "event": "CPI m/m",
      "impact": "high",
      "time": "2026-03-12 12:30:00",  # UTC
      "estimate": 0.3,
      "unit": "%"
    }
  ]
}

1日1回、フォワードテスト起動時に当日〜3日先分を一括取得してキャッシュする。無料枠は60リクエスト/分で、1日1回の呼び出しなら十分すぎる。

Finnhubを選んだ理由: 公式API・利用規約明確・無料枠十分・JSON形式でパース容易。Forex FactoryやInvesting.comのスクレイピングは利用規約がグレーなので除外した。

Layer 2: FRED API

FRED(Federal Reserve Economic Data)は米連邦準備銀行が提供するデータベースだ。カレンダーAPIは持っていないが、主要指標の発表日(release dates)を取得できる。

米国指標しかカバーしないが、FXで最もインパクトが大きいのは米国指標なので、フォールバックとしては十分機能する。

発表時刻はFREDのデータには含まれないため、13:30 UTCをデフォルト値として補完する設計にした。

Layer 3: ハードコードカレンダー

すべての外部APIが落ちた場合の最終安全網。コード内に「毎月第1金曜日 = NFP」「FOMC年8回の日程」のようなルールを静的に持たせている。

def _generate_monthly_nfp(self, from_date, to_date):
    """毎月第1金曜日(NFP)を算出"""
    events = []
    current = date(from_date.year, from_date.month, 1)
    while current <= to_date:
        # 第1金曜日を計算(weekday: 4=金曜)
        first_friday = current + timedelta(days=(4 - current.weekday()) % 7)
        if from_date <= first_friday <= to_date:
            events.append(EconomicEvent(
                event_name="US Employment Situation (NFP)",
                country="US",
                scheduled_at=datetime(
                    first_friday.year, first_friday.month, first_friday.day,
                    13, 30, 0, tzinfo=timezone.utc,
                ),
                impact=EventImpact.HIGH,
                # ...
            ))
        # 翌月へ
        current = next_month(current)
    return events

「毎月第1金曜」のようなルールは年が変わっても有効なので、APIが完全に死んでも最低限のNFPとFOMCはカバーできる。日銀会合のような不定期日程は年次スケジュール辞書として別途定義し、年1回のコード更新を許容する設計とした。

フェールセーフの設計原則

全層が失敗してもエントリー処理を停止しない。これが最重要の設計原則だ。

def get_blocking_score_multiplier(self, dt, currency_pair=None):
    if not self.enabled:
        return 1.0  # 無効なら通常動作

    try:
        result = self._check_filter(dt, currency_pair or self.currency_pair)
        return result.multiplier
    except Exception:
        # どんな例外が起きても1.0を返して通常動作を継続
        return 1.0

カレンダー取得に失敗した場合、フィルターは「存在しない」のと同じ状態になる。フィルターが効かないリスクよりも、システムが止まるリスクの方が遥かに大きいからだ。


既存システムへの統合:最小侵襲の設計

自動売買システムのコアロジックに大きく手を入れるのは危険だ。既存のシグナル判定フローに対して、変更は2箇所だけで済むように設計した。

変更前のフロー

トリガーJSON検出
  → min_total_score取得(ADXベース動的閾値)
  → UnifiedSignalEvaluator.should_entry(indicators, min_score)
  → エントリー実行

変更後のフロー

トリガーJSON検出
  → min_total_score取得(ADXベース動的閾値)
  → [NEW] eco_multiplier取得(経済指標カレンダー)
  → [NEW] adjusted_score = min_total_score × eco_multiplier
  → UnifiedSignalEvaluator.should_entry(indicators, adjusted_score)
  → エントリー実行

追加したのは「乗数を取得して掛ける」だけ。UnifiedSignalEvaluator自体のコードは一切変更していない。外部から閾値を調整するだけで効果を発揮する設計にすることで、既存の1,476テストに影響を与えないことを保証した。

設定ファイルにはenabled: falseをデフォルトとして追加し、JSONに設定が存在しない場合も既存動作を完全維持する後方互換設計にした。

{
  "economic_calendar": {
    "enabled": false
  }
}

通貨ペアと国の関連づけ

EUR_JPYの取引中に米国CPIが発表される場合、ブロック対象になるべきかどうか。答えはYesだ。EUR_JPYは「ユーロ/日本円」だが、米ドルの動きは間接的にすべてのクロス円に影響する。

通貨ペアと関連国のマッピングを定義して、適切なフィルタリングを実現した。

PAIR_COUNTRY_MAP = {
    "EUR_JPY":  ["US", "JP", "EU"],   # 米国・日本・EU指標でブロック
    "GBP_JPY":  ["US", "JP", "GB"],   # 米国・日本・英国指標でブロック
    "AUD_JPY":  ["US", "JP", "AU"],
    "CAD_JPY":  ["US", "JP", "CA"],
    "EUR_USD":  ["US", "EU"],
}

USはすべてのペアに含まれている。FOMCや雇用統計は全通貨ペアに影響するためだ。


Slack通知:何が起きているかを常に把握する

自動売買で重要なのは「今システムが何をしているか」の可視化だ。フィルターの動作状況をSlackで通知する仕組みを組み込んだ。

イベント通知例
HIGH指標ブロック開始[FX-ADD-D2] US CPI m/m (HIGH) at 2026-03-12 21:30 JST ブロック解除予定: 23:30 JST
ブロック解除[FX-ADD-D2] US CPI m/m ブロック解除(EUR_JPY)
Finnhub失敗[FX-ADD-D2 WARNING] Finnhub API取得失敗 → FRED APIへフォールバック
全API失敗[FX-ADD-D2 ERROR] 全カレンダーソース取得失敗。フィルター無効化・通常動作継続

通知の重複抑制も重要だ。同じ指標のブロック通知を毎回送ると通知疲れを起こす。event_idごとに送信済みフラグを管理し、1イベントにつき1回だけ通知する設計にした。

障害通知は既存のErrorAggregator(5分窓で集約する仕組み)を通して、通知の氾濫を防止している。


バックテストでの効果検証

フィルターの効果は「感覚」ではなく「数値」で検証すべきだ。バックテストエンジンにもフィルターを統合し、ON/OFFの比較ができるようにした。

# フィルターなし(従来通り)
python main_backtest.py --trade-type Swing

# フィルターあり(Finnhub APIで過去データ取得)
python main_backtest.py --trade-type Swing --use-economic-filter

# フィルターあり(ハードコードカレンダーのみ、API不要)
python main_backtest.py --trade-type Swing --use-economic-filter --no-api

バックテストで過去数年分の経済カレンダーを取得する場合、毎回APIを叩くと無駄なので、Parquetファイルにキャッシュする仕組みも入れた。2回目以降のバックテストはキャッシュから読み込むだけで済む。


学んだこと

1. 「事前に分かっていること」は最大限活用する

テクニカル分析は「起きたことへの反応」だが、経済指標カレンダーは「起きる前に分かっていること」だ。この2つを組み合わせることで、遅延のない防御層が追加できる。

2. フォールバックは最低3層欲しい

外部APIへの依存は1つでも危険だ。2つあれば安心と思いがちだが、同時障害は起きる(特にネットワーク障害の場合)。ローカルに静的データを持つ「最終安全網」があることで、完全なオフライン耐性を確保できる。

3. フェールセーフは「何もしない」こと

フィルターが壊れたときの正しい動作は「フィルターが存在しなかったときと同じ動作をする」ことだ。「安全側に倒す=全エントリーを止める」は一見正しそうだが、取引機会の逸失というリスクを見落としている。フィルターが効かないリスクと、システムが止まるリスクを天秤にかけたとき、後者の方が遥かに致命的だ。

4. 「最小侵襲」は設計方針として明文化する

既存システムへの変更箇所が少ないほど、バグの混入リスクは下がる。今回はUnifiedSignalEvaluator(シグナル評価器)のコードを一切変更せず、「外部から閾値に乗数を掛けるだけ」で実現した。既存の1,476テストがすべてそのままPASSすることが、この設計の正しさを証明している。


まとめ

経済指標フィルターの設計で重要なのは以下の3点だ。

  1. 重要度別の段階的制御: HIGH指標は完全ブロック、MEDIUM指標は閾値引き上げで柔軟に対応
  2. 3層フォールバック: Finnhub → FRED → ハードコードカレンダー。どの層が落ちても次の層で継続
  3. フェールセーフ: すべてが失敗しても通常動作を維持。止まらないことが最優先

テクニカル指標だけに頼るシステムから、ファンダメンタルズの「事前情報」を組み合わせるシステムへ。これは単なるフィルター追加ではなく、リスク管理の質的な進化だと考えている。


Edit page
Share this post on:

Previous Post
バックテストを並列化して7日→1日に短縮した話
Next Post
ファンダメンタル複合スコア:財務データ5軸で銘柄の質を判定する