From 66368d580cf569b50a33e438f2287a977e6fc704 Mon Sep 17 00:00:00 2001 From: TheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:11:51 +0900 Subject: test: add edge case tests for zero volume, empty data, extreme values --- tests/edge_cases/test_extreme_values.py | 172 ++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 tests/edge_cases/test_extreme_values.py (limited to 'tests/edge_cases/test_extreme_values.py') 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 -- cgit v1.2.3