Files
DOLPHIN/nautilus_dolphin/dvae/exp9b_liquidation_guard.py
hjnormey 01c19662cb initial: import DOLPHIN baseline 2026-04-21 from dolphinng5_predict working tree
Includes core prod + GREEN/BLUE subsystems:
- prod/ (BLUE harness, configs, scripts, docs)
- nautilus_dolphin/ (GREEN Nautilus-native impl + dvae/ preserved)
- adaptive_exit/ (AEM engine + models/bucket_assignments.pkl)
- Observability/ (EsoF advisor, TUI, dashboards)
- external_factors/ (EsoF producer)
- mc_forewarning_qlabs_fork/ (MC regime/envelope)

Excludes runtime caches, logs, backups, and reproducible artifacts per .gitignore.
2026-04-21 16:58:38 +02:00

280 lines
12 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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