astock-agent/backend/app/engine/trigger_monitor.py
2026-06-01 21:29:26 +08:00

169 lines
5.7 KiB
Python

"""盘中买点触发监控
从当日埋伏池加载标的,每分钟用腾讯批量行情检查是否命中买入条件。
命中后通过飞书 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}")