import pytest from strategies.rsi_strategy import RsiStrategy from strategies.macd_strategy import MacdStrategy from strategies.bollinger_strategy import BollingerStrategy from strategies.ema_crossover_strategy import EmaCrossoverStrategy from strategies.grid_strategy import GridStrategy from strategies.vwap_strategy import VwapStrategy from strategies.volume_profile_strategy import VolumeProfileStrategy # ── RSI ────────────────────────────────────────────────────────────────── class TestRsiValidation: def test_valid_params(self): s = RsiStrategy() s.configure({"period": 14, "oversold": 30, "overbought": 70, "quantity": "0.01"}) def test_period_too_small(self): s = RsiStrategy() with pytest.raises(ValueError, match="period must be >= 2"): s.configure({"period": 1}) def test_oversold_gte_overbought(self): s = RsiStrategy() with pytest.raises(ValueError, match="thresholds"): s.configure({"oversold": 70, "overbought": 30}) def test_oversold_equals_overbought(self): s = RsiStrategy() with pytest.raises(ValueError, match="thresholds"): s.configure({"oversold": 50, "overbought": 50}) def test_oversold_zero(self): s = RsiStrategy() with pytest.raises(ValueError, match="thresholds"): s.configure({"oversold": 0, "overbought": 70}) def test_overbought_100(self): s = RsiStrategy() with pytest.raises(ValueError, match="thresholds"): s.configure({"oversold": 30, "overbought": 100}) def test_quantity_zero(self): s = RsiStrategy() with pytest.raises(ValueError, match="Quantity must be positive"): s.configure({"quantity": "0"}) def test_quantity_negative(self): s = RsiStrategy() with pytest.raises(ValueError, match="Quantity must be positive"): s.configure({"quantity": "-1"}) # ── MACD ───────────────────────────────────────────────────────────────── class TestMacdValidation: def test_valid_params(self): s = MacdStrategy() s.configure({"fast_period": 12, "slow_period": 26, "signal_period": 9, "quantity": "0.05"}) def test_fast_gte_slow(self): s = MacdStrategy() with pytest.raises(ValueError, match="fast_period must be < slow_period"): s.configure({"fast_period": 26, "slow_period": 12}) def test_fast_equals_slow(self): s = MacdStrategy() with pytest.raises(ValueError, match="fast_period must be < slow_period"): s.configure({"fast_period": 12, "slow_period": 12}) def test_fast_period_too_small(self): s = MacdStrategy() with pytest.raises(ValueError, match="fast_period must be >= 2"): s.configure({"fast_period": 1, "slow_period": 26}) def test_signal_period_too_small(self): s = MacdStrategy() with pytest.raises(ValueError, match="signal_period must be >= 2"): s.configure({"signal_period": 1}) def test_quantity_zero(self): s = MacdStrategy() with pytest.raises(ValueError, match="Quantity must be positive"): s.configure({"quantity": "0"}) # ── Bollinger ──────────────────────────────────────────────────────────── class TestBollingerValidation: def test_valid_params(self): s = BollingerStrategy() s.configure({"period": 20, "num_std": 2.0, "quantity": "0.01"}) def test_period_too_small(self): s = BollingerStrategy() with pytest.raises(ValueError, match="period must be >= 2"): s.configure({"period": 1}) def test_num_std_zero(self): s = BollingerStrategy() with pytest.raises(ValueError, match="num_std must be > 0"): s.configure({"num_std": 0}) def test_num_std_negative(self): s = BollingerStrategy() with pytest.raises(ValueError, match="num_std must be > 0"): s.configure({"num_std": -1.0}) def test_quantity_zero(self): s = BollingerStrategy() with pytest.raises(ValueError, match="Quantity must be positive"): s.configure({"quantity": "0"}) # ── EMA Crossover ──────────────────────────────────────────────────────── class TestEmaCrossoverValidation: def test_valid_params(self): s = EmaCrossoverStrategy() s.configure({"short_period": 9, "long_period": 21, "quantity": "0.01"}) def test_short_gte_long(self): s = EmaCrossoverStrategy() with pytest.raises(ValueError, match="short_period must be < long_period"): s.configure({"short_period": 21, "long_period": 9}) def test_short_equals_long(self): s = EmaCrossoverStrategy() with pytest.raises(ValueError, match="short_period must be < long_period"): s.configure({"short_period": 10, "long_period": 10}) def test_short_period_too_small(self): s = EmaCrossoverStrategy() with pytest.raises(ValueError, match="short_period must be >= 2"): s.configure({"short_period": 1, "long_period": 21}) def test_quantity_zero(self): s = EmaCrossoverStrategy() with pytest.raises(ValueError, match="Quantity must be positive"): s.configure({"quantity": "0"}) # ── Grid ───────────────────────────────────────────────────────────────── class TestGridValidation: def test_valid_params(self): s = GridStrategy() s.configure({"lower_price": 100, "upper_price": 200, "grid_count": 5, "quantity": "0.01"}) def test_lower_gte_upper(self): s = GridStrategy() with pytest.raises(ValueError, match="lower_price must be < upper_price"): s.configure({"lower_price": 200, "upper_price": 100}) def test_lower_equals_upper(self): s = GridStrategy() with pytest.raises(ValueError, match="lower_price must be < upper_price"): s.configure({"lower_price": 100, "upper_price": 100}) def test_grid_count_too_small(self): s = GridStrategy() with pytest.raises(ValueError, match="grid_count must be >= 2"): s.configure({"lower_price": 100, "upper_price": 200, "grid_count": 1}) def test_quantity_zero(self): s = GridStrategy() with pytest.raises(ValueError, match="Quantity must be positive"): s.configure({"lower_price": 100, "upper_price": 200, "quantity": "0"}) # ── VWAP ───────────────────────────────────────────────────────────────── class TestVwapValidation: def test_valid_params(self): s = VwapStrategy() s.configure({"deviation_threshold": 0.002, "quantity": "0.01"}) def test_deviation_threshold_zero(self): s = VwapStrategy() with pytest.raises(ValueError, match="deviation_threshold must be > 0"): s.configure({"deviation_threshold": 0}) def test_deviation_threshold_negative(self): s = VwapStrategy() with pytest.raises(ValueError, match="deviation_threshold must be > 0"): s.configure({"deviation_threshold": -0.01}) def test_quantity_zero(self): s = VwapStrategy() with pytest.raises(ValueError, match="Quantity must be positive"): s.configure({"quantity": "0"}) # ── Volume Profile ─────────────────────────────────────────────────────── class TestVolumeProfileValidation: def test_valid_params(self): s = VolumeProfileStrategy() s.configure( { "lookback_period": 100, "num_bins": 50, "value_area_pct": 0.7, "quantity": "0.01", } ) def test_lookback_too_small(self): s = VolumeProfileStrategy() with pytest.raises(ValueError, match="lookback_period must be >= 2"): s.configure({"lookback_period": 1}) def test_num_bins_too_small(self): s = VolumeProfileStrategy() with pytest.raises(ValueError, match="num_bins must be >= 2"): s.configure({"num_bins": 1}) def test_value_area_pct_zero(self): s = VolumeProfileStrategy() with pytest.raises(ValueError, match="value_area_pct"): s.configure({"value_area_pct": 0}) def test_value_area_pct_negative(self): s = VolumeProfileStrategy() with pytest.raises(ValueError, match="value_area_pct"): s.configure({"value_area_pct": -0.5}) def test_value_area_pct_above_one(self): s = VolumeProfileStrategy() with pytest.raises(ValueError, match="value_area_pct"): s.configure({"value_area_pct": 1.5}) def test_value_area_pct_exactly_one(self): """value_area_pct=1.0 is valid (100% of volume).""" s = VolumeProfileStrategy() s.configure({"value_area_pct": 1.0}) def test_quantity_zero(self): s = VolumeProfileStrategy() with pytest.raises(ValueError, match="Quantity must be positive"): s.configure({"quantity": "0"})