技术面接入工作包一(页面):顶栏新鲜度芯片、持仓研究面列、单票抽屉技术面节

- 顶栏加技术面新鲜度芯片(调 /api/tech/status,绿=映射新鲜/黄=读不到或过期/灰=没开)
- 我的持仓加「研究面」列显示技术面立场,点击进单票抽屉
- 单票抽屉顶部加「研究面·技术面」节(调 /api/research),展示立场/相位/理由/三指标读数
- /api/positions 每行挂 tech 立场;设置页自动列出 11 个 PMS_TECH_* 参数(无需另做设置卡)
- 两道页面守卫 ALL OK;本地起服务冒烟确认不白屏、顶栏芯片与研究面列渲染正确

候选技术芯片、管理视图修显示错误并入工作包二一起铺。未真机判收。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zlt 2026-09-11 12:58:08 +08:00
parent 0d3984582c
commit d89580240b
3 changed files with 78 additions and 2 deletions

View File

@ -1341,7 +1341,7 @@ PMS_JUDGE_ONLINE_EFFORT=low / PMS_JUDGE_ONLINE_MAX_TOKENS=16384 / PMS_JUDGE_REPA
未判收。开发机临时 venv 跑全量单测见 ALL SUITES PASS含新增 batch27 二十七例)。接口字段已只读探活 188 坐实。待 155 部署后按方案第七节判收make test、pull_and_map 行数五千上下、映射只数等于持仓加候选、status、第一个交易日 08:4x 映射写入时刻与读数日。 未判收。开发机临时 venv 跑全量单测见 ALL SUITES PASS含新增 batch27 二十七例)。接口字段已只读探活 188 坐实。待 155 部署后按方案第七节判收make test、pull_and_map 行数五千上下、映射只数等于持仓加候选、status、第一个交易日 08:4x 映射写入时刻与读数日。
**还欠着什么** **还欠着什么**
1. 页面七处建议并入工作包二,与三源合议的提议卡芯片、基本面质地芯片一次铺齐,避免先做技术面芯片再返工重排提议卡 1. 页面已补技术面这一路的核心三项:顶栏新鲜度芯片、我的持仓「研究面」列、单票抽屉「研究面·技术面」节;设置页自动列出十一个 PMS_TECH_* 参数(无需另做设置卡)。持仓接口每行挂 tech 立场。开发机起本地服务冒烟确认不白屏、芯片与列渲染正确。剩下与三源合议耦合的两项——候选栏技术芯片、管理视图持仓总览修显示错误——并入工作包二,与提议卡三芯片、基本面质地芯片一次铺齐,避免返工。
2. 工作包二三源合议与仓位矩阵、工作包三离场纪律、工作包四选股打分另一仓库单独审批、工作包五README 调度总表补 13 位等文档)未做。 2. 工作包二三源合议与仓位矩阵、工作包三离场纪律、工作包四选股打分另一仓库单独审批、工作包五README 调度总表补 13 位等文档)未做。
3. BIONIC_PMS_INTERFACE.md 需补技术面只读接口一节(本次接口探活的字段事实源)。 3. BIONIC_PMS_INTERFACE.md 需补技术面只读接口一节(本次接口探活的字段事实源)。

View File

@ -381,6 +381,12 @@ def api_positions():
logic_state_service.decorate_positions(v["positions"]) logic_state_service.decorate_positions(v["positions"])
except Exception as e: # noqa: BLE001 except Exception as e: # noqa: BLE001
logger.warning("[持仓] 入场论点与逻辑状态两栏取不到: %s", e) logger.warning("[持仓] 入场论点与逻辑状态两栏取不到: %s", e)
# 技术面立场 (2026-09-11): 每行挂 tech看多/看空/中性/无读数),只是显示,取不到不塌。
try:
from app.services import tech_service
tech_service.attach(v["positions"])
except Exception as e: # noqa: BLE001
logger.warning("[持仓] 技术面立场挂不上: %s", e)
return {"ok": True, **v, "stock_params": sp} return {"ok": True, **v, "stock_params": sp}
return ok(_view) return ok(_view)

View File

@ -432,6 +432,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
<span class="chip hd-exec" :class="ov.exec_halt?'warn':'ok'"><span class="dot"></span>执行 {{ ov.exec_halt ? '已暂停':'正常' }}</span> <span class="chip hd-exec" :class="ov.exec_halt?'warn':'ok'"><span class="dot"></span>执行 {{ ov.exec_halt ? '已暂停':'正常' }}</span>
<span class="chip" :class="ov.autonomy==='off'?'warn':'ok'"><span class="dot"></span>自主 {{ tx('autonomy', ov.autonomy) }}<span class="help" data-tip="系统能自己做主到什么程度。全自动=各项检查通过就直接下单;只提议=先在「等我拍板」问过你;暂停=系统不自己动。">?</span></span> <span class="chip" :class="ov.autonomy==='off'?'warn':'ok'"><span class="dot"></span>自主 {{ tx('autonomy', ov.autonomy) }}<span class="help" data-tip="系统能自己做主到什么程度。全自动=各项检查通过就直接下单;只提议=先在「等我拍板」问过你;暂停=系统不自己动。">?</span></span>
<span class="chip hd-plan" :class="planStatus.ok===false?'warn':'ok'"><span class="dot"></span>选股计划<span class="hd-plan-d"> {{ planStatus.ok===false ? '读不到' : (planStatus.date || '-') }}</span></span> <span class="chip hd-plan" :class="planStatus.ok===false?'warn':'ok'"><span class="dot"></span>选股计划<span class="hd-plan-d"> {{ planStatus.ok===false ? '读不到' : (planStatus.date || '-') }}</span></span>
<span class="chip hd-tech" :class="techChipCls"><span class="dot"></span>技术面<span class="hd-plan-d"> {{ techChipText }}</span><span class="help" data-tip="每晚算的技术面读数布林线、多空布林线、SAR是哪天的。绿今天的映射新鲜读不到或已过期技术面这一层没开。">?</span></span>
<el-tooltip placement="bottom-start" effect="light" :show-after="120"> <el-tooltip placement="bottom-start" effect="light" :show-after="120">
<template #content> <template #content>
<div style="max-width:360px;font-size:12px;line-height:1.75"> <div style="max-width:360px;font-size:12px;line-height:1.75">
@ -926,6 +927,17 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
<div class="muted">{{ pct(s.row.cushion_pct) }}</div> <div class="muted">{{ pct(s.row.cushion_pct) }}</div>
<div class="muted" v-if="s.row.neg_cushion_days>0">连亏 {{ s.row.neg_cushion_days }} 日</div></template> <div class="muted" v-if="s.row.neg_cushion_days>0">连亏 {{ s.row.neg_cushion_days }} 日</div></template>
</el-table-column> </el-table-column>
<el-table-column label="研究面" width="92">
<template #default="s">
<template v-if="s.row.tech">
<b :class="techStanceCls(s.row.tech.stance)" style="cursor:pointer;text-decoration:underline dotted"
@click.stop="openCodeSignals(s.row.ts_code)"
:title="'技术面:' + s.row.tech.stance + '' + (s.row.tech.phase||'') + '\n' + (s.row.tech.reason||'') + '\n点一下看研究面'">{{ s.row.tech.stance }}</b>
<div class="muted" style="font-size:11px" v-if="s.row.tech.phase">{{ s.row.tech.phase }}</div>
</template>
<span class="muted" v-else>无读数</span>
</template>
</el-table-column>
<el-table-column label="今天的信号" width="108"> <el-table-column label="今天的信号" width="108">
<template #default="s"> <template #default="s">
<template v-if="heldSigOf(s.row.ts_code).length"> <template v-if="heldSigOf(s.row.ts_code).length">
@ -1947,6 +1959,30 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
下面列出的不是这只票的全部,是能读到的那部分。 下面列出的不是这只票的全部,是能读到的那部分。
</div> </div>
<div class="cs-sect" v-if="techFeed || techFeedErr">
<div class="h">研究面 · 技术面<span class="muted" style="font-weight:400" v-if="techFeed && techFeed.tech"> · 读数日 {{ techFeed.tech.data_date }}</span></div>
<div class="muted" v-if="techFeedErr">读不到技术面:{{ techFeedErr }}</div>
<div class="muted" v-else-if="!techFeed || !techFeed.tech">{{ (techFeed && techFeed.tech_note) || '这只票没有技术面读数' }}</div>
<template v-else>
<div class="cs-row">
<span class="s">立场</span>
<b :class="techStanceCls(techFeed.tech.stance)">{{ techFeed.tech.stance }}<span v-if="techFeed.tech.strength"> · {{ techFeed.tech.strength }}</span></b>
<span v-if="techFeed.tech.phase">{{ techFeed.tech.phase }}</span>
<span class="ab-stale" v-if="techFeed.tech.choppy">震荡市</span>
<div style="flex-basis:100%;font-size:12px;line-height:1.45" class="muted" v-if="techFeed.tech.reason">{{ techFeed.tech.reason }}</div>
</div>
<div class="cs-row" v-if="techFeed.tech.latest">
<span class="s">布林</span><span>{{ (techFeed.tech.latest.boll||{}).state || '—' }}</span>
<span class="muted" v-if="(techFeed.tech.latest.boll||{}).squeeze">收口</span>
<span class="s">多空布林线</span><span>{{ (techFeed.tech.latest.bbi||{}).state || '—' }}</span>
<span class="s">SAR</span>
<b :class="techFeed.tech.sar_side==='多'?'clr-up':'clr-down'">{{ techFeed.tech.sar_side || '—' }}</b>
<span class="muted" v-if="techFeed.tech.sar_value!=null">止损位 <span class="mono">{{ techFeed.tech.sar_value }}</span></span>
<span class="muted" v-if="techFeed.tech.sar_flip_days!=null">翻向 {{ techFeed.tech.sar_flip_days }} 天</span>
</div>
</template>
</div>
<div class="cs-sect"> <div class="cs-sect">
<div class="h">时间线 · {{ csTimeline.length }} 条</div> <div class="h">时间线 · {{ csTimeline.length }} 条</div>
<div class="muted" v-if="!csTimeline.length && !csLoading"> <div class="muted" v-if="!csTimeline.length && !csLoading">
@ -2326,6 +2362,26 @@ createApp({
const planStatus = computed(() => planRaw.value.status || {}); const planStatus = computed(() => planRaw.value.status || {});
const planRows = computed(() => planRaw.value.rows || []); const planRows = computed(() => planRaw.value.rows || []);
const planCand = computed(() => planRaw.value.candidates_raw || null); const planCand = computed(() => planRaw.value.candidates_raw || null);
// 技术面模块状态 (2026-09-11): 顶栏新鲜度芯片。只读 /api/tech/status, 交易员也看得到。
const techStatus = ref({});
const techChipText = computed(() => {
const t = techStatus.value || {};
if (t.enabled === false) return '关';
const dd = t.data_date;
if (!dd) return '无';
const s = String(dd);
return s.length === 8 ? s.slice(4, 6) + '-' + s.slice(6, 8) : s;
});
const techChipCls = computed(() => {
const t = techStatus.value || {};
if (t.enabled === false) return 'idle';
if (t.error || !t.data_date) return 'warn';
return (t.map || {}).fresh ? 'ok' : 'warn';
});
const loadTech = async () => {
const d = await call('get', '/api/tech/status');
techStatus.value = (d && d.ok) ? (d.data || {}) : { error: (d || {}).error || '读不到' };
};
// 榜单变化 (PMS 自算 —— 上游 changes 字段恒为 null)。另起一个接口, 不塞进 /api/upstream/plan: // 榜单变化 (PMS 自算 —— 上游 changes 字段恒为 null)。另起一个接口, 不塞进 /api/upstream/plan:
// 它要读库, 而计划预览要读上游, 两者的故障域不同, 混在一起会互相拖垮。 // 它要读库, 而计划预览要读上游, 两者的故障域不同, 混在一起会互相拖垮。
const chgRaw = ref({}), chgLoading = ref(false), chgTab = ref('held'); const chgRaw = ref({}), chgLoading = ref(false), chgTab = ref('held');
@ -3286,6 +3342,17 @@ createApp({
// 补全失败就保留快照并挂一条黄条, 不清空 —— 有一半总比没有强。 // 补全失败就保留快照并挂一条黄条, 不清空 —— 有一半总比没有强。
const csOpen = ref(false), csCode = ref(''), csLoading = ref(false), const csOpen = ref(false), csCode = ref(''), csLoading = ref(false),
csErr = ref(''), csData = ref(null), csFromSnap = ref(false); csErr = ref(''), csData = ref(null), csFromSnap = ref(false);
// 研究面·技术面 (2026-09-11): 单票抽屉顶部一节, 点票时顺带调 /api/research 拉回。
const techFeed = ref(null), techFeedErr = ref('');
const techStanceCls = (s) => s === '看多' ? 'clr-up' : (s === '看空' ? 'clr-down' : 'muted');
async function csLoadResearch() {
const code = csCode.value;
techFeed.value = null; techFeedErr.value = '';
const d = await call('get', '/api/research/' + encodeURIComponent(code));
if (csCode.value !== code) return; // 期间换了票, 丢弃这份回包
if (d && d.ok === false) { techFeedErr.value = d.error || '读不到研究面'; return; }
techFeed.value = (d && d.data) ? d.data : d;
}
function _csSnapRows(code) { function _csSnapRows(code) {
const k = code6(code), out = []; const k = code6(code), out = [];
const push = (src, label, r, extra) => out.push(Object.assign( const push = (src, label, r, extra) => out.push(Object.assign(
@ -3319,6 +3386,7 @@ createApp({
csOpen.value = true; csOpen.value = true;
csLoadFull(); csLoadFull();
csLoadLedger(); csLoadLedger();
csLoadResearch();
} }
function closeCodeSignals() { csOpen.value = false; } function closeCodeSignals() { csOpen.value = false; }
async function csLoadFull() { async function csLoadFull() {
@ -3500,7 +3568,7 @@ createApp({
// (通道告警对交易员改由 dispatch-mode 里的 channel 兜底, 见 wsc 的注释) // (通道告警对交易员改由 dispatch-mode 里的 channel 兜底, 见 wsc 的注释)
(isAdmin.value ? loadWs() : Promise.resolve()), (isAdmin.value ? loadWs() : Promise.resolve()),
loadStrategies(), loadOpLog(), loadOpenScan(), loadUpstream(), loadStrategies(), loadOpLog(), loadOpenScan(), loadUpstream(),
loadMacro()]); loadMacro(), loadTech()]);
await loadNames(); await loadNames();
lastRefresh.value = new Date().toTimeString().slice(0, 8); lastRefresh.value = new Date().toTimeString().slice(0, 8);
loading.value = false; loading.value = false;
@ -4003,6 +4071,7 @@ createApp({
cancelCmd, replan, decide, ops, loadSchema, openReport, cancelIns, cancelCmd, replan, decide, ops, loadSchema, openReport, cancelIns,
planDrawer, planLoading, planStatus, planRows, planCand, planResult, planDrawer, planLoading, planStatus, planRows, planCand, planResult,
loadPlan, openPlan, refreshPlan, loadPlan, openPlan, refreshPlan,
techStatus, techChipText, techChipCls,
chgRaw, chgSt, chgLoading, chgTab, chg, chgHeld, chgLog, chgRevised, chgWatch, chgRaw, chgSt, chgLoading, chgTab, chg, chgHeld, chgLog, chgRevised, chgWatch,
loadChanges, loadChanges,
mode, tx, nm, fullName, nameMap, attention, heldPositions, traderParams, mode, tx, nm, fullName, nameMap, attention, heldPositions, traderParams,
@ -4031,6 +4100,7 @@ createApp({
csOpen, csCode, csLoading, csErr, csData, csFromSnap, csTimeline, csSrcFails, csOpen, csCode, csLoading, csErr, csData, csFromSnap, csTimeline, csSrcFails,
csTruncated, csSrcLabel, csLedger, csLedgerErr, csHeld, csInPlan, csTruncated, csSrcLabel, csLedger, csLedgerErr, csHeld, csInPlan,
openCodeSignals, closeCodeSignals, csLoadFull, openCodeSignals, closeCodeSignals, csLoadFull,
techFeed, techFeedErr, techStanceCls,
macro, macroGateOn, macroUnavailable, mzLabel, mzColor, mzBarStyle, macro, macroGateOn, macroUnavailable, mzLabel, mzColor, mzBarStyle,
loadMacro, macroScan, macroAdopt, loadMacro, macroScan, macroAdopt,
mzSig, mzAdvicePending, mzChipCls, mzChipText, macroChipClick, mzSig, mzAdvicePending, mzChipCls, mzChipText, macroChipClick,