IMM円ポジションを2006年まで反映し、危機監視特徴量の欠落を修正する

IMM円ポジションを2006年まで反映し、危機監視特徴量の欠落を修正する

前回までに、FREDやStooqから取得した金利・VIX・信用スプレッド・金価格・WTIなどを使い、危機監視用の特徴量を research.sqlite に保存する流れを作成しました。

今回は、その特徴量に追加している IMM の円ポジションデータについて確認しました。

もともとの想定では、JPY の IMM データを使って position_score を作り、危機時に安全通貨である円へ資金が向かう動きを補助的にスコアへ反映したいと考えていました。

しかし、実際に build_crisis_features.py を実行してみると、imm_jpy_rate が 2015年以降しか入っていないことに気づきました。

このままだと、コロナショック、トラスショック、SVBショックのような比較的新しいイベントには使えますが、リーマンショック初期やそれ以前の危機比較には使えません。

今回の目的

今回の目的は、IMM JPY のデータが本当に 2015年以降しか存在しないのか、それとも読み込み処理の問題で 2006年以降のデータを拾えていないだけなのかを確認することです。

最終的には、crisis_features に保存される imm_jpy_* 系列を、可能な範囲で過去まで伸ばすことを目指します。

最初に気づいた問題

build_crisis_features.py を実行すると、IMM JPY の読み込み結果が次のようになっていました。

Loading IMM JPY from cot_legacy.db ...
IMM JPY weekly shape: (585, 1)
IMM JPY date range: 2015-01-06 -> 2026-03-17

この結果を見ると、IMM JPY の週次データは 2015年1月6日 からしか読み込まれていません。

そのため、research.sqlite 側でも imm_jpy_rate の開始日は 2015年になっていました。

sqlite3 research.sqlite
SELECT
  feature_name,
  MIN(date) AS first_non_null_date,
  MAX(date) AS last_non_null_date,
  COUNT(*) AS non_null_rows
FROM crisis_features
WHERE feature_name LIKE 'imm_jpy%'
  AND value IS NOT NULL
GROUP BY feature_name
ORDER BY feature_name;

実行結果は次の通りです。

imm_jpy_rate|2015-01-06|2026-03-13|4085
imm_jpy_rate_chg_4w|2015-01-26|2026-03-13|4065
imm_jpy_rate_ma20_gap|2015-01-25|2026-03-13|4066
imm_jpy_rate_z20|2015-01-25|2026-03-13|4066

この時点では、危機比較用の特徴量としては少し弱い状態です。

特に、以下のようなイベントには IMM の寄与を反映できません。

  • 1998年 LTCM危機
  • 2000年 ITバブル崩壊
  • 2001年 9.11
  • 2007〜2008年 リーマンショック初期

cot_legacy.db 側のデータを直接確認する

まず、そもそも cot_legacy.db に 2015年以前の JPY データが残っているのかを確認しました。

確認に使ったSQLは次です。

sqlite3 cot_legacy.db
SELECT
  MIN("As of Date in Form YYYY-MM-DD"),
  MAX("As of Date in Form YYYY-MM-DD")
FROM cot_legacy_fut_only
WHERE TRIM("Market and Exchange Names") = 'JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE';

結果は次の通りでした。

2006-01-03|2026-03-17

つまり、cot_legacy.db 側には JPY のIMMデータが 2006年1月3日 から入っていました。

この時点で、問題はデータベース側ではなく、build_crisis_features.py の読み込み条件にありそうだと分かります。

原因は Market and Exchange Names の空白だった

確認したところ、cot_legacy_fut_only から JPY の IMM データを読み込むSQLが、次のようになっていました。

WHERE "Market and Exchange Names" = ?

しかし、直接確認するときには TRIM() を使っていました。

WHERE TRIM("Market and Exchange Names") = ?

つまり、Market and Exchange Names に余分な空白が入っている行があり、完全一致では古いデータを拾えていなかった可能性があります。

これにより、DBには 2006年からのデータが存在しているにもかかわらず、特徴量生成時には 2015年以降しか読み込まれていなかったと考えられます。

load_imm_jpy_weekly() を修正する

そこで、load_imm_jpy_weekly() のSQLを修正しました。

修正前は次のような条件でした。

WHERE "Market and Exchange Names" = ?
  AND "As of Date in Form YYYY-MM-DD" IS NOT NULL

これを、次のように TRIM() 付きへ変更します。

WHERE TRIM("Market and Exchange Names") = ?
  AND "As of Date in Form YYYY-MM-DD" IS NOT NULL

修正後の関数は次のようになります。

def load_imm_jpy_weekly(cot_db_path: str) -> pd.DataFrame:
    sql = """
    SELECT
        "As of Date in Form YYYY-MM-DD" AS date,
        "Open Interest (All)" AS open_interest,
        "Noncommercial Positions-Long (All)" AS noncomm_long,
        "Noncommercial Positions-Short (All)" AS noncomm_short
    FROM cot_legacy_fut_only
    WHERE TRIM("Market and Exchange Names") = ?
      AND "As of Date in Form YYYY-MM-DD" IS NOT NULL
    ORDER BY "As of Date in Form YYYY-MM-DD"
    """

    with sqlite3.connect(cot_db_path) as conn:
        df = pd.read_sql_query(sql, conn, params=[IMM_JPY_MARKET_NAME])

    if df.empty:
        raise RuntimeError("cot_legacy.db から JPY の IMM データを取得できませんでした")

    df["date"] = pd.to_datetime(df["date"])
    for col in ["open_interest", "noncomm_long", "noncomm_short"]:
        df[col] = pd.to_numeric(df[col], errors="coerce")

    df["imm_jpy_net"] = df["noncomm_long"] - df["noncomm_short"]
    df["imm_jpy_rate"] = df["imm_jpy_net"] / df["open_interest"]

    print(f"IMM JPY loaded rows: {len(df)}, min={df['date'].min()}, max={df['date'].max()}")

    out = (
        df[["date", "imm_jpy_rate"]]
        .dropna(subset=["date", "imm_jpy_rate"])
        .drop_duplicates(subset=["date"])
        .set_index("date")
        .sort_index()
    )
    return out

今回は原因確認のために、読み込み件数と日付範囲を表示する print() も追加しました。

修正後に再実行する

修正後、再度 build_crisis_features.py を実行しました。

python build_crisis_features.py

すると、IMM JPY の読み込み結果が次のように変わりました。

Loading IMM JPY from cot_legacy.db ...
IMM JPY loaded rows: 1055, min=2006-01-03 00:00:00, max=2026-03-17 00:00:00
IMM JPY weekly shape: (1055, 1)
IMM JPY date range: 2006-01-03 -> 2026-03-17

これで、2015年開始だった問題は解消され、2006年からの IMM JPY データを読み込めるようになりました。

research.sqlite 側の保存結果を確認する

次に、research.sqlite に保存された crisis_features を確認します。

sqlite3 research.sqlite
SELECT
  feature_name,
  MIN(date) AS first_non_null_date,
  MAX(date) AS last_non_null_date,
  COUNT(*) AS non_null_rows
FROM crisis_features
WHERE feature_name LIKE 'imm_jpy%'
  AND value IS NOT NULL
GROUP BY feature_name
ORDER BY feature_name;

修正後の結果は次のようになりました。

imm_jpy_rate|2006-01-03|2026-03-13|7375
imm_jpy_rate_chg_4w|2006-01-23|2026-03-13|7355
imm_jpy_rate_ma20_gap|2006-01-22|2026-03-13|7356
imm_jpy_rate_z20|2006-01-22|2026-03-13|7356

これで、imm_jpy_rate は 2006年1月3日 から保存されるようになりました。

imm_jpy_rate_chg_4wimm_jpy_rate_z20 の開始日が少し後ろになるのは正常です。変化量や20期間z-scoreを計算するため、初期期間では値が作れないためです。

今回作成できた特徴量

今回、危機監視用の特徴量として使えるようになった IMM JPY 系列は次の通りです。

  • imm_jpy_rate:非商業部門の円ネットポジションを建玉で割った比率
  • imm_jpy_rate_chg_4w:IMM円ポジション比率の4週変化
  • imm_jpy_rate_z20:IMM円ポジション比率の20期間z-score
  • imm_jpy_rate_ma20_gap:20期間移動平均からの乖離

これにより、円ポジションの偏りや変化を、危機スコアの補助要素として扱えるようになります。

使えるようになった危機比較

今回の修正により、少なくとも 2006年以降の危機比較では IMM JPY を利用できるようになりました。

具体的には、次のようなイベントに対して position_score を反映できます。

  • 2007〜2008年 リーマンショック
  • 欧州債務危機
  • 米国債格下げ
  • チャイナショック
  • VIXショック
  • コロナショック
  • トラスショック
  • SVBショック

一方で、次のようなイベントにはまだ IMM JPY を使えません。

  • 1998年 LTCM危機
  • 2000年 ITバブル崩壊
  • 2001年 9.11

これは今回のスクリプトの問題というより、現在手元にある IMM JPY データの開始時期による制約です。

今回のハマりどころ

今回のポイントは、DBにデータが存在することと、スクリプトがそのデータを正しく読めることは別問題だという点です。

最初は research.sqlite 側だけを見ていたため、IMM JPY のデータ自体が 2015年以降しかないように見えました。

しかし、元DBである cot_legacy.db を直接確認すると、2006年からのデータは残っていました。

原因は、Market and Exchange Names の文字列に含まれる空白を考慮していなかったことです。

このようなデータ処理では、特に外部データ由来の文字列カラムに対して、完全一致だけで判定すると古いデータや一部データを取りこぼすことがあります。

今回の到達点

今回の作業で、以下が完了しました。

  • cot_legacy.db に JPY の IMM データが 2006年から存在することを確認
  • build_crisis_features.py の読み込み条件を修正
  • TRIM("Market and Exchange Names") を使うことで古いデータも取得可能にした
  • imm_jpy_rate を 2006年開始で crisis_features に保存できるようにした
  • imm_jpy_rate_chg_4wimm_jpy_rate_z20imm_jpy_rate_ma20_gap も生成できるようにした

次にやること

次は、build_crisis_score.pyposition_score を追加します。

まずは JPY の IMM ポジションだけを使い、シンプルに補助スコアとして組み込む予定です。

初期案としては、次のような構成にします。

score = (
    core_score
    + GEO_ADJUST_WEIGHT * (geo_score - GEO_NEUTRAL_LEVEL)
    + POSITION_ADJUST_WEIGHT * (position_score - POSITION_NEUTRAL_LEVEL)
)

初期値は次のように考えています。

POSITION_ADJUST_WEIGHT = 0.10
POSITION_NEUTRAL_LEVEL = 50.0

JPY は安全通貨として扱われることが多いため、円ロングが急増する方向をリスク側として扱う設計にします。

まとめ

今回は、危機監視特徴量に追加した IMM JPY 系列が 2015年以降しか入っていない問題を調査しました。

確認の結果、cot_legacy.db には 2006年からの JPY データが存在しており、問題はデータ取得ではなく、特徴量生成時のSQL条件にありました。

Market and Exchange Names の比較に TRIM() を入れることで、2006年以降の IMM JPY データを正しく読み込めるようになりました。

これで、リーマンショック以降の危機比較に対して、IMM円ポジションを使った position_score を組み込む準備ができました。

次回は、build_crisis_score.pyposition_score を追加し、既存の core_scoregeo_score と組み合わせて、危機レジーム判定へ反映していきます。

コメント

タイトルとURLをコピーしました