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