diff --git a/app/services/upstream_signals.py b/app/services/upstream_signals.py index cafaf68..51826de 100644 --- a/app/services/upstream_signals.py +++ b/app/services/upstream_signals.py @@ -103,6 +103,25 @@ def _read_intraday(c, ymd): return {"key": key, "decision": decision, "timing": timing} +# 告警源 → (方向, 类型key, 类型标签)。权威依据: intraday_timing/alerts/orchestrator.py 的 +# DIRECTION_DOWN_SOURCES / DIRECTION_UP_SOURCES (2026-06-15 现行) 与《盘中择时层_信号输出说明》§5。 +# · intraday_buy_emitted 是「每发一条 BUY 镜像一条」的事件, **无涨跌方向** (原来按子串误判成看涨)。 +# · multi_source 的方向看 metadata.direction。 +# · volume_capitulation/breakout 已退役(被 capital_distribution/accumulation 取代), 历史键仍可能在流里, 一并登记。 +_ALERT_META = { + "daily_qrs_symmetric_down": ("看跌", "qrs", "日QRS对称"), + "money_flow_out_intensity": ("看跌", "money_flow", "资金流强度"), + "capital_distribution": ("看跌", "capital", "资金分布"), + "volume_capitulation": ("看跌", "volume", "放量异动"), + "daily_qrs_symmetric_up": ("看涨", "qrs", "日QRS对称"), + "money_flow_in_intensity": ("看涨", "money_flow", "资金流强度"), + "capital_accumulation": ("看涨", "capital", "资金分布"), + "volume_breakout": ("看涨", "volume", "放量异动"), + "multi_source": (None, "multi", "多源升级"), + "intraday_buy_emitted": ("—", "buy_emitted", "买入镜像"), +} + + def _read_alerts(c, ymd): key = "intraday_alerts:%s" % ymd out = [] @@ -114,16 +133,15 @@ def _read_alerts(c, ymd): meta = json.loads(meta) except Exception: meta = {} - mdir = str((meta or {}).get("direction") or "").lower() - low = src.lower() - # 源名都含 "intensity", 不能用 "_in" 这种松匹配 (会误配 out_intensity)。用精确 token, 先判看跌。 - if mdir in ("down", "short", "bear") or any(x in low for x in ("_down", "out_intensity", "capitulation")): - d = "看跌" - elif mdir in ("up", "long", "bull") or any(x in low for x in ("_up", "in_intensity", "breakout", "buy_emitted")): - d = "看涨" + dir_fixed, cat, cat_label = _ALERT_META.get(src, (None, "other", "其他")) + if dir_fixed is None: + # multi_source 或未登记源: 方向以 metadata.direction 为准, 拿不到就 — + mdir = str((meta or {}).get("direction") or "").lower() + d = "看跌" if mdir in ("down", "short", "bear") else ("看涨" if mdir in ("up", "long", "bull") else "—") else: - d = "—" + d = dir_fixed out.append({"ts_code": f.get("ts_code"), "source": src, "direction": d, + "cat": cat, "cat_label": cat_label, "level": f.get("level"), "value": _num(f.get("value")), "time": _hm(f.get("trigger_time"))}) return {"key": key, "items": out} diff --git a/app/web/static/index.html b/app/web/static/index.html index ea7a6d1..ecd62fd 100644 --- a/app/web/static/index.html +++ b/app/web/static/index.html @@ -128,6 +128,12 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius: .alert-dot{width:7px;height:7px;border-radius:50%;flex:none;background:var(--muted);} .alert-dot.lv-danger{background:var(--stop);} .alert-dot.lv-warn{background:var(--warn);} .alert-panel{margin-top:0;} +.alert-wins{display:grid;grid-template-columns:repeat(auto-fit,minmax(430px,1fr));gap:14px;margin:2px 0 16px;} +.alert-win{border:1px solid var(--hair);border-radius:12px;padding:10px 14px 12px;background:var(--surface);box-shadow:var(--shadow);} +.alert-win .aw-h{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px;} +.alert-win .aw-title{font-size:13px;font-weight:640;color:var(--ink-2);} +.alert-win .aw-toggle{font-size:11px;font-weight:560;color:var(--accent);cursor:pointer;border:1px solid var(--accent);border-radius:999px;padding:1px 9px;background:var(--accent-weak);white-space:nowrap;} +.alert-win .aw-toggle.off{color:var(--muted);border-color:var(--hair);background:var(--surface-2);} @@ -180,17 +186,19 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius:
- +
风控告警
-
- - - - - - - +
+
+
+ {{ g.label }} · {{ g.items.length }} + {{ catShown(g.key) ? '折叠条显示 ✓' : '折叠条隐藏' }} +
+ + + + + + + +
@@ -1381,6 +1395,7 @@ createApp({ const upstream = ref({ sources: {} }); const showUpstream = ref(true); const showAlerts = ref(false); + const alertShowMap = reactive({ money_flow:true, capital:true, qrs:true, volume:true, multi:true, buy_emitted:false, other:true }); const openScan = ref({ by_code: {}, notes: [] }); const insTab = ref('live'); const stratDlg = reactive({ visible:false, busy:false, reasons:[], ts_code:'', price:0, @@ -1808,9 +1823,24 @@ createApp({ } const usrc = k => (upstream.value.sources || {})[k]; const alertItems = computed(() => { const a = usrc('alerts'); return (a && Array.isArray(a.items)) ? a.items : []; }); - const alertLatest = computed(() => alertItems.value[0] || {}); + const ALERT_CAT_ORDER = ['money_flow','capital','qrs','volume','multi','buy_emitted','other']; + const ALERT_CAT_DIRECTIONAL = { money_flow:true, capital:true, qrs:true, volume:true, multi:true, buy_emitted:false, other:false }; + const catShown = k => alertShowMap[k] !== false; // 未登记类默认进折叠条 + function toggleCat(k){ alertShowMap[k] = !catShown(k); } + const alertGroups = computed(() => { + const by = {}; + for (const a of alertItems.value) { (by[a.cat] || (by[a.cat] = [])).push(a); } + const keys = ALERT_CAT_ORDER.filter(k => by[k]); + Object.keys(by).forEach(k => { if (!keys.includes(k)) keys.push(k); }); + return keys.map(k => ({ key:k, label:(by[k][0].cat_label) || k, items:by[k], + directional: ALERT_CAT_DIRECTIONAL[k] !== false })); + }); + const alertStripItems = computed(() => alertItems.value.filter(a => catShown(a.cat))); + const alertLatest = computed(() => alertStripItems.value[0] || {}); + const alertStripCount = computed(() => alertStripItems.value.length); + const alertTotalCount = computed(() => alertItems.value.length); const alertLevelClass = computed(() => { - const it = alertItems.value; + const it = alertStripItems.value; if (it.some(x => x.level === 'DANGER')) return 'danger'; if (it.some(x => x.level === 'WARNING')) return 'warn'; return 'none'; @@ -2090,7 +2120,8 @@ createApp({ openScan, insTab, todayYmd, insLive, insDone, insEnd, dispOf, loadOpenScan, denyToday, showOpLog, showSettings, watchCandidates, marketOpen, marketState, lastRefresh, autoRefresh, upstream, showUpstream, usrc, loadUpstream, - showAlerts, alertItems, alertLatest, alertLevelClass }; + showAlerts, alertItems, alertLatest, alertLevelClass, alertGroups, alertStripItems, + alertStripCount, alertTotalCount, catShown, toggleCat }; } }).use(ElementPlus).mount('#app');