Files
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

198 lines
6.1 KiB
Python

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