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:
{{ nm(s.row.ts_code) }} {{ s.row.ts_code }}
- {{ s.row.direction }}—
+ {{ s.row.direction }}—
{{ s.row.level }}
{{ s.row.source }}
{{ s.row.time }}
@@ -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(() => {