summaryrefslogtreecommitdiff
path: root/services/backtester/tests/test_metrics.py
blob: b222b8ac0086a1e64b310a483a315389aeffeff4 (plain)
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