"""Property-based tests for the Backtester. Feature: autonomous-trading-engine Tests Property 36 (backtester produces equivalent metrics) from the design specification. Generates random sets of historical trades and verifies that the backtester's metric computation matches the PerformanceComputer for the same trade data. """ from __future__ import annotations from datetime import date, timedelta from hypothesis import given, settings from hypothesis import strategies as st from services.trading.backtester import BacktestConfig, BacktestEngine from services.trading.models import ClosedTrade from services.trading.performance_tracker import PerformanceComputer # --------------------------------------------------------------------------- # Hypothesis strategies # --------------------------------------------------------------------------- _tickers = st.sampled_from(["AAPL", "MSFT", "GOOG", "AMZN", "TSLA", "NVDA"]) _entry_price_st = st.floats( min_value=1.0, max_value=5000.0, allow_nan=False, allow_infinity=False ) _exit_price_st = st.floats( min_value=1.0, max_value=5000.0, allow_nan=False, allow_infinity=False ) _quantity_st = st.integers(min_value=1, max_value=1000) _hold_duration_st = st.timedeltas( min_value=timedelta(minutes=1), max_value=timedelta(days=365) ) @st.composite def closed_trade_st(draw: st.DrawFn) -> ClosedTrade: """Generate a random ClosedTrade with consistent pnl fields.""" ticker = draw(_tickers) entry_price = draw(_entry_price_st) exit_price = draw(_exit_price_st) quantity = draw(_quantity_st) hold_duration = draw(_hold_duration_st) pnl = (exit_price - entry_price) * quantity pnl_pct = (exit_price - entry_price) / entry_price if entry_price > 0 else 0.0 return ClosedTrade( ticker=ticker, entry_price=entry_price, exit_price=exit_price, quantity=quantity, pnl=pnl, pnl_pct=pnl_pct, hold_duration=hold_duration, ) _trades_st = st.lists(closed_trade_st(), min_size=0, max_size=50) _daily_returns_st = st.lists( st.floats(min_value=-0.20, max_value=0.20, allow_nan=False, allow_infinity=False), min_size=0, max_size=252, ) _initial_capital_st = st.floats( min_value=100.0, max_value=100000.0, allow_nan=False, allow_infinity=False ) _risk_tier_st = st.sampled_from(["conservative", "moderate", "aggressive"]) # --------------------------------------------------------------------------- # Property 36: Backtester produces equivalent metrics # **Validates: Requirements 15.3** # --------------------------------------------------------------------------- class TestProperty36BacktesterEquivalentMetrics: """Property 36: Backtester produces equivalent metrics. **Validates: Requirements 15.3** For any set of closed trades and daily returns, the backtester's win_rate, profit_factor, and trade_count must be identical to those computed directly by PerformanceComputer. """ @settings(max_examples=100) @given( trades=_trades_st, daily_returns=_daily_returns_st, initial_capital=_initial_capital_st, risk_tier=_risk_tier_st, ) def test_win_rate_matches_performance_tracker( self, trades: list[ClosedTrade], daily_returns: list[float], initial_capital: float, risk_tier: str, ) -> None: """win_rate from BacktestEngine matches PerformanceComputer.""" config = BacktestConfig( start_date=date(2024, 1, 1), end_date=date(2024, 12, 31), initial_capital=initial_capital, risk_tier=risk_tier, ) engine = BacktestEngine() result = engine.compute_result(config, trades, daily_returns, []) perf = PerformanceComputer() metrics = perf.compute_metrics( closed_trades=trades, portfolio_value=initial_capital, active_pool=initial_capital, reserve_pool=0.0, daily_pnl=0.0, unrealized_pnl=0.0, portfolio_heat=0.0, daily_returns=daily_returns, ) assert result.win_rate == metrics.win_rate, ( f"win_rate mismatch: backtest={result.win_rate}, " f"perf_tracker={metrics.win_rate}" ) @settings(max_examples=100) @given( trades=_trades_st, daily_returns=_daily_returns_st, initial_capital=_initial_capital_st, risk_tier=_risk_tier_st, ) def test_profit_factor_matches_performance_tracker( self, trades: list[ClosedTrade], daily_returns: list[float], initial_capital: float, risk_tier: str, ) -> None: """profit_factor from BacktestEngine matches PerformanceComputer.""" config = BacktestConfig( start_date=date(2024, 1, 1), end_date=date(2024, 12, 31), initial_capital=initial_capital, risk_tier=risk_tier, ) engine = BacktestEngine() result = engine.compute_result(config, trades, daily_returns, []) perf = PerformanceComputer() metrics = perf.compute_metrics( closed_trades=trades, portfolio_value=initial_capital, active_pool=initial_capital, reserve_pool=0.0, daily_pnl=0.0, unrealized_pnl=0.0, portfolio_heat=0.0, daily_returns=daily_returns, ) assert result.profit_factor == metrics.profit_factor, ( f"profit_factor mismatch: backtest={result.profit_factor}, " f"perf_tracker={metrics.profit_factor}" ) @settings(max_examples=100) @given( trades=_trades_st, daily_returns=_daily_returns_st, initial_capital=_initial_capital_st, risk_tier=_risk_tier_st, ) def test_trade_count_matches_number_of_trades( self, trades: list[ClosedTrade], daily_returns: list[float], initial_capital: float, risk_tier: str, ) -> None: """trade_count from BacktestEngine equals len(trades).""" config = BacktestConfig( start_date=date(2024, 1, 1), end_date=date(2024, 12, 31), initial_capital=initial_capital, risk_tier=risk_tier, ) engine = BacktestEngine() result = engine.compute_result(config, trades, daily_returns, []) # trade_count should equal the number of trades passed in, # which is also win_count + loss_count from PerformanceComputer perf = PerformanceComputer() metrics = perf.compute_metrics( closed_trades=trades, portfolio_value=initial_capital, active_pool=initial_capital, reserve_pool=0.0, daily_pnl=0.0, unrealized_pnl=0.0, portfolio_heat=0.0, daily_returns=daily_returns, ) assert result.trade_count == len(trades), ( f"trade_count mismatch: backtest={result.trade_count}, " f"expected={len(trades)}" ) assert result.trade_count == metrics.win_count + metrics.loss_count, ( f"trade_count={result.trade_count} != " f"win_count({metrics.win_count}) + loss_count({metrics.loss_count})" )