auto: hourly snapshot 2026-06-07 12:34
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""_composite_band.py
|
||||
|
||||
Composite rank-average vol-timing signal, with band-geometry analysis.
|
||||
|
||||
Construction (per-window, no leakage):
|
||||
- For each variable in VOL_VARS, compute rank within the current sample
|
||||
(0..1 normalised rank).
|
||||
- Composite = mean of per-variable ranks. Re-rank into 10 deciles.
|
||||
|
||||
Per-window means: when called on March data, ranks are computed within March.
|
||||
When called on April data, ranks are computed within April. There is no
|
||||
cross-window leakage.
|
||||
|
||||
Report:
|
||||
1. Full 10-decile ladder: n, composite_rank_mean, |ret|_mean, |ret|_median,
|
||||
signed_ret_mean — for both March (IS) and April (OOS).
|
||||
2. Inflection: per-decile delta vs the global mean — where does the ladder
|
||||
pull away from the middle?
|
||||
3. Stability: per-decile |ret| comparison March vs April.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import glob, sys
|
||||
from pathlib import Path
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
DATA_ROOT = "/mnt/d/PaperTrader/research/hl_data/minutes"
|
||||
|
||||
VOL_VARS = ["trade_count", "wallet_entropy", "taker_buy_usd", "taker_sell_usd"]
|
||||
HORIZONS = [15, 60, 120]
|
||||
N_DEC = 10
|
||||
|
||||
|
||||
def load_btc(glob_pattern: str) -> pd.DataFrame:
|
||||
dfs = []
|
||||
for d in sorted(glob.glob(glob_pattern)):
|
||||
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)
|
||||
return df.sort_values("minute").drop_duplicates("minute").reset_index(drop=True)
|
||||
|
||||
|
||||
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 build_composite(df: pd.DataFrame) -> pd.Series:
|
||||
"""Composite = mean of normalised ranks of VOL_VARS, computed within
|
||||
this sample only. Returns a Series aligned to df.index."""
|
||||
rank_cols = []
|
||||
for v in VOL_VARS:
|
||||
s = df[v].astype(float)
|
||||
# rank with pct=True gives 0..1; method='average' handles ties cleanly
|
||||
r = s.rank(method="average", pct=True, na_option="keep")
|
||||
rank_cols.append(r)
|
||||
composite = pd.concat(rank_cols, axis=1).mean(axis=1)
|
||||
return composite
|
||||
|
||||
|
||||
def decile_ladder(df: pd.DataFrame, composite: pd.Series, h: int) -> pd.DataFrame:
|
||||
y_abs = df[f"abs_fwd_{h}"]
|
||||
y_sgn = df[f"fwd_{h}"]
|
||||
mask = composite.notna() & y_abs.notna() & np.isfinite(composite) & np.isfinite(y_abs)
|
||||
sub = pd.DataFrame({
|
||||
"comp": composite[mask].values,
|
||||
"abs": y_abs[mask].values,
|
||||
"sgn": y_sgn[mask].values,
|
||||
})
|
||||
sub["dec"] = pd.qcut(sub["comp"].rank(method="first"), N_DEC, labels=False)
|
||||
g = sub.groupby("dec").agg(
|
||||
n=("comp", "size"),
|
||||
comp_mean=("comp", "mean"),
|
||||
abs_mean=("abs", "mean"),
|
||||
abs_med=("abs", "median"),
|
||||
sgn_mean=("sgn", "mean"),
|
||||
)
|
||||
g["abs_mean_bps"] = g["abs_mean"] * 10000
|
||||
g["abs_med_bps"] = g["abs_med"] * 10000
|
||||
g["sgn_mean_bps"] = g["sgn_mean"] * 10000
|
||||
return g
|
||||
|
||||
|
||||
def inflection_analysis(g: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Per-decile |ret| vs the global mean. Identifies where the ladder
|
||||
pulls away (positive or negative) from the middle."""
|
||||
global_mean = g["abs_mean_bps"].mean()
|
||||
g = g.copy()
|
||||
g["delta_vs_global_bps"] = g["abs_mean_bps"] - global_mean
|
||||
g["pct_vs_global"] = (g["abs_mean_bps"] / global_mean - 1) * 100
|
||||
# detect knee: largest decile-to-decile jump in |ret|
|
||||
diffs = g["abs_mean_bps"].diff()
|
||||
g["jump_from_prev_bps"] = diffs
|
||||
return g
|
||||
|
||||
|
||||
def main():
|
||||
pd.set_option("display.width", 220)
|
||||
pd.set_option("display.max_rows", None)
|
||||
pd.set_option("display.float_format", "{:+.3f}".format)
|
||||
|
||||
march = add_forward_returns(load_btc(f"{DATA_ROOT}/202603*"))
|
||||
april = add_forward_returns(load_btc(f"{DATA_ROOT}/202604*"))
|
||||
print(f"loaded March={len(march)} April={len(april)} BTC minutes")
|
||||
|
||||
comp_m = build_composite(march)
|
||||
comp_a = build_composite(april)
|
||||
|
||||
for h in HORIZONS:
|
||||
print("\n" + "="*92)
|
||||
print(f"COMPOSITE BAND LADDER h={h}min (vars={VOL_VARS}, rank-avg, in-window only)")
|
||||
print("="*92)
|
||||
gm = inflection_analysis(decile_ladder(march, comp_m, h))
|
||||
ga = inflection_analysis(decile_ladder(april, comp_a, h))
|
||||
|
||||
print(f"\n--- MARCH (IS) ---")
|
||||
print(gm[["n", "comp_mean", "abs_mean_bps", "abs_med_bps", "sgn_mean_bps",
|
||||
"delta_vs_global_bps", "pct_vs_global", "jump_from_prev_bps"]].to_string())
|
||||
|
||||
print(f"\n--- APRIL (OOS) ---")
|
||||
print(ga[["n", "comp_mean", "abs_mean_bps", "abs_med_bps", "sgn_mean_bps",
|
||||
"delta_vs_global_bps", "pct_vs_global", "jump_from_prev_bps"]].to_string())
|
||||
|
||||
# Side-by-side stability check
|
||||
comp = pd.DataFrame({
|
||||
"march_|ret|_bps": gm["abs_mean_bps"],
|
||||
"april_|ret|_bps": ga["abs_mean_bps"],
|
||||
"march_pct_vs_global": gm["pct_vs_global"],
|
||||
"april_pct_vs_global": ga["pct_vs_global"],
|
||||
})
|
||||
comp["bps_diff_AvsM"] = comp["april_|ret|_bps"] - comp["march_|ret|_bps"]
|
||||
comp["pct_shape_diff"] = comp["april_pct_vs_global"] - comp["march_pct_vs_global"]
|
||||
print(f"\n--- STABILITY (per-decile shape: March vs April) ---")
|
||||
print(comp.to_string())
|
||||
|
||||
# rank correlation between months on the per-decile |ret| ordering
|
||||
rho = gm["abs_mean_bps"].rank().corr(ga["abs_mean_bps"].rank(), method="spearman")
|
||||
print(f"\nSpearman rank correlation of decile |ret| (March vs April): {rho:+.4f}")
|
||||
|
||||
# ratio metrics
|
||||
top_m, bot_m = gm["abs_mean_bps"].iloc[-1], gm["abs_mean_bps"].iloc[0]
|
||||
top_a, bot_a = ga["abs_mean_bps"].iloc[-1], ga["abs_mean_bps"].iloc[0]
|
||||
print(f"top/bot ratio March={top_m/bot_m:.2f}x April={top_a/bot_a:.2f}x")
|
||||
|
||||
# vs best single-variable benchmark (trade_count)
|
||||
# for reference: trade_count March top/bot @ h=60 was 2.27x, April 2.42x
|
||||
if h == 60:
|
||||
print(f"\nBenchmark to beat (trade_count alone @ h=60): March 2.27x April 2.42x")
|
||||
comp_m_ratio = top_m / bot_m
|
||||
comp_a_ratio = top_a / bot_a
|
||||
verdict_m = "BEATS" if comp_m_ratio > 2.27 else "does not beat"
|
||||
verdict_a = "BEATS" if comp_a_ratio > 2.42 else "does not beat"
|
||||
print(f"Composite vs trade_count: March {comp_m_ratio:.2f}x ({verdict_m}) "
|
||||
f"April {comp_a_ratio:.2f}x ({verdict_a})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user