import pytest from app.core.trailing_stop import evaluate_trailing_stop, trailing_profile def test_fixed_trailing_stop_activates_with_profit_floor(): decision = evaluate_trailing_stop( position={"entry_price": 100, "max_price": 100, "min_price": 100, "trailing_stop": 0}, current_price=104, pnl_pct=4, config={ "trailing_stop_enabled": True, "trailing_mode": "fixed", "trailing_activate_pnl_pct": 3, "trailing_min_lock_profit_pct": 0.5, "trailing_distance_pct": 1.5, }, ).as_dict() assert decision["action"] == "activate" assert decision["event_type"] == "trailing_activate" assert decision["activated"] is True assert decision["trailing_mode"] == "fixed" assert decision["trailing_stop"] == pytest.approx(102.44) def test_trailing_stop_never_moves_down(): decision = evaluate_trailing_stop( position={"entry_price": 100, "max_price": 106, "min_price": 100, "trailing_stop": 104}, current_price=104.5, pnl_pct=4.5, config={ "trailing_stop_enabled": True, "trailing_mode": "fixed", "trailing_activate_pnl_pct": 3, "trailing_min_lock_profit_pct": 0.5, "trailing_distance_pct": 1.5, }, ).as_dict() assert decision["action"] == "hold" assert decision["reason"] == "unchanged" assert decision["trailing_stop"] == pytest.approx(104) def test_short_trailing_stop_activates_and_moves_down(): activated = evaluate_trailing_stop( position={"side": "short", "entry_price": 100, "max_price": 100, "min_price": 100, "trailing_stop": 0}, current_price=94, pnl_pct=6, config={ "trailing_stop_enabled": True, "trailing_mode": "fixed", "trailing_activate_pnl_pct": 3, "trailing_min_lock_profit_pct": 0.5, "trailing_distance_pct": 1.5, }, ).as_dict() moved = evaluate_trailing_stop( position={"side": "short", "entry_price": 100, "max_price": 100, "min_price": 94, "trailing_stop": activated["trailing_stop"]}, current_price=92, pnl_pct=8, config={ "trailing_stop_enabled": True, "trailing_mode": "fixed", "trailing_activate_pnl_pct": 3, "trailing_min_lock_profit_pct": 0.5, "trailing_distance_pct": 1.5, }, ).as_dict() assert activated["action"] == "activate" assert activated["trailing_stop"] == pytest.approx(95.41) assert moved["action"] == "move" assert moved["trailing_stop"] < activated["trailing_stop"] def test_volatility_profile_widens_activation_and_distance(): profile = trailing_profile( {"entry_price": 100, "max_price": 100, "min_price": 98}, current_price=110, pnl_pct=10, config={ "trailing_mode": "volatility", "trailing_activate_pnl_pct": 3, "trailing_distance_pct": 1.5, "trailing_volatility_activation_mult": 0.6, "trailing_volatility_distance_mult": 0.7, "trailing_volatility_max_activation_pct": 8, "trailing_volatility_max_distance_pct": 8, }, ) assert profile["trailing_mode"] == "volatility" assert profile["volatility_pct"] == pytest.approx(12) assert profile["activate_pnl_pct"] == pytest.approx(7.2) assert profile["distance_pct"] == pytest.approx(8.0)