Files
resonance-engine/_univariate_horizon_sweep.py
T
2026-06-07 11:34:30 +07:00

143 lines
4.6 KiB
Python

"""_univariate_horizon_sweep.py
For each candidate variable, compute correlation with |fwd_return| and
sign(fwd_return) across a horizon band. No lattice, no Ridge, no z-score
rolling. Just raw Pearson and Spearman on cleaned BTC minute data.
The point: find the horizon BAND each variable lives in, not a single number.
"""
from __future__ import annotations
import glob
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import pearsonr, spearmanr
DATA_GLOB = "/mnt/d/PaperTrader/research/hl_data/minutes/202603*"
VARIABLES = [
"trade_count",
"taker_buy_usd",
"taker_sell_usd",
"large_print_cnt",
"wallet_entropy",
"signed_flow_usd", # for sanity / comparison
"vwap_drift", # for sanity / comparison
]
HORIZONS = [5, 15, 30, 60, 120, 240, 480, 720, 1440, 2880]
def load_btc() -> pd.DataFrame:
dfs = []
for d in sorted(glob.glob(DATA_GLOB)):
for f in sorted(glob.glob(f"{d}/*.parquet")):
try:
df = pd.read_parquet(f)
except Exception:
continue
if "coin" in df.columns:
df = df[df["coin"] == "BTC"]
if len(df):
dfs.append(df)
df = pd.concat(dfs, ignore_index=True)
df = df.sort_values("minute").drop_duplicates("minute").reset_index(drop=True)
return df
def add_forward_returns(df: pd.DataFrame) -> pd.DataFrame:
p = df["mid_price"].astype(float).values
for h in HORIZONS:
future = pd.Series(p).shift(-h).values
fwd = (future - p) / p
df[f"fwd_{h}"] = fwd
df[f"abs_fwd_{h}"] = np.abs(fwd)
return df
def corr_table(df: pd.DataFrame) -> pd.DataFrame:
"""For each (variable, horizon) compute Pearson and Spearman vs
|fwd_return| and sign(fwd_return). Only on rows where both are finite."""
rows = []
for v in VARIABLES:
if v not in df.columns:
print(f" WARN: {v} missing from data")
continue
x_full = df[v].astype(float)
for h in HORIZONS:
y_abs = df[f"abs_fwd_{h}"]
y_sgn = np.sign(df[f"fwd_{h}"])
mask = x_full.notna() & y_abs.notna() & np.isfinite(x_full) & np.isfinite(y_abs)
x = x_full[mask].values
ya = y_abs[mask].values
ys = y_sgn[mask].values
if len(x) < 1000:
continue
try:
p_abs, _ = pearsonr(x, ya)
s_abs, _ = spearmanr(x, ya)
p_sgn, _ = pearsonr(x, ys)
s_sgn, _ = spearmanr(x, ys)
except Exception:
continue
rows.append({
"variable": v,
"h_min": h,
"n": len(x),
"pearson_abs": p_abs,
"spearman_abs": s_abs,
"pearson_sign": p_sgn,
"spearman_sign": s_sgn,
})
return pd.DataFrame(rows)
def main():
print("loading BTC minutes from", DATA_GLOB)
df = load_btc()
print(f"loaded {len(df)} unique minutes")
print("columns:", list(df.columns))
df = add_forward_returns(df)
print("\n=== sample variable stats ===")
for v in VARIABLES:
if v in df.columns:
s = df[v].astype(float)
n_zero = int((s == 0).sum())
print(f" {v:<20} mean={s.mean():+.4g} std={s.std():+.4g} "
f"min={s.min():+.4g} max={s.max():+.4g} zeros={n_zero}/{len(s)}")
tbl = corr_table(df)
pd.set_option("display.width", 200)
pd.set_option("display.max_rows", None)
pd.set_option("display.float_format", "{:+.4f}".format)
print("\n=== |fwd_return| correlations (Pearson) ===")
pivot_p = tbl.pivot(index="variable", columns="h_min", values="pearson_abs")
pivot_p = pivot_p.reindex(VARIABLES)
print(pivot_p.to_string())
print("\n=== |fwd_return| correlations (Spearman) ===")
pivot_s = tbl.pivot(index="variable", columns="h_min", values="spearman_abs")
pivot_s = pivot_s.reindex(VARIABLES)
print(pivot_s.to_string())
print("\n=== sign(fwd_return) correlations (Pearson) ===")
pivot_sgn = tbl.pivot(index="variable", columns="h_min", values="pearson_sign")
pivot_sgn = pivot_sgn.reindex(VARIABLES)
print(pivot_sgn.to_string())
print("\n=== sign(fwd_return) correlations (Spearman) ===")
pivot_sgn_s = tbl.pivot(index="variable", columns="h_min", values="spearman_sign")
pivot_sgn_s = pivot_sgn_s.reindex(VARIABLES)
print(pivot_sgn_s.to_string())
# Save raw table
out = Path("/mnt/d/Resonance_Engine/traj/univariate_sweep.csv")
tbl.to_csv(out, index=False)
print(f"\nsaved: {out}")
if __name__ == "__main__":
main()