Files
DOLPHIN/nautilus_dolphin/test_pf_acb_inverse.py

241 lines
9.8 KiB
Python
Raw Normal View History

"""Inverse ACB: BOOST shorts on stress days, normal on calm days.
Instead of ACB cutting size (hurts SHORT system), we BOOST on stress days
where shorts historically profit, and stay normal elsewhere.
Regime mapping:
0 signals SHORT normal (size_mult=1.0)
1 signal SHORT normal (size_mult=1.0) don't cut winning days!
2 signals SHORT boost (size_mult=1.3)
3+ signals SHORT boost (size_mult=1.5)
Plus: Intraday DD guard at 3% to protect against whipsaw (Feb 6 style).
"""
import sys, time
from pathlib import Path
from collections import Counter
import numpy as np
import pandas as pd
sys.path.insert(0, str(Path(__file__).parent))
print("Compiling numba kernels...")
t_jit = time.time()
from nautilus_dolphin.nautilus.alpha_asset_selector import compute_irp_nb, compute_ars_nb, rank_assets_irp_nb
from nautilus_dolphin.nautilus.alpha_bet_sizer import compute_sizing_nb
from nautilus_dolphin.nautilus.alpha_signal_generator import check_dc_nb
_p = np.array([1.0, 2.0, 3.0], dtype=np.float64)
compute_irp_nb(_p, -1)
compute_ars_nb(1.0, 0.5, 0.01)
rank_assets_irp_nb(np.ones((10, 2), dtype=np.float64), 8, -1, 5, 500.0, 20, 0.20)
compute_sizing_nb(-0.03, -0.02, -0.05, 3.0, 0.5, 5.0, 0.20, True, True, 0.0,
np.zeros(4, dtype=np.int64), np.zeros(4, dtype=np.int64),
np.zeros(5, dtype=np.float64), 0, -1, 0.01, 0.04)
check_dc_nb(_p, 3, 1, 0.75)
print(f" JIT compile: {time.time() - t_jit:.1f}s")
from nautilus_dolphin.nautilus.alpha_orchestrator import NDAlphaEngine
from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker
VBT_DIR = Path(r"C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict\vbt_cache")
META_COLS = {'timestamp', 'scan_number', 'v50_lambda_max_velocity', 'v150_lambda_max_velocity',
'v300_lambda_max_velocity', 'v750_lambda_max_velocity', 'vel_div',
'instability_50', 'instability_150'}
ENGINE_KWARGS = dict(
initial_capital=25000.0,
vel_div_threshold=-0.02, vel_div_extreme=-0.05,
min_leverage=0.5, max_leverage=5.0, leverage_convexity=3.0,
fraction=0.20, fixed_tp_pct=0.0099, stop_pct=1.0, max_hold_bars=120,
use_direction_confirm=True, dc_lookback_bars=7, dc_min_magnitude_bps=0.75,
dc_skip_contradicts=True, dc_leverage_boost=1.0, dc_leverage_reduce=0.5,
use_asset_selection=True, min_irp_alignment=0.45,
use_sp_fees=True, use_sp_slippage=True,
sp_maker_entry_rate=0.62, sp_maker_exit_rate=0.50,
use_ob_edge=True, ob_edge_bps=5.0, ob_confirm_rate=0.40,
lookback=100, use_alpha_layers=True, use_dynamic_leverage=True, seed=42,
)
acb = AdaptiveCircuitBreaker()
parquet_files = sorted(VBT_DIR.glob("*.parquet"))
acb_cuts = {pf.stem: acb.get_cut_for_date(pf.stem) for pf in parquet_files}
def get_inverse_regime(cut_info):
"""Inverse ACB: boost on stress, normal elsewhere."""
s = cut_info['signals']
if s >= 3: return 1.5 # Crash: big boost (shorts print)
elif s >= 2: return 1.3 # High stress: boost
else: return 1.0 # Normal: no change
print("\n=== Inverse ACB Regime ===")
for pf in parquet_files:
d = pf.stem
ci = acb_cuts[d]
mult = get_inverse_regime(ci)
if mult != 1.0:
print(f" {d}: x{mult:.1f} (signals={ci['signals']:.1f})")
boost_days = sum(1 for pf in parquet_files if get_inverse_regime(acb_cuts[pf.stem]) > 1.0)
print(f"Boost days: {boost_days}/{len(parquet_files)}")
# Vol percentiles
all_vols = []
for pf in parquet_files[:2]:
df = pd.read_parquet(pf)
if 'BTCUSDT' not in df.columns: continue
prices = df['BTCUSDT'].values
for i in range(60, len(prices)):
seg = prices[max(0, i-50):i]
if len(seg) < 10: continue
rets = np.diff(seg) / seg[:-1]
v = float(np.std(rets))
if v > 0: all_vols.append(v)
vol_p60 = float(np.percentile(all_vols, 60))
def run_backtest(mode="baseline"):
engine = NDAlphaEngine(**ENGINE_KWARGS)
bar_idx = 0
price_histories = {}
date_stats = []
peak_capital = engine.capital
max_dd = 0.0
for pf in parquet_files:
date_str = pf.stem
cap_start = engine.capital
trades_start = len(engine.trade_history)
# Always SHORT — just adjust size multiplier
engine.regime_direction = -1
engine.regime_dd_halt = False
if mode == "inverse":
engine.regime_size_mult = get_inverse_regime(acb_cuts[date_str])
else:
engine.regime_size_mult = 1.0
day_peak = cap_start
dd_threshold = 0.03 # 3% intraday DD guard
df = pd.read_parquet(pf)
asset_cols = [c for c in df.columns if c not in META_COLS]
btc_prices = df['BTCUSDT'].values if 'BTCUSDT' in df.columns else None
date_vol = np.full(len(df), np.nan)
if btc_prices is not None:
for i in range(50, len(btc_prices)):
seg = btc_prices[max(0, i-50):i]
if len(seg) < 10: continue
rets = np.diff(seg) / seg[:-1]
date_vol[i] = float(np.std(rets))
bars_in_date = 0
for row_i in range(len(df)):
row = df.iloc[row_i]
vel_div = row.get("vel_div")
if vel_div is None or not np.isfinite(vel_div):
bar_idx += 1; bars_in_date += 1; continue
prices = {}
for ac in asset_cols:
p = row[ac]
if p and p > 0 and np.isfinite(p):
prices[ac] = float(p)
if ac not in price_histories: price_histories[ac] = []
price_histories[ac].append(float(p))
if not prices:
bar_idx += 1; bars_in_date += 1; continue
if bars_in_date < 100:
vol_regime_ok = False
else:
v = date_vol[row_i]
vol_regime_ok = (np.isfinite(v) and v > vol_p60)
engine.process_bar(
bar_idx=bar_idx, vel_div=float(vel_div),
prices=prices, vol_regime_ok=vol_regime_ok,
price_histories=price_histories,
)
# Intraday DD guard (only on boost days)
if mode == "inverse" and engine.regime_size_mult > 1.0:
day_peak = max(day_peak, engine.capital)
if day_peak > 0:
intraday_dd = (day_peak - engine.capital) / day_peak
if intraday_dd > dd_threshold:
engine.regime_dd_halt = True
bar_idx += 1; bars_in_date += 1
cap_end = engine.capital
date_pnl = cap_end - cap_start
peak_capital = max(peak_capital, cap_end)
dd = (peak_capital - cap_end) / peak_capital * 100 if peak_capital > 0 else 0
max_dd = max(max_dd, dd)
date_stats.append({
'date': date_str, 'pnl': date_pnl,
'roi_pct': date_pnl / cap_start * 100 if cap_start > 0 else 0,
'capital': cap_end, 'dd_pct': dd,
'size_mult': engine.regime_size_mult if mode == "inverse" else 1.0,
'trades': len(engine.trade_history) - trades_start,
})
return engine, date_stats, max_dd, peak_capital
print("\n=== Running BASELINE ===")
t0 = time.time()
eng_base, stats_base, dd_base, peak_base = run_backtest("baseline")
print(f" Done: {time.time()-t0:.0f}s")
print("\n=== Running INVERSE ACB ===")
t1 = time.time()
eng_inv, stats_inv, dd_inv, peak_inv = run_backtest("inverse")
print(f" Done: {time.time()-t1:.0f}s")
# Per-date comparison (only show changed days)
print(f"\n{'='*95}")
print(f"{'DATE':<12} {'MULT':>5} {'BASE PnL':>10} {'INV PnL':>10} {'DELTA':>10} {'BASE CAP':>10} {'INV CAP':>10} {'B DD%':>7} {'I DD%':>7}")
print(f"{'='*95}")
for sb, si in zip(stats_base, stats_inv):
marker = ""
if si['pnl'] > sb['pnl'] + 50: marker = " ++"
elif si['pnl'] < sb['pnl'] - 50: marker = " --"
if si['size_mult'] != 1.0 or marker:
print(f"{sb['date']:<12} {si['size_mult']:>5.2f} {sb['pnl']:>+10.2f} {si['pnl']:>+10.2f} "
f"{si['pnl']-sb['pnl']:>+10.2f} {sb['capital']:>10.2f} {si['capital']:>10.2f} "
f"{sb['dd_pct']:>6.2f}% {si['dd_pct']:>6.2f}%{marker}")
# Summary
def summarize(label, engine, max_dd, peak, stats):
trades = engine.trade_history
if not trades: print(f"\n{label}: 0 trades"); return
wins = [t for t in trades if t.pnl_absolute > 0]
losses = [t for t in trades if t.pnl_absolute <= 0]
gross_win = sum(t.pnl_absolute for t in wins) if wins else 0
gross_loss = abs(sum(t.pnl_absolute for t in losses)) if losses else 0
pf_val = gross_win / gross_loss if gross_loss > 0 else float("inf")
daily_rets = [s['roi_pct'] for s in stats]
sharpe = np.mean(daily_rets) / np.std(daily_rets) * np.sqrt(365) if np.std(daily_rets) > 0 else 0
print(f"\n{'='*50}")
print(f" {label}")
print(f"{'='*50}")
print(f"Trades: {len(trades)}, WR: {len(wins)/len(trades)*100:.1f}%")
print(f"PF: {pf_val:.3f}")
print(f"ROI: {(engine.capital - 25000) / 25000 * 100:+.2f}%")
print(f"Final capital: ${engine.capital:.2f}")
print(f"Peak: ${peak:.2f}, MAX DRAWDOWN: {max_dd:.2f}%")
print(f"Sharpe (ann.): {sharpe:.2f}")
print(f"Fees: {engine.total_fees:.2f}")
summarize("BASELINE (SHORT-only)", eng_base, dd_base, peak_base, stats_base)
summarize("INVERSE ACB (boost on stress)", eng_inv, dd_inv, peak_inv, stats_inv)
base_roi = (eng_base.capital - 25000) / 25000 * 100
inv_roi = (eng_inv.capital - 25000) / 25000 * 100
print(f"\n{'='*50}")
print(f" DELTA")
print(f"{'='*50}")
print(f"ROI: {base_roi:+.2f}% -> {inv_roi:+.2f}% ({inv_roi-base_roi:+.2f}%)")
print(f"Max DD: {dd_base:.2f}% -> {dd_inv:.2f}% ({dd_inv-dd_base:+.2f}%)")
print(f"Total time: {time.time() - t0:.0f}s")