Spaces:
Sleeping
Sleeping
| import sys, os | |
| import numpy as np | |
| import pandas as pd | |
| import warnings; warnings.filterwarnings('ignore') | |
| from backtesting.framework.config import STRATEGY_NAME, ACTIVE_STRATEGY_FN, ACTIVE_PARAMS, load_data | |
| try: | |
| from v30_causal_engine import evaluate_slice, CAP | |
| from v36_engine import SECTOR_MAP | |
| except ImportError: | |
| from backtesting.strategies.v30_engine import evaluate_slice, CAP | |
| from backtesting.strategies.v36_engine import SECTOR_MAP | |
| def run_v68_monkey(dc, spy, vf, daily_ret, rebal_days=40, vol_target=0.18, | |
| riskoff_haircut=0.50, sma_lookback=200, mom_long=175, | |
| mom_short=21, txn_bps=20, consistency_window=63, | |
| top_n=15, use_dd_stop=True, **kwargs): | |
| """ | |
| EXACT copy of V68 Soft Tranche architecture, but replaces the Z-Score | |
| momentum sorting with pure random selection. | |
| """ | |
| sma = spy.rolling(sma_lookback).mean() | |
| nav = CAP | |
| paper_nav = CAP | |
| peak_paper_nav = CAP | |
| trough_paper_nav = CAP | |
| stop_active = False | |
| pick_tks = [] | |
| current_weights = pd.Series(dtype=float) | |
| port_rets = [] | |
| hist = [] | |
| txn_frac = txn_bps / 10000.0 | |
| days = 0 | |
| spy_vals = spy.values | |
| sma_vals = sma.values | |
| for i in range(1, len(dc)): | |
| if len(port_rets) >= 21: | |
| w_window = port_rets[-60:] if len(port_rets) >= 60 else port_rets[-21:] | |
| vs = vol_target / (np.std(w_window)*np.sqrt(252)+1e-8) | |
| else: vs = 0.5 | |
| sp, sm = spy_vals[i-1], sma_vals[i-1] | |
| if pd.isna(sm) or sp <= sm: vs *= riskoff_haircut | |
| vs = float(np.clip(vs, 0.05, 1.0)) | |
| day_ret = 0.0 | |
| if pick_tks: | |
| lr = daily_ret.iloc[i][[t for t in pick_tks if t in daily_ret.columns]].dropna() | |
| if not lr.empty: | |
| wt = current_weights.reindex(lr.index).fillna(0) | |
| if wt.sum() > 0: wt = wt / wt.sum() | |
| day_ret = (lr * wt).sum() * vs | |
| paper_nav *= (1 + day_ret) | |
| if not stop_active: | |
| nav *= (1 + day_ret) | |
| port_rets.append(day_ret) | |
| hist.append(nav) | |
| # Drawdown Stop Logic | |
| peak_paper_nav = max(peak_paper_nav, paper_nav) | |
| paper_dd = (paper_nav / peak_paper_nav) - 1.0 | |
| if use_dd_stop: | |
| if not stop_active: | |
| if paper_dd <= -0.15: | |
| stop_active = True | |
| trough_paper_nav = paper_nav | |
| nav -= nav * txn_frac | |
| else: | |
| trough_paper_nav = min(trough_paper_nav, paper_nav) | |
| if paper_nav >= trough_paper_nav * 1.05: | |
| stop_active = False | |
| peak_paper_nav = paper_nav | |
| nav -= nav * txn_frac | |
| days += 1 | |
| if days >= rebal_days: | |
| days = 0 | |
| # Get universe of valid stocks at T-1 | |
| valid_tks = [t for t in vf if pd.notna(dc[t].iloc[i-1])] | |
| if not valid_tks: | |
| continue | |
| # MONKEY: Randomly select Top N | |
| if len(valid_tks) >= top_n: | |
| new_picks = list(np.random.choice(valid_tks, top_n, replace=False)) | |
| else: | |
| new_picks = list(np.random.choice(valid_tks, len(valid_tks), replace=False)) | |
| # True Turnover Cost | |
| if pick_tks and new_picks: | |
| swaps = len(set(new_picks) - set(pick_tks)) | |
| turnover_cost = (swaps / top_n) * txn_frac | |
| nav -= nav * turnover_cost | |
| paper_nav -= paper_nav * turnover_cost | |
| if new_picks: | |
| current_weights = pd.Series(1.0/len(new_picks), index=new_picks) | |
| pick_tks = new_picks | |
| return pd.Series(hist, index=dc.index[1:len(hist)+1]) | |
| def run_test_1_3(): | |
| print("=" * 80) | |
| print(f" TEST 1.3: V68 ARCHITECTURE MONKEY TEST - {STRATEGY_NAME}") | |
| print("=" * 80) | |
| dc, spy, vf, daily_ret = load_data() | |
| # 1. Evaluate Active Strategy | |
| print("Evaluating Active V68 Strategy...") | |
| c_orig = ACTIVE_STRATEGY_FN(dc, spy, vf, daily_ret, **ACTIVE_PARAMS) | |
| if isinstance(c_orig, dict) and 'curve' in c_orig: | |
| c_orig = c_orig['curve'] | |
| m_orig = evaluate_slice(c_orig, "2008-01-01", "2025-12-31") | |
| orig_sharpe = m_orig['sharpe'] | |
| print(f" Active Sharpe: {orig_sharpe:.4f}") | |
| # 2. Evaluate 50 Random Monkey Iterations (Inside V68 Framework) | |
| print("\nRunning 50 Random Monkey Iterations (Apples-to-Apples)...") | |
| np.random.seed(42) # Fixed seed for reproducibility | |
| rand_sharpes = [] | |
| # Pre-calculate to speed up monkey tests (since we run 50 times) | |
| for step in range(50): | |
| c_rand = run_v68_monkey(dc, spy, vf, daily_ret, **ACTIVE_PARAMS) | |
| m_rand = evaluate_slice(c_rand, "2008-01-01", "2025-12-31") | |
| rand_sharpes.append(m_rand['sharpe']) | |
| if step % 10 == 0: | |
| print(".", end="", flush=True) | |
| print() | |
| r_med = np.median(rand_sharpes) | |
| r_max = np.max(rand_sharpes) | |
| print("-" * 80) | |
| print(f" Random-Monkey Median: {r_med:.4f}") | |
| print(f" Random-Monkey Max: {r_max:.4f}") | |
| excess_monkey = orig_sharpe - r_med | |
| print(f" Excess vs Monkey Median: {excess_monkey:+.4f}") | |
| if excess_monkey > 0.15: | |
| print(" VERDICT: PASS (Signal adds strong value beyond V68 risk management)") | |
| elif excess_monkey > 0.05: | |
| print(" VERDICT: WEAK PASS (Signal adds marginal value)") | |
| else: | |
| print(" VERDICT: FAIL (Signal has no mathematical edge over random selection inside V68)") | |
| if __name__ == "__main__": | |
| run_test_1_3() |