修改前端页面
This commit is contained in:
parent
1d003eee1a
commit
2e2a055dab
|
|
@ -0,0 +1,261 @@
|
|||
# PMS 前端「交易员视图」改造方案 v1.1
|
||||
|
||||
> 2026-08-10 起草,v1.1 并入用户四点反馈与「站在交易员角度」的重排。
|
||||
> 落地方向(已拍板):六块全套 + 同一单页加「交易员/运维」开关、默认交易员,运维模式保留现控制台。
|
||||
> 纪律:只做加法(不碰后端引擎与既有 API 行为)、可读性优先(完整中文句、不摆裸键/裸码/表名)、
|
||||
> 按交易员任务组织而非按后端模块。
|
||||
>
|
||||
> **v1.1 相对 v1 的四处变化**(详见文内标注):
|
||||
> 1. 配色定为 A 股习惯(红涨绿跌)。
|
||||
> 2. 股票中文名取自 153 表 `gp_code_all`(`gp_code_two = ts_code` → `gp_name`)。
|
||||
> 3. **命令重新归类**:多数命令是交易员动作(清仓某只/暂停买入/设止损/一键清仓),下沉到持仓行内与
|
||||
> 「组合操作」块;只有调度类运维按钮(回放/对账/日终/exec-tick/扫描…)才进运维模式。
|
||||
> 4. 翻译表全部改用**从代码核实**的真实枚举(DDL + cushion.py),措辞贴交易员语境,未登记的码显示原码并告警,不臆造。
|
||||
|
||||
---
|
||||
|
||||
## 一、目标与判收
|
||||
|
||||
**最终检验**:交易员打开页面,不需记任何内部词汇,就看清四件事——手里有什么、系统想干什么以及为什么、
|
||||
此刻要我拍板的是哪几条、今天发生与在办的是什么;并能安全完成交易员该做的动作(采纳/驳回提议、
|
||||
对某只票清/减/设止损、暂停买入、一键清仓、撤在办委托、调高层旋钮)。运维机制原样保留但不挡在面前。
|
||||
|
||||
**输出级判收(五条,任一不满足即未判收)**:交易员视图的首屏与每块里——
|
||||
|
||||
1. 不出现任何 `PMS_` 开头的裸参数键;
|
||||
2. 不出现任何数据库表名(`pms_qmt_order`、`pms_action_ledger` 之类);
|
||||
3. 不出现任何裸状态码(`WAIT_USER` / `EXECUTING` / `REJECT` / `full` 之类);
|
||||
4. 每一条提议、每一条裁决、每一个动作按钮都带一句**完整中文说明**(提议/裁决给理由,按钮给「点了会怎样」);
|
||||
5. 每个数字都有单位和中文标签。
|
||||
|
||||
平行判收:**运维模式里现控制台的每个按钮、每张表都还在,行为一字未变。**
|
||||
|
||||
---
|
||||
|
||||
## 二、现状定位(以代码为准)
|
||||
|
||||
前端是单页 `app/web/static/index.html`(Vue3 + ElementPlus,1088 行)+ `app/web/main.py`(FastAPI 托管,
|
||||
所有接口走 `ok()` 包装、失败也回 HTTP 200 由顶部横幅提示)。它面向搭系统的人:参数区摆裸键
|
||||
(`PMS_TOTAL_SCALE`)、分区标题印表名(「出口委托 (pms_qmt_order)」)、一排调度类运维按钮
|
||||
(回放/对账/日终/materialize/exec-tick/scan-proposals/digest-signals/清 resync)、状态全是裸码、
|
||||
按后端模块而非交易员任务组织。
|
||||
|
||||
**关键发现:翻译材料与动作命令基本都现成。** 命令目录 `command_spec.SPECS` 每条自带中文 `label`
|
||||
(`EXIT_STOCK`→清仓某股、`HALT_BUY`→全局暂停买入、`SET_STOP_PRICE`→设定某股止损价,还带
|
||||
`danger`/`instant`/`disabled_reason`);参数中心 `param_store` 每键配了中文说明;提议表 `pms_proposal`
|
||||
自带 `judge_verdict`/`judge_reason`;账本 `reason` 是现成文本。**缺的是把裸 token 换人话的一层薄映射 +
|
||||
按任务重排 + 把交易员动作从运维堆里拎出来。** 因此本次是纯加法的呈现层,后端引擎与既有 API 不动。
|
||||
|
||||
---
|
||||
|
||||
## 三、设计原则
|
||||
|
||||
1. **只做加法。** 后端引擎、状态机语义、既有 API 入参出参一律不改;交易员视图消费现有接口。
|
||||
2. **可读性优先。** 裸键→中文标签+说明;裸码→中文短语;表名不上台面;理由用完整自然句。
|
||||
3. **任务导向。** 按「交易员此刻要做什么」分块;命令按「作用在某只票 / 作用在整个组合」就近摆放,
|
||||
而不是集中在一个「命令台」里让人查手册。
|
||||
4. **分层可见。** 交易员默认视图 + 运维高级模式一个开关切换;危险动作(一键清仓/暂停/force 对账/清 resync)
|
||||
加二次确认,弹窗写清后果。
|
||||
5. **翻译单一事实源。** 所有 token→人话映射集中一处,全页共用,杜绝措辞打架。配色按 A 股习惯:**红涨绿跌**。
|
||||
|
||||
---
|
||||
|
||||
## 四、总体形态
|
||||
|
||||
同一单页,顶部「交易员 / 运维」切换,默认交易员。两套视图共用同一批后端接口,差别只在呈现:
|
||||
|
||||
- **交易员视图**:六块(下一节),只读接口 + 现有的少量写接口(采纳/驳回提议、下命令、撤在办委托、改参数),
|
||||
全部经翻译层渲染、按任务就近摆放。
|
||||
- **运维模式**:现控制台原样搬过来(**调度类**运维按钮、裸 `cmd_type` 命令台表单、通道明细、
|
||||
出口委托/上行消息原始表、全量参数),危险动作加二次确认。
|
||||
|
||||
**后端改动至多两处只读加法**:① `GET /api/labels` 吐翻译字典(或字典写死前端);② 一个批量「代码→中文名」
|
||||
只读查询(见 §5 名称映射)。不新增写接口、不改既有接口。部署仍 `make deploy` + `make test`。
|
||||
|
||||
---
|
||||
|
||||
## 五、六块逐块设计(数据来源锚到真实字段)
|
||||
|
||||
> 股票中文名映射(全块共用):PMS 用点式 `ts_code`(如 `688280.SH`)。**取名走 153 表 `gp_code_all`**,
|
||||
> 连接键 `gp_code_all.gp_code_two = ts_code` → 取 `gp_name`(简称,页面主显)与 `fullname`(全称,悬浮显示);
|
||||
> `gp_code_two` 缺时用 `gp_code`(`SH688280` 形)经 `command_spec.normalize_code` 归一兜底。落地=复用现有
|
||||
> 153 连接(`downstream_repo` 已在读 `gp_` 表)加一个 `name_map(codes)` 批量只读、按日缓存。
|
||||
> **注意**:该表 `industry` 列示例为 NULL,不拿它当行业源——行业仍走现有 `industry` 服务;这里只取名称。
|
||||
|
||||
### 1. 今日一览 + 需要注意(顶部)
|
||||
|
||||
- **给谁看**:一眼掌握账户全局,并把「今天有什么不对/降级」用人话摆出来。
|
||||
- **数据来源**:`GET /api/overview`(`portfolio.overview()`)+ 提议条数 + `GET /api/dispatch-mode` / `/api/ws-channel`(通道)。
|
||||
- **一览字段映射**:
|
||||
|
||||
| 接口字段 | 显示 |
|
||||
|---|---|
|
||||
| `total_asset`、`cash_avail`(`cash_source=ws`) | 账户总资产、可用资金(标「实时」);`estimate` 时标「估算」并置灰 |
|
||||
| `portfolio_mv`、`portfolio_pct` | 持仓市值、仓位占比 |
|
||||
| `float_pnl`、`float_pnl_pct` | 当日浮动盈亏(**红涨绿跌**) |
|
||||
| `names_count`/`max_names` | 持仓 N 只 / 上限 M 只 |
|
||||
| 提议条数 | **等我拍板 N 条**(可点跳第 2 块) |
|
||||
| `autonomy` | 自主档位:全自动 / 只提议不下手 / 暂停 |
|
||||
|
||||
- **需要注意条**(有才显示,各一句人话):`buy_halt`/`exec_halt`/`brake_until`→「已暂停买入 / 休假模式 /
|
||||
组合刹车中(至 X 日)」;`price_missing`→「N 只取不到现价(停牌等),其盈亏与安全垫暂不可用」;
|
||||
研判 `UNAVAILABLE` 占比高→「研判暂不可用,多只提议降级为需你确认」;通道 `conn_state≠ONLINE` 或
|
||||
`resync_flag=1`→「行情/成交通道断开 / 待人工对账」。这些让交易员知道「系统今天为什么不太动」。
|
||||
|
||||
### 2. 等我拍板(提议卡片,最高频,重心)
|
||||
|
||||
- **数据来源**:`GET /api/proposals?status=WAIT_USER`(`pms_repo.list_proposals`,含解析好的 `hard_numbers`、
|
||||
以及**提议自带的** `judge_verdict`/`judge_reason`)。
|
||||
- **每张卡片**:
|
||||
|
||||
| 来源字段 | 显示 |
|
||||
|---|---|
|
||||
| `ts_code` | 股票简称 + 代码(`gp_name`;悬浮全称 `fullname`) |
|
||||
| `action` | 中文动作(见翻译表:建新仓/加仓/补仓/换仓…) |
|
||||
| `judge_reason` | 研判意见原句(完整中文),如「站上关键位、量比放大,驱动仍成立」 |
|
||||
| `hard_numbers`(价/分/主题/档位/预期空间/热度/名次) | 一句人话理由 + 关键数字,如「候选榜第 3、强传导主题『算力』、预期空间 +42%、当前尚未过热」 |
|
||||
| `qty` × `hard_numbers.price` | 数量 / 参考价 / 约需金额 |
|
||||
| `expire_at` | 有效期(还剩 X 小时,过期自动作废) |
|
||||
|
||||
- **动作**:「采纳」「驳回」→ `POST /api/proposals/{id}/decide`;旁注「采纳=记一笔待执行指令,
|
||||
由择时在窗口内择机下单;驳回=今日不再提这只」。**采纳后端回的 `warning`(一次性守卫没锁上)原样弹出,不吞。**
|
||||
|
||||
### 3. 我的持仓(每行带行内快捷动作)
|
||||
|
||||
- **数据来源**:`GET /api/positions`(`portfolio.positions_view()`);展开 `GET /api/positions/{code}/lots` +
|
||||
`GET /api/ledger?ts_code=<code>`。
|
||||
- **行字段映射**:
|
||||
|
||||
| 来源字段 | 显示 |
|
||||
|---|---|
|
||||
| `ts_code` | 简称 + 代码 |
|
||||
| `price`/`price_ok` | 现价;`price_ok=false` 标「估价(停牌/无分钟线)」,该行盈亏与安全垫标不可用 |
|
||||
| `avg_cost`、`market_value`、`pct_of_scale` | 摊薄成本、市值、占规模比例 |
|
||||
| `price/avg_cost−1` | 浮动盈亏(**红涨绿跌**) |
|
||||
| `cushion_state`/`cushion_pct`/`cushion_peak` | 安全垫:浮亏无垫 / 薄垫 / 厚垫 + 当前垫幅 + 峰值 |
|
||||
| `neg_cushion_days` | 安全垫连续为负 N 日(清弱票判据提示) |
|
||||
| `status`、`frozen_reason` | 持仓状态(持有中/建仓中/清仓中…);冻结时标「已冻结(禁增持,卖出不受影响)」 |
|
||||
| `stop_ref`/目标价/`ref_source` | 止损价 / 目标价(用户设定标「手动」) |
|
||||
| 「下一步」 | 由 `cushion_state`+参数合成人话:「浮盈已过厚垫线,达标可加仓」「浮亏进入补仓评估档,深档需你确认」 |
|
||||
|
||||
- **行内快捷动作**(**v1.1 新增**,就近摆放,下命令走现有 `POST /api/commands`):清仓某只(`EXIT_STOCK`)、
|
||||
减至 X%(`REDUCE_STOCK`)、设止损价(`SET_STOP_PRICE`)、设目标价(`SET_TARGET_PRICE`)、
|
||||
冻结/解冻(`FREEZE_STOCK`/`UNFREEZE_STOCK`)、拉黑(`BLACKLIST_ADD`)。每个按钮旁注后果;卖出类给轻确认。
|
||||
- **展开区**:批次(底仓/回踩补足/盈利加仓/补仓/做T 各多少股,`lot_type` 见翻译表)+ 该股账本时间线(整句化)。
|
||||
|
||||
### 4. 今日在办与动向(**v1.1 强化:先看在办,再看已发生**)
|
||||
|
||||
- **在办(前瞻)**:`GET /api/instructions`(在途指令:要买/卖什么、限价、窗口、进度)+ `GET /api/ws-channel`
|
||||
的出口委托(`pms_qmt_order`,枚举简化:排队/已报/部分成交/已成/已撤/过期/被拒)。每条可**撤**
|
||||
(`POST /api/instructions/{id}/cancel`)。让交易员看清「系统今天正打算做什么、我能不能拦」。
|
||||
- **已发生(回溯)**:`GET /api/ledger` + 成交合并成人话时间线:「10:32 XX:研判驳回建新仓——证据不足」
|
||||
「10:35 你采纳了 XX 的建仓提议」「14:46 XX 成交 1700 股 @34.71」。
|
||||
|
||||
### 5. 组合操作 + 设置(**v1.1:把交易员级组合命令从运维里拎出来**)
|
||||
|
||||
- **组合操作**(下命令走 `POST /api/commands`,危险项二次确认):暂停买入/恢复买入(`HALT_BUY`/`RESUME_BUY`)、
|
||||
休假模式/恢复(`HALT_ALL`/`RESUME_ALL`)、**一键清仓(紧急)**(`LIQUIDATE_ALL`,`danger`,二次确认+输 YES)、
|
||||
降仓/升仓(`REDUCE_EXPOSURE`/`INCREASE_EXPOSURE`)、清仓某行业/限行业上限(`SECTOR_EXIT`/`SECTOR_CAP`,
|
||||
行业源未就绪时置灰并给 `disabled_reason`)。每个按钮旁用 `command_spec` 里现成的 `note` 说明后果。
|
||||
- **设置**(`GET/POST /api/params`,标签用 `param_store` 现成中文说明):总操作规模、总仓上限、单股上限、
|
||||
最大持仓只数、预留现金比例、自主档位(中文枚举下拉)。改「自主档位/总仓上限」这类会触发降仓提议的,改完给提示。
|
||||
其余几十个参数留运维模式。`SECRET_KEYS`(签名种子)本就不入库不显示,保持不动。
|
||||
|
||||
### 6. 运维模式(开关切过去,只留**调度/联调**这类)
|
||||
|
||||
- **内容**:调度类运维按钮(回放 replay、对账 reconcile、日终 daily-settle、materialize、exec-tick、
|
||||
sweep-windows、scan-proposals、digest-signals、premarket、build-report、rebuild-preflight/accept、
|
||||
清 resync、plan-refresh、downstream-schema)、裸 `cmd_type` 命令台表单(`GET /api/commands/catalog` 已带中文
|
||||
label,交易员平时用第 3/5 块的按钮,这里保留给联调)、通道明细(连接/seq 水位/出口队列/上行消息)、
|
||||
出口委托与上行消息原始表、全量参数、行业映射导入。
|
||||
- **加固(只加不改)**:`danger=true`、`HALT_*`、`reconcile?force=true`、`clear-resync` 一律二次确认。接口本身不动。
|
||||
|
||||
> 可选(v1 收尾或 v2):**候选/关注**——`GET /api/upstream/plan`(今日选股计划主榜/观察档)给交易员一个
|
||||
> 「系统在盯哪些、为什么会冒出这些提议」的上下文。默认折叠,不占首屏。
|
||||
|
||||
---
|
||||
|
||||
## 六、翻译层(从代码核实,未登记即显原码并告警)
|
||||
|
||||
**动作 `action`**(DDL `pms_plan`/`pms_qmt_order.intent` + `cushion.py` + `PMS_BATCH_SPLIT` 语义核实)
|
||||
|
||||
| 码 | 交易员看到的 | 依据 |
|
||||
|---|---|---|
|
||||
| OPEN | 建新仓 | 首次建仓 |
|
||||
| FILL | 回踩补足 | 建仓期按批把仓位补到目标(非「建满」) |
|
||||
| ADD | 盈利加仓 | 安全垫达厚垫(SOLID)才解锁 |
|
||||
| DCA | 浮亏补仓 | 跌到评估档补仓(深档需确认) |
|
||||
| TRIM | 保垫减仓 | 垫子回吐过半的止盈减仓 |
|
||||
| EXIT | 清仓 | |
|
||||
| T0 / T0_ROUND | 做T(日内) | 收盘应归零 |
|
||||
| SWITCH | 换仓 | 见 `PMS_JUDGE_ACTIONS`(研判侧动作),执行层少见,出现即按此显示 |
|
||||
| HALT | 停手(内部标记) | 方案层动作,一般不面向交易员 |
|
||||
|
||||
**状态类(全部取自 DDL 列注释)**
|
||||
|
||||
| 域 | 码 → 人话 |
|
||||
|---|---|
|
||||
| 提议 `pms_proposal.status` | WAIT_USER 等你确认 / ACCEPTED 已采纳 / DECLINED 已驳回 / EXPIRED 已过期 |
|
||||
| 指令 `pms_instruction.status` | PROPOSED 待校验 / RULE_PASSED 规则闸已过 / JUDGE_PASSED 研判已过 / DISPATCHED 已下发(挂单中) / CONFIRMED 已成交 / REJECTED 被拒 / EXPIRED 到期作废 / CANCELLED 已撤销 |
|
||||
| 委托 `pms_qmt_order.status` | QUEUED 排队 / SENDING·SENT 已报 / PARTIAL 部分成交 / FILLED 已成 / CANCELLED 已撤 / EXPIRED 过期 / REJECTED 被拒 / SEND_FAILED·ABORTED 未发出 |
|
||||
| 命令 `pms_command.status` | 任务:PENDING 待处理 / PLANNING 规划中 / EXECUTING 执行中 / PARTIAL 部分完成(顺延) / DONE 已完成 / CANCELLED 已撤销;参数:EFFECTIVE 生效中 / SUPERSEDED 已被覆盖 |
|
||||
| 方案 `pms_plan.status` | PENDING 待执行 / GATED 待解锁(动作引擎未放行) / EXECUTING 执行中 / DONE 已完成 / PARTIAL 部分完成 / CANCELLED 已撤销 |
|
||||
| 持仓 `pms_position.status` | PLANNED 计划建仓 / OPENING 建仓中 / HOLDING 持有中 / EXITING 清仓中 / CLOSED 已清仓 |
|
||||
| 冻结 `frozen_reason` | NONE 未冻结 / COMMAND_HALT 命令冻结(禁增持) / BRAKE 组合刹车冻结 / MANUAL 手动冻结 |
|
||||
| 安全垫 `cushion_state` | NONE 浮亏无垫 / THIN 薄垫 / SOLID 厚垫 |
|
||||
| 批次 `lot_type` | BASE 底仓 / FILL 回踩补足 / ADD 盈利加仓 / DCA 补仓 / T0 做T仓 / RECON 对账调整 |
|
||||
| 裁决 `arbiter`/`verdict` | rule 规则闸 / judge 研判闸 / user 你;PASS 放行 / REJECT 驳回 / NOTE 记录(信号留痕) / UNAVAILABLE 研判暂不可用 |
|
||||
| 现价来源 `cash_source`/参考位 `ref_source` | ws 实时(账户快照) / estimate 估算;bionic 决策系统 / self_calc 兜底自算 / user 你设定 |
|
||||
| 自主档位 `autonomy` | full 全自动 / propose_only 只提议不下手 / off 暂停 |
|
||||
| 通道 `conn_state`/`resync_flag` | ONLINE 已连接 / OFFLINE 断开 / CONNECTING 连接中 / STOPPED 已停;resync=1 待人工全量对账 |
|
||||
|
||||
> `verdict` 的 `NOTE`/`UNAVAILABLE` 是 DDL 注释(PASS/REJECT)之外、由信号/研判路径后加的实际取值,
|
||||
> 已在此登记。**翻译层遇到未登记的码:显示原码 + 一条告警**,绝不静默套错文案(防「显示得像对的」)。
|
||||
> 命令与参数标签直接用 `command_spec.list_commands()` 的 `label`/`note` 与 `param_store` 的中文说明,不自造。
|
||||
|
||||
---
|
||||
|
||||
## 七、落地路径(只做加法)
|
||||
|
||||
1. **前端**:`index.html` 顶部加「交易员/运维」切换;新增六块交易员组件,调用 §5 列出的现有接口;
|
||||
新增翻译字典模块(或拉 `GET /api/labels`);行内/组合动作复用现有 `POST /api/commands`。现有各块整体归入运维模式。
|
||||
2. **后端(至多两处只读加法)**:`GET /api/labels`(翻译字典,或写死前端);`name_map(codes)` 批量代码→中文名
|
||||
(复用 153 连接读 `gp_code_all`,按日缓存)。**不新增写接口、不改既有接口行为。**
|
||||
3. **部署**:源码打进镜像 → `make deploy`;`make test` 见 ALL SUITES PASS(前端不影响 469 例,按纪律照跑)。
|
||||
4. **离线兜底**:沿用 `static/vendor/`,新组件不加外网依赖。
|
||||
|
||||
---
|
||||
|
||||
## 八、判收清单(实机逐条对)
|
||||
|
||||
**交易员视图(五条输出级)**:首屏与六块搜不到裸 `PMS_` 键 / 搜不到表名 / 搜不到裸状态码 /
|
||||
每条提议·裁决·动作按钮都有一句完整中文 / 每个数字有单位与标签。
|
||||
|
||||
**动作走通**:采纳·驳回提议、行内清仓/减仓/设止损、组合暂停买入/一键清仓(二次确认)、撤在办委托——
|
||||
各走一遍,账本与指令落表与改造前一致。
|
||||
|
||||
**运维模式功能不减**:调度类按钮、裸命令台、通道明细、出口/上行原始表、全量参数逐一还在;
|
||||
危险动作弹二次确认;抽三个运维接口返回与改造前一致。
|
||||
|
||||
**回归**:`make test` ALL SUITES PASS。
|
||||
|
||||
---
|
||||
|
||||
## 九、边界与不做什么
|
||||
|
||||
- 不碰后端引擎、状态机语义、闸门逻辑;交易员视图只读 + 复用已有写接口。
|
||||
- 危险动作只在前端加确认,后端行为不动。
|
||||
- 单用户自用,v1 不做权限/角色体系(交易员/运维是同一人的两个视图,非权限隔离)。
|
||||
- 移动端适配、图表美化、候选/关注块留 v2(或 v1 收尾)。
|
||||
|
||||
---
|
||||
|
||||
## 十、已定与待办
|
||||
|
||||
**本轮已定(用户 08-10 反馈)**:① 红涨绿跌;② 中文名走 `gp_code_all`(`gp_code_two=ts_code→gp_name`);
|
||||
③ 命令按交易员/运维重分类(本稿 §5);④ 翻译措辞贴交易员语境、枚举以代码为准(本稿 §6)。
|
||||
|
||||
**仍想请你过一眼**:
|
||||
1. §5 第 3、5 块的**行内动作与组合动作清单**是不是你要的那几个(清/减/止损/冻结/拉黑;暂停/休假/一键清仓/降升仓/行业)——多退少补你定。
|
||||
2. §6 翻译表的**中文用词**逐条可改;你点头后写死进翻译层。
|
||||
3. 「候选/关注」块放 v1 收尾还是 v2。
|
||||
|
|
@ -288,3 +288,59 @@ def probe_category(ts_code: str) -> dict:
|
|||
def fetch_sector_from_category(ts_code: str):
|
||||
"""行业适配器 gp_stock_category (映射表就绪前的临时数据源 —— 设计 §5)。"""
|
||||
return probe_category(ts_code)["value"]
|
||||
|
||||
|
||||
# ================================================================ gp_code_all (中文名映射)
|
||||
# 只读, 严格单表 (session.assert_single_table 不许 JOIN/逗号连表): 名称走 153 代理的
|
||||
# gp_code_all。gp_code_two 是点式 (688280.SH, 与 PMS 内部一致), gp_code 是前缀式
|
||||
# (SH688280) —— 两列都匹配一遍, 兜住个别行 gp_code_two 为空。表里 industry 列不可靠
|
||||
# (示例为 NULL), 不在这里取行业, 行业仍走 fetch_sector_from_category。
|
||||
_NAME_CACHE = {} # 点式 ts_code -> {"name","full"}
|
||||
_NAME_AT = {} # 点式 ts_code -> 写入时刻 epoch
|
||||
_NAME_TTL = 6 * 3600 # 名称基本不变, 缓存 6 小时, 免得每次刷新都撞库
|
||||
|
||||
|
||||
def fetch_names(codes) -> dict:
|
||||
"""代码 → {"name": 简称, "full": 全称}。取不到的代码退回 name=代码本身。"""
|
||||
import time
|
||||
now = time.time()
|
||||
dots, out = [], {}
|
||||
for c in (codes or []):
|
||||
dot = to_dot(str(c or ""))
|
||||
if not dot:
|
||||
continue
|
||||
hit = _NAME_CACHE.get(dot)
|
||||
if hit is not None and now - _NAME_AT.get(dot, 0) < _NAME_TTL:
|
||||
out[dot] = hit
|
||||
else:
|
||||
dots.append(dot)
|
||||
dots = list(dict.fromkeys(dots))
|
||||
if not dots:
|
||||
return out
|
||||
prefixes = [to_prefix(x) for x in dots]
|
||||
binds, dkeys, pkeys = {}, [], []
|
||||
for i, (dot, pre) in enumerate(zip(dots, prefixes)):
|
||||
binds["d%d" % i] = dot
|
||||
dkeys.append(":d%d" % i)
|
||||
binds["p%d" % i] = pre
|
||||
pkeys.append(":p%d" % i)
|
||||
sql = ("SELECT gp_code, gp_code_two, gp_name, fullname FROM gp_code_all "
|
||||
"WHERE gp_code_two IN (%s) OR gp_code IN (%s)"
|
||||
% (", ".join(dkeys), ", ".join(pkeys)))
|
||||
found = {}
|
||||
try:
|
||||
for r in fetch_all(sql, binds):
|
||||
code = to_dot(str(r.get("gp_code_two") or r.get("gp_code") or ""))
|
||||
if not code:
|
||||
continue
|
||||
nm = str(r.get("gp_name") or "").strip()
|
||||
found[code] = {"name": nm or code, "full": str(r.get("fullname") or "").strip()}
|
||||
except Exception:
|
||||
# 库不可达 / 表不在: 本轮退回代码本身, **不写缓存** (下次重试)
|
||||
return {**out, **{dot: {"name": dot, "full": ""} for dot in dots}}
|
||||
for dot in dots:
|
||||
rec = found.get(dot) or {"name": dot, "full": ""}
|
||||
_NAME_CACHE[dot] = rec
|
||||
_NAME_AT[dot] = now
|
||||
out[dot] = rec
|
||||
return out
|
||||
|
|
|
|||
|
|
@ -110,6 +110,13 @@ def api_overview():
|
|||
return ok(portfolio.overview)
|
||||
|
||||
|
||||
@app.get("/api/names")
|
||||
def api_names(codes: str = Query("")):
|
||||
"""代码 → 中文名 (gp_code_all)。codes 逗号分隔点式代码。绝不抛错 —— 交易员视图靠它显示中文名。"""
|
||||
cs_list = [c.strip() for c in (codes or "").split(",") if c.strip()]
|
||||
return ok(lambda: {"ok": True, "names": downstream_repo.fetch_names(cs_list)})
|
||||
|
||||
|
||||
# ================================================================ ① 参数设置
|
||||
@app.get("/api/params")
|
||||
def api_params():
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@
|
|||
.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; }
|
||||
.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;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -44,19 +48,23 @@
|
|||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<el-tag type="warning" effect="dark" size="small">自主档位 {{ ov.autonomy || '-' }}</el-tag>
|
||||
<el-tag :type="dm.shadow ? 'info':'success'" effect="dark" size="small">
|
||||
<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'">
|
||||
下发 {{ dm.mode || '-' }}</el-tag>
|
||||
<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>
|
||||
<el-button size="small" @click="openReport">日报</el-button>
|
||||
<el-button size="small" @click="opsDrawer=true">运维</el-button>
|
||||
<el-button size="small" v-show="mode==='ops'" @click="opsDrawer=true">运维</el-button>
|
||||
<el-button size="small" type="primary" :loading="loading" @click="loadAll">刷新</el-button>
|
||||
</div>
|
||||
|
||||
|
|
@ -80,6 +88,190 @@
|
|||
<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>
|
||||
|
||||
<!-- ==================== 交易员视图 ==================== -->
|
||||
<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'">
|
||||
<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>
|
||||
|
|
@ -508,6 +700,8 @@
|
|||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 运维抽屉 -->
|
||||
<el-drawer v-model="opsDrawer" title="运维操作 (与调度器同一份实现)" size="46%">
|
||||
<div class="muted" style="margin-bottom:10px">
|
||||
|
|
@ -759,6 +953,7 @@ const api = axios.create({ timeout: 60000 });
|
|||
createApp({
|
||||
setup() {
|
||||
const tab = ref('cmd'), loading = ref(false), err = ref('');
|
||||
const mode = ref('trader');
|
||||
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('');
|
||||
|
|
@ -870,6 +1065,144 @@ createApp({
|
|||
const q = Number(r.qty || 0), e = Number(r.exec_qty || 0);
|
||||
return q > 0 ? Math.min(100, Math.round(e / q * 100)) : 0;
|
||||
};
|
||||
// ===================== 交易员视图:翻译层 + 名称 + 动作 =====================
|
||||
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 + '」的全部持仓?');
|
||||
}
|
||||
// ===================== /交易员视图 =====================
|
||||
|
||||
|
||||
async function call(method, url, body) {
|
||||
try {
|
||||
|
|
@ -950,6 +1283,7 @@ createApp({
|
|||
await Promise.all([loadOverview(), loadParams(), loadCatalog(), loadCommands(),
|
||||
loadPositions(), loadInstructions(), loadLedger(), loadProposals(),
|
||||
loadDispatchMode(), loadWs()]);
|
||||
await loadNames();
|
||||
loading.value = false;
|
||||
wsPollSync();
|
||||
}
|
||||
|
|
@ -1080,7 +1414,10 @@ createApp({
|
|||
planDrawer, planLoading, planStatus, planRows, planCand, planResult,
|
||||
loadPlan, openPlan, refreshPlan,
|
||||
chgRaw, chgSt, chgLoading, chgTab, chg, chgHeld, chgLog, chgRevised, chgWatch,
|
||||
loadChanges };
|
||||
loadChanges,
|
||||
mode, tx, nm, fullName, nameMap, attention, heldPositions, traderParams,
|
||||
propWhy, nextStep, scrollTo, actExitStock, actReduceStock, rowMore,
|
||||
pHaltBuy, pResumeBuy, pHaltAll, pResumeAll, pReduce, pIncrease, pLiquidate, pSectorExit };
|
||||
}
|
||||
}).use(ElementPlus).mount('#app');
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue