提议卡重排:要紧的放前面,证据收进展开,并标出「建议档位」与「实下股数」不一致

一、重排。原先一张卡一口气铺十六行,实测二十七八个可视行,大半是「暂无」
「近三天没点名」这类占位句,要人在一屏读不完的文字里自己找哪条要紧。
现在按「决定要不要点采纳所必需」分两层:

卡面留——这一笔买多少股多少钱占总规模几成、建议档位、选股系统判决、
决策系统结论(截到 60 字)、公司深度(真有报告才占一行)、为什么选它。
展开里收——研报、安全边际、催化事件、定价状态、相关快讯、行业催化、
判决收敛、建议方案整句、择时层两个期限、候选榜信息、决策系统原话。

「这一笔买多少」原先排在第十四行,而它才是采纳按钮真正会执行的东西,提到第一行。

二、删掉重复行。propBasis 与 propVerdictLine 在判决为「候选」或「仅展示」时
读的是同一个 hard_numbers.basis,逐字相同,模板里又挨着摆两行,于是卡上出现
两行一模一样的话。今天 77 只票里 38 只判候选,每一只出提议都重复一次。
删 propBasis,留 09-04 专门写的收口函数 propVerdictLine。

三、「为什么选它」第一次上卡。propReasons 这个函数写好之后模板里一直没引用过
(grep 只有定义与导出两处),而这恰恰是人做决定最先要看的。顺带不再只取前两条。

四、【要紧】新增一行红字,标出建议档位与实下股数不一致。

研判把握度低或不可用时,advice_service.apply_judge 把档位降成「试探仓」、
把 advice.target_pct 改成 1%,但它明确不改数量(函数说明原文「不改数量,
数量交人」),而页面上采纳按钮没有填数量的地方,这个「交人」无处兑现——
采纳时后端直接抄提议里的 qty 原样落单(web/main.py:485,只有卖出侧会重算)。

155 实测 8 张待确认提议,7 张写着「试探仓(目标 1.0%)」,而每一张实际都会
买 3.00%(标准仓 6% 的第一批),正好三倍:
  002812.SZ 1200股×48.55 实占 3.00%  建议 1%  算量用 6%
  300604.SZ  200股×264.81 实占 3.00%  建议 1%  算量用 6%
  300638.SZ 3300股×18.11 实占 3.05%  建议 1%  算量用 6%
  301525.SZ 1600股×37.37 实占 3.00%  建议 1%  算量用 6%
  688698.SH 1400股×41.49 实占 3.00%  建议 1%  算量用 6%
  605598.SH 2300股×25.24 实占 3.07%  建议 1%  算量用 6%
  688556.SH 7300股×8.11  实占 3.02%  建议 1%  算量用 6%
只有 300118.SZ(标准仓)是自洽的。

根子上的修法是让数量跟着档位重算,那会改变每一笔采纳真正发出去的钱,
还要一并处理「1% 在高价票上凑不满一手」与「pms_position.target_pct 从未写过、
试探仓建的仓日后会被按 6% 补满」两件事,所以单独拍板、本次不做。
在那之前先把矛盾摆到人眼前——让人看错数字下单,比让页面难看严重得多。

判定用的键一个都没动,只动 DOM 与显示函数。两道页面守卫通过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-10 13:00:03 +08:00
parent 9359056d04
commit d30440edb2
1 changed files with 113 additions and 25 deletions

View File

@ -1075,25 +1075,48 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
<span class="muted mono" style="font-size:11px">{{ p.ts_code }}</span> <span class="muted mono" style="font-size:11px">{{ p.ts_code }}</span>
<el-tag size="small" effect="dark" type="primary">{{ tx('action', p.action) }}</el-tag> <el-tag size="small" effect="dark" type="primary">{{ tx('action', p.action) }}</el-tag>
<span class="spacer"></span><span class="muted" style="font-size:11px">{{ p.expire_at }} 前有效</span></div> <span class="spacer"></span><span class="muted" style="font-size:11px">{{ p.expire_at }} 前有效</span></div>
<div class="st" v-if="p.judge_reason"><span class="muted">决策系统:</span>{{ p.judge_reason }}<span class="muted" v-if="(p.hard_numbers||{}).judge_conf!=null">(置信度 {{ (p.hard_numbers||{}).judge_conf }}</span></div> <!-- 2026-09-10 重排。原先这里一口气铺十六行、实测一张卡二十七八个可视行,
其中大半是「暂无」「近三天没点名」这类占位句 —— 要人在一屏读不完的
文字里自己找哪一条是要紧的。现在按「决定要不要点采纳所必需」分两层:
必需的留在卡面, 证据类的收进展开。判定用的键一个都没动, 只动 DOM。 -->
<!-- 第一行就是这一笔到底买多少。原先它排在第十四行, 而它才是采纳按钮真正
会执行的东西。 -->
<div class="st"><b>{{ p.qty }} 股</b> · 参考价 {{ (p.hard_numbers||{}).price==null ? '—' : (p.hard_numbers||{}).price }}
· 约需 <b>{{ money(((p.hard_numbers||{}).price||0)*(p.qty||0)) }}</b>
<span class="muted" v-if="propSizePct(p)">(占总规模 {{ propSizePct(p) }}</span></div>
<!-- 建议档位与实际下单量对不对得上。对不上就直接说, 别让人自己去算。 -->
<div class="st" v-if="propAdviceTier(p)"><span class="muted">建议档位:</span>{{ propAdviceTier(p) }}</div>
<div class="st" v-if="propSizeMismatch(p)" style="color:#d8574a">{{ propSizeMismatch(p) }}</div>
<div class="st" v-if="(p.hard_numbers||{}).verdict"><span class="muted">选股系统:</span>{{ propVerdictLine(p) }}</div> <div class="st" v-if="(p.hard_numbers||{}).verdict"><span class="muted">选股系统:</span>{{ propVerdictLine(p) }}</div>
<div class="st muted" v-if="propBasis(p)">{{ propBasis(p) }}</div> <div class="st" v-if="p.judge_reason"><span class="muted">决策系统:</span>{{ propJudgeShort(p) }}<span class="muted" v-if="(p.hard_numbers||{}).judge_conf!=null">(把握度 {{ (p.hard_numbers||{}).judge_conf }}</span></div>
<!-- 公司深度只在真有报告时才占一行。判据用结构化的 company_review 而不是
那句整句 —— 没报告时上游下发的是一句 21 字的占位, 实测 77 只票里 64 只
都是它, 占着位置又不说事。 -->
<div class="st" v-if="(p.hard_numbers||{}).company_review"><span class="muted">公司深度:</span>{{ propText(p, 'company_review_text') }}</div>
<!-- 为什么选它。这三条一直算好放在 hard_numbers.reasons 里, 但模板从来没用过。 -->
<div class="st muted" v-for="(x, ri) in propReasons(p)" :key="'r'+ri">{{ x }}</div>
<div class="st muted clickable" @click="togglePropMore(p.proposal_id)">
{{ propMoreOpen.has(p.proposal_id) ? '▾ 收起证据' : '▸ 展开证据(研报、安全边际、催化、定价、快讯、行业、择时层)' }}
</div>
<template v-if="propMoreOpen.has(p.proposal_id)">
<div class="st" v-if="propLogic(p)"><span class="muted">研报说:</span>{{ propLogic(p) }}<span class="muted" v-if="propLogicMore(p)">(另有 {{ propLogicMore(p) }} 条)</span></div> <div class="st" v-if="propLogic(p)"><span class="muted">研报说:</span>{{ propLogic(p) }}<span class="muted" v-if="propLogicMore(p)">(另有 {{ propLogicMore(p) }} 条)</span></div>
<!-- 三句整句都是选股系统写好的中文 (2026-09-07 安全边际、2026-09-08 催化事件与定价状态),
只展示不进判定; 缺就不显示这一行, 不留空壳。 -->
<div class="st muted" v-if="propText(p, 'valuation_text')">{{ propText(p, 'valuation_text') }}</div> <div class="st muted" v-if="propText(p, 'valuation_text')">{{ propText(p, 'valuation_text') }}</div>
<div class="st muted" v-if="propText(p, 'events_text')">{{ propText(p, 'events_text') }}</div> <div class="st muted" v-if="propText(p, 'events_text')">{{ propText(p, 'events_text') }}</div>
<div class="st muted" v-if="propText(p, 'pricing_text')">{{ propText(p, 'pricing_text') }}</div> <div class="st muted" v-if="propText(p, 'pricing_text')">{{ propText(p, 'pricing_text') }}</div>
<div class="st muted" v-if="propText(p, 'news_text')">{{ propText(p, 'news_text') }}</div> <div class="st muted" v-if="propText(p, 'news_text')">{{ propText(p, 'news_text') }}</div>
<div class="st muted" v-if="propText(p, 'industry_catalyst_text')">{{ propText(p, 'industry_catalyst_text') }}</div> <div class="st muted" v-if="propText(p, 'industry_catalyst_text')">{{ propText(p, 'industry_catalyst_text') }}</div>
<!-- 公司深度与建议方案 (2026-09-09 接入方案): 质地档、评议摘要、报告链接; 建议方案与机械方案并排。 -->
<div class="st" v-if="propText(p, 'company_review_text')"><span class="muted">公司深度:</span>{{ propText(p, 'company_review_text') }}</div>
<div class="st" v-if="propText(p, 'company_gate')"><span class="muted">判决收敛:</span>{{ propText(p, 'company_gate') }}</div> <div class="st" v-if="propText(p, 'company_gate')"><span class="muted">判决收敛:</span>{{ propText(p, 'company_gate') }}</div>
<div class="st" v-if="propText(p, 'advice_text')">{{ propText(p, 'advice_text') }}</div> <div class="st" v-if="propText(p, 'advice_text')">{{ propText(p, 'advice_text') }}</div>
<div class="st" v-if="propHeads(p)"><span class="muted">择时层两个期限:</span>{{ propHeads(p) }}</div> <div class="st" v-if="propHeads(p)"><span class="muted">择时层两个期限:</span>{{ propHeads(p) }}</div>
<div class="st muted">{{ propWhy(p) }}</div> <div class="st muted">{{ propWhy(p) }}</div>
<div class="st">约 {{ p.qty }} 股 · 参考价 {{ (p.hard_numbers||{}).price==null ? '—' : (p.hard_numbers||{}).price }} <div class="st muted" v-if="p.judge_reason && propJudgeShort(p) !== p.judge_reason">决策系统原话:{{ p.judge_reason }}</div>
· 约需 {{ money(((p.hard_numbers||{}).price||0)*(p.qty||0)) }}</div> </template>
<div style="margin-top:6px"> <div style="margin-top:6px">
<el-button type="success" size="small" @click="decide(p,'ACCEPTED')">采纳</el-button> <el-button type="success" size="small" @click="decide(p,'ACCEPTED')">采纳</el-button>
<el-button type="danger" size="small" @click="decide(p,'DECLINED')">驳回</el-button> <el-button type="danger" size="small" @click="decide(p,'DECLINED')">驳回</el-button>
@ -2733,15 +2756,69 @@ createApp({
if (h.heat != null) bits.push('热度 ' + (typeof h.heat === 'number' ? h.heat.toFixed(2) : h.heat)); if (h.heat != null) bits.push('热度 ' + (typeof h.heat === 'number' ? h.heat.toFixed(2) : h.heat));
return bits.join(' · ') || '—'; return bits.join(' · ') || '—';
} }
// 选股系统候选卡的理由 (硬数字 reasons, 后端已截到四条), 提议卡上只显示前两条 // 选股系统候选卡的理由 (硬数字 reasons, 后端已截到四条)。
// 2026-09-10: 这个函数写好之后模板里一直没引用过, 卡上从来没显示过「为什么选它」——
// 而这恰恰是人做决定最先要看的。现在放到卡面上, 并且不再只取前两条 (实测每票 2 到 3 条)。
function propReasons(p) { function propReasons(p) {
const r = (p.hard_numbers || {}).reasons; const r = (p.hard_numbers || {}).reasons;
return Array.isArray(r) ? r.slice(0, 2) : []; return Array.isArray(r) ? r.filter(x => typeof x === 'string' && x.trim()) : [];
}
// 2026-09-10 删掉了 propBasis。它 return 的是 hard_numbers.basis, 而 propVerdictLine
// 在判决为「候选」或「仅展示」时 return 的也是同一个 basis —— 两个函数逐字相同,
// 模板里又挨着摆了两行, 于是卡上出现两行一模一样的话 (今天 77 只票里 38 只判候选,
// 每一只出提议都重复一次)。留 propVerdictLine 那一个, 它是 09-04 专门写的收口函数,
// 历史提议表格那一处 (「选股判决」列) 本来就是单独用它、没配 basis 行。
// 这一笔占总规模多少。hard_numbers 里有 target_amount 与 target_pct 两个数,
// 相除就是总规模, 不必再从参数表取一遍 (取一遍还有取不到的可能)。
function propScale(p) {
const h = p.hard_numbers || {};
const amt = Number(h.target_amount), pct = Number(h.target_pct);
return (isFinite(amt) && isFinite(pct) && pct > 0) ? amt / pct : null;
}
function propSizePct(p) {
const h = p.hard_numbers || {};
const scale = propScale(p), price = Number(h.price), qty = Number(p.qty);
if (!scale || !isFinite(price) || !isFinite(qty) || qty <= 0) return '';
return (price * qty / scale * 100).toFixed(2) + '%';
}
// 建议档位的短版。原先卡上摆的是 advice_text 那一整句 (实测 90 多个字, 把档位、
// 机械方案、条件、依据、研判结论全串在一行), 读的人要在里面找「到底建议买多少」。
// 整句仍在展开里, 这里只留结论。
function propAdviceTier(p) {
const a = (p.hard_numbers || {}).advice;
if (!a || typeof a !== 'object') return '';
const tier = String(a.tier || '').trim();
const pct = Number(a.target_pct);
if (!tier) return '';
return tier + (isFinite(pct) ? '(目标 ' + (pct * 100).toFixed(1) + '%' : '');
}
// 【要紧】建议档位与真正会下的股数对不对得上。
//
// 2026-09-10 查出来的问题: 研判把握度低或不可用时, advice_service.apply_judge 会把
// 档位降成「试探仓」并把 advice.target_pct 改成 1%, 但它明确不改数量 (函数说明原文
// 「不改数量, 数量交人」), 而页面上采纳按钮没有填数量的地方, 这个「交人」无处兑现——
// 采纳时后端直接抄提议里的 qty 原样落单。于是卡上写着「试探仓, 目标 1.0%」,
// 点下去买的却是标准仓 6% 的第一批。实测恩捷股份那张卡: 卡面写 1.0%,
// 实下 1200 股 48.55 元共 58,260 元 = 总规模的 2.91%, 差 2.9 倍。
//
// 根子上的修法是让数量跟着档位重算, 那会改变每一笔采纳真正发出去的钱, 要单独拍板。
// 在那之前, 这一行先把矛盾摆到人眼前 —— 让人看错数字下单, 比让页面难看严重得多。
function propSizeMismatch(p) {
const h = p.hard_numbers || {};
const a = h.advice;
if (!a || typeof a !== 'object') return '';
const advPct = Number(a.target_pct), usedPct = Number(h.target_pct);
if (!isFinite(advPct) || !isFinite(usedPct) || usedPct <= 0) return '';
if (Math.abs(advPct - usedPct) < 1e-9) return '';
return '注意:上面那个股数是按 ' + (usedPct * 100).toFixed(1) + '% 那一档算的,'
+ '不是按建议档位的 ' + (advPct * 100).toFixed(1) + '%。采纳后下的就是上面那个数。';
}
// 决策系统那句话最长到 500 字, 而它常常是一句技术报错 (例如研判超时那句)。
// 卡面只留前 60 字, 原话收进展开。
function propJudgeShort(p) {
const r = String(p.judge_reason || '').trim();
return r.length > 60 ? r.slice(0, 60) + '…' : r;
} }
// 研究理由 (2026-09-03): 判决依据是上游对这条判决的一句话解释; 因果论断是研报里说这家
// 公司好在哪, 每行自带「——出处:《研报标题》 披露日」。卡片窄, 只摆第一条论断,
// 但那一条必须原样显示到出处与日期为止 —— 一条 2024 年的论断与一条上周的, 人得一眼分得出。
function propBasis(p) { return (p.hard_numbers || {}).basis || ''; }
// 硬数字里由上游写好的整句 (安全边际 / 催化事件 / 定价状态): 原样显示, 不是字符串就不显示。 // 硬数字里由上游写好的整句 (安全边际 / 催化事件 / 定价状态): 原样显示, 不是字符串就不显示。
function propText(p, key) { function propText(p, key) {
const v = (p.hard_numbers || {})[key]; const v = (p.hard_numbers || {})[key];
@ -3820,6 +3897,14 @@ createApp({
const lFold = ref(!!_rf.l), rFold = ref(!!_rf.r); const lFold = ref(!!_rf.l), rFold = ref(!!_rf.r);
const railOpen = ref(null); // null / 'l' / 'r' const railOpen = ref(null); // null / 'l' / 'r'
const sectClosed = reactive({ prop:false, strat:false, pool:false, ops:false, set:true }); const sectClosed = reactive({ prop:false, strat:false, pool:false, ops:false, set:true });
// 提议卡上「展开证据」的状态, 按提议号记。用整体替换而不是原地增删,
// 因为 Vue 对 Set 的方法调用不作响应, 替换引用才会重画。
const propMoreOpen = ref(new Set());
const togglePropMore = (id) => {
const next = new Set(propMoreOpen.value);
if (next.has(id)) next.delete(id); else next.add(id);
propMoreOpen.value = next;
};
function toggleSect(k) { sectClosed[k] = !sectClosed[k]; } function toggleSect(k) { sectClosed[k] = !sectClosed[k]; }
function _railSave() { function _railSave() {
try { sessionStorage.setItem(RAIL_STORE_KEY, JSON.stringify({ l:lFold.value, r:rFold.value })); } catch (e) {} try { sessionStorage.setItem(RAIL_STORE_KEY, JSON.stringify({ l:lFold.value, r:rFold.value })); } catch (e) {}
@ -3910,8 +3995,11 @@ createApp({
// 但从 2026-09-04 那批文案改造起就一直没写进这份清单, 模板拿到的是 undefined。 // 但从 2026-09-04 那批文案改造起就一直没写进这份清单, 模板拿到的是 undefined。
// 平时提议队列为空、那几行 v-if 不成立所以从不调用, 09-10 早上队列里出现 // 平时提议队列为空、那几行 v-if 不成立所以从不调用, 09-10 早上队列里出现
// 八条新建仓提议, 第一次真的调到它, 整页渲染当场炸掉 —— 表现是整个页面白屏。 // 八条新建仓提议, 第一次真的调到它, 整页渲染当场炸掉 —— 表现是整个页面白屏。
propVerdictLine, propBasis, propLogic, propLogicMore, propText, propHeads, propVerdictLine, propLogic, propLogicMore, propText, propHeads,
hardCn, opCn, paramsCn, dropCn, plainCheck, hardCn, opCn, paramsCn, dropCn, plainCheck,
// 2026-09-10 提议卡重排新增
propScale, propSizePct, propAdviceTier, propSizeMismatch, propJudgeShort,
propMoreOpen, togglePropMore,
pHaltBuy, pResumeBuy, pHaltAll, pResumeAll, pReduce, pIncrease, pLiquidate, pSectorExit, pHaltBuy, pResumeBuy, pHaltAll, pResumeAll, pReduce, pIncrease, pLiquidate, pSectorExit,
pctOf, tgtPos, posMoveValid, posMovePreview, doPosMove, heldSectors, secSel, doSectorExit, pctOf, tgtPos, posMoveValid, posMovePreview, doPosMove, heldSectors, secSel, doSectorExit,
pmap, pval, dcaOn, sumPosition, sumDca, sumAutonomy, pmap, pval, dcaOn, sumPosition, sumDca, sumAutonomy,