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 }}重试
+
该股票暂无判断依据。
@@ -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 ? '在当日选股计划' : '不在当日选股计划' }} + +
@@ -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,