危機監視スコアを「core優先・geo補助」型に調整する
前回までに、FREDやStooqから取得した市場データを使って、金融危機を監視するための crisis_score を作成しました。
危機監視スコアでは、VIX、信用スプレッド、金融環境指数、イールドカーブ、WTI、金、ドル円などを特徴量として使っています。
ただし、単純にすべての特徴量を同じように混ぜると、スコアの意味がぼやけます。
特に、信用不安や金融環境の悪化を表す core_score と、資源価格や為替の急変を表す geo_score を単純平均に近い形で合成すると、本来強く反応してほしい金融危機シグナルが薄まる可能性があります。
そこで今回は、build_crisis_score.py を修正し、危機判定の本体を core_score に置いたまま、geo_score は補助的に加点・減点する形へ変更しました。
今回の目的
今回やりたいことは、危機監視スコアの設計を次の形へ整理することです。
core_scoreを危機判定の本体にするgeo_scoreは地政学・資源ショックの補助指標として扱う- geo要因が弱いからといって、coreの強い危機シグナルを大きく下げない
- 将来的にIMMポジションやバフェット指数を追加しやすい構造にする
今後、IMMポジションレート、アジア通貨危機、9.11、バフェット指数なども追加していく予定です。
その前に、まずは現在のスコアを「core優先・geo補助」型に整えておきます。
なぜcore優先にするのか
危機監視では、すべての指標を同じ重みで扱えばよいわけではありません。
たとえば、金融危機に近いストレスを直接見やすいのは、次のような指標です。
- VIX
- ハイイールド債スプレッド
- BBB社債スプレッド
- NFCI / ANFCI
- イールドカーブ
これらは、信用不安、ボラティリティ、金融環境の悪化を直接反映しやすい指標です。
一方で、WTI、金、ドル円などは重要ではありますが、危機の種類によって反応が変わります。
原油高は資源ショックやインフレ圧力として重要ですが、すべての金融危機で同じ方向に動くとは限りません。金やドル円も同様に、局面によって解釈が変わります。
そのため、今回は次のように役割を分けました。
core_score: 信用・ボラティリティ・金融条件を中心とした危機判定の本体geo_score: 地政学・資源・為替ショックの補助
以前の合成方法
以前のスコアでは、core_score と geo_score を次のように合成していました。
score = core_score * 0.8 + geo_score * 0.2
この方法でも、coreを重めに扱うことはできます。
しかし、geo_score が低い場合、core_score が高くても最終スコアが少し押し下げられます。
つまり、信用スプレッドやVIXがかなり危険な状態を示していても、資源価格や為替の反応が弱いと、危機スコア全体が薄まる可能性があります。
危機監視レーダーとして使う場合、これは少し扱いづらいです。
新しい合成方法
今回の修正では、最終スコアを次のようにしました。
score = core_score + 0.15 * (geo_score - 50)
ポイントは、geo_score = 50 を中立点として扱うことです。
geo_scoreが50より高い場合、core_scoreを少し押し上げるgeo_scoreが50より低い場合、core_scoreを少し押し下げる- ただし、補正幅は
0.15倍なので、core_scoreを大きく壊さない
これにより、危機判定の主役はあくまで core_score のままになります。
geo_score は、危機の種類や背景を補足する役割に近づきます。
スコアの全体設計
今回の build_crisis_score.py では、まず2層構成にしています。
core_score
core_score では、信用・ボラティリティ・金融条件を中心に見ます。
vix_z20hy_oas_z20bbb_oas_z20hy_minus_bbb_z20nfci_z20anfci_z20yc_10y_2y_z20yc_10y_3m_z20
イールドカーブ系は、低下・逆イールド方向を危険側として扱いたいため、方向を反転させています。
geo_score
geo_score では、資源価格や為替の変化を見ます。
wti_z20gold_z20usdjpy_z20wti_chg_5dgold_chg_5dusdjpy_chg_5d
ドル円については、円高方向をリスクオフ寄りとみなすため、危険方向を反転させています。
修正後の主なパラメータ
今回の修正では、以下のパラメータを使いました。
SCORE_START_DATE = "1997-01-01" GEO_ADJUST_WEIGHT = 0.15 GEO_NEUTRAL_LEVEL = 50.0 RISK_OFF_THRESHOLD = 80.0 WARNING_THRESHOLD = 60.0 CAUTION_THRESHOLD = 40.0
GEO_ADJUST_WEIGHT は、geo_scoreをどれくらい最終スコアに反映するかを決める値です。
今回は 0.15 にしています。
この値を大きくすると、資源・為替要因の影響が強くなります。逆に小さくすると、ほぼcore_score中心のスコアになります。
build_crisis_score.py の修正ポイント
主な変更点は、compute_score() の中で最終スコアを作る部分です。
以前は、coreとgeoの加重平均でした。
out["score"] = np.where(
both_valid,
out["core_score"] * FINAL_CORE_WEIGHT + out["geo_score"] * FINAL_GEO_WEIGHT,
np.nan
)
これを、core優先・geo補助型に変更しました。
core_valid = out["core_score"].notna()
geo_valid = out["geo_score"].notna()
geo_adjust = np.where(
geo_valid,
GEO_ADJUST_WEIGHT * (out["geo_score"] - GEO_NEUTRAL_LEVEL),
0.0
)
out["score"] = np.where(core_valid, out["core_score"] + geo_adjust, np.nan)
out["score"] = out["score"].clip(lower=0.0, upper=100.0)
この形にすると、geo_score が欠損している場合でも、core_score が有効ならスコアを計算できます。
また、最終スコアが0未満や100超えにならないように、clip() で0〜100に収めています。
regime判定
スコアからレジームを判定する部分は、次のようにしています。
out["regime"] = np.select(
[
out["score"] >= RISK_OFF_THRESHOLD,
out["score"] >= WARNING_THRESHOLD,
out["score"] >= CAUTION_THRESHOLD,
],
[
"risk_off",
"warning",
"caution",
],
default="normal"
)
out.loc[out["score"].isna(), "regime"] = None
現在の判定基準は次の通りです。
80以上:risk_off60以上:warning40以上:caution- それ未満:
normal
実行結果
修正後に build_crisis_score.py を実行しました。
python build_crisis_score.py
実行結果の概要は次の通りです。
Loading crisis features from research.sqlite ... Loaded columns: ['anfci_z20', 'bbb_oas_z20', 'gold_chg_5d', 'gold_z20', 'hy_minus_bbb_z20', 'hy_oas_z20', 'nfci_z20', 'usdjpy_chg_5d', 'usdjpy_z20', 'vix_z20', 'wti_chg_5d', 'wti_z20', 'yc_10y_2y_z20', 'yc_10y_3m_z20'] Raw shape: (13221, 14) Raw date range: 1990-01-01 -> 2026-03-13 Computing core-first crisis score ... Score start date: 1997-01-01 Score shape: (10664, 24) Score date range: 1997-01-01 -> 2026-03-13 Upserted 10664 rows into crisis_score
crisis_features には1990年以降の特徴量がありますが、今回のスコアは SCORE_START_DATE = "1997-01-01" 以降を対象にしています。
そのため、最終的に crisis_score へ保存された行数は 10664 行になりました。
直近データの確認
SQLiteで crisis_score を確認しました。
sqlite3 research.sqlite
SELECT date, core_score, geo_score, score, regime FROM crisis_score ORDER BY date DESC LIMIT 30;
直近の結果は次のようになりました。
2026-03-13|66.1601642050738|48.3543330420987|65.9133141613886|warning 2026-03-12|68.0632443681154|50.7394789516565|68.1741662108639|warning 2026-03-11|59.5489775950895|69.445861603939|62.4658568356803|warning 2026-03-10|58.9525456470283|70.64111440104|62.0487128071843|warning 2026-03-09|70.417150420088|54.9988765090497|71.1669818964455|warning 2026-03-08|67.7454844082668|70.6429197865314|70.8419223762465|warning 2026-03-07|70.3967555220573|58.5546764658917|71.6799569919411|warning 2026-03-06|73.181937747609|57.7252183303472|74.3407204971611|warning 2026-03-05|62.003113251451|55.481390158248|62.8253217751882|warning 2026-03-04|59.4388207509542|57.0649035861564|60.4985562888777|warning
この結果を見ると、core_score が60〜70台で推移している日に、geo_score が中立より高ければ少し押し上げ、低ければ少し押し下げる形になっています。
ただし、geo要因によってcoreの判定が大きく崩れるほどではありません。
今回の狙い通り、core_score を主判定にしたまま、geo_score を補助的に反映できています。
過去イベントで比較する
次に、check_crisis_events.py を使って、旧版スコアと新版スコアを比較しました。
python check_crisis_events.py
比較対象にしたイベントは次の通りです。
- LTCM危機
- ITバブル崩壊
- 9.11後市場混乱
- パリバ・ショック
- リーマン破綻
- ギリシャ・欧州債務危機
- 米国格下げショック
- チャイナショック
- 人民元ショック余波
- VIXショック
- 2018年末リスクオフ
- コロナショック初動
- コロナショック本格化
- 英国トラス・ショック
- SVB破綻
比較では、イベント当日のスコアだけではなく、イベント前後の期間で次の項目も確認しています。
- 旧版スコア
- 新版スコア
- 旧版レジーム
- 新版レジーム
- 最初にwarningになった日
- 最初にrisk_offになった日
- イベント前後ウィンドウ内の最大スコア
比較結果の要点
比較結果の一部は次の通りです。
event_name event_date old_score old_regime new_score new_regime LTCM危機 1998-09-23 51.152148 caution 50.044526 caution ITバブル崩壊 2000-04-14 65.088530 warning 66.475569 warning 9.11後市場混乱 2001-09-17 71.652580 warning 75.909856 warning パリバ・ショック 2007-08-09 53.252177 caution 49.508318 caution リーマン破綻 2008-09-15 75.454872 warning 75.258431 warning ギリシャ・欧州債務危機 2010-05-06 90.891432 risk_off 91.347790 risk_off 米国格下げショック 2011-08-05 88.728786 risk_off 87.404328 risk_off チャイナショック 2015-08-24 85.991372 risk_off 87.256479 risk_off コロナショック初動 2020-02-24 91.542825 risk_off 92.618850 risk_off SVB破綻 2023-03-10 86.176509 risk_off 87.382709 risk_off
大きな危機イベントでは、旧版と新版でレジーム判定が大きく崩れていないことが確認できました。
特に、ギリシャ・欧州債務危機、米国格下げショック、チャイナショック、コロナショック初動、SVB破綻では、引き続き risk_off として判定されています。
一方で、イベントによっては新版スコアが少し下がるケースもあります。
これは、geo要因を補助に回したことで、資源・為替側の反応が強すぎる局面ではスコアが少し抑えられるためです。
ただし、今回の目的は「geo要因でcoreの判定を壊さないこと」なので、この挙動は許容範囲だと考えています。
今回の変更で良くなった点
今回の修正で、スコアの意味がかなり整理されました。
- 危機判定の本体が
core_scoreだと明確になった geo_scoreは危機の背景を補助する役割になった- geo要因が弱いだけで、coreの強い危機シグナルが大きく下がらなくなった
- IMMポジションやバフェット指数を追加する前の土台が整理できた
特に重要なのは、今後の拡張方針が見えやすくなったことです。
このまま特徴量を増やしていくと、スコアがどんどん複雑になります。
そのため、最初から役割ごとに層を分けておく方が、あとで検証しやすくなります。
今後の拡張方針
今後は、次のような多層構造にしていきたいと考えています。
core_score: 信用・ボラティリティ・金融条件geo_score: WTI、金、ドル円などの地政学・資源ショックposition_score: IMMポジションなどの投機筋ポジションstructural_score: バフェット指数、credit-to-GDP gap、DSRなどの構造リスク
IMMポジションは、geo_scoreではなく position_score として別層にするのが自然です。
IMMは、VIXやOASのような即時ストレスではありませんが、市場参加者のポジションの偏りを見るうえで重要です。
たとえば、円ロングの急増、CHFロングの急増、AUDやNZDのロング縮小などは、リスクオフの兆候として使える可能性があります。
一方で、バフェット指数は短期危機スコアに直接混ぜるより、structural_score として扱う方がよさそうです。
バフェット指数は、短期ショックというより、株式市場の長期的な割高・割安や構造的な過熱感を見る指標だからです。
次にやること
今回の修正で、core_score と geo_score の役割分担はかなり整理できました。
次は、IMMポジションレートを追加して、position_score を作る予定です。
進め方としては、次の順番がよさそうです。
build_crisis_features.pyにIMM関連特徴量を追加するbuild_crisis_score.pyにposition_score層を追加するcheck_crisis_events.pyで旧版・新版・IMM追加版を比較する
いきなりすべてを最終形にするのではなく、まずは差分ベースで追加し、過去イベントで検証しながら調整していく方針です。
まとめ
今回は、危機監視スコアを「core優先・geo補助」型に修正しました。
以前のように core_score と geo_score を加重平均するのではなく、core_score を主判定にして、geo_score は中立点からのズレだけを補助的に反映する形にしました。
変更後の式は次の通りです。
score = core_score + 0.15 * (geo_score - 50)
この変更により、geo要因が低いからといって、coreの強い危機シグナルが大きく薄まることを避けられます。
過去イベントで比較した結果、リーマン、欧州債務危機、米国格下げ、チャイナショック、コロナ、SVBなどの主要イベントでも、危機検知力を大きく落とさずに運用できそうです。
次回は、この構造に position_score を追加し、IMMポジションレートを危機監視レーダーに組み込んでいきます。

コメント