275 lines
11 KiB
Python
275 lines
11 KiB
Python
|
|
"""NO-PAUSE 5s Crossover Test
|
||
|
|
============================
|
||
|
|
Run vel_div crossover on ALL 56 days — no rvol gate, no dvol gate.
|
||
|
|
ACB is the intended braker in the live system; here we test raw signal viability.
|
||
|
|
|
||
|
|
Compare three variants in one pass:
|
||
|
|
A. NO_GATE — all 56 days, trade every day
|
||
|
|
B. RVOL_ONLY — pause only on rvol Q1 (original threshold = 0.000203)
|
||
|
|
C. FULL_GATE — rvol + dvol<47.5 gate (current default)
|
||
|
|
|
||
|
|
If signal is real: A should still have PF > 1, just lower than C.
|
||
|
|
If signal is curve-fit to gated days: A collapses.
|
||
|
|
"""
|
||
|
|
import sys, time, gc
|
||
|
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||
|
|
from pathlib import Path
|
||
|
|
from collections import defaultdict
|
||
|
|
import numpy as np
|
||
|
|
import pandas as pd
|
||
|
|
|
||
|
|
_here = Path(__file__).parent
|
||
|
|
sys.path.insert(0, str(_here))
|
||
|
|
sys.path.insert(0, str(_here.parent))
|
||
|
|
|
||
|
|
from nautilus_dolphin.nautilus.macro_posture_switcher import MacroPostureSwitcher, Posture
|
||
|
|
|
||
|
|
VBT_DIR = Path(r"C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict\vbt_cache")
|
||
|
|
EIGEN_PATH = Path(r"C:\Users\Lenovo\Documents\- Dolphin NG HD (NG3)\correlation_arb512\eigenvalues")
|
||
|
|
LOG_DIR = Path(r"C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict\nautilus_dolphin\run_logs")
|
||
|
|
|
||
|
|
ENTRY_T = 0.020
|
||
|
|
MAX_HOLD = 240 # 20 min on 5s
|
||
|
|
EXF_KEYS = ['dvol_btc', 'fng', 'funding_btc', 'taker']
|
||
|
|
|
||
|
|
def load_exf(date_str):
|
||
|
|
d = {'dvol_btc': 50.0, 'fng': 50.0, 'funding_btc': 0.0, 'taker': 1.0}
|
||
|
|
dp = EIGEN_PATH / date_str
|
||
|
|
if not dp.exists():
|
||
|
|
return d
|
||
|
|
files = sorted(dp.glob('scan_*__Indicators.npz'))[:5]
|
||
|
|
if not files:
|
||
|
|
return d
|
||
|
|
buckets = defaultdict(list)
|
||
|
|
for f in files:
|
||
|
|
try:
|
||
|
|
nd = np.load(f, allow_pickle=True)
|
||
|
|
if 'api_names' not in nd:
|
||
|
|
continue
|
||
|
|
names = list(nd['api_names'])
|
||
|
|
vals = nd['api_indicators']
|
||
|
|
for k in EXF_KEYS:
|
||
|
|
if k in names:
|
||
|
|
v = float(vals[names.index(k)])
|
||
|
|
if np.isfinite(v):
|
||
|
|
buckets[k].append(v)
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
for k, vs in buckets.items():
|
||
|
|
if vs:
|
||
|
|
d[k] = float(np.median(vs))
|
||
|
|
return d
|
||
|
|
|
||
|
|
parquet_files = sorted(VBT_DIR.glob("*.parquet"))
|
||
|
|
parquet_files = [p for p in parquet_files if 'catalog' not in str(p)]
|
||
|
|
total = len(parquet_files)
|
||
|
|
|
||
|
|
switchers = {
|
||
|
|
'A_NO_GATE': MacroPostureSwitcher(enable_long_posture=False,
|
||
|
|
rvol_pause_thresh=0.0,
|
||
|
|
dvol_none_below=0.0),
|
||
|
|
'B_RVOL_ONLY': MacroPostureSwitcher(enable_long_posture=False,
|
||
|
|
rvol_pause_thresh=0.000203,
|
||
|
|
dvol_none_below=0.0),
|
||
|
|
'C_FULL_GATE': MacroPostureSwitcher(enable_long_posture=False,
|
||
|
|
rvol_pause_thresh=0.000203,
|
||
|
|
dvol_none_below=47.5),
|
||
|
|
}
|
||
|
|
|
||
|
|
# Pass 1: lag-1 rvol
|
||
|
|
print("Pass 1: lag-1 rvol...")
|
||
|
|
t0 = time.time()
|
||
|
|
day_rvol, day_btcret = {}, {}
|
||
|
|
for pf in parquet_files:
|
||
|
|
ds = pf.stem
|
||
|
|
try:
|
||
|
|
df = pd.read_parquet(pf, columns=['BTCUSDT'])
|
||
|
|
except Exception:
|
||
|
|
continue
|
||
|
|
btc = df['BTCUSDT'].values.astype(np.float64)
|
||
|
|
btc = btc[np.isfinite(btc) & (btc > 0)]
|
||
|
|
if len(btc) < 2:
|
||
|
|
continue
|
||
|
|
log_r = np.diff(np.log(btc))
|
||
|
|
day_rvol[ds] = float(np.std(log_r))
|
||
|
|
day_btcret[ds] = float((btc[-1] - btc[0]) / btc[0])
|
||
|
|
dates_sorted = sorted(day_rvol.keys())
|
||
|
|
prev_rvol = {d: day_rvol.get(dates_sorted[i-1]) if i > 0 else None for i, d in enumerate(dates_sorted)}
|
||
|
|
prev_btcret = {d: day_btcret.get(dates_sorted[i-1]) if i > 0 else None for i, d in enumerate(dates_sorted)}
|
||
|
|
print(f" {time.time()-t0:.1f}s")
|
||
|
|
|
||
|
|
def make_acc():
|
||
|
|
return dict(wins=0, losses=0, gw=0.0, gl=0.0, n=0,
|
||
|
|
equity=1.0, ec=[1.0], active=0, paused=0,
|
||
|
|
day_rets=[], rows=[])
|
||
|
|
|
||
|
|
accs = {k: make_acc() for k in switchers}
|
||
|
|
|
||
|
|
print("Pass 2: simulate...")
|
||
|
|
for i, pf in enumerate(parquet_files):
|
||
|
|
ds = pf.stem
|
||
|
|
exf = load_exf(ds)
|
||
|
|
pr = prev_rvol.get(ds)
|
||
|
|
pb = prev_btcret.get(ds)
|
||
|
|
|
||
|
|
# Decide posture for each variant
|
||
|
|
decisions = {k: sw.decide(dvol_btc=exf['dvol_btc'], fng=exf['fng'],
|
||
|
|
funding_btc=exf['funding_btc'],
|
||
|
|
realized_vol=pr, btc_day_return=pb)
|
||
|
|
for k, sw in switchers.items()}
|
||
|
|
|
||
|
|
# Load data once (if any variant is active)
|
||
|
|
any_active = any(d.posture != Posture.NONE for d in decisions.values())
|
||
|
|
if not any_active:
|
||
|
|
for k, acc in accs.items():
|
||
|
|
acc['paused'] += 1
|
||
|
|
acc['rows'].append({'date': ds, 'posture': 'NONE', 'dvol': exf['dvol_btc'],
|
||
|
|
'fng': exf['fng'], 'n': 0, 'pf': 1.0, 'day_ret': 0.0})
|
||
|
|
continue
|
||
|
|
|
||
|
|
try:
|
||
|
|
df = pd.read_parquet(pf)
|
||
|
|
except Exception:
|
||
|
|
for acc in accs.values():
|
||
|
|
acc['paused'] += 1
|
||
|
|
continue
|
||
|
|
|
||
|
|
if 'vel_div' not in df.columns or 'BTCUSDT' not in df.columns:
|
||
|
|
for acc in accs.values():
|
||
|
|
acc['paused'] += 1
|
||
|
|
del df
|
||
|
|
continue
|
||
|
|
|
||
|
|
vd = df['vel_div'].values.astype(np.float64)
|
||
|
|
btc = df['BTCUSDT'].values.astype(np.float64)
|
||
|
|
vd = np.where(np.isfinite(vd), vd, 0.0)
|
||
|
|
btc = np.where(np.isfinite(btc) & (btc > 0), btc, np.nan)
|
||
|
|
n = len(btc)
|
||
|
|
del df
|
||
|
|
|
||
|
|
# SHORT crossover: enter when vd >= ENTRY_T, exit when vd <= -ENTRY_T
|
||
|
|
entry_mask = (vd >= ENTRY_T) & np.isfinite(btc)
|
||
|
|
cross_back = (vd <= -ENTRY_T)
|
||
|
|
|
||
|
|
if n < MAX_HOLD + 5:
|
||
|
|
for acc in accs.values():
|
||
|
|
acc['paused'] += 1
|
||
|
|
del vd, btc, entry_mask, cross_back
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Compute trades (same for all variants — only sizing differs by size_mult)
|
||
|
|
raw_trades = []
|
||
|
|
for t in range(n - MAX_HOLD):
|
||
|
|
if not entry_mask[t]:
|
||
|
|
continue
|
||
|
|
ep = btc[t]
|
||
|
|
if not np.isfinite(ep) or ep <= 0:
|
||
|
|
continue
|
||
|
|
exit_bar = MAX_HOLD
|
||
|
|
for k2 in range(1, MAX_HOLD + 1):
|
||
|
|
tb = t + k2
|
||
|
|
if tb >= n:
|
||
|
|
exit_bar = k2; break
|
||
|
|
if cross_back[tb]:
|
||
|
|
exit_bar = k2; break
|
||
|
|
tb = t + exit_bar
|
||
|
|
if tb >= n:
|
||
|
|
continue
|
||
|
|
xp = btc[tb]
|
||
|
|
if not np.isfinite(xp) or xp <= 0:
|
||
|
|
continue
|
||
|
|
raw_ret = -1.0 * (xp - ep) / ep # SHORT
|
||
|
|
raw_trades.append(raw_ret)
|
||
|
|
|
||
|
|
del vd, btc, entry_mask, cross_back
|
||
|
|
|
||
|
|
n_t = len(raw_trades)
|
||
|
|
raw_arr = np.array(raw_trades)
|
||
|
|
|
||
|
|
for k, acc in accs.items():
|
||
|
|
dec = decisions[k]
|
||
|
|
if dec.posture == Posture.NONE:
|
||
|
|
acc['paused'] += 1
|
||
|
|
acc['rows'].append({'date': ds, 'posture': 'NONE', 'dvol': round(exf['dvol_btc'],1),
|
||
|
|
'fng': round(exf['fng'],1), 'n': 0, 'pf': 1.0, 'day_ret': 0.0})
|
||
|
|
continue
|
||
|
|
|
||
|
|
acc['active'] += 1
|
||
|
|
if n_t == 0:
|
||
|
|
acc['rows'].append({'date': ds, 'posture': 'SHORT', 'dvol': round(exf['dvol_btc'],1),
|
||
|
|
'fng': round(exf['fng'],1), 'n': 0, 'pf': 1.0, 'day_ret': 0.0})
|
||
|
|
continue
|
||
|
|
|
||
|
|
sm = dec.size_mult
|
||
|
|
sized = raw_arr * sm
|
||
|
|
wins = int(np.sum(raw_arr >= 0))
|
||
|
|
losses = n_t - wins
|
||
|
|
gw = float(np.sum(raw_arr[raw_arr >= 0]))
|
||
|
|
gl = float(np.sum(np.abs(raw_arr[raw_arr < 0])))
|
||
|
|
day_ret = float(np.sum(sized))
|
||
|
|
|
||
|
|
acc['wins'] += wins; acc['losses'] += losses
|
||
|
|
acc['gw'] += gw; acc['gl'] += gl; acc['n'] += n_t
|
||
|
|
day_ret_c = max(-0.5, min(day_ret, 2.0))
|
||
|
|
acc['equity'] *= (1 + day_ret_c)
|
||
|
|
acc['ec'].append(acc['equity'])
|
||
|
|
acc['day_rets'].append(day_ret)
|
||
|
|
pf_d = gw / gl if gl > 0 else 999.0
|
||
|
|
acc['rows'].append({'date': ds, 'posture': 'SHORT', 'dvol': round(exf['dvol_btc'],1),
|
||
|
|
'fng': round(exf['fng'],1), 'pr': round(pr,7) if pr else None,
|
||
|
|
'n': n_t, 'wins': wins, 'losses': losses,
|
||
|
|
'pf': round(pf_d,4), 'day_ret': round(day_ret,6)})
|
||
|
|
|
||
|
|
gc.collect() if (i+1) % 10 == 0 else None
|
||
|
|
|
||
|
|
elapsed = time.time() - t0
|
||
|
|
|
||
|
|
# ── Report ──────────────────────────────────────────────────────────────────
|
||
|
|
print(f"\n{'='*70}")
|
||
|
|
print(f" NO-PAUSE 5s Crossover Test — {total} days Runtime: {elapsed:.0f}s")
|
||
|
|
print(f" Entry: vel_div >= +{ENTRY_T} Exit: vel_div <= -{ENTRY_T} MaxHold: {MAX_HOLD}b")
|
||
|
|
print(f"{'='*70}")
|
||
|
|
|
||
|
|
labels = {'A_NO_GATE': 'A. NO GATE (all days)',
|
||
|
|
'B_RVOL_ONLY': 'B. RVOL gate only (>0.000203)',
|
||
|
|
'C_FULL_GATE': 'C. FULL GATE (rvol+dvol<47.5)'}
|
||
|
|
|
||
|
|
results = {}
|
||
|
|
for k, acc in accs.items():
|
||
|
|
n = acc['n']
|
||
|
|
pf = acc['gw'] / acc['gl'] if acc['gl'] > 0 else 999.0
|
||
|
|
wr = acc['wins'] / n * 100 if n > 0 else 0.0
|
||
|
|
ec = np.array(acc['ec'])
|
||
|
|
roi = (ec[-1] - 1.0) * 100
|
||
|
|
rm = np.maximum.accumulate(ec)
|
||
|
|
dd = float(np.max((rm - ec) / rm)) * 100 if len(ec) > 1 else 0.0
|
||
|
|
dr = np.array(acc['day_rets'])
|
||
|
|
sharpe_ann = float(np.mean(dr) / np.std(dr, ddof=1) * np.sqrt(252)) if len(dr) > 1 and np.std(dr, ddof=1) > 0 else 0.0
|
||
|
|
sharpe_n = float(np.mean(dr) / np.std(dr, ddof=1) * np.sqrt(len(dr))) if len(dr) > 1 and np.std(dr, ddof=1) > 0 else 0.0
|
||
|
|
results[k] = dict(pf=pf, wr=wr, n=n, roi=roi, dd=dd, sharpe_ann=sharpe_ann, sharpe_n=sharpe_n,
|
||
|
|
active=acc['active'], paused=acc['paused'])
|
||
|
|
print(f"\n {labels[k]}")
|
||
|
|
print(f" Active: {acc['active']} Paused: {acc['paused']}")
|
||
|
|
print(f" PF: {pf:.4f} WR: {wr:.2f}% N: {n:,}")
|
||
|
|
print(f" ROI: {roi:+.2f}% MaxDD: {dd:.1f}%")
|
||
|
|
print(f" Sharpe *sqrt(252): {sharpe_ann:.3f} *sqrt(n={len(dr)}): {sharpe_n:.3f}")
|
||
|
|
|
||
|
|
print(f"\n {'Metric':<10} {'A_NO_GATE':>10} {'B_RVOL':>10} {'C_FULL':>10}")
|
||
|
|
print(f" {'-'*44}")
|
||
|
|
for m in ['pf','wr','sharpe_ann','sharpe_n','active']:
|
||
|
|
vals = [results[k][m] for k in ['A_NO_GATE','B_RVOL_ONLY','C_FULL_GATE']]
|
||
|
|
fmt = '.4f' if m == 'pf' else ('.2f' if m in ('wr','sharpe_ann','sharpe_n') else 'd')
|
||
|
|
row = f" {m:<10} " + " ".join(f"{v:>{10}{fmt}}" for v in vals)
|
||
|
|
print(row)
|
||
|
|
|
||
|
|
# Per-day detail for variant A
|
||
|
|
print(f"\n Variant A per-day (sorted by PF):")
|
||
|
|
print(f" {'Date':<12} {'dvol':>5} {'fng':>4} {'pr_rvol':>10} {'N':>5} {'PF':>7} {'ret':>9}")
|
||
|
|
a_rows = [r for r in accs['A_NO_GATE']['rows'] if r['posture'] != 'NONE' and r['n'] > 0]
|
||
|
|
a_rows.sort(key=lambda r: r['pf'])
|
||
|
|
for r in a_rows:
|
||
|
|
pr_s = f"{r.get('pr',0):.7f}" if r.get('pr') else ' ?'
|
||
|
|
marker = ' BAD' if r['pf'] < 0.85 else (' WIN' if r['pf'] > 1.3 else '')
|
||
|
|
print(f" {r['date']:<12} {r['dvol']:>5.1f} {r['fng']:>4.0f} {pr_s:>10} "
|
||
|
|
f"{r['n']:>5,} {r['pf']:>7.4f} {r['day_ret']:>+9.4f}{marker}")
|