summaryrefslogtreecommitdiff
path: root/tests/edge_cases/test_empty_data.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/edge_cases/test_empty_data.py')
-rw-r--r--tests/edge_cases/test_empty_data.py113
1 files changed, 113 insertions, 0 deletions
diff --git a/tests/edge_cases/test_empty_data.py b/tests/edge_cases/test_empty_data.py
new file mode 100644
index 0000000..2449b90
--- /dev/null
+++ b/tests/edge_cases/test_empty_data.py
@@ -0,0 +1,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)