1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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
|