"""Property-based tests for the Stop-Loss Manager. Feature: autonomous-trading-engine Tests properties 9, 10, 11, 15, and 25 from the design specification, covering initial stop/take-profit computation, price crossing triggers, trailing stop activation, high-severity event tightening, and proactive heat-based stop tightening. """ from __future__ import annotations from datetime import datetime, timezone from hypothesis import assume, given, settings from hypothesis import strategies as st from services.trading.models import ( OpenPosition, RiskTierConfig, StopLevels, ) from services.trading.stop_loss_manager import StopLossManager # --------------------------------------------------------------------------- # Hypothesis strategies # --------------------------------------------------------------------------- def _risk_tier_config_strategy() -> st.SearchStrategy[RiskTierConfig]: """Generate random RiskTierConfig objects with valid parameter ranges.""" return st.builds( RiskTierConfig, name=st.sampled_from(["conservative", "moderate", "aggressive"]), min_confidence=st.floats(min_value=0.10, max_value=0.95, allow_nan=False, allow_infinity=False), max_position_pct=st.floats(min_value=0.02, max_value=0.50, allow_nan=False, allow_infinity=False), stop_loss_atr_multiplier=st.floats(min_value=0.5, max_value=5.0, allow_nan=False, allow_infinity=False), reward_risk_ratio=st.floats(min_value=0.5, max_value=5.0, allow_nan=False, allow_infinity=False), max_sector_pct=st.floats(min_value=0.05, max_value=0.60, allow_nan=False, allow_infinity=False), max_portfolio_heat=st.floats(min_value=0.05, max_value=0.50, allow_nan=False, allow_infinity=False), ) def _open_position_strategy( ticker: st.SearchStrategy[str] | None = None, entry_price: st.SearchStrategy[float] | None = None, signal_confidence: st.SearchStrategy[float] | None = None, ) -> st.SearchStrategy[OpenPosition]: """Generate random OpenPosition objects.""" return st.builds( OpenPosition, ticker=ticker if ticker is not None else st.from_regex(r"[A-Z]{3,5}", fullmatch=True), quantity=st.integers(min_value=1, max_value=100), entry_price=entry_price if entry_price is not None else st.floats( min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False, ), current_price=st.floats(min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False), unrealized_pnl=st.floats(min_value=-1000.0, max_value=1000.0, allow_nan=False, allow_infinity=False), market_value=st.floats(min_value=10.0, max_value=5000.0, allow_nan=False, allow_infinity=False), sector=st.sampled_from(["Technology", "Healthcare", "Energy", "Financials", "Consumer"]), stop_loss_price=st.floats(min_value=1.0, max_value=400.0, allow_nan=False, allow_infinity=False), take_profit_price=st.floats(min_value=10.0, max_value=1000.0, allow_nan=False, allow_infinity=False), signal_confidence=signal_confidence if signal_confidence is not None else st.floats( min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False, ), is_micro_trade=st.just(False), ) # --------------------------------------------------------------------------- # Property 9: Stop-loss and take-profit initial computation # **Validates: Requirements 4.1, 4.2** # --------------------------------------------------------------------------- class TestProperty9InitialComputation: """Property 9: Stop-loss and take-profit initial computation. **Validates: Requirements 4.1, 4.2** """ manager = StopLossManager() @settings(max_examples=100) @given( entry_price=st.floats(min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False), atr=st.floats(min_value=0.1, max_value=20.0, allow_nan=False, allow_infinity=False), risk_tier=_risk_tier_config_strategy(), ) def test_stop_loss_equals_entry_minus_atr_times_multiplier( self, entry_price: float, atr: float, risk_tier: RiskTierConfig, ) -> None: """Stop-loss = entry_price - (ATR * stop_loss_atr_multiplier).""" levels = self.manager.compute_initial_levels( entry_price=entry_price, atr=atr, risk_tier=risk_tier, ) expected_stop = entry_price - (atr * risk_tier.stop_loss_atr_multiplier) assert abs(levels.stop_loss_price - expected_stop) < 1e-9, ( f"stop_loss {levels.stop_loss_price} != expected {expected_stop}" ) @settings(max_examples=100) @given( entry_price=st.floats(min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False), atr=st.floats(min_value=0.1, max_value=20.0, allow_nan=False, allow_infinity=False), risk_tier=_risk_tier_config_strategy(), ) def test_stop_loss_always_below_entry( self, entry_price: float, atr: float, risk_tier: RiskTierConfig, ) -> None: """Stop-loss is always below entry price (for positive ATR and multiplier).""" assume(atr * risk_tier.stop_loss_atr_multiplier > 0) levels = self.manager.compute_initial_levels( entry_price=entry_price, atr=atr, risk_tier=risk_tier, ) assert levels.stop_loss_price < entry_price, ( f"stop_loss {levels.stop_loss_price} >= entry {entry_price}" ) @settings(max_examples=100) @given( entry_price=st.floats(min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False), atr=st.floats(min_value=0.1, max_value=20.0, allow_nan=False, allow_infinity=False), risk_tier=_risk_tier_config_strategy(), ) def test_take_profit_equals_entry_plus_stop_distance_times_ratio( self, entry_price: float, atr: float, risk_tier: RiskTierConfig, ) -> None: """Take-profit = entry_price + (stop_distance * reward_risk_ratio).""" levels = self.manager.compute_initial_levels( entry_price=entry_price, atr=atr, risk_tier=risk_tier, ) stop_distance = atr * risk_tier.stop_loss_atr_multiplier expected_tp = entry_price + (stop_distance * risk_tier.reward_risk_ratio) assert abs(levels.take_profit_price - expected_tp) < 1e-9, ( f"take_profit {levels.take_profit_price} != expected {expected_tp}" ) @settings(max_examples=100) @given( entry_price=st.floats(min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False), atr=st.floats(min_value=0.1, max_value=20.0, allow_nan=False, allow_infinity=False), risk_tier=_risk_tier_config_strategy(), ) def test_take_profit_always_above_entry( self, entry_price: float, atr: float, risk_tier: RiskTierConfig, ) -> None: """Take-profit is always above entry price.""" assume(atr * risk_tier.stop_loss_atr_multiplier * risk_tier.reward_risk_ratio > 0) levels = self.manager.compute_initial_levels( entry_price=entry_price, atr=atr, risk_tier=risk_tier, ) assert levels.take_profit_price > entry_price, ( f"take_profit {levels.take_profit_price} <= entry {entry_price}" ) @settings(max_examples=100) @given( entry_price=st.floats(min_value=5.0, max_value=500.0, allow_nan=False, allow_infinity=False), atr=st.floats(min_value=0.1, max_value=20.0, allow_nan=False, allow_infinity=False), risk_tier=_risk_tier_config_strategy(), ) def test_trailing_stop_initially_inactive( self, entry_price: float, atr: float, risk_tier: RiskTierConfig, ) -> None: """Trailing stop is not active on initial computation.""" levels = self.manager.compute_initial_levels( entry_price=entry_price, atr=atr, risk_tier=risk_tier, ) assert levels.trailing_stop_active is False # --------------------------------------------------------------------------- # Property 10: Price crossing triggers immediate sell # **Validates: Requirements 4.4, 4.5** # --------------------------------------------------------------------------- class TestProperty10PriceCrossingTriggers: """Property 10: Price crossing triggers immediate sell. **Validates: Requirements 4.4, 4.5** """ manager = StopLossManager() @settings(max_examples=100) @given( entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False), stop_distance_pct=st.floats(min_value=0.02, max_value=0.20, allow_nan=False, allow_infinity=False), tp_distance_pct=st.floats(min_value=0.02, max_value=0.40, allow_nan=False, allow_infinity=False), ) def test_sell_triggered_when_price_at_or_below_stop_loss( self, entry_price: float, stop_distance_pct: float, tp_distance_pct: float, ) -> None: """Sell triggered when current price <= stop_loss.""" stop_loss = entry_price * (1 - stop_distance_pct) take_profit = entry_price * (1 + tp_distance_pct) assume(stop_loss > 0) assume(take_profit > stop_loss) ticker = "TEST" position = OpenPosition( ticker=ticker, quantity=10, entry_price=entry_price, current_price=stop_loss, unrealized_pnl=0.0, market_value=entry_price * 10, sector="Technology", stop_loss_price=stop_loss, take_profit_price=take_profit, signal_confidence=0.7, ) levels = StopLevels( stop_loss_price=stop_loss, take_profit_price=take_profit, trailing_stop_active=False, atr_value=1.0, atr_multiplier=2.0, reward_risk_ratio=1.5, last_updated=datetime.now(tz=timezone.utc), ) # Price at stop_loss triggers = self.manager.check_price_crossings( positions=[position], prices={ticker: stop_loss}, stop_levels={ticker: levels}, ) assert len(triggers) == 1 assert triggers[0].trigger_type == "stop_loss" assert triggers[0].ticker == ticker @settings(max_examples=100) @given( entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False), stop_distance_pct=st.floats(min_value=0.02, max_value=0.20, allow_nan=False, allow_infinity=False), tp_distance_pct=st.floats(min_value=0.02, max_value=0.40, allow_nan=False, allow_infinity=False), below_pct=st.floats(min_value=0.001, max_value=0.10, allow_nan=False, allow_infinity=False), ) def test_sell_triggered_when_price_below_stop_loss( self, entry_price: float, stop_distance_pct: float, tp_distance_pct: float, below_pct: float, ) -> None: """Sell triggered when current price is below stop_loss.""" stop_loss = entry_price * (1 - stop_distance_pct) take_profit = entry_price * (1 + tp_distance_pct) price_below = stop_loss * (1 - below_pct) assume(stop_loss > 0) assume(price_below > 0) assume(take_profit > stop_loss) ticker = "TEST" position = OpenPosition( ticker=ticker, quantity=10, entry_price=entry_price, current_price=price_below, unrealized_pnl=0.0, market_value=entry_price * 10, sector="Technology", stop_loss_price=stop_loss, take_profit_price=take_profit, signal_confidence=0.7, ) levels = StopLevels( stop_loss_price=stop_loss, take_profit_price=take_profit, trailing_stop_active=False, atr_value=1.0, atr_multiplier=2.0, reward_risk_ratio=1.5, last_updated=datetime.now(tz=timezone.utc), ) triggers = self.manager.check_price_crossings( positions=[position], prices={ticker: price_below}, stop_levels={ticker: levels}, ) assert len(triggers) == 1 assert triggers[0].trigger_type == "stop_loss" @settings(max_examples=100) @given( entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False), stop_distance_pct=st.floats(min_value=0.02, max_value=0.20, allow_nan=False, allow_infinity=False), tp_distance_pct=st.floats(min_value=0.02, max_value=0.40, allow_nan=False, allow_infinity=False), ) def test_sell_triggered_when_price_at_or_above_take_profit( self, entry_price: float, stop_distance_pct: float, tp_distance_pct: float, ) -> None: """Sell triggered when current price >= take_profit.""" stop_loss = entry_price * (1 - stop_distance_pct) take_profit = entry_price * (1 + tp_distance_pct) assume(stop_loss > 0) assume(take_profit > stop_loss) ticker = "TEST" position = OpenPosition( ticker=ticker, quantity=10, entry_price=entry_price, current_price=take_profit, unrealized_pnl=0.0, market_value=entry_price * 10, sector="Technology", stop_loss_price=stop_loss, take_profit_price=take_profit, signal_confidence=0.7, ) levels = StopLevels( stop_loss_price=stop_loss, take_profit_price=take_profit, trailing_stop_active=False, atr_value=1.0, atr_multiplier=2.0, reward_risk_ratio=1.5, last_updated=datetime.now(tz=timezone.utc), ) triggers = self.manager.check_price_crossings( positions=[position], prices={ticker: take_profit}, stop_levels={ticker: levels}, ) assert len(triggers) == 1 assert triggers[0].trigger_type == "take_profit" @settings(max_examples=100) @given( entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False), stop_distance_pct=st.floats(min_value=0.05, max_value=0.20, allow_nan=False, allow_infinity=False), tp_distance_pct=st.floats(min_value=0.05, max_value=0.40, allow_nan=False, allow_infinity=False), between_pct=st.floats(min_value=0.01, max_value=0.99, allow_nan=False, allow_infinity=False), ) def test_no_trigger_when_price_between_levels( self, entry_price: float, stop_distance_pct: float, tp_distance_pct: float, between_pct: float, ) -> None: """No trigger when price is strictly between stop_loss and take_profit.""" stop_loss = entry_price * (1 - stop_distance_pct) take_profit = entry_price * (1 + tp_distance_pct) assume(stop_loss > 0) assume(take_profit > stop_loss + 0.02) # Price strictly between stop_loss and take_profit price_between = stop_loss + (take_profit - stop_loss) * between_pct # Ensure strictly between (not at boundaries) assume(price_between > stop_loss) assume(price_between < take_profit) ticker = "TEST" position = OpenPosition( ticker=ticker, quantity=10, entry_price=entry_price, current_price=price_between, unrealized_pnl=0.0, market_value=entry_price * 10, sector="Technology", stop_loss_price=stop_loss, take_profit_price=take_profit, signal_confidence=0.7, ) levels = StopLevels( stop_loss_price=stop_loss, take_profit_price=take_profit, trailing_stop_active=False, atr_value=1.0, atr_multiplier=2.0, reward_risk_ratio=1.5, last_updated=datetime.now(tz=timezone.utc), ) triggers = self.manager.check_price_crossings( positions=[position], prices={ticker: price_between}, stop_levels={ticker: levels}, ) assert len(triggers) == 0 # --------------------------------------------------------------------------- # Property 11: Trailing stop activation at 50% of take-profit distance # **Validates: Requirements 4.6** # --------------------------------------------------------------------------- class TestProperty11TrailingStopActivation: """Property 11: Trailing stop activation at 50% of take-profit distance. **Validates: Requirements 4.6** """ manager = StopLossManager() @settings(max_examples=100) @given( entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False), atr=st.floats(min_value=0.5, max_value=10.0, allow_nan=False, allow_infinity=False), risk_tier=_risk_tier_config_strategy(), move_fraction=st.floats(min_value=0.51, max_value=0.99, allow_nan=False, allow_infinity=False), ) def test_trailing_stop_activates_when_move_exceeds_50_pct( self, entry_price: float, atr: float, risk_tier: RiskTierConfig, move_fraction: float, ) -> None: """Trailing stop activates when favorable move > 50% of TP distance.""" # Compute initial levels to get the TP distance initial_levels = self.manager.compute_initial_levels( entry_price=entry_price, atr=atr, risk_tier=risk_tier, ) tp_distance = initial_levels.take_profit_price - entry_price assume(tp_distance > 0.01) # Current price moved favorably by more than 50% of TP distance current_price = entry_price + (tp_distance * move_fraction) position = OpenPosition( ticker="TEST", quantity=10, entry_price=entry_price, current_price=current_price, unrealized_pnl=0.0, market_value=current_price * 10, sector="Technology", stop_loss_price=initial_levels.stop_loss_price, take_profit_price=initial_levels.take_profit_price, signal_confidence=0.7, ) result = self.manager.re_evaluate_levels( position=position, current_price=current_price, atr=atr, risk_tier=risk_tier, last_levels=initial_levels, ) # re_evaluate_levels returns None when no material change, or StopLevels # Since trailing stop activation IS a material change, we expect a result assert result is not None, "Expected re_evaluate to return updated levels" assert result.trailing_stop_active is True # When trailing stop is active, stop should be at least at entry (breakeven) assert result.stop_loss_price >= entry_price - 1e-9, ( f"Trailing stop {result.stop_loss_price} should be >= entry {entry_price}" ) @settings(max_examples=100) @given( entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False), atr=st.floats(min_value=0.5, max_value=10.0, allow_nan=False, allow_infinity=False), risk_tier=_risk_tier_config_strategy(), move_fraction=st.floats(min_value=0.0, max_value=0.49, allow_nan=False, allow_infinity=False), ) def test_trailing_stop_does_not_activate_when_move_at_or_below_50_pct( self, entry_price: float, atr: float, risk_tier: RiskTierConfig, move_fraction: float, ) -> None: """Trailing stop does NOT activate when favorable move <= 50% of TP distance.""" initial_levels = self.manager.compute_initial_levels( entry_price=entry_price, atr=atr, risk_tier=risk_tier, ) tp_distance = initial_levels.take_profit_price - entry_price assume(tp_distance > 0.01) # Current price moved favorably by <= 50% of TP distance current_price = entry_price + (tp_distance * move_fraction) position = OpenPosition( ticker="TEST", quantity=10, entry_price=entry_price, current_price=current_price, unrealized_pnl=0.0, market_value=current_price * 10, sector="Technology", stop_loss_price=initial_levels.stop_loss_price, take_profit_price=initial_levels.take_profit_price, signal_confidence=0.7, ) result = self.manager.re_evaluate_levels( position=position, current_price=current_price, atr=atr, risk_tier=risk_tier, last_levels=initial_levels, ) # Either None (no change) or result with trailing_stop_active=False if result is not None: assert result.trailing_stop_active is False, ( f"Trailing stop should not activate at {move_fraction*100:.1f}% move" ) # --------------------------------------------------------------------------- # Property 15: Stop tightening during high-severity events # **Validates: Requirements 7.2** # --------------------------------------------------------------------------- class TestProperty15HighSeverityEventTightening: """Property 15: Stop tightening during high-severity events. **Validates: Requirements 7.2** """ manager = StopLossManager() @settings(max_examples=100) @given( entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False), atr=st.floats(min_value=0.5, max_value=10.0, allow_nan=False, allow_infinity=False), risk_tier=_risk_tier_config_strategy(), ) def test_tightened_stop_uses_half_normal_multiplier( self, entry_price: float, atr: float, risk_tier: RiskTierConfig, ) -> None: """During high-severity events, stop uses 0.5x normal ATR multiplier.""" initial_levels = self.manager.compute_initial_levels( entry_price=entry_price, atr=atr, risk_tier=risk_tier, ) # Use a current price near entry so trailing stop doesn't activate current_price = entry_price + 0.01 position = OpenPosition( ticker="TEST", quantity=10, entry_price=entry_price, current_price=current_price, unrealized_pnl=0.0, market_value=current_price * 10, sector="Technology", stop_loss_price=initial_levels.stop_loss_price, take_profit_price=initial_levels.take_profit_price, signal_confidence=0.7, ) result = self.manager.re_evaluate_levels( position=position, current_price=current_price, atr=atr, risk_tier=risk_tier, last_levels=initial_levels, high_severity_event=True, ) # High-severity event changes the multiplier, so we expect a result assert result is not None, "Expected updated levels during high-severity event" # The tightened stop should use 0.5x the normal multiplier expected_tightened_multiplier = risk_tier.stop_loss_atr_multiplier * 0.5 expected_stop = entry_price - (atr * expected_tightened_multiplier) assert abs(result.stop_loss_price - expected_stop) < 1e-9, ( f"Tightened stop {result.stop_loss_price} != expected {expected_stop} " f"(0.5x multiplier = {expected_tightened_multiplier})" ) @settings(max_examples=100) @given( entry_price=st.floats(min_value=50.0, max_value=500.0, allow_nan=False, allow_infinity=False), atr=st.floats(min_value=0.5, max_value=10.0, allow_nan=False, allow_infinity=False), risk_tier=_risk_tier_config_strategy(), ) def test_tightened_stop_closer_to_entry_than_normal( self, entry_price: float, atr: float, risk_tier: RiskTierConfig, ) -> None: """Tightened stop is closer to entry price (higher) than normal stop.""" initial_levels = self.manager.compute_initial_levels( entry_price=entry_price, atr=atr, risk_tier=risk_tier, ) current_price = entry_price + 0.01 position = OpenPosition( ticker="TEST", quantity=10, entry_price=entry_price, current_price=current_price, unrealized_pnl=0.0, market_value=current_price * 10, sector="Technology", stop_loss_price=initial_levels.stop_loss_price, take_profit_price=initial_levels.take_profit_price, signal_confidence=0.7, ) result = self.manager.re_evaluate_levels( position=position, current_price=current_price, atr=atr, risk_tier=risk_tier, last_levels=initial_levels, high_severity_event=True, ) assert result is not None # Tightened stop should be closer to entry (higher value) than normal stop normal_stop = entry_price - (atr * risk_tier.stop_loss_atr_multiplier) assert result.stop_loss_price >= normal_stop - 1e-9, ( f"Tightened stop {result.stop_loss_price} should be >= normal stop {normal_stop}" ) # --------------------------------------------------------------------------- # Property 25: Proactive stop tightening at 80% heat threshold # **Validates: Requirements 13.3** # --------------------------------------------------------------------------- class TestProperty25ProactiveHeatTightening: """Property 25: Proactive stop tightening at 80% heat threshold. **Validates: Requirements 13.3** """ manager = StopLossManager() @settings(max_examples=100) @given( num_positions=st.integers(min_value=2, max_value=5), max_heat=st.floats(min_value=0.10, max_value=0.30, allow_nan=False, allow_infinity=False), ) def test_lowest_confidence_positions_tightened_first( self, num_positions: int, max_heat: float, ) -> None: """Lowest-confidence positions get stops tightened first when heat > 80% of max.""" # Create positions with distinct confidence levels positions: list[OpenPosition] = [] stop_levels_dict: dict[str, StopLevels] = {} for i in range(num_positions): ticker = f"T{i}" entry_price = 100.0 confidence = 0.3 + (i * 0.15) # ascending confidence: 0.3, 0.45, 0.6, ... stop_loss = 90.0 # 10% below entry take_profit = 115.0 positions.append(OpenPosition( ticker=ticker, quantity=10, entry_price=entry_price, current_price=entry_price, unrealized_pnl=0.0, market_value=entry_price * 10, sector="Technology", stop_loss_price=stop_loss, take_profit_price=take_profit, signal_confidence=confidence, )) stop_levels_dict[ticker] = StopLevels( stop_loss_price=stop_loss, take_profit_price=take_profit, trailing_stop_active=False, atr_value=5.0, atr_multiplier=2.0, reward_risk_ratio=1.5, last_updated=datetime.now(tz=timezone.utc), ) # Set heat above 80% of max to trigger tightening portfolio_heat = max_heat * 0.85 active_pool = 10000.0 updated = self.manager.tighten_for_heat( positions=positions, stop_levels=stop_levels_dict, portfolio_heat=portfolio_heat, max_heat=max_heat, active_pool=active_pool, ) if updated: # Verify that tightened positions are ordered by confidence (lowest first) tightened_tickers = list(updated.keys()) tightened_confidences = [ next(p.signal_confidence for p in positions if p.ticker == t) for t in tightened_tickers ] # The first tightened position should have the lowest confidence min_confidence_in_portfolio = min(p.signal_confidence for p in positions) if tightened_tickers: first_tightened_confidence = next( p.signal_confidence for p in positions if p.ticker == tightened_tickers[0] ) assert first_tightened_confidence == min_confidence_in_portfolio, ( f"First tightened position confidence {first_tightened_confidence} " f"!= min confidence {min_confidence_in_portfolio}" ) # All tightened stops should be >= original stops (moved closer to entry) for ticker, new_levels in updated.items(): original_stop = stop_levels_dict[ticker].stop_loss_price assert new_levels.stop_loss_price >= original_stop - 1e-9, ( f"{ticker}: tightened stop {new_levels.stop_loss_price} " f"< original {original_stop}" ) @settings(max_examples=100) @given( max_heat=st.floats(min_value=0.10, max_value=0.30, allow_nan=False, allow_infinity=False), heat_fraction=st.floats(min_value=0.0, max_value=0.79, allow_nan=False, allow_infinity=False), ) def test_no_tightening_when_heat_below_80_pct_threshold( self, max_heat: float, heat_fraction: float, ) -> None: """No tightening when portfolio heat <= 80% of max.""" portfolio_heat = max_heat * heat_fraction positions = [ OpenPosition( ticker="T0", quantity=10, entry_price=100.0, current_price=100.0, unrealized_pnl=0.0, market_value=1000.0, sector="Technology", stop_loss_price=90.0, take_profit_price=115.0, signal_confidence=0.5, ), ] stop_levels_dict = { "T0": StopLevels( stop_loss_price=90.0, take_profit_price=115.0, trailing_stop_active=False, atr_value=5.0, atr_multiplier=2.0, reward_risk_ratio=1.5, last_updated=datetime.now(tz=timezone.utc), ), } updated = self.manager.tighten_for_heat( positions=positions, stop_levels=stop_levels_dict, portfolio_heat=portfolio_heat, max_heat=max_heat, active_pool=10000.0, ) assert updated == {}, ( f"Expected no tightening at heat {portfolio_heat} " f"(80% threshold = {max_heat * 0.8}), got {len(updated)} updates" )