""" Exp 9b — Liquidation Guard on Extended Leverage Configs Exp9 found a DD plateau after 7x soft cap: each additional leverage unit above 7x costs only +0.12pp DD while adding ~20pp ROI. However, exp9 does NOT model exchange liquidation: the existing stop_pct=1.0 is effectively disabled, and a position at 10x leverage would be force-closed by the exchange after a 10% adverse move. This experiment adds a per-trade liquidation floor using the _pending_stop_override hook: stop_override = (1.0 / abs_cap) * 0.95 (95% of exchange margin = early warning floor) 6/7x config: fires if price moves >13.6% against position in the hold window 7/8x config: >11.9% 8/9x config: >10.6% 9/10x config: >9.5% All of these thresholds are wide relative to a 10-min BTC hold — the 55-day gold dataset almost certainly contains 0 such events for B/C, possibly 0-1 for D/E. The goal is to CONFIRM that exp9 results are not artefacts of missing liquidation model. If results are identical to exp9: liquidation risk is immaterial for this dataset. If results differ: we know exactly which config/day triggered it and the cost. Configs run (skip A = GOLD reference, already verified in exp9): B_liq: 6/7x mc_ref=5.0 + liquidation guard at 13.6% C_liq: 7/8x mc_ref=5.0 + liquidation guard at 11.9% D_liq: 8/9x mc_ref=5.0 + liquidation guard at 10.6% E_liq: 9/10x mc_ref=5.0 + liquidation guard at 9.5% Total: 4 runs × ~250s ≈ 17 min Results → exp9b_liquidation_guard_results.json """ import sys, time, json, math sys.stdout.reconfigure(encoding='utf-8', errors='replace') from pathlib import Path import numpy as np _HERE = Path(__file__).resolve().parent sys.path.insert(0, str(_HERE.parent)) from exp_shared import ( ensure_jit, ENGINE_KWARGS, GOLD, MC_BASE_CFG, load_data, load_forewarner, log_results, ) from exp9_leverage_ceiling import ExtendedLeverageEngine from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker from nautilus_dolphin.nautilus.proxy_boost_engine import DEFAULT_THRESHOLD, DEFAULT_ALPHA # Known exp9 results for comparison _EXP9 = { 'B': dict(roi=119.34, dd=16.92, trades=2155), 'C': dict(roi=141.80, dd=18.32, trades=2155), 'D': dict(roi=162.28, dd=18.44, trades=2155), 'E': dict(roi=184.00, dd=18.56, trades=2155), } _GOLD_EXP9 = dict(roi=96.55, dd=14.32, trades=2155) # ── LiquidationGuardEngine ──────────────────────────────────────────────────── class LiquidationGuardEngine(ExtendedLeverageEngine): """ Adds an exchange-liquidation floor stop to ExtendedLeverageEngine. For each entry, sets _pending_stop_override = (1/abs_cap) * margin_buffer BEFORE calling super()._try_entry(). The NDAlphaEngine._try_entry() consumes this and passes it to exit_manager.setup_position() as stop_pct_override. The exit_manager then monitors: if pnl_pct < -stop_pct_override → EXIT (liquidation). Using abs_cap (hard ceiling) slightly underestimates the actual liquidation price, i.e. we exit slightly before the exchange would — a conservative model. liquidation_stops counter tracks how many times this fires. """ def __init__(self, *args, margin_buffer: float = 0.95, **kwargs): super().__init__(*args, **kwargs) self.margin_buffer = margin_buffer self._liq_stop_pct = (1.0 / self._extended_abs_cap) * margin_buffer self.liquidation_stops = 0 def _try_entry(self, bar_idx, vel_div, prices, price_histories, v50_vel=0.0, v750_vel=0.0): # Arm the liquidation floor before parent entry logic consumes it self._pending_stop_override = self._liq_stop_pct result = super()._try_entry(bar_idx, vel_div, prices, price_histories, v50_vel, v750_vel) return result def _execute_exit(self, reason, bar_idx, pnl_pct_raw=0.0, bars_held=0): if reason == 'STOP_LOSS': self.liquidation_stops += 1 return super()._execute_exit(reason, bar_idx, pnl_pct_raw, bars_held) def reset(self): super().reset() self._liq_stop_pct = (1.0 / self._extended_abs_cap) * self.margin_buffer self.liquidation_stops = 0 # ── Run harness ─────────────────────────────────────────────────────────────── def _run(engine_factory, name, d, fw): kw = ENGINE_KWARGS.copy() acb = AdaptiveCircuitBreaker() acb.preload_w750(d['date_strings']) eng = engine_factory(kw) eng.set_ob_engine(d['ob_eng']) eng.set_acb(acb) if fw is not None: eng.set_mc_forewarner(fw, MC_BASE_CFG) eng.set_esoteric_hazard_multiplier(0.0) daily_caps, daily_pnls = [], [] for pf in d['parquet_files']: ds = pf.stem df, acols, dvol = d['pq_data'][ds] cap_before = eng.capital vol_ok = np.where(np.isfinite(dvol), dvol > d['vol_p60'], False) eng.process_day(ds, df, acols, vol_regime_ok=vol_ok) daily_caps.append(eng.capital) daily_pnls.append(eng.capital - cap_before) tr = eng.trade_history n = len(tr) roi = (eng.capital - 25000.0) / 25000.0 * 100.0 liq_stops = getattr(eng, 'liquidation_stops', 0) if n == 0: return dict(name=name, roi=roi, pf=0.0, dd=0.0, wr=0.0, sharpe=0.0, trades=0, avg_leverage=0.0, liq_stops=liq_stops, liq_stop_pct=getattr(eng, '_liq_stop_pct', 0.0)) def _abs(t): return t.pnl_absolute if hasattr(t, 'pnl_absolute') else t.pnl_pct * 250.0 wins = [t for t in tr if _abs(t) > 0] losses = [t for t in tr if _abs(t) <= 0] wr = len(wins) / n * 100.0 pf_val = sum(_abs(t) for t in wins) / max(abs(sum(_abs(t) for t in losses)), 1e-9) peak_cap, max_dd = 25000.0, 0.0 for cap in daily_caps: peak_cap = max(peak_cap, cap) max_dd = max(max_dd, (peak_cap - cap) / peak_cap * 100.0) dr = np.array([p / 25000.0 * 100.0 for p in daily_pnls]) sharpe = float(dr.mean() / (dr.std() + 1e-9) * math.sqrt(365)) if len(dr) > 1 else 0.0 lev_vals = [t.leverage for t in tr if hasattr(t, 'leverage') and t.leverage > 0] avg_lev = float(np.mean(lev_vals)) if lev_vals else 0.0 return dict( name=name, roi=roi, pf=pf_val, dd=max_dd, wr=wr, sharpe=sharpe, trades=n, avg_leverage=avg_lev, liq_stops=liq_stops, liq_stop_pct=getattr(eng, '_liq_stop_pct', 0.0), liq_stop_rate_pct=liq_stops / n * 100.0 if n else 0.0, ) # ── Main ───────────────────────────────────────────────────────────────────── def main(): t_start = time.time() print("=" * 76) print("Exp 9b — Liquidation Guard on Extended Leverage Configs") print("=" * 76) print(" Adding exchange-liquidation floor stop to exp9 B/C/D/E configs.") print(" Stop fires if adverse move exceeds (1/abs_cap)*0.95 of position.") ensure_jit() d = load_data() fw = load_forewarner() _PROXY = dict(threshold=DEFAULT_THRESHOLD, alpha=DEFAULT_ALPHA, adaptive_beta=True, adaptive_alpha=False, adaptive_thr=False) # (label, soft_cap, abs_cap, mc_ref, exp9_key) configs = [ ("B_liq_6/7x_liqstop13.6%", 6.0, 7.0, 5.0, 'B'), ("C_liq_7/8x_liqstop11.9%", 7.0, 8.0, 5.0, 'C'), ("D_liq_8/9x_liqstop10.6%", 8.0, 9.0, 5.0, 'D'), ("E_liq_9/10x_liqstop9.5%", 9.0, 10.0, 5.0, 'E'), ] results = [] for i, (label, soft, hard, mc_ref, exp9_key) in enumerate(configs): t0 = time.time() liq_pct = (1.0 / hard) * 0.95 * 100 print(f"\n[{i+1}/{len(configs)}] {label} (floor={liq_pct:.1f}%) ...") def _factory(kw, s=soft, h=hard, r=mc_ref): return LiquidationGuardEngine( extended_soft_cap=s, extended_abs_cap=h, mc_leverage_ref=r, margin_buffer=0.95, **_PROXY, **kw, ) res = _run(_factory, label, d, fw) elapsed = time.time() - t0 ref = _EXP9[exp9_key] dROI = res['roi'] - ref['roi'] dDD = res['dd'] - ref['dd'] print(f" ROI={res['roi']:>8.2f}% (vs exp9: {dROI:+.2f}pp) " f"DD={res['dd']:>6.2f}% (vs exp9: {dDD:+.2f}pp) " f"Trades={res['trades']}") print(f" avg_lev={res['avg_leverage']:.2f}x " f"liquidation_stops={res['liq_stops']} " f"liq_rate={res['liq_stop_rate_pct']:.2f}% ({elapsed:.0f}s)") results.append(res) # ── Summary ────────────────────────────────────────────────────────────── print(f"\n{'='*76}") print("LIQUIDATION GUARD IMPACT vs EXP9 (unguarded)") print(f"{'Config':<34} {'ROI(9b)':>8} {'ROI(9)':>8} {'ΔROI':>7} " f"{'DD(9b)':>7} {'DD(9)':>7} {'ΔDD':>6} {'LiqStops':>9}") print('-' * 90) for r in results: key = r['name'][0] # B/C/D/E ref = _EXP9[key] dROI = r['roi'] - ref['roi'] dDD = r['dd'] - ref['dd'] identical = (abs(dROI) < 0.01 and abs(dDD) < 0.01 and r['liq_stops'] == 0) flag = '✓ identical' if identical else '✗ CHANGED' print(f"{r['name']:<34} {r['roi']:>8.2f} {ref['roi']:>8.2f} {dROI:>+7.2f} " f"{r['dd']:>7.2f} {ref['dd']:>7.2f} {dDD:>+6.2f} " f"{r['liq_stops']:>6} {flag}") # ── Compounding with liquidation-adjusted numbers ───────────────────────── print(f"\n{'='*76}") print("COMPOUNDING TABLE — liquidation-adjusted (starting $25k, 56-day periods)") all_configs = [ ("GOLD 5/6x", _GOLD_EXP9['roi'], _GOLD_EXP9['dd']), ] + [ (r['name'][:10], r['roi'], r['dd']) for r in results ] print(f"{'Config':<20} {'ROI%':>7} {'DD%':>6} {'Calmar':>7} " f"{'3per(~5mo)':>12} {'6per(~1yr)':>12} {'12per(~2yr)':>13}") print('-' * 85) for label, roi, dd in all_configs: mult = 1.0 + roi / 100.0 calmar = roi / dd if dd > 0 else 0 v3 = 25000 * mult**3 v6 = 25000 * mult**6 v12 = 25000 * mult**12 print(f"{label:<20} {roi:>7.2f} {dd:>6.2f} {calmar:>7.2f} " f"${v3:>11,.0f} ${v6:>11,.0f} ${v12:>12,.0f}") print(f"\n{'='*76}") any_changed = any(r['liq_stops'] > 0 for r in results) if not any_changed: print("VERDICT: Zero liquidation stops fired across all configs.") print(" Exp9 results are accurate — liquidation risk is immaterial for this 55-day dataset.") print(" The compounding numbers from exp9 stand as-is.") else: changed = [r for r in results if r['liq_stops'] > 0] print(f"VERDICT: {sum(r['liq_stops'] for r in results)} liquidation stop(s) fired.") for r in changed: print(f" {r['name']}: {r['liq_stops']} stops, ΔROI={r['roi']-_EXP9[r['name'][0]]['roi']:+.2f}pp") outfile = _HERE / "exp9b_liquidation_guard_results.json" log_results(results, outfile, gold=_GOLD_EXP9, meta={ "exp": "exp9b", "question": "Do liquidation stops fire in 55-day dataset? Are exp9 results accurate?", "exp9_reference": _EXP9, "total_elapsed_s": round(time.time() - t_start, 1), }) total = time.time() - t_start print(f"\nTotal elapsed: {total / 60:.1f} min") print("Done.") if __name__ == "__main__": main()