diff --git a/backend/api/routes/recommendations.py b/backend/api/routes/recommendations.py index 31e5a1a..bdf6257 100644 --- a/backend/api/routes/recommendations.py +++ b/backend/api/routes/recommendations.py @@ -273,6 +273,92 @@ async def get_recommendations( # 保留推荐生成时的 current_price,但给出来源/时间(用于提示“可能过时”) rec.setdefault("current_price_source", rec.get("current_price_source") or "snapshot") rec.setdefault("price_updated", False) + + # 4) 过滤“过期/不再适用”的推荐(短期可借鉴) + # 规则: + # - 时间过期:超过 max_age_sec 直接丢弃 + # - 价格偏离:当前价偏离 planned_entry_price / suggested_limit_price 过大丢弃 + # - 合法性校验:BUY 需要 SL < entry < TP;SELL 需要 SL > entry > TP + import time as _time + now_s = _time.time() + try: + max_age_sec = int(os.getenv("RECOMMENDATIONS_MAX_AGE_SEC", "1800")) # 默认30分钟 + except Exception: + max_age_sec = 1800 + try: + max_price_drift_pct = float(os.getenv("RECOMMENDATIONS_MAX_PRICE_DRIFT_PCT", "1.5")) # 默认1.5% + except Exception: + max_price_drift_pct = 1.5 + + dropped_age = 0 + dropped_drift = 0 + dropped_invalid = 0 + + def _f(v): + try: + return float(v) + except Exception: + return None + + filtered_recs: List[Dict[str, Any]] = [] + for rec in recommendations: + if not isinstance(rec, dict): + continue + + # 4.1 时间过期 + ts = rec.get("timestamp", 0) + try: + ts = float(ts) if ts is not None else 0.0 + except Exception: + ts = 0.0 + if ts and max_age_sec > 0 and (now_s - ts) > max_age_sec: + dropped_age += 1 + continue + + direction_u = str(rec.get("direction", "") or "").upper() + cur = _f(rec.get("current_price")) + # 基准入场价:优先 planned_entry_price(新逻辑),再 suggested_limit_price + entry_base = _f(rec.get("planned_entry_price")) + if entry_base is None: + entry_base = _f(rec.get("suggested_limit_price")) + if entry_base is None: + entry_base = _f(rec.get("analysis_price")) or cur + + # 4.2 价格偏离过大(挂单参考已失效) + if max_price_drift_pct > 0 and cur and entry_base and entry_base > 0: + drift = abs((cur - entry_base) / entry_base) * 100 + if drift > max_price_drift_pct: + dropped_drift += 1 + continue + + # 4.3 合法性校验:止损/止盈相对关系 + sl = _f(rec.get("suggested_stop_loss")) + tp1 = _f(rec.get("suggested_take_profit_1")) + tp2 = _f(rec.get("suggested_take_profit_2")) + if direction_u == "BUY": + if entry_base and sl and sl >= entry_base: + dropped_invalid += 1 + continue + if entry_base and tp1 and tp1 <= entry_base: + dropped_invalid += 1 + continue + if entry_base and tp2 and tp2 <= entry_base: + dropped_invalid += 1 + continue + elif direction_u == "SELL": + if entry_base and sl and sl <= entry_base: + dropped_invalid += 1 + continue + if entry_base and tp1 and tp1 >= entry_base: + dropped_invalid += 1 + continue + if entry_base and tp2 and tp2 >= entry_base: + dropped_invalid += 1 + continue + + filtered_recs.append(rec) + + recommendations = filtered_recs # 方向过滤 if direction: @@ -291,6 +377,13 @@ async def get_recommendations( "price_source": "mark_price" if mark_items else None, "price_updated_at": mark_updated_at, "price_updated_at_ms": mark_updated_at_ms, + "recommendations_max_age_sec": max_age_sec, + "recommendations_max_price_drift_pct": max_price_drift_pct, + "dropped": { + "age": dropped_age, + "price_drift": dropped_drift, + "invalid": dropped_invalid, + }, }, "data": recommendations }