diff --git a/app/web/main.py b/app/web/main.py
index 36a3ca3..a8ab670 100644
--- a/app/web/main.py
+++ b/app/web/main.py
@@ -787,10 +787,15 @@ def api_research(ts_code: str):
# 四块意见: assemble 用 company_review 算基本面、用映射算技术面、用昨夜定性算择时, 再合议。
nightly = consensus_service.nightly_map([code]).get(code)
tech_states = consensus_service.state_map()
- blocks = consensus_service.assemble({"ts_code": code, "company_review": cr},
- nightly=nightly, tech_states=tech_states)
# 技术面详细读数 (点击时现读, 带最新一行与近日翻向次数); 缺则退回映射里的紧凑块。
tfeed = tech_service.research_feed(code)
+ # 合议也用现读的这份 (2026-09-15): 早上映射只覆盖相关票, 不在映射里的票合议会按技术面无读数算,
+ # 而抽屉又显示现读的立场, 两处对不上 (实见 300750: 技术面看空却判「只有基本面一路看多」)。
+ # 扫描那条路仍用映射, 这里只管显示自洽。
+ if tfeed.get("tech"):
+ tech_states = {**(tech_states or {}), code: tfeed["tech"]}
+ blocks = consensus_service.assemble({"ts_code": code, "company_review": cr},
+ nightly=nightly, tech_states=tech_states)
tech = tfeed.get("tech") or blocks["tech"]
tm = blocks["timing"]
timing = {"stance": tm.get("stance"), "nightly": tm.get("nightly"),
diff --git a/app/web/static/index.html b/app/web/static/index.html
index 2b911f7..e0b138d 100644
--- a/app/web/static/index.html
+++ b/app/web/static/index.html
@@ -395,6 +395,17 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
.cs-anchors a:hover{background:rgba(64,120,220,.10);color:var(--fg,#222);}
.cs-part{margin:0 0 16px;scroll-margin-top:44px;}
.cs-part>.h{font-size:13px;font-weight:700;margin:0 0 8px;padding-bottom:5px;border-bottom:1px solid var(--hair);}
+/* 系统判断卡与三路意见行 (2026-09-15): 沿用主页面的 .disp 芯片与提议卡 .prop-src-row 来源行。 */
+.cs-verdict{display:flex;align-items:flex-start;gap:12px;background:var(--surface-2);border:1px solid var(--hair);border-radius:11px;padding:11px 13px;margin:0 0 8px;}
+.cs-verdict .disp{flex:none;margin-top:1px;}
+.cs-verdict .txt{flex:1;min-width:0;}
+.cs-verdict .main{font-size:13px;color:var(--ink);line-height:1.5;}
+.cs-verdict .votes{font-size:12px;color:var(--muted);margin-top:3px;}
+.cs-ops{margin:0;border-top:none;}
+.cs-ops .prop-src-row .sub{white-space:normal;}
+.cs-detail{padding:2px 2px 8px 60px;font-size:12px;color:var(--ink-2);line-height:1.55;border-bottom:1px solid rgba(0,0,0,.04);}
+.cs-detail .slow{margin-top:4px;color:var(--muted);}
+.cs-detail .k{display:inline-block;font-size:11px;padding:0 5px;border-radius:5px;background:var(--chip);color:var(--muted);margin-right:6px;}
.cs-row{display:flex;align-items:baseline;gap:7px;padding:4px 0;font-size:12.5px;
border-bottom:1px solid rgba(0,0,0,.04);flex-wrap:wrap;}
.cs-row .t{font-size:11.5px;color:var(--muted);width:40px;flex:none;}
@@ -2105,55 +2116,56 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
-
+
-
研究面 · 三源合议 · 技术面读数日 {{ techFeed.tech.data_date }}
-
研究面读取失败:{{ techFeedErr }}重试
-
该股票暂无研究面读数。
+
系统判断 · 数据到 {{ csYmd(techFeed.tech.data_date) }}
+
判断依据读取失败:{{ techFeedErr }}重试
+
该股票暂无判断依据。
-
-
- 基本面 {{ (techFeed.fund||{}).stance || '无读数' }} · {{ techFeed.fund.mark }}
- 技术面 {{ (techFeed.tech||{}).stance || '无读数' }} · {{ techFeed.tech.phase }}
- 择时 {{ (techFeed.timing||{}).stance || '无读数' }}
- 合议 {{ (techFeed.consensus||{}).direction || '—' }} · {{ techFeed.consensus.strength }}
-
-
- 路由{{ techFeed.consensus.route }}
- {{ techFeed.consensus.route_reason }}
-
-
-
质地{{ techFeed.company.line }}
-
评析报告
-
硬疑点
-
-
- 失效条件{{ techFeed.company.invalidation }}
-
-
-
-
技术面
-
{{ techFeed.tech.stance }} · {{ techFeed.tech.strength }}
-
{{ techFeed.tech.phase }}
-
震荡市
-
{{ techFeed.tech.reason }}
+
+
{{ csVerdict.label }}
+
+
{{ csVerdict.text }}
+
{{ csVerdict.votes }}
-
- 布林{{ (techFeed.tech.latest.boll||{}).state || '—' }}
- 收口
- 多空布林线{{ (techFeed.tech.latest.bbi||{}).state || '—' }}
- SAR
- {{ techFeed.tech.sar_side || '—' }}
- 止损位 {{ techFeed.tech.sar_value }}
- 翻向 {{ techFeed.tech.sar_flip_days }} 天
+
+
+
+ 基本面
+ {{ csFundRow.tag.text }}
+ {{ csFundRow.sub }}{{ csDetail.fund ? '⌄' : '›' }}
-
-
{{ (techFeed.sources||{}).tech || '这只票没有技术面读数' }}
+
+
{{ l }}
+
失效条件{{ csFundRow.invalidation }}
+
+
{{ csFundRow.empty }}
+
+
+ 技术面
+ {{ csTechRow.tag.text }}
+ {{ csTechRow.sub }}{{ csDetail.tech ? '⌄' : '›' }}
+
+
+
{{ l }}
+
{{ csTechRow.empty }}
+
+
+ 择时
+ {{ csTimingRow.tag.text }}
+ {{ csTimingRow.sub }}{{ csDetail.timing ? '⌄' : '›' }}
+
+
+
{{ l }}
+
{{ csTimingRow.empty }}
+
+
@@ -2185,7 +2197,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
同样的又来了 {{ r.dup }} 次
写入时刻
已过时
-
{{ r.reason }}
+
{{ csPlainReason(r.reason) }}
当前快照这两路上游只给当前值,不带时间
@@ -2218,10 +2230,14 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
{{ tx('arbiter', r.arbiter) || r.arbiter }}
{{ r.reason }}
- 处置
- {{ csDisp.held ? '在持仓里' : '没有持仓' }}
- ·
- {{ csDisp.inPlan ? '在今天的选股计划里' : '不在今天的选股计划里' }}
+ 处置
+ {{ csDisp.held ? '持仓中' : '未持仓' }}
+ {{ csDisp.inPlan ? '在当日选股计划' : '不在当日选股计划' }}
+
+ {{ csDisp.cand.label }}
+ {{ csDisp.cand.why }}
+
+
@@ -3637,6 +3653,7 @@ createApp({
return out;
}
function openCodeSignals(code) {
+ Object.assign(csDetail, { fund: false, tech: false, timing: false });
if (!code) return;
csCode.value = String(code);
csErr.value = '';
@@ -3698,7 +3715,93 @@ createApp({
const csSnapMetrics = computed(() => (csData.value && csData.value.snapshot && csData.value.snapshot.metrics) || []);
const csSnapMr = computed(() => (csData.value && csData.value.snapshot && csData.value.snapshot.mr) || {});
const csSignalCount = computed(() => (csTimeline.value || []).length);
- const csDisp = computed(() => ({ held: csHeld(csCode.value), inPlan: csInPlan(csCode.value) }));
+ const csDisp = computed(() => ({ held: csHeld(csCode.value), inPlan: csInPlan(csCode.value),
+ cand: csCode.value ? dispOf(csCode.value) : null }));
+ // ---- 系统判断 (2026-09-15 抽屉重做): 结论一句白话 + 三路意见各一行, 内部设计词不上页面 ----
+ const csDetail = reactive({ fund: false, tech: false, timing: false });
+ function csToggle(k) { csDetail[k] = !csDetail[k]; }
+ const csStanceTag = (st) => ({ text: st || '无读数', cls: st === '看多' ? 'up' : (st === '看空' ? 'down' : 'neutral') });
+ const csYmd = (v) => { const x = String(v || ''); return /^\d{8}$/.test(x) ? x.slice(0, 4) + '-' + x.slice(4, 6) + '-' + x.slice(6) : x; };
+ const csVoteLine = (f) => [['基本面', f.fund], ['技术面', f.tech], ['择时', f.timing]]
+ .map(([k, b]) => k + ((b && b.stance) || '无读数')).join('、');
+ // 后端的路由原因是给复盘看的, 这里翻成用户看得懂的一句话。
+ function csPlainRoute(r) {
+ r = String(r || '');
+ if (r.includes('没有买方评析')) return '没有买方评析,不建仓';
+ if (r.startsWith('方向看空')) return '多数意见看空,不建仓';
+ if (r.includes('只有一方表态')) return '只有一路意见看多,其余没有明确意见,最多按试探仓小仓建';
+ if (r.includes('基本面看空') && r.includes('技术面看多')) return '基本面偏弱但技术面走强,最多按试探仓小仓建';
+ if (r.includes('等择时转多')) return '基本面与技术面看多,等择时系统转多';
+ if (r.includes('择时')) return '择时系统有异议,等其转多';
+ if (r.includes('等技术面开口')) return '布林带仍在收口,等开口后再评估';
+ if (r.includes('等技术面转向')) return '技术面偏空,等转向后再评估';
+ return r;
+ }
+ const csVerdict = computed(() => {
+ const f = techFeed.value; if (!f) return null;
+ const con = f.consensus || {}, tech = f.tech || {}, fund = f.fund || {};
+ const votes = csVoteLine(f);
+ if (csHeld(csCode.value)) {
+ if (tech.phase === '转空') return { cls: 'deny', label: '离场纪律', text: '技术面刚转空,按纪律减持或清仓', votes };
+ if (con.direction === '看空') return { cls: 'deny', label: '增持暂停', text: '多数意见看空,只减不加', votes };
+ if (tech.stance === '看空') return { cls: 'queue', label: '增持受限', text: '技术面偏空,暂停回踩补足与盈利加仓,减持照常', votes };
+ if (fund.stance === '看空') return { cls: 'queue', label: '补仓受限', text: '基本面偏弱,暂停补仓', votes };
+ return { cls: 'done', label: '持有', text: '意见未偏空,按纪律可增持', votes };
+ }
+ const route = con.route;
+ if (!route) return { cls: 'wait', label: '暂无判断', text: '意见不全,等数据到齐', votes };
+ if (route === '放行') return { cls: 'done', label: '符合建仓条件', text: con.strength === '强' ? '三路意见一致看多,研判通过即建仓' : '多数意见看多,研判通过即建仓', votes };
+ if (route === '观察') return { cls: 'wait', label: '观察中', text: csPlainRoute(con.route_reason), votes };
+ if (route === '交人') return { cls: 'queue', label: '试探仓候选', text: csPlainRoute(con.route_reason) + ';研判通过后再定', votes };
+ if (route === '跳过') return { cls: 'deny', label: '不建仓', text: csPlainRoute(con.route_reason), votes };
+ return { cls: 'wait', label: route, text: con.route_reason || '', votes };
+ });
+ const csFundRow = computed(() => {
+ const f = techFeed.value || {}, fund = f.fund || {}, c = f.company || null;
+ const tag = csStanceTag(fund.stance);
+ const bits = [];
+ if (c && c.overall) bits.push('质地' + c.overall);
+ if (c && c.valuation) bits.push('估值' + c.valuation);
+ if (fund.mark) bits.push(fund.mark);
+ if (c && c.doubt_hard) bits.push('有硬疑点');
+ const sub = bits.length ? bits.join(' · ') : (fund.fact || fund.why || ((f.sources || {}).fund) || '暂无买方评析');
+ const lines = [];
+ if (c && c.line) lines.push(c.line);
+ if (c && c.period) lines.push('报告期 ' + c.period);
+ return { tag, sub, lines, invalidation: c ? c.invalidation : null, report_url: c ? c.report_url : null,
+ empty: (f.sources || {}).company || '暂无买方评析' };
+ });
+ const csTechRow = computed(() => {
+ const f = techFeed.value || {}, t = f.tech || {};
+ const tag = csStanceTag(t.stance);
+ const head = [t.phase, t.strength ? t.strength : null].filter(Boolean).join('·');
+ const sub = t.stance ? (head ? head + ':' : '') + (t.reason || '') : ((f.sources || {}).tech || '暂无技术面读数');
+ const lines = [];
+ const L = t.latest || null;
+ if (L) {
+ const b = L.boll || {}, bb = L.bbi || {}, sr = L.sar || {};
+ lines.push('布林线 ' + (b.state || '—') + (b.squeeze ? '(收口)' : '') + (b.bw_pct != null ? ',带宽 ' + b.bw_pct : ''));
+ lines.push('多空布林线 ' + (bb.state || '—'));
+ lines.push('SAR ' + (sr.side || t.sar_side || '—') + (sr.value != null ? ',止损位 ' + sr.value : (t.sar_value != null ? ',止损位 ' + t.sar_value : '')) + (sr.flip_days != null ? ',翻向 ' + sr.flip_days + ' 天' : ''));
+ } else if (t.sar_side) {
+ lines.push('SAR ' + t.sar_side + (t.sar_value != null ? ',止损位 ' + t.sar_value : '') + (t.sar_flip_days != null ? ',翻向 ' + t.sar_flip_days + ' 天' : ''));
+ }
+ if (t.choppy) lines.push('近二十日翻向频繁,属震荡市');
+ if (t.data_date) lines.push('读数日 ' + csYmd(t.data_date));
+ return { tag, sub, lines, empty: (f.sources || {}).tech || '暂无技术面读数' };
+ });
+ const csTimingRow = computed(() => {
+ const f = techFeed.value || {}, m = f.timing || {};
+ const tag = csStanceTag(m.stance);
+ const NIGHT = { BUY: '建议买入', WATCH: '观望', SELL: '建议卖出', AVOID: '回避', DROPPED: '已剔除' };
+ const bits = [];
+ if (m.nightly) bits.push('昨夜定性 ' + (NIGHT[m.nightly] || m.nightly) + (m.trade_date ? '(' + csYmd(m.trade_date) + ')' : ''));
+ if (m.flip_at) bits.push('盘中 ' + m.flip_at + ' 判转多');
+ const sub = bits.length ? bits.join(' · ') : ((f.sources || {}).timing || '决策系统昨夜没有这只票的结论');
+ return { tag, sub, lines: bits, empty: (f.sources || {}).timing || '暂无择时读数' };
+ });
+ // 信号说明里的裸英文码 (如 money_flow_in_intensity) 不当句子显示。
+ const csPlainReason = (r) => { const s = String(r || '').trim(); return (!s || /^[A-Za-z0-9_.:\-]+$/.test(s)) ? '' : s; };
// 「今天的信号」列与持仓角标都只数今天的 (2026-08-28): 昨天的风控卖出不再冒充今天
const sigBriefHeldCount = computed(() => sigBrief.value.filter(x => x.held && x.today !== false).length);
@@ -4127,7 +4230,12 @@ createApp({
onUnmounted(() => { if (refreshTimer) clearInterval(refreshTimer); });
onMounted(async () => {
await checkMe();
- if (authed.value) { loadAll(); loadPlan(); startAutoRefresh(); }
+ if (authed.value) {
+ loadAll(); loadPlan(); startAutoRefresh();
+ // 地址栏带 #detail=代码 就直接打开该票的个股详情 (2026-09-15): 分享链接与预览都用得上。
+ const h = String(location.hash || '');
+ if (h.startsWith('#detail=')) openCodeSignals(decodeURIComponent(h.slice(8)));
+ }
}); // 首屏: 先验登录, 已登录才拉数据与启动自动刷新
// ═══════════ 交易员工作台改版 (2026-08-26) ═══════════
// 一、融合消息栏: 把四类消息合成一条按时间倒序的流。
@@ -4391,6 +4499,7 @@ createApp({
csOpen, csCode, csLoading, csErr, csData, csFromSnap, csTimeline, csSrcFails,
csTruncated, csSrcLabel, csLedger, csLedgerErr, csHeld, csInPlan,
csSnapMetrics, csSnapMr, csSignalCount, csDisp, csLoadResearch, csLoadLedger,
+ csDetail, csToggle, csVerdict, csFundRow, csTechRow, csTimingRow, csPlainReason, csYmd,
openCodeSignals, closeCodeSignals, csLoadFull,
techFeed, techFeedErr, techStanceCls,
macro, macroGateOn, macroUnavailable, mzLabel, mzColor, mzBarStyle,