"""Fixed-budget validation of registered strategies on the shared execution engine.""" from __future__ import annotations import copy import hashlib import json from datetime import datetime, timezone from pathlib import Path import pandas as pd import metrics import data_manager from backtest_runner import (ENGINE_VERSION, load_etf_panels, load_stock_panels, prepare_strategy, replay_prepared) from trader import ETF_STRATEGY_UNIVERSES, make_strategy from strategies.robust_momentum import RobustMomentumStrategy from research.lab.trials import append_trials def _stats(equity): return {k: v for k, v in metrics.raw_summary(equity).items() if k in ('annualizedReturn', 'maxDrawdown', 'sharpeRatio', 'annualizedVolatility')} def evaluate_registered(args): market = 'cn' if args.market.startswith('cn') else 'us' destination = Path(args.output or f'data/research/results/registered_{market}') destination.mkdir(parents=True, exist_ok=True) etf_names = [n for n in args.registered if n in ETF_STRATEGY_UNIVERSES] stock_names = [n for n in args.registered if n not in ETF_STRATEGY_UNIVERSES] stock = load_stock_panels(market, stock_names, None, refresh=False) if stock_names else None etfs = load_etf_panels(market, etf_names, refresh=False) if etf_names else None def source(name): """(panels, universe, membership) the strategy is replayed on, as in run_market.""" if name in ETF_STRATEGY_UNIVERSES: return etfs, ETF_STRATEGY_UNIVERSES[name], None return stock['panels'], stock['stocks'], stock['member'] ends = [p['close'].index[-1] for p in ([stock['panels']] if stock else []) + ([etfs] if etfs else [])] end = args.end or str(min(ends).date()) start = args.start or '2018-01-01' profiles = ['baseline', 'resize20', 'buffer3'] + (['cash_shy'] if market == 'us' else []) protocol = { 'created_at': datetime.now(timezone.utc).isoformat(), 'engine': ENGINE_VERSION, 'market': market, 'strategies': args.registered, 'start': start, 'end': end, 'profiles': profiles, 'one_way_costs': [.001, .0025], 'capital': 10000 if market == 'us' else 70000, 'selection_rule': 'CAGR improves at both costs; drawdown deterioration <= 2pp; improves in >=2/3 time blocks', 'role': 'historical development comparison; not independent OOS; do not auto-promote', 'source_hashes': {str(p): hashlib.sha256(p.read_bytes()).hexdigest() for p in [Path('execution.py'), Path('signals.py'), Path('backtest_runner.py')] + sorted(Path('strategies').glob('*.py'))}, 'data_hashes': {str(p): hashlib.sha256(p.read_bytes()).hexdigest() for p in ([Path('data') / f'{market}.csv', Path('data') / f'{market}_open.csv', Path('data/sp500_history.json' if market == 'us' else 'data/csi300_history.json')] if stock_names else []) + ([Path('data/etfs.csv'), Path('data/etfs_open.csv')] if etf_names else [])}, } protocol_path = destination / 'protocol.json' if protocol_path.exists(): raise FileExistsError(f'Choose a new --output to preserve the completed study: {destination}') protocol_path.write_text(json.dumps(protocol, indent=2)) rows, verdicts = [], [] fees = {'tx_cost': .001, 'fixed_fee': 2 if market == 'us' else 5, 'fee_base': 1.88 if market == 'us' else 0, 'fee_per_share': .009 if market == 'us' else 0} for name in args.registered: base_strategy = make_strategy(name, market) panels, universe, member = source(name) base_plan = prepare_strategy(name, market, panels, universe, member=member, start=start, end=end, strategy=base_strategy) if base_plan['warmup_rows'] < 315: raise ValueError('Registered comparison requires >=315 pre-window sessions; move --start or add history') dates = base_plan['prices'].index cuts = [0, len(dates)//3, 2*len(dates)//3, len(dates)] for profile in profiles: plan, band = base_plan, .1 if profile == 'resize20': band = .2 elif profile == 'buffer3': if not isinstance(base_strategy, RobustMomentumStrategy): continue changed = copy.deepcopy(base_strategy); changed.buffer = 3.0 plan = prepare_strategy(name, market, panels, universe, member=member, start=start, end=end, strategy=changed) elif profile == 'cash_shy': if 'SHY' not in panels['close']: raise ValueError('SHY bars are required for a traded cash sleeve') plan = dict(base_plan) plan['prices'] = base_plan['prices'].copy() plan['prices']['SHY'] = panels['close']['SHY'].reindex(dates) if base_plan.get('volume') is not None: plan['volume'] = base_plan['volume'].copy() plan['volume']['SHY'] = panels['volume']['SHY'].reindex(dates) target = base_plan['targets'].reindex(columns=plan['prices'].columns, fill_value=0).copy() target['SHY'] += (1-target.sum(axis=1)).clip(lower=0) plan['targets'] = target for cost in protocol['one_way_costs']: run = replay_prepared(plan, protocol['capital'], **{**fees, 'tx_cost': cost}, resize_band=band) row = {'strategy': name, 'profile': profile, 'cost': cost, **_stats(run['equity']), 'orders': len(run['trade_log']), 'fees': sum(t['commission'] for t in run['trade_log']), 'minimum_cash': float(run['cash'].min()), 'blocks': []} for a, b in zip(cuts[:-1], cuts[1:]): row['blocks'].append(_stats(run['equity'].iloc[a:b+1])) rows.append(row) append_trials(destination/'trials.jsonl', [row], protocol) print(name, profile, cost, f"CAGR {row['annualizedReturn']:.2%}", f"DD {row['maxDrawdown']:.2%}", flush=True) baseline = {r['cost']: r for r in rows if r['strategy'] == name and r['profile'] == 'baseline'} for profile in profiles[1:]: candidates = [r for r in rows if r['strategy'] == name and r['profile'] == profile] if not candidates: continue passed = all(r['annualizedReturn'] > baseline[r['cost']]['annualizedReturn'] and r['maxDrawdown'] >= baseline[r['cost']]['maxDrawdown']-.02 and sum(a['annualizedReturn'] > b['annualizedReturn'] for a,b in zip(r['blocks'],baseline[r['cost']]['blocks'])) >= 2 for r in candidates) verdicts.append({'strategy': name, 'profile': profile, 'verdict': 'candidate_for_forward_only' if passed else 'retain_baseline'}) summary = {'protocol': protocol, 'comparisons': rows, 'decisions': verdicts, 'boundary': 'Missing historical constituents and adjusted-share execution remain limitations.'} (destination/'results.json').write_text(json.dumps(summary, indent=2, allow_nan=False)) pd.DataFrame([{k:v for k,v in r.items() if k!='blocks'} for r in rows]).to_csv(destination/'results.csv',index=False) print('Saved',destination/'results.json',flush=True) return summary