diff --git a/app/web/static/index.html b/app/web/static/index.html
index 4508765..e33e9ad 100644
--- a/app/web/static/index.html
+++ b/app/web/static/index.html
@@ -139,6 +139,17 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius:
.alert-win .el-table__row.alert-stale{opacity:.5;}
.alert-win .el-table__row.alert-stale:hover{opacity:.72;}
.alert-age{color:var(--muted);font-size:10.5px;margin-left:5px;}
+/* 上游信号中心: 最新告警细提示 + 级别筛选 chip + 分节标题 */
+.sig-ticker{display:flex;align-items:center;gap:8px;flex-wrap:wrap;background:var(--surface);border:1px solid var(--hair);border-left:3px solid var(--hair);border-radius:12px;padding:8px 14px;margin:16px 0 8px;box-shadow:var(--shadow);font-size:13px;}
+.sig-ticker.lv-danger{border-left-color:var(--stop);} .sig-ticker.lv-warn{border-left-color:var(--warn);}
+.sig-ticker .sig-tk-label{font-weight:640;color:var(--ink-2);white-space:nowrap;}
+.sig-filter{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin:2px 0 12px;}
+.lv-chip{font-size:12px;font-weight:560;padding:2px 11px;border-radius:999px;cursor:pointer;border:1px solid var(--hair);user-select:none;color:var(--ink-2);background:var(--surface-2);}
+.lv-chip.off{color:var(--muted);text-decoration:line-through;opacity:.65;}
+.lv-chip.on.lv-danger{color:var(--stop);background:var(--stop-weak);border-color:var(--stop-weak);}
+.lv-chip.on.lv-warn{color:var(--warn);background:var(--warn-weak);border-color:var(--warn-weak);}
+.lv-chip.on.lv-info{color:var(--ink-2);background:rgba(138,136,130,.14);}
+.sig-sep{font-size:12px;color:var(--muted);font-weight:600;margin:16px 0 8px;padding-top:9px;border-top:1px dashed var(--hair);}
@@ -157,6 +168,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.cat_label }}
- {{ alertLatest.level }}
- {{ alertLatest.time }} · {{ ageText(alertLatest) }}
- 陈旧
-
- · 折叠条已隐藏当前类型(展开后可切回)
-
+
+
+
最新告警
+
+ {{ nm(alertLatest.ts_code) }} {{ alertLatest.ts_code }}
+ {{ alertLatest.direction }}
+ {{ alertLatest.cat_label }}
+ {{ lvLabel(alertLatest.level) }}
+ {{ alertLatest.time }} · {{ ageText(alertLatest) }}
+ 陈旧
-
- · 读取失败:{{ usrc('alerts').error }}
-
-
- · 暂无风控告警
+ · 读取失败:{{ usrc('alerts').error }}
+ · 暂无告警
+
+ 下方查看全部 ▾
+
+
+
+
+
上游信号 · 只读参考,不进下单逻辑 {{ showUpstream?'▲ 收起':'▼ 展开' }}
+
+ 最近取数 {{ (upstream.at||'').slice(11,19) }}(每次刷新拉一次;源自 mtf / 盘中择时层 / 决策系统)
+
+
+ 级别筛选
+ {{ L.label }} {{ levelCount(L.key) }}
+ 点一下切换显隐,只前端过滤、不查后端
+
+
+
+
{{ g.label }} · {{ g.items.length }}
+
+ {{ nm(s.row.ts_code) }} {{ s.row.ts_code }}
+ {{ s.row.direction }}—
+ {{ lvLabel(s.row.level) }}
+ {{ s.row.time }}{{ ageText(s.row) }}
+
+
+
+
+
决策系统 买/卖 · 读取失败:{{ usrc('intraday').error }}
+
+ {{ nm(s.row.ts_code) }} {{ s.row.ts_code }}
+ {{ s.row.action }} · {{ s.row.signal_type }}
+ {{ s.row.time }}
+
+
+
+
+
盘中择时层 · BUY 入场 · 读取失败
+
+ {{ nm(s.row.ts_code) }} {{ s.row.ts_code }}
+ {{ s.row.price==null?'—':s.row.price }}
+ {{ s.row.confidence==null?'—':s.row.confidence }}
+ {{ s.row.time }}
+
+
+
+
+
mtf 资金异动 · 读取失败
+
+ {{ nm(s.row.ts_code) }} {{ s.row.ts_code }}
+ {{ s.row.direction || '—' }}
+ {{ s.row.z_dd==null?'—':s.row.z_dd }}
+ {{ lvLabel(s.row.level) }}
+
+
+
+
+
决策系统 · 风控卖出 · 读取失败
+
+ {{ nm(s.row.ts_code) }}
+ {{ s.row.confidence==null?'—':s.row.confidence }}
+ {{ s.row.reason || '—' }}
+ {{ s.row.time }}
+
+
+
+
+
mtf 实况分 · 关注 / 回避榜 · 读取失败
+
+
+
关注(高分)
+
{{ nm(r.ts_code) }} {{ r.ts_code }}{{ r.score }}
+
暂无
+
+
+
回避(低分)
+
{{ nm(r.ts_code) }} {{ r.ts_code }}{{ r.score }}
+
暂无
+
+
+
+
+
-
-
-
- {{ g.label }} · {{ g.items.length }}
- {{ catShown(g.key) ? '折叠条显示 ✓' : '折叠条隐藏' }}
-
-
- {{ nm(s.row.ts_code) }} {{ s.row.ts_code }}
- {{ s.row.direction }}—
- {{ s.row.level }}
- {{ s.row.source }}
- {{ s.row.time }}{{ ageText(s.row) }}
-
-
-
@@ -637,72 +708,6 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius:
-
-
-
上游信号 · 只读参考,不进下单逻辑 {{ showUpstream?'▲ 收起':'▼ 展开' }}
-
- 最近取数 {{ (upstream.at||'').slice(11,19) }}(每次刷新拉一次;源自 mtf / 盘中择时层 / 决策系统)
-
-
-
-
决策系统 买/卖 · 读取失败:{{ usrc('intraday').error }}
-
- {{ nm(s.row.ts_code) }} {{ s.row.ts_code }}
- {{ s.row.action }} · {{ s.row.signal_type }}
- {{ s.row.time }}
-
-
-
-
-
盘中择时层 · BUY 入场 · 读取失败
-
- {{ nm(s.row.ts_code) }} {{ s.row.ts_code }}
- {{ s.row.price==null?'—':s.row.price }}
- {{ s.row.confidence==null?'—':s.row.confidence }}
- {{ s.row.time }}
-
-
-
-
-
mtf 资金异动 · 读取失败
-
- {{ nm(s.row.ts_code) }} {{ s.row.ts_code }}
- {{ s.row.direction || '—' }}
- {{ s.row.z_dd==null?'—':s.row.z_dd }}
- {{ s.row.level || '—' }}
-
-
-
-
-
决策系统 · 风控卖出 · 读取失败
-
- {{ nm(s.row.ts_code) }}
- {{ s.row.confidence==null?'—':s.row.confidence }}
- {{ s.row.reason || '—' }}
- {{ s.row.time }}
-
-
-
-
-
mtf 实况分 · 关注 / 回避榜 · 读取失败
-
-
-
关注(高分)
-
{{ nm(r.ts_code) }} {{ r.ts_code }}{{ r.score }}
-
暂无
-
-
-
回避(低分)
-
{{ nm(r.ts_code) }} {{ r.ts_code }}{{ r.score }}
-
暂无
-
-
-
-
-
-
-
-
@@ -1402,6 +1407,7 @@ createApp({
const showUpstream = ref(true);
const showAlerts = ref(false);
const alertShowMap = reactive({ money_flow:true, capital:true, qrs:true, volume:true, multi:true, price_notice:false, buy_emitted:false, other:true });
+ const levelShow = reactive({ DANGER:true, WARNING:true, IMPORTANT:true, INFO:false, NORMAL:false });
const openScan = ref({ by_code: {}, notes: [] });
const insTab = ref('live');
const stratDlg = reactive({ visible:false, busy:false, reasons:[], ts_code:'', price:0,
@@ -1540,14 +1546,29 @@ createApp({
};
const nameMap = ref({});
- const nm = c => (nameMap.value[c] && nameMap.value[c].name) || c;
- const fullName = c => (nameMap.value[c] && nameMap.value[c].full) || '';
+ const normCode = c => {
+ // mtf 用 SH688662/SZ300404 前缀格式, 名称库是 688662.SH 点分格式; 归一后才查得到名字。
+ const m = String(c || '').match(/^(SH|SZ|BJ)(\d{6})$/i);
+ return m ? (m[2] + '.' + m[1].toUpperCase()) : String(c || '');
+ };
+ const nm = c => { const k = normCode(c); return (nameMap.value[k] && nameMap.value[k].name) || c; };
+ const fullName = c => { const k = normCode(c); return (nameMap.value[k] && nameMap.value[k].full) || ''; };
async function loadNames() {
const set = new Set();
- (positions.value || []).forEach(p => p.ts_code && set.add(p.ts_code));
- (proposals.value || []).forEach(p => p.ts_code && set.add(p.ts_code));
- (instructions.value || []).forEach(p => p.ts_code && set.add(p.ts_code));
- (ledger.value || []).forEach(p => p.ts_code && set.add(p.ts_code));
+ const push = c => { if (c) set.add(normCode(c)); };
+ (positions.value || []).forEach(p => push(p.ts_code));
+ (proposals.value || []).forEach(p => push(p.ts_code));
+ (instructions.value || []).forEach(p => push(p.ts_code));
+ (ledger.value || []).forEach(p => push(p.ts_code));
+ // 上游信号面板的代码原来没送去查名 —— 只有恰好在持仓里的才带名。这里一并收进来 (含 mtf 前缀码归一)。
+ const up = (upstream.value && upstream.value.sources) || {};
+ ((up.alerts && up.alerts.items) || []).forEach(x => push(x.ts_code));
+ ((up.intraday && up.intraday.decision) || []).forEach(x => push(x.ts_code));
+ ((up.intraday && up.intraday.timing) || []).forEach(x => push(x.ts_code));
+ ((up.metrics && up.metrics.items) || []).forEach(x => push(x.ts_code));
+ ((up.sell_actions && up.sell_actions.items) || []).forEach(x => push(x.ts_code));
+ ((up.mr && up.mr.watch) || []).forEach(x => push(x.ts_code));
+ ((up.mr && up.mr.avoid) || []).forEach(x => push(x.ts_code));
const codes = [...set].filter(Boolean);
if (!codes.length) return;
const d = await call('get', '/api/names?codes=' + encodeURIComponent(codes.join(',')));
@@ -1829,6 +1850,34 @@ createApp({
}
const usrc = k => (upstream.value.sources || {})[k];
const alertItems = computed(() => { const a = usrc('alerts'); return (a && Array.isArray(a.items)) ? a.items : []; });
+ // 级别筛选 (纯前端): 已知级别按开关过滤; 未知级别一律放行 (风控从宽, 不误杀)。
+ const KNOWN_LV = new Set(['DANGER','WARNING','IMPORTANT','INFO','NORMAL']);
+ const levelShown = k => levelShow[k] !== false;
+ function toggleLevel(k){ levelShow[k] = !levelShown(k); }
+ const passLevel = lv => { const x = String(lv == null ? '' : lv); return !KNOWN_LV.has(x) || levelShow[x] !== false; };
+ const alertItemsF = computed(() => alertItems.value.filter(a => passLevel(a.level)));
+ const levelCount = k => alertItems.value.reduce((n, a) => n + (String(a.level) === k ? 1 : 0), 0);
+ const ALERT_LEVELS = [
+ { key:'DANGER', label:'危险', cls:'danger' }, { key:'WARNING', label:'警告', cls:'warn' },
+ { key:'IMPORTANT', label:'重要', cls:'warn' }, { key:'INFO', label:'提示', cls:'info' },
+ { key:'NORMAL', label:'普通', cls:'info' }];
+ const LV_LABEL = { DANGER:'危险', WARNING:'警告', IMPORTANT:'重要', INFO:'提示', NORMAL:'普通' };
+ const lvLabel = v => LV_LABEL[v] || v || '—';
+ const lvClass = v => v === 'DANGER' ? 'deny' : ((v === 'WARNING' || v === 'IMPORTANT') ? 'queue' : 'wait');
+ // 告警固定分类格子: 6 类常驻 + 其他(catch-all), 永远这几个框、空的显示暂无 —— 不再随数据冒出冒消。
+ const ALERT_CATS_FIXED = [
+ { key:'money_flow', label:'资金流强度' }, { key:'capital', label:'资金分布' }, { key:'qrs', label:'日QRS对称' },
+ { key:'volume', label:'放量异动' }, { key:'multi', label:'多源升级' }, { key:'price_notice', label:'股价异动' }];
+ const alertGridFixed = computed(() => {
+ const by = {};
+ for (const a of alertItemsF.value) { (by[a.cat] || (by[a.cat] = [])).push(a); }
+ const fixed = new Set(ALERT_CATS_FIXED.map(c => c.key));
+ const boxes = ALERT_CATS_FIXED.map(c => ({ key:c.key, label:c.label, items: by[c.key] || [] }));
+ const other = [];
+ Object.keys(by).forEach(k => { if (!fixed.has(k)) other.push(...(by[k] || [])); });
+ boxes.push({ key:'other', label:'其他', items: other });
+ return boxes;
+ });
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; // 未登记类默认进折叠条
@@ -1841,7 +1890,7 @@ createApp({
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 alertStripItems = computed(() => alertItemsF.value.filter(a => catShown(a.cat)));
const alertLatest = computed(() => alertStripItems.value[0] || {});
const alertStripCount = computed(() => alertStripItems.value.length);
const alertTotalCount = computed(() => alertItems.value.length);
@@ -2138,7 +2187,8 @@ createApp({
showOpLog, showSettings, watchCandidates, marketOpen, marketState, lastRefresh, autoRefresh,
upstream, showUpstream, usrc, loadUpstream,
showAlerts, alertItems, alertLatest, alertLevelClass, alertGroups, alertStripItems,
- alertStripCount, alertTotalCount, catShown, toggleCat, ageText, alertRowClass };
+ alertStripCount, alertTotalCount, catShown, toggleCat, ageText, alertRowClass,
+ alertItemsF, levelShown, toggleLevel, levelCount, ALERT_LEVELS, lvLabel, lvClass, alertGridFixed, passLevel };
}
}).use(ElementPlus).mount('#app');