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.
280 lines
12 KiB
Python
Executable File
280 lines
12 KiB
Python
Executable File
"""
|
||
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()
|