""" Exp 7 — Live proxy_B Coupling: Scale, Hold-Limit, Rising-Exit Context: proxy_B = instability_50 - v750_lambda_max_velocity AUC=0.715 for eigenspace stress, r=+0.42 (p=0.003) with intraday MAE. Orthogonal to vel_div (r=-0.03, ns). Exp6 confirmed: stop-tightening is DEAD (re-entry cascade, worse DD always). Exp4 retroactive modes A/B/C/D were 98% invalid (entry_bar alignment bug — entry_bar = global_bar_idx but day_proxy keyed by local ri). This experiment is the FIRST valid live test of those four coupling modes. Modes tested: B. scale_boost: size UP when proxy_B is LOW at entry (calm = better quality) A. scale_suppress: size DOWN when proxy_B is HIGH at entry (stress = worse quality) C. hold_limit: exit early when proxy_B_max during hold exceeds threshold D. rising_exit: exit early when current proxy_B rises above threshold during hold Implementation notes: - Modes B and A: post-entry notional scaling. No timing change → no re-entry dynamics. ProxyScaleEngine._try_entry calls super() then scales self.position.notional. - Modes C and D: in process_day loop, per-bar condition check BEFORE step_bar. If triggered: _execute_exit(force reason) → position cleared → step_bar enters fresh. No re-entry on same bar (process_bar checks bar_idx > _last_exit_bar). - proxy_B history: rolling 500-bar window, minimum 20 samples before any gating activates. - threshold at entry: np.percentile(history, thr_pct * 100) computed from rolling window. Configs: Baseline: 1 scale_boost (B): 4 (thr in {0.25, 0.35} x alpha in {0.5, 1.0}) scale_suppress (A): 4 (thr in {0.75, 0.85} x alpha in {0.5, 1.0}, s_min=0.25) hold_limit (C): 3 (frac=0.5, thr_pct in {0.65, 0.75, 0.85}) rising_exit (D): 3 (frac=0.5, thr_pct in {0.65, 0.75, 0.85}) Total: 15 configs x ~240s = ~60 min Focus metric: DD < 15.05% AND ROI >= 84.1% (95% of gold=88.55%) Results logged to exp7_live_coupling_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, print_table, ) from nautilus_dolphin.nautilus.esf_alpha_orchestrator import NDAlphaEngine from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker # ── Base: proxy_B per-bar tracking ─────────────────────────────────────────── class ProxyBaseEngine(NDAlphaEngine): """Tracks proxy_B = instability_50 - v750_lambda_max_velocity per bar.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._current_proxy_b: float = 0.0 self._proxy_b_history: list = [] # rolling 500-bar window def _update_proxy(self, inst: float, v750: float) -> float: pb = inst - v750 self._current_proxy_b = pb self._proxy_b_history.append(pb) if len(self._proxy_b_history) > 500: self._proxy_b_history = self._proxy_b_history[-500:] return pb def _proxy_prank(self) -> float: """Percentile rank of current proxy_B in history (0=low, 1=high).""" if not self._proxy_b_history: return 0.5 n = len(self._proxy_b_history) return sum(v < self._current_proxy_b for v in self._proxy_b_history) / n def _proxy_threshold(self, thr_pct: float) -> float: """Absolute threshold: percentile thr_pct of rolling history.""" if len(self._proxy_b_history) < 20: return float('inf') return float(np.percentile(self._proxy_b_history, thr_pct * 100.0)) def process_day(self, date_str, df, asset_columns, vol_regime_ok=None, direction=None, posture='APEX'): self.begin_day(date_str, posture=posture, direction=direction) bid = 0 for ri in range(len(df)): row = df.iloc[ri] vd = row.get('vel_div') if vd is None or not np.isfinite(float(vd)): self._global_bar_idx += 1; bid += 1; continue def gf(col): v = row.get(col) if v is None: return 0.0 try: return float(v) if np.isfinite(float(v)) else 0.0 except: return 0.0 v50 = gf('v50_lambda_max_velocity') v750 = gf('v750_lambda_max_velocity') inst = gf('instability_50') self._update_proxy(inst, v750) prices = {} for ac in asset_columns: p = row.get(ac) if p is not None and p > 0 and np.isfinite(float(p)): prices[ac] = float(p) if not prices: self._global_bar_idx += 1; bid += 1; continue vrok = bool(vol_regime_ok[ri]) if vol_regime_ok is not None else (bid >= 100) self._per_bar_hook(ri, float(vd), prices, v50, v750, vrok) self.step_bar(bar_idx=ri, vel_div=float(vd), prices=prices, vol_regime_ok=vrok, v50_vel=v50, v750_vel=v750) bid += 1 return self.end_day() def _per_bar_hook(self, ri, vd, prices, v50, v750, vrok): """Override in subclasses to inject pre-step_bar logic.""" pass # ── Mode B/A: position scaling ──────────────────────────────────────────────── class ProxyScaleEngine(ProxyBaseEngine): """ Mode B (scale_boost): size UP when proxy_B is LOW (calm entry conditions). Mode A (scale_suppress): size DOWN when proxy_B is HIGH (stress entry conditions). Scale factor computed from percentile rank of current proxy_B in rolling history. boost: scale = 1.0 + alpha * max(0, threshold - prank) [rk < thr → boost] suppress: scale = max(s_min, 1.0 - alpha * max(0, prank - threshold)) [rk > thr → reduce] Applied post-entry by multiplying self.position.notional directly. Does NOT change timing → no re-entry cascade dynamics. """ def __init__(self, *args, mode: str = 'boost', threshold: float = 0.35, alpha: float = 1.0, s_min: float = 0.25, **kwargs): super().__init__(*args, **kwargs) assert mode in ('boost', 'suppress') self.mode = mode self.threshold = threshold self.alpha = alpha self.s_min = s_min self._scale_history: list = [] @property def sizing_scale_mean(self) -> float: return float(np.mean(self._scale_history)) if self._scale_history else 1.0 def _try_entry(self, bar_idx, vel_div, prices, price_histories, v50_vel=0.0, v750_vel=0.0): result = super()._try_entry(bar_idx, vel_div, prices, price_histories, v50_vel, v750_vel) if result and self.position: prank = self._proxy_prank() if self.mode == 'boost': scale = 1.0 + self.alpha * max(0.0, self.threshold - prank) else: # suppress scale = max(self.s_min, 1.0 - self.alpha * max(0.0, prank - self.threshold)) self.position.notional *= scale self._scale_history.append(scale) return result def reset(self): super().reset() self._scale_history = [] # ── Mode C: hold_limit ──────────────────────────────────────────────────────── class ProxyHoldLimitEngine(ProxyBaseEngine): """ Exit early when: - proxy_B during hold has exceeded thr_pct-percentile threshold - AND bars_held >= frac * max_hold_bars Threshold computed at entry time from rolling proxy_B history. Tracks max proxy_B seen since entry; fires as soon as condition is met. """ def __init__(self, *args, frac: float = 0.5, thr_pct: float = 0.75, **kwargs): super().__init__(*args, **kwargs) self.frac = frac self.thr_pct = thr_pct self._pb_hold_threshold: float = float('inf') # set at entry self._pb_max_so_far: float = 0.0 # tracked during hold self.early_exits: int = 0 def _try_entry(self, bar_idx, vel_div, prices, price_histories, v50_vel=0.0, v750_vel=0.0): result = super()._try_entry(bar_idx, vel_div, prices, price_histories, v50_vel, v750_vel) if result: self._pb_hold_threshold = self._proxy_threshold(self.thr_pct) self._pb_max_so_far = self._current_proxy_b return result def _per_bar_hook(self, ri, vd, prices, v50, v750, vrok): if self.position is None: return # Track proxy_B max during hold self._pb_max_so_far = max(self._pb_max_so_far, self._current_proxy_b) bars_held = self._global_bar_idx - self.position.entry_bar min_bars = int(self.frac * self.exit_manager.max_hold_bars) if (bars_held >= min_bars and self._pb_max_so_far > self._pb_hold_threshold and self.position.asset in prices): self.position.current_price = prices[self.position.asset] self._execute_exit("PROXY_HOLD_LIMIT", self._global_bar_idx, bars_held=bars_held) self._pb_max_so_far = 0.0 self._pb_hold_threshold = float('inf') self.early_exits += 1 def reset(self): super().reset() self._pb_hold_threshold = float('inf') self._pb_max_so_far = 0.0 self.early_exits = 0 # ── Mode D: rising_exit ─────────────────────────────────────────────────────── class ProxyRisingExitEngine(ProxyBaseEngine): """ Exit early when: - current proxy_B exceeds thr_pct-percentile of rolling history - AND bars_held >= frac * max_hold_bars Interpretation: proxy_B has risen to an elevated level during hold → eigenspace stress is actively high → exit before MAE deepens further. Threshold computed at entry time from rolling proxy_B history. """ def __init__(self, *args, frac: float = 0.5, thr_pct: float = 0.80, **kwargs): super().__init__(*args, **kwargs) self.frac = frac self.thr_pct = thr_pct self._pb_entry_threshold: float = float('inf') # set at entry self.early_exits: int = 0 def _try_entry(self, bar_idx, vel_div, prices, price_histories, v50_vel=0.0, v750_vel=0.0): result = super()._try_entry(bar_idx, vel_div, prices, price_histories, v50_vel, v750_vel) if result: self._pb_entry_threshold = self._proxy_threshold(self.thr_pct) return result def _per_bar_hook(self, ri, vd, prices, v50, v750, vrok): if self.position is None: return bars_held = self._global_bar_idx - self.position.entry_bar min_bars = int(self.frac * self.exit_manager.max_hold_bars) if (bars_held >= min_bars and self._current_proxy_b > self._pb_entry_threshold and self.position.asset in prices): self.position.current_price = prices[self.position.asset] self._execute_exit("PROXY_RISING_EXIT", self._global_bar_idx, bars_held=bars_held) self._pb_entry_threshold = float('inf') self.early_exits += 1 def reset(self): super().reset() self._pb_entry_threshold = float('inf') self.early_exits = 0 # ── Run harness ─────────────────────────────────────────────────────────────── def _run(engine_factory, name, d, fw): """Full 55-day backtest. Returns gold-comparable metrics + coupling-specific stats.""" 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 if n == 0: return dict(name=name, roi=roi, pf=0.0, dd=0.0, wr=0.0, sharpe=0.0, trades=0, early_exits=0, sizing_scale_mean=1.0, exit_reasons={}) 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 = 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 exit_reasons = {} for t in tr: r = t.exit_reason if hasattr(t, 'exit_reason') else 'UNKNOWN' exit_reasons[r] = exit_reasons.get(r, 0) + 1 early_exits = getattr(eng, 'early_exits', 0) sizing_scale_mean = getattr(eng, 'sizing_scale_mean', 1.0) return dict( name=name, roi=roi, pf=pf, dd=max_dd, wr=wr, sharpe=sharpe, trades=n, early_exits=early_exits, early_exit_rate=early_exits / n if n > 0 else 0.0, sizing_scale_mean=sizing_scale_mean, exit_reasons=exit_reasons, ) # ── Main ────────────────────────────────────────────────────────────────────── def main(): t_start = time.time() print("=" * 72) print("Exp 7 — Live proxy_B Coupling: Scale + Hold-Limit + Rising-Exit") print("Note: First valid live test — exp4 retroactive was 98% median-filled.") print("=" * 72) ensure_jit() d = load_data() fw = load_forewarner() configs = [] # Baseline configs.append(("A00_baseline", lambda kw: NDAlphaEngine(**kw))) # Mode B: scale_boost — size UP when proxy_B is LOW for thr in [0.25, 0.35]: for alpha in [0.5, 1.0]: tag = f"B_boost_thr={thr:.2f}_a={alpha:.1f}" def _make_boost(t, a): return lambda kw: ProxyScaleEngine(mode='boost', threshold=t, alpha=a, **kw) configs.append((tag, _make_boost(thr, alpha))) # Mode A: scale_suppress — size DOWN when proxy_B is HIGH for thr in [0.75, 0.85]: for alpha in [0.5, 1.0]: tag = f"A_suppress_thr={thr:.2f}_a={alpha:.1f}_smin=0.25" def _make_suppress(t, a): return lambda kw: ProxyScaleEngine(mode='suppress', threshold=t, alpha=a, s_min=0.25, **kw) configs.append((tag, _make_suppress(thr, alpha))) # Mode C: hold_limit — exit early when pb_max during hold exceeds threshold for thr_pct in [0.65, 0.75, 0.85]: tag = f"C_hold_limit_frac=0.5_thr={thr_pct:.2f}" def _make_hold(tp): return lambda kw: ProxyHoldLimitEngine(frac=0.5, thr_pct=tp, **kw) configs.append((tag, _make_hold(thr_pct))) # Mode D: rising_exit — exit early when current pb exceeds threshold during hold for thr_pct in [0.65, 0.75, 0.85]: tag = f"D_rising_exit_frac=0.5_thr={thr_pct:.2f}" def _make_rising(tp): return lambda kw: ProxyRisingExitEngine(frac=0.5, thr_pct=tp, **kw) configs.append((tag, _make_rising(thr_pct))) results = [] for i, (name, factory) in enumerate(configs): t0 = time.time() print(f"\n[{i+1}/{len(configs)}] {name} ...") res = _run(factory, name, d, fw) elapsed = time.time() - t0 extra = "" if res['early_exits'] > 0: extra = f" early_exits={res['early_exits']}" if abs(res['sizing_scale_mean'] - 1.0) > 0.001: extra += f" scale_mean={res['sizing_scale_mean']:.4f}" print(f" ROI={res['roi']:.2f}% PF={res['pf']:.4f} DD={res['dd']:.2f}% " f"WR={res['wr']:.2f}% Sharpe={res['sharpe']:.3f} Trades={res['trades']}" f"{extra} ({elapsed:.0f}s)") results.append(res) # Verification baseline = results[0] gold_match = ( abs(baseline['roi'] - GOLD['roi']) < 0.5 and abs(baseline['pf'] - GOLD['pf']) < 0.005 and abs(baseline['dd'] - GOLD['dd']) < 0.5 and abs(baseline['trades'] - GOLD['trades']) < 10 ) print(f"\n{'='*72}") print(f"VERIFICATION: baseline ROI={baseline['roi']:.2f}% DD={baseline['dd']:.2f}%" f" Trades={baseline['trades']} Match={'PASS ✓' if gold_match else 'FAIL ✗'}") base_roi = baseline['roi'] base_dd = baseline['dd'] target_roi = GOLD['roi'] * 0.95 # 84.12% target_dd = GOLD['dd'] # 15.05% print(f"\n{'='*72}") print(f"RESULTS TABLE (target: DD<{target_dd:.2f}% AND ROI>={target_roi:.1f}%)") hdr = f"{'Config':<46} {'ROI%':>7} {'PF':>6} {'DD%':>6} {'ΔDD':>6} {'ΔROI':>6} {'extra':>20} {'OK':>4}" print(hdr) print('-' * 100) for r in results: delta_roi = r['roi'] - base_roi delta_dd = r['dd'] - base_dd ok = 'Y' if (r['dd'] < target_dd and r['roi'] >= target_roi) else 'N' extra = '' if r['early_exits'] > 0: extra = f"early={r['early_exits']}" elif abs(r['sizing_scale_mean'] - 1.0) > 0.001: extra = f"scale={r['sizing_scale_mean']:.4f}" print(f"{r['name']:<46} {r['roi']:>7.2f} {r['pf']:>6.4f} {r['dd']:>6.2f} " f"{delta_dd:>+6.2f} {delta_roi:>+6.2f} {extra:>20} {ok:>4}") # Summary by mode print(f"\n{'='*72}") print("BEST PER MODE:") def best_dd(group): return min(group, key=lambda r: r['dd']) groups = {'baseline': [], 'B_boost': [], 'A_suppress': [], 'C_hold_limit': [], 'D_rising_exit': []} for r in results: for k in groups: if r['name'].startswith(k.split('_')[0]) or r['name'].startswith(k): groups[k].append(r); break for mode, rs in groups.items(): if not rs: continue b = best_dd(rs) delta_roi = b['roi'] - base_roi delta_dd = b['dd'] - base_dd ok = 'Y' if (b['dd'] < target_dd and b['roi'] >= target_roi) else 'N' print(f" {mode:<16} best: {b['name']:<46} " f"DD={b['dd']:.2f}% ({delta_dd:+.2f}) ROI={b['roi']:.2f}% ({delta_roi:+.2f}) [{ok}]") # Exit breakdown print(f"\n{'='*72}") print("EXIT REASON BREAKDOWN:") for r in results: reasons = r.get('exit_reasons', {}) parts = ', '.join(f"{k}={v}" for k, v in sorted(reasons.items())) print(f" {r['name']:<46}: {parts}") # Save outfile = _HERE / "exp7_live_coupling_results.json" log_results(results, outfile, gold=GOLD, meta={ "exp": "exp7", "note": "First valid live test of modes A/B/C/D — exp4 retroactive was invalid (entry_bar bug)", "total_elapsed_s": round(time.time() - t_start, 1), "gold_match": gold_match, "modes_tested": ["B_scale_boost", "A_scale_suppress", "C_hold_limit", "D_rising_exit"], }) total = time.time() - t_start print(f"\nTotal elapsed: {total/60:.1f} min") print("Done.") if __name__ == "__main__": main()