4ffde8cc06
- 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
228 lines
7.2 KiB
Python
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})"
|
|
)
|