金融時系列データからreturnsを生成し、LTCM・リーマン・コロナ期の相関構造を比較する

1990年以降の金融時系列データからreturnsを生成し、LTCM・リーマン・コロナ期の相関を比較する

FREDやStooqから、USDJPY、WTI、金価格、米国金利などの時系列データをSQLiteへ保存しました。

次は、この価格データや金利データからリターンを生成し、危機局面ごとの相関を比較します。

価格水準そのものの相関を見ると、トレンドによる偽相関が出やすくなります。

そのため、まず returns_daily テーブルを作成し、各系列のリターンを保存します。

その後、1998年のLTCM危機、2008年のリーマンショック、2020年のコロナショックを切り出し、平常時と比較します。

今回の目的

今回の目的は、以下です。

  • SQLiteに保存した金融時系列データからreturnsを生成する
  • returnsをDBに保存する
  • 1998年のLTCM期だけを切り出して相関を見る
  • 平常時、LTCM、リーマン、コロナの相関構造を比較する
  • 危機ごとに市場の結びつきがどう変わるか確認する

なぜreturnsを使うのか

相関分析では、価格水準そのものではなく、リターンや変化量を使う方が安全です。

価格そのものを使うと、長期トレンドによって本来関係が薄い系列同士でも高い相関が出ることがあります。

そのため、今回の順番は固定です。

  1. 全期間のreturnsを生成する
  2. 危機期間だけを切り出す
  3. 切り出した期間で相関を計算する

現在のDB状態を確認する

まず、現在のSQLiteデータベースを確認します。

sqlite3 market.db ".tables"

現状では、元データを保存する series_observations テーブルだけがあります。

series_observations

まだreturns保存用のテーブルはありません。

returns_dailyテーブルを作成する

リターン保存用のテーブルを作成します。

sqlite3 market.db "
CREATE TABLE IF NOT EXISTS returns_daily (
  series_id TEXT NOT NULL,
  date TEXT NOT NULL,
  ret REAL,
  PRIMARY KEY (series_id, date)
);
"

テーブルが作成されたか確認します。

sqlite3 market.db ".tables"

結果は以下です。

returns_daily        series_observations

これでDB構造は以下のようになります。

market.db
 ├ series_observations   ← 元データ
 └ returns_daily         ← リターン格納先

returns生成スクリプトを作成する

次に、各系列のリターンを生成するスクリプトを作成します。

vim make_returns.py

コードは以下です。

import sqlite3
import math

DB_PATH = "market.db"

SERIES = [
    "DEXJPUS",
    "DCOILWTICO",
    "XAUUSD_STOOQ",
    "DGS2",
    "DGS10",
    "DFF",
]

def generate_returns(conn, series_id):
    rows = conn.execute("""
        SELECT date, value
        FROM series_observations
        WHERE series_id = ?
          AND value IS NOT NULL
        ORDER BY date
    """, (series_id,)).fetchall()

    inserts = []
    prev = None

    for date, price in rows:
        if prev is None:
            prev = price
            continue

        if price <= 0 or prev <= 0:
            prev = price
            continue

        r = math.log(price / prev)
        inserts.append((series_id, date, r))
        prev = price

    conn.executemany("""
        INSERT OR REPLACE INTO returns_daily(series_id, date, ret)
        VALUES (?, ?, ?)
    """, inserts)

    conn.commit()
    print(f"{series_id}: {len(inserts)} returns inserted")

def main():
    with sqlite3.connect(DB_PATH) as conn:
        for s in SERIES:
            generate_returns(conn, s)

if __name__ == "__main__":
    main()

リターン計算の考え方

今回のスクリプトでは、対数リターンを計算しています。

ret = log(current_value / previous_value)

価格や金利が0以下の場合は、対数計算できないためスキップします。

元データは series_observations に残し、加工後のリターンだけを returns_daily に保存します。

returns生成を実行する

スクリプトを実行します。

python make_returns.py

実行結果は以下です。

DEXJPUS: 9058 returns inserted
DCOILWTICO: 9073 returns inserted
XAUUSD_STOOQ: 15180 returns inserted
DGS2: 9032 returns inserted
DGS10: 9032 returns inserted
DFF: 13190 returns inserted

returns_dailyの件数を確認する

SQLiteで系列ごとの件数を確認します。

sqlite3 market.db "
SELECT series_id, COUNT(*)
FROM returns_daily
GROUP BY series_id
ORDER BY series_id;"

結果は以下です。

DCOILWTICO|9073
DEXJPUS|9058
DFF|13190
DGS10|9032
DGS2|9032
XAUUSD_STOOQ|15180

これで、1990年以降の金融時系列データに対してreturnsを計算できました。

件数差は正常

系列ごとに件数が違いますが、これは正常です。

series returns数 コメント
DEXJPUS 9058 休日除外で正常
DCOILWTICO 9073 原油は営業日が少し違う
DGS2 9032 金利は祝日欠損あり
DGS10 9032 金利は祝日欠損あり
DFF 13190 日次フルに近い
XAUUSD_STOOQ 15180 長期データ

市場ごとに営業日や休日が異なるため、件数差が出ます。

相関計算では、各系列を日付で inner join し、共通日だけを使います。

LTCM期の相関を見る

まずは、1998年のLTCM危機期間を切り出します。

LTCM危機のピークはおおむね1998年8月から10月ですが、少し広げて以下の期間を分析します。

1998-06-01 〜 1998-12-31

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

vim ltcm_corr.py

コードは以下です。

import sqlite3
import pandas as pd

DB = "market.db"

START = "1998-06-01"
END = "1998-12-31"

SERIES = [
    "DEXJPUS",
    "DCOILWTICO",
    "XAUUSD_STOOQ",
    "DGS2",
    "DGS10",
    "DFF"
]

def load_series(conn, sid):
    df = pd.read_sql_query("""
        SELECT date, ret
        FROM returns_daily
        WHERE series_id = ?
        AND date BETWEEN ? AND ?
        ORDER BY date
    """, conn, params=(sid, START, END))

    df.rename(columns={"ret": sid}, inplace=True)
    return df

def main():
    with sqlite3.connect(DB) as conn:
        dfs = [load_series(conn, s) for s in SERIES]

        df = dfs[0]

        for d in dfs[1:]:
            df = df.merge(d, on="date", how="inner")

        print("data points:", len(df))
        print()

        corr = df.drop(columns=["date"]).corr()

        print("LTCM period correlation matrix")
        print(corr)

if __name__ == "__main__":
    main()

LTCM期の相関を実行する

実行します。

python ltcm_corr.py

結果は以下です。

data points: 143

LTCM period correlation matrix
               DEXJPUS  DCOILWTICO  XAUUSD_STOOQ      DGS2     DGS10       DFF
DEXJPUS       1.000000   -0.041521     -0.404416  0.012041 -0.187619 -0.003648
DCOILWTICO   -0.041521    1.000000      0.200492 -0.014291  0.011570 -0.062789
XAUUSD_STOOQ -0.404416    0.200492      1.000000 -0.115592 -0.098610 -0.018625
DGS2          0.012041   -0.014291     -0.115592  1.000000  0.788374 -0.013241
DGS10        -0.187619    0.011570     -0.098610  0.788374  1.000000 -0.060410
DFF          -0.003648   -0.062789     -0.018625 -0.013241 -0.060410  1.000000

ここでは、USDJPY、金、金利、原油の危機時相関が見えます。

特に、DEXJPUSXAUUSD_STOOQ の相関が -0.404 になっており、LTCM期には円と金がリスク回避資産として動いていた可能性が見えます。

複数の危機期間を比較する

LTCM期だけを見ると、危機時の構造が見えます。

ただし、危機には種類があります。

そこで、以下の期間を比較します。

期間名 期間 目的
平常時 1996-01-01〜1997-12-31 LTCM前の通常環境
LTCM危機 1998-06-01〜1998-12-31 流動性危機
リーマンショック 2008-08-01〜2009-03-31 ドル不足型危機
コロナショック 2020-02-01〜2020-06-30 金融政策・金利主導局面

危機比較スクリプトを作成する

期間ごとの相関行列と、USDJPY基準の相関比較を出すスクリプトを作成します。

vim compare_crisis_corr.py

コードは以下です。

import sqlite3
import pandas as pd

DB = "market.db"

SERIES = [
    "DEXJPUS",
    "DCOILWTICO",
    "XAUUSD_STOOQ",
    "DGS2",
    "DGS10",
    "DFF",
]

PERIODS = {
    "normal_1996_1997": ("1996-01-01", "1997-12-31"),
    "ltcm_1998": ("1998-06-01", "1998-12-31"),
    "lehman_2008": ("2008-08-01", "2009-03-31"),
    "covid_2020": ("2020-02-01", "2020-06-30"),
}

def load_period_df(conn: sqlite3.Connection, start: str, end: str, series_list: list[str]) -> pd.DataFrame:
    dfs = []

    for sid in series_list:
        df = pd.read_sql_query(
            """
            SELECT date, ret
            FROM returns_daily
            WHERE series_id = ?
              AND date BETWEEN ? AND ?
            ORDER BY date
            """,
            conn,
            params=(sid, start, end),
        )
        df = df.rename(columns={"ret": sid})
        dfs.append(df)

    merged = dfs[0]
    for df in dfs[1:]:
        merged = merged.merge(df, on="date", how="inner")

    return merged

def print_corr_matrix(name: str, df: pd.DataFrame) -> pd.DataFrame:
    corr = df.drop(columns=["date"]).corr()
    print("=" * 80)
    print(f"{name}  data_points={len(df)}")
    print(corr.round(3))
    print()
    return corr

def make_usdjpy_comparison(corr_by_period: dict[str, pd.DataFrame], base_series: str = "DEXJPUS") -> pd.DataFrame:
    rows = []

    for period_name, corr in corr_by_period.items():
        row = {"period": period_name}
        for col in corr.columns:
            if col == base_series:
                continue
            row[col] = corr.loc[base_series, col]
        rows.append(row)

    return pd.DataFrame(rows)

def make_pair_comparison(corr_by_period: dict[str, pd.DataFrame], pairs: list[tuple[str, str]]) -> pd.DataFrame:
    rows = []

    for period_name, corr in corr_by_period.items():
        row = {"period": period_name}
        for a, b in pairs:
            row[f"{a}__{b}"] = corr.loc[a, b]
        rows.append(row)

    return pd.DataFrame(rows)

def main():
    corr_by_period = {}

    with sqlite3.connect(DB) as conn:
        for period_name, (start, end) in PERIODS.items():
            df = load_period_df(conn, start, end, SERIES)
            corr = print_corr_matrix(period_name, df)
            corr_by_period[period_name] = corr

    # DEXJPUS を基準にした比較
    usdjpy_comp = make_usdjpy_comparison(corr_by_period, base_series="DEXJPUS")
    print("=" * 80)
    print("DEXJPUS correlation comparison by period")
    print(usdjpy_comp.round(3).to_string(index=False))
    print()

    # 特に見たいペアだけ比較
    pairs = [
        ("DEXJPUS", "DCOILWTICO"),
        ("DEXJPUS", "XAUUSD_STOOQ"),
        ("DEXJPUS", "DGS2"),
        ("DEXJPUS", "DGS10"),
        ("DEXJPUS", "DFF"),
        ("DGS2", "DGS10"),
        ("XAUUSD_STOOQ", "DCOILWTICO"),
    ]
    pair_comp = make_pair_comparison(corr_by_period, pairs)
    print("=" * 80)
    print("Selected pair comparison by period")
    print(pair_comp.round(3).to_string(index=False))

if __name__ == "__main__":
    main()

危機比較スクリプトの出力内容

このスクリプトを実行すると、以下の3種類の結果が出ます。

  1. 各期間の相関行列
  2. DEXJPUSを基準にした比較表
  3. 注目ペアだけを抜き出した比較表

これにより、平常時と危機時で、USDJPY、金、原油、米金利の関係がどう変わったかを横並びで見られます。

危機比較を実行する

実行します。

python compare_crisis_corr.py

出力のうち、DEXJPUS基準の比較表は以下です。

DEXJPUS correlation comparison by period
          period  DCOILWTICO  XAUUSD_STOOQ   DGS2  DGS10    DFF
normal_1996_1997       0.110        -0.208 -0.078 -0.094 -0.003
       ltcm_1998      -0.042        -0.404  0.012 -0.188 -0.004
     lehman_2008       0.306        -0.064  0.263  0.267 -0.086
      covid_2020       0.230        -0.077  0.468  0.565  0.153

注目ペアだけを抜き出した比較表は以下です。

Selected pair comparison by period
          period  DEXJPUS__DCOILWTICO  DEXJPUS__XAUUSD_STOOQ  DEXJPUS__DGS2  DEXJPUS__DGS10  DEXJPUS__DFF  DGS2__DGS10  XAUUSD_STOOQ__DCOILWTICO
normal_1996_1997                0.110                 -0.208         -0.078          -0.094        -0.003        0.908                    -0.013
       ltcm_1998               -0.042                 -0.404          0.012          -0.188        -0.004        0.788                     0.200
     lehman_2008                0.306                 -0.064          0.263           0.267        -0.086        0.752                     0.273
      covid_2020                0.230                 -0.077          0.468           0.565         0.153        0.779                     0.031

相関係数の読み方

相関係数は、2つの系列がどの程度同じ方向に動いたかを見る指標です。

意味
+1 完全に同方向
0 線形関係がほぼない
-1 完全に逆方向

ただし、相関は因果関係を示すものではありません。

ここでは、市場構造の変化を見るための手がかりとして使います。

平常時:1996〜1997年

平常時として、1996年から1997年を見ます。

主な特徴は以下です。

  • USDJPYとGold:-0.208
  • USDJPYとDGS2:-0.078
  • USDJPYとDGS10:-0.094

金とUSDJPYには軽い逆相関がありますが、全体としては強い関係ではありません。

米金利との関係も弱く、市場は比較的安定した通常マクロ環境だったと見られます。

LTCM危機:1998年

LTCM期では、平常時と比べて相関構造が変わっています。

ペア 平常時 LTCM期
USDJPY – Gold -0.208 -0.404
USDJPY – Oil 0.110 -0.042
USDJPY – DGS10 -0.094 -0.188

特に、USDJPYとGoldの逆相関が強くなっています。

これは、リスクオフ局面で円と金が同時に安全資産として買われた可能性を示しています。

USDJPYが下がる、つまり円高方向に動く一方で、Goldが上がるため、負の相関が強くなったと考えられます。

LTCM期は、流動性危機の性格が強い局面として見ることができます。

リーマンショック:2008年

リーマン期では、LTCM期とは違う構造が見えます。

ペア LTCM期 リーマン期
USDJPY – Oil -0.042 0.306
USDJPY – DGS10 -0.188 0.267
USDJPY – DGS2 0.012 0.263

リーマン期では、USDJPY、原油、米金利が同方向に動きやすくなっています。

これは、単純なリスクオフというより、ドル資金不足型の危機として見る方が自然です。

世界的にドル調達圧力が強まり、金融市場全体がドル流動性に影響された局面だったと考えられます。

コロナショック:2020年

コロナ期では、米金利との相関がさらに強くなっています。

ペア 相関
USDJPY – DGS10 0.565
USDJPY – DGS2 0.468
USDJPY – DFF 0.153

この期間は、FRBの緊急利下げや量的緩和など、金融政策の影響が非常に大きい局面でした。

USDJPYと米金利の相関が強く出ており、金利主導の市場として見えます。

危機タイプごとの違い

今回の比較では、危機ごとに相関構造がかなり違うことが分かりました。

危機 見えた構造
LTCM期 流動性危機。円と金が安全資産化
リーマン期 ドル不足型危機。USDJPY、原油、金利が同方向化
コロナ期 金融政策主導。USDJPYと米金利の相関が強い

同じ「危機」でも、市場の結びつき方は同じではありません。

危機を一括りにするのではなく、相関構造によって分類する視点が重要になりそうです。

今回できたこと

  • returns_daily テーブルを作成した
  • USDJPY、WTI、Gold、米2年金利、米10年金利、DFFのreturnsを生成した
  • returnsをSQLiteへ保存した
  • LTCM期の相関行列を作成した
  • 平常時、LTCM、リーマン、コロナ期を比較した
  • DEXJPUS基準の相関比較表を作成した
  • 危機ごとの相関構造の違いを確認した

ハマりどころ

価格そのものではなくreturnsを使う

価格水準で相関を見ると、トレンドによる偽相関が出やすくなります。

危機分析では、まずreturnsや差分を作るのが重要です。

市場ごとの営業日差がある

為替、金利、原油、金では営業日や欠損日が違います。

相関計算では、共通してデータがある日だけを使うため、inner joinします。

相関は因果ではない

相関係数は、2つの系列が同じ方向に動いたかどうかを見る指標です。

なぜそう動いたかを判断するには、当時の政策、流動性、ニュース、ポジションなども確認する必要があります。

次にやること

次は、今回の相関比較をさらに実用的にします。

  • rolling相関を計算する
  • 相関構造の急変を検知する
  • 危機前後の相関変化を可視化する
  • VIXやMOVEなどのボラティリティ指標を追加する
  • IMMポジションと組み合わせる
  • Neo4jへ資産間の相関関係を保存する

特に、rolling相関を使えば、相関が急に崩れたタイミングや、危機局面に近づいた兆候を見つけやすくなります。

まとめ

今回は、SQLiteに保存した金融時系列データからreturnsを生成し、平常時、LTCM危機、リーマンショック、コロナショックの相関構造を比較しました。

結果として、LTCM期は円と金が安全資産化する流動性危機、リーマン期はドル不足型危機、コロナ期は金融政策主導の市場として、それぞれ異なる相関構造が見えました。

このように、危機ごとの相関構造を比較することで、単純な価格変動だけでは見えにくい市場の状態変化を把握できます。

次はrolling相関やボラティリティ指標を追加し、危機検知に使える特徴量へ発展させていきます。

コメント

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