forked from mnbvc188199/Warrior_EA
126 lines
5.1 KiB
Python
126 lines
5.1 KiB
Python
|
|
"""Macro screen: rates, curve, inflation expectations, policy differentials,
|
||
|
|
and - to settle the question rather than assert it - monthly country statistics.
|
||
|
|
|
||
|
|
THE POWER POINT, made concretely instead of rhetorically. A feature joined to
|
||
|
|
D1 bars as-of its publication is a STEP FUNCTION: monthly unemployment takes
|
||
|
|
~200 distinct values across a 16-year sample no matter how many bars it is
|
||
|
|
pasted onto. The `distinct` column below is the honest sample size, and it is
|
||
|
|
what decides whether a feature can clear a permutation gate at all. Daily rate
|
||
|
|
series do not have this problem; monthly country statistics do, badly.
|
||
|
|
|
||
|
|
Vintage discipline: every series here comes through fred.py's ALFRED first-print
|
||
|
|
path where an archive exists (so revised series are joined at what was actually
|
||
|
|
knowable), and the unrevised path with published = observed + 1 day otherwise.
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
python -m altdata.screen_macro [--perms 499]
|
||
|
|
"""
|
||
|
|
import argparse
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import pandas as pd
|
||
|
|
|
||
|
|
from .common import DATA_ROOT
|
||
|
|
from .screen import _tercile, asof_join, cmi_3x3x3, load_bars, mi_3x3, true_range
|
||
|
|
|
||
|
|
|
||
|
|
def series(sid: str) -> pd.DataFrame:
|
||
|
|
return pd.read_csv(DATA_ROOT / "fred" / f"{sid}.csv",
|
||
|
|
parse_dates=["observed", "published"])
|
||
|
|
|
||
|
|
|
||
|
|
def build(bars: pd.DataFrame) -> pd.DataFrame:
|
||
|
|
"""One column per candidate, as-of joined onto the bar grid."""
|
||
|
|
out = pd.DataFrame(index=bars.index)
|
||
|
|
|
||
|
|
def col(sid: str, name: str, kind: str, k: int = 5):
|
||
|
|
f = series(sid)
|
||
|
|
v = f["value"]
|
||
|
|
if kind == "level":
|
||
|
|
val = v
|
||
|
|
elif kind == "diff":
|
||
|
|
val = v - v.shift(k)
|
||
|
|
elif kind == "yoy":
|
||
|
|
val = v / v.shift(k) - 1.0
|
||
|
|
frame = pd.DataFrame({"published": f["published"], name: val})
|
||
|
|
out[name] = asof_join(bars, frame)[name].to_numpy()
|
||
|
|
|
||
|
|
# ---- daily: rates, curve, inflation expectations
|
||
|
|
col("DGS10", "dgs10", "level")
|
||
|
|
col("DGS10", "dgs10_chg5", "diff", 5)
|
||
|
|
col("DGS10", "dgs10_chg20", "diff", 20)
|
||
|
|
col("DGS2", "dgs2_chg5", "diff", 5)
|
||
|
|
col("T10Y2Y", "curve", "level")
|
||
|
|
col("T10Y2Y", "curve_chg20", "diff", 20)
|
||
|
|
col("T5YIE", "bei5_chg20", "diff", 20)
|
||
|
|
col("DFF", "dff_chg20", "diff", 20)
|
||
|
|
# ---- policy differential (each leg joined independently, then differenced)
|
||
|
|
col("DFF", "_dff", "level")
|
||
|
|
col("ECBDFR", "_ecb", "level")
|
||
|
|
out["polidiff"] = out["_dff"] - out["_ecb"]
|
||
|
|
out["polidiff_chg20"] = out["polidiff"] - out["polidiff"].shift(20)
|
||
|
|
out.drop(columns=["_dff", "_ecb"], inplace=True)
|
||
|
|
# ---- monthly country statistics: the power demonstration
|
||
|
|
col("UNRATE", "unrate", "level")
|
||
|
|
col("UNRATE", "unrate_chg12", "diff", 12)
|
||
|
|
col("CPIAUCSL", "cpi_yoy", "yoy", 12)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> None:
|
||
|
|
ap = argparse.ArgumentParser()
|
||
|
|
ap.add_argument("--sym", nargs="*", default=["SP500", "EURUSD", "USDJPY", "XAUUSD"])
|
||
|
|
ap.add_argument("--perms", type=int, default=499)
|
||
|
|
ap.add_argument("--seed", type=int, default=17)
|
||
|
|
args = ap.parse_args()
|
||
|
|
rng = np.random.default_rng(args.seed)
|
||
|
|
|
||
|
|
for sym in args.sym:
|
||
|
|
bars = load_bars(sym)
|
||
|
|
tr = true_range(bars)
|
||
|
|
atr = tr.ewm(alpha=1 / 14, min_periods=14).mean()
|
||
|
|
trailing = tr.rolling(5).sum() / atr
|
||
|
|
y_raw = tr.shift(-1).rolling(5).sum().shift(-4) / atr
|
||
|
|
|
||
|
|
feats = build(bars)
|
||
|
|
feats["CTRL_trailing_range"] = trailing
|
||
|
|
feats["CTRL_noise"] = rng.standard_normal(len(bars))
|
||
|
|
|
||
|
|
valid = y_raw.notna() & trailing.notna()
|
||
|
|
y_bin = _tercile(y_raw[valid].to_numpy())
|
||
|
|
z_bin = _tercile(trailing[valid].to_numpy())
|
||
|
|
print(f"\n=== {sym}: {int(valid.sum())} D1 bars, forward-5d range, {args.perms} perms ===")
|
||
|
|
|
||
|
|
results, null_max = [], np.zeros(args.perms)
|
||
|
|
for c in feats.columns:
|
||
|
|
x = feats.loc[valid, c].to_numpy(dtype=float)
|
||
|
|
ok = ~np.isnan(x)
|
||
|
|
if ok.sum() < 500:
|
||
|
|
continue
|
||
|
|
xo, yo, zo = x[ok], y_bin[ok], z_bin[ok]
|
||
|
|
n = len(xo)
|
||
|
|
distinct = len(np.unique(xo))
|
||
|
|
obs, obs_c = mi_3x3(xo, yo), cmi_3x3x3(xo, yo, zo)
|
||
|
|
null = np.empty(args.perms)
|
||
|
|
null_c = np.empty(args.perms)
|
||
|
|
for k in range(args.perms):
|
||
|
|
xs = np.roll(xo, rng.integers(63, n - 63))
|
||
|
|
null[k] = mi_3x3(xs, yo)
|
||
|
|
null_c[k] = cmi_3x3x3(xs, yo, zo)
|
||
|
|
p = (np.sum(null >= obs) + 1) / (args.perms + 1)
|
||
|
|
p_c = (np.sum(null_c >= obs_c) + 1) / (args.perms + 1)
|
||
|
|
results.append((c, obs, p, obs_c, p_c, n, distinct))
|
||
|
|
if not c.startswith("CTRL_"):
|
||
|
|
null_max = np.maximum(null_max, null)
|
||
|
|
fam = np.quantile(null_max, 0.95)
|
||
|
|
print(f" family-wise 5% bar: {fam:.5f}")
|
||
|
|
print(f" {'feature':20s} {'n':>6s} {'distinct':>9s} MI / p | MI|vol / p")
|
||
|
|
for c, obs, p, obs_c, p_c, n, dis in sorted(results, key=lambda r: -r[3]):
|
||
|
|
mark = "FAM" if obs > fam else " "
|
||
|
|
inc = "INCR" if p_c <= 0.05 else " "
|
||
|
|
print(f" {c:20s} {n:6d} {dis:9d} {obs:.5f} p={p:.4f} {mark} | "
|
||
|
|
f"{obs_c:.5f} p={p_c:.4f} {inc}")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|