summaryrefslogtreecommitdiff
path: root/tests/edge_cases/test_empty_data.py
blob: 2449b90f48189a5f553ea99a283a817b10da502d (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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
"""Tests for empty/zero data edge cases."""

import sys
from datetime import datetime, timezone, timedelta
from decimal import Decimal
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "services" / "strategy-engine"))
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "services" / "backtester" / "src"))
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "services" / "order-executor" / "src"))
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "services" / "portfolio-manager" / "src"))

from shared.models import Candle, Signal, OrderSide, Position
from backtester.engine import BacktestEngine
from backtester.metrics import TradeRecord, compute_detailed_metrics
from portfolio_manager.portfolio import PortfolioTracker
from order_executor.risk_manager import RiskManager
from strategies.rsi_strategy import RsiStrategy


class TestBacktestEngineEmptyCandles:
    """BacktestEngine.run([]) should return valid result with 0 trades."""

    def test_run_empty_candles(self):
        strategy = RsiStrategy()
        engine = BacktestEngine(strategy, initial_balance=Decimal("10000"))
        result = engine.run([])
        assert result.total_trades == 0
        assert result.initial_balance == Decimal("10000")
        assert result.final_balance == Decimal("10000")
        assert result.profit == Decimal("0")
        assert result.symbol == ""


class TestPortfolioTrackerEmpty:
    """PortfolioTracker.get_all_positions() on fresh tracker returns empty list."""

    def test_fresh_tracker_returns_empty(self):
        tracker = PortfolioTracker()
        positions = tracker.get_all_positions()
        assert positions == []

    def test_get_position_returns_none_for_unknown_symbol(self):
        tracker = PortfolioTracker()
        assert tracker.get_position("BTCUSDT") is None


class TestRiskManagerZeroBalance:
    """RiskManager.check with zero balance should reject BUY signals."""

    def test_zero_balance_rejects_buy(self):
        rm = RiskManager(
            max_position_size=Decimal("0.5"),
            stop_loss_pct=Decimal("5"),
            daily_loss_limit_pct=Decimal("3"),
        )
        signal = Signal(
            strategy="test",
            symbol="BTCUSDT",
            side=OrderSide.BUY,
            price=Decimal("50000"),
            quantity=Decimal("0.01"),
            reason="test buy",
        )
        result = rm.check(
            signal=signal,
            balance=Decimal("0"),
            positions={},
            daily_pnl=Decimal("0"),
        )
        assert result.allowed is False

    def test_zero_balance_allows_sell(self):
        rm = RiskManager(
            max_position_size=Decimal("0.5"),
            stop_loss_pct=Decimal("5"),
            daily_loss_limit_pct=Decimal("3"),
        )
        signal = Signal(
            strategy="test",
            symbol="BTCUSDT",
            side=OrderSide.SELL,
            price=Decimal("50000"),
            quantity=Decimal("0.01"),
            reason="test sell",
        )
        result = rm.check(
            signal=signal,
            balance=Decimal("0"),
            positions={},
            daily_pnl=Decimal("0"),
        )
        # SELL doesn't require balance, so should be allowed
        assert result.allowed is True


class TestComputeDetailedMetricsEmpty:
    """compute_detailed_metrics with empty trades returns zeroed metrics."""

    def test_empty_trades(self):
        metrics = compute_detailed_metrics(
            trades=[],
            initial_balance=Decimal("10000"),
            final_balance=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.sharpe_ratio == 0.0
        assert metrics.max_drawdown == 0.0
        assert metrics.max_drawdown_duration == timedelta(0)