2026-07-27 17:12:09 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN" class="dark">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
|
|
|
<title>PMS 持仓管理系统</title>
|
|
|
|
|
|
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css"/>
|
|
|
|
|
|
<link rel="stylesheet" href="https://unpkg.com/element-plus/theme-chalk/dark/css-vars.css"/>
|
|
|
|
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
|
|
|
|
<script src="https://unpkg.com/element-plus"></script>
|
|
|
|
|
|
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
body { margin:0; background:#0f0f0f; color:#ccc; font-family:'Inter',system-ui,sans-serif; }
|
|
|
|
|
|
.app { padding:12px 18px 32px; }
|
|
|
|
|
|
.hd { display:flex; align-items:center; gap:14px; flex-wrap:wrap;
|
|
|
|
|
|
padding:10px 14px; background:#1a1a1a; border:1px solid #333; border-radius:8px; }
|
|
|
|
|
|
.hd h1 { font-size:17px; margin:0; color:#eee; font-weight:600; letter-spacing:.5px; }
|
|
|
|
|
|
.hd .ver { color:#666; font-size:12px; }
|
|
|
|
|
|
.spacer { flex:1; }
|
|
|
|
|
|
.metrics { display:flex; gap:10px; flex-wrap:wrap; margin:12px 0; }
|
|
|
|
|
|
.m { background:#1a1a1a; border:1px solid #333; border-radius:8px; padding:10px 14px; min-width:118px; }
|
|
|
|
|
|
.m .k { color:#888; font-size:12px; }
|
|
|
|
|
|
.m .v { color:#eee; font-size:18px; font-weight:600; margin-top:3px; }
|
|
|
|
|
|
.m .v.up { color:#f56c6c; } .m .v.down { color:#67c23a; }
|
|
|
|
|
|
.panel { background:#1a1a1a; border:1px solid #333; border-radius:8px; padding:14px; margin-bottom:14px; }
|
|
|
|
|
|
.panel h3 { margin:0 0 10px; font-size:14px; color:#409EFF; font-weight:600; }
|
|
|
|
|
|
.muted { color:#777; font-size:12px; }
|
|
|
|
|
|
.row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
|
|
|
|
|
|
.banner { margin:10px 0; }
|
|
|
|
|
|
.el-table { background:transparent !important; font-size:13px; }
|
|
|
|
|
|
.tier { font-size:12px; color:#909399; }
|
|
|
|
|
|
.mono { font-family:ui-monospace,Menlo,Consolas,monospace; }
|
|
|
|
|
|
.nowrap { white-space:nowrap; }
|
|
|
|
|
|
pre.json { background:#111; border:1px solid #333; border-radius:6px; padding:10px;
|
|
|
|
|
|
max-height:420px; overflow:auto; font-size:12px; color:#bbb; }
|
2026-08-10 15:09:29 +08:00
|
|
|
|
.tcard{background:#161616;border:1px solid #333;border-radius:8px;padding:10px 12px;margin-bottom:10px;}
|
|
|
|
|
|
.clr-up{color:#f56c6c;} .clr-down{color:#67c23a;}
|
|
|
|
|
|
.attn{color:#e6a23c;padding:3px 0;}
|
|
|
|
|
|
.tblk h3{margin:0 0 10px;font-size:14px;color:#409EFF;font-weight:600;}
|
2026-07-27 17:12:09 +08:00
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div id="app" class="app" v-cloak>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="hd">
|
|
|
|
|
|
<h1>PMS 持仓管理系统</h1>
|
|
|
|
|
|
<span class="ver">v{{ health.version || '-' }} · {{ health.now || '' }}
|
|
|
|
|
|
<el-tag v-if="health.trade_day" size="small" type="success" effect="dark">交易日</el-tag>
|
|
|
|
|
|
<el-tag v-else size="small" type="info" effect="dark">非交易日</el-tag>
|
|
|
|
|
|
</span>
|
2026-08-10 15:09:29 +08:00
|
|
|
|
<el-button-group style="margin-left:8px">
|
|
|
|
|
|
<el-button size="small" :type="mode==='trader'?'primary':''" @click="mode='trader'">交易员</el-button>
|
|
|
|
|
|
<el-button size="small" :type="mode==='ops'?'primary':''" @click="mode='ops'">运维</el-button>
|
|
|
|
|
|
</el-button-group>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<div class="spacer"></div>
|
|
|
|
|
|
<el-tag :type="ov.buy_halt ? 'danger':'info'" effect="dark" size="small">
|
|
|
|
|
|
买入 {{ ov.buy_halt ? '已暂停':'正常' }}</el-tag>
|
|
|
|
|
|
<el-tag :type="ov.exec_halt ? 'danger':'info'" effect="dark" size="small">
|
|
|
|
|
|
执行 {{ ov.exec_halt ? '已暂停':'正常' }}</el-tag>
|
2026-08-10 15:09:29 +08:00
|
|
|
|
<el-tag type="warning" effect="dark" size="small">自主档位 {{ tx('autonomy', ov.autonomy) }}</el-tag>
|
|
|
|
|
|
<el-tag :type="dm.shadow ? 'info':'success'" effect="dark" size="small" v-show="mode==='ops'">
|
2026-07-28 09:10:07 +08:00
|
|
|
|
下发 {{ dm.mode || '-' }}</el-tag>
|
2026-07-30 16:21:16 +08:00
|
|
|
|
<el-tag :type="planStatus.ok===false ? 'danger':'success'" effect="dark" size="small">
|
|
|
|
|
|
计划 {{ planStatus.ok===false ? '不可用' : (planStatus.date || '-') }}</el-tag>
|
|
|
|
|
|
<el-button size="small" @click="openPlan">上游计划</el-button>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-button size="small" @click="openReport">日报</el-button>
|
2026-08-10 15:09:29 +08:00
|
|
|
|
<el-button size="small" v-show="mode==='ops'" @click="opsDrawer=true">运维</el-button>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-button size="small" type="primary" :loading="loading" @click="loadAll">刷新</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-alert v-if="err" class="banner" :title="err" type="error" effect="dark" show-icon
|
2026-07-29 10:01:00 +08:00
|
|
|
|
@close="err=''"></el-alert>
|
2026-07-30 16:21:16 +08:00
|
|
|
|
<el-alert v-if="planStatus.ok===false" class="banner" type="error" effect="dark" show-icon
|
|
|
|
|
|
:closable="false"
|
|
|
|
|
|
:title="'上游选股计划不可用 —— 升仓/建仓类命令将无票可选: ' + (planStatus.hint||'')">
|
|
|
|
|
|
</el-alert>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-alert v-if="ov.sector_ready===false" class="banner" type="warning" effect="dark" show-icon
|
|
|
|
|
|
:closable="false"
|
2026-07-29 10:01:00 +08:00
|
|
|
|
title="行业划分数据源未配置 —— 行业集中度硬拦截已停用, 行业类命令置灰 (设计 §5)"></el-alert>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-alert v-if="ov.price_missing && ov.price_missing.length" class="banner" type="warning"
|
|
|
|
|
|
effect="dark" show-icon :closable="false"
|
2026-07-29 10:01:00 +08:00
|
|
|
|
:title="'以下持仓取不到实时价, 已用摊薄成本兜底: ' + ov.price_missing.join(', ')"></el-alert>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-alert v-if="health.calendar_degraded" class="banner" type="warning" effect="dark" show-icon
|
2026-07-29 10:01:00 +08:00
|
|
|
|
:closable="false" title="交易日历降级: 未安装 chinesecalendar, 节假日不可辨"></el-alert>
|
2026-07-28 09:10:07 +08:00
|
|
|
|
<el-alert v-if="dm.shadow" class="banner" type="info" effect="dark" show-icon :closable="false"
|
2026-07-29 10:01:00 +08:00
|
|
|
|
:title="'影子运行中 —— ' + (dm.hint || '')"></el-alert>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
|
2026-07-30 13:02:51 +08:00
|
|
|
|
<el-alert v-if="scaleGap" class="banner" type="warning" effect="dark" show-icon :closable="false"
|
|
|
|
|
|
:title="'总规模与账户不符 —— 参数 ' + money(ov.scale) + ' / 账户总资产 ' + money(ov.total_asset) + ' (' + pct(scaleGap) + ')。仓位上限按参数算、资金校验按账户算, 差得远时方案会过闸却下不出去'"></el-alert>
|
|
|
|
|
|
|
2026-08-10 15:09:29 +08:00
|
|
|
|
<!-- ==================== 交易员视图 ==================== -->
|
|
|
|
|
|
<div v-show="mode==='trader'">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 1) 今日一览 + 需要注意 -->
|
|
|
|
|
|
<div class="metrics">
|
|
|
|
|
|
<div class="m"><div class="k">账户总资产</div><div class="v">{{ money(ov.total_asset) }}</div></div>
|
|
|
|
|
|
<div class="m"><div class="k">可用资金<span class="muted" v-if="ov.cash_source!=='ws'"> (估算)</span></div>
|
|
|
|
|
|
<div class="v" :class="ov.cash_source==='ws'?'':'muted'">{{ money(ov.cash_source==='ws' ? ov.cash_avail : ov.cash_est) }}</div></div>
|
|
|
|
|
|
<div class="m"><div class="k">持仓市值</div><div class="v">{{ money(ov.portfolio_mv) }}</div></div>
|
|
|
|
|
|
<div class="m"><div class="k">仓位</div><div class="v">{{ pct(ov.portfolio_pct) }}<span class="muted"> / {{ pct(ov.portfolio_cap) }}</span></div></div>
|
|
|
|
|
|
<div class="m"><div class="k">浮动盈亏</div>
|
|
|
|
|
|
<div class="v" :class="(ov.float_pnl||0)>=0?'up':'down'">{{ money(ov.float_pnl) }}<span style="font-size:13px"> {{ ov.float_pnl_pct==null?'':pct(ov.float_pnl_pct) }}</span></div></div>
|
|
|
|
|
|
<div class="m"><div class="k">持仓数</div><div class="v">{{ ov.names_count }}<span class="muted"> / {{ ov.max_names }}</span></div></div>
|
|
|
|
|
|
<div class="m" style="cursor:pointer" @click="scrollTo('t-prop')"><div class="k">等我拍板</div>
|
|
|
|
|
|
<div class="v" :class="proposals.length?'up':''">{{ proposals.length }} 条</div></div>
|
|
|
|
|
|
<div class="m"><div class="k">自主档位</div><div class="v" style="font-size:15px">{{ tx('autonomy', ov.autonomy) }}</div></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="panel" v-if="attention.length" style="border-color:#7a5b1e">
|
|
|
|
|
|
<h3 style="color:#e6a23c">需要注意</h3>
|
|
|
|
|
|
<div v-for="(a,i) in attention" :key="i" class="attn">· {{ a }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 2) 等我拍板 -->
|
|
|
|
|
|
<div class="panel tblk" id="t-prop">
|
|
|
|
|
|
<h3>等我拍板 · {{ proposals.length }} 条</h3>
|
|
|
|
|
|
<div class="muted" v-if="!proposals.length">暂无需要你确认的提议。</div>
|
|
|
|
|
|
<div v-for="p in proposals" :key="p.proposal_id" class="tcard">
|
|
|
|
|
|
<div class="row" style="justify-content:space-between">
|
|
|
|
|
|
<div><b style="font-size:15px">{{ nm(p.ts_code) }}</b> <span class="muted mono">{{ p.ts_code }}</span>
|
|
|
|
|
|
<el-tag size="small" effect="dark" type="primary" style="margin-left:8px">{{ tx('action', p.action) }}</el-tag></div>
|
|
|
|
|
|
<div class="muted">{{ p.expire_at }} 前有效</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="margin:6px 0" v-if="p.judge_reason"><span class="muted">研判:</span>{{ p.judge_reason }}</div>
|
|
|
|
|
|
<div class="muted" style="margin:4px 0">{{ propWhy(p) }}</div>
|
|
|
|
|
|
<div class="row" style="margin-top:6px">
|
|
|
|
|
|
<span class="muted">约 {{ p.qty }} 股 · 参考价 {{ (p.hard_numbers||{}).price==null ? '—' : (p.hard_numbers||{}).price }}
|
|
|
|
|
|
· 约需 {{ money(((p.hard_numbers||{}).price||0)*(p.qty||0)) }}</span>
|
|
|
|
|
|
<div class="spacer"></div>
|
|
|
|
|
|
<el-button type="success" size="small" @click="decide(p,'ACCEPTED')">采纳</el-button>
|
|
|
|
|
|
<el-button type="danger" size="small" @click="decide(p,'DECLINED')">驳回</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="muted" style="font-size:11px;margin-top:4px">采纳=记一笔待执行指令,由择时在窗口内择机下单;驳回=今日不再提这只。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 3) 我的持仓 -->
|
|
|
|
|
|
<div class="panel tblk" id="t-pos">
|
|
|
|
|
|
<h3>我的持仓 · {{ heldPositions.length }} 只</h3>
|
|
|
|
|
|
<el-table :data="heldPositions" size="small" border max-height="460">
|
|
|
|
|
|
<el-table-column type="expand">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<div style="padding:6px 16px">
|
|
|
|
|
|
<el-button size="small" @click="loadLots(s.row.ts_code)">查看批次</el-button>
|
|
|
|
|
|
<el-table v-if="lotsOf===s.row.ts_code" :data="lots" size="small" border style="margin-top:6px">
|
|
|
|
|
|
<el-table-column label="批次" width="100"><template #default="l">{{ tx('lot', l.row.lot_type) }}</template></el-table-column>
|
|
|
|
|
|
<el-table-column prop="qty" label="剩余" width="80"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="open_price" label="开仓价" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="open_date" label="开仓日" width="110"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="realized_pnl" label="已实现" width="90"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<div class="muted" style="margin-top:8px">该股近期账本:</div>
|
|
|
|
|
|
<div class="muted" v-if="!ledger.filter(x=>x.ts_code===s.row.ts_code).length">无</div>
|
|
|
|
|
|
<div v-for="(l,i) in ledger.filter(x=>x.ts_code===s.row.ts_code)" :key="i" style="padding:2px 0">
|
|
|
|
|
|
<span class="muted">{{ (l.decided_at||'').slice(5,16) }}</span>
|
|
|
|
|
|
{{ tx('arbiter', l.arbiter) }}{{ tx('verdict', l.verdict) }} {{ tx('action', l.action) }}<span v-if="l.reason" class="muted"> — {{ l.reason }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="股票" width="150">
|
|
|
|
|
|
<template #default="s"><b>{{ nm(s.row.ts_code) }}</b><br><span class="muted mono">{{ s.row.ts_code }}</span></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="现价 / 成本" width="120">
|
|
|
|
|
|
<template #default="s">{{ s.row.price }}<span v-if="!s.row.price_ok" class="muted"> 估</span><br><span class="muted">{{ s.row.avg_cost||'—' }}</span></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="浮动盈亏" width="110">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<span v-if="s.row.price_ok && s.row.avg_cost" :class="(s.row.price/s.row.avg_cost-1)>=0?'clr-up':'clr-down'">{{ pct(s.row.price/s.row.avg_cost-1) }}</span>
|
|
|
|
|
|
<span v-else class="muted">—</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="市值 / 占比" width="130">
|
|
|
|
|
|
<template #default="s">{{ money(s.row.market_value) }}<br><span class="muted">{{ pct(s.row.pct_of_scale) }}</span></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="安全垫" width="120">
|
|
|
|
|
|
<template #default="s"><el-tag size="small" effect="dark" :type="cuTag(s.row.cushion_state)">{{ tx('cushion', s.row.cushion_state) }}</el-tag>
|
|
|
|
|
|
<div class="muted">{{ pct(s.row.cushion_pct) }}<span v-if="s.row.neg_cushion_days>0"> · 连负{{ s.row.neg_cushion_days }}日</span></div></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="状态" width="120">
|
|
|
|
|
|
<template #default="s">{{ tx('posStatus', s.row.status) }}
|
|
|
|
|
|
<div v-if="s.row.frozen_reason && s.row.frozen_reason!=='NONE'" style="color:#f56c6c">{{ tx('frozen', s.row.frozen_reason) }}</div></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="下一步" min-width="150"><template #default="s"><span class="muted">{{ nextStep(s.row) }}</span></template></el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="220">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-button size="small" @click="actReduceStock(s.row)">减仓</el-button>
|
|
|
|
|
|
<el-button size="small" type="danger" @click="actExitStock(s.row)">清仓</el-button>
|
|
|
|
|
|
<el-dropdown style="margin-left:6px" @command="c=>rowMore(c,s.row)">
|
|
|
|
|
|
<el-button size="small">更多 ▾</el-button>
|
|
|
|
|
|
<template #dropdown>
|
|
|
|
|
|
<el-dropdown-menu>
|
|
|
|
|
|
<el-dropdown-item command="stop">设止损价</el-dropdown-item>
|
|
|
|
|
|
<el-dropdown-item command="target">设目标价</el-dropdown-item>
|
|
|
|
|
|
<el-dropdown-item command="freeze">{{ (s.row.frozen_reason && s.row.frozen_reason!=='NONE') ? '解冻' : '冻结(禁增持)' }}</el-dropdown-item>
|
|
|
|
|
|
<el-dropdown-item command="black">加入黑名单</el-dropdown-item>
|
|
|
|
|
|
</el-dropdown-menu>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-dropdown>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 4) 今日在办与动向 -->
|
|
|
|
|
|
<div class="panel tblk">
|
|
|
|
|
|
<h3>今日在办</h3>
|
|
|
|
|
|
<el-table :data="instructions" size="small" border max-height="240" empty-text="今天没有在办的买卖">
|
|
|
|
|
|
<el-table-column label="股票" width="150"><template #default="s"><b>{{ nm(s.row.ts_code) }}</b> <span class="muted mono">{{ s.row.ts_code }}</span></template></el-table-column>
|
|
|
|
|
|
<el-table-column label="动作" width="150"><template #default="s">{{ tx('action', s.row.action) }} · {{ tx('side', s.row.side) }}</template></el-table-column>
|
|
|
|
|
|
<el-table-column label="数量 / 已成" width="110"><template #default="s">{{ s.row.qty }} / {{ s.row.exec_qty||0 }}</template></el-table-column>
|
|
|
|
|
|
<el-table-column prop="limit_price" label="限价" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column label="状态" width="130"><template #default="s">{{ tx('insStatus', s.row.status) }}</template></el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="90"><template #default="s"><el-button size="small" @click="cancelIns(s.row.instruction_id)">撤单</el-button></template></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<h3 style="margin-top:14px">今天发生了什么</h3>
|
|
|
|
|
|
<div class="muted" v-if="!ledger.length">今天还没有记录。</div>
|
|
|
|
|
|
<div v-for="(l,i) in ledger" :key="i" style="padding:3px 0;border-bottom:1px solid #262626">
|
|
|
|
|
|
<span class="muted">{{ (l.decided_at||'').slice(11,16) }}</span>
|
|
|
|
|
|
<b> {{ nm(l.ts_code) }}</b>:{{ tx('arbiter', l.arbiter) }}{{ tx('verdict', l.verdict) }} {{ tx('action', l.action) }}<span v-if="l.reason" class="muted"> —— {{ l.reason }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 5) 组合操作 + 设置 -->
|
|
|
|
|
|
<div class="panel tblk">
|
|
|
|
|
|
<h3>组合操作</h3>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<el-button size="small" :type="ov.buy_halt?'success':'warning'" @click="ov.buy_halt?pResumeBuy():pHaltBuy()">{{ ov.buy_halt?'恢复买入':'暂停买入' }}</el-button>
|
|
|
|
|
|
<el-button size="small" :type="ov.exec_halt?'success':''" @click="ov.exec_halt?pResumeAll():pHaltAll()">{{ ov.exec_halt?'结束休假':'休假模式' }}</el-button>
|
|
|
|
|
|
<el-button size="small" @click="pReduce()">降仓…</el-button>
|
|
|
|
|
|
<el-button size="small" @click="pIncrease()">升仓…</el-button>
|
|
|
|
|
|
<el-button size="small" :disabled="!ov.sector_ready" @click="pSectorExit()">清仓某行业…</el-button>
|
|
|
|
|
|
<el-button size="small" type="danger" @click="pLiquidate()">一键清仓(紧急)</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="muted" style="margin-top:6px">暂停买入不影响卖出与止损;一键清仓会把全部持仓按最快节奏卖出、不做择时优化。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="panel tblk">
|
|
|
|
|
|
<h3>设置</h3>
|
|
|
|
|
|
<el-table :data="traderParams" size="small" border>
|
|
|
|
|
|
<el-table-column label="项" min-width="240"><template #default="s">{{ s.row.desc || s.row.key }}</template></el-table-column>
|
|
|
|
|
|
<el-table-column label="当前值" width="240">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-select v-if="s.row.key==='PMS_AUTONOMY'" v-model="s.row.value" size="small">
|
|
|
|
|
|
<el-option label="全自动" value="full"></el-option>
|
|
|
|
|
|
<el-option label="只提议不下手" value="propose_only"></el-option>
|
|
|
|
|
|
<el-option label="暂停" value="off"></el-option>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-input v-else v-model="s.row.value" size="small"></el-input>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<div class="row" style="margin-top:8px">
|
|
|
|
|
|
<el-button size="small" type="primary" :disabled="!dirtyCount" @click="saveParams">保存修改 ({{ dirtyCount }})</el-button>
|
|
|
|
|
|
<el-button size="small" @click="loadParams">放弃</el-button>
|
|
|
|
|
|
<span class="muted">改「自主档位」或「总仓上限」会即时生效;调低总仓上限可能触发降仓提议。</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 6) 系统在盯的候选 -->
|
|
|
|
|
|
<div class="panel tblk">
|
|
|
|
|
|
<h3>系统在盯的候选(今日选股计划)<span class="muted" style="font-weight:400"> · 计划日 {{ planStatus.date || '—' }}</span></h3>
|
|
|
|
|
|
<div class="muted" v-if="planStatus.ok===false">上游计划暂不可用。</div>
|
|
|
|
|
|
<el-table v-else :data="planRows.slice(0,20)" size="small" border max-height="300" empty-text="暂无">
|
|
|
|
|
|
<el-table-column prop="rank" label="名次" width="60"></el-table-column>
|
|
|
|
|
|
<el-table-column label="股票" width="160"><template #default="s"><b>{{ s.row.name || nm(s.row.ts_code) }}</b> <span class="muted mono">{{ s.row.ts_code }}</span></template></el-table-column>
|
|
|
|
|
|
<el-table-column prop="theme" label="主题" width="130"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="tier" label="传导档" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column label="预期空间" width="100"><template #default="s">{{ s.row.upside==null ? '—' : ((s.row.upside>0?'+':'')+(s.row.upside*100).toFixed(0)+'%') }}</template></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<div class="muted" style="margin-top:6px">这是系统当前在盯、可能冒出提议的池子;仅供参考,不自动下单。</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-show="mode==='ops'">
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<div class="metrics">
|
|
|
|
|
|
<div class="m"><div class="k">总规模</div><div class="v">{{ money(ov.scale) }}</div></div>
|
|
|
|
|
|
<div class="m"><div class="k">组合市值</div><div class="v">{{ money(ov.portfolio_mv) }}</div></div>
|
|
|
|
|
|
<div class="m"><div class="k">总仓位</div>
|
|
|
|
|
|
<div class="v">{{ pct(ov.portfolio_pct) }}<span class="muted"> / {{ pct(ov.portfolio_cap) }}</span></div></div>
|
|
|
|
|
|
<div class="m"><div class="k">可用额度</div><div class="v">{{ money(ov.cap_room) }}</div></div>
|
2026-07-30 13:02:51 +08:00
|
|
|
|
<div class="m"><div class="k">账户可用<span class="muted" v-if="ov.cash_source!=='ws'"> (估算)</span></div>
|
|
|
|
|
|
<div class="v" :class="ov.cash_source==='ws'?'':'muted'">{{ money(ov.cash_source==='ws' ? ov.cash_avail : ov.cash_est) }}</div></div>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<div class="m"><div class="k">持仓数</div>
|
|
|
|
|
|
<div class="v">{{ ov.names_count }}<span class="muted"> / {{ ov.max_names }}</span></div></div>
|
|
|
|
|
|
<div class="m"><div class="k">浮动盈亏</div>
|
|
|
|
|
|
<div class="v" :class="(ov.float_pnl||0)>=0?'up':'down'">{{ money(ov.float_pnl) }}</div></div>
|
|
|
|
|
|
<div class="m"><div class="k">厚垫 / 负垫</div>
|
|
|
|
|
|
<div class="v">{{ ov.solid_names }} / {{ ov.neg_names }}</div></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<el-tabs v-model="tab" type="border-card">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ==================== ① 参数设置 ==================== -->
|
|
|
|
|
|
<el-tab-pane label="参数设置" name="params">
|
|
|
|
|
|
<div class="row" style="margin-bottom:10px">
|
|
|
|
|
|
<span class="muted">页面改动持久化到 pms_runtime_param, 优先于 settings.py 初值; 基础设施连接串只在 .env 维护。</span>
|
|
|
|
|
|
<div class="spacer"></div>
|
|
|
|
|
|
<el-button size="small" type="primary" :disabled="!dirtyCount" @click="saveParams">
|
|
|
|
|
|
保存修改 ({{ dirtyCount }})</el-button>
|
|
|
|
|
|
<el-button size="small" @click="loadParams">放弃修改</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-table :data="params" size="small" height="560" border>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="key" label="参数" width="240" class-name="mono"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column label="当前值" width="200">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-select v-if="s.row.key==='PMS_AUTONOMY'" v-model="s.row.value" size="small">
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-option v-for="o in ['full','propose_only','off']" :key="o" :label="o" :value="o"></el-option>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-select v-else-if="s.row.key==='PMS_SECTOR_SOURCE'" v-model="s.row.value" size="small">
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-option label="(空) 停用行业约束" value=""></el-option>
|
|
|
|
|
|
<el-option label="custom_table" value="custom_table"></el-option>
|
|
|
|
|
|
<el-option label="gp_stock_category" value="gp_stock_category"></el-option>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
</el-select>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-switch v-else-if="s.row.type==='bool'" v-model="s.row.value"></el-switch>
|
|
|
|
|
|
<el-input v-else v-model="s.row.value" size="small"></el-input>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="file_default" label="文件初值" width="130"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column label="来源" width="80">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-tag size="small" :type="s.row.source==='table'?'success':'info'" effect="dark">
|
|
|
|
|
|
{{ s.row.source==='table' ? '页面' : '文件' }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="desc" label="说明" min-width="260"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="updated_at" label="更新时间" width="160" class-name="muted"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ==================== ② 命令台 ==================== -->
|
|
|
|
|
|
<el-tab-pane label="命令台" name="cmd">
|
|
|
|
|
|
<div class="panel">
|
|
|
|
|
|
<h3>下达命令</h3>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<el-select v-model="form.cmd_type" filterable placeholder="选择命令" style="width:300px"
|
|
|
|
|
|
@change="onCmdChange">
|
|
|
|
|
|
<el-option-group v-for="g in ['A','B','C']" :key="g" :label="groupLabel(g)">
|
|
|
|
|
|
<el-option v-for="c in catalog.filter(x=>x.group===g)" :key="c.cmd_type"
|
|
|
|
|
|
:label="c.label + ' · ' + c.cmd_type" :value="c.cmd_type"
|
|
|
|
|
|
:disabled="c.disabled">
|
|
|
|
|
|
<span>{{ c.label }}</span>
|
|
|
|
|
|
<span class="muted" style="float:right">{{ c.cls==='param'?'参数':'任务' }}</span>
|
|
|
|
|
|
</el-option>
|
|
|
|
|
|
</el-option-group>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-tag v-if="curSpec" size="small" effect="dark"
|
|
|
|
|
|
:type="curSpec.cls==='param'?'warning':'primary'">
|
|
|
|
|
|
{{ curSpec.cls==='param' ? '参数命令 (立即生效)' : '任务命令 (生成方案)' }}</el-tag>
|
|
|
|
|
|
<el-tag v-if="curSpec && curSpec.danger" size="small" type="danger" effect="dark">高危</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="muted" v-if="curSpec && curSpec.note" style="margin:8px 0">{{ curSpec.note }}</div>
|
|
|
|
|
|
<div class="row" style="margin-top:10px" v-if="curSpec">
|
|
|
|
|
|
<div v-for="(f,name) in curSpec.fields" :key="name" class="row">
|
|
|
|
|
|
<span class="muted">{{ fieldLabel(name) }}</span>
|
|
|
|
|
|
<el-select v-if="f.type==='enum'" v-model="form.params[name]" size="small" style="width:150px">
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-option v-for="o in f.choices" :key="o" :label="o" :value="o"></el-option>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-input v-else v-model="form.params[name]" size="small" style="width:150px"
|
|
|
|
|
|
:placeholder="f.required===false ? '可留空(默认)' : ''">
|
|
|
|
|
|
<template v-if="f.type==='pct'" #append>%</template>
|
|
|
|
|
|
<template v-else-if="f.type==='money'" #append>元</template>
|
|
|
|
|
|
<template v-else-if="f.type==='price'" #append>元</template>
|
|
|
|
|
|
</el-input>
|
|
|
|
|
|
</div>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-input v-model="form.note" size="small" style="width:200px" placeholder="备注(可选)"></el-input>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-button type="primary" size="small" :loading="issuing" @click="issue(false)">下达</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="muted" style="margin-top:8px" v-if="curSpec && curSpec.cls==='task'">
|
|
|
|
|
|
任务命令下达后立即生成方案并落 pms_plan; 指令下发由择时执行器负责 (下一批交付)。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="panel">
|
|
|
|
|
|
<h3>在途命令与进度</h3>
|
|
|
|
|
|
<el-table :data="commands" size="small" border max-height="420"
|
|
|
|
|
|
:row-class-name="()=>''">
|
|
|
|
|
|
<el-table-column type="expand">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<div style="padding:8px 16px">
|
|
|
|
|
|
<div class="muted">参数: <span class="mono">{{ JSON.stringify(s.row.params) }}</span></div>
|
|
|
|
|
|
<div class="muted" v-if="s.row.progress && s.row.progress.notes">
|
|
|
|
|
|
说明: {{ (s.row.progress.notes||[]).join(' | ') }}</div>
|
|
|
|
|
|
<el-button size="small" style="margin:8px 0" @click="loadPlans(s.row.command_id)">
|
|
|
|
|
|
查看方案明细</el-button>
|
|
|
|
|
|
<el-table v-if="plansOf===s.row.command_id" :data="plans" size="small" border>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="action" label="动作" width="80"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="qty" label="数量" width="90"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column label="金额" width="110">
|
|
|
|
|
|
<template #default="p">{{ money(p.row.amount) }}</template></el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="priority" label="优先级" width="80"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="status" label="状态" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="deadline" label="截止" width="110"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="reason" label="理由" min-width="260"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="command_id" label="命令号" width="160" class-name="mono"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="cmd_type" label="类型" width="160"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="ts_code" label="标的" width="110" class-name="mono"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column label="状态" width="100">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-tag size="small" effect="dark" :type="stTag(s.row.status)">{{ s.row.status }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="进度" min-width="220">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<div v-if="s.row.progress && s.row.progress.target_amount">
|
|
|
|
|
|
<el-progress :percentage="progPct(s.row.progress)" :stroke-width="10"
|
2026-07-29 10:01:00 +08:00
|
|
|
|
:text-inside="true" status="success"></el-progress>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<span class="muted">{{ money(s.row.progress.done_amount) }} /
|
|
|
|
|
|
{{ money(s.row.progress.target_amount) }}
|
|
|
|
|
|
<template v-if="s.row.progress.gap"> · 缺口 {{ money(s.row.progress.gap) }}</template>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span v-else class="muted">{{ (s.row.progress||{}).plan_count || 0 }} 条方案</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="issued_at" label="下达时间" width="160" class-name="muted"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column label="操作" width="150">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-button size="small" :disabled="!canCancel(s.row)"
|
|
|
|
|
|
@click="cancelCmd(s.row.command_id)">撤销</el-button>
|
|
|
|
|
|
<el-button size="small" :disabled="!canCancel(s.row) || s.row.cmd_class!=='task'"
|
|
|
|
|
|
@click="replan(s.row.command_id)">重规划</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ==================== ③ 持仓与账本 ==================== -->
|
|
|
|
|
|
<el-tab-pane label="持仓与账本" name="pos">
|
|
|
|
|
|
<div class="panel">
|
|
|
|
|
|
<h3>持仓总览 ({{ positions.length }} 只)</h3>
|
|
|
|
|
|
<el-table :data="positions" size="small" border max-height="460">
|
|
|
|
|
|
<el-table-column type="expand">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<div style="padding:8px 16px">
|
|
|
|
|
|
<el-button size="small" @click="loadLots(s.row.ts_code)">查看批次</el-button>
|
|
|
|
|
|
<el-table v-if="lotsOf===s.row.ts_code" :data="lots" size="small" border
|
|
|
|
|
|
style="margin-top:8px">
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="lot_type" label="批次" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="qty" label="剩余" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="closed_qty" label="已核销" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="open_price" label="开仓价" width="100"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="close_avg_price" label="核销均价" width="100"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="realized_pnl" label="已实现" width="100"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="open_date" label="开仓日" width="110"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column prop="instruction_id" label="来源指令" min-width="180"
|
2026-07-29 10:01:00 +08:00
|
|
|
|
class-name="mono"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="note" label="备注" min-width="160"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="sector" label="行业" width="100"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="status" label="状态" width="90"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column label="冻结" width="110">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-tag v-if="s.row.frozen_reason && s.row.frozen_reason!=='NONE'" size="small"
|
|
|
|
|
|
type="danger" effect="dark">{{ s.row.frozen_reason }}</el-tag>
|
|
|
|
|
|
<span v-else class="muted">—</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="total_qty" label="持仓" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="avail_qty" label="可卖" width="90"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column label="批次(底/补/加/补仓/T)" width="170">
|
|
|
|
|
|
<template #default="s"><span class="mono nowrap">{{ s.row.base_qty }}/{{ s.row.fill_qty }}/{{ s.row.add_qty }}/{{ s.row.dca_qty }}/{{ s.row.t0_qty }}</span></template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="avg_cost" label="摊薄成本" width="100"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="price" label="现价" width="90"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column label="安全垫" width="130">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-tag size="small" effect="dark" :type="cuTag(s.row.cushion_state)">
|
|
|
|
|
|
{{ pct(s.row.cushion_pct) }} {{ s.row.cushion_state }}</el-tag>
|
|
|
|
|
|
<span v-if="s.row.neg_cushion_days>0" class="muted"> 连负{{ s.row.neg_cushion_days }}日</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="占规模" width="90">
|
|
|
|
|
|
<template #default="s">{{ pct(s.row.pct_of_scale) }}</template></el-table-column>
|
|
|
|
|
|
<el-table-column label="市值" width="110">
|
|
|
|
|
|
<template #default="s">{{ money(s.row.market_value) }}</template></el-table-column>
|
|
|
|
|
|
<el-table-column label="参考位(支/压/止损)" width="180">
|
|
|
|
|
|
<template #default="s"><span class="mono nowrap">{{ s.row.support_ref||'-' }}/{{ s.row.pressure_ref||'-' }}/{{ s.row.stop_ref||'-' }}</span>
|
|
|
|
|
|
<span class="muted"> {{ s.row.ref_source||'' }}</span></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="做T" width="80">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-tag v-if="s.row.t0_enabled" size="small" type="warning" effect="dark">
|
|
|
|
|
|
{{ pct(s.row.t0_ratio) }}</el-tag><span v-else class="muted">—</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="panel">
|
|
|
|
|
|
<h3>在途指令</h3>
|
2026-07-28 09:10:07 +08:00
|
|
|
|
<el-table :data="instructions" size="small" border max-height="320">
|
|
|
|
|
|
<el-table-column type="expand">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<div style="padding:6px 16px">
|
|
|
|
|
|
<div class="muted" v-if="(s.row.progress||{}).last_decision">
|
|
|
|
|
|
最近判定 [{{ s.row.progress.last_decision.at }}]
|
|
|
|
|
|
<b>{{ s.row.progress.last_decision.action }}</b> —
|
|
|
|
|
|
{{ s.row.progress.last_decision.reason }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="muted" v-if="(s.row.progress||{}).deadline">
|
|
|
|
|
|
执行窗口截止: {{ s.row.progress.deadline }}
|
|
|
|
|
|
<template v-if="(s.row.progress||{}).window_verdict">
|
|
|
|
|
|
· {{ s.row.progress.window_verdict.note }}</template>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-table v-if="((s.row.progress||{}).children||[]).length"
|
|
|
|
|
|
:data="s.row.progress.children" size="small" border
|
|
|
|
|
|
style="margin-top:6px">
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="ymd" label="日期" width="100"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="at" label="时点" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="qty" label="数量" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="limit" label="限价" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="mode" label="通道" width="100"></el-table-column>
|
2026-07-28 09:10:07 +08:00
|
|
|
|
<el-table-column label="兜底" width="70">
|
|
|
|
|
|
<template #default="c">{{ c.row.forced ? '是' : '—' }}</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="reason" label="出手依据" min-width="260"></el-table-column>
|
2026-07-28 09:10:07 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
<div v-else class="muted" style="margin-top:6px">尚无出手记录</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="instruction_id" label="指令号" width="230" class-name="mono"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="action" label="动作" width="80"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="side" label="方向" width="70"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="qty" label="数量" width="80"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="exec_qty" label="已成交" width="80"></el-table-column>
|
2026-07-28 09:10:07 +08:00
|
|
|
|
<el-table-column label="进度" width="130">
|
|
|
|
|
|
<template #default="s">
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-progress :percentage="insPct(s.row)" :stroke-width="10" :text-inside="true"></el-progress>
|
2026-07-28 09:10:07 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="status" label="状态" width="115"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="origin_type" label="来源" width="80"></el-table-column>
|
2026-07-28 09:10:07 +08:00
|
|
|
|
<el-table-column label="操作" width="90">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-button size="small" @click="cancelIns(s.row.instruction_id)">撤销</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
<div class="muted" style="margin-top:6px">
|
2026-07-28 09:10:07 +08:00
|
|
|
|
{{ dm.hint }} 展开可看每日子单与择时依据; 成交量以下游回放为准, 不拿下发量当成交量。
|
2026-07-27 17:12:09 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="panel">
|
|
|
|
|
|
<h3>评审账本 (规则闸 / 研判闸 / 人工裁决全量留痕)</h3>
|
|
|
|
|
|
<el-table :data="ledger" size="small" border max-height="260">
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="decided_at" label="时间" width="160"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="action" label="动作" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="arbiter" label="评审方" width="90"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column label="结论" width="90">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-tag size="small" effect="dark" :type="s.row.verdict==='PASS'?'success':'danger'">
|
|
|
|
|
|
{{ s.row.verdict }}</el-tag></template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="price_at" label="评审价" width="90"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column label="未通过项" min-width="240">
|
|
|
|
|
|
<template #default="s"><span class="muted">{{ (s.row.failed_checks||[]).join(' | ') }}</span></template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="reason" label="理由" min-width="200"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ==================== ④ 提议确认 ==================== -->
|
|
|
|
|
|
<el-tab-pane name="prop">
|
|
|
|
|
|
<template #label>
|
|
|
|
|
|
提议确认
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-badge v-if="proposals.length" :value="proposals.length" class="ml"></el-badge>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
<div class="panel">
|
|
|
|
|
|
<h3>待确认队列 (自主档位 {{ ov.autonomy }})</h3>
|
|
|
|
|
|
<div class="muted" style="margin-bottom:8px">
|
|
|
|
|
|
propose_only 档位下, 增持类自主动作全部在此等待裁决; 减持方向不设确认门槛。
|
|
|
|
|
|
采纳后先落指令表 (先记账后动作), 由择时执行器分日出手。
|
|
|
|
|
|
</div>
|
2026-07-28 09:29:53 +08:00
|
|
|
|
<el-alert v-if="dm.judge && !dm.judge.available" type="warning" effect="dark" show-icon
|
|
|
|
|
|
:closable="false" style="margin-bottom:10px"
|
2026-07-29 10:01:00 +08:00
|
|
|
|
:title="'研判闸未接通 —— ' + dm.judge.reason + ' (补足/加仓/补仓一律降级为人工确认)'"></el-alert>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table :data="proposals" size="small" border max-height="420">
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="proposal_id" label="提议号" width="230" class-name="mono"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="action" label="动作" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="qty" label="数量" width="90"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column label="硬数字" min-width="320">
|
|
|
|
|
|
<template #default="s"><span class="mono muted">{{ JSON.stringify(s.row.hard_numbers) }}</span></template>
|
|
|
|
|
|
</el-table-column>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="judge_verdict" label="研判" width="100"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="expire_at" label="失效时间" width="160"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column label="裁决" width="170">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-button size="small" type="success" @click="decide(s.row,'ACCEPTED')">采纳</el-button>
|
|
|
|
|
|
<el-button size="small" type="danger" @click="decide(s.row,'DECLINED')">驳回</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
2026-07-29 13:51:10 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- ==================== ④ ws 通道 (只读监控) ====================
|
|
|
|
|
|
只读: 任何会产生新委托的动作都不放在页面上。联调发单仍走
|
|
|
|
|
|
scripts/ws_smoke.py place --yes —— 那条路径有显式确认、有参数校验、
|
|
|
|
|
|
有"这张单会真成交"的警告, 且不受 PMS_DISPATCH_MODE 影响。页面上加个
|
|
|
|
|
|
发单按钮等于在生产界面开了第二条绕过开关的下单入口, 不值当。 -->
|
|
|
|
|
|
<el-tab-pane label="ws 通道" name="ws">
|
|
|
|
|
|
<div class="row" style="margin-bottom:10px">
|
|
|
|
|
|
<el-tag :type="wsc.process_alive ? 'success':'danger'" effect="dark" size="small">
|
|
|
|
|
|
进程 {{ wsc.process_alive ? '在线':'不在线' }}</el-tag>
|
|
|
|
|
|
<el-tag :type="wsc.conn_state==='ONLINE' ? 'success':'warning'" effect="dark" size="small">
|
|
|
|
|
|
{{ wsc.conn_state || '-' }}</el-tag>
|
|
|
|
|
|
<span class="muted">心跳 {{ wsc.heartbeat_age_sec==null ? '从未' : wsc.heartbeat_age_sec+'s 前' }}</span>
|
|
|
|
|
|
<div class="spacer"></div>
|
|
|
|
|
|
<el-checkbox v-model="wsAuto" size="small">每 3 秒自动刷新</el-checkbox>
|
|
|
|
|
|
<el-button size="small" @click="loadWs">刷新</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 异常自检: 与 scripts/ws_smoke.py 用同一套判据, 页面和命令行的结论必须一致,
|
|
|
|
|
|
否则联调时两边打架比没有告警更糟 -->
|
|
|
|
|
|
<el-alert v-for="(w,i) in wsWarnings" :key="i" class="banner" type="warning" effect="dark"
|
|
|
|
|
|
show-icon :closable="false" :title="w"></el-alert>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="metrics">
|
|
|
|
|
|
<div class="m"><div class="k">seq 已落库</div><div class="v">{{ wsc.last_seq }}</div></div>
|
|
|
|
|
|
<div class="m"><div class="k">seq 已确认</div><div class="v">{{ wsc.acked_seq }}</div></div>
|
|
|
|
|
|
<div class="m"><div class="k">待入账上行</div><div class="v">{{ wsc.inbox_pending }}</div></div>
|
|
|
|
|
|
<div class="m"><div class="k">收 / 发</div>
|
|
|
|
|
|
<div class="v">{{ wst.rx||0 }} / {{ wst.tx||0 }}</div></div>
|
|
|
|
|
|
<div class="m"><div class="k">成交 / 拒绝</div>
|
|
|
|
|
|
<div class="v">{{ wst.trades||0 }} / {{ wst.rejects||0 }}</div></div>
|
|
|
|
|
|
<div class="m"><div class="k">重连</div><div class="v">{{ wst.reconnects||0 }}</div></div>
|
|
|
|
|
|
<div class="m"><div class="k">对端时钟偏差</div>
|
|
|
|
|
|
<div class="v" :class="Math.abs(wst.peer_skew_ms||0)>20000?'up':''">
|
|
|
|
|
|
{{ wst.peer_skew_ms==null ? '—' : (wst.peer_skew_ms>0?'+':'') + wst.peer_skew_ms + 'ms' }}</div></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="panel">
|
|
|
|
|
|
<h3>通道明细</h3>
|
|
|
|
|
|
<div class="row" style="gap:24px">
|
|
|
|
|
|
<span class="muted">下发模式 <span class="mono">{{ wsMode || '-' }}</span></span>
|
|
|
|
|
|
<span class="muted">上次连上 {{ wsc.connected_at || '—' }}</span>
|
|
|
|
|
|
<span class="muted">出口队列 <span class="mono">{{ JSON.stringify(wsc.queue||{}) }}</span></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="wsc.last_error" class="muted" style="margin-top:8px;color:#e6a23c">
|
|
|
|
|
|
最后错误: {{ wsc.last_error }}</div>
|
|
|
|
|
|
<div v-if="wst.last_reject" class="muted" style="margin-top:6px">
|
|
|
|
|
|
最后一条拒绝: <span class="mono">{{ wst.last_reject }}</span></div>
|
|
|
|
|
|
<div v-if="wst.last_close" class="muted" style="margin-top:6px">
|
|
|
|
|
|
最后断开原因: <span class="mono">{{ wst.last_close }}</span></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="panel">
|
|
|
|
|
|
<h3>出口委托 (pms_qmt_order)</h3>
|
|
|
|
|
|
<el-table :data="wsOrders" size="small" border max-height="300" empty-text="暂无委托">
|
|
|
|
|
|
<el-table-column prop="instruction_id" label="指令号" width="230" class-name="mono"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="ts_code" label="股票" width="110" class-name="mono"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="side" label="方向" width="70"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="qty" label="数量" width="80"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="limit_price" label="限价" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="status" label="状态" width="120"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="cum_qty" label="已成" width="80"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="cancel_state" label="撤单" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="reject_code" label="拒绝码" min-width="140"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="panel">
|
|
|
|
|
|
<h3>上行消息 (pms_qmt_inbox)
|
|
|
|
|
|
<span class="muted" style="font-weight:400">· 新→旧</span></h3>
|
|
|
|
|
|
<div class="row" style="margin-bottom:8px">
|
|
|
|
|
|
<el-checkbox v-model="wsHidePong" size="small">隐藏 pong</el-checkbox>
|
|
|
|
|
|
<span class="muted">pong 每 5 秒一条且按协议 §5 带 seq, 不隐藏会把成交冲没</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-table :data="wsInbox" size="small" border max-height="380" empty-text="还没收到上行消息">
|
|
|
|
|
|
<el-table-column prop="seq" label="seq" width="90" class-name="mono"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="msg_type" label="类型" width="130"></el-table-column>
|
|
|
|
|
|
<el-table-column label="入账" width="80">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<span class="muted">{{ ({0:'待入账',1:'已入账',2:'已消化'})[s.row.processed] || '?' }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="内容" min-width="380">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<span class="mono muted">{{ JSON.stringify(s.row.payload || {}) }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
</el-tabs>
|
|
|
|
|
|
|
2026-08-10 15:09:29 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<!-- 运维抽屉 -->
|
|
|
|
|
|
<el-drawer v-model="opsDrawer" title="运维操作 (与调度器同一份实现)" size="46%">
|
|
|
|
|
|
<div class="muted" style="margin-bottom:10px">
|
|
|
|
|
|
调度器 (--profile sched) 会自动按调度总表执行; 此处按钮用于未接调度或临时验证时手动触发。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="row">
|
2026-07-28 09:10:07 +08:00
|
|
|
|
<el-button @click="ops('plan-pending')" :loading="opsLoading">推进待规划命令</el-button>
|
|
|
|
|
|
<el-button @click="ops('materialize')" :loading="opsLoading">方案转指令</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="ops('exec-tick?dry_run=true')" :loading="opsLoading">
|
|
|
|
|
|
出手试算 (不下发)</el-button>
|
|
|
|
|
|
<el-button @click="ops('exec-tick')" :loading="opsLoading">出手一跳</el-button>
|
|
|
|
|
|
<el-button @click="ops('sweep-windows')" :loading="opsLoading">窗口收口</el-button>
|
|
|
|
|
|
</div>
|
2026-07-28 09:29:53 +08:00
|
|
|
|
<div class="row" style="margin-top:8px">
|
|
|
|
|
|
<el-button type="primary" @click="ops('scan-proposals?dry_run=true')" :loading="opsLoading">
|
|
|
|
|
|
提议扫描试算 (不落表)</el-button>
|
|
|
|
|
|
<el-button @click="ops('scan-proposals')" :loading="opsLoading">提议扫描</el-button>
|
|
|
|
|
|
</div>
|
2026-07-28 09:10:07 +08:00
|
|
|
|
<div class="row" style="margin-top:8px">
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-button @click="ops('replay')" :loading="opsLoading">成交回放</el-button>
|
|
|
|
|
|
<el-button @click="ops('reconcile')" :loading="opsLoading">账本对账</el-button>
|
|
|
|
|
|
<el-button @click="ops('premarket')" :loading="opsLoading">盘前准备</el-button>
|
|
|
|
|
|
<el-button @click="ops('daily-settle')" :loading="opsLoading">日终结算</el-button>
|
|
|
|
|
|
<el-button @click="ops('report')" :loading="opsLoading">生成日报</el-button>
|
|
|
|
|
|
<el-button @click="loadSchema" :loading="opsLoading">导出下游表结构</el-button>
|
|
|
|
|
|
</div>
|
2026-07-28 09:10:07 +08:00
|
|
|
|
<div class="muted" style="margin-top:10px">
|
|
|
|
|
|
当前下发通道: <b>{{ dm.mode }}</b> —— {{ dm.hint }}
|
|
|
|
|
|
(通道在「参数设置 → PMS_DISPATCH_MODE」切换)
|
|
|
|
|
|
</div>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<pre class="json" v-if="opsResult">{{ opsResult }}</pre>
|
|
|
|
|
|
</el-drawer>
|
|
|
|
|
|
|
2026-07-30 16:21:16 +08:00
|
|
|
|
<!-- 上游选股计划抽屉 -->
|
|
|
|
|
|
<el-drawer v-model="planDrawer" title="上游选股计划 (候选池的事实源)" size="56%">
|
|
|
|
|
|
<div class="row" style="margin-bottom:8px">
|
|
|
|
|
|
<el-button type="primary" :loading="planLoading" @click="refreshPlan">
|
|
|
|
|
|
强刷 + 灌行业映射</el-button>
|
|
|
|
|
|
<el-button :loading="planLoading" @click="loadPlan">只重读</el-button>
|
|
|
|
|
|
<span class="muted" style="margin-left:8px">
|
|
|
|
|
|
计划不带价格与金额 —— 买多少/什么价一律 PMS 自己算; 行情取不到价的票会被剔除。
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-alert v-if="planStatus.ok===false" type="error" effect="dark" show-icon :closable="false"
|
|
|
|
|
|
:title="planStatus.hint || '不可用'" style="margin-bottom:8px"></el-alert>
|
|
|
|
|
|
<div class="muted" v-else style="margin-bottom:8px">
|
|
|
|
|
|
计划日 <b>{{ planStatus.date }}</b> (日龄 {{ planStatus.age_tdays }} 交易日) ·
|
|
|
|
|
|
热度日 {{ planStatus.heat_date || '-' }} · 快照 {{ (planStatus.market_snapshot_days||[]).join(',') }} ·
|
2026-07-31 08:54:36 +08:00
|
|
|
|
上游主题上限 {{ planStatus.theme_cap }} · theme 灌映射 {{ JSON.stringify(planStatus.theme_sync||{}) }}
|
|
|
|
|
|
<br>请求 {{ JSON.stringify(planStatus.query||{}) }} · 漏斗: 打分池
|
|
|
|
|
|
主榜 {{ (planStatus.funnel||{}).scored_main }} / 观察 {{ (planStatus.funnel||{}).scored_observe }}
|
|
|
|
|
|
→(券商预期 + 目标价≥现价 + 主题限额 + top)→ 实收
|
|
|
|
|
|
主榜 {{ (planStatus.funnel||{}).returned_main }} / 观察 {{ (planStatus.funnel||{}).returned_observe }}
|
2026-07-30 16:21:16 +08:00
|
|
|
|
</div>
|
2026-07-31 09:17:54 +08:00
|
|
|
|
<el-alert v-if="planCand && planCand.tier_complete===false" type="warning" effect="dark"
|
|
|
|
|
|
show-icon :closable="false" style="margin-bottom:8px"
|
|
|
|
|
|
:title="'返回的全是白名单档位 (top=' + ((planStatus.requested||{}).top)
|
|
|
|
|
|
+ ' 吃满了) —— 白名单档位可能没取全, 调大 PMS_PLAN_TOP'">
|
2026-07-30 17:00:15 +08:00
|
|
|
|
</el-alert>
|
2026-07-30 16:21:16 +08:00
|
|
|
|
<div class="muted" v-if="planCand" style="margin-bottom:8px">
|
|
|
|
|
|
按当前参数筛选: 排序池 {{ planCand.considered }} → 合格 {{ planCand.eligible }} →
|
2026-07-31 09:17:54 +08:00
|
|
|
|
取 {{ (planCand.items||[]).length }} 只 ·
|
|
|
|
|
|
白名单档位取全 {{ planCand.tier_complete===null ? '不适用' : (planCand.tier_complete ? '是':'否') }} ·
|
|
|
|
|
|
丢弃 {{ JSON.stringify(planCand.dropped||{}) }}
|
|
|
|
|
|
<span v-if="(planCand.st_unknown||[]).length" style="color:#E6A23C">·
|
|
|
|
|
|
{{ planCand.st_unknown.length }} 只无名称, ST 判不了已放行</span>
|
|
|
|
|
|
<br><span class="mono">{{ (planCand.items||[]).map(x=>x.ts_code).join(' ') }}</span>
|
2026-07-30 16:21:16 +08:00
|
|
|
|
</div>
|
2026-07-31 13:28:10 +08:00
|
|
|
|
<!-- 榜单变化 (PMS 自算; 上游 changes 恒为 null) -->
|
|
|
|
|
|
<el-divider content-position="left">榜单变化</el-divider>
|
|
|
|
|
|
<div class="row" style="margin-bottom:6px">
|
|
|
|
|
|
<el-button size="small" :loading="chgLoading" @click="loadChanges">重算变化</el-button>
|
|
|
|
|
|
<span class="muted" style="margin-left:8px" v-if="chgSt.hint">{{ chgSt.hint }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-alert v-if="chgSt.in_sync === false" type="error" effect="dark" show-icon
|
|
|
|
|
|
:closable="false" style="margin-bottom:8px"
|
|
|
|
|
|
title="手上这份计划没落进快照 —— 下面比的是两个旧版本, 与当前候选池不是同一份">
|
|
|
|
|
|
</el-alert>
|
|
|
|
|
|
<el-alert v-else-if="chg && chg.kind === 'same_date_revision'" type="warning" effect="dark"
|
|
|
|
|
|
show-icon :closable="false" style="margin-bottom:8px"
|
|
|
|
|
|
:title="'同一计划日 ' + chg.curr_date + ' 的新版本 —— 上游重算过, '
|
|
|
|
|
|
+ '此前拿到的候选池与现在不是同一份 (上游 /plan 是实时算的, 没有版本戳)'">
|
|
|
|
|
|
</el-alert>
|
|
|
|
|
|
<el-alert v-if="chg && chg.prev_broken" type="error" effect="dark" show-icon
|
|
|
|
|
|
:closable="false" style="margin-bottom:8px"
|
|
|
|
|
|
title="上一版名册读不出来 (快照损坏) —— 这次比不了, 下面的空白不代表没有变化">
|
|
|
|
|
|
</el-alert>
|
|
|
|
|
|
<el-alert v-if="chgHeld.n_watch" type="error" effect="dark" show-icon :closable="false"
|
|
|
|
|
|
style="margin-bottom:8px"
|
|
|
|
|
|
:title="'持仓票 ' + chgHeld.n_watch + ' 只掉榜 / 降档 / 丢了券商覆盖 —— 见下表红字'">
|
|
|
|
|
|
</el-alert>
|
|
|
|
|
|
<el-alert v-if="chg && chg.date_regressed" type="warning" effect="dark" show-icon
|
|
|
|
|
|
:closable="false" style="margin-bottom:8px"
|
|
|
|
|
|
title="计划日往回走了 (拉过一份旧计划?) —— 新进/掉榜的方向是反的">
|
|
|
|
|
|
</el-alert>
|
|
|
|
|
|
<div class="muted" v-if="chg" style="margin-bottom:8px">
|
|
|
|
|
|
{{ chg.kind === 'first' ? '首次落快照' : (chg.prev_date + ' → ' + chg.curr_date) }} ·
|
|
|
|
|
|
名册 {{ chg.roster_size.prev }} → {{ chg.roster_size.curr }} ·
|
|
|
|
|
|
新进 {{ chg.counts.entered }} · 掉榜 {{ chg.counts.exited }} ·
|
|
|
|
|
|
升档 {{ chg.counts.tier_up }} · 降档 {{ chg.counts.tier_down }} ·
|
|
|
|
|
|
覆盖翻转 {{ chg.counts.bucket_moved }} · 名次跳变 {{ chg.counts.rank_jump }}
|
|
|
|
|
|
<span v-if="(chg.tail_churn.entered + chg.tail_churn.exited)">·
|
|
|
|
|
|
榜尾进出 {{ chg.tail_churn.entered + chg.tail_churn.exited }} 只未计入 (那一版吃满 top,
|
|
|
|
|
|
尾部是截断噪音不是观点变化)</span>
|
|
|
|
|
|
<span v-if="chg.tail_churn.held_exempt">· 其中
|
|
|
|
|
|
<b>{{ chg.tail_churn.held_exempt }} 只因是持仓票照报</b> (持仓票不受榜尾闸约束 ——
|
|
|
|
|
|
漏报一条就是一个仓位)</span>
|
|
|
|
|
|
<span v-if="chgRevised.length">· <b>同一计划日出现多版</b>:
|
|
|
|
|
|
{{ chgRevised.map(x => x[0] + '×' + x[1]).join(', ') }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-tabs v-model="chgTab" v-if="chg">
|
|
|
|
|
|
<el-tab-pane name="held" :label="'持仓票 (' + chgHeld.n_total + ')'">
|
|
|
|
|
|
<el-table :data="chgWatch.filter(x => x.held)" size="small" border max-height="220"
|
|
|
|
|
|
empty-text="持仓票没被摘也没降档">
|
|
|
|
|
|
<el-table-column prop="ts_code" label="代码" width="104"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="name" label="名称" width="92"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="why" label="发生了什么" min-width="200">
|
|
|
|
|
|
<template #default="s"><span style="color:#F56C6C">{{ s.row.why }}</span></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="rank" label="上版 rank" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="theme" label="主题" width="120"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<div class="muted" style="margin-top:6px">
|
|
|
|
|
|
上游把一只<b>持仓票</b>摘出榜或降了档, 是"还该不该继续拿着"的直接信号 ——
|
|
|
|
|
|
单独拎出来免得被几十条新进榜淹掉。<b>它只是提示, 不自动产生任何指令</b>:
|
|
|
|
|
|
要减要清仍走命令台或提议确认。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
<el-tab-pane name="watch" :label="'掉榜与降档 (' + chgWatch.length + ')'">
|
|
|
|
|
|
<el-table :data="chgWatch" size="small" border max-height="220" empty-text="无">
|
|
|
|
|
|
<el-table-column prop="ts_code" label="代码" width="104"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="name" label="名称" width="92"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="why" label="发生了什么" min-width="180"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="rank" label="上版 rank" width="90"></el-table-column>
|
|
|
|
|
|
<el-table-column label="持仓" width="70">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-tag v-if="s.row.held" type="danger" size="small">持仓</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
<el-tab-pane name="in" :label="'新进榜 (' + chg.counts.entered + ')'">
|
|
|
|
|
|
<el-table :data="chg.entered" size="small" border max-height="220" empty-text="无">
|
|
|
|
|
|
<el-table-column prop="rank" label="rank" width="62"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="ts_code" label="代码" width="104"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="name" label="名称" width="92"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="tier" label="传导档" width="82"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="theme" label="主题" width="120"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="score" label="score" width="82"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
<el-tab-pane name="up" :label="'升档 (' + chg.counts.tier_up + ')'">
|
|
|
|
|
|
<el-table :data="chg.tier_up" size="small" border max-height="220" empty-text="无">
|
|
|
|
|
|
<el-table-column prop="rank" label="rank" width="62"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="ts_code" label="代码" width="104"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="name" label="名称" width="92"></el-table-column>
|
|
|
|
|
|
<el-table-column label="档位" min-width="150">
|
|
|
|
|
|
<template #default="s">{{ s.row.tier_from }} → <b>{{ s.row.tier_to }}</b></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="theme" label="主题" width="120"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
<el-tab-pane name="jump" :label="'名次跳变 (' + chg.counts.rank_jump + ')'">
|
|
|
|
|
|
<el-table :data="chg.rank_jump" size="small" border max-height="220" empty-text="无">
|
|
|
|
|
|
<el-table-column prop="ts_code" label="代码" width="104"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="name" label="名称" width="92"></el-table-column>
|
|
|
|
|
|
<el-table-column label="名次" min-width="150">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
{{ s.row.rank_from }} → {{ s.row.rank_to }}
|
|
|
|
|
|
<span :style="{color: s.row.rank_delta > 0 ? '#F56C6C' : '#67C23A'}">
|
|
|
|
|
|
({{ s.row.rank_delta > 0 ? '+' : '' }}{{ s.row.rank_delta }})</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="tier" label="传导档" width="82"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="theme" label="主题" width="120"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
<el-tab-pane name="log" :label="'快照台账 (' + (chgLog.rows||[]).length + ')'">
|
|
|
|
|
|
<el-table :data="chgLog.rows" size="small" border max-height="220" empty-text="还没有快照">
|
|
|
|
|
|
<el-table-column prop="plan_date" label="计划日" width="104"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="fetched_at" label="拉到的时刻" width="170"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="n_main" label="主榜" width="70"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="n_observe" label="观察" width="70"></el-table-column>
|
|
|
|
|
|
<el-table-column label="吃满 top" width="90">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<el-tag v-if="s.row.capped_main" type="warning" size="small">主榜</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="digest" label="指纹" min-width="140"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<div class="muted" style="margin-top:6px">
|
|
|
|
|
|
<b>同一「计划日」出现多行 = 上游那天重算过多版</b>(UPSTREAM_PLAN_API.md §7.3:
|
|
|
|
|
|
`/plan` 是实时算的, 应答里没有 generated_at, 下游本来没办法判断手上这份是哪一版)。
|
|
|
|
|
|
指纹不含 score —— 分数抖动但次序没变不算新版本, 否则表白涨而信息量为零。
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
<el-divider content-position="left">主榜明细</el-divider>
|
2026-07-30 16:21:16 +08:00
|
|
|
|
<el-table :data="planRows" size="small" border height="420">
|
|
|
|
|
|
<el-table-column prop="rank" label="rank" width="62"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="ts_code" label="代码" width="104"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="name" label="名称" width="92"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="score" label="score" width="82"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="tier" label="传导档" width="82"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="theme" label="主题" width="120"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="heat" label="heat" width="78"></el-table-column>
|
2026-07-30 16:38:24 +08:00
|
|
|
|
<el-table-column label="预期空间" width="96">
|
|
|
|
|
|
<template #default="s">
|
|
|
|
|
|
<span class="muted">{{ s.row.upside==null ? '-' :
|
|
|
|
|
|
((s.row.upside>0?'+':'') + (s.row.upside*100).toFixed(0) + '%') }}</span>
|
|
|
|
|
|
</template>
|
2026-07-30 16:21:16 +08:00
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="n_sources" label="源数" width="66"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<div class="muted" style="margin-top:6px">
|
2026-07-30 16:38:24 +08:00
|
|
|
|
「预期空间」= 券商目标价相对现价的空间 (上游 upside 字段, 2.12 → +212%)。噪音大,
|
|
|
|
|
|
所以<b>永不参与排序</b> —— 排序始终按 score; 只在 PMS_PLAN_MIN_UPSIDE 上做下限过滤。
|
2026-07-30 16:21:16 +08:00
|
|
|
|
筛选参数在「参数设置」里: PMS_PLAN_TOP_N / PMS_PLAN_TIERS / PMS_PLAN_INCLUDE_OBSERVE /
|
2026-07-31 08:54:36 +08:00
|
|
|
|
PMS_PLAN_MIN_SCORE / PMS_PLAN_MIN_SOURCES / PMS_PLAN_MIN_UPSIDE / PMS_PLAN_STALE_TDAYS。
|
|
|
|
|
|
<br>发给上游的三个: PMS_PLAN_TOP / PMS_PLAN_OBS_TOP / PMS_PLAN_THEME_CAP (0=不传, 用上游默认
|
|
|
|
|
|
20/10/5)。<b>PMS_PLAN_THEME_CAP_LOCAL</b> 是 PMS 侧的同主题限额, 在 TOP_N 截断之前生效 ——
|
|
|
|
|
|
让上游别裁 (THEME_CAP 设大) + PMS 自己裁, 前 N 名才不会被单一主题垄断。
|
2026-07-31 09:17:54 +08:00
|
|
|
|
<br><b>PMS_PLAN_EXCLUDE_ST</b> 默认开: 上游名单明确不剔除 ST/*ST, 这道闸在 PMS 侧。
|
|
|
|
|
|
「主题」是<b>传导主题</b>(事件驱动、天天变、覆盖约三成), <b>不是行业</b> —— 行业硬拦截
|
|
|
|
|
|
请把 PMS_SECTOR_SOURCE 设成 gp_stock_category, 别用 PMS_PLAN_THEME_SYNC。
|
2026-07-30 16:21:16 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<pre class="json" v-if="planResult">{{ planResult }}</pre>
|
|
|
|
|
|
</el-drawer>
|
|
|
|
|
|
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<!-- 日报抽屉 -->
|
|
|
|
|
|
<el-drawer v-model="reportDrawer" :title="'运营日报 ' + (report.ymd||'')" size="52%">
|
|
|
|
|
|
<div v-if="(report.report||{}).attention">
|
|
|
|
|
|
<h3 style="color:#409EFF;font-size:14px">关注区</h3>
|
|
|
|
|
|
<el-table :data="report.report.attention" size="small" border>
|
2026-07-29 10:01:00 +08:00
|
|
|
|
<el-table-column prop="type" label="事项" width="130"></el-table-column>
|
2026-07-27 17:12:09 +08:00
|
|
|
|
<el-table-column label="内容">
|
|
|
|
|
|
<template #default="s"><span class="mono muted">{{ JSON.stringify(s.row) }}</span></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<pre class="json">{{ JSON.stringify(report.report||{}, null, 2) }}</pre>
|
|
|
|
|
|
</el-drawer>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-07-29 13:51:10 +08:00
|
|
|
|
const { createApp, ref, reactive, computed, onMounted, onUnmounted, watch } = Vue;
|
2026-07-27 17:12:09 +08:00
|
|
|
|
const api = axios.create({ timeout: 60000 });
|
|
|
|
|
|
|
|
|
|
|
|
createApp({
|
|
|
|
|
|
setup() {
|
|
|
|
|
|
const tab = ref('cmd'), loading = ref(false), err = ref('');
|
2026-08-10 15:09:29 +08:00
|
|
|
|
const mode = ref('trader');
|
2026-07-27 17:12:09 +08:00
|
|
|
|
const health = ref({}), ov = ref({}), params = ref([]), paramsRaw = ref({});
|
|
|
|
|
|
const catalog = ref([]), commands = ref([]), plans = ref([]), plansOf = ref('');
|
|
|
|
|
|
const positions = ref([]), lots = ref([]), lotsOf = ref('');
|
|
|
|
|
|
const instructions = ref([]), ledger = ref([]), proposals = ref([]);
|
2026-07-28 09:10:07 +08:00
|
|
|
|
const report = ref({}), reportDrawer = ref(false), dm = ref({});
|
2026-07-27 17:12:09 +08:00
|
|
|
|
const opsDrawer = ref(false), opsResult = ref(''), opsLoading = ref(false);
|
2026-07-30 16:21:16 +08:00
|
|
|
|
// 上游选股计划: 单独取数, 不塞进 /api/overview —— overview 是轮询的, 上游不通时
|
|
|
|
|
|
// 会把 10 秒超时拖进每一次页面刷新。
|
|
|
|
|
|
const planDrawer = ref(false), planLoading = ref(false), planRaw = ref({}),
|
|
|
|
|
|
planResult = ref('');
|
|
|
|
|
|
const planStatus = computed(() => planRaw.value.status || {});
|
|
|
|
|
|
const planRows = computed(() => planRaw.value.rows || []);
|
|
|
|
|
|
const planCand = computed(() => planRaw.value.candidates_raw || null);
|
2026-07-31 13:28:10 +08:00
|
|
|
|
// 榜单变化 (PMS 自算 —— 上游 changes 字段恒为 null)。另起一个接口, 不塞进 /api/upstream/plan:
|
|
|
|
|
|
// 它要读库, 而计划预览要读上游, 两者的故障域不同, 混在一起会互相拖垮。
|
|
|
|
|
|
const chgRaw = ref({}), chgLoading = ref(false), chgTab = ref('held');
|
|
|
|
|
|
const chgSt = computed(() => chgRaw.value.changes || {});
|
|
|
|
|
|
const chg = computed(() => chgSt.value.diff || null);
|
|
|
|
|
|
const chgHeld = computed(() => (chg.value || {}).held || {});
|
|
|
|
|
|
const chgLog = computed(() => chgRaw.value.log || {});
|
|
|
|
|
|
const chgRevised = computed(() => Object.entries(chgLog.value.revised || {}));
|
|
|
|
|
|
// 掉榜/降档合成一张表 —— 「上游把这只票摘了或降了档」是同一个意思的两种说法
|
|
|
|
|
|
const chgWatch = computed(() => {
|
|
|
|
|
|
const d = chg.value; if (!d) return [];
|
|
|
|
|
|
// 三类都算「上游把这只票看淡了」: 掉榜 / 降档 / 掉进观察档(=没券商覆盖了, 估值锚没了)。
|
|
|
|
|
|
// 与 plan_diff 的 held.n_watch 口径一致 —— 两边分家的话红条会亮而表里没有对应行。
|
|
|
|
|
|
return (d.exited || []).map(x => ({ ...x, why: '掉榜' }))
|
|
|
|
|
|
.concat((d.tier_down || []).map(x => ({ ...x, why: '降档 ' + x.tier_from + '→' + x.tier_to })))
|
|
|
|
|
|
.concat((d.bucket_moved || []).filter(x => x.moved_to === 'observe')
|
|
|
|
|
|
.map(x => ({ ...x, why: '券商覆盖没了 (主榜→观察档)' })));
|
|
|
|
|
|
});
|
2026-07-27 17:12:09 +08:00
|
|
|
|
const issuing = ref(false);
|
|
|
|
|
|
const form = reactive({ cmd_type: '', params: {}, note: '' });
|
|
|
|
|
|
|
2026-07-29 13:51:10 +08:00
|
|
|
|
// ---- ws 通道监控 (只读)
|
|
|
|
|
|
const wsRaw = ref({}), wsAuto = ref(true), wsHidePong = ref(true);
|
|
|
|
|
|
let wsTimer = null;
|
2026-07-30 13:02:51 +08:00
|
|
|
|
// 总规模(用户命令参数) 与 账户实际总资产(ws 资金快照) 的偏差。三道仓位闸都按 scale 算,
|
|
|
|
|
|
// 资金校验按账户算 —— 差得远时方案会过闸但下不出去, 而页面上原来完全看不出来。
|
|
|
|
|
|
const scaleGap = computed(() => {
|
|
|
|
|
|
const ta = Number(ov.value.total_asset || 0), s = Number(ov.value.scale || 0);
|
|
|
|
|
|
if (!ta || !s) return 0;
|
|
|
|
|
|
const g = ta / s - 1;
|
|
|
|
|
|
return Math.abs(g) > 0.10 ? g : 0;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-29 13:51:10 +08:00
|
|
|
|
const wsc = computed(() => wsRaw.value.channel || {});
|
|
|
|
|
|
const wst = computed(() => wsc.value.stat || {});
|
|
|
|
|
|
const wsMode = computed(() => wsRaw.value.mode || '');
|
|
|
|
|
|
const wsOrders = computed(() => wsRaw.value.orders || []);
|
|
|
|
|
|
const wsInbox = computed(() => (wsRaw.value.inbox || [])
|
|
|
|
|
|
.filter(r => !(wsHidePong.value && r.msg_type === 'pong')));
|
|
|
|
|
|
|
|
|
|
|
|
// 判据与 scripts/ws_smoke.py 的自检逐条对应。改一边就要改另一边 ——
|
|
|
|
|
|
// 页面说"正常"而命令行说"异常"(或反过来), 比两边都没有告警更能误导人。
|
|
|
|
|
|
const wsWarnings = computed(() => {
|
|
|
|
|
|
const c = wsc.value, s = wst.value, out = [];
|
|
|
|
|
|
if (!c.process_alive)
|
|
|
|
|
|
out.push('pms-ws 进程不在线 (心跳陈旧) —— dispatcher 会一律拒发。'
|
|
|
|
|
|
+ '起进程: docker compose --profile ws up -d pms-ws');
|
|
|
|
|
|
const hsSrv = +(s.hs_server_seq || 0), hsLast = +(s.hs_last_seq || 0);
|
|
|
|
|
|
if (hsSrv && hsSrv < hsLast)
|
|
|
|
|
|
out.push(`序号倒挂: 握手时对端自报 ${hsSrv}, 本端水位已到 ${hsLast}。对端此后发的每一条`
|
|
|
|
|
|
+ `(含成交)都会因「不高于水位」被静默丢弃。不要自行下调水位 —— 会把 `
|
|
|
|
|
|
+ `${hsSrv}..${hsLast} 重复入账。须先与 QMT 侧对齐 seq 语义`);
|
|
|
|
|
|
if (+(s.dropped || 0))
|
|
|
|
|
|
out.push(`已丢弃 ${s.dropped} 条上行 (seq 不高于水位)。补发时命中是正常的, `
|
|
|
|
|
|
+ `持续增长说明序号语义对不上`);
|
|
|
|
|
|
if (s.ack_seq_degraded)
|
|
|
|
|
|
out.push('ack_seq 已降级停发 (对端不认这个类型, 协议 §4.5)。水位照常落库、重连补发靠 '
|
|
|
|
|
|
+ 'hello.last_seq, 不丢成交; 代价是 QMT 侧 Redis 清不掉');
|
|
|
|
|
|
if (+(s.proto_rejects || 0))
|
|
|
|
|
|
out.push(`协议级 reject ${s.proto_rejects} 条 (不带 instruction_id, 拒的不是委托而是`
|
|
|
|
|
|
+ `我们发的协议消息)。若收发计数同步增长, 基本是「ack→reject→再 ack」的自激循环`);
|
|
|
|
|
|
if (s.peer_skew_ms != null && Math.abs(+s.peer_skew_ms) > 20000)
|
|
|
|
|
|
out.push(`对端时钟偏差 ${s.peer_skew_ms} ms, 已逼近 QMT 侧 ±30 秒时间窗。再漂下去连 `
|
|
|
|
|
|
+ `place_order 都会被 TS_SKEW 拒, 现象是「下单没反应」。两台机器都对一下 NTP`);
|
|
|
|
|
|
if (c.resync_required)
|
|
|
|
|
|
out.push('resync_flag=1: 对端补发不全, 须走全量对账后经运维抽屉清除标记');
|
|
|
|
|
|
if (c.error) out.push(c.error);
|
|
|
|
|
|
return out;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-27 17:12:09 +08:00
|
|
|
|
const curSpec = computed(() => catalog.value.find(c => c.cmd_type === form.cmd_type));
|
|
|
|
|
|
const dirtyCount = computed(() =>
|
|
|
|
|
|
params.value.filter(p => String(p.value) !== String(paramsRaw.value[p.key])).length);
|
|
|
|
|
|
|
|
|
|
|
|
const money = v => (v === null || v === undefined || v === '') ? '—'
|
|
|
|
|
|
: Number(v).toLocaleString('zh-CN', { maximumFractionDigits: 0 });
|
|
|
|
|
|
const pct = v => (v === null || v === undefined || v === '') ? '—'
|
|
|
|
|
|
: (Number(v) * 100).toFixed(2) + '%';
|
|
|
|
|
|
const groupLabel = g => ({ A: 'A 资金与总体参数', B: 'B 组合级动作', C: 'C 个股级动作' }[g]);
|
|
|
|
|
|
const fieldLabel = n => ({
|
|
|
|
|
|
pct: '比例', scale: '总规模', cap: '上限', target: '目标', n: '数量', mode: '档位',
|
|
|
|
|
|
ratio: '比例', window_tdays: '窗口(交易日)', ts_code: '股票代码', target_pct: '目标仓位',
|
|
|
|
|
|
sector: '行业', t_ratio: 'T仓比例', price: '价格', confirm: '二次确认',
|
|
|
|
|
|
target_command_id: '目标命令号'
|
|
|
|
|
|
}[n] || n);
|
|
|
|
|
|
const stTag = s => ({ EXECUTING: 'primary', DONE: 'success', PARTIAL: 'warning',
|
|
|
|
|
|
CANCELLED: 'info', EFFECTIVE: 'success', SUPERSEDED: 'info' }[s] || 'warning');
|
|
|
|
|
|
const cuTag = s => ({ SOLID: 'success', THIN: 'warning', NONE: 'danger' }[s] || 'info');
|
|
|
|
|
|
const canCancel = r => r.cmd_class === 'task' &&
|
|
|
|
|
|
['PENDING', 'PLANNING', 'EXECUTING', 'PARTIAL'].includes(r.status);
|
|
|
|
|
|
const progPct = p => {
|
|
|
|
|
|
const t = Number(p.target_amount || 0), d = Number(p.done_amount || 0);
|
|
|
|
|
|
return t > 0 ? Math.min(100, Math.round(d / t * 100)) : 0;
|
|
|
|
|
|
};
|
2026-07-28 09:10:07 +08:00
|
|
|
|
const insPct = r => {
|
|
|
|
|
|
const q = Number(r.qty || 0), e = Number(r.exec_qty || 0);
|
|
|
|
|
|
return q > 0 ? Math.min(100, Math.round(e / q * 100)) : 0;
|
|
|
|
|
|
};
|
2026-08-10 15:09:29 +08:00
|
|
|
|
// ===================== 交易员视图:翻译层 + 名称 + 动作 =====================
|
|
|
|
|
|
const T = {
|
|
|
|
|
|
action: { OPEN:'建新仓', FILL:'回踩补足', ADD:'盈利加仓', DCA:'浮亏补仓', TRIM:'保垫减仓',
|
|
|
|
|
|
EXIT:'清仓', T0:'做T', T0_ROUND:'做T', SWITCH:'换仓', HALT:'停手' },
|
|
|
|
|
|
side: { buy:'买入', sell:'卖出' },
|
|
|
|
|
|
autonomy: { full:'全自动', propose_only:'只提议不下手', off:'暂停' },
|
|
|
|
|
|
cushion: { SOLID:'厚垫', THIN:'薄垫', NONE:'浮亏无垫' },
|
|
|
|
|
|
lot: { BASE:'底仓', FILL:'回踩补足', ADD:'盈利加仓', DCA:'补仓', T0:'做T仓', RECON:'对账调整' },
|
|
|
|
|
|
frozen: { NONE:'', COMMAND_HALT:'命令冻结(禁增持)', BRAKE:'刹车冻结', MANUAL:'手动冻结' },
|
|
|
|
|
|
posStatus: { PLANNED:'计划建仓', OPENING:'建仓中', HOLDING:'持有中', EXITING:'清仓中', CLOSED:'已清仓' },
|
|
|
|
|
|
insStatus: { PROPOSED:'待校验', RULE_PASSED:'规则闸已过', JUDGE_PASSED:'研判已过', DISPATCHED:'已下发挂单',
|
|
|
|
|
|
CONFIRMED:'已成交', REJECTED:'被拒', EXPIRED:'到期作废', CANCELLED:'已撤销' },
|
|
|
|
|
|
arbiter: { rule:'规则闸', judge:'研判闸', user:'你' },
|
|
|
|
|
|
verdict: { PASS:'放行', REJECT:'驳回', NOTE:'记录', UNAVAILABLE:'研判暂不可用' },
|
|
|
|
|
|
};
|
|
|
|
|
|
const tx = (g, c) => {
|
|
|
|
|
|
const m = T[g];
|
|
|
|
|
|
if (m && Object.prototype.hasOwnProperty.call(m, c)) return m[c];
|
|
|
|
|
|
if (c == null || c === '') return '—';
|
|
|
|
|
|
return String(c); // 未登记的码:显示原样, 不臆造
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const nameMap = ref({});
|
|
|
|
|
|
const nm = c => (nameMap.value[c] && nameMap.value[c].name) || c;
|
|
|
|
|
|
const fullName = c => (nameMap.value[c] && nameMap.value[c].full) || '';
|
|
|
|
|
|
async function loadNames() {
|
|
|
|
|
|
const set = new Set();
|
|
|
|
|
|
(positions.value || []).forEach(p => p.ts_code && set.add(p.ts_code));
|
|
|
|
|
|
(proposals.value || []).forEach(p => p.ts_code && set.add(p.ts_code));
|
|
|
|
|
|
(instructions.value || []).forEach(p => p.ts_code && set.add(p.ts_code));
|
|
|
|
|
|
(ledger.value || []).forEach(p => p.ts_code && set.add(p.ts_code));
|
|
|
|
|
|
const codes = [...set].filter(Boolean);
|
|
|
|
|
|
if (!codes.length) return;
|
|
|
|
|
|
const d = await call('get', '/api/names?codes=' + encodeURIComponent(codes.join(',')));
|
|
|
|
|
|
const names = (d && (d.names || (d.data && d.data.names))) || null;
|
|
|
|
|
|
if (names) nameMap.value = { ...nameMap.value, ...names };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const heldPositions = computed(() => (positions.value || []).filter(p => (p.total_qty || 0) > 0));
|
|
|
|
|
|
const settingKeys = ['PMS_TOTAL_SCALE', 'PMS_PORTFOLIO_CAP', 'PMS_STOCK_CAP',
|
|
|
|
|
|
'PMS_MAX_NAMES', 'PMS_CASH_RESERVE', 'PMS_AUTONOMY'];
|
|
|
|
|
|
const traderParams = computed(() => (params.value || []).filter(p => settingKeys.includes(p.key)));
|
|
|
|
|
|
|
|
|
|
|
|
const attention = computed(() => {
|
|
|
|
|
|
const o = ov.value || {}, out = [];
|
|
|
|
|
|
if (o.buy_halt) out.push('已暂停买入(卖出与止损不受影响)');
|
|
|
|
|
|
if (o.exec_halt) out.push('休假模式:所有自动动作暂停,仅保留对账与日报');
|
|
|
|
|
|
if (o.brake_until) out.push('组合刹车中(解除日 ' + o.brake_until + ')');
|
|
|
|
|
|
if ((o.price_missing || []).length)
|
|
|
|
|
|
out.push(o.price_missing.length + ' 只取不到实时价(停牌等),其盈亏与安全垫暂不可用');
|
|
|
|
|
|
if (o.sector_ready === false) out.push('行业数据未就绪,行业类操作暂不可用');
|
|
|
|
|
|
const j = (dm.value || {}).judge;
|
|
|
|
|
|
if (j && j.available === false) out.push('研判暂不可用:补足/加仓/补仓一律降级为需你确认');
|
|
|
|
|
|
const c = wsc.value || {};
|
|
|
|
|
|
if (c.conn_state && c.conn_state !== 'ONLINE') out.push('行情/成交通道未连接(' + c.conn_state + ')');
|
|
|
|
|
|
if (c.resync_required) out.push('通道待人工全量对账');
|
|
|
|
|
|
return out;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function scrollTo(id) { const e = document.getElementById(id); if (e) e.scrollIntoView({ behavior: 'smooth', block: 'start' }); }
|
|
|
|
|
|
function propWhy(p) {
|
|
|
|
|
|
const h = p.hard_numbers || {}, bits = [];
|
|
|
|
|
|
if (h.rank != null) bits.push('候选榜第 ' + h.rank);
|
|
|
|
|
|
if (h.theme) bits.push('主题「' + h.theme + '」' + (h.tier ? (' · ' + h.tier) : ''));
|
|
|
|
|
|
if (h.upside != null) bits.push('预期空间 ' + (h.upside > 0 ? '+' : '') + (h.upside * 100).toFixed(0) + '%');
|
|
|
|
|
|
if (h.score != null) bits.push('评分 ' + (typeof h.score === 'number' ? h.score.toFixed(0) : h.score));
|
|
|
|
|
|
if (h.heat != null) bits.push('热度 ' + (typeof h.heat === 'number' ? h.heat.toFixed(2) : h.heat));
|
|
|
|
|
|
return bits.join(' · ') || '—';
|
|
|
|
|
|
}
|
|
|
|
|
|
function nextStep(r) {
|
|
|
|
|
|
if (r.frozen_reason && r.frozen_reason !== 'NONE') return '已冻结,暂不增持';
|
|
|
|
|
|
if (r.cushion_state === 'SOLID') return '安全垫较厚,达标可加仓';
|
|
|
|
|
|
if ((r.cushion_pct || 0) < 0) return '浮亏中,跌到评估档会考虑补仓(深档需你确认)';
|
|
|
|
|
|
return '持有观察';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function promptNum(msg, title) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { value } = await ElementPlus.ElMessageBox.prompt(msg, title, {});
|
|
|
|
|
|
const n = parseFloat(value);
|
|
|
|
|
|
if (isNaN(n)) { ElementPlus.ElMessage.warning('请输入数字'); return null; }
|
|
|
|
|
|
return n;
|
|
|
|
|
|
} catch (e) { return null; }
|
|
|
|
|
|
}
|
|
|
|
|
|
async function quickCmd(cmd_type, params, confirmMsg) {
|
|
|
|
|
|
if (confirmMsg) {
|
|
|
|
|
|
try { await ElementPlus.ElMessageBox.confirm(confirmMsg, '确认', { type: 'warning' }); }
|
|
|
|
|
|
catch (e) { return; }
|
|
|
|
|
|
}
|
|
|
|
|
|
let d = await call('post', '/api/commands', { cmd_type, params: params || {}, note: '交易员视图' });
|
|
|
|
|
|
if (!d.ok && (d.conflicts || []).length) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await ElementPlus.ElMessageBox.confirm('与在途命令冲突,仍要下达?', '命令冲突', { type: 'warning' });
|
|
|
|
|
|
d = await call('post', '/api/commands', { cmd_type, params: params || {}, note: '交易员视图', force: true });
|
|
|
|
|
|
} catch (e) { return; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (d.ok) { ElementPlus.ElMessage.success('已下达 ' + (d.command_id || '')); await loadAll(); }
|
|
|
|
|
|
else ElementPlus.ElMessage.error((d.errors || [d.error]).join('; '));
|
|
|
|
|
|
}
|
|
|
|
|
|
// 个股
|
|
|
|
|
|
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));
|
|
|
|
|
|
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));
|
|
|
|
|
|
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));
|
|
|
|
|
|
if (v != null) quickCmd('SET_TARGET_PRICE', { ts_code: row.ts_code, price: v });
|
|
|
|
|
|
}
|
|
|
|
|
|
function actFreeze(row) {
|
|
|
|
|
|
const on = row.frozen_reason && row.frozen_reason !== 'NONE';
|
|
|
|
|
|
quickCmd(on ? 'UNFREEZE_STOCK' : 'FREEZE_STOCK', { ts_code: row.ts_code });
|
|
|
|
|
|
}
|
|
|
|
|
|
function actBlacklist(row) { quickCmd('BLACKLIST_ADD', { ts_code: row.ts_code }, '把 ' + nm(row.ts_code) + ' 加入黑名单(永不买入)?'); }
|
|
|
|
|
|
function rowMore(c, row) {
|
|
|
|
|
|
if (c === 'stop') actStop(row);
|
|
|
|
|
|
else if (c === 'target') actTargetPrice(row);
|
|
|
|
|
|
else if (c === 'freeze') actFreeze(row);
|
|
|
|
|
|
else if (c === 'black') actBlacklist(row);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 组合
|
|
|
|
|
|
function pHaltBuy() { quickCmd('HALT_BUY', {}, '暂停一切新增买入并撤销在途买入指令?卖出与止损不受影响。'); }
|
|
|
|
|
|
function pResumeBuy() { quickCmd('RESUME_BUY', {}); }
|
|
|
|
|
|
function pHaltAll() { quickCmd('HALT_ALL', {}, '进入休假模式:所有自动动作暂停,只保留对账与日报?'); }
|
|
|
|
|
|
function pResumeAll() { quickCmd('RESUME_ALL', {}); }
|
|
|
|
|
|
function pLiquidate() { quickCmd('LIQUIDATE_ALL', { confirm: 'YES' }, '【一键清仓】将把全部持仓按最快节奏卖出、不做择时优化。确认?'); }
|
|
|
|
|
|
async function pReduce() { const v = await promptNum('降低敞口(占总规模,%)', '降仓'); if (v != null) quickCmd('REDUCE_EXPOSURE', { pct: v + '%' }); }
|
|
|
|
|
|
async function pIncrease() { const v = await promptNum('提高敞口(占总规模,%)', '升仓'); if (v != null) quickCmd('INCREASE_EXPOSURE', { pct: v + '%' }); }
|
|
|
|
|
|
async function pSectorExit() {
|
|
|
|
|
|
let name; try { name = (await ElementPlus.ElMessageBox.prompt('要清仓的行业名称', '清仓某行业', {})).value; } catch (e) { return; }
|
|
|
|
|
|
if (name) quickCmd('SECTOR_EXIT', { sector: name }, '清仓行业「' + name + '」的全部持仓?');
|
|
|
|
|
|
}
|
|
|
|
|
|
// ===================== /交易员视图 =====================
|
|
|
|
|
|
|
2026-07-27 17:12:09 +08:00
|
|
|
|
|
|
|
|
|
|
async function call(method, url, body) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const r = await api[method](url, body);
|
|
|
|
|
|
if (r.data && r.data.ok === false) { err.value = r.data.error || '请求失败'; }
|
|
|
|
|
|
return r.data;
|
|
|
|
|
|
} catch (e) { err.value = url + ' → ' + e.message; return { ok: false, error: e.message }; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadParams() {
|
|
|
|
|
|
const d = await call('get', '/api/params');
|
|
|
|
|
|
const list = ((d.data || d).params) || [];
|
|
|
|
|
|
params.value = list;
|
|
|
|
|
|
paramsRaw.value = Object.fromEntries(list.map(p => [p.key, p.value]));
|
|
|
|
|
|
}
|
|
|
|
|
|
async function saveParams() {
|
|
|
|
|
|
const items = params.value
|
|
|
|
|
|
.filter(p => String(p.value) !== String(paramsRaw.value[p.key]))
|
|
|
|
|
|
.map(p => ({ key: p.key, value: p.value }));
|
|
|
|
|
|
const d = await call('post', '/api/params', { items });
|
|
|
|
|
|
const bad = (d.results || []).filter(r => !r.ok);
|
|
|
|
|
|
if (bad.length) ElementPlus.ElMessage.error(bad.map(b => b.error).join('; '));
|
|
|
|
|
|
else ElementPlus.ElMessage.success('已保存 ' + items.length + ' 项');
|
|
|
|
|
|
await loadParams(); await loadOverview();
|
|
|
|
|
|
}
|
|
|
|
|
|
async function loadOverview() {
|
|
|
|
|
|
const d = await call('get', '/api/overview'); ov.value = d.data || d || {};
|
|
|
|
|
|
health.value = await call('get', '/health');
|
|
|
|
|
|
}
|
|
|
|
|
|
async function loadCatalog() {
|
|
|
|
|
|
const d = await call('get', '/api/commands/catalog');
|
|
|
|
|
|
catalog.value = ((d.data || d).commands) || [];
|
|
|
|
|
|
}
|
|
|
|
|
|
async function loadCommands() {
|
|
|
|
|
|
const d = await call('get', '/api/commands?limit=60'); commands.value = d.data || [];
|
|
|
|
|
|
}
|
|
|
|
|
|
async function loadPositions() {
|
|
|
|
|
|
const d = await call('get', '/api/positions'); positions.value = d.positions || [];
|
|
|
|
|
|
}
|
|
|
|
|
|
async function loadInstructions() {
|
|
|
|
|
|
const d = await call('get',
|
|
|
|
|
|
'/api/instructions?status=PROPOSED,RULE_PASSED,JUDGE_PASSED,DISPATCHED&limit=100');
|
|
|
|
|
|
instructions.value = d.data || [];
|
|
|
|
|
|
}
|
|
|
|
|
|
async function loadLedger() {
|
|
|
|
|
|
const d = await call('get', '/api/ledger?limit=60'); ledger.value = d.data || [];
|
|
|
|
|
|
}
|
|
|
|
|
|
async function loadProposals() {
|
|
|
|
|
|
const d = await call('get', '/api/proposals'); proposals.value = d.data || [];
|
|
|
|
|
|
}
|
|
|
|
|
|
async function loadPlans(cid) {
|
|
|
|
|
|
plansOf.value = cid;
|
|
|
|
|
|
const d = await call('get', '/api/plans?command_id=' + cid); plans.value = d.data || [];
|
|
|
|
|
|
}
|
|
|
|
|
|
async function loadLots(code) {
|
|
|
|
|
|
lotsOf.value = code;
|
|
|
|
|
|
const d = await call('get', '/api/positions/' + code + '/lots?status='); lots.value = d.data || [];
|
|
|
|
|
|
}
|
2026-07-28 09:10:07 +08:00
|
|
|
|
async function loadDispatchMode() {
|
|
|
|
|
|
const d = await call('get', '/api/dispatch-mode'); dm.value = d.data || d || {};
|
|
|
|
|
|
}
|
2026-07-29 13:51:10 +08:00
|
|
|
|
async function loadWs() {
|
|
|
|
|
|
const d = await call('get', '/api/ws-channel?limit=60');
|
|
|
|
|
|
wsRaw.value = d.data || d || {};
|
|
|
|
|
|
}
|
|
|
|
|
|
// 只在 ws 页签可见时轮询。3 秒一次对一个只读接口不算什么, 但让它在后台一直跑
|
|
|
|
|
|
// 会把日志和连接数刷得很难看, 排查问题时反而碍事。
|
|
|
|
|
|
function wsPollSync() {
|
|
|
|
|
|
const want = tab.value === 'ws' && wsAuto.value;
|
|
|
|
|
|
if (want && !wsTimer) { loadWs(); wsTimer = setInterval(loadWs, 3000); }
|
|
|
|
|
|
if (!want && wsTimer) { clearInterval(wsTimer); wsTimer = null; }
|
|
|
|
|
|
}
|
|
|
|
|
|
watch([tab, wsAuto], wsPollSync);
|
|
|
|
|
|
onUnmounted(() => { if (wsTimer) clearInterval(wsTimer); });
|
|
|
|
|
|
|
2026-07-27 17:12:09 +08:00
|
|
|
|
async function loadAll() {
|
|
|
|
|
|
loading.value = true; err.value = '';
|
|
|
|
|
|
await Promise.all([loadOverview(), loadParams(), loadCatalog(), loadCommands(),
|
2026-07-28 09:10:07 +08:00
|
|
|
|
loadPositions(), loadInstructions(), loadLedger(), loadProposals(),
|
2026-07-29 13:51:10 +08:00
|
|
|
|
loadDispatchMode(), loadWs()]);
|
2026-08-10 15:09:29 +08:00
|
|
|
|
await loadNames();
|
2026-07-27 17:12:09 +08:00
|
|
|
|
loading.value = false;
|
2026-07-29 13:51:10 +08:00
|
|
|
|
wsPollSync();
|
2026-07-27 17:12:09 +08:00
|
|
|
|
}
|
2026-07-28 09:10:07 +08:00
|
|
|
|
async function cancelIns(iid) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await ElementPlus.ElMessageBox.confirm('撤销指令 ' + iid + '?', '确认',
|
|
|
|
|
|
{ type: 'warning' });
|
|
|
|
|
|
} catch (e) { return; }
|
|
|
|
|
|
const d = await call('post', '/api/instructions/' + iid + '/cancel', {});
|
|
|
|
|
|
ElementPlus.ElMessage[(d.ok ? 'success' : 'error')](d.message || d.error);
|
|
|
|
|
|
await Promise.all([loadInstructions(), loadLedger()]);
|
|
|
|
|
|
}
|
2026-07-27 17:12:09 +08:00
|
|
|
|
|
|
|
|
|
|
function onCmdChange() {
|
|
|
|
|
|
form.params = {};
|
|
|
|
|
|
const s = curSpec.value; if (!s) return;
|
|
|
|
|
|
Object.entries(s.fields).forEach(([k, f]) => {
|
|
|
|
|
|
if (f.default !== undefined) form.params[k] = f.default;
|
|
|
|
|
|
if (f.type === 'enum' && f.choices && f.choices.length === 1) form.params[k] = f.choices[0];
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
function buildParams() {
|
|
|
|
|
|
const s = curSpec.value, out = {};
|
|
|
|
|
|
Object.entries(s.fields).forEach(([k, f]) => {
|
|
|
|
|
|
let v = form.params[k];
|
|
|
|
|
|
if (v === undefined || v === null || v === '') return;
|
|
|
|
|
|
out[k] = (f.type === 'pct') ? (String(v).trim() + '%') : v;
|
|
|
|
|
|
});
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
async function issue(force) {
|
|
|
|
|
|
const s = curSpec.value;
|
|
|
|
|
|
if (!s) { ElementPlus.ElMessage.warning('请先选择命令'); return; }
|
|
|
|
|
|
if (s.danger && !force) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await ElementPlus.ElMessageBox.confirm(
|
|
|
|
|
|
'【' + s.label + '】是高危命令, 将对全部持仓生效。确认执行?', '二次确认',
|
|
|
|
|
|
{ type: 'warning', confirmButtonText: '确认执行', cancelButtonText: '取消' });
|
|
|
|
|
|
} catch (e) { return; }
|
|
|
|
|
|
}
|
|
|
|
|
|
issuing.value = true;
|
|
|
|
|
|
const body = { cmd_type: s.cmd_type, params: buildParams(), note: form.note, force: !!force };
|
|
|
|
|
|
const d = await call('post', '/api/commands', body);
|
|
|
|
|
|
issuing.value = false;
|
|
|
|
|
|
if (d.ok) {
|
|
|
|
|
|
ElementPlus.ElMessage.success('命令 ' + d.command_id + ' 已下达 · ' + (d.status || ''));
|
|
|
|
|
|
const notes = ((d.plan || {}).notes || []); if (notes.length)
|
|
|
|
|
|
ElementPlus.ElMessage.info(notes.join(' | '));
|
|
|
|
|
|
err.value = '';
|
|
|
|
|
|
await Promise.all([loadCommands(), loadOverview(), loadPositions(), loadParams()]);
|
|
|
|
|
|
} else if ((d.conflicts || []).length) {
|
|
|
|
|
|
const txt = d.conflicts.map(c => c.with_cmd_type + '(' + c.with_command_id + '): ' + c.reason)
|
|
|
|
|
|
.join('\n');
|
|
|
|
|
|
try {
|
|
|
|
|
|
await ElementPlus.ElMessageBox.confirm(
|
|
|
|
|
|
'与在途命令冲突:\n' + txt + '\n\n命令冲突由用户裁决 —— 仍要并行下达?', '命令冲突',
|
|
|
|
|
|
{ type: 'warning', confirmButtonText: '仍然下达', cancelButtonText: '放弃' });
|
|
|
|
|
|
await issue(true);
|
|
|
|
|
|
} catch (e) { /* 用户放弃 */ }
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElementPlus.ElMessage.error((d.errors || [d.error]).join('; '));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
async function cancelCmd(cid) {
|
|
|
|
|
|
const d = await call('post', '/api/commands/' + cid + '/cancel');
|
|
|
|
|
|
ElementPlus.ElMessage[(d.ok ? 'success' : 'error')](d.message || d.error);
|
|
|
|
|
|
await loadCommands();
|
|
|
|
|
|
}
|
|
|
|
|
|
async function replan(cid) {
|
|
|
|
|
|
const d = await call('post', '/api/commands/' + cid + '/replan');
|
|
|
|
|
|
ElementPlus.ElMessage[(d.ok ? 'success' : 'error')](d.ok ? '已重新生成方案' :
|
|
|
|
|
|
(d.errors || [d.error]).join('; '));
|
|
|
|
|
|
await loadCommands(); if (plansOf.value === cid) await loadPlans(cid);
|
|
|
|
|
|
}
|
|
|
|
|
|
async function decide(row, decision) {
|
|
|
|
|
|
const d = await call('post', '/api/proposals/' + row.proposal_id + '/decide', { decision });
|
|
|
|
|
|
if (d.ok) ElementPlus.ElMessage.success(decision === 'ACCEPTED'
|
|
|
|
|
|
? ('已采纳, 指令 ' + d.instruction_id) : '已驳回');
|
|
|
|
|
|
else ElementPlus.ElMessage.error(d.error);
|
|
|
|
|
|
await Promise.all([loadProposals(), loadInstructions(), loadLedger()]);
|
|
|
|
|
|
}
|
|
|
|
|
|
async function ops(name) {
|
|
|
|
|
|
opsLoading.value = true;
|
|
|
|
|
|
const d = await call('post', '/api/ops/' + name);
|
|
|
|
|
|
opsResult.value = JSON.stringify(d, null, 2); opsLoading.value = false;
|
|
|
|
|
|
await loadAll();
|
|
|
|
|
|
}
|
|
|
|
|
|
async function loadSchema() {
|
|
|
|
|
|
opsLoading.value = true;
|
|
|
|
|
|
const d = await call('get', '/api/ops/downstream-schema');
|
|
|
|
|
|
opsResult.value = JSON.stringify(d, null, 2); opsLoading.value = false;
|
|
|
|
|
|
}
|
2026-07-30 16:21:16 +08:00
|
|
|
|
async function loadPlan() {
|
|
|
|
|
|
planLoading.value = true;
|
|
|
|
|
|
const d = await call('get', '/api/upstream/plan?limit=50');
|
|
|
|
|
|
planRaw.value = (d && d.ok) ? d : { status: { ok: false, hint: (d||{}).error || '取数失败' } };
|
|
|
|
|
|
planLoading.value = false;
|
|
|
|
|
|
}
|
2026-07-31 13:28:10 +08:00
|
|
|
|
async function loadChanges() {
|
|
|
|
|
|
chgLoading.value = true;
|
|
|
|
|
|
const d = await call('get', '/api/upstream/plan-changes?limit=50');
|
|
|
|
|
|
chgRaw.value = (d && d.ok) ? d
|
|
|
|
|
|
: { changes: { ok: false, hint: (d || {}).error || '取数失败' }, log: {} };
|
|
|
|
|
|
chgLoading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
async function openPlan() { planDrawer.value = true; await Promise.all([loadPlan(), loadChanges()]); }
|
2026-07-30 16:21:16 +08:00
|
|
|
|
async function refreshPlan() {
|
|
|
|
|
|
planLoading.value = true;
|
|
|
|
|
|
const d = await call('post', '/api/ops/plan-refresh');
|
|
|
|
|
|
planResult.value = JSON.stringify(d, null, 2);
|
|
|
|
|
|
planLoading.value = false;
|
2026-07-31 13:28:10 +08:00
|
|
|
|
await Promise.all([loadPlan(), loadChanges(), loadOverview()]);
|
2026-07-30 16:21:16 +08:00
|
|
|
|
}
|
2026-07-27 17:12:09 +08:00
|
|
|
|
async function openReport() {
|
|
|
|
|
|
const d = await call('get', '/api/report'); report.value = d.data || d || {};
|
|
|
|
|
|
reportDrawer.value = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 16:21:16 +08:00
|
|
|
|
onMounted(() => { loadAll(); loadPlan(); }); // 计划状态并行拉, 不阻塞首屏
|
2026-07-27 17:12:09 +08:00
|
|
|
|
return { tab, loading, err, health, ov, params, catalog, commands, plans, plansOf,
|
|
|
|
|
|
positions, lots, lotsOf, instructions, ledger, proposals, report, reportDrawer,
|
2026-07-28 09:10:07 +08:00
|
|
|
|
opsDrawer, opsResult, opsLoading, issuing, form, curSpec, dirtyCount, dm,
|
|
|
|
|
|
money, pct, groupLabel, fieldLabel, stTag, cuTag, canCancel, progPct, insPct,
|
2026-07-30 13:02:51 +08:00
|
|
|
|
scaleGap,
|
2026-07-29 13:51:10 +08:00
|
|
|
|
wsRaw, wsAuto, wsHidePong, wsc, wst, wsMode, wsOrders, wsInbox, wsWarnings, loadWs,
|
2026-07-27 17:12:09 +08:00
|
|
|
|
loadAll, loadParams, saveParams, loadPlans, loadLots, onCmdChange, issue,
|
2026-07-30 16:21:16 +08:00
|
|
|
|
cancelCmd, replan, decide, ops, loadSchema, openReport, cancelIns,
|
|
|
|
|
|
planDrawer, planLoading, planStatus, planRows, planCand, planResult,
|
2026-07-31 13:28:10 +08:00
|
|
|
|
loadPlan, openPlan, refreshPlan,
|
|
|
|
|
|
chgRaw, chgSt, chgLoading, chgTab, chg, chgHeld, chgLog, chgRevised, chgWatch,
|
2026-08-10 15:09:29 +08:00
|
|
|
|
loadChanges,
|
|
|
|
|
|
mode, tx, nm, fullName, nameMap, attention, heldPositions, traderParams,
|
|
|
|
|
|
propWhy, nextStep, scrollTo, actExitStock, actReduceStock, rowMore,
|
|
|
|
|
|
pHaltBuy, pResumeBuy, pHaltAll, pResumeAll, pReduce, pIncrease, pLiquidate, pSectorExit };
|
2026-07-27 17:12:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
}).use(ElementPlus).mount('#app');
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|