CFTC/COT(IMMポジション)を取得してSQLiteに保存し、JPY先物データを分析用に整える

CFTCのCOTレポートからIMMポジションを取得し、SQLiteに保存してJPY先物データを整える

リーマンショック時には、円キャリー取引の巻き戻しによって大きな円高が発生しました。

そのような中長期のマクロ環境や市場ポジションの偏りを分析するために、CFTCのCOTレポート、いわゆるIMMポジションを取得してSQLiteに保存してみます。

今回は、2006年から2025年までのCOTデータを取得し、SQLiteに保存したうえで、日本円先物の投機筋ネットポジションを分析しやすい形に整えます。

IMMポジションとは

IMMポジションとは、米CFTCが毎週公表しているCOTレポートの建玉データを使い、市場参加者のポジションの偏りを見る指標です。

特にFXやコモディティ分析では、投機筋のポジションである Non-Commercial が重要です。

区分 意味 自動売買での重要度
Non-Commercial 投機筋、ファンドなど 高い
Commercial 実需、ヘッジ目的 中程度
Non-reportable 小口投資家 低め

実質的には、投機筋の偏りを見るためのデータとして使います。

取得元はCFTC公式サイト

COTレポートの公式データは、CFTCのHistorical Compressedデータから取得できます。

年ごとにZIPファイルが用意されており、以下のようなURLから取得できます。

https://www.cftc.gov/files/dea/history/fut_disagg_txt_2025.zip

今回は、Legacy Futures Onlyの年別ZIPを使い、以下のような形式で取得します。

https://www.cftc.gov/files/dea/history/deacot2025.zip

どの年まで遡れるか

CFTCのCOTデータは、形式によって取得できる最古年が異なります。

データ形式 利用可能な最古データ
Legacy Futures-Only 1986年頃から
Disaggregated Futures Only / TFF 2006年6月頃から

今回はまず、2006年以降を対象にします。

実運用では、Legacy Futures-Onlyを使えばさらに古い期間まで取得できますが、列名やデータ仕様の揺れがあるため、まずは2006年以降で検証します。

対象にしたい主要通貨

IMMポジションとしてよく見る主要通貨は以下です。

通貨 CFTCコード例
EUR 099741
JPY 097741
GBP 096742
AUD 232741
CAD 090741
CHF 092741
NZD 112741
USD Index 098662

USDJPYを分析する場合は、JPY先物の投機筋ネットポジションと、USDJPYレートを組み合わせるのが基本になりそうです。

作業ディレクトリを作成する

まず、作業用ディレクトリを作成します。

mkdir fx_tools
cd fx_tools

取得スクリプトを作成します。

vim get_imm_rate.py

最初に作成した取得スクリプト

まず、2006年から2025年までの年別ZIPを取得し、ZIP内のテキストファイルを読み込んでSQLiteへ保存するスクリプトを作成しました。

import io
import zipfile
import requests
import pandas as pd
import sqlite3
from pathlib import Path

BASE = "https://www.cftc.gov/files/dea/history"
DB_PATH = "cot_legacy.db"
TABLE = "cot_legacy_fut_only"

START_YEAR = 2006
END_YEAR = 2025

def download_zip(year: int) -> bytes:
    url = f"{BASE}/deacot{year}.zip"
    r = requests.get(url, timeout=60)
    r.raise_for_status()
    return r.content

def read_first_txt_from_zip(zip_bytes: bytes) -> pd.DataFrame:
    z = zipfile.ZipFile(io.BytesIO(zip_bytes))

    candidates = [
        n for n in z.namelist()
        if n.lower().endswith((".txt", ".csv"))
    ]

    if not candidates:
        raise RuntimeError(f"no txt/csv found in zip: {z.namelist()[:10]}")

    name = candidates[0]

    with z.open(name) as f:
        df = pd.read_csv(f)

    return df

def ensure_table(conn: sqlite3.Connection, df: pd.DataFrame):
    df.head(0).to_sql(TABLE, conn, if_exists="append", index=False)

def get_existing_keys(conn: sqlite3.Connection, key_cols: list[str]) -> set[tuple]:
    cur = conn.cursor()
    cols = ", ".join([f"\"{c}\"" for c in key_cols])
    cur.execute(f"SELECT DISTINCT {cols} FROM {TABLE}")
    return set(cur.fetchall())

def main():
    Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True)

    with sqlite3.connect(DB_PATH) as conn:
        existing = None

        for y in range(START_YEAR, END_YEAR + 1):
            print(f"== {y} ==")

            zip_bytes = download_zip(y)
            df = read_first_txt_from_zip(zip_bytes)

            ensure_table(conn, df)

            possible_date_cols = [
                "Report_Date_as_YYYY-MM-DD",
                "As_of_Date_In_Form_YYMMDD",
            ]

            date_col = next((c for c in possible_date_cols if c in df.columns), None)

            if date_col is None:
                raise ValueError(f"date col not found. columns={list(df.columns)[:30]}")

            possible_mkt_cols = [
                "Market_and_Exchange_Names",
                "Market and Exchange Names",
            ]

            mkt_col = next((c for c in possible_mkt_cols if c in df.columns), None)

            if mkt_col is None:
                mkt_col = date_col

            key_cols = [mkt_col, date_col]

            if existing is None:
                try:
                    existing = get_existing_keys(conn, key_cols)
                except sqlite3.OperationalError:
                    existing = set()

            keys = list(zip(df[mkt_col].astype(str), df[date_col].astype(str)))
            mask = [k not in existing for k in keys]
            new_df = df[mask].copy()

            if new_df.empty:
                print("  no new rows")
                continue

            new_df.to_sql(TABLE, conn, if_exists="append", index=False)

            for k in set(zip(new_df[mkt_col].astype(str), new_df[date_col].astype(str))):
                existing.add(k)

            print(f"  inserted: {len(new_df)} rows")

    print("done.")

if __name__ == "__main__":
    main()

最初のエラー:日付列が見つからない

実行すると、2006年のデータでエラーになりました。

python get_imm_rate.py
== 2006 ==
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/fx_tools/get_imm_rate.py", line 101, in <module>
    main()
  File "/Users/snowpool/aw10s/fx_tools/get_imm_rate.py", line 64, in main
    raise ValueError(f"date col not found. columns={list(df.columns)[:30]}")
ValueError: date col not found. columns=['Market and Exchange Names', 'As of Date in Form YYMMDD', 'As of Date in Form YYYY-MM-DD', ...]

原因は、2006年の列名が想定と違っていたことです。

スクリプト側では以下のような列名を探していました。

Report_Date_as_YYYY-MM-DD
As_of_Date_In_Form_YYMMDD

しかし、実際のCSVには以下の列名が入っていました。

As of Date in Form YYMMDD
As of Date in Form YYYY-MM-DD

つまり、スペースとアンダースコアの違いで列名を見つけられていませんでした。

列名ゆらぎ対策を追加する

年によって列名の表記が微妙に違うため、列名のゆらぎを吸収するヘルパー関数を追加します。

ポイントは、空白・記号・大小文字の差を無視して照合することです。

import re

def norm_col(s: str) -> str:
    s = s.strip().lower()
    s = re.sub(r"[^a-z0-9]+", "_", s)
    s = re.sub(r"_+", "_", s).strip("_")
    return s

def find_col(df: pd.DataFrame, candidates: list[str]) -> str | None:
    norm_map = {norm_col(c): c for c in df.columns}

    for cand in candidates:
        key = norm_col(cand)
        if key in norm_map:
            return norm_map[key]

    return None

ただし、ヘルパーを追加しただけではエラーが解消しませんでした。

日付列を探す処理が、まだ find_col() を使う形に置き換わっていなかったためです。

日付列と市場名列の探索処理を修正する

古い処理を以下のように置き換えます。

possible_date_cols = [
    "Report_Date_as_YYYY-MM-DD",
    "As_of_Date_In_Form_YYMMDD",
    "As of Date in Form YYMMDD",
    "As of Date in Form YYYY-MM-DD",
    "As_of_Date_In_Form_YYYY-MM-DD",
]

date_col = find_col(df, possible_date_cols)

if date_col is None:
    raise ValueError(f"date col not found. columns={list(df.columns)[:50]}")

possible_mkt_cols = [
    "Market_and_Exchange_Names",
    "Market and Exchange Names",
]

mkt_col = find_col(df, possible_mkt_cols)

if mkt_col is None:
    mkt_col = date_col

key_cols = [mkt_col, date_col]

2015年で新しいエラー:列が増えていた

修正後、2006年から2014年までは取り込めました。

== 2006 ==
  inserted: 4926 rows
== 2007 ==
  inserted: 5394 rows
== 2008 ==
  inserted: 5631 rows
== 2009 ==
  inserted: 6570 rows
== 2010 ==
  inserted: 7308 rows
== 2011 ==
  inserted: 7365 rows
== 2012 ==
  inserted: 7838 rows
== 2013 ==
  inserted: 10159 rows
== 2014 ==
  inserted: 12131 rows

しかし、2015年で以下のエラーが出ました。

sqlite3.OperationalError: table cot_legacy_fut_only has no column named CFTC Commodity Code (Quotes)

これは、2015年のCSVに、それ以前のテーブルには存在しない新しい列が追加されていたためです。

pandas.to_sql(..., if_exists="append") は、既存テーブルにない列がDataFrame側にあるとエラーになります。

足りない列を自動追加する

年によって列が増えることがあるため、DataFrameにある列がSQLiteテーブルに存在しなければ、自動で列を追加する関数を作成します。

def ensure_columns(conn: sqlite3.Connection, table: str, df: pd.DataFrame):
    """
    dfにある列がテーブルに無ければSQLiteへADD COLUMNする。
    型は厳密に合わせずTEXTで追加する。
    """
    cur = conn.cursor()
    cur.execute(f'PRAGMA table_info("{table}")')
    existing_cols = {row[1] for row in cur.fetchall()}

    missing = [c for c in df.columns if c not in existing_cols]

    for c in missing:
        cur.execute(f'ALTER TABLE "{table}" ADD COLUMN "{c}" TEXT')

    if missing:
        conn.commit()

ensure_table(conn, df) の直後に、以下を追加します。

ensure_columns(conn, TABLE, df)

さらに、new_df.to_sql() の直前にも念のため追加します。

ensure_columns(conn, TABLE, new_df)
new_df.to_sql(TABLE, conn, if_exists="append", index=False)

これで、年によって新しい列が増えても、自動でSQLite側に列を追加してから追記できるようになります。

2015年以降を再実行する

途中まで入っているため、いったん START_YEAR を2015に変更して再実行しました。

START_YEAR = 2015

実行結果は以下です。

== 2015 ==
  inserted: 11610 rows
== 2016 ==
  inserted: 12434 rows
== 2017 ==
/Users/snowpool/aw10s/fx_tools/get_imm_rate.py:66: DtypeWarning: Columns (3,37,38,39,40,41,42,43,44,45,46,126) have mixed types. Specify dtype option on import or set low_memory=False.
  df = pd.read_csv(f)
  inserted: 12314 rows
== 2018 ==
  inserted: 13410 rows
== 2019 ==
  inserted: 13468 rows
== 2020 ==
  inserted: 13131 rows
== 2021 ==
  inserted: 13307 rows
== 2022 ==
  inserted: 14245 rows
== 2023 ==
  inserted: 15343 rows
== 2024 ==
  inserted: 16764 rows
== 2025 ==
  inserted: 17316 rows
done.

これで2006年から2025年までのデータがSQLiteに入りました。

DtypeWarningについて

途中で DtypeWarning が出ています。

DtypeWarning: Columns (...) have mixed types. Specify dtype option on import or set low_memory=False.

これは、pandasが列の型推定に迷っている警告です。

今回の目的はまずrawデータをSQLiteへ保存することなので、ひとまず放置で問題ないと判断しました。

ただし、将来の数値計算や比較では、文字列が混ざると面倒になる可能性があります。

最終的には、生データ用のrawテーブルと、分析用に型を整えたcleanテーブルを分けるのがよさそうです。

SQLiteでDBを確認する

SQLiteでデータベースを確認します。

sqlite3 cot_legacy.db

テーブル一覧を確認します。

.tables

結果は以下です。

cot_legacy_fut_only

件数を確認します。

SELECT COUNT(*) FROM cot_legacy_fut_only;

結果は以下です。

374006

2006年から2025年までのデータが入っています。

日付レンジを確認する

取得したデータの日付範囲を確認します。

SELECT
  MIN("As of Date in Form YYYY-MM-DD"),
  MAX("As of Date in Form YYYY-MM-DD")
FROM cot_legacy_fut_only;

結果は以下です。

2006-01-03|2025-12-30

2006年初から2025年末まで入っています。

週次データとして入っているか確認する

COTは通常、火曜日基準の週次データです。

同じ日付に複数の市場データがあるのは正常です。

SELECT
  "As of Date in Form YYYY-MM-DD",
  COUNT(*)
FROM cot_legacy_fut_only
GROUP BY 1
ORDER BY 1 DESC
LIMIT 10;

結果は以下です。

2025-12-30|698
2025-12-23|704
2025-12-16|706
2025-12-09|706
2025-12-02|704
2025-11-25|710
2025-11-18|708
2025-11-10|692
2025-11-04|700
2025-10-28|698

JPY先物の市場名を確認する

USDJPY分析で使うため、日本円先物の市場名を確認します。

SELECT DISTINCT "Market and Exchange Names"
FROM cot_legacy_fut_only
WHERE "Market and Exchange Names" LIKE '%YEN%'
LIMIT 20;

結果は以下です。

JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE
NIKKEI STOCK AVERAGE YEN DENOM - CHICAGO MERCANTILE EXCHANGE
EURO FX/JAPANESE YEN XRATE - NEW YORK BOARD OF TRADE
3-MO. EUROYEN TIBOR - CHICAGO MERCANTILE EXCHANGE
EURO FX/JAPANESE YEN XRATE - CHICAGO MERCANTILE EXCHANGE

今回使う対象は、以下です。

JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE

JPY先物の欠損を確認する

JPY先物の主要列に欠損がないか確認します。

SELECT
  COUNT(*) AS rows_total,
  SUM(CASE WHEN "Noncommercial Positions-Long (All)" IS NULL THEN 1 ELSE 0 END) AS null_nc_long,
  SUM(CASE WHEN "Noncommercial Positions-Short (All)" IS NULL THEN 1 ELSE 0 END) AS null_nc_short,
  SUM(CASE WHEN "Open Interest (All)" IS NULL THEN 1 ELSE 0 END) AS null_oi
FROM cot_legacy_fut_only
WHERE TRIM("Market and Exchange Names") = 'JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE';

結果は以下です。

1618|0|0|0

欠損はありませんでした。

JPY先物の週数を確認する

次に、日付の種類数を確認します。

SELECT
  COUNT(DISTINCT "As of Date in Form YYYY-MM-DD") AS weeks
FROM cot_legacy_fut_only
WHERE TRIM("Market and Exchange Names") = 'JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE';

結果は以下です。

1044

2006年から2025年までで約20年分なので、週次データとして妥当です。

同一市場×同一日付の重複を確認する

通常は、1市場×1日付で1行になることを期待します。

しかし確認すると、同じ日付に2行ずつ存在していました。

SELECT
  "As of Date in Form YYYY-MM-DD" AS dt,
  COUNT(*) AS n
FROM cot_legacy_fut_only
WHERE TRIM("Market and Exchange Names") = 'JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE'
GROUP BY dt
HAVING n > 1
ORDER BY dt DESC
LIMIT 20;

結果は以下です。

2025-12-30|2
2025-12-23|2
2025-12-16|2
2025-12-09|2
2025-12-02|2
2025-11-25|2
2025-11-18|2
2025-11-10|2
2025-11-04|2
2025-10-28|2

同一市場×同一日付が常に2行になっているようです。

投機筋ネットポジションを確認する

投機筋ネットポジションは、以下で計算します。

Noncommercial Long - Noncommercial Short

実際にJPY先物の直近データを確認します。

SELECT
  "As of Date in Form YYYY-MM-DD" AS dt,
  ("Noncommercial Positions-Long (All)" - "Noncommercial Positions-Short (All)") AS net_nc,
  ("Commercial Positions-Long (All)" - "Commercial Positions-Short (All)") AS net_comm,
  "Open Interest (All)" AS oi
FROM cot_legacy_fut_only
WHERE TRIM("Market and Exchange Names") = 'JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE'
ORDER BY dt DESC
LIMIT 20;

結果を見ると、同じ日付の行が2回ずつ出ています。

2025-12-30|14068|-19076|286132
2025-12-30|14068|-19076|286132
2025-12-23|1223|-5904|285084
2025-12-23|1223|-5904|285084
2025-12-16|-2942|-4744|379679
2025-12-16|-2942|-4744|379679

値も完全に同じなので、実質的には重複データです。

市場名の末尾スペースが原因か確認する

最初は、市場名の末尾スペースや不可視文字の違いで重複している可能性を疑いました。

そこで、16進表記と文字列長を確認します。

SELECT
  HEX("Market and Exchange Names") AS hex_name,
  LENGTH("Market and Exchange Names") AS len_name,
  COUNT(*) AS n
FROM cot_legacy_fut_only
WHERE TRIM("Market and Exchange Names") = 'JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE'
  AND "As of Date in Form YYYY-MM-DD" = '2025-12-30'
GROUP BY hex_name, len_name;

結果は以下です。

4A4150414E4553452059454E202D204348494341474F204D455243414E54494C452045584348414E4745|42|2

市場名は1種類で、文字列長も同じです。

つまり、末尾スペースや不可視文字が原因ではありませんでした。

完全重複か確認する

次に、同じ日付の2行を詳細に確認します。

SELECT
  rowid,
  "Market and Exchange Names" AS mkt,
  "As of Date in Form YYYY-MM-DD" AS dt,
  "CFTC Contract Market Code" AS cmc,
  "CFTC Commodity Code" AS ccc,
  "Open Interest (All)" AS oi,
  "Noncommercial Positions-Long (All)" AS nc_l,
  "Noncommercial Positions-Short (All)" AS nc_s
FROM cot_legacy_fut_only
WHERE TRIM("Market and Exchange Names") = 'JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE'
  AND "As of Date in Form YYYY-MM-DD" = '2025-12-30';

結果は以下です。

352955|JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE|2025-12-30|097741|97|286132|144596|130528
370271|JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE|2025-12-30|097741|97|286132|144596|130528

rowid 以外は完全に同じです。

つまり、同じレコードが2回入っている完全重複でした。

DBをバックアップする

重複削除の前に、DBをバックアップします。

cp cot_legacy.db cot_legacy_backup.db

重複を削除する

主要列が一致している重複について、最小の rowid だけを残して削除します。

DELETE FROM cot_legacy_fut_only
WHERE rowid NOT IN (
  SELECT MIN(rowid)
  FROM cot_legacy_fut_only
  GROUP BY
    "Market and Exchange Names",
    "As of Date in Form YYYY-MM-DD",
    "CFTC Contract Market Code",
    "CFTC Commodity Code",
    "Open Interest (All)",
    "Noncommercial Positions-Long (All)",
    "Noncommercial Positions-Short (All)",
    "Noncommercial Positions-Spreading (All)",
    "Commercial Positions-Long (All)",
    "Commercial Positions-Short (All)"
);

削除後、件数を確認します。

SELECT COUNT(*) FROM cot_legacy_fut_only;

結果は以下です。

220664

重複削除前は374006件だったため、かなり重複が含まれていたことが分かります。

再発防止のユニークインデックスを作成する

同じ市場・同じ日付・同じ市場コード・同じ商品コードのデータが重複しないよう、ユニークインデックスを作成します。

CREATE UNIQUE INDEX IF NOT EXISTS ux_cot_key
ON cot_legacy_fut_only(
  "Market and Exchange Names",
  "As of Date in Form YYYY-MM-DD",
  "CFTC Contract Market Code",
  "CFTC Commodity Code"
);

これにより、次回同じデータを取り込もうとしても、DB側で重複を拒否できます。

重複削除後の確認

まず、件数を再確認します。

SELECT COUNT(*) FROM cot_legacy_fut_only;

結果は以下です。

220664

次に、JPY先物で同一日付の重複が消えたか確認します。

SELECT
  "As of Date in Form YYYY-MM-DD" AS dt,
  COUNT(*) AS n
FROM cot_legacy_fut_only
WHERE TRIM("Market and Exchange Names")
      = 'JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE'
GROUP BY dt
HAVING n > 1
ORDER BY dt DESC
LIMIT 10;

何も返らなければ成功です。

最後に、JPY先物の件数を確認します。

SELECT COUNT(*) FROM cot_legacy_fut_only
WHERE TRIM("Market and Exchange Names")
      = 'JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE';

結果は以下です。

1044

先ほど確認した週数と一致しました。

JPY専用の軽量テーブルを作成する

分析や可視化をしやすくするため、JPY先物だけを抽出した軽量テーブルを作成します。

DROP TABLE IF EXISTS cot_jpy;

CREATE TABLE cot_jpy AS
SELECT
  "As of Date in Form YYYY-MM-DD" AS dt,
  "Open Interest (All)" AS oi,
  "Noncommercial Positions-Long (All)" AS nc_long,
  "Noncommercial Positions-Short (All)" AS nc_short,
  ("Noncommercial Positions-Long (All)"
   - "Noncommercial Positions-Short (All)") AS nc_net,
  "Commercial Positions-Long (All)" AS comm_long,
  "Commercial Positions-Short (All)" AS comm_short,
  ("Commercial Positions-Long (All)"
   - "Commercial Positions-Short (All)") AS comm_net
FROM cot_legacy_fut_only
WHERE TRIM("Market and Exchange Names")
      = 'JAPANESE YEN - CHICAGO MERCANTILE EXCHANGE';

CREATE UNIQUE INDEX idx_cot_jpy_dt ON cot_jpy(dt);

件数を確認します。

SELECT COUNT(*) FROM cot_jpy;

結果は以下です。

1044

これで、JPY先物の週次データを分析しやすい形にできました。

IMMポジションとUSDJPYを組み合わせる方向性

今回作成した cot_jpync_net が、一般的にIMMポジションとして見る投機筋ネットポジションです。

定義は以下です。

nc_net = Noncommercial Long - Noncommercial Short

これをUSDJPYの価格データと同じ時間軸で結合すれば、為替レートと投機筋ポジションの流れを比較できます。

表示方法としては、以下のような2軸グラフが使えそうです。

  • 左軸:USDJPYレート
  • 右軸:JPY先物の投機筋ネットポジション

合わせ方は2通りあります。

  1. COTの基準日である火曜日の日付に合わせる
  2. 市場がデータを知る金曜日公表日にずらして合わせる

バックテストやシグナル生成では、「市場がいつ知った情報か」が重要になるため、最終的には金曜日公表日ベースにずらす処理も検討します。

今回の結果

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

  • CFTCの年別ZIPからCOTデータを取得できた
  • 2006年から2025年までのデータをSQLiteへ保存できた
  • 列名ゆらぎに対応した
  • 年によって増える列を自動追加できるようにした
  • SQLiteにrawデータを格納できた
  • JPY先物の市場名を確認できた
  • JPY先物に欠損がないことを確認した
  • 重複データを発見して削除した
  • 再発防止のユニークインデックスを作成した
  • JPY専用の cot_jpy テーブルを作成した

ハマりどころ

列名が年によって違う

CFTCのCOTデータは、年や形式によって列名が微妙に違います。

スペース、アンダースコア、ハイフン、大文字小文字の違いがあるため、列名をそのまま完全一致で探すと失敗します。

列名を正規化して照合する処理を入れると安定します。

途中の年から列が増える

2015年のデータでは、以前のテーブルに存在しない列が追加されていました。

SQLiteへ追記する場合は、足りない列を ALTER TABLE ADD COLUMN で追加してからINSERTする必要があります。

DtypeWarningはraw格納なら一旦放置でもよい

pandasの DtypeWarning は、列内に複数の型が混ざっている可能性を示します。

rawデータをDBへ保存する段階では放置しても進められますが、分析用テーブルでは型を明示的に整える方が安全です。

重複データが入っていた

当初の取り込みでは、同じ市場・同じ日付の完全重複が入っていました。

重複削除後にユニークインデックスを作成して、再発を防ぎました。

次にやること

次は、JPY先物のIMMポジションとUSDJPY価格を結合して可視化します。

具体的には以下を行います。

  • yfinanceでUSDJPY価格を取得する
  • 週次データに変換する
  • cot_jpy と日付で結合する
  • USDJPYとIMMネットポジションを2軸グラフで表示する
  • 必要に応じてCOT公表日ベースにずらす
  • Zスコアや変化量を特徴量化する

最終的には、USDJPY、IMMポジション、日米金利差、VIX、金価格などを組み合わせて、金融危機監視レーダーや中長期マクロ戦略に使える特徴量へ発展させたいです。

まとめ

CFTCのCOTレポートから、2006年から2025年までのIMMポジションデータを取得し、SQLiteへ保存しました。

途中で列名ゆらぎ、列追加、DtypeWarning、重複データなどの問題がありましたが、列名正規化、列自動追加、重複削除、ユニークインデックス作成で対応できました。

最後にJPY先物だけを抽出した cot_jpy テーブルを作成し、投機筋ネットポジションである nc_net を分析しやすい形にしました。

次はUSDJPY価格と結合し、為替レートとIMMポジションの流れを可視化していきます。

コメント

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