98 lines
3.4 KiB
Python
98 lines
3.4 KiB
Python
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)
|