summaryrefslogtreecommitdiff
path: root/tests/edge_cases/test_extreme_values.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/edge_cases/test_extreme_values.py')
-rw-r--r--tests/edge_cases/test_extreme_values.py172
1 files changed, 172 insertions, 0 deletions
diff --git a/tests/edge_cases/test_extreme_values.py b/tests/edge_cases/test_extreme_values.py
new file mode 100644
index 0000000..fe9dc1a
--- /dev/null
+++ b/tests/edge_cases/test_extreme_values.py
@@ -0,0 +1,172 @@
+"""Tests for extreme value edge cases."""
+
+import sys
+from datetime import datetime, timezone
+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"))
+
+from shared.models import Candle, Signal, OrderSide
+from strategies.rsi_strategy import RsiStrategy
+from strategies.vwap_strategy import VwapStrategy
+from strategies.bollinger_strategy import BollingerStrategy
+from backtester.engine import BacktestEngine
+from backtester.simulator import OrderSimulator
+from order_executor.risk_manager import RiskManager
+
+
+def _candle(close: str, volume: str = "1000", idx: int = 0) -> Candle:
+ from datetime import timedelta
+ base = datetime(2025, 1, 1, tzinfo=timezone.utc)
+ return Candle(
+ symbol="BTCUSDT",
+ timeframe="1h",
+ open_time=base + timedelta(hours=idx),
+ open=Decimal(close),
+ high=Decimal(close),
+ low=Decimal(close),
+ close=Decimal(close),
+ volume=Decimal(volume),
+ )
+
+
+class TestZeroPriceCandle:
+ """Strategy should handle Decimal('0') price without crashing."""
+
+ def test_rsi_zero_price(self):
+ strategy = RsiStrategy()
+ for i in range(20):
+ strategy.on_candle(_candle("0", idx=i))
+ # Should not crash; RSI is undefined with constant price
+ result = strategy.on_candle(_candle("0", idx=20))
+ # With all-zero prices, diffs are zero, RSI is NaN -> returns None
+ assert result is None
+
+ def test_vwap_zero_price(self):
+ strategy = VwapStrategy()
+ for i in range(40):
+ result = strategy.on_candle(_candle("0", "100", idx=i))
+ # VWAP of zero-price candles is 0; deviation calc is 0/0 -> should handle gracefully
+ # This tests the division by vwap when vwap == 0
+ assert result is None
+
+
+class TestVeryLargePrice:
+ """Strategy should handle extremely large prices."""
+
+ def test_rsi_large_price(self):
+ strategy = RsiStrategy()
+ for i in range(20):
+ strategy.on_candle(_candle("999999999", idx=i))
+ result = strategy.on_candle(_candle("999999999", idx=20))
+ # Constant large price -> no signal
+ assert result is None
+
+ def test_bollinger_large_price(self):
+ strategy = BollingerStrategy()
+ for i in range(25):
+ strategy.on_candle(_candle("999999999", idx=i))
+ # Should not overflow; constant price -> std=0, bandwidth=0 -> skip
+ result = strategy.on_candle(_candle("999999999", idx=25))
+ assert result is None
+
+
+class TestZeroInitialBalance:
+ """Backtester with Decimal('0') initial balance."""
+
+ def test_backtest_zero_balance(self):
+ strategy = RsiStrategy()
+ engine = BacktestEngine(strategy, initial_balance=Decimal("0"))
+ candles = [_candle(str(100 - i), idx=i) for i in range(30)]
+ result = engine.run(candles)
+ # Cannot buy with 0 balance, so 0 trades
+ assert result.total_trades == 0
+ assert result.final_balance == Decimal("0")
+ assert result.profit_pct == Decimal("0")
+
+
+class TestOrderQuantityZero:
+ """Order with quantity=0."""
+
+ def test_simulator_zero_quantity_buy(self):
+ sim = OrderSimulator(initial_balance=Decimal("10000"))
+ signal = Signal(
+ strategy="test",
+ symbol="BTCUSDT",
+ side=OrderSide.BUY,
+ price=Decimal("50000"),
+ quantity=Decimal("0"),
+ reason="test zero qty",
+ )
+ result = sim.execute(signal)
+ # cost = 50000 * 0 = 0; this is technically valid (no cost)
+ assert result is True
+ # Balance unchanged
+ assert sim.balance == Decimal("10000")
+
+ def test_simulator_zero_quantity_sell(self):
+ sim = OrderSimulator(initial_balance=Decimal("10000"))
+ signal = Signal(
+ strategy="test",
+ symbol="BTCUSDT",
+ side=OrderSide.SELL,
+ price=Decimal("50000"),
+ quantity=Decimal("0"),
+ reason="test zero qty sell",
+ )
+ result = sim.execute(signal)
+ # No position to sell -> rejected
+ assert result is False
+
+
+class TestRiskManagerZeroDailyLossLimit:
+ """RiskManager with 0% daily loss limit should reject everything on any loss."""
+
+ def test_zero_daily_loss_limit_rejects_on_loss(self):
+ rm = RiskManager(
+ max_position_size=Decimal("0.5"),
+ stop_loss_pct=Decimal("5"),
+ daily_loss_limit_pct=Decimal("0"),
+ )
+ signal = Signal(
+ strategy="test",
+ symbol="BTCUSDT",
+ side=OrderSide.BUY,
+ price=Decimal("50000"),
+ quantity=Decimal("0.01"),
+ reason="test",
+ )
+ # Any negative pnl with 0% limit -> should reject
+ result = rm.check(
+ signal=signal,
+ balance=Decimal("10000"),
+ positions={},
+ daily_pnl=Decimal("-1"), # any loss at all
+ )
+ assert result.allowed is False
+
+ def test_zero_daily_loss_limit_allows_no_loss(self):
+ rm = RiskManager(
+ max_position_size=Decimal("0.5"),
+ stop_loss_pct=Decimal("5"),
+ daily_loss_limit_pct=Decimal("0"),
+ )
+ signal = Signal(
+ strategy="test",
+ symbol="BTCUSDT",
+ side=OrderSide.BUY,
+ price=Decimal("100"),
+ quantity=Decimal("0.01"),
+ reason="test",
+ )
+ # No loss -> should allow
+ result = rm.check(
+ signal=signal,
+ balance=Decimal("10000"),
+ positions={},
+ daily_pnl=Decimal("0"),
+ )
+ assert result.allowed is True