import os import sys import pandas as pd PROJECT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) if PROJECT_DIR not in sys.path: sys.path.insert(0, PROJECT_DIR) from app.services import price_tracker def test_trailing_stop_activation_uses_profit_floor_when_atr_is_too_wide(monkeypatch): """MLN-style case: +5% float profit with huge ATR must still protect above entry.""" monkeypatch.setattr(price_tracker, "calc_atr", lambda df, period=14: 0.447143) monkeypatch.setattr(price_tracker, "detect_trend_exhaustion", lambda df, atr: {"exhausted": False, "signals": [], "severity": "low"}) monkeypatch.setattr(price_tracker, "full_pa_analysis", lambda df, timeframe: {"candles_class": []}) monkeypatch.setattr(price_tracker, "load_rules", lambda: { "tracker": { "trailing_stop": { "enabled": True, "activate_pnl_pct": 3, "min_lock_profit_pct": 0.5, "breakeven_buffer_pct": 0.5, "tiers": [ {"min_pnl_pct": 0, "atr_mult": 3.0, "label": "防震"}, {"min_pnl_pct": 5, "atr_mult": 2.0, "label": "锁利"}, ], } } }) df = pd.DataFrame( [ {"open": 3.5, "high": 3.8, "low": 3.4, "close": 3.7, "volume": 100}, {"open": 3.7, "high": 3.9, "low": 3.6, "close": 3.8, "volume": 120}, ] * 20 ) monkeypatch.setattr(price_tracker, "fetch_klines", lambda symbol, timeframe, limit=100: df) rec = { "entry_price": 3.61, "stop_loss": 3.249, "tp1": 4.822857, "tp2": 5.631429, "entry_plan": {"trailing_stop_level": 0.0}, } result = price_tracker.analyze_tracking_signals("MLN/USDT", rec, 3.8) assert result["trailing_stop_activated"] is True assert result["trailing_stop_level"] == max(3.249, 3.61 * 1.005) assert result["trailing_stop_level"] > rec["entry_price"] assert any("跟踪止盈激活" in signal for signal in result["sell_signals"])