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,197 @@
|
||||
"""Performance tracker for the autonomous trading engine.
|
||||
|
||||
Pure computation module that computes portfolio-wide performance metrics
|
||||
from closed trades and portfolio state data.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from services.trading.models import ClosedTrade, PerformanceMetrics
|
||||
|
||||
|
||||
class PerformanceComputer:
|
||||
"""Computes portfolio performance metrics from trade data.
|
||||
|
||||
All methods are pure computations with no side effects or I/O.
|
||||
"""
|
||||
|
||||
def compute_metrics(
|
||||
self,
|
||||
closed_trades: list[ClosedTrade],
|
||||
portfolio_value: float,
|
||||
active_pool: float,
|
||||
reserve_pool: float,
|
||||
daily_pnl: float,
|
||||
unrealized_pnl: float,
|
||||
portfolio_heat: float,
|
||||
daily_returns: list[float],
|
||||
) -> PerformanceMetrics:
|
||||
"""Compute all performance metrics from trade data and portfolio state.
|
||||
|
||||
Args:
|
||||
closed_trades: List of completed trades.
|
||||
portfolio_value: Current total portfolio value.
|
||||
active_pool: Current active pool value.
|
||||
reserve_pool: Current reserve pool balance.
|
||||
daily_pnl: Today's P&L.
|
||||
unrealized_pnl: Unrealized P&L across open positions.
|
||||
portfolio_heat: Current portfolio heat value.
|
||||
daily_returns: List of daily return percentages for Sharpe/drawdown.
|
||||
|
||||
Returns:
|
||||
PerformanceMetrics with all computed fields.
|
||||
"""
|
||||
wins = [t for t in closed_trades if t.pnl > 0]
|
||||
losses = [t for t in closed_trades if t.pnl <= 0]
|
||||
|
||||
win_count = len(wins)
|
||||
loss_count = len(losses)
|
||||
total_trades = len(closed_trades)
|
||||
|
||||
win_rate = win_count / total_trades if total_trades > 0 else 0.0
|
||||
|
||||
avg_win = (
|
||||
sum(t.pnl for t in wins) / win_count if win_count > 0 else 0.0
|
||||
)
|
||||
avg_loss = (
|
||||
sum(t.pnl for t in losses) / loss_count if loss_count > 0 else 0.0
|
||||
)
|
||||
|
||||
gross_profits = sum(t.pnl for t in wins)
|
||||
gross_losses = abs(sum(t.pnl for t in losses))
|
||||
|
||||
if gross_losses > 0:
|
||||
profit_factor = gross_profits / gross_losses
|
||||
else:
|
||||
profit_factor = float("inf") if gross_profits > 0 else 0.0
|
||||
|
||||
realized_pnl = sum(t.pnl for t in closed_trades)
|
||||
|
||||
sharpe_ratio = self._compute_sharpe_ratio(daily_returns)
|
||||
max_drawdown = self._compute_max_drawdown(daily_returns)
|
||||
current_drawdown_pct = self._compute_current_drawdown(daily_returns)
|
||||
|
||||
return PerformanceMetrics(
|
||||
total_portfolio_value=portfolio_value,
|
||||
active_pool=active_pool,
|
||||
reserve_pool=reserve_pool,
|
||||
unrealized_pnl=unrealized_pnl,
|
||||
realized_pnl=realized_pnl,
|
||||
daily_pnl=daily_pnl,
|
||||
win_count=win_count,
|
||||
loss_count=loss_count,
|
||||
win_rate=win_rate,
|
||||
avg_win=avg_win,
|
||||
avg_loss=avg_loss,
|
||||
profit_factor=profit_factor,
|
||||
sharpe_ratio=sharpe_ratio,
|
||||
max_drawdown=max_drawdown,
|
||||
current_drawdown_pct=current_drawdown_pct,
|
||||
portfolio_heat=portfolio_heat,
|
||||
)
|
||||
|
||||
def compute_trade_metrics(self, trade: ClosedTrade) -> dict:
|
||||
"""Compute per-trade metrics for a single closed trade.
|
||||
|
||||
Args:
|
||||
trade: A completed trade.
|
||||
|
||||
Returns:
|
||||
Dictionary with per-trade metrics.
|
||||
"""
|
||||
return {
|
||||
"ticker": trade.ticker,
|
||||
"entry_price": trade.entry_price,
|
||||
"exit_price": trade.exit_price,
|
||||
"quantity": trade.quantity,
|
||||
"pnl": trade.pnl,
|
||||
"pnl_pct": trade.pnl_pct,
|
||||
"hold_duration": str(trade.hold_duration),
|
||||
"recommendation_id": trade.recommendation_id,
|
||||
"is_micro_trade": trade.is_micro_trade,
|
||||
"is_win": trade.pnl > 0,
|
||||
}
|
||||
|
||||
def filter_by_micro_trade(
|
||||
self,
|
||||
trades: list[ClosedTrade],
|
||||
is_micro: bool,
|
||||
) -> list[ClosedTrade]:
|
||||
"""Filter trades by micro-trade flag.
|
||||
|
||||
Args:
|
||||
trades: List of closed trades.
|
||||
is_micro: If True, return only micro-trades; if False, only standard.
|
||||
|
||||
Returns:
|
||||
Filtered list of trades.
|
||||
"""
|
||||
return [t for t in trades if t.is_micro_trade == is_micro]
|
||||
|
||||
@staticmethod
|
||||
def _compute_sharpe_ratio(daily_returns: list[float]) -> float:
|
||||
"""Compute annualized Sharpe ratio from daily returns.
|
||||
|
||||
Formula: (mean_daily_return / std_daily_return) * sqrt(252)
|
||||
Returns 0.0 if fewer than 2 data points or std is 0.
|
||||
"""
|
||||
if len(daily_returns) < 2:
|
||||
return 0.0
|
||||
|
||||
n = len(daily_returns)
|
||||
mean_return = sum(daily_returns) / n
|
||||
variance = sum((r - mean_return) ** 2 for r in daily_returns) / (n - 1)
|
||||
std_return = math.sqrt(variance)
|
||||
|
||||
if std_return < 1e-12:
|
||||
return 0.0
|
||||
|
||||
return (mean_return / std_return) * math.sqrt(252)
|
||||
|
||||
@staticmethod
|
||||
def _compute_max_drawdown(daily_returns: list[float]) -> float:
|
||||
"""Compute maximum drawdown from daily returns.
|
||||
|
||||
Tracks cumulative returns and finds the largest peak-to-trough decline.
|
||||
Returns 0.0 if no drawdown or insufficient data.
|
||||
"""
|
||||
if not daily_returns:
|
||||
return 0.0
|
||||
|
||||
cumulative = 1.0
|
||||
peak = 1.0
|
||||
max_dd = 0.0
|
||||
|
||||
for r in daily_returns:
|
||||
cumulative *= (1.0 + r)
|
||||
if cumulative > peak:
|
||||
peak = cumulative
|
||||
drawdown = (peak - cumulative) / peak if peak > 0 else 0.0
|
||||
if drawdown > max_dd:
|
||||
max_dd = drawdown
|
||||
|
||||
return max_dd
|
||||
|
||||
@staticmethod
|
||||
def _compute_current_drawdown(daily_returns: list[float]) -> float:
|
||||
"""Compute current drawdown percentage from daily returns.
|
||||
|
||||
Returns the drawdown from the most recent peak to the current value.
|
||||
"""
|
||||
if not daily_returns:
|
||||
return 0.0
|
||||
|
||||
cumulative = 1.0
|
||||
peak = 1.0
|
||||
|
||||
for r in daily_returns:
|
||||
cumulative *= (1.0 + r)
|
||||
if cumulative > peak:
|
||||
peak = cumulative
|
||||
|
||||
if peak <= 0:
|
||||
return 0.0
|
||||
|
||||
return (peak - cumulative) / peak
|
||||
Reference in New Issue
Block a user