221 lines
8.7 KiB
Python
221 lines
8.7 KiB
Python
"""_analyze_continuous.py — analyze inject_continuous_stream 4-arm output.
|
|
|
|
For each arm A/B/C (read from parquet):
|
|
- merge with source data to get forward returns at h=60, h=240
|
|
- fit linear regression: |fwd_return(h)| ~ field_state_features
|
|
- report R² (out-of-sample, time-series-aware k-fold)
|
|
|
|
For arm D (raw data baseline, computed inline here):
|
|
- fit linear regression: |fwd_return(h)| ~ raw z-scores of 3 variables
|
|
- report R²
|
|
|
|
Conclusion logic:
|
|
If R²(B or C) > R²(D) + meaningful margin → lattice adds information.
|
|
If R²(B or C) <= R²(D) → lattice is a noisy filter, use raw data directly.
|
|
|
|
Usage:
|
|
python3 _analyze_continuous.py latest
|
|
python3 _analyze_continuous.py <RUN_ID>
|
|
"""
|
|
from __future__ import annotations
|
|
import glob, json, sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
from sklearn.linear_model import Ridge
|
|
from sklearn.model_selection import TimeSeriesSplit
|
|
from sklearn.metrics import r2_score
|
|
|
|
ROOT = Path("/mnt/d/Resonance_Engine/traj")
|
|
DATA_ROOT = "/mnt/d/PaperTrader/research/hl_data/minutes"
|
|
|
|
FIELD_FEATURES = [
|
|
"asymmetry", "coherence",
|
|
"stress_xx", "stress_yy", "stress_xy",
|
|
"vorticity_mean", "vel_mean", "vel_max", "vel_var",
|
|
]
|
|
VARIABLES = ["signed_flow_usd", "vwap_drift", "wallet_entropy"]
|
|
HORIZONS = [60, 240, 720, 1440]
|
|
|
|
|
|
def find_run(arg: str) -> Path:
|
|
if arg == "latest":
|
|
runs = sorted([p for p in ROOT.glob("contstream_*") if p.is_dir()])
|
|
if not runs:
|
|
sys.exit("no contstream_* runs in " + str(ROOT))
|
|
return runs[-1]
|
|
p = ROOT / (arg if arg.startswith("contstream_") else f"contstream_{arg}")
|
|
if not p.exists():
|
|
sys.exit(f"missing: {p}")
|
|
return p
|
|
|
|
|
|
def load_btc_data(meta: dict) -> pd.DataFrame:
|
|
day_dirs = sorted(glob.glob(f"{DATA_ROOT}/{meta['days_glob']}"))
|
|
dfs = []
|
|
for d in day_dirs:
|
|
for f in sorted(glob.glob(f"{d}/*.parquet")):
|
|
dfs.append(pd.read_parquet(f))
|
|
df = pd.concat(dfs, ignore_index=True)
|
|
df = df[df.coin == meta["coin"]].sort_values("minute").reset_index(drop=True)
|
|
df = df.drop_duplicates(subset=["minute"]).reset_index(drop=True)
|
|
return df
|
|
|
|
|
|
def add_forward_returns(df: pd.DataFrame, horizons: list[int]) -> pd.DataFrame:
|
|
df = df.copy()
|
|
mid = df["mid_price"].values
|
|
for h in horizons:
|
|
fwd = np.full_like(mid, np.nan, dtype=float)
|
|
fwd[: -h] = (mid[h:] - mid[:-h]) / mid[:-h]
|
|
df[f"fwd_ret_{h}"] = fwd
|
|
df[f"abs_fwd_ret_{h}"] = np.abs(fwd)
|
|
return df
|
|
|
|
|
|
def compute_one_sided_zscores(df: pd.DataFrame, vars_: list[str], win: int) -> pd.DataFrame:
|
|
out = pd.DataFrame({"minute": df["minute"].values})
|
|
for v in vars_:
|
|
s = df[v].astype(float)
|
|
roll = s.shift(1).rolling(window=win, min_periods=50)
|
|
z = (s - roll.mean()) / roll.std().replace(0, np.nan)
|
|
out[f"z_{v}"] = z.values
|
|
return out
|
|
|
|
|
|
def cv_r2(X: np.ndarray, y: np.ndarray, n_splits: int = 5) -> dict:
|
|
"""Time-series-aware k-fold R² with Ridge regression."""
|
|
mask = np.all(np.isfinite(X), axis=1) & np.isfinite(y)
|
|
X = X[mask]
|
|
y = y[mask]
|
|
if len(y) < 500:
|
|
return {"r2_mean": float("nan"), "r2_std": float("nan"), "n": len(y)}
|
|
tscv = TimeSeriesSplit(n_splits=n_splits)
|
|
scores = []
|
|
for tr, te in tscv.split(X):
|
|
model = Ridge(alpha=1.0)
|
|
model.fit(X[tr], y[tr])
|
|
scores.append(r2_score(y[te], model.predict(X[te])))
|
|
return {
|
|
"r2_mean": float(np.mean(scores)),
|
|
"r2_std": float(np.std(scores)),
|
|
"r2_folds": [float(s) for s in scores],
|
|
"n": int(len(y)),
|
|
}
|
|
|
|
|
|
def main():
|
|
arg = sys.argv[1] if len(sys.argv) > 1 else "latest"
|
|
run = find_run(arg)
|
|
print(f"=== analyzing {run.name} ===\n")
|
|
|
|
meta = json.loads((run / "meta.json").read_text())
|
|
print(f"run_id={meta['run_id']} start={meta.get('wall_iso_start')}")
|
|
print(f"coin={meta['coin']} days={meta['days_glob']} n_minutes={meta['n_minutes']}")
|
|
print(f"vars={meta['variables']} roll_win={meta['roll_win']} str_cap={meta['str_cap']}\n")
|
|
|
|
# load source data + add forward returns + z-scores
|
|
src = load_btc_data(meta)
|
|
src = add_forward_returns(src, HORIZONS)
|
|
z = compute_one_sided_zscores(src, VARIABLES, meta["roll_win"])
|
|
src = src.merge(z, on="minute", how="left")
|
|
print(f"source data: {len(src)} minutes, fwd returns at h={HORIZONS} added\n")
|
|
|
|
# ───── ARM D: raw-data baseline (offline) ─────
|
|
print("=== ARM D: RAW DATA BASELINE (no lattice) ===")
|
|
Xd = src[[f"z_{v}" for v in VARIABLES]].values
|
|
arm_d_results = {}
|
|
for h in HORIZONS:
|
|
y = src[f"abs_fwd_ret_{h}"].values
|
|
res = cv_r2(Xd, y)
|
|
arm_d_results[h] = res
|
|
print(f" h={h}min R²(|fwd_ret|)= {res['r2_mean']:+.5f} +/- {res['r2_std']:.5f} "
|
|
f"folds={[f'{x:+.4f}' for x in res.get('r2_folds',[])]} n={res['n']}")
|
|
print()
|
|
|
|
# ───── ARMS A/B/C ─────
|
|
arm_files = {
|
|
"A": run / "arm_A_no_inject.parquet",
|
|
"B": run / "arm_B_stacked.parquet",
|
|
"C": run / "arm_C_separated.parquet",
|
|
}
|
|
arm_results = {arm: {} for arm in arm_files}
|
|
|
|
for arm, fpath in arm_files.items():
|
|
if not fpath.exists():
|
|
print(f"=== ARM {arm}: skipped (no file {fpath.name}) ===\n")
|
|
continue
|
|
df_arm = pd.read_parquet(fpath)
|
|
# merge with source on minute (to attach forward returns)
|
|
m = df_arm.merge(src[["minute"] + [f"abs_fwd_ret_{h}" for h in HORIZONS]],
|
|
on="minute", how="left")
|
|
print(f"=== ARM {arm}: {fpath.name} rows={len(m)} ===")
|
|
# describe field
|
|
for ch in ["asymmetry", "coherence", "stress_xy"]:
|
|
v = m[ch].dropna()
|
|
if len(v):
|
|
print(f" {ch}: mean={v.mean():+.4f} std={v.std():.4f} "
|
|
f"min={v.min():+.4f} max={v.max():+.4f}")
|
|
# build feature matrix from field channels
|
|
X = m[FIELD_FEATURES].values
|
|
for h in HORIZONS:
|
|
y = m[f"abs_fwd_ret_{h}"].values
|
|
res = cv_r2(X, y)
|
|
arm_results[arm][h] = res
|
|
print(f" h={h}min R²(|fwd_ret|)= {res['r2_mean']:+.5f} +/- {res['r2_std']:.5f} "
|
|
f"folds={[f'{x:+.4f}' for x in res.get('r2_folds',[])]} n={res['n']}")
|
|
# also: field + raw z (combined)
|
|
m2 = m.merge(z, on="minute", how="left")
|
|
Xc = m2[FIELD_FEATURES + [f"z_{v}" for v in VARIABLES]].values
|
|
for h in HORIZONS:
|
|
y = m2[f"abs_fwd_ret_{h}"].values
|
|
res = cv_r2(Xc, y)
|
|
arm_results[arm][f"{h}_combined"] = res
|
|
print(f" h={h}min COMBINED(field+rawZ) R²= {res['r2_mean']:+.5f} n={res['n']}")
|
|
print()
|
|
|
|
# ───── verdict ─────
|
|
print("=" * 70)
|
|
print("VERDICT")
|
|
print("=" * 70)
|
|
print(f"{'metric':<35}{'h=60':>14}{'h=240':>14}")
|
|
print("-" * 70)
|
|
print(f"{'arm D (raw data only)':<35}"
|
|
f"{arm_d_results.get(60,{}).get('r2_mean',float('nan')):>+14.5f}"
|
|
f"{arm_d_results.get(240,{}).get('r2_mean',float('nan')):>+14.5f}")
|
|
for arm in ["A", "B", "C"]:
|
|
if arm not in arm_results or not arm_results[arm]:
|
|
continue
|
|
print(f"{'arm '+arm+' (field only)':<35}"
|
|
f"{arm_results[arm].get(60,{}).get('r2_mean',float('nan')):>+14.5f}"
|
|
f"{arm_results[arm].get(240,{}).get('r2_mean',float('nan')):>+14.5f}")
|
|
print(f"{'arm '+arm+' (field + rawZ combined)':<35}"
|
|
f"{arm_results[arm].get('60_combined',{}).get('r2_mean',float('nan')):>+14.5f}"
|
|
f"{arm_results[arm].get('240_combined',{}).get('r2_mean',float('nan')):>+14.5f}")
|
|
print()
|
|
|
|
# decision
|
|
print("Interpretation:")
|
|
d60 = arm_d_results.get(60, {}).get("r2_mean", float("nan"))
|
|
for arm in ["B", "C"]:
|
|
if arm not in arm_results or not arm_results[arm]:
|
|
continue
|
|
r60 = arm_results[arm].get(60, {}).get("r2_mean", float("nan"))
|
|
rc60 = arm_results[arm].get("60_combined", {}).get("r2_mean", float("nan"))
|
|
if np.isfinite(r60) and np.isfinite(d60):
|
|
delta_pure = r60 - d60
|
|
delta_comb = (rc60 - d60) if np.isfinite(rc60) else float("nan")
|
|
v_pure = "lattice ADDS info" if delta_pure > 0.005 else \
|
|
"lattice DOES NOT add info beyond raw data"
|
|
print(f" arm {arm} h=60: field-only ΔR² vs raw={delta_pure:+.5f} → {v_pure}")
|
|
if np.isfinite(delta_comb):
|
|
v_comb = "combination beats raw" if delta_comb > 0.005 else \
|
|
"combination no better than raw"
|
|
print(f" arm {arm} h=60: combined ΔR² vs raw={delta_comb:+.5f} → {v_comb}")
|
|
print("\n=== DONE ===")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|