diff --git a/app/services/upstream_signals.py b/app/services/upstream_signals.py index 51826de..8f56a25 100644 --- a/app/services/upstream_signals.py +++ b/app/services/upstream_signals.py @@ -36,6 +36,16 @@ def _limit() -> int: return max(1, param_store.get_int("PMS_UPSTREAM_LIMIT", 40)) +def _alert_scan() -> int: + # 告警流扫更大窗口, 防高频源(如 price_notice)把风控告警挤出"最新N"。 + return max(_limit(), param_store.get_int("PMS_UPSTREAM_ALERT_SCAN", 300)) + + +def _alert_per_cat() -> int: + # 每个类型最多留最新 N 条, 高频源只占它自己那一档, 不挤别的类。 + return max(1, param_store.get_int("PMS_UPSTREAM_ALERT_PER_CAT", 20)) + + def _c208(db: int): """208(与 SIGNAL_REDIS 同实例)的只读客户端, 按库缓存。""" key = ("208", db) @@ -117,6 +127,9 @@ _ALERT_META = { "money_flow_in_intensity": ("看涨", "money_flow", "资金流强度"), "capital_accumulation": ("看涨", "capital", "资金分布"), "volume_breakout": ("看涨", "volume", "放量异动"), + # 股价异动通知: 是"价格大幅变动"事件, 不是情绪信号 → 方向用 涨/跌(区别于看涨/看跌), 单独一类。 + "price_notice_up": ("涨", "price_notice", "股价异动"), + "price_notice_down": ("跌", "price_notice", "股价异动"), "multi_source": (None, "multi", "多源升级"), "intraday_buy_emitted": ("—", "buy_emitted", "买入镜像"), } @@ -124,8 +137,10 @@ _ALERT_META = { def _read_alerts(c, ymd): key = "intraday_alerts:%s" % ymd + per_cat = _alert_per_cat() + counts = {} # cat -> 已收数量; 每类只留最新 per_cat 条(仍按 newest-first) out = [] - for _id, f in c.xrevrange(key, count=_limit()): + for _id, f in c.xrevrange(key, count=_alert_scan()): src = str(f.get("source") or "") meta = f.get("metadata") if not isinstance(meta, dict): @@ -134,12 +149,15 @@ def _read_alerts(c, ymd): except Exception: meta = {} dir_fixed, cat, cat_label = _ALERT_META.get(src, (None, "other", "其他")) + if counts.get(cat, 0) >= per_cat: # 该类已满 → 跳过, 高频源不挤掉别的类 + continue 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 = dir_fixed + counts[cat] = counts.get(cat, 0) + 1 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"))}) diff --git a/app/web/static/index.html b/app/web/static/index.html index df32ba7..c948636 100644 --- a/app/web/static/index.html +++ b/app/web/static/index.html @@ -193,7 +193,7 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius: {{ alertStripCount }} 条 / 全部 {{ alertTotalCount }} 最新 · {{ nm(alertLatest.ts_code) }} {{ alertLatest.ts_code }} - {{ alertLatest.direction }} + {{ alertLatest.direction }} {{ alertLatest.cat_label }} {{ alertLatest.level }} {{ alertLatest.time }} @@ -217,7 +217,7 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius: - + @@ -1395,7 +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 alertShowMap = reactive({ money_flow:true, capital:true, qrs:true, volume:true, multi:true, price_notice:false, 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, @@ -1823,8 +1823,8 @@ createApp({ } const usrc = k => (upstream.value.sources || {})[k]; const alertItems = computed(() => { const a = usrc('alerts'); return (a && Array.isArray(a.items)) ? a.items : []; }); - 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 ALERT_CAT_ORDER = ['money_flow','capital','qrs','volume','multi','price_notice','buy_emitted','other']; + const ALERT_CAT_DIRECTIONAL = { money_flow:true, capital:true, qrs:true, volume:true, multi:true, price_notice:true, buy_emitted:false, other:false }; const catShown = k => alertShowMap[k] !== false; // 未登记类默认进折叠条 function toggleCat(k){ alertShowMap[k] = !catShown(k); } const alertGroups = computed(() => {