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
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
"""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})"
|
||||
)
|
||||
Reference in New Issue
Block a user