| 市場 | 取引時間(日本時間) |
|---|---|
| 東証 | 9:00 〜 15:30(同日) |
| 米国市場 | 22:30 〜 翌朝 5:00 |
米国が引けるのは日本の翌朝です。つまりSOXのt日の終値は、日本のt+1日にしか反映されません。同じ日付で並べて回帰すると、まだ起きていない米国の値動きで日本株を説明していることになります。
これをやると相関が異常に高く出ます。予測に使えば当然勝てますが、実際にはその情報を持っていません。この記事の元になったコードは、まさにこの状態でした。
import numpy as np
import pandas as pd
import yfinance as yf
def load_close(ticker: str, start: str = "2015-01-01") -> pd.Series:
df = yf.download(ticker, start=start, auto_adjust=True, progress=False)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
s = df["Close"].dropna()
s.index = pd.to_datetime(s.index).tz_localize(None).normalize()
return s
def check_lag(jp: pd.Series, us: pd.Series) -> None:
"""何日ずらすと相関が最大になるかを確認する。"""
rj, ru = jp.pct_change(), us.pct_change()
both = pd.concat([rj.rename("jp"), ru.rename("us")], axis=1).dropna()
for lag in range(-2, 3):
c = both["jp"].corr(both["us"].shift(lag))
mark = " ← 最大になるはず" if lag == 1 else ""
print(f"US を {lag:+d}日ずらす: 相関 = {c:.4f}{mark}")
jp = load_close("6857.T") # アドバンテスト
sox = load_close("^SOX") # フィラデルフィア半導体指数
check_lag(jp, sox)
実行すると、lag=1(USを1日遅らせる)で相関が最大になります。lag=0でも相関はそこそこ出ますが、それは「同じ日の東京の値動きが、その夜の米国に影響している」という逆方向の因果を拾っているだけです。
この確認は、市場をまたぐ分析をするときは毎回やってください。ラグがゼロで最大になったら、データの日付処理を間違えています。
イベントスタディを正しく実装する
手順は4段階です。
- イベント日を機械的に定義する(SOXが-3%以上下落した日)
- イベント前の推定期間でマーケットモデル(
stock = α + β × market)を推定する - イベント前後で異常リターン(AR)を計算する
- ARを積み上げた累積異常リターン(CAR)にt検定をかける
import statsmodels.api as sm
from scipy import stats
def find_events(us: pd.Series, thresh: float = -0.03) -> pd.DatetimeIndex:
"""SOXが大きく動いた日を機械的に抽出する。"""
r = us.pct_change()
return pd.DatetimeIndex(r[r <= thresh].index)
def event_study(stock: pd.Series, market: pd.Series,
events: pd.DatetimeIndex,
est_len: int = 120, gap: int = 10,
pre: int = 5, post: int = 10) -> dict:
"""米国イベントのt日を、日本のt+1日に対応させて分析する。"""
ret = pd.DataFrame({
"stock": stock.pct_change(),
"market": market.pct_change(),
}).dropna()
dates = ret.index
ars, cars, betas, used = [], [], [], []
for ev in events:
# 米国のイベント日以降で、最初に開いた日本の営業日
after = dates[dates > ev]
if len(after) == 0:
continue
ev_jp = after[0]
i = dates.get_loc(ev_jp)
lo = i - gap - est_len
if lo < 0 or i + post >= len(dates):
continue
est = ret.iloc[lo:i - gap]
X = sm.add_constant(est["market"])
fit = sm.OLS(est["stock"], X).fit()
a, b = fit.params["const"], fit.params["market"]
sigma = float(fit.resid.std()) # AR の標準偏差
win = ret.iloc[i - pre:i + post + 1]
ar = (win["stock"] - (a + b * win["market"])).to_numpy()
if len(ar) != pre + post + 1:
continue
ars.append(ar / sigma) # 標準化してから平均する
cars.append(np.cumsum(ar))
betas.append(b)
used.append(ev_jp)
if not ars:
return {}
ar_mat = np.vstack(ars) # 標準化AR
car_mat = np.vstack(cars)
n = len(ars)
offsets = np.arange(-pre, post + 1)
# 各相対日の平均標準化ARに対するt値
t_ar = ar_mat.mean(axis=0) * np.sqrt(n)
table = pd.DataFrame({
"相対日": offsets,
"平均AR%": (np.vstack([np.diff(np.r_[0, c]) for c in car_mat])
.mean(axis=0) * 100).round(3),
"平均CAR%": (car_mat.mean(axis=0) * 100).round(3),
"t値": t_ar.round(2),
"prob": (2 * (1 - stats.norm.cdf(np.abs(t_ar)))).round(4),
}).set_index("相対日")
return {"table": table, "n": n,
"平均ベータ": round(float(np.mean(betas)), 3),
"イベント日": used}
ポイントは3つあります。
dates[dates > ev][0]で日本の翌営業日に対応させる。祝日でも正しくずれます- 推定期間とイベントの間に
gap日を空ける。イベント直前の異常な値動きでベータが歪むのを防ぎます - ARを残差の標準偏差で割ってから平均する。ボラティリティの高い時期のイベントが結果を支配するのを防ぎます
実行して結果を見る
TARGETS = {
"6857.T": "アドバンテスト",
"6146.T": "ディスコ",
"6920.T": "レーザーテック",
"8035.T": "東京エレクトロン",
"7203.T": "トヨタ(対照群)",
}
sox = load_close("^SOX")
topix = load_close("1306.T")
events = find_events(sox, -0.03)
print(f"SOXが-3%以上下落した日: {len(events)}回")
for t, name in TARGETS.items():
res = event_study(load_close(t), topix, events)
if not res:
continue
print(f"\n=== {name} n={res['n']} β={res['平均ベータ']} ===")
print(res["table"].loc[-2:5])
結果ははっきりしています。相対日0(=日本の翌営業日)に大きなマイナスのARが出て、t値が明確に有意になります。そして相対日1以降のARはほぼゼロで、t値も有意ではありません。
対照群のトヨタを入れているのが大事です。トヨタでもARが出るなら、それは半導体固有ではなくベンチマークの選び方の問題です。実際にはトヨタのARは小さく、半導体株との差がセクター固有の反応だと確認できます。
イベントの重複を処理する
ここに落とし穴があります。SOXの急落は連日で起きることが多いので、イベントウィンドウが重なります。重なったイベントを独立サンプルとして扱うと、t値が過大に出ます。
def deduplicate(events: pd.DatetimeIndex,
min_gap: int = 15) -> pd.DatetimeIndex:
"""min_gap日以内に続くイベントは最初の1つだけ残す。"""
keep, last = [], None
for d in sorted(events):
if last is None or (d - last).days >= min_gap:
keep.append(d)
last = d
return pd.DatetimeIndex(keep)
clean = deduplicate(events, min_gap=15)
print(f"{len(events)}件 → {len(clean)}件(重複除去後)")
res_all = event_study(load_close("6857.T"), topix, events)
res_clean = event_study(load_close("6857.T"), topix, clean)
print(f"重複あり: n={res_all['n']} "
f"day0のt値={res_all['table'].loc[0, 't値']}")
print(f"重複除去: n={res_clean['n']} "
f"day0のt値={res_clean['table'].loc[0, 't値']}")
サンプル数が減るのでt値も下がりますが、下がったほうが正しい数字です。それでもday0の有意性は残ります。この波及は本物です。
で、トレードに使えるのか
ここがいちばん知りたかったところです。「SOXが下がったら翌日に日本の半導体株が下がる」と分かっても、その情報で儲けられるかは別問題です。
翌日のリターンを「寄り付きのギャップ」と「ザラ場の値動き」に分解します。
def decompose(ticker: str, events: pd.DatetimeIndex) -> None:
df = yf.download(ticker, start="2015-01-01", auto_adjust=True,
progress=False)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
df.index = pd.to_datetime(df.index).tz_localize(None).normalize()
gap = (df["Open"] / df["Close"].shift(1) - 1) # 寄り付きギャップ
intra = (df["Close"] / df["Open"] - 1) # ザラ場
total = (df["Close"] / df["Close"].shift(1) - 1)
rows = []
for ev in events:
after = df.index[df.index > ev]
if len(after) == 0:
continue
d = after[0]
rows.append({"gap": gap.get(d, np.nan),
"intra": intra.get(d, np.nan),
"total": total.get(d, np.nan)})
t = pd.DataFrame(rows).dropna()
print(f"n={len(t)}")
for col, label in [("gap", "寄り付きギャップ"),
("intra", "ザラ場"), ("total", "1日合計")]:
m = t[col].mean() * 100
tt, pv = stats.ttest_1samp(t[col], 0.0)
print(f"{label:12s} 平均={m:+.3f}% t={tt:+.2f} p={pv:.4f}")
print(f"ギャップが1日合計に占める割合: "
f"{t['gap'].mean() / t['total'].mean() * 100:.0f}%")
decompose("6857.T", deduplicate(events))
これを実行すると、下落のほとんどが寄り付きのギャップで完了していることが分かります。ザラ場の平均リターンはほぼゼロで、統計的にも有意ではありません。
| 取れる部分 | 実際に取れるか |
|---|---|
| 寄り付きギャップ | 取れない。前日のうちに売る必要がある |
| ザラ場 | 取れるが、平均はほぼゼロ |
つまり「SOXが下がったから寄り付きで売る」では遅いということです。市場は効率的で、寄り付きの気配値の時点でSOXの下落は織り込まれています。
これは残念な結論に見えますが、実は有用です。この分析が本当に役立つのは、シグナルではなくリスク管理のほうでした。
使い道はポジションサイズの決定
def sox_sensitivity(tickers: dict, sox: pd.Series,
lookback: int = 250) -> pd.DataFrame:
"""SOX前日リターンに対する感応度を銘柄ごとに測る。"""
rs = sox.pct_change().shift(1) # 前日のSOX(これが使える情報)
rows = []
for t, name in tickers.items():
r = load_close(t).pct_change()
both = pd.concat([r.rename("y"), rs.rename("x")], axis=1).dropna()
both = both.iloc[-lookback:]
fit = sm.OLS(both["y"], sm.add_constant(both["x"])).fit()
rows.append({
"銘柄": name,
"SOX感応度": round(fit.params["x"], 2),
"t値": round(fit.tvalues["x"], 1),
"決定係数": round(fit.rsquared, 3),
"年率ボラ%": round(both["y"].std() * np.sqrt(252) * 100, 1),
})
return pd.DataFrame(rows).set_index("銘柄").sort_values(
"SOX感応度", ascending=False)
print(sox_sensitivity(TARGETS, sox))
感応度が1.5を超える銘柄と、0.3程度の銘柄が同じポートフォリオに入っていると、SOXが5%下げた日の含み損はまったく違います。感応度で割ってポジションサイズを揃えるだけで、1日の変動幅がかなり均される感覚がありました。
また、「半導体株を4銘柄持って分散している」というのが実は分散になっていないことも数字で見えます。全部が同じSOXという1つの因子で動いているなら、銘柄数を増やしてもリスクは減りません。
まとめ
- yfinanceの複数銘柄取得は辞書順で返る。列名で明示的に取り出す
- 市場をまたぐ分析ではラグを確認する。
lag=0で相関が最大なら日付処理が誤り - 米国のt日は日本のt+1日。同日で並べると未来参照になる
- イベントスタディは複数イベント×複数銘柄で平均し、対照群を置く
- ARは標準化してから平均し、重複イベントは除去する
- 波及は本物だが寄り付きギャップで完了している。シグナルには使えない
- 使い道は感応度に応じたポジションサイズの調整
体感で「SOXが下がると半導体装置株もつられて下がる」と思っていたのは、数字で見ても正しかったです。ただ「だから前日のSOXを見て翌朝に売ればいい」は成り立ちません。そこまで確かめて初めて、体感を数字にした意味がありました。
次は、SOXそのものではなくSOX構成銘柄の決算発表をイベントにして、どの企業の決算が日本の装置株にいちばん効くのかを見てみようと思っています。
「SOX指数が弱気相場入り」というニュースを見て、正直ビビっていました。ディスコやアドバンテストのような半導体製造装置株をポートフォリオの端に持っているので、SOXが下がるたびに「これ、自分の持ち株にどれくらい影響が出るんだろう」と体感でしか判断していなかったんです。
そこでイベントスタディ分析という手法を使って数字で確かめてみたのですが、最初に書いたコードには3つのバグがあり、しかもそのうち1つは分析の前提を根本から壊すものでした。
結論から言うと、SOXの急落は日本の半導体株に確実に波及します。ただし波及するのは「翌日」で、しかもその大半は寄り付きのギャップで終わっています。これが分かると、この分析の使い道もはっきりします。
最初のコードのバグ
バグ1:列が入れ替わっていた
# ❌ yfinanceは列をティッカーの辞書順で返す
df = yf.download(["6857.T", "1306.T"], ...)["Close"]
ret.columns = ["stock", "market"] # 1306.T が stock になっている!
"1306.T" < "6857.T"なので、返ってくる列の順番は指定した順ではありません。個別株とベンチマークが逆になったまま回帰していました。ベータが1.4と出て「ハイベータだ」と納得していたのですが、実際に測っていたのはTOPIX ETFのアドバンテストに対するベータでした。
# ✅ 列名で明示的に取り出す
px = yf.download(["6857.T", "1306.T"], ...)["Close"]
ret = pd.DataFrame({
"stock": px["6857.T"].pct_change(),
"market": px["1306.T"].pct_change(),
}).dropna()
バグ2:推定期間が足りていなかった
start="2026-04-01"で取ったデータから「イベント日の120営業日前」を切り出そうとしていました。3ヶ月ぶんしかないので、そもそも120日前が存在しません。.iloc[-131:-11]はエラーを出さずに短い期間で静かに回帰していました。
バグ3:イベントウィンドウが空だった
window = window.iloc[-5:6] # 11行なら slice(6, 6) = 空
負のインデックスと正のインデックスを混ぜたスライスです。意図した「前後5営業日」にはまったくなりません。イベント日の位置を求めてから相対位置で切り出すべきでした。
いちばん重要な問題:時差
バグを直しても、まだ根本的な問題が残ります。SOX指数が6月24日に急落したとして、その情報を日本市場は6月24日には知りません。
| 市場 | 取引時間(日本時間) |
|---|---|
| 東証 | 9:00 〜 15:30(同日) |
| 米国市場 | 22:30 〜 翌朝 5:00 |
米国が引けるのは日本の翌朝です。つまりSOXのt日の終値は、日本のt+1日にしか反映されません。同じ日付で並べて回帰すると、まだ起きていない米国の値動きで日本株を説明していることになります。
これをやると相関が異常に高く出ます。予測に使えば当然勝てますが、実際にはその情報を持っていません。この記事の元になったコードは、まさにこの状態でした。
import numpy as np
import pandas as pd
import yfinance as yf
def load_close(ticker: str, start: str = "2015-01-01") -> pd.Series:
df = yf.download(ticker, start=start, auto_adjust=True, progress=False)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
s = df["Close"].dropna()
s.index = pd.to_datetime(s.index).tz_localize(None).normalize()
return s
def check_lag(jp: pd.Series, us: pd.Series) -> None:
"""何日ずらすと相関が最大になるかを確認する。"""
rj, ru = jp.pct_change(), us.pct_change()
both = pd.concat([rj.rename("jp"), ru.rename("us")], axis=1).dropna()
for lag in range(-2, 3):
c = both["jp"].corr(both["us"].shift(lag))
mark = " ← 最大になるはず" if lag == 1 else ""
print(f"US を {lag:+d}日ずらす: 相関 = {c:.4f}{mark}")
jp = load_close("6857.T") # アドバンテスト
sox = load_close("^SOX") # フィラデルフィア半導体指数
check_lag(jp, sox)
実行すると、lag=1(USを1日遅らせる)で相関が最大になります。lag=0でも相関はそこそこ出ますが、それは「同じ日の東京の値動きが、その夜の米国に影響している」という逆方向の因果を拾っているだけです。
この確認は、市場をまたぐ分析をするときは毎回やってください。ラグがゼロで最大になったら、データの日付処理を間違えています。
イベントスタディを正しく実装する
手順は4段階です。
- イベント日を機械的に定義する(SOXが-3%以上下落した日)
- イベント前の推定期間でマーケットモデル(
stock = α + β × market)を推定する - イベント前後で異常リターン(AR)を計算する
- ARを積み上げた累積異常リターン(CAR)にt検定をかける
import statsmodels.api as sm
from scipy import stats
def find_events(us: pd.Series, thresh: float = -0.03) -> pd.DatetimeIndex:
"""SOXが大きく動いた日を機械的に抽出する。"""
r = us.pct_change()
return pd.DatetimeIndex(r[r <= thresh].index)
def event_study(stock: pd.Series, market: pd.Series,
events: pd.DatetimeIndex,
est_len: int = 120, gap: int = 10,
pre: int = 5, post: int = 10) -> dict:
"""米国イベントのt日を、日本のt+1日に対応させて分析する。"""
ret = pd.DataFrame({
"stock": stock.pct_change(),
"market": market.pct_change(),
}).dropna()
dates = ret.index
ars, cars, betas, used = [], [], [], []
for ev in events:
# 米国のイベント日以降で、最初に開いた日本の営業日
after = dates[dates > ev]
if len(after) == 0:
continue
ev_jp = after[0]
i = dates.get_loc(ev_jp)
lo = i - gap - est_len
if lo < 0 or i + post >= len(dates):
continue
est = ret.iloc[lo:i - gap]
X = sm.add_constant(est["market"])
fit = sm.OLS(est["stock"], X).fit()
a, b = fit.params["const"], fit.params["market"]
sigma = float(fit.resid.std()) # AR の標準偏差
win = ret.iloc[i - pre:i + post + 1]
ar = (win["stock"] - (a + b * win["market"])).to_numpy()
if len(ar) != pre + post + 1:
continue
ars.append(ar / sigma) # 標準化してから平均する
cars.append(np.cumsum(ar))
betas.append(b)
used.append(ev_jp)
if not ars:
return {}
ar_mat = np.vstack(ars) # 標準化AR
car_mat = np.vstack(cars)
n = len(ars)
offsets = np.arange(-pre, post + 1)
# 各相対日の平均標準化ARに対するt値
t_ar = ar_mat.mean(axis=0) * np.sqrt(n)
table = pd.DataFrame({
"相対日": offsets,
"平均AR%": (np.vstack([np.diff(np.r_[0, c]) for c in car_mat])
.mean(axis=0) * 100).round(3),
"平均CAR%": (car_mat.mean(axis=0) * 100).round(3),
"t値": t_ar.round(2),
"prob": (2 * (1 - stats.norm.cdf(np.abs(t_ar)))).round(4),
}).set_index("相対日")
return {"table": table, "n": n,
"平均ベータ": round(float(np.mean(betas)), 3),
"イベント日": used}
ポイントは3つあります。
dates[dates > ev][0]で日本の翌営業日に対応させる。祝日でも正しくずれます- 推定期間とイベントの間に
gap日を空ける。イベント直前の異常な値動きでベータが歪むのを防ぎます - ARを残差の標準偏差で割ってから平均する。ボラティリティの高い時期のイベントが結果を支配するのを防ぎます
実行して結果を見る
TARGETS = {
"6857.T": "アドバンテスト",
"6146.T": "ディスコ",
"6920.T": "レーザーテック",
"8035.T": "東京エレクトロン",
"7203.T": "トヨタ(対照群)",
}
sox = load_close("^SOX")
topix = load_close("1306.T")
events = find_events(sox, -0.03)
print(f"SOXが-3%以上下落した日: {len(events)}回")
for t, name in TARGETS.items():
res = event_study(load_close(t), topix, events)
if not res:
continue
print(f"\n=== {name} n={res['n']} β={res['平均ベータ']} ===")
print(res["table"].loc[-2:5])
結果ははっきりしています。相対日0(=日本の翌営業日)に大きなマイナスのARが出て、t値が明確に有意になります。そして相対日1以降のARはほぼゼロで、t値も有意ではありません。
対照群のトヨタを入れているのが大事です。トヨタでもARが出るなら、それは半導体固有ではなくベンチマークの選び方の問題です。実際にはトヨタのARは小さく、半導体株との差がセクター固有の反応だと確認できます。
イベントの重複を処理する
ここに落とし穴があります。SOXの急落は連日で起きることが多いので、イベントウィンドウが重なります。重なったイベントを独立サンプルとして扱うと、t値が過大に出ます。
def deduplicate(events: pd.DatetimeIndex,
min_gap: int = 15) -> pd.DatetimeIndex:
"""min_gap日以内に続くイベントは最初の1つだけ残す。"""
keep, last = [], None
for d in sorted(events):
if last is None or (d - last).days >= min_gap:
keep.append(d)
last = d
return pd.DatetimeIndex(keep)
clean = deduplicate(events, min_gap=15)
print(f"{len(events)}件 → {len(clean)}件(重複除去後)")
res_all = event_study(load_close("6857.T"), topix, events)
res_clean = event_study(load_close("6857.T"), topix, clean)
print(f"重複あり: n={res_all['n']} "
f"day0のt値={res_all['table'].loc[0, 't値']}")
print(f"重複除去: n={res_clean['n']} "
f"day0のt値={res_clean['table'].loc[0, 't値']}")
サンプル数が減るのでt値も下がりますが、下がったほうが正しい数字です。それでもday0の有意性は残ります。この波及は本物です。
で、トレードに使えるのか
ここがいちばん知りたかったところです。「SOXが下がったら翌日に日本の半導体株が下がる」と分かっても、その情報で儲けられるかは別問題です。
翌日のリターンを「寄り付きのギャップ」と「ザラ場の値動き」に分解します。
def decompose(ticker: str, events: pd.DatetimeIndex) -> None:
df = yf.download(ticker, start="2015-01-01", auto_adjust=True,
progress=False)
if isinstance(df.columns, pd.MultiIndex):
df.columns = df.columns.get_level_values(0)
df.index = pd.to_datetime(df.index).tz_localize(None).normalize()
gap = (df["Open"] / df["Close"].shift(1) - 1) # 寄り付きギャップ
intra = (df["Close"] / df["Open"] - 1) # ザラ場
total = (df["Close"] / df["Close"].shift(1) - 1)
rows = []
for ev in events:
after = df.index[df.index > ev]
if len(after) == 0:
continue
d = after[0]
rows.append({"gap": gap.get(d, np.nan),
"intra": intra.get(d, np.nan),
"total": total.get(d, np.nan)})
t = pd.DataFrame(rows).dropna()
print(f"n={len(t)}")
for col, label in [("gap", "寄り付きギャップ"),
("intra", "ザラ場"), ("total", "1日合計")]:
m = t[col].mean() * 100
tt, pv = stats.ttest_1samp(t[col], 0.0)
print(f"{label:12s} 平均={m:+.3f}% t={tt:+.2f} p={pv:.4f}")
print(f"ギャップが1日合計に占める割合: "
f"{t['gap'].mean() / t['total'].mean() * 100:.0f}%")
decompose("6857.T", deduplicate(events))
これを実行すると、下落のほとんどが寄り付きのギャップで完了していることが分かります。ザラ場の平均リターンはほぼゼロで、統計的にも有意ではありません。
| 取れる部分 | 実際に取れるか |
|---|---|
| 寄り付きギャップ | 取れない。前日のうちに売る必要がある |
| ザラ場 | 取れるが、平均はほぼゼロ |
つまり「SOXが下がったから寄り付きで売る」では遅いということです。市場は効率的で、寄り付きの気配値の時点でSOXの下落は織り込まれています。
これは残念な結論に見えますが、実は有用です。この分析が本当に役立つのは、シグナルではなくリスク管理のほうでした。
使い道はポジションサイズの決定
def sox_sensitivity(tickers: dict, sox: pd.Series,
lookback: int = 250) -> pd.DataFrame:
"""SOX前日リターンに対する感応度を銘柄ごとに測る。"""
rs = sox.pct_change().shift(1) # 前日のSOX(これが使える情報)
rows = []
for t, name in tickers.items():
r = load_close(t).pct_change()
both = pd.concat([r.rename("y"), rs.rename("x")], axis=1).dropna()
both = both.iloc[-lookback:]
fit = sm.OLS(both["y"], sm.add_constant(both["x"])).fit()
rows.append({
"銘柄": name,
"SOX感応度": round(fit.params["x"], 2),
"t値": round(fit.tvalues["x"], 1),
"決定係数": round(fit.rsquared, 3),
"年率ボラ%": round(both["y"].std() * np.sqrt(252) * 100, 1),
})
return pd.DataFrame(rows).set_index("銘柄").sort_values(
"SOX感応度", ascending=False)
print(sox_sensitivity(TARGETS, sox))
感応度が1.5を超える銘柄と、0.3程度の銘柄が同じポートフォリオに入っていると、SOXが5%下げた日の含み損はまったく違います。感応度で割ってポジションサイズを揃えるだけで、1日の変動幅がかなり均される感覚がありました。
また、「半導体株を4銘柄持って分散している」というのが実は分散になっていないことも数字で見えます。全部が同じSOXという1つの因子で動いているなら、銘柄数を増やしてもリスクは減りません。
まとめ
- yfinanceの複数銘柄取得は辞書順で返る。列名で明示的に取り出す
- 市場をまたぐ分析ではラグを確認する。
lag=0で相関が最大なら日付処理が誤り - 米国のt日は日本のt+1日。同日で並べると未来参照になる
- イベントスタディは複数イベント×複数銘柄で平均し、対照群を置く
- ARは標準化してから平均し、重複イベントは除去する
- 波及は本物だが寄り付きギャップで完了している。シグナルには使えない
- 使い道は感応度に応じたポジションサイズの調整
体感で「SOXが下がると半導体装置株もつられて下がる」と思っていたのは、数字で見ても正しかったです。ただ「だから前日のSOXを見て翌朝に売ればいい」は成り立ちません。そこまで確かめて初めて、体感を数字にした意味がありました。
次は、SOXそのものではなくSOX構成銘柄の決算発表をイベントにして、どの企業の決算が日本の装置株にいちばん効くのかを見てみようと思っています。

