55 lines
2.0 KiB
Python
55 lines
2.0 KiB
Python
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"])
|