forked from mnbvc188199/Warrior_EA
Same harness as the D1 screens, forward 30 H4 bars (~5 days), 199 perms, on the surviving htf mid bars (19.8k-36.4k bars per symbol). Question: does the wired alt/volume information carry to H4, for the chart-timeframe decision. Answer: the signal survives but is roughly halved, and the cost side worsens 2.6x per step down. SP500 vix_chg5 clears the family bar with MI|vol 0.0112 (vs 0.031 at D1); volLevel50 (the EA activity feature) is incremental on all four symbols at H4 - USDJPY family-clean, and it is EURUSD strongest non-control signal there too. Gold gvz_chg5 stays incremental (0.0038 vs 0.0197 at D1 - a fifth of the strength). COT is null at H4 on FX (weekly cadence pasted across 30 bars/week dilutes it below detection on EURUSD/JPY; survives conditionally on SP500). Cost table (median spread/ATR; the 1.74xATR geometry in spread units): SP500 138->52->25, EURUSD 282->113->57, USDJPY 230->87->44, XAUUSD 92->34->16 for D1->H4->H1. Every step down multiplies the cost share ~2.6x. Also fixes the disaggregated-COT column name for gold/WTI in the screen (M_Money vs Lev_Money - the same crash the EA-side catalog documents). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
142 lines
6.2 KiB
Python
142 lines
6.2 KiB
Python
"""Does the wired alt-data + volume information survive a move from D1 to H4?
|
|
|
|
The user is choosing a chart timeframe. The D1 screens are the evidence for
|
|
D1; this is the SAME test at H4 - forward range over 30 H4 bars (~5 trading
|
|
days, comparable horizon), trailing-range positive control, noise negative
|
|
control, circular-shift permutation p-values, and the conditional MI|vol
|
|
column that decides incrementality.
|
|
|
|
Also reports the COST side of the timeframe question: ATR in spread units at
|
|
each timeframe, because EV per trade = edge x width, and width measured in
|
|
spreads is what shrinks when you shorten the timeframe.
|
|
|
|
Usage:
|
|
python -m altdata.screen_tf [--tf H4] [--perms 199]
|
|
"""
|
|
import argparse
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from .common import DATA_ROOT
|
|
from .screen import _tercile, cmi_3x3x3, mi_3x3
|
|
|
|
HTF = DATA_ROOT.parent / "htf"
|
|
|
|
# per-symbol candidate map: (name, source csv, transform)
|
|
FRED = DATA_ROOT / "fred"
|
|
|
|
|
|
def load_tf(sym: str, tf: str) -> pd.DataFrame:
|
|
z = np.load(HTF / f"{sym}_{tf}_mid.npz")
|
|
return pd.DataFrame({
|
|
"time": pd.to_datetime(z["t"], unit="ms"),
|
|
"open": z["o"], "high": z["h"], "low": z["l"], "close": z["c"],
|
|
"volume": z["v"], "spread": z["spread"],
|
|
})
|
|
|
|
|
|
def fred_series(sid: str) -> pd.DataFrame:
|
|
return pd.read_csv(FRED / f"{sid}.csv", parse_dates=["observed", "published"])
|
|
|
|
|
|
def asof(bars: pd.DataFrame, pub: pd.Series, val: pd.Series, name: str) -> np.ndarray:
|
|
src = pd.DataFrame({"published": pub.astype("datetime64[ns]"), name: val}) \
|
|
.sort_values("published").dropna()
|
|
j = pd.merge_asof(bars[["time"]].astype({"time": "datetime64[ns]"}), src,
|
|
left_on="time", right_on="published", direction="backward")
|
|
return j[name].to_numpy()
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--sym", nargs="*", default=["SP500", "EURUSD", "USDJPY", "XAUUSD"])
|
|
ap.add_argument("--tf", default="H4")
|
|
ap.add_argument("--fwd", type=int, default=30) # ~5 trading days of H4 bars
|
|
ap.add_argument("--perms", type=int, default=199)
|
|
ap.add_argument("--seed", type=int, default=23)
|
|
args = ap.parse_args()
|
|
rng = np.random.default_rng(args.seed)
|
|
|
|
for sym in args.sym:
|
|
bars = load_tf(sym, args.tf)
|
|
prev_close = bars["close"].shift(1)
|
|
tr = pd.concat([bars["high"] - bars["low"],
|
|
(bars["high"] - prev_close).abs(),
|
|
(bars["low"] - prev_close).abs()], axis=1).max(axis=1)
|
|
atr = tr.ewm(alpha=1 / 14, min_periods=14).mean()
|
|
f = args.fwd
|
|
y_raw = tr.shift(-1).rolling(f).sum().shift(-(f - 1)) / atr
|
|
trailing = tr.rolling(f).sum() / atr
|
|
|
|
feats = pd.DataFrame(index=bars.index)
|
|
vix = fred_series("VIXCLS")
|
|
feats["vix_chg5"] = asof(bars, vix["published"],
|
|
vix["value"] - vix["value"].shift(5), "vix_chg5")
|
|
if sym == "XAUUSD":
|
|
gvz = fred_series("GVZCLS")
|
|
feats["gvz_chg5"] = asof(bars, gvz["published"],
|
|
gvz["value"] - gvz["value"].shift(5), "gvz_chg5")
|
|
usd = fred_series("DTWEXBGS")
|
|
feats["usd_chg5"] = asof(bars, usd["published"],
|
|
usd["value"] - usd["value"].shift(5), "usd_chg5")
|
|
cotp = DATA_ROOT / "cot" / f"{sym}_cot.csv"
|
|
if cotp.exists():
|
|
c = pd.read_csv(cotp, parse_dates=["observed", "published"])
|
|
oi = c["Open_Interest_All"].replace(0, np.nan)
|
|
# TFF family carries Lev_Money; the disaggregated family (gold, WTI)
|
|
# carries M_Money - same economic role (the speculators)
|
|
if "Lev_Money_Positions_Long_All" in c.columns:
|
|
spec = (c["Lev_Money_Positions_Long_All"]
|
|
- c["Lev_Money_Positions_Short_All"]) / oi
|
|
else:
|
|
spec = (c["M_Money_Positions_Long_All"]
|
|
- c["M_Money_Positions_Short_All"]) / oi
|
|
if sym == "USDJPY":
|
|
spec = -spec
|
|
feats["cot_spec_net"] = asof(bars, c["published"], spec, "cot_spec_net")
|
|
#--- the EA's activity feature at this timeframe (tick volume vs 50-bar mean)
|
|
feats["volLevel50"] = bars["volume"] / bars["volume"].rolling(50).mean() \
|
|
.replace(0, np.nan)
|
|
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())
|
|
spr_atr = (bars["spread"] / atr).median()
|
|
print(f"\n=== {sym} {args.tf}: {int(valid.sum())} bars, fwd {f} bars, "
|
|
f"{args.perms} perms | median spread = {spr_atr:.4f} x ATR({args.tf}) ===")
|
|
|
|
results, null_max = [], np.zeros(args.perms)
|
|
for col in feats.columns:
|
|
x = feats.loc[valid, col].to_numpy(dtype=float)
|
|
ok = ~np.isnan(x)
|
|
if ok.sum() < 2000:
|
|
continue
|
|
xo, yo, zo = x[ok], y_bin[ok], z_bin[ok]
|
|
n = len(xo)
|
|
obs, obs_c = mi_3x3(xo, yo), cmi_3x3x3(xo, yo, zo)
|
|
null = np.empty(args.perms)
|
|
null_c = np.empty(args.perms)
|
|
#--- shift by whole ~weeks of H4 bars so the null respects serial dependence
|
|
for k in range(args.perms):
|
|
xs = np.roll(xo, rng.integers(63 * 6, n - 63 * 6))
|
|
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((col, obs, p, obs_c, p_c))
|
|
if not col.startswith("CTRL_"):
|
|
null_max = np.maximum(null_max, null)
|
|
fam = np.quantile(null_max, 0.95)
|
|
print(f" family-wise 5% bar: {fam:.5f}")
|
|
for col, obs, p, obs_c, p_c in sorted(results, key=lambda r: -r[3]):
|
|
mark = "FAM" if obs > fam else " "
|
|
inc = "INCR" if p_c <= 0.05 else " "
|
|
print(f" {col:22s} MI {obs:.5f} p={p:.4f} {mark} | "
|
|
f"MI|vol {obs_c:.5f} p={p_c:.4f} {inc}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|