Files
stonks-oracle/tests/test_pbt_backtester.py
T
Celes Renata 4ffde8cc06 feat: autonomous trading engine — full implementation
- Database migration 018 with 13 tables for trading engine state
- Trading engine service (services/trading/) with 12 pure computation modules:
  position sizer, stop-loss manager, reserve pool, circuit breaker,
  risk tier controller, correlation matrix, tax lots, trading window,
  gradual entry, notifications, micro-trading, backtester
- Core TradingEngine with pre-trade evaluation pipeline and integration wiring
- FastAPI HTTP service with 14 endpoints (health, config, decisions, metrics, backtest)
- Performance tracker with Sharpe ratio, drawdown, profit factor computation
- 194 Python tests (165 property-based + 29 integration)
- Frontend: 13 TanStack Query hooks, 7 dashboard panels, tabbed Trading Engine page
- Helm chart entry, network policy, nginx proxy, ingress for trading-engine
- Shared infrastructure: enums, Redis keys, TradingConfig in AppConfig
2026-04-15 16:12:22 +00:00

228 lines
7.2 KiB
Python

"""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})"
)