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
114
115
116
|
"""Tests for empty/zero data edge cases."""
import sys
from datetime import 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 backtester.engine import BacktestEngine
from backtester.metrics import compute_detailed_metrics
from order_executor.risk_manager import RiskManager
from portfolio_manager.portfolio import PortfolioTracker
from strategies.rsi_strategy import RsiStrategy
from shared.models import OrderSide, Signal
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("AAPL") 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="AAPL",
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="AAPL",
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)
|