169 lines
5.7 KiB
Python
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}")
|