Files
resonance-engine/calibrate_4hr_scales.py
T
2026-06-08 06:34:31 +07:00

81 lines
3.8 KiB
Python

"""Pure-compute calibration pass. No injections. Pull all April BTC + funding,
compute 4hr levels and 4hr velocities, report distributions so we can set
MAP_SCALE values that produce strength p90 ~ 0.5-1.0 (responsive but not saturated).
"""
import glob, json, urllib.request, time
import numpy as np, pandas as pd
from pathlib import Path
DATA = "/mnt/d/PaperTrader/research/hl_data/minutes"
LEVEL_WIN = 240
ROLL_ACT = 500
ROLL_CVD = 60
EPS_BPS = 0.5
# load BTC april
files=sorted(glob.glob(f"{DATA}/202604*/*.parquet"))
print(f"loading {len(files)} files...")
dfs=[pd.read_parquet(f) for f in files]
df=pd.concat(dfs,ignore_index=True)
df=df[df.coin=="BTC"].sort_values("minute").drop_duplicates("minute").reset_index(drop=True)
print(f"{len(df):,} minutes BTC")
# funding (re-fetch or use cached)
cache = Path("/tmp/april_funding_verify.parquet")
if cache.exists():
fnd = pd.read_parquet(cache)
fnd["minute_hour"] = (fnd["time"].astype("int64") // (1000*60))
fnd["funding_bps_annual"] = fnd["fundingRate"].astype(float)*8760*10000
fnd = fnd[["minute_hour","funding_bps_annual"]]
else:
raise SystemExit("need /tmp/april_funding_verify.parquet")
print(f"{len(fnd)} funding rows")
# tensions
df["minute_hour_floor"]=(df.minute//60)*60
df=df.merge(fnd.rename(columns={"minute_hour":"minute_hour_floor"}),on="minute_hour_floor",how="left")
df["funding_bps"]=df["funding_bps_annual"].ffill()
denom=(df.taker_buy_usd+df.taker_sell_usd).replace(0,np.nan)
df["bs_ratio_signed"]=(df.taker_buy_usd/denom)-0.5
rm=df.trade_count.astype(float).shift(1).rolling(ROLL_ACT,min_periods=50).mean()
df["activity_excess"]=(df.trade_count.astype(float)/rm)-1.0
sflow=df.signed_flow_usd.astype(float)
cvd60=sflow.shift(1).rolling(ROLL_CVD,min_periods=10).sum()
px=df.mid_price.astype(float)
pn=px.shift(1); pt=px.shift(1+ROLL_CVD)
chg=((pn-pt)/pt*10000).abs()
df["cvd_divergence"]=cvd60/(chg+EPS_BPS)
# 4hr levels
for c in ["funding_bps","bs_ratio_signed","activity_excess","cvd_divergence"]:
df[f"{c}_level"]=df[c].astype(float).shift(1).rolling(LEVEL_WIN,min_periods=60).mean()
# 4hr velocities
for c in ["funding_bps","bs_ratio_signed","activity_excess","cvd_divergence"]:
df[f"{c}_vel"]=df[f"{c}_level"]-df[f"{c}_level"].shift(LEVEL_WIN)
print()
print(f"{'tension':<28} {'n_fin':>7} {'mean':>10} {'std':>10} {'p5':>10} {'p50':>10} {'p95':>10} {'p99':>10} {'absp50':>10} {'absp90':>10} {'absp99':>10}")
for col in ["funding_bps","funding_bps_level","funding_bps_vel",
"bs_ratio_signed","bs_ratio_signed_level","bs_ratio_signed_vel",
"activity_excess","activity_excess_level","activity_excess_vel",
"cvd_divergence","cvd_divergence_level","cvd_divergence_vel"]:
v = pd.to_numeric(df[col], errors="coerce").dropna()
av = v.abs()
print(f" {col:<26} {len(v):>7} {v.mean():>+10.3g} {v.std():>10.3g} {v.quantile(.05):>+10.3g} {v.quantile(.5):>+10.3g} {v.quantile(.95):>+10.3g} {v.quantile(.99):>+10.3g} {av.quantile(.5):>10.3g} {av.quantile(.9):>10.3g} {av.quantile(.99):>10.3g}")
# proposed scales: per_strength = absp90 (so q90 maps to strength ~1.0)
print()
print("=== PROPOSED SCALES (per_strength = |val| q90 -> strength 1.0) ===")
for col,prefix in [("funding_bps_level","funding_level"),("bs_ratio_signed_level","bsratio_level"),
("activity_excess_level","activity_level"),("cvd_divergence_level","cvd_level"),
("funding_bps_vel","funding_vel"),("bs_ratio_signed_vel","bsratio_vel"),
("activity_excess_vel","activity_vel"),("cvd_divergence_vel","cvd_vel")]:
av = df[col].abs().dropna()
if len(av)==0: continue
p90 = av.quantile(.9); p99 = av.quantile(.99)
print(f" {prefix:<22} per_strength = {p90:.3g} (q99 would map to {p99/p90:.2f}, suggest cap = {min(p99/p90*1.1, 5.0):.1f})")