"""Tests for detailed backtest metrics.""" import math from datetime import datetime, timedelta, timezone from decimal import Decimal import pytest from backtester.metrics import TradeRecord, compute_detailed_metrics def _make_trade(side: str, price: str, minutes_offset: int = 0) -> TradeRecord: return TradeRecord( time=datetime(2025, 1, 1, tzinfo=timezone.utc) + timedelta(minutes=minutes_offset), symbol="BTCUSDT", side=side, price=Decimal(price), quantity=Decimal("1"), ) def test_compute_metrics_basic(): """Two round-trip trades: 1 win, 1 loss. Verify counts and win_rate.""" trades = [ _make_trade("BUY", "100", 0), _make_trade("SELL", "120", 10), # win: +20 _make_trade("BUY", "130", 20), _make_trade("SELL", "110", 30), # loss: -20 ] metrics = compute_detailed_metrics(trades, Decimal("10000"), Decimal("10000")) assert metrics.total_trades == 4 assert metrics.winning_trades == 1 assert metrics.losing_trades == 1 assert metrics.win_rate == pytest.approx(50.0) def test_compute_metrics_profit_factor(): """Verify profit_factor = gross_profit / gross_loss.""" trades = [ _make_trade("BUY", "100", 0), _make_trade("SELL", "150", 10), # win: +50 _make_trade("BUY", "150", 20), _make_trade("SELL", "130", 30), # loss: -20 ] metrics = compute_detailed_metrics(trades, Decimal("10000"), Decimal("10030")) # gross_profit=50, gross_loss=20 → profit_factor=2.5 assert metrics.profit_factor == pytest.approx(2.5) def test_compute_metrics_max_drawdown(): """Max drawdown should be > 0 when there is a losing trade after a peak.""" trades = [ _make_trade("BUY", "100", 0), _make_trade("SELL", "150", 10), # win: equity goes up _make_trade("BUY", "150", 20), _make_trade("SELL", "120", 30), # loss: equity drops ] metrics = compute_detailed_metrics(trades, Decimal("10000"), Decimal("10020")) assert metrics.max_drawdown > 0 def test_compute_metrics_sharpe_ratio(): """Sharpe ratio should be a finite number with multiple trades.""" trades = [ _make_trade("BUY", "100", 0), _make_trade("SELL", "110", 60), _make_trade("BUY", "105", 120), _make_trade("SELL", "115", 180), _make_trade("BUY", "110", 240), _make_trade("SELL", "108", 300), ] metrics = compute_detailed_metrics(trades, Decimal("10000"), Decimal("10018")) assert math.isfinite(metrics.sharpe_ratio) assert math.isfinite(metrics.sortino_ratio) def test_compute_metrics_empty_trades(): """Empty trades should return all zeros.""" metrics = compute_detailed_metrics([], Decimal("10000"), Decimal("10000")) assert metrics.total_return == 0.0 assert metrics.total_trades == 0 assert metrics.winning_trades == 0 assert metrics.losing_trades == 0 assert metrics.win_rate == 0.0 assert metrics.profit_factor == 0.0 assert metrics.sharpe_ratio == 0.0 assert metrics.sortino_ratio == 0.0 assert metrics.calmar_ratio == 0.0 assert metrics.max_drawdown == 0.0 assert metrics.monthly_returns == {}