diff --git a/app/services/upstream_signals.py b/app/services/upstream_signals.py
index ab19f82..4a68b81 100644
--- a/app/services/upstream_signals.py
+++ b/app/services/upstream_signals.py
@@ -182,14 +182,27 @@ def _read_alerts(c, ymd):
def _read_metrics(c):
- """mtf 资金异动: payload schema 未在契约里写全, 挑常见字段, 其余整体带过去供页面兜底显示。"""
+ """mtf 资金异动: 真实载荷打包在 stream 的 data(JSON字符串)里, 顶层只有 ts_code ——
+ 之前直接取顶层 z_dd/direction 全落空(页面显示为空)。这里拆 data 取真实字段, 顶层作兜底。
+ mtf 侧字段: direction(inflow/outflow), z_dd(横截面异动z), window_net(净额), window_ret(区间涨跌); 无原生 level。"""
key = "mtf:intraday:stream:metrics"
out = []
for _id, f in c.xrevrange(key, count=_limit()):
code = f.get("ts_code") or f.get("code")
- out.append({"ts_code": code, "z_dd": _num(f.get("z_dd")), "direction": f.get("direction"),
- "level": f.get("level"), "value": _num(f.get("value") if f.get("value") is not None else f.get("net")),
- "raw": {k: v for k, v in f.items() if k not in ("ts_code", "code")}})
+ payload = f.get("data")
+ if isinstance(payload, str):
+ try:
+ payload = json.loads(payload)
+ except Exception:
+ payload = {}
+ if not isinstance(payload, dict):
+ payload = {}
+ g = lambda k, d=None: payload.get(k, f.get(k, d))
+ out.append({"ts_code": code, "direction": g("direction"),
+ "z_dd": _num(g("z_dd")), "window_net": _num(g("window_net")),
+ "window_ret": _num(g("window_ret")), "level": g("level"),
+ "value": _num(g("value") if g("value") is not None else g("net")),
+ "raw": payload})
return {"key": key, "items": out}
diff --git a/app/web/static/index.html b/app/web/static/index.html
index e33e9ad..95d4d67 100644
--- a/app/web/static/index.html
+++ b/app/web/static/index.html
@@ -263,11 +263,11 @@ pre.json{background:var(--surface-2);border:1px solid var(--hair);border-radius:
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) }}
+ {{ flowText(s.row.direction) }}—
+ {{ s.row.z_dd==null?'—':s.row.z_dd }}
+ {{ s.row.window_net==null?'—':money(s.row.window_net) }}
@@ -1404,10 +1404,14 @@ createApp({
const lastRefresh = ref('');
const autoRefresh = ref(true);
const upstream = ref({ sources: {} });
- const showUpstream = ref(true);
+ const showUpstream = ref(false); // 默认折叠, 点标题展开
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 });
+ // 级别筛选记忆: 存 sessionStorage —— 普通刷新保留, 关标签页/新开标签=回默认。默认关 普通/提示。
+ const LV_STORE_KEY = 'pms_level_show';
+ const _lvDefault = { DANGER:true, WARNING:true, IMPORTANT:true, INFO:false, NORMAL:false };
+ const _lvLoad = () => { try { const v = JSON.parse(sessionStorage.getItem(LV_STORE_KEY)); if (v && typeof v === 'object') return { ..._lvDefault, ...v }; } catch (e) {} return { ..._lvDefault }; };
+ const levelShow = reactive(_lvLoad());
const openScan = ref({ by_code: {}, notes: [] });
const insTab = ref('live');
const stratDlg = reactive({ visible:false, busy:false, reasons:[], ts_code:'', price:0,
@@ -1853,7 +1857,7 @@ createApp({
// 级别筛选 (纯前端): 已知级别按开关过滤; 未知级别一律放行 (风控从宽, 不误杀)。
const KNOWN_LV = new Set(['DANGER','WARNING','IMPORTANT','INFO','NORMAL']);
const levelShown = k => levelShow[k] !== false;
- function toggleLevel(k){ levelShow[k] = !levelShown(k); }
+ function toggleLevel(k){ levelShow[k] = !levelShown(k); try { sessionStorage.setItem(LV_STORE_KEY, JSON.stringify(levelShow)); } catch (e) {} }
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);
@@ -1864,6 +1868,8 @@ createApp({
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');
+ const flowText = d => d === 'inflow' ? '流入' : (d === 'outflow' ? '流出' : (d || '—'));
+ const flowClass = d => d === 'inflow' ? 'clr-up' : (d === 'outflow' ? 'clr-down' : 'muted');
// 告警固定分类格子: 6 类常驻 + 其他(catch-all), 永远这几个框、空的显示暂无 —— 不再随数据冒出冒消。
const ALERT_CATS_FIXED = [
{ key:'money_flow', label:'资金流强度' }, { key:'capital', label:'资金分布' }, { key:'qrs', label:'日QRS对称' },
@@ -2188,7 +2194,7 @@ createApp({
upstream, showUpstream, usrc, loadUpstream,
showAlerts, alertItems, alertLatest, alertLevelClass, alertGroups, alertStripItems,
alertStripCount, alertTotalCount, catShown, toggleCat, ageText, alertRowClass,
- alertItemsF, levelShown, toggleLevel, levelCount, ALERT_LEVELS, lvLabel, lvClass, alertGridFixed, passLevel };
+ alertItemsF, levelShown, toggleLevel, levelCount, ALERT_LEVELS, lvLabel, lvClass, alertGridFixed, passLevel, flowText, flowClass };
}
}).use(ElementPlus).mount('#app');