"""RSK01 — Vol-target B&H + DD breaker. Hypothesis: Long-only vol-targeted (no trend signal) with a circuit breaker: - Normally always long, scaled by vol-targeting (target 20%, cap 2x) - Goes FLAT when the strategy equity drawdown from peak exceeds `dd_thresh` - Re-enters when the MARKET (asset price) recovers by `recovery_frac` from its trough level at the time the breaker fired (NOTE: recovery on MARKET price, not strategy equity — otherwise the flat position freezes equity and the breaker never clears, a death spiral) - Does the breaker beat pure vol-targeted buy&hold? Grid: 3 configs on 1d (= 6 asset-backtests, within the 6-backtest limit). """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np def rsk01_target(df, dd_thresh: float = 0.15, recovery_frac: float = 0.50) -> np.ndarray: """ Causal vol-targeted long-only position with equity-DD circuit breaker. Breaker fires when strategy equity drawdown > dd_thresh. Recovery: re-enter when asset price has risen by recovery_frac * (asset price drop from the time breaker fired). This is observable from MARKET price, avoids death-spiral. At each bar i: 1. Base vol-targeted position (direction=+1) computed causally 2. Simulated strategy equity updated by previous bar's held position 3. If equity-DD > dd_thresh → BREAKER ON, record price_trough = close[i] 4. BREAKER recovers when close[i] >= price_trough * (1 + recovery_frac * rel_drop) where rel_drop = (price_at_breaker_on - price_trough_at_bar_i) / price_at_breaker_on More simply: re-enter when close[i] >= price_trough * (1 + recovery_frac * dd_thresh) """ c = df["close"].values.astype(float) r = al.simple_returns(c) # Base vol-targeted position (always long direction=+1) direction = np.ones(len(c)) base_pos = al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) n = len(c) final_pos = np.zeros(n) # Strategy equity tracking (causal: equity at i reflects positions through i-1) eq = 1.0 peak = 1.0 breaker_on = False price_trough = np.nan # asset price when breaker fired recovery_target_price = np.nan # asset price target for re-entry for i in range(n): # Update strategy equity from previous bar's position if i > 0: prev_pos = final_pos[i - 1] eq *= (1.0 + prev_pos * r[i]) # Update running equity peak if eq > peak: peak = eq dd = (peak - eq) / peak if peak > 0 else 0.0 price_now = c[i] if not breaker_on: if dd > dd_thresh: breaker_on = True # Record asset price trough at breakout trigger price_trough = price_now # Recovery target: price rises by recovery_frac * dd_thresh above trough # (dd_thresh is a proxy for the % drop in the asset that caused the DD) recovery_target_price = price_trough * (1.0 + recovery_frac * dd_thresh) else: # Re-enter when asset recovers to recovery_target_price if price_now >= recovery_target_price: breaker_on = False price_trough = np.nan recovery_target_price = np.nan # Also reset the equity peak to current level to avoid immediate re-trigger peak = eq final_pos[i] = 0.0 if breaker_on else base_pos[i] return final_pos def make_target(dd_thresh: float, recovery_frac: float): """Factory to create a target function with fixed params.""" def _target(df): return rsk01_target(df, dd_thresh=dd_thresh, recovery_frac=recovery_frac) _target.__name__ = f"RSK01_dd{int(dd_thresh*100)}_rec{int(recovery_frac*100)}" return _target # Grid: 3 configs on 1d (= 6 asset-backtests, within the 6-backtest limit) CONFIGS_SCREEN = [ (0.10, 0.50), # tight breaker, recover 50% of dd_thresh in price terms (0.15, 0.50), # moderate breaker (0.20, 0.50), # loose breaker ] print("=== RSK01: Vol-target B&H + DD circuit breaker ===") print("Recovery measured on MARKET PRICE (not frozen strategy equity)") print("Screening 3 configs on 1d (6 asset-backtests)...") print() best_rep = None best_score = -999 best_cfg = None for dd_thresh, rec_frac in CONFIGS_SCREEN: name = f"RSK01_dd{int(dd_thresh*100)}_rec{int(rec_frac*100)}" target_fn = make_target(dd_thresh, rec_frac) rep = al.study_weights(name, target_fn, tfs=("1d",)) score = rep["verdict"].get("best_holdout_sharpe", -9) btc = rep["cells"][0]["per_asset"]["BTC"] eth = rep["cells"][0]["per_asset"]["ETH"] print(f" {name}:") print(f" BTC: full Sh={btc['full']['sharpe']:.2f} DD={btc['full']['maxdd']:.1%} " f"TIM={btc['tim']:.1%} hold Sh={btc['holdout']['sharpe']:.2f}") print(f" ETH: full Sh={eth['full']['sharpe']:.2f} DD={eth['full']['maxdd']:.1%} " f"TIM={eth['tim']:.1%} hold Sh={eth['holdout']['sharpe']:.2f}") print(f" grade={rep['verdict']['grade']} minFull={rep['verdict'].get('best_full_sharpe'):.2f} " f"minHold={score:.2f}") print() if score > best_score: best_score = score best_rep = rep best_cfg = (dd_thresh, rec_frac) print(f"Best config: dd_thresh={best_cfg[0]}, recovery_frac={best_cfg[1]}") print() # Final clean report on best config dd_thresh, rec_frac = best_cfg name = f"RSK01_dd{int(dd_thresh*100)}_rec{int(rec_frac*100)}" target_fn = make_target(dd_thresh, rec_frac) final_rep = al.study_weights(name, target_fn, tfs=("1d",)) print(al.fmt(final_rep)) print("JSON:", al.as_json(final_rep))