diff --git a/FRONTEND_TRADER_VIEW_PLAN.md b/FRONTEND_TRADER_VIEW_PLAN.md new file mode 100644 index 0000000..ce0af6f --- /dev/null +++ b/FRONTEND_TRADER_VIEW_PLAN.md @@ -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=`。 +- **行字段映射**: + + | 来源字段 | 显示 | + |---|---| + | `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。 diff --git a/app/repo/downstream_repo.py b/app/repo/downstream_repo.py index 58a9f21..066d4e7 100644 --- a/app/repo/downstream_repo.py +++ b/app/repo/downstream_repo.py @@ -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 diff --git a/app/web/main.py b/app/web/main.py index 0cc7966..9fce73b 100644 --- a/app/web/main.py +++ b/app/web/main.py @@ -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(): diff --git a/app/web/static/index.html b/app/web/static/index.html index 9cab865..211ca41 100644 --- a/app/web/static/index.html +++ b/app/web/static/index.html @@ -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;} @@ -44,19 +48,23 @@ 交易日 非交易日 + + 交易员 + 运维 +
买入 {{ ov.buy_halt ? '已暂停':'正常' }} 执行 {{ ov.exec_halt ? '已暂停':'正常' }} - 自主档位 {{ ov.autonomy || '-' }} - + 自主档位 {{ tx('autonomy', ov.autonomy) }} + 下发 {{ dm.mode || '-' }} 计划 {{ planStatus.ok===false ? '不可用' : (planStatus.date || '-') }} 上游计划 日报 - 运维 + 运维 刷新 @@ -80,6 +88,190 @@ + +
+ + +
+
账户总资产
{{ money(ov.total_asset) }}
+
可用资金 (估算)
+
{{ money(ov.cash_source==='ws' ? ov.cash_avail : ov.cash_est) }}
+
持仓市值
{{ money(ov.portfolio_mv) }}
+
仓位
{{ pct(ov.portfolio_pct) }} / {{ pct(ov.portfolio_cap) }}
+
浮动盈亏
+
{{ money(ov.float_pnl) }} {{ ov.float_pnl_pct==null?'':pct(ov.float_pnl_pct) }}
+
持仓数
{{ ov.names_count }} / {{ ov.max_names }}
+
等我拍板
+
{{ proposals.length }} 条
+
自主档位
{{ tx('autonomy', ov.autonomy) }}
+
+
+

需要注意

+
· {{ a }}
+
+ + +
+

等我拍板 · {{ proposals.length }} 条

+
暂无需要你确认的提议。
+
+
+
{{ nm(p.ts_code) }} {{ p.ts_code }} + {{ tx('action', p.action) }}
+
{{ p.expire_at }} 前有效
+
+
研判:{{ p.judge_reason }}
+
{{ propWhy(p) }}
+
+ 约 {{ p.qty }} 股 · 参考价 {{ (p.hard_numbers||{}).price==null ? '—' : (p.hard_numbers||{}).price }} + · 约需 {{ money(((p.hard_numbers||{}).price||0)*(p.qty||0)) }} +
+ 采纳 + 驳回 +
+
采纳=记一笔待执行指令,由择时在窗口内择机下单;驳回=今日不再提这只。
+
+
+ + +
+

我的持仓 · {{ heldPositions.length }} 只

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+

今日在办

+ + + + + + + + +

今天发生了什么

+
今天还没有记录。
+
+ {{ (l.decided_at||'').slice(11,16) }} + {{ nm(l.ts_code) }}:{{ tx('arbiter', l.arbiter) }}{{ tx('verdict', l.verdict) }} {{ tx('action', l.action) }} —— {{ l.reason }} +
+
+ + +
+

组合操作

+
+ {{ ov.buy_halt?'恢复买入':'暂停买入' }} + {{ ov.exec_halt?'结束休假':'休假模式' }} + 降仓… + 升仓… + 清仓某行业… + 一键清仓(紧急) +
+
暂停买入不影响卖出与止损;一键清仓会把全部持仓按最快节奏卖出、不做择时优化。
+
+
+

设置

+ + + + + + +
+ 保存修改 ({{ dirtyCount }}) + 放弃 + 改「自主档位」或「总仓上限」会即时生效;调低总仓上限可能触发降仓提议。 +
+
+ + +
+

系统在盯的候选(今日选股计划) · 计划日 {{ planStatus.date || '—' }}

+
上游计划暂不可用。
+ + + + + + + +
这是系统当前在盯、可能冒出提议的池子;仅供参考,不自动下单。
+
+ +
+ +
总规模
{{ money(ov.scale) }}
组合市值
{{ money(ov.portfolio_mv) }}
@@ -508,6 +700,8 @@ +
+
@@ -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');