from __future__ import annotations import asyncio import sqlite3 from datetime import date, timedelta import pytest from app.services import backtest_service as bt from scripts.portfolio_capacity_research import ( ANCHOR_YEARS, aggregate_results, bootstrap_median_interval, build_cells, build_cohort_manifest, summarize_simulation, validate_cohort_manifest, ) from scripts.run_portfolio_construction_matrix import ( _assert_clean_worktree, _checkpoint_state, _load_snapshot, _markdown, _operational_summary, _worker_init, _worker_run_cell, _write_cell_checkpoint, ) def _prices(ords: list[int], close: float = 100.0) -> tuple: closes = [close] * len(ords) return ( ords, list(closes), [value + 1.0 for value in closes], [value - 1.0 for value in closes], list(closes), [1_000_000] * len(ords), ) def _candidate( symbol: str, day: date, *, entry: float = 100.0, stop: float = 80.0, rank: float = 90.0, ) -> dict: return { 'qualified': True, 'direction': 'long', 'symbol': symbol, 'date': day.isoformat(), 'entry': entry, 'stop': stop, 'target': entry + 100.0, 'momentum_percentile': rank, 'activation_momentum_percentile': rank, 'residual_high_vol_blend_80_20': rank, } def _business_days(start: date, end: date) -> list[date]: days: list[date] = [] current = start while current <= end: if current.weekday() < 5: days.append(current) current += timedelta(days=1) return days def test_new_simulator_option_defaults_match_explicit_defaults(): start = date(2025, 1, 6) ords = [start.toordinal() + offset for offset in range(8)] prices = {'AAA': _prices(ords)} candidates = [_candidate('AAA', start)] legacy = bt._simulate_portfolio( candidates, prices, None, 'hold', 3, include_trades=True, ) explicit = bt._simulate_portfolio( candidates, prices, None, 'hold', 3, max_positions=10, min_initial_risk_fraction=None, weekly_top_n_rebalance=False, measurement_start_date=None, hard_end_date=None, include_capacity_diagnostics=False, include_trades=True, ) assert legacy == explicit def test_load_snapshot_accepts_pre_sec_ticker_schema(tmp_path, monkeypatch): snapshot = tmp_path / 'legacy-research.sqlite' with sqlite3.connect(snapshot) as connection: connection.executescript( ''' CREATE TABLE tickers ( id INTEGER PRIMARY KEY, symbol VARCHAR(10) NOT NULL UNIQUE, name VARCHAR(120), created_at DATETIME NOT NULL ); CREATE TABLE ohlcv_records ( id INTEGER PRIMARY KEY, ticker_id INTEGER NOT NULL, date DATE NOT NULL, open FLOAT NOT NULL, high FLOAT NOT NULL, low FLOAT NOT NULL, close FLOAT NOT NULL, volume BIGINT NOT NULL, created_at DATETIME NOT NULL ); INSERT INTO tickers VALUES (1, 'LEGACY', 'Legacy Co', '2024-01-01 00:00:00'); INSERT INTO ohlcv_records VALUES (1, 1, '2024-01-02', 100, 102, 99, 101, 1000000, '2024-01-02 00:00:00'); ''' ) async def recommendation_config(_db): return {} async def activation_config(_db): return {'min_momentum_percentile': 80.0} async def exit_policy(_db): return {'mode': 'atr_trailing', 'hold_days': 30, 'atr_multiplier': 3.0} async def benchmark_closes(_db, *, days, refresh): assert days is None assert refresh is False return {date(2024, 1, 2): 100.0} monkeypatch.setattr( 'app.services.recommendation_service.get_recommendation_config', recommendation_config, ) monkeypatch.setattr( 'app.services.admin_service.get_activation_config', activation_config, ) monkeypatch.setattr( 'app.services.paper_trade_service.get_exit_policy', exit_policy, ) monkeypatch.setattr( 'app.services.backtest_service._load_benchmark_closes_for_backtest', benchmark_closes, ) loaded = asyncio.run(_load_snapshot(snapshot, quiet=True)) assert loaded['symbols'] == ['LEGACY'] assert loaded['prices']['LEGACY'] == ( [date(2024, 1, 2).toordinal()], [100.0], [102.0], [99.0], [101.0], [1_000_000], ) with sqlite3.connect(snapshot) as connection: columns = { row[1] for row in connection.execute('PRAGMA table_info(tickers)') } assert {'cik', 'sic', 'sic_description'}.isdisjoint(columns) def test_unbounded_count_and_effective_risk_floor(): start = date(2025, 1, 6) ords = [start.toordinal() + offset for offset in range(4)] symbols = [f'S{index}' for index in range(25)] prices = {symbol: _prices(ords) for symbol in symbols} candidates = [ _candidate(symbol, start, stop=80.0, rank=100.0 - index) for index, symbol in enumerate(symbols) ] capped = bt._simulate_portfolio( candidates, prices, None, 'hold', 30, max_positions=1, hard_end_date=start + timedelta(days=4), measurement_start_date=start, include_capacity_diagnostics=True, ) unbounded = bt._simulate_portfolio( candidates, prices, None, 'hold', 30, max_positions=None, min_initial_risk_fraction=0.005, hard_end_date=start + timedelta(days=4), measurement_start_date=start, include_capacity_diagnostics=True, ) assert capped is not None and unbounded is not None assert capped['peak_positions'] == 1 assert capped['measurement_skipped_book_full'] == 24 assert unbounded['peak_positions'] > 1 assert unbounded['measurement_skipped_book_full'] == 0 assert unbounded['skipped_min_initial_risk'] > 0 assert unbounded['peak_positions'] == unbounded['trades'] def test_measurement_window_carries_state_but_excludes_pre_anchor_trade_ev(): start = date(2025, 1, 6) anchor = start + timedelta(days=2) hard_end = start + timedelta(days=7) ords = [ start.toordinal() + offset for offset in range((hard_end - start).days) ] prices = { 'AAA': _prices(ords, 100.0), 'BBB': _prices(ords, 100.0), } candidates = [ _candidate('AAA', start), _candidate('BBB', anchor + timedelta(days=1)), ] sim = bt._simulate_portfolio( candidates, prices, None, 'hold', 30, start_date=start, end_date=hard_end, measurement_start_date=anchor, hard_end_date=hard_end, include_curve=True, include_trades=True, ) assert sim is not None assert sim['simulation_start_date'] == start.isoformat() assert sim['start_date'] == anchor.isoformat() assert sim['measurement_start_positions'] == 1 assert sim['trades'] == 1 assert [trade['symbol'] for trade in sim['trade_details']] == ['BBB'] assert sim['equity_curve'][0]['date'] == anchor.isoformat() def test_weekly_top10_uses_current_rank_for_both_sides_not_entry_rank(): monday = date(2025, 1, 6) friday = date(2025, 1, 10) sessions = _business_days(monday, friday) ords = [session.toordinal() for session in sessions] prices = { 'AAA': _prices(ords), 'BBB': _prices(ords), } candidates = [ _candidate('AAA', monday, rank=99.0), _candidate('BBB', friday, rank=10.0), ] rank_map = { ('AAA', friday.isoformat()): {'strategy_rank': 10.0}, ('BBB', friday.isoformat()): {'strategy_rank': 90.0}, } sim = bt._simulate_portfolio( candidates, prices, None, 'hold', 30, max_positions=1, weekly_top_n_rebalance=True, daily_rank_map=rank_map, measurement_start_date=monday, hard_end_date=friday + timedelta(days=1), include_trades=True, include_capacity_diagnostics=True, ) assert sim is not None assert [trade['symbol'] for trade in sim['trade_details']] == ['AAA', 'BBB'] assert sim['trade_details'][0]['reason'] == 'weekly_rebalance' event = sim['weekly_rebalance_events'][0] assert event['exited_symbols'] == ['AAA'] assert event['selected_entrant_symbols'] == ['BBB'] def test_weekly_top10_incumbent_wins_exact_current_rank_tie(): monday = date(2025, 1, 6) friday = date(2025, 1, 10) sessions = _business_days(monday, friday) ords = [session.toordinal() for session in sessions] prices = { 'AAA': _prices(ords), 'BBB': _prices(ords), } candidates = [ _candidate('AAA', monday, rank=10.0), _candidate('BBB', friday, rank=99.0), ] rank_map = { ('AAA', friday.isoformat()): {'strategy_rank': 80.0}, ('BBB', friday.isoformat()): {'strategy_rank': 80.0}, } sim = bt._simulate_portfolio( candidates, prices, None, 'hold', 30, max_positions=1, weekly_top_n_rebalance=True, daily_rank_map=rank_map, measurement_start_date=monday, hard_end_date=friday + timedelta(days=1), include_trades=True, ) assert sim is not None assert [trade['symbol'] for trade in sim['trade_details']] == ['AAA'] assert sim['trade_details'][0]['reason'] == 'open_at_end' assert sim['weekly_rebalance_events'][0]['replacements'] == 0 def test_weekly_rebalance_exit_bypasses_cooldown_and_churn_is_counted(): first_monday = date(2025, 1, 6) friday = date(2025, 1, 10) next_monday = date(2025, 1, 13) sessions = _business_days(first_monday, next_monday) ords = [session.toordinal() for session in sessions] prices = { 'AAA': _prices(ords), 'BBB': ( ords, [100.0] * len(ords), [101.0] * len(ords), [99.0] * (len(ords) - 1) + [70.0], [100.0] * len(ords), [1_000_000] * len(ords), ), } candidates = [ _candidate('AAA', first_monday, rank=99.0), _candidate('BBB', friday, rank=10.0), _candidate('AAA', next_monday, rank=99.0), ] rank_map = { ('AAA', friday.isoformat()): {'strategy_rank': 10.0}, ('BBB', friday.isoformat()): {'strategy_rank': 90.0}, } sim = bt._simulate_portfolio( candidates, prices, None, 'hold', 30, max_positions=1, reentry_cooldown_sessions=5, weekly_top_n_rebalance=True, daily_rank_map=rank_map, measurement_start_date=first_monday, hard_end_date=next_monday + timedelta(days=1), include_trades=True, ) assert sim is not None assert [trade['symbol'] for trade in sim['trade_details']] == [ 'AAA', 'BBB', 'AAA', ] assert sim['trade_details'][0]['reason'] == 'weekly_rebalance' assert sim['rebalance_reentries_within_5_sessions'] == 1 assert sim['skipped_cooldown'] == 0 def test_cohort_manifest_realizes_seven_frozen_clusters(): sessions = _business_days(date(2016, 1, 4), date(2026, 7, 17)) manifest = build_cohort_manifest(sessions) assert validate_cohort_manifest(manifest) == [] assert manifest['empty_cluster_count'] == 7 assert manifest['warm_cluster_count'] == 7 assert set(map(int, manifest['empty_cluster_counts'])) == set(ANCHOR_YEARS) assert all( int(count) >= 12 for count in manifest['warm_seed_counts'].values() ) cells = build_cells(manifest) assert len(cells) == ( len(manifest['empty_book']) + len(manifest['warm_book']) ) * 4 * 2 def test_zero_outcome_horizon_extends_rank_replay_to_last_session(monkeypatch): monkeypatch.setattr(bt, '_window_setups', lambda *_args, **_kwargs: []) count = bt.MIN_LOOKBACK + bt.HORIZON start = date(2025, 1, 1) ords = [start.toordinal() + offset for offset in range(count)] columns = _prices(ords) legacy = bt._replay_candidates_for_period( 'AAA', columns, {}, {}, None, date.min, 'daily', True, True, ) zero_horizon = bt._replay_candidates_for_period( 'AAA', columns, {}, {}, None, date.min, 'daily', True, True, 0, ) assert len(zero_horizon) == len(legacy) + bt.HORIZON assert zero_horizon[-1]['date'] == date.fromordinal(ords[-1]).isoformat() def test_gain_to_pain_uses_all_monthly_returns_and_net_r(): sim = { 'measurement_start_equity': 100.0, 'trade_details': [ { 'net_r': 1.0, 'pnl': 10.0, 'shares': 1.0, 'entry': 100.0, 'fill': 110.0, 'transaction_cost': 0.0, }, { 'net_r': -0.5, 'pnl': -5.0, 'shares': 1.0, 'entry': 100.0, 'fill': 95.0, 'transaction_cost': 0.0, }, ], 'equity_curve': [ {'date': '2025-01-31', 'equity': 110.0}, {'date': '2025-02-28', 'equity': 99.0}, ], 'trades': 2, 'skipped_book_full': 0, } summary = summarize_simulation(sim) assert summary['ev_net_r'] == pytest.approx(0.25) assert summary['profit_factor'] == pytest.approx(2.0) # Monthly returns are +10% and -10%; all-return numerator is zero. assert summary['gain_to_pain'] == pytest.approx(0.0) def test_simple_cluster_bootstrap_is_deterministic_and_not_a_gate(): first = bootstrap_median_interval( [1, 2, 3, 4, 5, 6, 7], seed_parts=('determinism',), replicates=500, ) second = bootstrap_median_interval( [1, 2, 3, 4, 5, 6, 7], seed_parts=('determinism',), replicates=500, ) assert first == second assert first['point'] == 4 assert first['p05'] <= first['point'] <= first['p95'] def test_aggregate_reports_paired_years_and_separate_warm_iqrs(): cells: list[dict] = [] for cost in (0.1, 0.2): for cluster in ANCHOR_YEARS: for seed in range(3): path_id = f'warm-{cluster}-{seed}' for arm_id, shift in ( ('cap10_incumbent', 0.0), ('cash_unbounded', 0.2), ('cap10_weekly_top10', 0.1), ('cap15_incumbent', 0.05), ): cells.append({ 'arm_id': arm_id, 'protocol': 'warm_book', 'path_id': path_id, 'cluster': cluster, 'cost_per_side_pct': cost, 'metrics': { 'ev_net_r': seed + shift, 'calmar': 1.0 + seed * 0.1 + shift, 'profit_factor': 1.5 + shift, 'gain_to_pain': 2.0 + shift, 'sortino': 1.0 + shift, 'cagr_pct': 10.0 + shift, 'max_drawdown_pct': 5.0, 'total_return_pct': 10.0 + shift, 'sharpe': 1.0 + shift, }, }) for arm_id, shift in ( ('cap10_incumbent', 0.0), ('cash_unbounded', 0.2), ('cap10_weekly_top10', 0.1), ('cap15_incumbent', 0.05), ): cells.append({ 'arm_id': arm_id, 'protocol': 'empty_book', 'path_id': f'empty-{cluster}', 'cluster': cluster, 'cost_per_side_pct': cost, 'metrics': { 'ev_net_r': 1.0 + shift, 'calmar': 2.0 + shift, 'profit_factor': 1.5 + shift, 'gain_to_pain': 2.0 + shift, 'sortino': 1.0 + shift, 'cagr_pct': 10.0 + shift, 'max_drawdown_pct': 5.0, 'total_return_pct': 10.0 + shift, 'sharpe': 1.0 + shift, }, }) report = aggregate_results(cells) cash_empty = next( row for row in report['paired_per_year'] if row['arm_id'] == 'cash_unbounded' and row['protocol'] == 'empty_book' and row['cost_per_side_pct'] == 0.1 ) assert cash_empty['headline']['ev_net_r']['paired_delta_median'] == pytest.approx( 0.2 ) cash_warm = next( row for row in report['warm_seed_dispersion'] if row['arm_id'] == 'cash_unbounded' and row['cost_per_side_pct'] == 0.1 ) assert set(cash_warm['headline']) == {'ev_net_r', 'calmar'} assert 'D' not in cash_warm markdown = _markdown({ 'generated_at': '2026-08-05T00:00:00Z', 'analysis': report, 'operational_summary': _operational_summary(cells), }) assert 'ΔGain-to-Pain' in markdown assert 'formal promotion gate' in markdown def test_synthetic_worker_matrix_covers_four_arms_protocols_and_costs(): start = date(2025, 1, 6) sessions = _business_days(start, date(2025, 1, 17)) ords = [session.toordinal() for session in sessions] symbols = [f'S{index}' for index in range(12)] prices = {symbol: _prices(ords) for symbol in symbols} candidates = [ _candidate(symbol, start, rank=99.0 - index) for index, symbol in enumerate(symbols[:11]) ] friday = date(2025, 1, 10) candidates.append(_candidate('S11', friday, rank=99.0)) rank_map = { (symbol, friday.isoformat()): { 'strategy_rank': 100.0 if symbol == 'S11' else float(index) } for index, symbol in enumerate(symbols) } _worker_init({ 'qualified_candidates': candidates, 'daily_rank_map': rank_map, 'prices': prices, 'benchmark_closes': None, 'ranking_key': 'residual_high_vol_blend_80_20', 'exit_policy': 'hold', 'hold_days': 30, 'risk_per_trade': 0.01, 'atr_trail_multiplier': 3.0, }) rows = [] for protocol, measurement_start in ( ('empty_book', start), ('warm_book', date(2025, 1, 8)), ): for cost in (0.1, 0.2): for arm_id in ( 'cap10_incumbent', 'cash_unbounded', 'cap10_weekly_top10', 'cap15_incumbent', ): rows.append(_worker_run_cell({ 'cell_id': f'{arm_id}|{protocol}|{cost}', 'arm_id': arm_id, 'protocol': protocol, 'path_id': f'{protocol}-synthetic', 'cluster': 2025, 'simulation_start': start.isoformat(), 'measurement_start': measurement_start.isoformat(), 'hard_end_exclusive': date(2025, 1, 14).isoformat(), 'cost_per_side_pct': cost, })) assert len(rows) == 16 assert {row['arm_id'] for row in rows} == { 'cap10_incumbent', 'cash_unbounded', 'cap10_weekly_top10', 'cap15_incumbent', } assert {row['protocol'] for row in rows} == {'empty_book', 'warm_book'} assert {row['cost_per_side_pct'] for row in rows} == {0.1, 0.2} assert all('ev_net_r' in row['metrics'] for row in rows) def test_checkpoint_resume_rejects_fingerprint_mismatch(tmp_path): checkpoint = tmp_path / 'checkpoint' completed = _checkpoint_state(checkpoint, 'fingerprint-a', resume=False) assert completed == {} _write_cell_checkpoint( checkpoint, {'cell_id': 'one', 'metrics': {'ev_net_r': 1.0}}, ) resumed = _checkpoint_state(checkpoint, 'fingerprint-a', resume=True) assert set(resumed) == {'one'} with pytest.raises(SystemExit, match='fingerprint mismatch'): _checkpoint_state(checkpoint, 'fingerprint-b', resume=True) def test_dirty_worktree_guard(monkeypatch): monkeypatch.setattr( 'scripts.run_portfolio_construction_matrix._git_output', lambda *_args: ' M changed.py', ) with pytest.raises(SystemExit, match='dirty worktree'): _assert_clean_worktree()