これを
市場データを危機監視に使いやすい特徴量へ変換し、SQLiteへ保存する
前回までに、FREDやStooqから取得した市場データをSQLiteの market.db に保存できるようにしました。
ただし、取得した生データをそのまま危機監視に使うのは少し扱いづらいです。
理由は、FREDやStooqから取得した系列には、日次・週次・月次・四半期といった頻度の違いがあり、さらに系列ごとに意味も異なるためです。
たとえば、VIXは市場の警戒感を見る指標、米国債利回りは金利環境を見る指標、社債スプレッドは信用不安を見る指標です。
これらをそのまま並べるだけでは、後で危機スコアを作るときに扱いにくくなります。
そこで今回は、market.db に保存した生データをもとに、危機監視用の特徴量を作成し、分析用データベースである research.sqlite に保存します。
今回の位置づけ
今回の作業は、金融時系列データを「取得する段階」から「危機監視に使いやすい形へ加工する段階」へ進めるものです。
全体の流れは以下です。
fetch_fred_to_sqlite.pyでFREDやStooqから生データを取得するmarket.dbのseries_observationsに保存するbuild_crisis_features.pyで危機監視用の特徴量を生成するresearch.sqliteのcrisis_featuresに保存する- 次回以降、
build_crisis_score.pyで危機スコアを作る
今回はこのうち、build_crisis_features.py の作成と検証を行います。
生データと分析用データを分ける理由
今回のポイントは、market.db を直接加工して上書きしないことです。
生データと分析用データは、役割が違います。
market.db は、FREDやStooqから取得した元データを保存する場所です。
一方、research.sqlite は、分析しやすいように加工したデータを保存する場所です。
この2つを分けておくと、後で別の分析をしたくなったときに便利です。
たとえば、金価格の長期データは、危機監視の日次スコアには長すぎるかもしれません。
しかし、ドル、金、原油、金融危機を長期比較する用途では価値があります。
そのため、生データは削除せずに残し、危機監視用の特徴量を作る段階で必要な期間だけを使う方針にします。
今回使う市場データ
今回の特徴量生成では、以下の系列を使います。
DGS10:米10年国債利回りDGS2:米2年国債利回りDGS3MO:米3か月国債利回りVIXCLS:VIX指数BAMLH0A0HYM2:米ハイイールド債OASBAMLC0A4CBBB:米BBB格社債OASSOFR:SOFRNFCI:Chicago Fed National Financial Conditions IndexANFCI:Chicago Fed Adjusted National Financial Conditions IndexDRTSCILM:銀行の貸出態度を示すSLOOS系列DEXJPUS:ドル円DCOILWTICO:WTI原油価格XAUUSD_STOOQ:Stooqから取得した金価格
これらをそのまま使うのではなく、後で危機スコアに使いやすい名前へ変換します。
たとえば、VIXCLS は vix、BAMLH0A0HYM2 は hy_oas のように扱います。
作成する特徴量
今回作る特徴量は、大きく分けて4種類です。
1. イールドカーブ系
米国債利回りから、イールドカーブを見るための特徴量を作ります。
yc_10y_2y = dgs10 - dgs2yc_10y_3m = dgs10 - dgs3mo
イールドカーブは、景気後退懸念や金融政策への見方を確認するためによく使われます。
特に、長期金利と短期金利の差が小さくなったり、逆転したりすると、市場が将来の景気悪化を警戒している可能性があります。
そのため、10年-2年、10年-3か月の差を特徴量として保存しておきます。
2. 信用スプレッド系
社債市場のストレスを見るために、信用スプレッド系の特徴量を作ります。
hy_oas:ハイイールド債OASbbb_oas:BBB格社債OAShy_minus_bbb = hy_oas - bbb_oas
信用不安が強まると、リスクの高い社債ほど投資家から避けられやすくなります。
その結果、ハイイールド債のスプレッドが拡大しやすくなります。
hy_minus_bbb を作ることで、ハイイールド債とBBB格社債のストレス差を見やすくします。
3. 5日変化
危機監視では、水準だけでなく、短期間の変化も重要です。
たとえば、VIXが高いかどうかだけでなく、直近数日で急に上がったかどうかも見たいです。
そこで、主要な特徴量について5日変化を作ります。
vix_chg_5dhy_oas_chg_5dbbb_oas_chg_5dhy_minus_bbb_chg_5dyc_10y_2y_chg_5dyc_10y_3m_chg_5dnfci_chg_5danfci_chg_5d
これにより、直近1週間程度で市場環境がどの方向に動いたかを見やすくなります。
4. 20日z-score
20日z-scoreは、直近20日間の平均からどのくらい離れているかを見るための特徴量です。
同じVIXの25でも、普段から25前後で推移している時期と、普段は15前後なのに急に25になった時期では意味が違います。
z-scoreを使うと、「その系列にとって今の値がどれくらい異常か」を比較しやすくなります。
危機スコアを作るときには、単純な水準だけでなく、このような標準化した特徴量が役立ちます。
5. 20日移動平均乖離
20日移動平均乖離は、現在値が直近20日平均からどれくらい離れているかを見るための特徴量です。
z-scoreほど標準化はされませんが、直近の平均からのズレを直感的に見られます。
今回は、VIX、信用スプレッド、ドル円、WTI、金価格などに対して作成します。
日次化とforward fillの考え方
FREDの系列は、すべてが日次ではありません。
VIXや米国債利回りは日次で取得できます。
一方で、NFCIやANFCIは週次、SLOOSは四半期です。
このまま横結合すると、低頻度系列は多くの日付で欠損になります。
しかし、危機スコアを日次で作るなら、各日付で参照できる値が必要です。
そこで、低頻度系列についてはforward fillします。
forward fillは、直近の公表値を次の公表値が出るまで使う方法です。
たとえば、四半期ごとに発表されるSLOOSは、毎日更新されるわけではありません。
それでも、次の値が出るまでは、直近の値がその時点で利用できる最新情報です。
この考え方で、週次・四半期系列も日次の危機監視データに組み込みます。
build_crisis_features.pyを作成する
それでは、build_crisis_features.py を作成します。
vim build_crisis_features.py
コードは以下です。
import sqlite3
import pandas as pd
import numpy as np
MARKET_DB = "market.db"
RESEARCH_DB = "research.sqlite"
# 危機監視用の分析期間
ANALYSIS_START_DATE = "1990-01-01"
# True にすると保存前に crisis_features を全削除
RESET_CRISIS_FEATURES = True
# 使用する系列
FEATURE_SERIES = {
"DGS10": "dgs10",
"DGS2": "dgs2",
"DGS3MO": "dgs3mo",
"VIXCLS": "vix",
"BAMLH0A0HYM2": "hy_oas",
"BAMLC0A4CBBB": "bbb_oas",
"SOFR": "sofr",
"NFCI": "nfci",
"ANFCI": "anfci",
"DRTSCILM": "sloos",
"DEXJPUS": "usdjpy",
"DCOILWTICO": "wti",
"XAUUSD_STOOQ": "gold",
}
def load_series_wide(db_path: str, series_map: dict[str, str]) -> pd.DataFrame:
series_ids = list(series_map.keys())
placeholders = ",".join(["?"] * len(series_ids))
```
sql = f"""
SELECT series_id, date, value
FROM series_observations
WHERE series_id IN ({placeholders})
ORDER BY date
"""
with sqlite3.connect(db_path) as conn:
df = pd.read_sql_query(sql, conn, params=series_ids)
if df.empty:
raise RuntimeError("series_observations から対象系列を取得できませんでした")
df["date"] = pd.to_datetime(df["date"])
df["value"] = pd.to_numeric(df["value"], errors="coerce")
wide = df.pivot(index="date", columns="series_id", values="value").sort_index()
rename_map = {k: v for k, v in series_map.items() if k in wide.columns}
wide = wide.rename(columns=rename_map)
wide.columns.name = None
return wide
```
def zscore_rolling(s: pd.Series, window: int = 20) -> pd.Series:
mean_ = s.rolling(window).mean()
std_ = s.rolling(window).std()
```
return (s - mean_) / std_
```
def build_features(wide: pd.DataFrame) -> pd.DataFrame:
df = wide.copy()
```
# 危機監視用の分析期間だけ使う
df = df[df.index >= ANALYSIS_START_DATE]
if df.empty:
raise RuntimeError(f"{ANALYSIS_START_DATE} 以降のデータがありません")
# 日次軸へそろえる
full_index = pd.date_range(df.index.min(), df.index.max(), freq="D")
df = df.reindex(full_index)
# 低頻度系列も含めて前方補完
df = df.ffill()
# イールドカーブ
df["yc_10y_2y"] = df["dgs10"] - df["dgs2"]
df["yc_10y_3m"] = df["dgs10"] - df["dgs3mo"]
# 信用スプレッド差
df["hy_minus_bbb"] = df["hy_oas"] - df["bbb_oas"]
# 5日変化
change_cols = [
"vix", "hy_oas", "bbb_oas", "hy_minus_bbb",
"yc_10y_2y", "yc_10y_3m", "sofr", "nfci", "anfci", "sloos",
"usdjpy", "wti", "gold"
]
for col in change_cols:
if col in df.columns:
df[f"{col}_chg_5d"] = df[col] - df[col].shift(5)
# 20日 z-score
z_cols = [
"vix", "hy_oas", "bbb_oas", "hy_minus_bbb",
"yc_10y_2y", "yc_10y_3m", "sofr", "nfci", "anfci",
"usdjpy", "wti", "gold"
]
for col in z_cols:
if col in df.columns:
df[f"{col}_z20"] = zscore_rolling(df[col], window=20)
# 20日移動平均乖離
ma_cols = ["vix", "hy_oas", "bbb_oas", "usdjpy", "wti", "gold"]
for col in ma_cols:
if col in df.columns:
ma20 = df[col].rolling(20).mean()
df[f"{col}_ma20_gap"] = df[col] - ma20
preferred_order = [
"dgs10", "dgs2", "dgs3mo",
"yc_10y_2y", "yc_10y_3m",
"hy_oas", "bbb_oas", "hy_minus_bbb",
"vix", "sofr", "nfci", "anfci", "sloos",
"usdjpy", "wti", "gold"
]
others = [c for c in df.columns if c not in preferred_order]
df = df[[c for c in preferred_order if c in df.columns] + others]
df.index.name = "date"
return df
```
def init_research_db(db_path: str) -> None:
with sqlite3.connect(db_path) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS crisis_features (
date TEXT NOT NULL,
feature_name TEXT NOT NULL,
value REAL,
PRIMARY KEY (date, feature_name)
);
""")
conn.commit()
def reset_crisis_features_table(db_path: str) -> None:
with sqlite3.connect(db_path) as conn:
conn.execute("DELETE FROM crisis_features;")
conn.commit()
def save_features_long(db_path: str, feature_df: pd.DataFrame) -> int:
out = feature_df.reset_index().melt(
id_vars="date",
var_name="feature_name",
value_name="value"
)
```
out["date"] = pd.to_datetime(out["date"]).dt.strftime("%Y-%m-%d")
out = out.replace({np.nan: None})
rows = list(out.itertuples(index=False, name=None))
with sqlite3.connect(db_path) as conn:
cur = conn.executemany("""
INSERT OR REPLACE INTO crisis_features(date, feature_name, value)
VALUES (?, ?, ?)
""", rows)
conn.commit()
return cur.rowcount
```
def main():
print("Loading series from market.db ...")
```
wide = load_series_wide(MARKET_DB, FEATURE_SERIES)
print(f"Loaded columns: {list(wide.columns)}")
print(f"Raw shape: {wide.shape}")
print(f"Raw date range: {wide.index.min().date()} -> {wide.index.max().date()}")
print("Building crisis features ...")
feature_df = build_features(wide)
print(f"Analysis start date: {ANALYSIS_START_DATE}")
print(f"Feature shape: {feature_df.shape}")
print(f"Feature date range: {feature_df.index.min().date()} -> {feature_df.index.max().date()}")
print("Preparing research.sqlite ...")
init_research_db(RESEARCH_DB)
if RESET_CRISIS_FEATURES:
print("Resetting existing crisis_features ...")
reset_crisis_features_table(RESEARCH_DB)
print("Saving to research.sqlite ...")
n = save_features_long(RESEARCH_DB, feature_df)
print(f"Upserted {n} rows into crisis_features")
```
if **name** == "**main**":
main()
コードのポイント
このコードで特に重要なのは、次の3点です。
1. 分析期間を1990年以降に制限している
ANALYSIS_START_DATE = "1990-01-01"
危機監視用の特徴量では、すべての生データを最初から使う必要はありません。
後述しますが、今回のデータには1793年から始まる金価格が含まれていました。
そのまま日次化すると、危機監視には長すぎる期間まで特徴量が作られてしまいます。
そこで、危機監視用の特徴量は1990年以降に制限しています。
2. 低頻度系列をforward fillしている
df = df.ffill()
NFCIやANFCIは週次、SLOOSは四半期です。
これらは毎日値があるわけではありません。
しかし、危機スコアを日次で作る場合、各日付で参照できる値が必要になります。
そのため、直近の公表値を次の公表値まで使う形にしています。
3. 保存前にcrisis_featuresを削除している
RESET_CRISIS_FEATURES = True
特徴量の定義や対象期間を変えた場合、古い行が残ると分析結果が混ざります。
INSERT OR REPLACE は同じ主キーの行を置き換えますが、今回対象外になった古い日付の行までは削除しません。
そのため、再生成するときは保存前に crisis_features を空にするようにしました。
スクリプトを実行する
作成したスクリプトを実行します。
python build_crisis_features.py
実行結果は以下です。
Loading series from market.db ... Loaded columns: ['anfci', 'bbb_oas', 'hy_oas', 'wti', 'usdjpy', 'dgs10', 'dgs2', 'dgs3mo', 'sloos', 'nfci', 'sofr', 'vix', 'gold'] Raw shape: (15484, 13) Raw date range: 1793-03-01 -> 2026-03-13 Building crisis features ... Analysis start date: 1990-01-01 Feature shape: (13221, 47) Feature date range: 1990-01-01 -> 2026-03-13 Preparing research.sqlite ... Resetting existing crisis_features ... Saving to research.sqlite ... Upserted 621387 rows into crisis_features
ここで見るべきポイントは、Raw date range と Feature date range の違いです。
読み込んだ生データの期間は、1793-03-01 から 2026-03-13 です。
一方で、特徴量として作成した期間は、1990-01-01 から 2026-03-13 です。
これは、生データを削除したという意味ではありません。
生データは market.db に残したまま、危機監視用の特徴量だけ1990年以降に制限しています。
Feature shape: (13221, 47) は、13221日分、47個の特徴量が作られたことを表します。
保存件数も次の計算と一致します。
13221日 × 47特徴量 = 621387行
そのため、保存件数としても整合しています。
ハマりどころ:正常終了しても結果が正しいとは限らない
今回の作業で一番大事だったのは、エラーではなく「成功しているように見える異常」を見つけたことです。
最初に作った版でも、スクリプト自体は正常終了していました。
しかし、結果は以下のようになっていました。
Feature shape: (85114, 47) Upserted 4000358 rows into crisis_features
Pythonのエラーは出ていません。
SQLiteへの保存もできています。
しかし、危機監視用の特徴量としては、85114日分というのは大きすぎます。
1990年から2026年までの日次データなら、だいたい1.3万日程度です。
そのため、Feature shape を見た時点で、何かの系列が想定より古い日付から始まっている可能性を疑いました。
原因確認:系列ごとの開始日を調べる
どの系列が原因かを調べるため、market.db 側で系列ごとの開始日を確認します。
sqlite3 market.db
SELECT series_id, MIN(date) AS min_date, MAX(date) AS max_date, COUNT(*) AS cnt FROM series_observations WHERE series_id IN ( 'DGS10','DGS2','DGS3MO','VIXCLS','BAMLH0A0HYM2','BAMLC0A4CBBB', 'SOFR','NFCI','ANFCI','DRTSCILM','DEXJPUS','DCOILWTICO','XAUUSD_STOOQ' ) GROUP BY series_id ORDER BY min_date;
結果は以下でした。
XAUUSD_STOOQ|1793-03-01|2026-03-13|15201 DCOILWTICO|1990-01-01|2026-03-09|9441 DEXJPUS|1990-01-01|2026-03-06|9440 DGS10|1990-01-01|2026-03-12|9444 DGS2|1990-01-01|2026-03-12|9444 DGS3MO|1990-01-01|2026-03-12|9444 VIXCLS|1990-01-02|2026-03-12|9443 ANFCI|1990-01-05|2026-03-06|1888 NFCI|1990-01-05|2026-03-06|1888 DRTSCILM|1990-04-01|2026-01-01|144 BAMLC0A4CBBB|1996-12-31|2026-03-12|7718 BAMLH0A0HYM2|1996-12-31|2026-03-12|7718 SOFR|2018-04-03|2026-03-12|2073
原因は、XAUUSD_STOOQ でした。
金価格データが 1793-03-01 から入っていたため、最初のコードでは1793年から2026年までの日次インデックスを作っていました。
full_index = pd.date_range(df.index.min(), df.index.max(), freq="D")
このコード自体は間違っていません。
ただし、読み込むデータに非常に古い系列が含まれている場合、その古い日付まで含めて日次化されます。
今回はそれが危機監視用の特徴量としては長すぎる結果につながりました。
なぜ金価格データを削除しないのか
ここで、XAUUSD_STOOQ の古いデータを削除するという対応も考えられます。
しかし、今回は削除しません。
理由は、古い金価格データにも別の用途があるからです。
長期の金融市場分析では、金価格、ドル、原油、金利、過去の金融危機を比較したくなる可能性があります。
そのとき、1793年からの金価格データは価値があります。
問題は、データが古いことではありません。
長期分析用のデータを、危機監視用の日次特徴量にそのまま混ぜたことです。
そのため、今回は次の方針にしました。
market.dbには長期データを残すbuild_crisis_features.pyでは1990年以降に制限するresearch.sqliteには危機監視に使う特徴量だけ保存する
このように分けることで、長期分析と危機監視を両立できます。
SQLiteで保存結果を確認する
特徴量を保存したら、research.sqlite の中身を確認します。
sqlite3 research.sqlite
以下のSQLを実行します。
SELECT feature_name, MIN(date) AS min_date, MAX(date) AS max_date, COUNT(*) AS total_rows, COUNT(value) AS non_null_rows FROM crisis_features GROUP BY feature_name ORDER BY feature_name;
ここで重要なのは、COUNT(*) だけでなく、COUNT(value) も見ることです。
COUNT(*) は、値がNULLの行も含めて数えます。
一方、COUNT(value) は、値が入っている行だけを数えます。
つまり、total_rows と non_null_rows を比較することで、特徴量ごとの欠損状況を確認できます。
確認結果の一部は以下です。
anfci|1990-01-01|2026-03-13|13221|13221 anfci_chg_5d|1990-01-01|2026-03-13|13221|13216 anfci_z20|1990-01-01|2026-03-13|13221|13202 bbb_oas|1990-01-01|2026-03-13|13221|10665 bbb_oas_chg_5d|1990-01-01|2026-03-13|13221|10660 bbb_oas_ma20_gap|1990-01-01|2026-03-13|13221|10646 bbb_oas_z20|1990-01-01|2026-03-13|13221|10646 dgs10|1990-01-01|2026-03-13|13221|13221 dgs2|1990-01-01|2026-03-13|13221|13221 dgs3mo|1990-01-01|2026-03-13|13221|13221 gold|1990-01-01|2026-03-13|13221|13221 gold_chg_5d|1990-01-01|2026-03-13|13221|13216 gold_ma20_gap|1990-01-01|2026-03-13|13221|13202 gold_z20|1990-01-01|2026-03-13|13221|13202 hy_minus_bbb|1990-01-01|2026-03-13|13221|10665 hy_minus_bbb_chg_5d|1990-01-01|2026-03-13|13221|10660 hy_minus_bbb_z20|1990-01-01|2026-03-13|13221|10646 hy_oas|1990-01-01|2026-03-13|13221|10665 hy_oas_chg_5d|1990-01-01|2026-03-13|13221|10660 hy_oas_ma20_gap|1990-01-01|2026-03-13|13221|10646 hy_oas_z20|1990-01-01|2026-03-13|13221|10646 nfci|1990-01-01|2026-03-13|13221|13221 nfci_chg_5d|1990-01-01|2026-03-13|13221|13216 nfci_z20|1990-01-01|2026-03-13|13221|13202 sofr|1990-01-01|2026-03-13|13221|2901 sofr_chg_5d|1990-01-01|2026-03-13|13221|2896 sofr_z20|1990-01-01|2026-03-13|13221|2882 usdjpy|1990-01-01|2026-03-13|13221|13221 usdjpy_chg_5d|1990-01-01|2026-03-13|13221|13216 usdjpy_ma20_gap|1990-01-01|2026-03-13|13221|13202 usdjpy_z20|1990-01-01|2026-03-13|13221|13202 vix|1990-01-01|2026-03-13|13221|13220 vix_chg_5d|1990-01-01|2026-03-13|13221|13215 vix_ma20_gap|1990-01-01|2026-03-13|13221|13201 vix_z20|1990-01-01|2026-03-13|13221|13201 wti|1990-01-01|2026-03-13|13221|13221 wti_chg_5d|1990-01-01|2026-03-13|13221|13216 wti_ma20_gap|1990-01-01|2026-03-13|13221|13202 wti_z20|1990-01-01|2026-03-13|13221|13202 yc_10y_2y|1990-01-01|2026-03-13|13221|13221 yc_10y_2y_chg_5d|1990-01-01|2026-03-13|13221|13216 yc_10y_2y_z20|1990-01-01|2026-03-13|13221|13202 yc_10y_3m|1990-01-01|2026-03-13|13221|13221 yc_10y_3m_chg_5d|1990-01-01|2026-03-13|13221|13216 yc_10y_3m_z20|1990-01-01|2026-03-13|13221|13202
確認結果の読み方
今回の確認では、全特徴量の total_rows が 13221 でそろいました。
これは、すべての特徴量が同じ日次インデックスにそろっていることを意味します。
一方で、non_null_rows は特徴量によって異なります。
これは必ずしも異常ではありません。
たとえば、5日変化の特徴量は、最初の5日分を計算できません。
そのため、*_chg_5d は通常の系列より少しだけ non_null_rows が少なくなります。
また、20日z-scoreや20日移動平均乖離は、最初の20日程度を計算できません。
そのため、*_z20 や *_ma20_gap の値が20件前後少ないのは自然です。
さらに、OAS系の系列は1996年末ごろから、SOFRは2018年以降から始まります。
このような欠損は、取得ミスではなく元系列の開始時期によるものです。
欠損を見たときは、次の3つに分けて考える必要があります。
- 計算仕様上、自然に発生する欠損
- 元系列の開始時期による欠損
- 取得失敗や処理ミスによる欠損
この切り分けをしておくと、正常な欠損をエラー扱いせずに済みます。
次の危機スコアで使う特徴量候補
今回作成した crisis_features は、次に作る build_crisis_score.py の土台になります。
最初の版では、複雑な機械学習モデルではなく、ルールベースのスコアから始める予定です。
優先して使う特徴量
vix_z20yc_10y_2y_z20yc_10y_3m_z20nfci_z20anfci_z20
これらは比較的長い期間で使いやすく、市場の警戒感、イールドカーブ、金融環境をまとめて見られます。
追加で使いたい特徴量
hy_oas_z20bbb_oas_z20hy_minus_bbb_z20
信用市場のストレスを見るには、OAS系の特徴量が重要です。
ただし、これらは1996年末ごろから始まるため、スコアの開始日を考える必要があります。
補助的に見る特徴量
sofr_z20sloosgold_z20usdjpy_z20
SOFRは重要ですが、履歴が2018年以降と短いため、最初の危機スコアでは補助指標として扱う方がよさそうです。
SLOOSも四半期系列なので、日次スコアの中心というより、信用環境の背景を見るための補助指標として使う方が自然です。
危機スコアの開始日は1997年以降が扱いやすい
今回の特徴量は、1990年以降で作成しました。
ただし、すべての特徴量が1990年から使えるわけではありません。
特に、信用スプレッド系の hy_oas や bbb_oas は1996年末ごろから始まります。
そのため、次に作る危機スコアでは、1997-01-01 以降を対象にすると扱いやすそうです。
SOFRまで含めて完全にそろえようとすると、2018年以降になります。
しかし、それではドットコムバブル崩壊、リーマンショック、欧州債務危機などの比較に使える期間が短くなります。
そのため、最初は1997年以降を対象にし、SOFRは補助指標として扱う方針にします。
今回できたこと
今回の作業で、危機監視用の特徴量テーブルを作る流れができました。
market.dbから必要な市場データを読み込んだ- 系列IDを分析で使いやすい名前に変換した
- 日次インデックスへそろえた
- 週次・四半期系列をforward fillした
- イールドカーブ系の特徴量を作成した
- 信用スプレッド系の特徴量を作成した
- 5日変化を作成した
- 20日z-scoreを作成した
- 20日移動平均乖離を作成した
research.sqliteのcrisis_featuresに保存した- 1793年から始まる金価格データによる日次展開の巨大化に気づいた
- 生データは残したまま、特徴量生成側で1990年以降に制限した
- SQLiteで保存件数と欠損状況を確認した
ハマりどころ
正常終了したログだけでは安心できない
今回の最初の実行では、PythonエラーもSQLiteエラーも出ていませんでした。
しかし、Feature shape: (85114, 47) という数字を見れば、危機監視用としては大きすぎることに気づけます。
処理が正常終了したかどうかだけでなく、行数、期間、欠損数を確認することが大事です。
生データの期間が分析用途に合うとは限らない
金価格データのように、長期データが入っていること自体は悪いことではありません。
しかし、危機監視用の日次特徴量としては長すぎる場合があります。
用途に応じて、特徴量生成側で期間を切る方が安全です。
INSERT OR REPLACEだけでは古い対象外レコードは消えない
一度1793年起点で crisis_features を作ってしまうと、あとから1990年以降に制限して再実行しても、古い日付のレコードが残る可能性があります。
INSERT OR REPLACE は同じ主キーの行を置き換えるだけで、今回対象外になった古い日付の行までは削除しません。
そのため、今回は保存前に以下で全削除しました。
conn.execute("DELETE FROM crisis_features;")
次にやること
次は、今回作成した crisis_features を使って、build_crisis_score.py を作ります。
最初の版では、説明しやすいルールベースの危機スコアにする予定です。
- VIXが通常より高いか
- 信用スプレッドが拡大しているか
- イールドカーブが悪化しているか
- NFCIやANFCIが悪化しているか
これらを重み付きで合計し、スコアに応じて次のようなレジームに分類します。
normalcautionwarningrisk_off
まずは説明できるシンプルなルールから始めて、過去の危機局面と照らし合わせながら調整していきます。
まとめ
今回は、FREDやStooqから取得した市場データを、危機監視に使いやすい特徴量へ変換する build_crisis_features.py を作成しました。
生データは market.db に残し、分析用の特徴量は research.sqlite の crisis_features に保存する構成にしました。
日次、週次、四半期の系列を日次インデックスへそろえ、イールドカーブ、信用スプレッド差、5日変化、20日z-score、20日移動平均乖離を作成しています。
途中で、金価格データ XAUUSD_STOOQ が1793年から入っていたため、最初の特徴量生成では85114日分まで膨らむ問題がありました。
この問題は、生データを削除せず、危機監視用の特徴量生成側で 1990-01-01 以降に期間制限することで対応しました。
修正後は、1990年以降の13221日分、47特徴量を生成し、621387行として保存できました。
これで、危機スコアを作るための土台ができました。
次は build_crisis_score.py を作成し、危機スコアとレジーム判定へ進みます。
[/writing]
カテゴリ
開発ログ
タグ候補
Python, SQLite, FRED, Stooq, 金融データ分析, 危機監視, 特徴量生成, 時系列分析
タイトル候補
市場データを危機監視に使いやすい特徴量へ変換し、SQLiteへ保存する
メタディスクリプション案
FREDやStooqから取得した市場データを、危機監視に使いやすい特徴量へ変換する build_crisis_features.py を作成。日次化、forward fill、5日変化、20日z-score、金価格データの1793年問題への対応を整理しました。

コメント