"""盘中买点触发监控 从当日埋伏池加载标的,每分钟用腾讯批量行情检查是否命中买入条件。 命中后通过飞书 webhook 推送告警,每只票每天最多触发一次。 """ import logging from datetime import datetime from app.data.cache import cache from app.config import is_trading_hours logger = logging.getLogger(__name__) async def check_triggers(): """主入口:检查当日埋伏池是否有买点触发。""" if not is_trading_hours(): return pool = await _load_today_ambush_pool() if not pool: return from app.data.tencent_client import get_realtime_quotes_batch codes = [item["ts_code"] for item in pool] quotes = await get_realtime_quotes_batch(codes) for item in pool: ts_code = item["ts_code"] quote = quotes.get(ts_code) if not quote or quote.price <= 0: continue # 去重:每只票每天最多触发一次 dedup_key = f"trigger_fired:{ts_code}:{datetime.now().strftime('%Y%m%d')}" if cache.get(dedup_key): continue trigger_type = _check_trigger_condition(item, quote) if trigger_type: cache.set(dedup_key, True, 86400) await _send_trigger_alert(item, quote, trigger_type) def _check_trigger_condition(item: dict, quote) -> str | None: """检查是否命中买入条件。返回触发类型或 None。""" entry_price = item.get("entry_price") entry_signal_type = item.get("entry_signal_type", "") volume_ratio = quote.volume_ratio or 0 if not entry_price or entry_price <= 0: return None price = quote.price # 突破型:价格站上 entry_price 且量比 > 1.5 if entry_signal_type in ("breakout", "breakout_confirm"): if price >= entry_price and volume_ratio >= 1.5: return "突破确认" # 回踩型:价格回到 entry_price 附近(±2%) 且缩量 elif entry_signal_type == "pullback": if abs(price - entry_price) / entry_price <= 0.02 and volume_ratio < 0.8: return "回踩到位" # 启动型:价格突破 entry_price 且量比 > 1.2 elif entry_signal_type in ("launch", "reversal"): if price >= entry_price and volume_ratio >= 1.2: return "启动放量" # 通用:价格站上 entry_price 且量比 > 1.5 else: if price >= entry_price and volume_ratio >= 1.5: return "放量突破" return None async def _load_today_ambush_pool() -> list[dict]: """从数据库加载当日可操作/重点关注的埋伏标的。""" cache_key = "trigger_pool:today" cached = cache.get(cache_key) if cached is not None: return cached try: from sqlalchemy import text from app.db.database import get_db today = datetime.now().strftime("%Y-%m-%d") async with get_db() as db: result = await db.execute( text( "SELECT ts_code, name, entry_price, stop_loss, target_price, " "entry_signal_type, action_plan, sector, score " "FROM recommendations " "WHERE date(created_at) = :today " "AND action_plan IN ('可操作', '重点关注') " "ORDER BY score DESC LIMIT 10" ), {"today": today}, ) rows = result.fetchall() pool = [ { "ts_code": r._mapping["ts_code"], "name": r._mapping["name"], "entry_price": r._mapping["entry_price"], "stop_loss": r._mapping["stop_loss"], "target_price": r._mapping["target_price"], "entry_signal_type": r._mapping["entry_signal_type"] or "", "action_plan": r._mapping["action_plan"], "sector": r._mapping["sector"] or "", "score": r._mapping["score"] or 0, } for r in rows ] # 缓存 5 分钟,避免每分钟查 DB cache.set(cache_key, pool, 300) return pool except Exception as e: logger.warning(f"加载埋伏池失败: {e}") return [] async def _send_trigger_alert(item: dict, quote, trigger_type: str): """通过飞书发送买点触发告警。""" from app.notifications.feishu import send_feishu_alert from app.config import settings if not settings.recommendation_push_enabled: return name = item["name"] ts_code = item["ts_code"] sector = item.get("sector", "") entry_price = item.get("entry_price", 0) target_price = item.get("target_price") stop_loss = item.get("stop_loss") score = item.get("score", 0) price_str = f"{quote.price:.2f}" pct_str = f"{quote.pct_chg:+.1f}%" vr_str = f"{quote.volume_ratio:.1f}" if quote.volume_ratio else "-" lines = [ f"🎯 买点触发: {name} ({ts_code})", f"触发类型: {trigger_type}", f"当前价: {price_str} ({pct_str})", f"量比: {vr_str}", f"板块: {sector}", f"评分: {score:.0f}", f"入场价: {entry_price:.2f}" if entry_price else "", f"目标价: {target_price:.2f}" if target_price else "", f"止损价: {stop_loss:.2f}" if stop_loss else "", f"建议: {item.get('action_plan', '观察')}", ] message = "\n".join(line for line in lines if line) await send_feishu_alert( source="trigger_monitor", message=f"买点触发: {name} {trigger_type}", detail=message, level="info", ) logger.info(f"买点触发告警已发送: {name} {trigger_type} @ {quote.price}")