From cd3a06c7788ad8a747b1b4579fb6c45b6c43008e Mon Sep 17 00:00:00 2001 From: TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> Date: Wed, 1 Apr 2026 18:23:39 +0900 Subject: feat(backtester): improve metrics with daily Sharpe, recovery factor, consecutive stats --- services/backtester/tests/test_metrics.py | 68 +++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) (limited to 'services/backtester/tests/test_metrics.py') diff --git a/services/backtester/tests/test_metrics.py b/services/backtester/tests/test_metrics.py index 68bc0b5..34314b3 100644 --- a/services/backtester/tests/test_metrics.py +++ b/services/backtester/tests/test_metrics.py @@ -93,3 +93,71 @@ def test_compute_metrics_empty_trades(): assert metrics.calmar_ratio == 0.0 assert metrics.max_drawdown == 0.0 assert metrics.monthly_returns == {} + + +def test_recovery_factor(): + """Recovery factor should be positive when there is a drawdown.""" + trades = [ + _make_trade("BUY", "100", 0), + _make_trade("SELL", "150", 10), # win + _make_trade("BUY", "150", 20), + _make_trade("SELL", "120", 30), # loss: creates drawdown + ] + metrics = compute_detailed_metrics(trades, Decimal("10000"), Decimal("10020")) + assert metrics.recovery_factor > 0 + + +def test_consecutive_losses(): + """Consecutive loss tracking should count streaks correctly.""" + trades = [ + _make_trade("BUY", "100", 0), + _make_trade("SELL", "110", 10), # win + _make_trade("BUY", "110", 20), + _make_trade("SELL", "105", 30), # loss + _make_trade("BUY", "105", 40), + _make_trade("SELL", "100", 50), # loss + ] + metrics = compute_detailed_metrics(trades, Decimal("10000"), Decimal("10005")) + assert metrics.max_consecutive_losses >= 1 + assert metrics.max_consecutive_wins >= 1 + + +def test_risk_free_rate_affects_sharpe(): + """Higher risk-free rate should lower Sharpe ratio.""" + base = datetime(2025, 1, 1, tzinfo=timezone.utc) + trades = [ + TradeRecord(time=base, symbol="BTCUSDT", side="BUY", price=Decimal("100"), quantity=Decimal("1")), + TradeRecord(time=base + timedelta(days=1), symbol="BTCUSDT", side="SELL", price=Decimal("110"), quantity=Decimal("1")), + TradeRecord(time=base + timedelta(days=2), symbol="BTCUSDT", side="BUY", price=Decimal("105"), quantity=Decimal("1")), + TradeRecord(time=base + timedelta(days=3), symbol="BTCUSDT", side="SELL", price=Decimal("115"), quantity=Decimal("1")), + TradeRecord(time=base + timedelta(days=4), symbol="BTCUSDT", side="BUY", price=Decimal("110"), quantity=Decimal("1")), + TradeRecord(time=base + timedelta(days=5), symbol="BTCUSDT", side="SELL", price=Decimal("108"), quantity=Decimal("1")), + ] + m1 = compute_detailed_metrics(trades, Decimal("10000"), Decimal("10018"), risk_free_rate=0.0) + m2 = compute_detailed_metrics(trades, Decimal("10000"), Decimal("10018"), risk_free_rate=0.10) + assert m2.sharpe_ratio <= m1.sharpe_ratio + + +def test_daily_returns_populated(): + """Daily returns list should be populated when there are trades.""" + trades = [ + _make_trade("BUY", "100", 0), + _make_trade("SELL", "110", 60), + _make_trade("BUY", "105", 120), + _make_trade("SELL", "115", 180), + ] + metrics = compute_detailed_metrics(trades, Decimal("10000"), Decimal("10020")) + assert len(metrics.daily_returns) > 0 + + +def test_fee_subtracted_from_pnl(): + """Fees should be subtracted from trade PnL.""" + base = datetime(2025, 1, 1, tzinfo=timezone.utc) + trades_with_fees = [ + TradeRecord(time=base, symbol="BTC", side="BUY", price=Decimal("100"), quantity=Decimal("1"), fee=Decimal("1")), + TradeRecord(time=base + timedelta(minutes=10), symbol="BTC", side="SELL", price=Decimal("110"), quantity=Decimal("1"), fee=Decimal("1")), + ] + # PnL should be 10 - 1 - 1 = 8 + metrics = compute_detailed_metrics(trades_with_fees, Decimal("10000"), Decimal("10008")) + assert metrics.winning_trades == 1 + assert metrics.trade_pairs[0]["pnl"] == pytest.approx(8.0) -- cgit v1.2.3