diff --git a/app/web/static/index.html b/app/web/static/index.html
index 81f29f0..83ab0d1 100644
--- a/app/web/static/index.html
+++ b/app/web/static/index.html
@@ -42,6 +42,9 @@
.op-btn{flex:none;width:150px;}
.op-btn .el-button{width:100%;}
.op-row .d{color:#999;font-size:12px;line-height:1.5;padding-top:3px;}
+ .set-card{border:1px solid #333;border-radius:6px;padding:10px 12px;margin-bottom:10px;}
+ .set-sum{color:#ddd;font-size:13px;margin-bottom:8px;line-height:1.6;}
+ .set-fields{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center;color:#999;font-size:12px;}
@@ -228,52 +231,77 @@
组合操作
-
对整个组合的手动动作。带「…」的会先弹窗让你填数值;危险动作要二次确认。
+
对整个组合的手动动作。数值都基于当前实际持仓算出、并预览影响金额,不用盲填;危险动作要二次确认。
{{ ov.buy_halt?'恢复买入':'暂停买入' }}
- 停止一切新增买入,并撤销在途买入指令;卖出与止损不受影响。当前:{{ ov.buy_halt?'已暂停买入':'买入正常' }}。
+ 停止一切新增买入并撤销在途买入指令;卖出与止损不受影响。当前:{{ ov.buy_halt?'已暂停买入':'买入正常' }}。
{{ ov.exec_halt?'结束休假':'休假模式' }}
- 所有自动动作暂停,只保留对账与日报(长假或手动接管时用)。当前:{{ ov.exec_halt?'休假中':'正常运行' }}。
+ 所有自动动作暂停,只保留对账与日报。当前:{{ ov.exec_halt?'休假中':'正常运行' }}。
-
-
降仓…
-
按占总规模的百分比整体减少敞口。优先级:停新买 → 清弱票 → 收利润 → 等比微减。
+
+ 调整仓位
+
+ 当前总仓位 {{ pct(ov.portfolio_pct) }}(市值 {{ money(ov.portfolio_mv) }} / 规模 {{ money(ov.scale) }}),可用资金 {{ money(ov.cash_source==='ws'?ov.cash_avail:ov.cash_est) }}。
+ 目标 %
+ {{ posMovePreview }}
+
-
- 升仓…
- 按占总规模的百分比整体提高敞口。来源:持仓补到目标 + 候选池新票建仓。
-
-
-
清仓某行业…
-
卖光指定行业的全部持仓。{{ ov.sector_ready?'':'(行业数据未就绪,暂不可用)' }}
+
+ 清仓行业
+
+ 卖光某个行业的全部持仓,从下面选(不用手打):
+
+
+
+ —— {{ ov.sector_ready===false?'行业数据未就绪':'当前无带行业的持仓' }}
+
一键清仓(紧急)
- 全部持仓按最快节奏卖出、不做择时优化。用于紧急离场,需二次确认。
+ 全部持仓({{ heldPositions.length }} 只,市值 {{ money(ov.portfolio_mv) }})按最快节奏卖出、不做择时优化。紧急离场用,需二次确认。
设置
-
- {{ s.row.desc || s.row.key }}
-
-
-
-
-
-
-
-
+ 每组先给一句「现在是什么状态」的合成说明,再让你改其中的项。改完点保存即时生效。
+
+
资金与仓位纪律:{{ sumPosition }}
+
+ 总操作规模 元
+ 总仓上限
+ 单股上限
+ 最多持仓 只
+ 预留现金
+
+
+
+
补仓策略:{{ sumDca }}
+
+
是否补仓
+
+ 上限(占底仓)
+ 评估档(浮亏)
+ 深档确认
+ 回踩止损
-
-
-
+
+
+
+
自主档位:{{ sumAutonomy }}
+
+
+
+
+
+
+
+
+
保存修改 ({{ dirtyCount }})
放弃
- 改「自主档位」或「总仓上限」会即时生效;调低总仓上限可能触发降仓提议。
- 补仓(浮亏时逢跌补仓):「补仓上限」设为 0 即关闭补仓;「补仓评估档」是触发补仓的浮亏档位(如 -0.08,-0.15);到「深档确认」及更深的补仓永远要你点头。
+ 关闭补仓=把「补仓上限」设为 0;调低总仓上限可能触发降仓提议。
@@ -1194,15 +1222,19 @@ createApp({
// 个股
function actExitStock(row) { quickCmd('EXIT_STOCK', { ts_code: row.ts_code }, '清仓 ' + nm(row.ts_code) + '?系统会在窗口内择机把它卖光。'); }
async function actReduceStock(row) {
- const v = await promptNum('减到目标仓位(占总规模,%)', '减仓 ' + nm(row.ts_code));
+ const curPct = row.pct_of_scale != null ? (row.pct_of_scale * 100).toFixed(1) + '%' : '—';
+ const v = await promptNum('当前 ' + row.total_qty + ' 股,市值 ' + money(row.market_value)
+ + '(占规模 ' + curPct + ')。减到目标仓位(占总规模,%)', '减仓 ' + nm(row.ts_code));
if (v != null) quickCmd('REDUCE_STOCK', { ts_code: row.ts_code, target_pct: v + '%' });
}
async function actStop(row) {
- const v = await promptNum('止损价(元)', '设止损 ' + nm(row.ts_code));
+ const v = await promptNum('现价 ' + row.price + ',成本 ' + (row.avg_cost || '—')
+ + '。止损价(元,跌破即挂卖)', '设止损 ' + nm(row.ts_code));
if (v != null) quickCmd('SET_STOP_PRICE', { ts_code: row.ts_code, price: v });
}
async function actTargetPrice(row) {
- const v = await promptNum('目标价(元)', '设目标价 ' + nm(row.ts_code));
+ const v = await promptNum('现价 ' + row.price + ',成本 ' + (row.avg_cost || '—')
+ + '。目标价(元,到价生成止盈提议)', '设目标价 ' + nm(row.ts_code));
if (v != null) quickCmd('SET_TARGET_PRICE', { ts_code: row.ts_code, price: v });
}
function actFreeze(row) {
@@ -1228,6 +1260,76 @@ createApp({
let name; try { name = (await ElementPlus.ElMessageBox.prompt('要清仓的行业名称', '清仓某行业', {})).value; } catch (e) { return; }
if (name) quickCmd('SECTOR_EXIT', { sector: name }, '清仓行业「' + name + '」的全部持仓?');
}
+ // ---- 组合仓位调整: 显示当前 + 算出金额, 不盲填 ----
+ const pctOf = v => (v == null || v === '') ? '—' : (Number(v) * 100).toFixed(0) + '%';
+ const tgtPos = ref(null);
+ const posMoveValid = computed(() => {
+ const t = Number(tgtPos.value);
+ return tgtPos.value !== null && tgtPos.value !== '' && !isNaN(t) && t >= 0 && Number(ov.value.scale || 0) > 0;
+ });
+ const posMovePreview = computed(() => {
+ if (!posMoveValid.value) return '';
+ const cur = Number(ov.value.portfolio_pct || 0), scale = Number(ov.value.scale || 0);
+ const delta = Number(tgtPos.value) / 100 - cur, amt = Math.abs(delta) * scale;
+ if (Math.abs(delta) < 0.001) return '与当前仓位一致,无需调整。';
+ return delta < 0
+ ? '→ 降仓:从 ' + pctOf(cur) + ' 降到 ' + tgtPos.value + '%,释放约 ' + money(amt) + ' 元。'
+ : '→ 升仓:从 ' + pctOf(cur) + ' 升到 ' + tgtPos.value + '%,动用约 ' + money(amt) + ' 元。';
+ });
+ function doPosMove() {
+ if (!posMoveValid.value) { ElementPlus.ElMessage.warning('请输入目标仓位'); return; }
+ const cur = Number(ov.value.portfolio_pct || 0), scale = Number(ov.value.scale || 0);
+ const delta = Number(tgtPos.value) / 100 - cur, amt = Math.abs(delta) * scale;
+ if (Math.abs(delta) < 0.001) { ElementPlus.ElMessage.info('与当前仓位一致'); return; }
+ if (delta < 0) quickCmd('REDUCE_EXPOSURE', { pct: (Math.abs(delta) * 100).toFixed(1) + '%' },
+ '降仓到 ' + tgtPos.value + '%,释放约 ' + money(amt) + ' 元?');
+ else quickCmd('INCREASE_EXPOSURE', { pct: (delta * 100).toFixed(1) + '%' },
+ '升仓到 ' + tgtPos.value + '%,动用约 ' + money(amt)
+ + ' 元(可用资金 ' + money(ov.value.cash_source === 'ws' ? ov.value.cash_avail : ov.value.cash_est) + ')?');
+ }
+ // ---- 清仓行业: 下拉选持仓行业, 不手输 ----
+ const heldSectors = computed(() => {
+ const m = {};
+ (positions.value || []).filter(p => (p.total_qty || 0) > 0 && p.sector).forEach(p => {
+ if (!m[p.sector]) m[p.sector] = { sector: p.sector, count: 0, mv: 0 };
+ m[p.sector].count++; m[p.sector].mv += (p.market_value || 0);
+ });
+ return Object.values(m).sort((a, b) => b.mv - a.mv);
+ });
+ const secSel = ref('');
+ function doSectorExit() {
+ if (!secSel.value) { ElementPlus.ElMessage.warning('请选择行业'); return; }
+ const x = heldSectors.value.find(v => v.sector === secSel.value);
+ quickCmd('SECTOR_EXIT', { sector: secSel.value },
+ '清仓行业「' + secSel.value + '」的全部持仓(' + (x ? x.count : '?') + ' 只,约 '
+ + (x ? money(x.mv) : '?') + ' 元)?');
+ }
+ // ---- 设置: 参数映射 + 组合摘要 (给用户看合成结果, 不是孤立参数) ----
+ const pmap = computed(() => { const m = {}; (params.value || []).forEach(p => m[p.key] = p); return m; });
+ const pval = k => { const p = pmap.value[k]; return p ? p.value : null; };
+ const dcaOn = computed({
+ get: () => Number(pval('PMS_DCA_MAX_RATIO') || 0) > 0,
+ set: v => { const p = pmap.value['PMS_DCA_MAX_RATIO']; if (!p) return; p.value = v ? (Number(p.value) > 0 ? p.value : 0.5) : 0; }
+ });
+ const sumPosition = computed(() =>
+ '规模 ' + money(pval('PMS_TOTAL_SCALE')) + ' 元;总仓 ≤ ' + pctOf(pval('PMS_PORTFOLIO_CAP'))
+ + ',单股 ≤ ' + pctOf(pval('PMS_STOCK_CAP')) + ',最多 ' + (pval('PMS_MAX_NAMES') || '—')
+ + ' 只,预留现金 ' + pctOf(pval('PMS_CASH_RESERVE')) + '。');
+ const sumDca = computed(() => {
+ if (!dcaOn.value) return '已关闭 —— 浮亏时不补仓。';
+ const trg = String(pval('PMS_DCA_TRIGGERS') || '').split(',').map(x => x.trim()).filter(Boolean)
+ .map(x => Math.abs(Number(x) * 100).toFixed(0) + '%').join('、');
+ const dc = Math.abs(Number(pval('PMS_DCA_DEEP_CONFIRM') || 0) * 100).toFixed(0) + '%';
+ const fl = Math.abs(Number(pval('PMS_FILL_MAX_LOSS') || 0) * 100).toFixed(0) + '%';
+ return '开启,上限为底仓的 ' + pctOf(pval('PMS_DCA_MAX_RATIO')) + '。浮亏 ' + (trg || '—')
+ + ' 各评估一次补仓;浮亏 ' + dc + ' 及更深的补仓永远需你确认;浮亏超过 ' + fl
+ + ' 就不再回踩补足、转入补仓评估。';
+ });
+ const sumAutonomy = computed(() => ({
+ full: '全自动 —— 系统自主提议并直接执行。',
+ propose_only: '只提议不下手 —— 系统提议,等你在「等我拍板」里确认。',
+ off: '暂停 —— 系统不自主提议。'
+ }[pval('PMS_AUTONOMY')] || (pval('PMS_AUTONOMY') || '—')));
// ===================== /交易员视图 =====================
@@ -1444,7 +1546,9 @@ createApp({
loadChanges,
mode, tx, nm, fullName, nameMap, attention, heldPositions, traderParams,
propWhy, nextStep, scrollTo, actExitStock, actReduceStock, rowMore,
- pHaltBuy, pResumeBuy, pHaltAll, pResumeAll, pReduce, pIncrease, pLiquidate, pSectorExit };
+ pHaltBuy, pResumeBuy, pHaltAll, pResumeAll, pReduce, pIncrease, pLiquidate, pSectorExit,
+ pctOf, tgtPos, posMoveValid, posMovePreview, doPosMove, heldSectors, secSel, doSectorExit,
+ pmap, pval, dcaOn, sumPosition, sumDca, sumAutonomy };
}
}).use(ElementPlus).mount('#app');