"""Property-based tests for the Micro-Trading Module. Feature: autonomous-trading-engine Property 31: Micro-trade parameter constraints. Property 32: Micro-trade auto-close after max hold duration. Property 34: Micro-trades respect all existing constraints. """ from __future__ import annotations from hypothesis import given, settings from hypothesis import strategies as st from services.trading.micro_trading import MicroTradeConfig, MicroTradingModule # --------------------------------------------------------------------------- # Hypothesis strategies # --------------------------------------------------------------------------- def _micro_config_strategy() -> st.SearchStrategy[MicroTradeConfig]: """Generate random MicroTradeConfig objects with valid ranges.""" return st.builds( MicroTradeConfig, enabled=st.just(True), allocation_cap_pct=st.floats( min_value=0.01, max_value=0.10, allow_nan=False, allow_infinity=False, ), max_daily=st.integers(min_value=1, max_value=50), max_hold_minutes=st.integers(min_value=10, max_value=480), stop_loss_atr_multiplier=st.just(1.0), reward_risk_ratio=st.just(1.5), ) # --------------------------------------------------------------------------- # Property 31: Micro-trade parameter constraints # **Validates: Requirements 20.3, 20.4, 20.5** # --------------------------------------------------------------------------- class TestProperty31MicroTradeParameterConstraints: """Property 31: Micro-trade parameter constraints. Verify allocation does not exceed micro_trading_allocation_cap_pct. Verify daily count does not exceed configured maximum. **Validates: Requirements 20.3, 20.4, 20.5** """ module = MicroTradingModule() @settings(max_examples=100) @given( config=_micro_config_strategy(), active_pool=st.floats( min_value=100.0, max_value=100_000.0, allow_nan=False, allow_infinity=False, ), ) def test_allocation_does_not_exceed_cap( self, config: MicroTradeConfig, active_pool: float, ) -> None: """Micro-trade allocation never exceeds allocation_cap_pct * active_pool.""" cap = self.module.compute_allocation_cap(config, active_pool) expected = config.allocation_cap_pct * active_pool assert abs(cap - expected) < 1e-6 assert cap <= config.allocation_cap_pct * active_pool + 1e-6 @settings(max_examples=100) @given( config=_micro_config_strategy(), daily_count=st.integers(min_value=0, max_value=100), ) def test_daily_count_enforced( self, config: MicroTradeConfig, daily_count: int, ) -> None: """should_evaluate returns False when daily count >= max_daily.""" result = self.module.should_evaluate(config, daily_count) if daily_count >= config.max_daily: assert result is False else: assert result is True @settings(max_examples=100) @given( config=_micro_config_strategy(), ) def test_stop_loss_at_1x_atr( self, config: MicroTradeConfig, ) -> None: """Micro-trade stop-loss uses 1.0x ATR multiplier.""" assert config.stop_loss_atr_multiplier == 1.0 @settings(max_examples=100) @given( config=_micro_config_strategy(), ) def test_take_profit_at_1_5x_stop_distance( self, config: MicroTradeConfig, ) -> None: """Micro-trade take-profit uses 1.5x reward-risk ratio.""" assert config.reward_risk_ratio == 1.5 @settings(max_examples=100) @given( config=_micro_config_strategy(), active_pool=st.floats( min_value=100.0, max_value=100_000.0, allow_nan=False, allow_infinity=False, ), ) def test_allocation_cap_scales_with_active_pool( self, config: MicroTradeConfig, active_pool: float, ) -> None: """Allocation cap is proportional to active pool.""" cap = self.module.compute_allocation_cap(config, active_pool) # Doubling active pool should double the cap cap_double = self.module.compute_allocation_cap(config, active_pool * 2) assert abs(cap_double - cap * 2) < 1e-4 # --------------------------------------------------------------------------- # Property 32: Micro-trade auto-close after max hold duration # **Validates: Requirements 20.6** # --------------------------------------------------------------------------- class TestProperty32MicroTradeAutoClose: """Property 32: Micro-trade auto-close after max hold duration. Verify positions closed when hold exceeds max duration. Verify positions not closed when hold is within duration. **Validates: Requirements 20.6** """ module = MicroTradingModule() @settings(max_examples=100) @given( config=_micro_config_strategy(), hold_minutes=st.floats( min_value=0.0, max_value=1000.0, allow_nan=False, allow_infinity=False, ), ) def test_auto_close_when_exceeds_max_hold( self, config: MicroTradeConfig, hold_minutes: float, ) -> None: """Position auto-closed when hold_minutes > max_hold_minutes.""" result = self.module.should_auto_close(config, hold_minutes) if hold_minutes > config.max_hold_minutes: assert result is True else: assert result is False @settings(max_examples=100) @given( config=_micro_config_strategy(), ) def test_not_closed_at_exact_max( self, config: MicroTradeConfig, ) -> None: """Position is NOT auto-closed at exactly max_hold_minutes.""" result = self.module.should_auto_close(config, float(config.max_hold_minutes)) assert result is False @settings(max_examples=100) @given( config=_micro_config_strategy(), ) def test_closed_just_past_max( self, config: MicroTradeConfig, ) -> None: """Position IS auto-closed just past max_hold_minutes.""" result = self.module.should_auto_close( config, float(config.max_hold_minutes) + 0.01, ) assert result is True # --------------------------------------------------------------------------- # Property 34: Micro-trades respect all existing constraints # **Validates: Requirements 20.10** # --------------------------------------------------------------------------- class TestProperty34MicroTradesRespectConstraints: """Property 34: Micro-trades respect all existing constraints. Verify trading window, circuit breakers, portfolio heat constraints all enforced for micro-trades. **Validates: Requirements 20.10** """ module = MicroTradingModule() @settings(max_examples=100) @given( config=_micro_config_strategy(), daily_count=st.integers(min_value=0, max_value=5), ) def test_circuit_breaker_blocks_micro_trade( self, config: MicroTradeConfig, daily_count: int, ) -> None: """Micro-trades blocked when circuit breaker is active.""" allowed, reason = self.module.check_constraints( config=config, daily_count=daily_count, is_within_window=True, circuit_breaker_active=True, portfolio_heat_pct=0.05, max_heat=0.20, ) assert allowed is False assert reason == "circuit_breaker_active" @settings(max_examples=100) @given( config=_micro_config_strategy(), daily_count=st.integers(min_value=0, max_value=5), ) def test_outside_trading_window_blocks_micro_trade( self, config: MicroTradeConfig, daily_count: int, ) -> None: """Micro-trades blocked outside trading window.""" allowed, reason = self.module.check_constraints( config=config, daily_count=daily_count, is_within_window=False, circuit_breaker_active=False, portfolio_heat_pct=0.05, max_heat=0.20, ) assert allowed is False assert reason == "outside_trading_window" @settings(max_examples=100) @given( config=_micro_config_strategy(), heat_pct=st.floats( min_value=0.20, max_value=1.0, allow_nan=False, allow_infinity=False, ), ) def test_portfolio_heat_blocks_micro_trade( self, config: MicroTradeConfig, heat_pct: float, ) -> None: """Micro-trades blocked when portfolio heat exceeds max.""" max_heat = 0.20 allowed, reason = self.module.check_constraints( config=config, daily_count=0, is_within_window=True, circuit_breaker_active=False, portfolio_heat_pct=heat_pct, max_heat=max_heat, ) assert allowed is False assert reason == "portfolio_heat_exceeded" @settings(max_examples=100) @given( config=_micro_config_strategy(), ) def test_daily_limit_blocks_micro_trade( self, config: MicroTradeConfig, ) -> None: """Micro-trades blocked when daily limit reached.""" allowed, reason = self.module.check_constraints( config=config, daily_count=config.max_daily, is_within_window=True, circuit_breaker_active=False, portfolio_heat_pct=0.05, max_heat=0.20, ) assert allowed is False assert reason == "daily_limit_reached" @settings(max_examples=100) @given( config=_micro_config_strategy(), daily_count=st.integers(min_value=0, max_value=5), ) def test_disabled_blocks_micro_trade( self, config: MicroTradeConfig, daily_count: int, ) -> None: """Micro-trades blocked when module is disabled.""" disabled_config = MicroTradeConfig( enabled=False, allocation_cap_pct=config.allocation_cap_pct, max_daily=config.max_daily, max_hold_minutes=config.max_hold_minutes, ) allowed, reason = self.module.check_constraints( config=disabled_config, daily_count=daily_count, is_within_window=True, circuit_breaker_active=False, portfolio_heat_pct=0.05, max_heat=0.20, ) assert allowed is False assert reason == "micro_trading_disabled" @settings(max_examples=100) @given( config=_micro_config_strategy(), daily_count=st.integers(min_value=0, max_value=5), heat_pct=st.floats( min_value=0.0, max_value=0.15, allow_nan=False, allow_infinity=False, ), ) def test_all_constraints_pass_allows_trade( self, config: MicroTradeConfig, daily_count: int, heat_pct: float, ) -> None: """Micro-trade allowed when all constraints pass.""" # Ensure daily_count is under the limit safe_count = min(daily_count, config.max_daily - 1) allowed, reason = self.module.check_constraints( config=config, daily_count=safe_count, is_within_window=True, circuit_breaker_active=False, portfolio_heat_pct=heat_pct, max_heat=0.20, ) assert allowed is True assert reason == "ok"