diff options
Diffstat (limited to 'tests/edge_cases/test_empty_data.py')
| -rw-r--r-- | tests/edge_cases/test_empty_data.py | 113 |
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) |
