summaryrefslogtreecommitdiff
path: root/services/backtester/tests
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-01 16:22:07 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-01 16:22:07 +0900
commitc89701668527ab94a124ac5ceb7a8e1045da1d72 (patch)
treee0ef1d021668eac9dc5f274f89d2bd7c7295cb8e /services/backtester/tests
parente0320a4d4b7d22d7d663ef474c7d5e081f4e83a1 (diff)
feat(backtester): add detailed metrics (Sharpe, Sortino, drawdown)
Add metrics.py with TradeRecord/DetailedMetrics dataclasses and compute_detailed_metrics() that pairs BUY/SELL trades FIFO, computes win/loss stats, profit factor, equity curve, max drawdown, Sharpe, Sortino, Calmar ratios, and monthly returns.
Diffstat (limited to 'services/backtester/tests')
-rw-r--r--services/backtester/tests/test_metrics.py96
1 files changed, 96 insertions, 0 deletions
diff --git a/services/backtester/tests/test_metrics.py b/services/backtester/tests/test_metrics.py
new file mode 100644
index 0000000..b222b8a
--- /dev/null
+++ b/services/backtester/tests/test_metrics.py
@@ -0,0 +1,96 @@
+"""Tests for detailed backtest metrics."""
+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 == {}
+
+
+import math