Files
DOLPHIN/nautilus_dolphin/dvae/exp9b_liquidation_guard.py

280 lines
12 KiB
Python
Raw Normal View History

"""
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()