Compare commits

..

25 Commits

Author SHA1 Message Date
kyugao ec02a791ee 迭代22+23: 策略tab静态化 + 计算字段(数字/判定型)+ 网格超市信号字段
- R-027 静态化:策略恒为内置两项(不可增删改名,保留显隐/排序);设置页移除「策略分组」;
  字段配置入口迁到策略 tab 内「列设置」旁(FieldConfigDialog + schema-update 单策略写入)
- R-026 计算字段:自写公式引擎(中文变量、四则/括号/round·abs·min·max、缺值短路→—)+
  变量目录(行情/合约/持仓/自定义字段)+ strategy-positions 逐行现算(只读内存缓存,不落库、列只读)
- R-028 判定型:比较运算 + and/or + inferResultKind + 结果类型一致性校验 + 判定列 ✓/— 渲染;
  网格超市落地 可下空单=涨停价>基准值+网格大小、可下多单=跌停价<基准值-网格大小
- 变量选择改标签平铺(老师反馈);公式手册 docs/99-其他材料/计算字段公式说明.md
- 约束同步:产品约束-002/003/004/009/013/014、技术约束-014/015/022/023/024、UI约束-002/003/005/008
- 回归:新增 test-r026/test-r027,更新 r011/r013/r017,17 个脚本全绿;typecheck/build 通过
2026-09-10 14:05:06 +08:00
kyugao 3dfa39b8bf 迭代19-21收尾: 指示灯自适应探测(R-022/R-023)+ 列设置拖拽排序(R-024)+ Tab设置间隙线统一(R-025)+ 需求池归档(R-014/15/16/17/19 → 已完成) 2026-09-10 14:04:51 +08:00
kyugao 8782aa9c50 迭代18归档: 需求 R-020 移入已完成(验收通过)+ 迭代复盘 + 索引更新 2026-09-09 09:03:48 +08:00
kyugao c700f2f760 迭代18: QMT Bridge MCP 能力内建(R-020)—— 动态挂载 dsh-mcp-client + 状态出口(设置页/会话头部灯) 2026-09-09 08:58:49 +08:00
kyugao 9382ace3a0 docs: 迭代17 策略会话(R-019)设计文档 + 开盘啦 API 参考材料 2026-09-09 08:58:49 +08:00
kyugao 6a578cc598 feat(UI): 交易记录归属分配改为 Modal 弹窗,行内只留精简态 2026-09-08 11:13:55 +08:00
kyugao 1223e2e74b 迭代16: 交易关联驱动持仓份额动态调整(R-018) 2026-09-08 11:09:05 +08:00
kyugao 22ec732607 迭代15: 委托归属候选纳入近期清仓持仓(R-017)
- 交易记录 tab 归属候选只回当前持仓 → 清仓后当日委托失去下拉锚点无法补关联
  (2026-09-07 大连热电实际发生: 盘中归属后被清为未关联,下拉已无做T候选)
- SqliteStore.getAttributionCandidates: 候选 = 该 code 当前持仓 + 近 7 天清仓的持仓
  (closed 标记 + closedAt,排序 当前持仓在前 → 清仓行 closed_at DESC)
- AttributionSelect: 下拉 value 改 holdingId 键控(同策略多轮持仓不撞值)、
  清仓选项「(已清仓 MM-DD)」后缀、已归属但候选缺失显示「当前归属」占位
- 回归: test-r017-candidates 12/12 + 存量回归全绿
2026-09-07 18:15:20 +08:00
kyugao 51c70c48fb 迭代14: 策略tab历史持仓展示(R-016)+ 今日已清仓默认层
- R-016 Q1-Q7: 两个策略 tab title 旁「历史持仓」开关 + 清仓范围筛选(近一周默认/1月/3月/半年/1年)
  - SqliteStore.getHoldingsHistory(strategy_id + closed_at 非空 + sinceMs,closed_at DESC,只读)
  - DataStore 门面透传 + strategy-holdings/history 端点(name 兜底 + 参数校验)
  - 前端: 开关/范围下拉/历史行灰显+「已清仓」徽标/展开复用 R-010(rowKey 唯一化)
- R-016 二轮补充(Q8-Q9 老师拍板): 今日已清仓默认层
  - range=today = 本地自然日 00:00 起(特判零点,不落回溯毫秒档)
  - 当天已清仓的持仓默认恒显示、不受「历史持仓」开关控制(开关只控今天之前)
  - rowsToRender 分层合成 + holdingId 去重;标题计数不含已清仓行;零写路径变更
- 回归: test-r016-history 24/24 + 存量回归全绿
2026-09-07 18:15:07 +08:00
kyugao ed43ecae39 feat(UI): 展开箭头全站统一为 ExpandChevron 共享组件
- 新增 src/client/views/ExpandChevron.jsx:14x14 chevron-right、success 绿、展开旋转 90°(源自策略 tab 表格行图标)
- 设置页策略分组行首 ▸/▾ 文字箭头改用该组件,并修复箭头与策略名的水平对齐(基线 trick 改 flex 精确居中)
- StrategyTab / TradeRecordsTab 两处重复内联 SVG 收敛为共享组件
- 去掉字段编辑器展开区冗余「收起」按钮(行首箭头/「收起字段」已可收起),清理 onClose prop
- docs: 新增 UI约束-006(展开/收起箭头全站统一)
2026-09-03 17:39:49 +08:00
kyugao d793d2cf35 迭代13: 盘口内存快照(QuoteSync/QuoteHub)+ 同步指示灯 + 行情列
- QuoteSync/QuoteHub 替换 MarketFeed/MarketDataHub(方案A 删除重写):
  5s REST 同步 watch 集合 + 涨停/跌停经 /data/instrument 按交易日内存缓存
  + 读穿透走适配层 getTicks(修正旧 hub 绕适配层的层级破洞)
- WS 数据通路移除(从无生效结论;ingest 留 source 标签回归口子)
- market_quotes_cache 表退役 DROP(幂等);价格单一入口 = QuoteHub
- 数据同步指示灯组: QMT连接|持仓数据|行情数据(绿/黄/灰,点击即同步/即时探测)
  + sync-status 端点(吸收 market-stats)+ sync-now;修复 runtime 漏传 positionSync 致持灯恒灰
- 策略持仓表行情列: 涨停价/跌停价/今开/最高(COLUMN_META defaultVisible=false,纯价格)
- 回归 test-quote-sync.mjs 27 项(含 DROP 幂等真实 SQLite 验证)
- 文档链: R-015 + PLAN-014 + 迭代三件套 + 技术约束-010/012变更、018新增 + 产品约束-012

需求: R-015(老师五拍板: 替换/WS移除/纯内存DROP/watch现状/指示灯)
2026-09-03 13:26:25 +08:00
kyugao 245fcad95c 迭代12: 持仓内存快照(PositionSync 10s 定时同步,请求不再穿透 QMT)
- PositionSync: 内存快照 + 10s 定时全量同步 + 失败保留旧快照 + 空快照双重确认
  (/health + getAsset 账户身份)+ 幽灵持仓自动清仓(连续3轮消失 + 账户身份守卫防误清)
- PositionManager.getAllPositions 改读快照,空则读穿透兜底;未注入 sync 时兼容旧穿透
- 回归 test-position-sync.mjs 35 项(纯内存 mock,含 kept 分支刷新 syncedAt 防灯灰)
- 文档链: R-014 + PLAN-013 + 迭代三件套 + 数据存储设计 §11(内存不落库决策与三问标准)

需求: R-014(老师拍板: 内存不落库/读穿透兜底/幽灵自动清仓/不显示同步时间)
2026-09-03 13:25:50 +08:00
kyugao 39b135fa36 fix(持仓): strategy-positions 返回 values 按当前 configSchema 过滤(防字段收窄后残留键混入展示) 2026-09-02 18:14:36 +08:00
kyugao 329ac67a71 docs(迭代11+归档): R-013 归档完成 + 迭代 11 复盘
- 迭代 11 复盘:实施事实(R-013 自定义字段 + 列显隐 + 单元格编辑 + unit)+
  经验教训(JSX 未导入白屏 / z.dict dts / SQLite 关键字列 / map this 丢失 / 列宽稳定)
- R-013 归档至 已完成/(归档头:日期/状态/实现迭代/讨论记录)
- 索引 R-013 → 已实现(已归档);PLAN-012 → 已完成
2026-09-02 17:34:51 +08:00
kyugao aac16b904c feat(R-013): 策略自定义字段完整实现 + 列显隐配置 + 单元格编辑
功能实现(迭代 11):
- 数据层: strategies.configSchema 自定义字段定义(text/number/boolean/enum + def + unit 单位可空)
  + strategy_holdings "values" JSON 列(幂等 ALTER)+ readValues/writeValues + 归一化
- API: holdings/values-update 端点(服务端按 configSchema 校验: number有限/enum在选项/boolean布尔)
- 设置页: 「策略分组」策略行展开配置字段(StrategyFieldsEditor 独立组件)
- 持仓表: 自定义字段列化 + 列显隐/排序(ColumnSettingsPopover,每策略独立 strategyColumns 配置)
- 单元格点击编辑(FieldCellEditor): 文本/数字输入框、布尔开关、枚举下拉 + ✓ 确认
- UI 细节: ✎ 可编辑标记(值后)、编辑列宽稳定、数字输入框固定 5 位宽

修复:
- SettingsSection Fragment/StrategyFieldsEditor 未导入导致的配置页白屏
- z.dict schema dts 推断 cosmokit 引用(显式类型注解)
- 自定义字段从展开交易明细移出(字段已列化)

文档: R-013/迭代11 技术方案/验收标准/迭代目标 同步(D6 演进 + unit + 列配置)
回归: test-r013-custom-fields 21/21 + test-r013-columns 16/16 + tabs 23/23
2026-09-02 17:21:41 +08:00
kyugao 6f74ed4a79 docs(结构约束同步): 活动文档路径对齐新目录结构
归类重构(技术约束-016)落地后同步活动文档旧路径:
- 数据存储设计.md: 三处对应实现表 12 处 src/component/* → storage/position/market/trades
- R-013 / PLAN-012 / 迭代11技术实现方案: SqliteStore/PositionManager 路径更新
- 迭代11程序结构设计: 补后续动作记录(历史档案按追加式原则不改写)
2026-09-02 15:43:26 +08:00
kyugao a4902bb730 feat(R-013+重构): 策略自定义字段需求文档 + 结构归类优化
需求与文档:
- R-013 策略自定义字段配置(定义随策略 configSchema 存 settings、值落
  strategy_holdings.values JSON、类型化文本/数字/布尔/枚举)定稿并进入
  PLAN-012 / 迭代 11(含目标数据模型 + 端到端示例 + 技术方案 + 验收标准)
- R-011(Tab设置)/ R-012(UI主题适配)归档至 已完成/,索引标记已归档
- 沉淀约束: 产品约束-010 / 技术约束-015 / 技术约束-016 / UI约束-005

结构重构(技术约束-016):
- src/component/ 平铺还原为语义分域目录: data-source/ storage/ position/
  market/ trades/(git rename 保留历史)
- 清理死代码: 删除 AllocationStorage.js(0 引用)、DataStore
  setDataset/removeDataset(无调用方)
- 同步更新 src/index.js / scripts/*.mjs / tsdown.config.ts 引用
- typecheck + build + test-r009 回归 14/14 通过
2026-09-02 15:38:51 +08:00
kyugao f1e7e785a1 docs(迭代09/10): Tab 设置统一管理 + UI 主题适配 全量文档
迭代09 (R-011 Tab 设置统一管理):
- 需求 R-011 (定稿 Q1-Q5) + 需求池索引
- 计划 PLAN-010 + 迭代09 四件套 (目标/技术方案/验收/复盘)
- 约束: 产品约束-009 / UI约束-003 / 技术约束-014
- UI约束-002 修订: 通用设置→Tab 设置

迭代10 (R-012 UI 适配 DSH 主题):
- 需求 R-012 (定稿: 暂定跟随系统) + 需求池索引
- 计划 PLAN-011 + 迭代10 四件套
- 约束: UI约束-004 (主题适配约定)
2026-09-02 14:01:08 +08:00
kyugao 4ac8a29805 feat(Tab设置+UI主题): 迭代09 Tab设置统一管理 + 迭代10 UI适配DSH主题
迭代09 (R-011): Tab 设置统一管理所有会话 tab
- settings.tabs 布尔对象 → 统一有序数组 (kind: builtin|strategy)
- 旧配置 schema union 兼容 + 读取归一化无感迁移
- strategies 收窄为 {id,name},策略行 name join 自动跟随改名
- 策略增删联动 tabs 条目 (add 追加末尾 / remove 联动删除)
- 客户端统一注册读 tabs 数组,内置与策略可任意混排
- 设置页 Tab 设置子 tab:混排表格 + 拖动手柄 + 显隐开关 + 落点立即持久化
- 策略分组瘦身:仅 新增/重命名/删除 + 指引提示
- 回归测试: test-r011-tabs (23) + test-r011-api (12)

迭代10 (R-012): UI 适配 DSH 浅色/深色/跟随系统主题
- 10 个视图文件 141 处硬编码色 → var(--dsw-alias-*, fallback)
- 语义映射: bg-layer/label/border/state/button-contrast 等
- 淡底 color-mix 自适应;遮罩 bg-mask-1;阴影 shadow-lv3
2026-09-02 14:00:56 +08:00
kyugao 35f2ad1a23 docs(归档): 迭代 07/08 归档整理(R-009/R-010 归档文件规范化)
- R-009 归档头更新为二次定稿结论(手动归属 + 冗余),清理重复标题
- R-010 归档头补充讨论记录,清理重复标题
- 需求池索引:R-009 描述与最终实现一致(手动归属,非零冗余推导)
2026-09-02 09:42:28 +08:00
kyugao d122d88bce feat(策略持仓): 表格加昨收/涨幅列(涨幅支持升降排序)+ 去掉总持仓列
- 昨收列:行情缓存 lastClose
- 涨幅列:(现价-昨收)/昨收,红涨绿跌着色
- 涨幅排序:点击表头降序/升序切换(▼/▲指示)
- 移除总持仓列
2026-09-02 09:23:02 +08:00
kyugao 2aacd8a25d docs(迭代07/08): 交易记录本地存储 + 策略持仓展开全量文档 + 需求归档(R-009/R-010)
- PLAN-008/PLAN-009 计划(已完成)
- 迭代 07/08 四件套(目标/技术方案/验收标准/复盘)
- 设计约束:数据存储设计 §10/§10.1、技术约束-012/013、产品约束-008
- R-009/R-010 需求归档(含二次定稿:手动归属、Q4 成本价/成交价)
- 需求池索引同步
2026-09-02 01:21:10 +08:00
kyugao 859938c4f1 test(交易记录): 迭代 07/08 回归测试(独立数据目录)
- test-r009: 手动归属核心(建表/幂等/UPSERT 不覆盖归属/候选/持久化过滤)
- test-r009-sync: TradeSync 同步集成
- test-r009-normalize: QMT code 归一化
- test-r009-api: API 层端点集成
- verify-real-r009: 真实数据只读验证
- verify-real-attribution: 归属写闭环(被插件锁时跳过)
2026-09-02 01:21:04 +08:00
kyugao 2790b92378 feat(策略持仓): 持仓行展开关联交易 + 成本价/最后成交价 + 隐藏输入框(迭代 08,R-010)
- strategy-positions 附加 holding_id + avgPrice/lastTradePrice(无关联默认成本价)
- trades/by-holding 端点(按 holding 查委托汇总)
- StrategyTab 持仓行展开(懒加载 + 内嵌汇总表,含交易日/时间列)
- 神之一手 tab 激活时隐藏 AI 对话输入框(纯 CSS :has() 方案)
2026-09-02 01:20:58 +08:00
kyugao b3526c7693 feat(交易记录): SQLite 本地存储 + 手动归属(迭代 07,R-009)
- trade_orders + trade_fills 两表(委托/成交,holding_id/strategy_id 手动归属)
- TradeSync 定时同步(启动预热 + 60s UPSERT,不覆盖归属列)
- 手动设置归属(candidates 候选 + set-attribution 端点 + 前端归属下拉)
- 历史查询 trades/history + 策略过滤
- QMT code 归一化(补后缀)+ tradeDate 兜底 insertDate
2026-09-02 01:20:50 +08:00
178 changed files with 18736 additions and 1883 deletions
+4
View File
@@ -26,6 +26,10 @@ Thumbs.db
.env.* .env.*
!.env.example !.env.example
# Temp artifacts (AI session diagrams / scratch)
.tmp-diagrams/
.tmp-*/
# DSH / runtime # DSH / runtime
.dsh/ .dsh/
*.tmp *.tmp
@@ -0,0 +1,92 @@
# 计划:QMT Bridge MCP 能力内建(神之一手自注册 MCP,去三方插件依赖)(阶段航点)
> 编号:PLAN-018 | 粒度:阶段航点 创建:2026-09-08 | 状态:待实施(拟迭代 18)
> 派生自终极目标:目标-001(DSH 插件形态·复用宿主 MCP 能力)、目标-008(真实交易系统接入)
> 依据需求:**R-020(已定稿,2026-09-08Q1-Q6 老师拍板)** —— 符合入范围门槛
> 设计约束:技术约束-003(REST 直连不变)、技术约束-008(连接配置存储/热切换)、产品约束-012(会话头部指示灯体系)、UI约束-002(连接卡片);沿用技术约束-004/005/006/007
## 目标
让神之一手插件自带 QMT Bridge 的 MCP 能力:插件在自己的 apply 内按 QMT 连接配置动态挂载 **DSH 官方 @deepseek-ai/dsh-mcp-client**(url 自动派生自激活连接 baseUrl + /mcpserverName 固定 QMT_Bridge_MCP,工具以 mcp__QMT_Bridge_MCP__* 注册给模型;挂载/切换/卸载/状态均由神之一手在插件内管理);连接 CRUD/激活切换与 MCP 实例全生命周期联动;**卸载三方 dsh-skill-mcp-panel 并移除其宿主 cordis.patch.yml 受管块**,不再依赖三方面板加入 MCP 能力。REST 直连架构不变(技术约束-003)。
## 范围
**做**(对应 R-020 定稿边界,决策编号 Q1-Q6):
1. **依赖与可行性前置(R-020 计划阶段待办 #1,已实证 2026-09-08**@deepseek-ai/dsh-mcp-client@0.1.1-rc.2 已加为 one-divine-lot dependencies,从插件 lib 可 import(),模块导出 {name:'mcp-client', inject:['tools'], apply, Config},可用 ctx.plugin() 动态挂载(实证通过);
2. **服务端 MCP 管理器(新增 src/mcp/QmtMcpManager.js**
- 唯一 dsh-mcp-client 实例,serverName 固定 `QMT_Bridge_MCP`url = 激活连接 baseUrl + `/mcp`(自动派生,Q3);
- 启动:解析启动连接(默认→上次激活→第一条,空表回退 cordis qmtBaseUrl)后挂载(对齐 R-004 resolveStartupConnection);
- 生命周期编排:activate/update(地址变)/remove(激活被删切默认)→ 先卸后挂(对齐 qmt-connections API 现有热切换点,Q1 仅激活连接挂载);
- 失败语义(Q5):failOnStartupError:false + reconnect(对齐宿主现有受管块配置:initial 500ms / max 30s / maxAttempts 10);
- 状态缓存:{ state: connected|connecting|disconnected|disabled, serverName, url, toolCount, error, checkedAt },供状态端点与指示灯读取;
- 释放:ctx.effect dispose 时卸载实例;
3. **服务端状态出口(api/qmt-connections.js 或 market.js 扩展)**
- mcp-status 端点(含 refresh 即时重查)→ 设置页连接卡片 MCP 状态行 + 会话头部指示灯读取;
- sync-status 增加 mcp 域(Q4:头部指示灯数据源合并一处);
4. **设置页「QMT 连接配置」卡片 MCP 状态(UI约束-002 形态内扩展)**:卡片显示 MCP 状态(绿=已连接 + N 工具 / 红=失败原因 / 灰=未启用),「检查 MCP」按钮(手动触发,Q4);
5. **会话头部指示灯新增 MCP 状态灯(产品约束-012 体系扩展)**SyncIndicators 三灯 → 四灯(QMT连接|持仓数据|行情数据|MCP),MCP 灯三态对齐(绿=已连接 / 黄=重连中 / 红=断开 / 灰=未启用),点击=refreshQ4);
6. **移除三方 dsh-skill-mcp-panelQ22026-09-08 老师澄清)**dsh plugin remove dsh-skill-mcp-panel(连带其「技能 / MCP」设置菜单);编辑 ~/.dsh/profiles/web/cordis.patch.yml 删除其受管块(QMT_Bridge_MCP 条目,块外内容逐字节保留)——属宿主组成变更,按 editing-cordis-compositions 技能流程执行 + 老师确认;
7. **构建安装与验收**
**不做**(本期,R-020 边界):通用 MCP 服务器管理面板(Q6,含自建任意 MCP 管理 UI);插件 UI/tab 数据源改走 MCP(技术约束-003);多 serverName 并存;自研 MCP 协议客户端(老师定:官方 dsh-mcp-client 可依赖,不重造);认证/鉴权增强(QMT Bridge 无鉴权)。
## 程序结构(新增/改动)
```
src/
├── index.js # 服务端入口(改):注入 mcpManager,启动挂载 + 释放卸载;把 mcpManager 传入 registerApi
├── mcp/ # (新增域目录,技术约束-016 语义:无合适域先讨论,此处讨论定 = 新增 mcp 域)
│ └── QmtMcpManager.js # MCP 实例管理(官方 dsh-mcp-client 动态挂载:唯一实例 + 生命周期编排 + 状态缓存)
├── settings.js # (小改):暴露当前激活连接解析辅助(已有 getActiveQmtConnection 可复用,必要时补暴露)
├── api/
│ ├── index.js # (改):runtime 增 mcpManager,注册 mcp-status 方法分发
│ ├── qmt-connections.js # (改):热切换点(activate/update/remove)联动 mcpManager 重挂;新增 mcp-status 处理
│ └── market.js # (改):sync-status 增加 mcp 域
└── client/views/
├── QmtConnectionChip.jsx # (改):内置 SyncIndicators 传 mcp 状态读取
├── SyncIndicators.jsx # (改):三灯 → 四灯(+MCP),轮询 sync-status 读 mcp 域,点击 refresh
└── SettingsSection.jsx # (改):QmtConnectionCard 增 MCP 状态行 + 「检查 MCP」按钮
package.json # (改,已完成):dependencies + @deepseek-ai/dsh-mcp-client@0.1.1-rc.2 + @modelcontextprotocol/sdk@1.30.0
~/.dsh/profiles/web/cordis.patch.yml # (改):移除 dsh-skill-mcp-panel 受管块(Q2,宿主配置,独立步骤;插件本体 dsh plugin remove
```
## 实现步骤(建议顺序)
1. **依赖可行性实证**(任务 1 决策门):声明 peer + 构建后在宿主实测 import 解析;不可解析则停在这里与老师讨论替代通道;
2. **服务端 mcpManager**QmtMcpManager 骨架 + 启动挂载/卸载 + 状态缓存(先不接 API,日志验证挂载与工具注册);
3. **热切换联动**qmt-connections activate/update/remove 编排点追加 mcpManager.resync()(复用现有 dataSource.setBaseUrl 同点);
4. **状态端点**mcp-status + sync-status.mcp 域;本地 curl/HTTP 验证;
5. **设置页卡片**QmtConnectionCard 增 MCP 状态 + 检查按钮(复用测试连接交互模式与 Toast);
6. **会话头部 MCP 灯**SyncIndicators 四灯改造;验证与三灯并存样式一致(对齐 IndicatorDot/sep 布局与主题 token);
7. **宿主受管块移除**:编辑 cordis.patch.ymlediting-cordis-compositions 流程)→ 重载宿主 → 验证 mcp__QMT_Bridge_MCP__* 工具仍可用(这次来自神之一手);
8. **构建安装验收**:服务端+客户端构建,dsh plugin add / 重载;逐条核对验收标准。
## 验收标准
1. 移除宿主 cordis.patch.yml QMT_Bridge_MCP 受管块后,模型仍能使用 mcp__QMT_Bridge_MCP__qmt_* 工具(来源=神之一手动态挂载实例),工具集与移除前一致(同一 serverName/url);
2. 激活连接切换 → MCP 实例 url 跟随切换(工具可用目标随激活配置变化,观察工具调用返回目标数据源变化);
3. 编辑激活连接地址、删除激活连接(自动切默认)→ MCP 实例同步重挂,无残留旧连接工具;
4. 连接列表为空(无激活)→ 不挂 MCP 实例(mcp-status=disabled),插件其它功能不受影响;
5. QMT Bridge 不可达 → MCP 灯=红(断开),插件不崩、其余灯正常;恢复可达后自动重连至绿灯(Q5 语义);
6. 设置页连接卡片显示 MCP 状态(已连接 + N 工具 / 失败原因 / 未启用),「检查 MCP」可手动触发并刷新;
7. 会话头部出现第四个指示灯「MCP」:绿=已连接 / 黄=重连中 / 红=断开 / 灰=未启用,点击触发即时检查(Q4);
8. 现有功能(分仓/策略/交易/设置子 tab/三灯)无回归;typecheck + build 通过。
## 验收方法
- 移除受管块前先完成 1-6 步并在旧块存在时验证(同名冲突预期:两实例同 serverName 后加载报错——因此**验证顺序必须先移除受管块再加载新实例**,步骤 7 与 1-6 的顺序需在实施时按 HMR 重载节奏小心编排,见实施时核查点);
- 用 qmt 工具的 listTools/实际调用观察目标切换;断开 QMT 观察灯态与自动恢复;
- settings 卡片与头部灯截图/人工核验。
> **实施注意(同名冲突时序)**:宿主现有受管块与神之一手新实例同用 serverName QMT_Bridge_MCP,存活实例重复会报错。安全顺序:① 插件侧完整实现并构建 → ② 移除宿主受管块并重载(此窗口工具短暂消失可接受)→ ③ 插件实例接管(工具恢复,来源变为神之一手)。禁止两实例并存重载。
## 关联
- 需求:R-020(已定稿,2026-09-08)| 参考:mcp_router 动态挂载实证、dsh-skill-mcp-panel 受管块现状
- 设计约束新增(定稿时补充):技术约束:「插件内建 MCP 客户端注册规范」(唯一实例 serverName 固定 + 随激活连接 url 派生 + 生命周期联动);产品约束:「MCP 状态出口(设置卡片 + 会话头部 MCP 灯)」
## 记录
| 日期 | 变更 |
|---|---|
| 2026-09-08 | 依据 R-020 定稿(Q1-Q6)创建 PLAN-018 |
@@ -0,0 +1,61 @@
# 计划:Tab 设置——统一管理所有会话 tab(内置 + 策略分组,显示/隐藏 + 拖动排序)(阶段航点)
> 编号:PLAN-010 | 粒度:阶段航点(大粒度) | 创建:2026-09-02 状态:**执行中**
> 派生自终极目标:目标-002(分仓管理工具)、目标-003(按策略监控市场)
> 依据需求:**R-011(已定稿,2026-09-02Q1-Q5 确认)** —— 符合入范围门槛
> 设计约束:产品约束-009、UI约束-003、技术约束-014R-011 新增沉淀)、技术约束-004/005/006(沿用)
## 目标
将设置页「通用设置」升级为 **「Tab 设置」**,成为所有会话 tab(系统内置 + 策略分组)**唯一的顺序与显隐入口**:两类 tab 混排一张表,每行 = 拖动手柄 + 名称 + 显示/隐藏开关;**任何 tab 均不支持重命名与删除**;拖动落点立即持久化。
## 范围
**做**
1. **数据模型统一**src/settings.js):settings.tabs 从布尔对象 → 有序数组 [{id, kind: builtin|strategy, refKey|refId, name, visible, order}]strategies 收窄为 {id, name};读取时旧格式静默归一化(旧 tabs 布尔对象 → 三个内置条目保留原显隐;旧策略按 order 接续追加,旧隐藏策略迁移后 visible=true);新增整表更新(顺序 + 显隐);DEFAULT_TABS 升级为默认数组;
2. **API 层**src/api/strategies.js):tabs/update 语义改为整表更新(顺序 + 显隐);strategies/add 联动追加 tab 条目(末尾);strategies/remove 联动删除对应 tab 条目;废弃 strategies/movestrategies/update 仅剩重命名;
3. **客户端注册**src/client/index.js):合并 registerGeneralTabs + registerStrategyTabs 为统一注册(读 tabs 数组按 order 排序、过滤 visiblebuiltin 走内置 render、strategy 走 StrategyTab),移除硬编码 order 间隔(10/11/12 与 13+);
4. **设置页 UI**src/client/views/SettingsSection.jsx):
- 「Tab 设置」子 tab 取代「通用设置」:混排表格,内置行「内置」徽标、策略行「策略」徽标;每行 = 拖动手柄 + 名称 + 显隐开关;无任何重命名/删除按钮;
- 拖动:原生 HTML5 Drag & Drop,落点重排 → 立即调 tabs/update 持久化 → 提示「刷新页面后生效」;
- 「策略分组」子 tab 瘦身:只留 新增/重命名/删除,移除排序箭头与显隐开关,加提示「顺序与显示请在「Tab 设置」中调整」。
**不做**
- 策略的命名/删除入口迁移(仍在「策略分组」子 tab);
- 内置 tab 的重命名/删除(禁);
- tab 注册热更新(沿用「刷新页面后生效」机制,无事件机制);
- 其他设置子 tab(QMT 连接配置)改动(仅 UI约束-002 文案「通用设置」→「Tab 设置」对齐)。
## 程序结构
```
src/
├── settings.js # 改:tabs 有序数组 + 归一化迁移 + 整表更新;strategies 收窄
├── api/strategies.js # 改:tabs/update 整表;add/remove 联动 tabs;废弃 move
src/client/
├── index.js # 改:统一注册(读 tabs 数组)
└── views/SettingsSection.jsx # 改:「Tab 设置」子 tab + 拖动排序 + 策略分组瘦身
```
## 实现步骤
1. **文档骨架**PLAN-010 + 迭代 09 文档(本步);
2. **settings.js**tabs 有序数组 schema + DEFAULT_TABS + 归一化(getTabs 旧格式转新)+ updateTabs 整表 + strategies 收窄(去 visible/order 读写);
3. **api/strategies.js**tabs/update 整表语义;strategies/add 追加 tabstrategies/remove 联动删 tab;废弃 moveAPI 表 + handler);
4. **client/index.js**:统一注册函数(读 tabs 数组),移除硬编码 order;
5. **SettingsSection.jsx**:「Tab 设置」子 tab(混排表格 + 徽标 + 显隐开关 + 拖动排序 + 立即持久化);策略分组子 tab 瘦身;
6. **构建测试**pnpm run build + typecheck + 独立数据目录回归(技术约束-011,注意只读端点可直连、写操作走临时目录);
7. **验收 + 复盘**
## 验收要点
- 「通用设置」子 tab 更名为「Tab 设置」,表格混排内置 + 策略全部 tab;
- 内置行带「内置」徽标、策略行带「策略」徽标;
- 每行可拖动排序,落点立即持久化(刷新页面后生效);
- 每行有显示/隐藏开关,切换立即持久化;
- 任何行均无重命名/删除按钮;
- 策略分组子 tab 只留 新增/重命名/删除,无排序箭头/显隐开关,有指引提示;
- 新增策略出现在 Tab 设置列表末尾;删除策略后对应 tab 条目消失;
- 老配置(布尔对象 tabs + 策略 order/visible)读取时自动归一化,无报错、无数据丢失;
- 会话 tab 栏按新顺序与显隐注册(内置与策略可交错);
- 现有功能不回归(全部持仓/交易记录/策略持仓/QMT 切换)。
+55
View File
@@ -0,0 +1,55 @@
# 计划:UI 适配 DSH 主题(浅色 / 深色 / 跟随系统)(阶段航点)
> 编号:PLAN-011 | 粒度:阶段航点 创建:2026-09-02 状态:**执行中**
> 派生自终极目标:目标-001(神之一手工具集)
> 依据需求:**R-012(已定稿,2026-09-02,暂定跟随系统)** —— 符合入范围门槛
> 设计约束:UI约束-003Tab 设置)、UI约束-002(QMT 卡片)沿用;本次新增色值 token 化约定
## 目标
神之一手客户端 UI 全部硬编码色值替换为宿主 `--dsw-*` token,使各页面在 DSH 浅色 / 深色 / 跟随系统主题下均可读、协调,随主题切换自动适配,不自行维护主题偏好。
## 范围
**做**
1. 客户端 10 个文件 141 处硬编码颜色按语义映射表替换为 `var(--dsw-*)``color-mix`(随主题淡色底);
2. 确认 QmtConnectionChip 已用 token 与硬编码混杂处统一;
3. 涨跌色(红涨绿跌)→ 宿主 state-error/success;主按钮 / 徽标 / 提示底色 → 宿主语义 token + color-mix
4. 构建 + typecheck + 深浅主题人工验收。
**不做**
- 自定义 `--odl-*` 覆盖 tokenD5 暂缓);
- 布局 / 间距 / 圆角改动;
- 服务端 / API / 数据改动;
- 插件自维护主题偏好(跟随宿主)。
## 涉及文件
```
src/client/views/
├── SettingsSection.jsx # 55 处(最大)
├── StrategyTab.jsx # 24
├── TradeRecordsTab.jsx # 21
├── QmtConnectionChip.jsx # 13(部分已 token
├── RangeSelector.jsx # 7
├── PriceCell.jsx # 5(涨跌色)
├── AllPositionsTab.jsx # 4
├── LoadState.jsx # 3
├── Toast.jsx # 2
└── PlaceholderTab.jsx # 1
```
## 实现步骤
1. **文档骨架**PLAN-011 + 迭代 10(本步);
2. **批量替换**:按文件逐处替换(先小文件确认模式,再大文件);
3. **验证**build + typecheck;人工切 DSH 浅/深主题检查各页;
4. **验收 + 复盘**
## 验收要点
- 浅色主题下观感与现状基本一致(无突兀色差);
- 深色主题下所有页面可读(背景/文字/边框/按钮/涨跌/徽标/提示/下拉菜单均协调);
- 跟随系统切换实时生效;
- 无残留硬编码色(#xxx / rgb / rgba 全部清理,注释可留);
- 布局与交互不变(仅色值)。
@@ -0,0 +1,36 @@
# 计划:交易关联驱动持仓份额动态调整(阶段航点)
> 编号:PLAN-017 | 粒度:阶段航点 创建:2026-09-08 | 状态:**已完成(迭代 16 验收通过 2026-09-08**
> 派生自终极目标:目标-006(交易复盘)/ 目标-007(人机合一)
> 依据需求:**R-018(已定稿,2026-09-08,老师逐项拍板)** —— 符合入范围门槛
> 设计约束:技术约束-017(变更:幽灵清仓条款)、产品约束-011(变更)、数据存储设计.md §11(变更);新增归属分段存储设计
## 目标
从交易记录出发的关联操作升级为**账本写操作**:策略级候选 + 统一份额分配器(一笔委托拆 0..N 段 × 策略)、买卖联动调整 strategy_holdings 份额(买入加仓/建新仓、卖出减仓/归零清仓、仓不足自动截断续分)、撤段逆操作回滚(作废第三态 / 恢复活动仓)、**数据域分界**(幽灵清仓等同步机制不再写账本,账本只由交易关联 + 手动份额操作驱动)。
## 范围
**做**
1. 数据域分界落地:PositionSync 删除幽灵清仓账本写逻辑(同步机制只维护对账单快照)+ 漏关联软提示检测(账本有活动行 + QMT 快照无此 code → 提示,不动账本);
2. 存储地基:strategy_holdings 增 `void_at` 作废第三态(活动 = closed_at IS NULL AND void_at IS NULL,唯一索引收窄)+ 幂等补列;新增归属分段表 `trade_order_attributions`order_id → strategy_id/holding_id/volume/direction/created_at+ 存量归属迁移(trade_orders 两列 → 段表第一段);
3. 归属服务(核心):份额分配器 apply/revoke——判定动作表(买/卖 × 活动仓状态 → open/add/reduce/close/截断/失败)+ 撤段逆操作(撤建仓买入=作废、撤加仓=减回归零作废、撤卖出=恢复活动+份额加回)+ 原子性(改归属 = 撤旧段 + 加新段);
4. 策略级候选 + 分配器 API:候选回归「当前持仓 + 未关联」(R-017 7 天清仓候选退役,holdingId 键控/占位安全逻辑保留);
5. UITradeRecordsTab 归属入口改造为份额分配器(策略下拉 + 段量输入 + 余额提示 + 部分关联);
6. 回归:新脚本(分配器动作/撤段/迁移/软提示)+ 存量回归 + typecheck + build。
**不做**:自动归属推导暴露;全部持仓/未分配 tab 份额联动 UI;未分配余额自动兜底;账本写自动化兜底(软提示不改账本)。
## 实施步骤(阶段划分,供迭代跟踪)
1. 阶段APositionSync 幽灵清仓退役 + 软提示检测基础;
2. 阶段B:存储地基(void_at + 段表 + 迁移);
3. 阶段C:归属服务核心(apply/revoke 动作表 + 原子性);
4. 阶段D:策略级候选 + API(含 R-017 退役);
5. 阶段EUI 份额分配器;
6. 阶段F:回归 + typecheck + build + 存量回归;
7. 迭代复盘 + R-018 索引状态更新 + 需求归档。
## 验收要点
`docs/04-迭代记录/16-交易关联驱动持仓份额动态调整/验收标准.md`
@@ -0,0 +1,66 @@
# 计划:交易记录本地存储(SQLite)+ 策略关联(阶段航点)
> 编号:PLAN-008 | 粒度:阶段航点(大粒度) | 创建:2026-09-01 状态:**已完成(2026-09-01 迭代 07 验收通过)**
> 派生自终极目标:目标-006(交易复盘)、目标-003(按策略监控市场)
> 依据需求:**R-009(已定稿,2026-09-01Q1-Q8 全部确认)** —— 符合入范围门槛
> 设计约束:技术约束-012(数据存储设计)、技术约束-001/003/004/010(沿用)、产品约束-007 方向语义
## 目标
在 SQLite 新增**交易记录表**trade_orders 委托 + trade_fills 成交),将 QMT Bridge 当日交易数据**本地持久化**(跨日积累成本地历史库),并按**外键链**(成交→委托→策略→holding)建立与插件策略体系的关联,支持**按策略过滤 / 复盘**交易。
- 承接 R-007 的 Q5(本地持久化)与 Q10(按策略过滤);
- 落实迭代 06 复盘遗留项 2(holding_id 关联锚点);
- 解决 R-007 历史范围「接口开发中」占位(本地积累后历史可查)。
## 范围
**做**
1. **两表落地**trade_orders(委托主行,order_id 主键,UPSERT 幂等)+ trade_fills(成交明细,trade_id 主键、order_id 外键关联委托)两表;**两表均不冗余 strategy_id / holding_id**Q3 老师定稿:外键链推导);trade_orders 加派生列 insert_tsinsert_date+insert_time 合成毫秒时间戳,join 持仓窗口用);
2. **定时同步**:服务端 TradeSync 模块 —— 定时(60s)拉当日 orders+trades → UPSERT 落库(幂等);插件启动同步一次(预热今日数据);前端今日轮询写穿(机会式);
3. **策略关联(外键链推导)**:策略/持仓归属不在写入期计算,查询期由委托时间(insert_tsJOIN strategy_holdings 生命周期窗口(created_at ≤ t < closed_atclosed_at 为 NULL=当前持仓)推导;同码多策略取份额最大持仓;无命中视为未关联;
4. **本地历史查询端点**trades/history —— 按 { start, end, code, strategyId, direction } 查本地 SQLite,返回 { orders, fills }(策略过滤走 FK 链 join);
5. **前端**TradeRecordsTab 加策略过滤下拉(全部/各策略/未关联);历史范围从「占位」切换为查本地库;今日仍走 QMT 实时 + 写穿本地。
**不做**
- 手动修正策略关联入口(后续迭代);
- 导出/清理管理界面(后续迭代;复盘需要历史,不做自动清理);
- QMT 历史接口接入(老师完善后再接);
- 下单/撤单等交易操作(目标-007 另议)。
## 程序结构(改造后)
```
src/
├── component/
│ ├── SqliteStore.js # 改:+trade_orders/trade_fills 建表 + 交易 UPSERT/查询 + 策略归属推导
│ ├── DataStore.js # 改:委托交易记录方法
│ └── TradeSync.js # 新增:服务端定时同步(60s + 启动预热 + UPSERT 幂等)
├── api/
│ └── trades.js # 改:+trades/history 端点
└── index.js # 改:装配 TradeSync + storage 注入 api runtime
src/client/views/
└── TradeRecordsTab.jsx # 改:策略过滤下拉 + 历史范围查本地库
```
## 实现步骤(建议顺序)
1. **文档骨架**PLAN-008 + 迭代 07 子目录(本步);
2. **设计约束落地**:数据存储设计.md §10 增补交易记录表设计 + 技术方案约束新增条目;
3. **SqliteStore**:建表(trade_orders/trade_fills+ 交易 UPSERT(幂等)+ 本地历史查询 + 策略归属推导(insert_ts join 持仓窗口);
4. **DataStore**:委托交易记录方法;
5. **TradeSync**:定时同步模块(60s + 启动预热 + 今日范围 UPSERT);
6. **api/trades.js**trades/history 端点(+ 现有 orders/trades 响应附加策略归属信息可选);
7. **index.js**:装配 TradeSyncdispose 清理)+ storage 注入 api runtime
8. **前端**TradeRecordsTab 策略过滤 + 历史范围本地展示;
9. **构建测试**pnpm run build + typecheck + 独立数据目录回归测试(技术约束-011);
10. **验收**:对照迭代 07 验收标准逐条核验,记录迭代复盘。
## 验收要点
- trade_orders + trade_fills 两表落地(零冗余 strategy_id/holding_idinsert_ts 派生列);
- 同步:启动预热 + 60s 定时 UPSERT 幂等(重复同步不重复行、状态覆盖更新);
- 历史查询:按时间段/code/策略/方向查本地库,策略过滤 = 委托时间 join 持仓生命周期窗口(一码多策略取份额最大);
- 前端:策略过滤下拉 + 历史范围展示本地数据(不再占位);今日实时 + 写穿;
- 现有功能不回归(持仓/策略/行情/连接配置/交易记录今日实时);
- 技术约束-011:回归测试用独立数据目录。
@@ -0,0 +1,27 @@
# 计划:委托归属候选纳入近期清仓持仓(阶段航点)
> 编号:PLAN-016 | 粒度:阶段航点 创建:2026-09-07 | 状态:**已实施(待验收)**
> 派生自终极目标:目标-008(真实交易系统接入)
> 依据需求:**R-017(已定稿,2026-09-07,老师指令确认方向)** —— 符合入范围门槛
> 设计约束:技术约束-013 语义延伸(候选窗口 + holdingId 键控);不新增约束条目
## 目标
清仓后当日委托可补关联:归属候选 = 该 code 当前持仓 + 近 7 天清仓持仓(closed 标记),下拉锚点改 holdingId 键控。
## 范围
**做**getAttributionCandidates 扩展(closed_at IS NULL OR ≥ 7 天前;closed/closedAt 字段;当前在前/清仓 closed_at DESC+ AttributionSelect holdingId 键控 + 「(已清仓 MM-DD)」后缀 + 「当前归属」占位选项 + 回归 test-r017-candidates.mjs。
**不做**:候选窗口可配置;自动归属;其他候选消费方调整;历史数据修复工具(一次性修复走 set-attribution)。
## 涉及文件
src/storage/SqliteStore.jsgetAttributionCandidates)、src/client/views/TradeRecordsTab.jsxAttributionSelect)、scripts/test-r017-candidates.mjs(新增)。
## 实现步骤
1. R-017 + 本计划 + 三件套;2. 存储扩展;3. UI 重构;4. 回归 + typecheck + build + 存量回归;5. 复盘。
## 验收要点
`docs/04-迭代记录/15-归属候选纳入清仓持仓/验收标准.md`
@@ -0,0 +1,50 @@
# 计划:持仓内存快照(服务端 10s 定时同步,请求不再穿透 QMT)(阶段航点)
> 编号:PLAN-013 | 粒度:阶段航点 创建:2026-09-02 | 状态:**已实施(待验收)**
> 派生自终极目标:目标-008(真实交易系统接入)、目标-001(DSH 插件形态)
> 依据需求:**R-014(已定稿,2026-09-02,老师逐项拍板 4 问)** —— 符合入范围门槛
> 设计约束:技术约束-010(服务端中转 + 缓存模式沿用)、技术约束-012(存储设计)、技术约束-016(分域目录);本次新增 技术约束-017
> 补充说明:本计划为「先实施后补档」——代码随架构梳理讨论当场落地(老师指令),文档链(本计划 + 迭代三件套 + 约束)事后补全,事实以迭代复盘为准
## 目标
服务端建立全量持仓**内存快照**PositionSync10s 定时与 QMT 同步),策略持仓 / 全部持仓 / 未分配三个接口改读快照为准,**消除「QMT 抖动 → 持仓页面白屏」**并去掉每次进 tab 的穿透 HTTP 调用;前端零改动。
## 范围
**做**
1. PositionSyncsrc/position/PositionSync.js):启动预热 + 10s 定时全量拉 QMT → 校验 → 整体替换内存快照;同步失败**保留上次快照**(不清空不报错);
2. 空快照双重确认:/health 可用 + getAsset 账户身份可识别,二者兼备才接受为「真清仓」,否则视为异常保留旧快照;
3. 读穿透兜底:PositionManager.getAllPositions() 快照为空 → 当场拉一次 QMT 并回填;未注入 positionSync 时保持旧穿透行为(兼容);
4. 幽灵持仓自动清仓:QMT 快照连续 3 轮(约 30s)消失的 code,本地全部策略当前持仓 closeHolding 转历史;防抖护栏 = 账户身份守卫(accountId 未知当轮跳过;账户切换当轮重置计数并跳过);部分减持不触发;
5. index.js 装配(start/dispose/注入 manager 与 api runtime);回归脚本(纯内存 mock,技术约束-011 不受影响)。
**不做**
- 不落库(无 positions_cache 表;不建表、不改 SQLite schema)——R-014 讨论 #1 明确;
- 不复用 strategy_holdings(账本与对账单分离,holding_id 锚点语义不掺快照);
- 前端改动(零改动;同步时间显示本轮不做);
- 部分减持的自动修正(负数未分配仍由 UI 暴露,后续可另立需求);
- N+1 委托查询、api/trades.js 绕门面等既有观察点(另行登记)。
## 涉及文件
```
src/
├── position/PositionSync.js # 新增:持仓内存快照同步服务
├── position/PositionManager.js # getAllPositions 改读快照 + 读穿透兜底(兼容旧构造)
└── index.js # 装配 PositionSync + 注入
scripts/
└── test-position-sync.mjs # 回归脚本(纯内存 mock 34 项)
```
## 实现步骤
1. PositionSync 模块(快照 + 定时 + 校验 + 幽灵清仓);
2. PositionManager 读路径切换 + 读穿透 + 兼容分支;
3. index.js 装配;
4. 回归脚本(34 项)+ typecheck + build + r013 回归;
5. 文档链补全(本计划 + 迭代三件套 + 技术约束-017 + 数据存储设计 §11)。
## 验收要点
-`docs/04-迭代记录/12-持仓内存快照/验收标准.md`
@@ -0,0 +1,38 @@
# 计划:盘口内存快照(QuoteSync/QuoteHub + 同步指示灯)(阶段航点)
> 编号:PLAN-014 | 粒度:阶段航点 创建:2026-09-02 | 状态:**已实施(待验收)**
> 派生自终极目标:目标-003(按策略监控市场)、目标-008(真实交易系统接入)
> 依据需求:**R-015(已定稿,2026-09-02,老师逐项拍板)** —— 符合入范围门槛
> 设计约束:技术约束-010 变更(行情服务端中转改纯内存)、技术约束-012 变更(market_quotes_cache 退役)、技术约束-016(分域沿用);本次新增 技术约束-018、产品约束-012
## 目标
盘口数据与持仓数据对称收敛:QuoteSync/QuoteHub 纯内存管理(5s REST 同步 + 涨停跌停合约信息),market_quotes_cache 退役(DROP),价格单一入口;会话头部新增持仓/盘口同步指示灯(绿黄灰、点击即同步)。
## 范围
**做**
1. QuoteSync(新):启动 prime + 5s REST 刷新 watch 集合 + 涨停跌停按交易日拉取缓存 + 热切换重置;WS 通路不迁移(删除);
2. QuoteHub(新):内存快照 + 合约信息缓存 + watch 集合 + 读穿透(走 dataSource.getTicks+ getQuote(s) 对外;对外方法签名与旧 hub 兼容(getByCodes/watch/stats/getQuote);
3. 数据源适配:QmtBridgeRestDataSource 新增 getTicks(codes)/data/tick 语义化批量);
4. 存储清理:SqliteStore 删行情表读写 + DROP TABLE market_quotes_cache(幂等)+ migrateJson/isEmpty 联动;DataStore 删 loadMarket/getMarketQuote(s)/setMarketQuotes
5. APImarket-snapshot 返回加涨停/跌停/昨收/updatedAt;新增 sync-status(吸收 market-stats+ sync-now {domain}
6. 前端:QmtConnectionChip 加 SyncIndicators(两圆点,10s 轮询,点击即同步);MarketDataProvider 清理 wsInfo 残留;
7. 策略持仓表行情列(老师确认并入):COLUMN_META + 涨停价/跌停价/今开/最高(defaultVisible: false,纯价格);StrategyTab.renderDataCell 对应 casenormalizeStrategyColumns 缺省显隐跟随 defaultVisible
7. 回归脚本 test-quote-sync.mjs(纯内存 mock)。
**不做**:watch 收缩/降频;个股停牌标识;WS 相关需求推进;PriceCell 改动;最低/成交量/成交额列;距离涨停百分比展示。
## 涉及文件
- 新增:src/market/QuoteSync.js、src/market/QuoteHub.js、src/client/views/SyncIndicators.jsx、scripts/test-quote-sync.mjs
- 删除:src/market/MarketFeed.js、src/market/MarketDataHub.js
- 修改:src/data-source/QmtBridgeRestDataSource.jsgetTicks)、src/storage/SqliteStore.js、src/storage/DataStore.js、src/api/market.js、src/api/qmt-connections.js(热切换联动)、src/index.js、src/client/views/QmtConnectionChip.jsx、src/client/market/MarketDataProvider.jsx
## 实现步骤
1. 文档链(本计划 + 迭代三件套 + 约束条目);2. 数据源 getTicks3. QuoteHub + QuoteSync4. 存储清理 + DROP5. API 改造;6. 前端指示灯 + 清理;7. 回归脚本 + typecheck + build + 存量回归;8. 复盘。
## 验收要点
`docs/04-迭代记录/13-盘口内存快照/验收标准.md`
@@ -0,0 +1,49 @@
# 计划:策略 tab 历史持仓展示(显示/隐藏已清仓持仓 + 清仓时间范围筛选)(阶段航点)
> 编号:PLAN-015 | 粒度:阶段航点 创建:2026-09-07 | 状态:**已实施(待验收)**
> 派生自终极目标:目标-003(按策略监控市场)、目标-008(真实交易系统接入)
> 依据需求:**R-016(已定稿,2026-09-07Q1-Q7 老师拍板)** —— 符合入范围门槛
> 设计约束:新增 技术约束-019(历史持仓查询走本地库,当前持仓快照语义不掺历史)
## 目标
两个策略 tab(做T / 网格超市)title 旁提供「历史持仓」开关 + 清仓时间范围筛选(默认近一周,另有近 1 个月 / 近 3 个月 / 近半年 / 近 1 年);历史持仓行可展开查看关联交易记录(复用 R-010),补上「清仓即失联」的操作追溯缺口。
## 范围
**做**
1. **存储层**SqliteStore):新增 getHoldingsHistory(strategyId, { sinceMs }) —— closed_at 非空 + closed_at ≥ sinceMs 过滤 + 按 closed_at DESC 排序(读只读,零写路径变更,不改 closeHolding 语义);
2. **门面层**DataStore):getHoldingsHistory 透传(await _ensure 后委托);
3. **API**:新增 strategy-holdings/history 端点(args: strategyId 必填 + range ∈ week|month|quarter|halfYear|year,服务端换算 sinceMs;缺失/非法参数报参数错误);strategy-positions 等现有端点零改动;
4. **前端 StrategyTab**
- 头部按钮区(+ 添加持仓 / 列设置 旁)新增「历史持仓」开关按钮 + 范围下拉(仅开关开启时显示;默认 week);
- 开启时懒加载历史数据(切换范围重新拉取;关闭即隐藏不销毁缓存);进 tab 默认关(会话级,不持久化);
- 历史行渲染在当前持仓行之后:代码 / 名称(关联委托 name 兜底,无则 —)/ 份额(显示 DB 原值 0,Q2)/ 清仓时间 / 持有天数;实时行情列(现价/涨幅/涨停跌停/今开/最高)显示 —;
- 历史行灰显 + 「已清仓」徽标(含清仓日期);标题计数(N 只)只数当前持仓;
- 历史行行展开复用 R-010trades/by-holding,按 holdingId 懒加载);
- 自定义字段列历史行只读展示(values 随行返回,无编辑入口);
5. **回归脚本** scripts/test-r016-history.mjs(独立临时数据目录,技术约束-011)。
**不做**:全部持仓 tab;盈亏统计;清仓方式标记;历史行实时行情;成本价/收益推导;closeHolding 语义与任何存储写入路径变更;范围偏好持久化;「全部历史」档位。
## 涉及文件
- 修改:src/storage/SqliteStore.js、src/storage/DataStore.js、src/api/strategies.js、src/client/views/StrategyTab.jsx
- 新增:scripts/test-r016-history.mjs
## 实现步骤
1. 本文档链(PLAN + 迭代三件套 + 技术约束-019);2. SqliteStore.getHoldingsHistory + DataStore 门面;3. API 端点;4. StrategyTab 开关/范围/历史行渲染/展开复用;5. 回归脚本 + typecheck + build + 存量回归(test-position-sync / test-r013 / test-quote-sync);6. 复盘。
## 验收要点
`docs/04-迭代记录/14-策略tab历史持仓展示/验收标准.md`
## 二轮补充(2026-09-07 Q8-Q9 老师拍板:今日已清仓默认层)
- **补充范围**:已清仓行分两层——L1「今日已清仓」(range='today',本地自然日 00:00 起)默认恒显示、不受「历史持仓」开关控制;L2「历史范围层」(今天之前的 week/month/quarter/halfYear/year)仍由开关控制(默认关)。前端按 holdingId 去重两层(开启范围不重复出两行);标题计数不含已清仓行维持;零写路径变更维持。
- **不做(维持首轮)**:不做统计 / 不改 closeHolding / 范围偏好不持久化 / 全部持仓 tab 不加 / 不新增「全部历史」档。
## 验收要点(二轮)
见验收标准「二轮补充验收线 7-10」;自动化已复跑:test-r016 24/24 + 存量回归全绿。
@@ -0,0 +1,23 @@
# 计划:策略会话——会话以策略当前数据为依据做讨论(阶段航点)
> 来源:R-021(合并原 R-019+R-0212026-09-08 已定稿)| 状态:执行中(设计阶段先行)| 所属迭代:17-策略会话
> 粒度:阶段航点(覆盖设计 → 宿主调研 → 实现 → 验收全过程,随进展拆分小任务)
## 目标
让 AI 会话可以"以策略为 Workspace"(数据依据版):**对话开始前选「讨论策略」→ 服务端生成该策略当前数据摘要 → 预注入会话上下文 → 以该策略当前数据为依据进行复盘/分析讨论**;只读、会话中可换/刷新、产物不落盘。
## 范围与分阶段
1. **设计阶段(当前,老师指令:先做产品逻辑 + UI 交互逻辑)**:产出《产品逻辑设计》《UI交互设计》,D 系列问题老师拍板后定稿;
2. **宿主注入通道调研**:确认把外部摘要注入会话上下文的可行通道/形态(决定"折叠数据依据条"还是"文本消息形态"呈现;影响客户端渲染方案与注入动作实现);
3. **技术实现方案**(技术约束核对、服务端摘要服务 + 客户端选择 UI + 注入动作 + 回归);
4. **实现 + 验收**
## 验收指向
`docs/04-迭代记录/17-策略会话/验收标准.md`(实现阶段补充细化)。
## 边界
见 R-021 定稿边界(合并原 R-019+R-021):不用宿主工作区;不开放账本写;不做按需取数工具;复盘产物不落盘;本期不做通用视图(全部持仓/交易记录)作依据。
@@ -0,0 +1,55 @@
# 计划:策略持仓行展开关联交易记录(Holding → 交易汇总)(阶段航点)
> 编号:PLAN-009 | 粒度:阶段航点(大粒度) | 创建:2026-09-02 状态:**已完成(2026-09-02 迭代 08 老师确认)**
> 派生自终极目标:目标-006(交易复盘)、目标-003(按策略监控市场)
> 依据需求:**R-010(已定稿,2026-09-02Q1-Q3 确认)** —— 符合入范围门槛
> 设计约束:技术约束-012(数据存储设计)、技术约束-001/003/004(沿用)
## 目标
策略持仓 tab 每个持仓行(Holding)可展开,展开显示与该 holding 关联的交易记录(仅委托汇总,不分笔成交),实现「持仓 ↔ 交易」的双向追溯(R-009 反向:交易→持仓已实现,本迭代持仓→交易)。
## 范围
**做**
1. **strategy-positions 附加 holding_id**PositionManager.getStrategyPositions 返回时,查 strategy_holdings 附加当前持仓的 holding_idQ1);
2. **trades/by-holding 端点**:按 holding_id 查 trade_orders,返回委托汇总列表(时间/方向/状态/委托量/成交量/均价/金额/费用)(Q2);
3. **持仓行展开 UI**StrategyTab 持仓行加展开(类似交易记录 tab 展开效果),展开后内嵌小表格显示该 holding 的委托汇总;懒加载(展开时才请求)(Q3)。
**不做**
- 分笔成交明细展示(老师明确只要汇总);
- 交易记录 tab 改动;
- 归属设置入口(R-009 已有)。
## 程序结构
```
src/
├── component/
│ ├── PositionManager.js # 改:getStrategyPositions 附加 holding_id
│ ├── SqliteStore.js # 改:+getOrdersByHolding(holdingId)
│ └── DataStore.js # 改:+getTradeOrdersByHolding
├── api/
│ └── trades.js # 改:+trades/by-holding 端点
src/client/views/
└── StrategyTab.jsx # 改:持仓行展开 + 内嵌委托汇总表(懒加载)
```
## 实现步骤
1. **文档骨架**PLAN-009 + 迭代 08(本步);
2. **SqliteStore**getOrdersByHoldingWHERE holding_id=? ORDER BY insert_ts DESC);
3. **DataStore**:委托方法;
4. **PositionManager**getStrategyPositions 附加 holding_id
5. **api/trades.js**trades/by-holding 端点;
6. **StrategyTab**:持仓行展开 + 懒加载 + 内嵌汇总表;
7. **构建测试**pnpm run build + typecheck + 独立数据目录回归(技术约束-011);
8. **验收 + 复盘**
## 验收要点
- strategy-positions 每行含 holding_id(同策略同 code 当前持仓);
- trades/by-holding 返回该 holding 的委托汇总(无成交的委托也显示);
- StrategyTab 持仓行可展开,展开显示委托汇总表(不分笔成交);
- 懒加载:不展开不请求;
- 现有功能不回归。
@@ -0,0 +1,58 @@
# 计划:策略自定义字段配置(定义随策略,值落库)(阶段航点)
> 编号:PLAN-012 | 粒度:阶段航点 创建:2026-09-02 | 状态:**已完成(迭代 11 验收通过,2026-09-02**
> 派生自终极目标:目标-002(策略定义能力)、目标-007(人机合一)
> 依据需求:**R-013(已定稿,2026-09-02,老师确认 Q1-Q4 + D6)** —— 符合入范围门槛
> 设计约束:技术约束-012(策略定义仍存 DSH settings / SQLite 存储)沿用;本次新增 产品约束-010、技术约束-015、UI约束-005
## 目标
策略可**自定义、可扩展**:每个策略在设置页「策略分组」子 tab 配置自己的自定义字段定义(configSchema,含类型/枚举/默认值,随策略定义存 settings 不落库);该策略下的每个持仓(strategy_holdings 行)按定义存取一份键值对值(新增 values JSON 列落库);策略持仓 tab 持仓行展开区按定义渲染并编辑值。旧策略无定义的行为与现状完全一致。
## 范围
**做**
1. settings.strategies 扩展 configSchemaschemasteryarray of objecttype union text|number|boolean|enum + enum 选项 + def 默认值),读取归一化(旧项缺省 []);
2. strategy_holdings 新增 values TEXT(JSON) 列(幂等 ALTER,沿用 _ensureTradeAttributionColumns 模式,只读容忍);新增 readValues(holdingId) / writeValues(holdingId, values);现有 openHolding/addShares/reduceShares/closeHolding 保持 values 不随份额操作变动;
3. APIstrategy-positions 每行附 values;新增 holdings/values-update {holdingId, values} 写回(服务端按 configSchema 校验:数字有限数、枚举在选项内、布尔为布尔;允许额外键=可扩展;空值/缺省可写入);
4. 设置页「策略分组」子 tab:策略行可展开 → 展开区字段列表(label/key/type/enum/def+ 添加/编辑/删除字段(字段名/类型/选项/默认值表单)+ 保存走 strategies/update(整表);
5. 策略持仓 tab:持仓行展开区(R-010 基础上)增加「自定义字段」区块:按该策略 configSchema 渲染输入控件(文本=输入框、数字=数字输入、布尔=开关、枚举=下拉),值来自该行 values,编辑即保存调 values-update,保存成功 Toast 反馈;
6. 回归脚本(独立数据目录,技术约束-011):定义字段 → 批量写/改持仓行 values → 校验存储与读取、校验过界值拒绝。
**不做**
- 字段定义落库(定义随策略定义走,D1);
- 策略级(整策略一份)值(值=持仓级,Q1);
- T-006 数据池 / 表格列动态配置(独立需求,起草中);
- T-003 策略配置文件骨架(被本需求吸收,不另做);
- 删除策略时字段定义联动处理之外的数据迁移(兼容:旧策略无定义、旧行 NULL,不迁移,Q4)。
## 涉及文件
```
src/
├── settings.js # strategySchema 扩展 configSchema + 归一化
├── storage/SqliteStore.js # _ensureHoldingValuesColumn + readValues/writeValues
├── api/strategies.js # strategy-positions 附加 values + holdings/values-update 端点
├── client/views/SettingsSection.jsx # 「策略分组」子 tab 策略行展开配置字段
└── client/views/StrategyTab.jsx # 持仓行展开区「自定义字段」编辑
scripts/
└── test-r013-custom-fields.mjs # 回归脚本(独立数据目录)
```
## 实现步骤
1. **文档骨架**PLAN-012 + 迭代 11(迭代目标 / 技术实现方案 / 验收标准)+ 约束条目(产品约束-010、技术约束-015、UI约束-005)(本步);
2. **数据层**settings.js configSchema 扩展 + SqliteStore 补列/读写(幂等迁移验证);
3. **API 层**strategy-positions 附加 values + holdings/values-update(含 configSchema 校验);
4. **设置页 UI**:「策略分组」展开配置字段;
5. **策略持仓 UI**:展开区自定义字段编辑(文本/数字/布尔/枚举);
6. **回归脚本 + 验证 + 验收复盘**
## 验收要点
- 设置页「策略分组」:策略行可展开,添加/编辑/删除字段(四种类型 + 枚举选项 + 默认值)后保存,重启后定义仍在;
- 策略持仓 tab:持仓行展开可见「自定义字段」区块,四种类型控件按定义渲染,编辑值保存后刷新仍在(落库);
- 同策略多行各有各的值;不同策略字段集互不影响;
- 旧策略(无 configSchema)持仓行不出现字段区,现有操作(加仓/减仓/清仓/展开交易记录)不受影响;
- 服务端校验生效:数字填非数字、枚举填选项外拒绝并提示;
- typecheck + build 通过;回归脚本全绿。
@@ -0,0 +1,92 @@
# 计划:策略计算字段 + 策略 tab 静态化重构(阶段航点)
> 编号:PLAN-019 | 粒度:阶段航点 创建:2026-09-10 | 状态:**进行中(迭代 22 交互设计阶段)**
> 派生自终极目标:目标-002(策略定义能力)、目标-003(按策略监控市场)
> 依据需求:**R-027(已定稿,2026-09-10,重构·第一阶段)+ R-026(已定稿,2026-09-10,计算字段·第二阶段)** —— 均符合入范围门槛
> 设计约束:沿用 技术约束-012(策略定义存储)、技术约束-015configSchema)、UI约束-003/004/005/006/007
> 本次拟修订(实施时执行):产品约束-002/003/004/009、UI约束-002/003/005;新增计算字段约束条目
## 目标
**一句话**:策略字段能力升级为「可算」,并把字段配置的入口搬到它该在的地方。
- **第一阶段(R-027 重构)**:策略 tab **静态化**(插件内置、不可增删改名,保留显隐/排序);设置页「策略分组」移除;
字段编辑功能迁到**主窗口策略 tab 内、「列设置」旁的「字段配置」弹层**,保存走单策略字段端点;
- **第二阶段(R-026 计算字段)**:在迁移后的入口上新增第五种字段类型「**计算(formula)**」——用户写中文变量公式
引用服务端缓存数据集,服务端按持仓行**实时计算**列值(不落库、列只读、缺数据显示 `—`)。
**为什么合并成一个迭代**:计算字段的公式表单必须落在字段配置入口上;若先按旧入口(设置页)实现再搬迁,等于白做一遍。
## 范围
**第一阶段 · 重构(R-027**
1. **tab 体系**`strategies` 常量化为内置两项(网格超市 / 手动做T);`settings.tabs` 中策略条目固定(不可增删);
保留 Tab 设置的显隐 + 拖拽排序;`addStrategy / removeStrategy / appendStrategyTab / removeStrategyTab / generateStrategyId` 退役;
2. **设置页**:移除「策略分组」子 tab(含策略新增/重命名/删除与确认弹窗)→ 设置页 = Tab 设置 + QMT 连接配置;
3. **字段配置入口**:策略 tab 顶部新增「字段配置」按钮(列设置旁)→ 独立弹层(字段列表 + 添加/编辑/删除 + 保存);
`StrategyFieldsEditor` 改造为弹层内容;保存改走新增端点 `strategies/schema-update { strategyId, configSchema }`
4. **存储归一化**:settings 只保留字段定义(按 strategyId+ `strategyColumns` 覆盖;存量 settings 读取时归一化(无迁移脚本)。
**第二阶段 · 计算字段(R-026)**
5. **轻量变量目录**(服务端):数据集 → 中文变量名 → 取数函数(行情+合约 / 持仓账本 / 本行自定义字段值),T-006 数据池的第一块砖;
6. **公式引擎**:自写小型表达式解析器(零依赖、支持中文标识符、四则 + 括号 + round/abs/min/max),含语法校验与求值;
7. **数据/API**`configSchema` 支持 `type='formula'` + 公式串 + 小数位;`strategy-positions` 逐行现算随行返回;
字段保存校验(语法 / 变量存在性 / 变量命名规则)+ **试算端点**(取一行真实持仓试算);
8. **UI**:字段配置弹层内公式分支(公式框 + 变量选择器 + 试算 + 单位/小数位);策略持仓表计算列只读渲染(`ƒ` 表头 + 悬停看公式 + 格式化 + 缺数据 `—`);
9. 交互设计 → 技术方案 → 实现 → 回归脚本(独立数据目录,技术约束-011)→ 验收。
**不做**(本期边界):
- 策略级聚合(跨行求和)——留二期;QMT 账户域 / 交易记录作为变量来源(用例不明确,后补);
- 公式引用公式(变量目录排除 formula 字段);历史持仓行时点计算(显示 `—`);布尔型公式结果;公式框实时高亮/自动补全;
- 完整 T-006 数据池中间层(本期只落变量目录);
- 策略的重命名 / 增删 / 归档(静态化后不存在);自建策略数据迁移(实测无自建策略)。
## 涉及文件(技术方案阶段细化)
```
src/
├── settings.js # strategies 常量 + tabs 固定 + 字段定义/列配置归一化;退役 CRUD 辅助
├── api/strategies.js # 策略 CRUD 端点退役;新增 strategies/schema-update(单策略字段)
├── formula/ # 新增:公式引擎(第二阶段)
│ ├── variables.js # 轻量变量目录(数据集 → 中文变量名 → 取数函数)
│ └── evaluator.js # 表达式解析 + 校验 + 求值
├── position/PositionManager.js # strategy-positions 行内公式现算
├── client/views/SettingsSection.jsx # 移除「策略分组」子 tab;Tab 设置保留
├── client/views/StrategyTab.jsx # 顶栏「字段配置」+「列设置」;计算列只读渲染
├── client/views/FieldConfigDialog.jsx # 新增:字段配置弹层(复用/改造 StrategyFieldsEditor
└── client/views/StrategyFieldsEditor.jsx # 改造为弹层内容 + 单策略保存 + 公式分支
scripts/
└── test-r026-formula-fields.mjs # 回归脚本(第二阶段)
```
## 实现步骤
1. **文档骨架**PLAN-019 + 迭代 22(迭代目标 / UI交互设计),R-026 + R-027 定稿 —— 本步;
2. **交互设计过审**(老师拍板)→ 出技术实现方案(两阶段规格)+ 验收标准;
3. **第一阶段实现**:tab 静态化 + 设置页收敛 + 字段配置弹层 + 单策略端点 + 存量归一化 → 回归(策略 CRUD 相关用例退役);
4. **第二阶段实现**:变量目录 + 公式引擎 + 现算/校验/试算端点 + UI(公式表单 / 只读列)→ 回归;
5. **验证 + 老师人工验收 + 迭代复盘**
## 验收要点
**第一阶段(重构)**
- 设置页只有「Tab 设置 / QMT 连接配置」,无「策略分组」;无新增/重命名/删除策略入口;
- 策略 tab 恒为「网格超市 / 手动做T」两个:不可增删改名;Tab 设置里仍可显隐 + 拖拽排序(现有偏好生效);
- 策略 tab 顶部「字段配置」按钮打开弹层:可添加/编辑/删除字段并保存;保存后表格列同步跟随;重启后定义仍在;
- 服务端只接受内置两个 strategyId;`strategy_holdings` / 归属数据零影响(13 / 3 行不变)。
**第二阶段(计算字段)**
- 字段类型可选「计算」:写中文变量公式、选择器插入、试算可算出结果;非法公式/未知变量被拒并提示;
- 策略持仓表计算列只读,按单位/小数位格式化,悬停可见公式;缺数据/除零显示 `—`,不炸表;
- 计算字段不落库(公式存 settings、值每次现算);四式用例(浮动盈亏 / 盈亏比例 / 距涨停 / 网格占用)可算出;
- 既有四类字段与手填值编辑、列设置、历史持仓展示零回归;
- typecheck + build + 回归脚本通过;老师人工验收。
## 关联
- 需求:**R-027**(重构·第一阶段)、**R-026**(计算字段·第二阶段);上游 R-013(字段模型/列机制)、R-011(Tab 显隐排序)、R-015(行情/合约缓存)、R-010(最后一笔成交价)、R-018(份额账本);
- 取代:R-003、R-011(部分)、R-013(入口条款)——由 R-027 取代;
- 延展:T-006(数据池中间层草稿)——变量目录为其起点;
- 迭代:22-策略计算字段与策略tab静态化(两阶段)。
+10 -1
View File
@@ -10,7 +10,16 @@
| 编号 | 约束说明 | 添加日期 | 生效状态 | 失效日期 | 最后一次变更描述 | | 编号 | 约束说明 | 添加日期 | 生效状态 | 失效日期 | 最后一次变更描述 |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| UI约束-001 | 会话头部 QMT 连接切换 chip:紧凑形态 `QMT: <激活配置名> ▾`,与 PTC 模式标签并排;下拉菜单列出全部配置(当前激活项勾选标记),点选即激活;操作结果用顶部轻提示反馈(成功绿/失败红,约 3s 消失,沿用现有 Toast 体系) | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q9/Q10 确认的头部控件形态 | | UI约束-001 | 会话头部 QMT 连接切换 chip:紧凑形态 `QMT: <激活配置名> ▾`,与 PTC 模式标签并排;下拉菜单列出全部配置(当前激活项勾选标记),点选即激活;操作结果用顶部轻提示反馈(成功绿/失败红,约 3s 消失,沿用现有 Toast 体系) | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q9/Q10 确认的头部控件形态 |
| UI约束-002 | 设置页「QMT 连接配置」子 tab:与「通用设置 / 策略分组」并列的第三个子 tab;配置以**卡片形式**展示(自适应网格 minmax(260px,1fr),激活卡片绿色描边):卡片含名称 + 激活/默认徽标 + HTTP 地址 + 行内操作(激活 / 设为默认 / 编辑 / 测试连接 / 删除)+ 新增/编辑表单(纵排两字段);删除沿用现有确认弹窗模式,删除最后一条时给出禁止提示;超时不设输入框(Q5);长文本(地址/错误信息)单行省略 + 悬停看全文,组件设 minWidth:0 防撑行 | 2026-08-29 | 生效 | - | 变更(2026-08-29 老师反馈):列表形式改卡片形式,约束组件宽度、长文本省略不换行;原定稿:设置页子 tab 形态沿用现有设置交互体系 | | UI约束-002 | 设置页「QMT 连接配置」子 tab:与「Tab 设置」并列的子 tab(R-027 后设置页仅此两个子 tab;配置以**卡片形式**展示(自适应网格 minmax(260px,1fr),激活卡片绿色描边):卡片含名称 + 激活/默认徽标 + HTTP 地址 + 行内操作(激活 / 设为默认 / 编辑 / 测试连接 / 删除)+ 新增/编辑表单(纵排两字段);删除沿用现有确认弹窗模式,删除最后一条时给出禁止提示;超时不设输入框(Q5);长文本(地址/错误信息)单行省略 + 悬停看全文,组件设 minWidth:0 防撑行 | 2026-08-29 | 生效 | - | 变更(2026-08-29 老师反馈):列表形式改卡片形式,约束组件宽度、长文本省略不换行;原定稿:设置页子 tab 形态沿用现有设置交互体系;变更(2026-09-02 R-011 定稿):「通用设置」更名为「Tab 设置」;**变更(2026-09-10 R-027**:移除「策略分组」子 tab,设置页子 tab 由三变二 |
| UI约束-003 | 设置页「Tab 设置」子 tab:表格列出**全部会话 tab(系统内置 + 策略分组)混排**,内置行带「内置」徽标、策略行带「策略」徽标;每行 = 拖动手柄(原生 HTML5 Drag & Drop+ 名称 + 显示/隐藏开关;**任何行均无重命名/删除按钮**;拖动落点确认后**立即持久化**(每次 drop 提交整表,沿用「刷新页面后生效」提示机制);**落点指示为行间间隙高亮线**(迭代 21,与列设置弹层一致,见 UI约束-007);「策略分组」子 tab 已移除(R-027 策略静态化),设置页不再有任何策略增删改名入口 | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1/Q4/Q5 确认):拖动排序 + 显隐开关,禁重命名/删除;变更(2026-09-10 R-025 老师指令):落点从整行高亮改行间间隙高亮线;**变更(2026-09-10 R-027**:策略 tab 静态化、策略分组子 tab 移除 |
| UI约束-005 | 策略自定义字段配置 UI(R-013 → **R-027 入口迁移 + 现状更正**):**入口** = 策略 tab 顶部「列设置」**旁**的「字段配置」按钮 → 独立弹层(约 520px 宽、70vh 高、内部滚动,锚定弹层视觉同列设置):字段列表(展示名 / key / 类型 / 公式或默认值 / 单位 / 操作)+ 添加/编辑/删除字段表单(名称/类型/枚举选项/默认值/单位)+ 底部 [关闭] + [保存字段](保存走 strategies/schema-update 单策略写入);有未保存改动时关闭需二次确认;**现状更正**:字段**值**不再是「持仓行展开区编辑」,而是**作为持仓表列展示 + 单元格点击内联编辑**(R-013 §4 演进:文本=输入框、数字=数字输入、布尔=开关、枚举=下拉,编辑即保存调 holdings/values-update + Toast);列显隐/顺序由「列设置」弹层管理(每策略独立);未配置字段定义的策略该列不存在 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-013 定稿 D6);**变更(2026-09-10 R-027/R-026 实施)**:入口由设置页「策略分组」迁至策略 tab 内「字段配置」弹层;同时更正文案到现状(值编辑 = 表格列内联编辑,展开区仅保留交易明细) |
| UI约束-004 | 神之一手 UI 适配 DSH 主题:所有颜色引用宿主 `--dsw-*` token(带 fallback `var(--dsw-alias-xxx, #原色)`),随 DSH 浅色 / 深色 / 跟随系统自动切换,不自行维护主题偏好(跟随宿主);语义映射:表面底→bg-layer-1、次级面→bg-layer-2、hover→interactive-bg-hover、主/次/弱文字→label-primary/secondary/tertiary、边框→border-l1..l4、红(涨/删/错)→state-error-primary、绿(跌/成/激活)→state-success-primary、蓝(信息/业务)→state-business-primary、实心按钮字→button-contrast-fill、遮罩→bg-mask-1、阴影→shadow-lv3;淡底用 color-mix(in srgb, var(--语义色) 10-12%, transparent);禁止硬编码色值(#hex/rgb)进运行代码 | 2026-09-02 | 生效 | - | R-012 定稿(2026-09-02,暂定跟随系统)+ 迭代 10 实施:141 处硬编码色 token 化,宿主机制实证(body[data-ds-dark-theme] + alias token 双值定义) |
| UI约束-006 | 展开/收起箭头全站统一:使用共享组件 `ExpandChevron`src/client/views/ExpandChevron.jsx)——14×14 chevron-right、描边圆角、主题 success 绿、展开时旋转 90°(0.15s 过渡);适用:策略持仓 tab / 交易记录 tab 表格行、设置页「策略分组」行首箭头(原 ▸/▾ 文字箭头废弃);不得再散落内联 SVG 或文字符号实现 | 2026-09-03 | 生效 | - | 新增(2026-09-03 老师反馈:设置页策略分组箭头与策略 tab 表格行展开图标一致;顺带消除两处内联 SVG 重复) |
| UI约束-007 | 列表/弹层排序交互:优先**原生 HTML5 拖拽排序**draggable 行 + ≡ 拖拽手柄 + drop 重排 + 落点立即持久化,行之间允许 ↑↓ 箭头兜底微调);**落点指示用间隙高亮线**(两行之间的主色线,指示精确插入位置/分清前与后,不做整行高亮)——策略 tab 列设置弹层(R-024)与设置页 Tab 设置(R-025)均已采用;不做表头直拖列排序(兼容成本高,需处理排序点击/固定列冲突) | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-024 定稿:策略 tab 列设置弹层从 ↑↓ 升级为拖拽排序,复用 Tab 设置已验证模式,去掉「保存」按钮改立即持久化);变更(2026-09-10 老师反馈:整行高亮无法判断插入前/后,落点改行间间隙高亮线);变更(2026-09-10 R-025 老师指令:设置页「Tab 设置」落点统一为同款间隙线,整行高亮全站废止) |
| UI约束-008 | 计算字段 UI(R-026):字段类型下拉含「计算」——选中后**隐藏默认值**,显示公式输入框(等宽字体)+ 变量**标签平铺**(行情 / 合约 / 持仓 / 自定义字段 分组成行,每组 = 灰色组名 + 胶囊形 tag,点选即插入光标处;不自弹下拉)+「试算」按钮(用该策略一行真实持仓数据算出结果行内展示:`试算:平安银行 000001.SZ → 6.67 %`;无持仓提示「暂无可试算的持仓数据」)+ **结果类型下拉(数字 / 判定,R-028)**:选「判定」时**隐藏小数位**、公式框 placeholder 切换为判定示例(如 `涨停价 > 基准值 + 网格大小`),公式框下方常驻运算符提示(`运算:+ - × ÷ 括号;判定:> < >= <= == != ,组合:and / or`+ 判定型试算显示 `→ ✓ 满足 / — 不满足`;数字型仍为小数位输入(0-4,默认 2);保存失败时公式框**描红**(state-error)+ 显示服务端提示,表单不关闭、改动保留;持仓表中计算列**只读**:表头列名前缀 `ƒ`tertiary 色,title="计算字段(只读)")、列头与单元格 title 显示公式原文(`= <公式>`)、**判定型(R-028)**:满足 → `✓ <字段展示名>`(success 绿),不满足或数据缺失 → `—`(tertiary 灰,两者同形以免把「缺数据」误读为「不可以」);数字型按小数位 + 单位格式化(如 `6.67 %``700.00 元`)、缺数据/计算失败显示 `—`(tertiary 色)、点击不进内联编辑;历史持仓行显示 `—`;列设置弹层中该列名同样带 `ƒ` 前缀 | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-026 定稿 + D-1~D-7 老师拍板:中文变量名 + 选择器插入、表头 ƒ 前缀 + 悬停看公式、本期做试算、已删变量 ⚠ 标记、小数位 0-4 默认 2、不做实时高亮);**变更(2026-09-10 R-028)**:新增「结果类型(数字/判定)」下拉与判定列渲染(`✓ 字段名` / `—`+ 判定试算;**变更(2026-09-10 老师反馈)**:变量选择从「+ 插入变量 ▾」下拉改为**标签平铺**(分组成行、点选插入),弃用下拉交互;判定型隐藏小数位 |
<!-- 示例条目(确认格式后删除): <!-- 示例条目(确认格式后删除):
| UI约束-001 | 示例:求签页面必须保持单屏完整,不出现滚动 | 2026-08-26 | 生效 | - | 讨论确认:移动端优先,避免滚动打断仪式感 | | UI约束-001 | 示例:求签页面必须保持单屏完整,不出现滚动 | 2026-08-26 | 生效 | - | 讨论确认:移动端优先,避免滚动打断仪式感 |
+10 -2
View File
@@ -11,12 +11,20 @@
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 产品约束-001 | 分仓管理以「全量持仓」为数据基础:系统必须完整展示账户全部持仓信息(代码/名称/数量/可用/均价/现价/市值/盈亏),不遗漏、不裁剪 | 2026-08-27 | 生效 | - | 讨论确认(R-002):老师要求「首先肯定支持全部持仓信息」 | | 产品约束-001 | 分仓管理以「全量持仓」为数据基础:系统必须完整展示账户全部持仓信息(代码/名称/数量/可用/均价/现价/市值/盈亏),不遗漏、不裁剪 | 2026-08-27 | 生效 | - | 讨论确认(R-002):老师要求「首先肯定支持全部持仓信息」 |
| 产品约束-002 | 分仓通过「持仓标签」体系实现:在全量持仓之上,用不同标签对持仓进行逻辑分组,每个标签展示其下的仓位汇总(数量/市值/盈亏/占比) | 2026-08-27 | 生效 | - | 讨论确认(R-002):老师要求「分不同的持仓标签,各自有多少仓位」 | | 产品约束-002 | 分仓通过「持仓标签」体系实现:在全量持仓之上,用不同标签对持仓进行逻辑分组,每个标签展示其下的仓位汇总(数量/市值/盈亏/占比) | 2026-08-27 | 生效 | - | 讨论确认(R-002):老师要求「分不同的持仓标签,各自有多少仓位」 |
| 产品约束-004 | 删除持仓策略时,该策略下已分配的份额自动回到「未分配」,数据不丢失(删除前需弹窗确认) | 2026-08-28 | 生效 | - | R-003 O3 定稿(2026-08-28:删除策略 = 份额回未分配 + 弹窗确认 | | 产品约束-004 | 删除持仓策略时,该策略下已分配的份额自动回到「未分配」,数据不丢失(删除前需弹窗确认) | 2026-08-28 | **失效** | 2026-09-10 | **失效(2026-09-10 R-027 策略静态化)**:策略不可删除,本条款无适用场景,随之退役(原定稿:删除策略 = 份额回未分配 + 弹窗确认 |
| 产品约束-003 | 持仓标签可自定义、可增删改;标签维度候选包括策略/用途/风险等级等(具体维度待讨论确认) | 2026-08-27 | 生效 | - | 讨论确认(R-002):标签体系需支持自定义,维度待细化 | | 产品约束-003 | 持仓标签(策略)**集合与名称固定**为插件内置两项(网格超市 / 手动做T):不可新增、不可删除、不可重命名(R-027 策略静态化);「可自定义」收窄为**标签下的字段定义可自定义**(见 产品约束-010 / 产品约束-014);策略 tab 仍支持显隐与拖拽排序(见 产品约束-009) | 2026-08-27 | 生效 | - | **变更(2026-09-10 R-027**:原「标签可自定义、可增删改」(维度候选策略/用途/风险等级)收窄为「固定两标签 + 字段自定义」;老师重构指令「策略 tab 做成静态的,和全部持仓、交易记录一样是插件内置 tab」 |
| 产品约束-005 | QMT 连接配置多配置管理:设置页「QMT 连接配置」子 tab 维护多个连接配置(名称 + HTTP 地址,增/删/改/查);一次只能激活一个,激活配置为当前生效连接;默认 = 插件启动时自动激活的配置(运行中手动激活其他配置立即生效,重启回到默认);测试连接为手动操作(请求 /health 返回可达性与延迟,激活不强制先测);删除边界:删除激活配置自动切换到默认,删除默认配置则默认标记转移到列表第一条并激活之,禁止删除最后一条 | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q1-Q6/Q8 逐条确认(不迁移宿主配置、超时不纳入表单) | | 产品约束-005 | QMT 连接配置多配置管理:设置页「QMT 连接配置」子 tab 维护多个连接配置(名称 + HTTP 地址,增/删/改/查);一次只能激活一个,激活配置为当前生效连接;默认 = 插件启动时自动激活的配置(运行中手动激活其他配置立即生效,重启回到默认);测试连接为手动操作(请求 /health 返回可达性与延迟,激活不强制先测);删除边界:删除激活配置自动切换到默认,删除默认配置则默认标记转移到列表第一条并激活之,禁止删除最后一条 | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q1-Q6/Q8 逐条确认(不迁移宿主配置、超时不纳入表单) |
| 产品约束-007 | 3 个监控表格(全部持仓 / 手动做T / 网格超市)展示**盘中现价**:现价列显示 lastPrice,红涨绿跌着色(对比昨收),价格变化轻微高亮;本期仅现价一项实时数据 | 2026-08-31 | 生效 | - | R-005 定稿(2026-08-31):老师确认现价列 + 涨跌色 + 变化高亮 | | 产品约束-007 | 3 个监控表格(全部持仓 / 手动做T / 网格超市)展示**盘中现价**:现价列显示 lastPrice,红涨绿跌着色(对比昨收),价格变化轻微高亮;本期仅现价一项实时数据 | 2026-08-31 | 生效 | - | R-005 定稿(2026-08-31):老师确认现价列 + 涨跌色 + 变化高亮 |
| 产品约束-006 | QMT 连接会话头部快捷切换:会话窗口顶栏(PTC 模式标签旁)常驻下拉控件(chip 显示 `QMT: <激活配置名>`),点开列出全部配置(激活项勾选),点选即激活并轻提示反馈;头部控件仅做切换,配置管理(增删改/测试连接/默认标记)仍在设置页子 tab | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q9/Q10 确认(入口=会话头部 PTC 旁,菜单仅切换激活) | | 产品约束-006 | QMT 连接会话头部快捷切换:会话窗口顶栏(PTC 模式标签旁)常驻下拉控件(chip 显示 `QMT: <激活配置名>`),点开列出全部配置(激活项勾选),点选即激活并轻提示反馈;头部控件仅做切换,配置管理(增删改/测试连接/默认标记)仍在设置页子 tab | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q9/Q10 确认(入口=会话头部 PTC 旁,菜单仅切换激活) |
| 产品约束-008 | 交易记录支持**按策略过滤**:交易记录 tab 提供策略过滤下拉(全部 / 各策略 / 未关联),对今日(QMT 实时)与历史(本地 SQLite)均生效;策略归属 = 委托时间 join 持仓生命周期窗口推导(一码多策略取份额最大,未命中=未关联);历史范围展示本地积累数据(不再「接口开发中」占位) | 2026-09-01 | 生效 | - | 新增(2026-09-01 R-009 定稿 + 迭代 07 实施):策略过滤 + 历史本地展示 |
| 产品约束-010 | 策略自定义字段配置:每个策略在**策略 tab 顶部「列设置」旁的「字段配置」按钮(独立弹层)**中配置本策略的字段定义(字段名 / 类型文本·数字·布尔·枚举·**计算** / 枚举选项 / 默认值 / 单位 / 计算字段的公式与小数位),定义存 settings(不落库);该策略下每个持仓(strategy_holdings 行)按所属策略的定义存取一份字段值(values JSON 列,持仓级键值对,key 对齐定义、允许扩展额外键);旧策略无定义时行为与现状一致(不渲染字段区、不迁移历史值) | 2026-09-02 | 生效 | - | **变更(2026-09-10 R-027/R-026**:入口由设置页「策略分组」迁到策略 tab 内「字段配置」弹层;类型新增「计算」(见 产品约束-014);定义真相源迁 settings.strategyFields(原定稿:设置页策略分组配置、四类型、定义随策略存 settings) |
| 产品约束-009 | 会话 tab 统一由「Tab 设置」管理:设置页「Tab 设置」子 tab 是**所有会话 tab(系统内置 + 策略)的唯一顺序与显隐入口**,两类 tab 同权混排;每行 = 拖动排序(间隙高亮线)+ 显示/隐藏开关;**任何 tab 均不支持重命名与删除**;R-027 静态化后策略 tab 恒存在(不随任何 CRUD 增删),故「策略改名跟随 / 新增追加末尾 / 删除联动」等条款失效;顺序与显隐唯一数据源 = settings.tabs(由内置常量序列 + 存量偏好归一化得出) | 2026-09-02 | 生效 | - | **变更(2026-09-10 R-027 策略静态化)**:策略 tab 与内置 tab 完全同权且恒存在;原「策略命名/删除在策略分组子 tab」及联动增删条款失效(设置页「策略分组」子 tab 已移除) |
| 产品约束-011 | 持仓页数据(策略持仓 / 全部持仓 / 未分配)以服务端 10s 内存快照为准,**接受最多 10s 滞后**(同步时间前端不显示,老师拍板);QMT 抖动/掉线时持仓页面显示**最后一次快照**而非空白;**幽灵自动清仓退役(变更 1,2026-09-08 R-018 数据域分界)**:同步机制不再把本地策略持仓自动转历史——策略持仓份额只由「交易关联(归属=账本写操作)」与「手动份额操作」驱动,账本转历史唯一途径 = 卖出单关联份额减至 0;对账单码消失仅表现为快照无此行 + **漏关联软提示**(只读提示去关联/移出,不自动写账本);部分减持仅表现为「未分配为负」,不做自动修正 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-014 定稿);**变更 12026-09-08 R-018/迭代 16 拍板)**:幽灵自动清仓退役(同步机制不写账本),策略持仓由交易关联 + 手动份额操作驱动;漏关联软提示替代自动归档 |
| 产品约束-012 | 行情数据服务形态(R-015):现价/昨收/涨停/跌停统一由盘口内存快照提供(≤5s 更新),价格单一入口;QMT 抖动时页面价格保持旧值不空白;会话头部(QMT 健康灯旁)提供**持仓/盘口同步指示灯**——绿=同步正常(持仓 30s/盘口 15s 内)、黄=同步失败中快照陈旧、灰=从未同步;悬停显示同步时间/快照量/失败数/错误摘要;点击灯 = 立即触发该域同步;不做个股停牌标识(另议) | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-015 定稿):老师提出指示灯,采纳 AI 推荐三态/点击即同步/取消 PriceCell 灰点 |
| 产品约束-013 | 策略 tab 静态化(R-027):会话 tab 中的策略 tab 恒为**插件内置两项**(网格超市 / 手动做T),与「全部持仓 / 交易记录」同类的内置 tab —— **不可新增、不可删除、不可重命名**;仍支持 Tab 设置中的显隐与拖拽排序;设置页不再有「策略分组」子 tab(设置页 = Tab 设置 / QMT 连接配置);策略字段配置入口 = 策略 tab 顶部「列设置」**旁**的「字段配置」按钮(独立弹层),每个策略 tab 各管本策略字段 | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-027 定稿 Q1-Q4/Q6 老师拍板):静态化程度=固定不可增删改名但保留显隐/排序;入口形态=列设置旁独立按钮;范围=每策略管自己的字段 |
| 产品约束-014 | 策略计算字段(R-026):字段类型新增「计算(formula)」——用户在字段配置中写**公式**(**中文变量名**,如 `(现价 - 成本价) × 份额`),公式引用**服务端缓存数据集**(行情盘口 / 合约信息(涨停跌停)/ 持仓账本(份额·成本价·最后成交价)/ 本行手填字段值),由服务端按**持仓行实时计算**得出列值;计算字段**不落库**(不写 values 列)、在持仓表中**只读**(不可内联编辑);变量**只能引用非计算字段**(零循环依赖);缺数据 / 除零 / 无法计算 → 该格显示 `—`(不显示 NaN、不报错、不炸表);历史持仓行显示 `—`(不做历史时点计算);**结果类型两种(R-028 扩展)**:`resultKind = number | boolean` —— **数字**(默认,按小数位+单位格式化)与**判定**(比较运算结果:满足显示 `✓ <字段名>`(success 绿)、不满足或数据缺失显示 `—`),结果类型在字段表单**显式声明**且必须与公式形态一致(服务端校验);公式能力边界 = 四则运算 + 括号 + 比较(`> < >= <= == !=`+ 逻辑组合(`and / or`,含 `&& ||` 与全角 `≥ ≤ ≠`+ round/abs/min/max,禁止任意代码执行 | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-026 定稿 Q1-Q3 + Q4-Q10 + D-1~D-7 老师拍板):数据集首批三组、中文变量名、仅行级、服务端现算、只读列、缺值 —;**变更(2026-09-10 R-028)**:原「首批结果只做数字」扩展为「数字 / 判定」双类型 + 逻辑组合,首个落地用例 = 网格超市「可下空单 / 可下多单」 |
<!-- 示例条目(确认格式后删除): <!-- 示例条目(确认格式后删除):
| 产品约束-001 | 示例:求签功能必须保证抽取结果的不可预测性 | 2026-08-26 | 生效 | - | 讨论确认:为保证公平性,抽取必须不可预测 | | 产品约束-001 | 示例:求签功能必须保证抽取结果的不可预测性 | 2026-08-26 | 生效 | - | 讨论确认:为保证公平性,抽取必须不可预测 |
--> -->
+16 -2
View File
@@ -17,11 +17,25 @@
| 技术约束-006 | 客户端 slots.register 的 component 必须是第二参数(register({...}, Component));settings schema 必须用 schemastery z.object() 函数式定义 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:component 位置错误致 React #130;普通对象 schema 报 schema is not a function | | 技术约束-006 | 客户端 slots.register 的 component 必须是第二参数(register({...}, Component));settings schema 必须用 schemastery z.object() 函数式定义 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:component 位置错误致 React #130;普通对象 schema 报 schema is not a function |
| 技术约束-007 | 插件安装用 dsh plugin add(自动 reconcile bundles),不直接用 pnpm addbundle patch 顶层必须是 insert 操作 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:pnpm add 不会更新 dsh.profile.bundles | | 技术约束-007 | 插件安装用 dsh plugin add(自动 reconcile bundles),不直接用 pnpm addbundle patch 顶层必须是 insert 操作 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:pnpm add 不会更新 dsh.profile.bundles |
| 技术约束-008 | QMT 连接配置存储复用 one-divine-lot settings namespace(新增 qmtConnections 字段:list[{id,name,baseUrl,order}] + activeId + defaultId),与策略配置同机制持久化;激活切换 = 更新数据源实例的 baseUrl(数据源按请求读取地址,已核实),立即生效无需重启 DSH;插件启动时激活默认配置(列表为空时回退 cordis 注入的 qmtBaseUrl 兜底,不做自动迁移);测试连接由服务端代理请求 {baseUrl}/health(避免浏览器跨域) | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q1/Q2/Q4/Q5/Q8 确认(启动自动激活默认、立即切换、不迁移、超时不配置化、复用 settings) | | 技术约束-008 | QMT 连接配置存储复用 one-divine-lot settings namespace(新增 qmtConnections 字段:list[{id,name,baseUrl,order}] + activeId + defaultId),与策略配置同机制持久化;激活切换 = 更新数据源实例的 baseUrl(数据源按请求读取地址,已核实),立即生效无需重启 DSH;插件启动时激活默认配置(列表为空时回退 cordis 注入的 qmtBaseUrl 兜底,不做自动迁移);测试连接由服务端代理请求 {baseUrl}/health(避免浏览器跨域) | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q1/Q2/Q4/Q5/Q8 确认(启动自动激活默认、立即切换、不迁移、超时不配置化、复用 settings) |
| 技术约束-012 | 插件数据存储遵循 **docs/03-设计约束/数据存储设计.md**:存储引擎为 **SQLitenode:sqlite**——strategy_holdings + market_quotes_cache 两表(持仓生命周期表 + 行情极简两列缓存表);存储层单票生命周期操作;策略定义仍存 DSH settings;旧 JSONstore.json / store.market.json / allocations.json)经一次性迁移脚本 + 启动自动迁移(幂等、迁移前自动备份)后废弃;仅存储引擎替换,对外行为不变 | 2026-09-01 | 生效 | - | 变更(2026-09-01 R-008 定稿 + 迭代 06 实施):JSON data store → SQLitenode:sqlite,零依赖分发;strategy_holdings 持仓生命周期表+market_quotes_cache 两表;存储层单票生命周期;自动迁移;JSON 废弃);R-006 原 JSON 设计为上一版本基线 | | 技术约束-012 | 插件数据存储遵循 **docs/03-设计约束/数据存储设计.md**:存储引擎为 **SQLitenode:sqlite**——strategy_holdings + ~~market_quotes_cache~~**R-015 退役 DROP2026-09-02**;行情改内存快照)+ **trade_orders + trade_fills(交易记录两表,R-009**;存储层单票生命周期操作;交易记录两表零冗余 strategy_id/holding_id(外键链推导策略归属);策略定义仍存 DSH settings;旧 JSONstore.json / store.market.json / allocations.json)经一次性迁移脚本 + 启动自动迁移(幂等、迁移前自动备份)后废弃;仅存储引擎替换,对外行为不变 | 2026-09-01 | 生效 | - | 变更(2026-09-01 R-008 定稿 + 迭代 06 实施):JSON data store → SQLite;变更(2026-09-01 R-009 定稿 + 迭代 07 实施):+trade_orders/trade_fills 交易记录两表;**变更 32026-09-02 R-015/迭代 13**market_quotes_cache 退役(DROP),行情移出 SQLite |
| 技术约束-011 | 测试/回归脚本**禁止在真实数据上执行写操作**:份额写操作(add/remove/move/clear)必须使用独立数据目录(AllocationStorage 支持 ODL_TEST_DATA_DIR 环境变量或 dataDir 参数指向临时目录),只读端点(positions/summary/strategies/market-snapshot)可直连生产 API | 2026-08-31 | 生效 | - | 2026-08-31 数据误删事故沉淀:回归测试误删大连热电/万顺新材份额分配,老师定「测试用独立数据目录」 | | 技术约束-011 | 测试/回归脚本**禁止在真实数据上执行写操作**:份额写操作(add/remove/move/clear)必须使用独立数据目录(AllocationStorage 支持 ODL_TEST_DATA_DIR 环境变量或 dataDir 参数指向临时目录),只读端点(positions/summary/strategies/market-snapshot)可直连生产 API | 2026-08-31 | 生效 | - | 2026-08-31 数据误删事故沉淀:回归测试误删大连热电/万顺新材份额分配,老师定「测试用独立数据目录」 |
| 技术约束-010 | 行情实时数据由**服务端中转 + 缓存**提供(R-005 演进,2026-08-31 老师改):DSH 服务端做「WS 订阅 + REST 轮询 + 行情缓存」,前端统一轮询 /odl/api/market-snapshot(不做前端直连,无跨域);行情持久化到 store.market.json(重启不丢价,首屏快速展现);QMT Bridge WS 推送是会话级/有状态行为(归属 QMT Bridge 工作空间 | 2026-08-31 | 生效 | - | 变更(2026-08-31):老师由「前端直连 WS」改为「服务端中转 + 缓存」——解决跨域与 WS 语义不稳定问题2026-09-01 加行情持久化与启动 prime | | 技术约束-010 | 行情实时数据由**服务端中转 + 内存缓存**提供(R-005 演进,2026-08-31 老师改R-015 二改,2026-09-02):DSH 服务端做「REST 轮询 + 行情内存缓存」,前端统一轮询 /odl/api/market-snapshot(不做前端直连,无跨域);~~行情持久化到 store.market.json~~**R-015 失效:改纯内存,QuoteSync 启动 prime + 读穿透保证秒级有价**);~~QMT Bridge WS 推送~~**R-015 移除 WS 通路**,见技术约束-018 | 2026-08-31 | 生效 | - | 变更(2026-08-31):老师由「前端直连 WS」改为「服务端中转 + 缓存」;2026-09-01 加行情持久化与启动 prime**变更 22026-09-02 R-015/迭代 13**:持久化与 WS 均失效——行情纯内存(QuoteSync/QuoteHub),落库与 WS 通路删除 |
| 技术约束-009 | 会话头部快捷切换控件挂载 DSH 开放 slot `conversation.session.header.actions`(多实例挂载点,按 order 排序多插件共存):客户端插件以独立 id 并排注册(DSH 内置 PTC 标签 order=-10,本控件 order=-9),不改动 DSH 宿主;控件经 ConnectionProvider 包装复用现有 RPC 通道与 /odl/api/* 端点 | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q9 确认;宿主代码审查核实 slot 机制与内置插件注册方式 | | 技术约束-009 | 会话头部快捷切换控件挂载 DSH 开放 slot `conversation.session.header.actions`(多实例挂载点,按 order 排序多插件共存):客户端插件以独立 id 并排注册(DSH 内置 PTC 标签 order=-10,本控件 order=-9),不改动 DSH 宿主;控件经 ConnectionProvider 包装复用现有 RPC 通道与 /odl/api/* 端点 | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q9 确认;宿主代码审查核实 slot 机制与内置插件注册方式 |
| 技术约束-013 | 交易记录本地存储(R-009):QMT 当日交易数据(委托/成交)由服务端 TradeSync 定时同步落 SQLite(启动预热 + 60s 定时 + UPSERT 幂等,只同步当日);trade_orders(委托主行,order_id 主键 + insert_ts 派生时间列 + **strategy_id/holding_id 手动归属列**+ trade_fills(成交明细,trade_id 主键、order_id 外键)两表;**委托归属由用户在交易记录 tab 手动设置**(候选 = 该 code 当前持仓策略 + 未关联,全手动选、可随时改、以最终为准);**UPSERT 不覆盖归属列**(手动指定为插件逻辑);本地历史查询走 trades/history 端点(策略过滤 = 用户设置的归属);今日实时仍走 QMT Bridge;QMT 委托/成交 code 无后缀、持仓带后缀 —— 数据源映射层统一 normalizeInstrumentCode 归一化;委托交易日 = insertDatetradeDate 兜底) | 2026-09-01 | 生效 | - | 新增(2026-09-01 R-009 定稿 + 迭代 07 实施):两表 + 定时同步 + 本地历史查询;变更 12026-09-01):+code 归一化 + tradeDate 兜底;变更 22026-09-01 老师二次定稿):归属改**手动设置**trade_orders 冗余 strategy_id+holding_idUPSERT 不覆盖归属列),弃算法推导 |
| 技术约束-015 | 策略自定义字段存储(R-013 → **R-027 真相源迁移**):字段定义存 **settings.strategyFields**`{ [strategyId]: [{ key, label, type, enum?, def?, unit?, formula?, decimals? }] }`type ∈ text|number|boolean|enum|**formula**);`settings.strategies` 收窄为**内置身份表**(仅兼容读取旧 `configSchema`strategyFields 缺失时按内置 id 回填视图,首次保存即落新键,无迁移脚本、不动库数据);写路径唯一 = `strategies/schema-update { strategyId, configSchema }`(单策略整份覆盖,未知 strategyId 抛 strategy-not-found,校验见 技术约束-024);`normalizeFields` 归一化(类型白名单回退 text、decimals 夹取 0-4、formula 字段不带 def);字段值落 strategy_holdings 新增 values TEXTJSON 键值对,key 对齐 configSchema.key,允许额外键=可扩展,NULL=未配置);补列用幂等 ALTER(沿用 _ensureTradeAttributionColumns 模式,只读连接容忍);APIstrategy-positions 每行附 values,新增 holdings/values-update {holdingId, values} 写回,服务端按 configSchema 校验(number=有限数、enum=在选项内、boolean=布尔),空值/缺省可写入;持仓生命周期操作(openHolding/addShares/reduceShares/closeHolding)不碰 values 列 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-013 定稿 + PLAN-012):定义 settings + 值 SQLite 列 + 幂等补列 + 类型校验;**变更(2026-09-10 R-027/R-026**:定义真相源迁 strategyFieldsstrategies 只读兼容)、类型白名单加计算类型、写路径改单策略端点 |
| 技术约束-014 | 会话 tab 注册与顺序显隐(R-011 → **R-027 静态化**):客户端注册统一读 **settings.tabs**(唯一顺序与显隐来源,内置条目 refKey + 策略条目 refId),按 order 排序、过滤 visible 后注册(builtin 走内置 render、strategy 走 StrategyTab);**R-027 后**tab 序列由**内置常量派生**(3 内置 tab + 2 内置策略 tab),`normalizeTabs` 只保留常量表内条目的 visible/order 偏好、**丢弃**未知条目(自建策略 tab / 已退役 tab)、补齐缺失条目;`updateTabs` 只接受内置 id(未知条目过滤),不再有联动增删(`appendStrategyTab/removeStrategyTab` 退役);旧布尔对象格式(迭代 02)继续兼容 | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1-Q5 确认):统一 tabs 有序数组 + 自动迁移 + 联动增删;**变更(2026-09-10 R-027**:策略静态化,tabs 由常量派生、联动增删与 strategies CRUD 一并退役 |
| 技术约束-016 | src 目录按功能域归类(2026-09-02 结构优化):服务端代码**禁止平铺**,按职责域分目录 —— src/data-source/QmtBridgeRestDataSource + data-source-types + QmtHealthMonitor,数据源与连接健康)、src/storage/SqliteStore + DataStore,存储层)、src/position/PositionManager,分仓逻辑)、src/market/MarketDataHub + MarketFeed,行情)、src/trades/TradeSync,交易同步);api/ 按领域拆分子文件(positions/strategies/qmt-connections/market/trades),client/ 仅放 UIviews/ 组件 + market/ provider);文件命名 = 类名(PascalCase+ .js/.jsx;新增服务端模块必须先落对应域目录,无合适域时先讨论补域,不得回退平铺 | 2026-09-02 | 生效 | - | 新增(2026-09-02 结构审查 + 优化落地):component/ 平铺还原为语义分域,删除死代码 AllocationStorage、DataStore.setDataset/removeDataset |
| 技术约束-017 | 持仓内存快照(R-0142026-09-02**变更 12026-09-08 R-018 数据域分界**):全量实盘持仓由服务端 PositionSync **进程内内存快照**管理(启动预热 + 10s 定时全量拉 /trade/positions → 校验 → 整体替换),**不落库**(账本与对账单分离,holding_id 交易锚点不掺易变快照);PositionManager.getAllPositions 以快照为准(strategy-positions / unallocated / summary 三接口不再请求时穿透 QMT),快照为空读穿透兜底;同步失败保留上次快照;空快照双重确认(/health 可用 + getAsset 账户身份可识别)才接受为真清仓;syncNow 允许手动调用。**幽灵自动清仓**(R-014 原条款:快照连续 3 轮消失 → 本地全部策略当前持仓 closeHolding 转历史)**退役**——同步机制只作用于对账单域,不再写 strategy_holdings 账本(R-018 老师拍板:幽灵清仓本就是一个同步机制,不可以让幽灵把爪子伸太长);账本行转历史唯一途径 = 卖出单关联减至 0 closeHolding(R-018);对账单域「码消失」仅表现为快照无此行 + 漏关联软提示(positions/orphan-hints 只读提示) | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-014 定稿 + 迭代 12 实施);**变更 12026-09-08 R-018/迭代 16 拍板)**:幽灵自动清仓退役(不再 closeHolding 本地账本),PositionSync 只同步对账单快照;漏关联由只读软提示承担 |
| 技术约束-018 | 盘口内存快照(R-0152026-09-02):行情数据由 QuoteSync(取数:启动 prime + 5s REST 定时刷 watch 集合 + 涨停跌停经 /data/instrument 按**交易日**内存缓存)与 QuoteHub(存查:内存快照 Map + watchCodes Set + 读穿透走 dataSource.getTicks + getQuote/getByCodes 对外)管理,**替换并删除 MarketFeed/MarketDataHub**(方案 A,不留兼容壳);**WS 数据通路移除**(ingest 入口带 source 标签留回归口子);**market_quotes_cache 表退役 DROP**(幂等),价格单一入口 = QuoteHubDataStore 行情方法(loadMarket/getMarketQuote(s)/setMarketQuotes)删除;同步失败保留内存旧值;watchCodes 维持只进不出无上限;涨停/跌停/昨收不落库 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-015 定稿 + 迭代 13 实施):老师五拍板(替换/WS 移除/纯内存 DROP/watch 现状/指示灯);warmup bug 复现(loadMarket return this 残迹)为不落库关键证据 |
| 技术约束-019 | 历史持仓查询(R-0162026-09-07):策略 tab 历史持仓展示走**本地库只读查询**SqliteStore.getHoldingsHistorystrategy_id + closed_at IS NOT NULL + closed_at ≥ sinceMsclosed_at DESC),经独立端点 strategy-holdings/history 暴露;**当前持仓路径(strategy-positions / PositionSync 快照语义)不掺历史数据**(两份结果前端合并渲染);closeHolding 置 shares=0 语义维持不变(Q2 老师拍板:历史行份额显示 0,重点在追溯该持仓的历史操作而非清仓时份额);范围换算服务端做(week=7d/month=30d/quarter=90d/halfYear=182d/year=365d 自然日近似)——**变更 12026-09-07 二轮补充 Q8-Q9 老师拍板)**range 新增 'today' = **本地自然日 00:00 起**(特判零点,不落回溯毫秒档),供前端「今日已清仓默认层」(恒显示、不受历史持仓开关控制);历史范围层语义收窄为**今天之前**;其余不变 | 2026-09-07 | 生效 | - | 变更 12026-09-07 R-016 二轮补充 Q8-Q9 老师拍板):+range=today 自然日边界,历史开关只控今天之前;首轮新增(Q1-Q7):历史行=追溯操作锚点,不动存储写路径 |
| 技术约束-020 | QMT 连接健康自适应探测(R-0222026-09-09):QmtHealthMonitor 探测节奏由固定 5 分钟改为**结果驱动自适应**——健康 → intervalMs(默认 5 分钟,稳态低开销);异常/未知(含从未探测成功或 baseUrl 解析失败)→ retryMs(默认 10 秒)快速重试,与前端 sync-status 10s 轮询对齐,QMT Bridge 启动/恢复后指示灯 ≤20s 自动回绿(原固定 5 分钟导致恢复感知滞后,老师反馈「自动检查没生效」);间隔经构造参数 { intervalMs, retryMs } 注入(测试用短间隔);setTimeout 自适应链 + _inFlight 防并发,手动探测/配置热切换共用 probe() 并重排下轮节奏;stop() 即停;resolveActiveBaseUrl 失败也落 healthy:false 缓存走快速重试;前端读缓存秒回与 sync-status/qmt-health 端点语义不变 | 2026-09-09 | 生效 | - | 新增(2026-09-09 R-022 讨论 + 迭代 19 实施):健康灯恢复感知修复;稳态间隔维持原 5 分钟设计不变 |
| 技术约束-021 | MCP 状态自适应探测(R-0232026-09-09):QmtMcpManager 状态缓存增加**结果驱动自适应探测**(原无定时回路,只在挂载/切换/手动探测时刷新)——connected → intervalMs(默认 5 分钟,稳态低开销);非 connected → retryMs(默认 10 秒)快速重试,MCP 服务(激活连接 baseUrl + /mcp)恢复后状态灯 ≤20s 自动回绿;dsh-mcp-client 自带 reconnect 只恢复真实连接、不刷新状态缓存,本类缓存必须自带刷新回路;无激活连接(url 为空)不探测不调度,dispose()/unmount() 停止调度;探测走 SDK 独立只读握手(Client connect + initialize + tools/list),与 dsh-mcp-client 自管连接互不干扰;间隔经构造参数 { intervalMs, retryMs } 注入(测试用短间隔);_probing 防并发与手动/热切换探测共用 probe() 重排节奏;前端 getStatus 读缓存与 mcp-status/sync-status 端点语义不变 | 2026-09-09 | 生效 | - | 新增(2026-09-09 R-023 讨论 + 迭代 19 实施):MCP 状态灯恢复感知修复(与 R-022 同模式);挂载/重连逻辑零改动 |
| 技术约束-022 | 公式引擎与变量目录(R-0262026-09-10):`src/formula/evaluator.js` **自写小型表达式引擎**(零依赖,**禁止 `eval/Function`**):词法支持 Unicode 标识符(**中文变量名**)与全角归一(()+-×÷),语法 = 四则 + 括号 + 一元负 + 函数 round/abs/min/max(递归下降 + 深度≤32 + 长度≤500 上限);**R-028 扩展**:比较 `> < >= <= == !=` 与逻辑 `and / or`(含 `&& ||`、全角 `≥ ≤ ≠ `),优先级 `or < and < 比较 < 加减 < 乘除`,判定结果缺值短路;新增 `inferResultKind(ast)` 静态推断结果类型(顶层比较/逻辑 → boolean,`neg` 下钻);`validateFormula(expr, allowedVars)` 返回 { ok, vars } 或 { ok:false, error:{ code: empty|syntax|unknown-var|unknown-func|arity, message, position } };求值语义 = **缺值短路**(任一变量 null/undefined/NaN → 整式 null)、除零/非有限 → null、**最终结果浮点归一**|v|<1e12 时四舍五入 10 位,消除 0.7000000000000002 类噪声,中间步骤不归一再保精度);`src/formula/variables.js` 为**变量目录单一入口**(行情 7 项 / 合约 2 项 / 持仓账本 3 项 + 运行时按策略非 formula 字段展示名展开「自定义字段」组),`allowedVarNames` 排除 formula 字段(零循环依赖) | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-026 定稿 + 技术方案 §2.2/§2.3):中文标识符需自有词法(第三方库均不支持中文变量),该层即 T-006 数据池的表达式层;**变更(2026-09-10 R-028**:新增比较与逻辑运算 + `inferResultKind`(判定型计算字段);**复核(2026-09-10 老师提问「有没有现成的库」)**:对比 expr-evalCVE-2025-12735 变量对象注入 → RCE)、mathjs(标识符不含 CJK)、Jexl(ASCII 标识符)、CEL、解析器工具包后**维持自写引擎**,论证见 迭代 23 技术方案 §4.5 |
| 技术约束-023 | 计算字段现算口径(R-0262026-09-10):`src/formula/FormulaService.js#computeRows`**API 层**为 `strategy-positions` 返回行附加 `computed: { [fieldKey]: number|null }`(不改 PositionManager 份额语义);取数**只读 QuoteHub 内存缓存**`quotes` / `instruments`,与 api/market.js#projectQuote 同口径),**不做读穿透**(高频读路径零网络;miss → 相关变量 null → 该格 `—`,等 QuoteSync 下轮 ≤5s 覆盖);**无 formula 字段的策略直接返回原数组(零开销)**;值**不落库**(公式存 settings,值每次现算);`strategy-holdings/history` 不计算(历史行前端显示 `—` | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-026 技术方案 §2.4):计算位置与缓存口径由 AI 定并写入方案,老师验收覆盖缺数据边界 |
| 技术约束-024 | 字段定义 API 与校验(R-027/R-0262026-09-10):写路径唯一 `strategies/schema-update { strategyId, configSchema }`(返回该策略对象);校验顺序 = 结构(展示名/key 非空、类型白名单、枚举选项非空)→ 唯一性(key 唯一、展示名唯一)→ **内置变量名冲突**(字段展示名不得等于现价/份额/…等内置变量名)→ 公式(`validateFormula`,变量集合 = 内置名 ∪ 本策略非 formula 字段展示名);失败统一抛 `code='field-validation'`(前端 Toast + 公式框描红);`formula/variables { strategyId? }` 提供变量目录、`formula/trial { strategyId, formula }` 试算(取份额>0 首行,返回 { code, name, value, reason: no-holding|no-data|null });**退役端点** `strategies/add|remove|update`(调用返回 not-found | 2026-09-10 | 生效 | - | 新增(2026-09-10 R-027/R-026 定稿 + 技术方案 §1.2/§2.5):单策略写入避开整表覆盖,校验规则服务端唯一;**变更(2026-09-10 R-028)**:公式字段增加「结果类型一致性」校验——`resultKind`(默认 number)必须等于 `inferResultKind(compileFormula(formula).ast)`,不一致抛 `field-validation`(双向提示)|
<!-- 示例条目(确认格式后删除): <!-- 示例条目(确认格式后删除):
| 技术约束-001 | 示例:技术栈以 Node.js / TypeScript 为准,不引入未讨论的新框架 | 2026-08-26 | 生效 | - | 讨论确认:优先复用 DSH 既有能力,新框架需论证 | | 技术约束-001 | 示例:技术栈以 Node.js / TypeScript 为准,不引入未讨论的新框架 | 2026-08-26 | 生效 | - | 讨论确认:优先复用 DSH 既有能力,新框架需论证 |
--> -->
+196 -11
View File
@@ -135,10 +135,10 @@
| 模块 | 职责 | 文件 | | 模块 | 职责 | 文件 |
|---|---|---| |---|---|---|
| DataStore | 份额分配 + 行情持久化读写、迁移、schema | src/component/DataStore.js | | DataStore | 份额分配 + 行情持久化读写、迁移、schema | src/storage/DataStore.js |
| PositionManager | 份额业务逻辑(依赖 DataStore | src/component/PositionManager.js | | PositionManager | 份额业务逻辑(依赖 DataStore | src/position/PositionManager.js |
| MarketDataHub | 行情缓存(内存 + 磁盘持久化 + 查询) | src/component/MarketDataHub.js | | MarketDataHub | 行情缓存(内存 + 磁盘持久化 + 查询) | src/market/MarketDataHub.js |
| MarketFeed | 行情获取(WS + REST 定时刷新 + 启动 prime | src/component/MarketFeed.js | | MarketFeed | 行情获取(WS + REST 定时刷新 + 启动 prime | src/market/MarketFeed.js |
## 8. 约束条目(引用) ## 8. 约束条目(引用)
@@ -254,15 +254,200 @@ CREATE TABLE IF NOT EXISTS market_quotes_cache (
| 模块 | 职责 | 文件 | | 模块 | 职责 | 文件 |
|---|---|---| |---|---|---|
| SqliteStore | SQLite 存储层封装(node:sqliteinit/迁移/持仓生命周期/行情读写/close) | src/component/SqliteStore.js | | SqliteStore | SQLite 存储层封装(node:sqliteinit/迁移/持仓生命周期/行情读写/close) | src/storage/SqliteStore.js |
| DataStore | 持仓 + 行情持久化(委托 SqliteStore,对外 API 升级为生命周期语义) | src/component/DataStore.js | | DataStore | 持仓 + 行情持久化(委托 SqliteStore,对外 API 升级为生命周期语义) | src/storage/DataStore.js |
| PositionManager | 份额业务(适配单票生命周期:openHolding/addShares/reduceShares/closeHolding | src/component/PositionManager.js | | PositionManager | 份额业务(适配单票生命周期:openHolding/addShares/reduceShares/closeHolding | src/position/PositionManager.js |
| MarketDataHub | 行情缓存(内存 + SQLite 持久化 + 查询) | src/component/MarketDataHub.js | | MarketDataHub | 行情缓存(内存 + SQLite 持久化 + 查询) | src/market/MarketDataHub.js |
| MarketFeed | 行情获取(WS + REST 定时刷新 + 启动 prime | src/component/MarketFeed.js | | MarketFeed | 行情获取(WS + REST 定时刷新 + 启动 prime | src/market/MarketFeed.js |
| migrate 脚本 | 一次性迁移脚本 | scripts/migrate-json-to-sqlite.mjs | | migrate 脚本 | 一次性迁移脚本 | scripts/migrate-json-to-sqlite.mjs |
## 10. 约束条目(引用 ## 10. 交易记录存储设计(R-009 落实,2026-09-01 迭代 07 实施
- 技术约束-012:数据存储遵循本文件(SQLite 存储设计); > 依据:R-009Q1-Q8 定稿,2026-09-01)。在 SQLite 新增交易记录两表(trade_orders + trade_fills),QMT 当日交易数据本地持久化(跨日积累成历史库),支持按策略过滤 / 复盘交易。
### 10.1 表结构
```sql
-- 交易委托(委托主行;order_id 唯一,UPSERT 幂等;零冗余 strategy_id/holding_id
CREATE TABLE IF NOT EXISTS trade_orders (
order_id TEXT PRIMARY KEY, -- m_strOrderSysID
trade_date TEXT NOT NULL, -- 交易日 YYYYMMDDm_strInsertDate
code TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
exchange TEXT NOT NULL DEFAULT '',
direction TEXT NOT NULL DEFAULT '', -- buy / sell
direction_code INTEGER,
opt_name TEXT NOT NULL DEFAULT '',
status INTEGER,
order_volume REAL NOT NULL DEFAULT 0,
traded_volume REAL NOT NULL DEFAULT 0,
limit_price REAL NOT NULL DEFAULT 0,
traded_price REAL NOT NULL DEFAULT 0,
amount REAL NOT NULL DEFAULT 0,
insert_date TEXT NOT NULL DEFAULT '',
insert_time TEXT NOT NULL DEFAULT '',
insert_ts INTEGER NOT NULL, -- 派生:insert_date+insert_time 合成毫秒时间戳
cancel_info TEXT NOT NULL DEFAULT '',
error_msg TEXT NOT NULL DEFAULT '',
strategy_id TEXT, -- 用户手动设置的策略归属(冗余,Q1 确认)
holding_id INTEGER, -- 用户手动设置的持仓归属(冗余,Q1 确认)
fetched_at INTEGER NOT NULL -- 同步时间戳
);
-- 交易成交(trade_id 唯一,order_id 外键关联委托;零冗余 strategy_id/holding_id
CREATE TABLE IF NOT EXISTS trade_fills (
trade_id TEXT PRIMARY KEY, -- m_strTradeID
order_id TEXT NOT NULL, -- → trade_orders.order_id
trade_date TEXT NOT NULL,
code TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
exchange TEXT NOT NULL DEFAULT '',
direction TEXT NOT NULL DEFAULT '',
direction_code INTEGER,
opt_name TEXT NOT NULL DEFAULT '',
price REAL NOT NULL DEFAULT 0,
volume REAL NOT NULL DEFAULT 0,
amount REAL NOT NULL DEFAULT 0,
commission_rate_wan REAL NOT NULL DEFAULT 0,
trade_time TEXT NOT NULL DEFAULT '',
fetched_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_trade_orders_date ON trade_orders (trade_date);
CREATE INDEX IF NOT EXISTS idx_trade_orders_ts ON trade_orders (insert_ts);
CREATE INDEX IF NOT EXISTS idx_trade_fills_order ON trade_fills (order_id);
CREATE INDEX IF NOT EXISTS idx_trade_fills_date ON trade_fills (trade_date);
```
### 10.2 关键设计决策(R-009 Q1-Q8 定稿)
| 决策 | 结论 | 理由 |
|---|---|---|
| 表范围 | trade_orders + trade_fills 两表 | 委托 1:N 成交,忠实 R-007 单表合并模型;保留无成交委托(待报/已撤/废单);避免 JSON 压扁 |
| 冗余 | **trade_orders 冗余 strategy_id + holding_id**(用户手动设置) | 老师二次定稿(Q3 修正):手动设置归属,便于过滤/展示/复盘;trade_fills 仍零冗余(跟随委托) |
| 委托时间派生列 | insert_tsinsert_date+insert_time 合成毫秒) | join 持仓生命周期窗口用(created_at ≤ t < closed_at |
| 策略/持仓归属 | **用户手动设置**(交易记录 tab 下拉:候选 = 该 code 当前持仓策略 + 未关联);持久化到 trade_orders.strategy_id + holding_idUPSERT 不覆盖归属列;可随时改以最终为准 | 一票多策略无法算法区分(300057.SZ 分属 grid/manual),人为确认符合「人机合一」(2026-09-01 老师二次定稿) |
| 同步 | 服务端 TradeSync:启动预热 + 60s 定时 UPSERT(幂等);前端今日轮询写穿 | 不依赖前端开 tab 也持续积累 |
| 代码归一化 | QMT 委托/成交 code 无后缀(001330),持仓带后缀(001330.SZ)——数据源映射层统一 `normalizeInstrumentCode`(补交易所后缀);委托交易日 = insertDate(无独立 tradeDate 字段),tradeDate 兜底 insertDate | 保证 FK 链 join 匹配 + 历史按时间段过滤正确(迭代 07 真实数据发现) |
| 同步范围 | 只同步当日(QMT 无历史接口) | 数据逐日积累 = 本地历史库 |
| 历史查询 | trades/history 端点(时间段/code/策略/方向过滤) | 复盘 + 按策略过滤 |
| 数据清理 | 不做自动清理(复盘需要历史) | 导出/清理后续迭代 |
### 10.3 数据流
```
启动时:TradeSync 启动预热一次(拉今日 orders+trades → UPSERT 落库,不覆盖归属列)
实盘中:TradeSync 60s 定时同步(UPSERT 幂等,状态覆盖更新,归属列保留)
前端今日轮询命中 orders/trades 端点 → 写穿本地(机会式)
用户: 交易记录 tab 手动设置每笔委托归属(strategy_id + holding_id 落库,可随时改)
查询时:今日 → QMT 实时(+ 本地归属);历史范围 → trades/history 查本地 SQLite(策略过滤 = 用户设置的归属)
```
### 10.4 对应实现(更新)
| 模块 | 职责 | 文件 |
|---|---|---|
| SqliteStore | +trade_orders/trade_fills 建表 + 交易 UPSERT/历史查询 + 策略归属推导 | src/storage/SqliteStore.js |
| DataStore | +交易记录方法(upsertTradeOrders/upsertTradeFills/queryTradeHistory | src/storage/DataStore.js |
| TradeSync | 服务端定时同步(启动预热 + 60s + UPSERT 幂等) | src/trades/TradeSync.js |
| api/trades.js | +trades/history 端点 | src/api/trades.js |
## 11. 持仓内存快照(2026-09-02 优化)
> **变更理由**(老师拍板讨论):原实盘持仓在每次请求时穿透 QMT`PositionManager.getAllPositions` 实时拉 `/trade/positions`),带来 ① QMT 抖动 → 策略/全部持仓/未分配三个页面当场空白;② 每次进 tab 都打一次 QMT HTTP。改为服务端内存快照为准,10s 定时同步。
### 11.1 决策
| 决策 | 结论 | 理由 |
|---|---|---|
| 存储介质 | **纯内存**PositionSync 进程内快照),**不落库** | 持仓快照随时可用一次调用重拿全,不满足落库的任一正当条件(不可再生/重启首屏依赖);落库反而引入「过期快照冒充实时的说谎风险」;不复用 strategy_holdings(账本 ≠ 对账单,holding_id 是交易归属锚点,不可掺易变快照) |
| 同步 | PositionSync:启动预热 + 10s 定时全量拉取 → 校验 → 整体替换内存 | 与 TradeSync 同构;闸门校验与页面显示同源同鲜度 |
| 失败策略 | 同步失败**保留上次快照**,不清空不报错 | 读方继续消费旧快照,QMT 抖动不再白屏(强于旧的穿透行为) |
| 空快照 | 双重确认(/health 可用 + getAsset 账户身份可识别)才接受为「真清仓」,否则视为异常保留旧快照 | 防一次异常响应清空缓存 |
| 读路径 | `getAllPositions()` 读内存快照;快照为空 → 读穿透当场拉一次并回填;QMT 也挂 → 抛错(前端 LoadState 重试) | 首启兜底;未注入 positionSync 时保持旧行为(兼容) |
| ~~幽灵持仓自动清仓~~**退役,2026-09-08 R-018 数据域分界** | ~~QMT 快照连续 3 轮消失 → 本地全部策略当前持仓 closeHolding 转历史~~**不再写 strategy_holdings**:同步机制只作用于对账单域(老师拍板:幽灵清仓本就是一个同步机制,不可以让幽灵把爪子伸太长);账本转历史唯一途径 = 卖出单关联减至 0(R-018);对账单码消失 → 快照无此行 + 漏关联软提示(positions/orphan-hints,只读) | R-014 选定时防本地账本残留;R-018 改由交易关联驱动账本生命周期,幽灵自动归档与「账本只由关联交易改」冲突 → 退役(迭代 16 实施,见 §12) |
| 前端 | 零改动 | 三个接口数据源自动切换 |
### 11.2 数据流(持仓部分,替代原「读取时组装」描述)
```
启动:PositionSync 预热一次 → 内存快照
实盘:10s 定时全量同步(失败保留旧快照;空快照双重确认;幽灵清仓判定)
读取:getAllPositions() → 内存快照 →(空)读穿透回填
```
### 11.3 对应实现(追加)
| 模块 | 职责 | 文件 |
|---|---|---|
| PositionSync | 持仓内存快照同步(10s 定时 + 失败保留 + 空快照双重确认 + 幽灵清仓防抖) | src/position/PositionSync.js |
| PositionManager | getAllPositions 改读快照 + 读穿透兜底(未注入 sync 时兼容旧穿透) | src/position/PositionManager.js |
| 回归测试 | 纯内存 mock 34 项(同步/失败保留/空快照/幽灵防抖/账户守卫/读穿透/组装回归) | scripts/test-position-sync.mjs |
## 12. 归属分段存储设计(R-018 落实,2026-09-08 迭代 16 实施)
> **变更理由**(R-018 老师拍板):交易归属从「纯标签」(trade_orders 单组 strategy_id/holding_id,不改数量)升级为**账本写操作**——关联即自动调整 strategy_holdings 份额;支持一笔委托拆多段分配多策略;撤段做逆操作。归属真相源从 trade_orders 两列迁移到**分段表**。
### 12.1 strategy_holdings 状态扩展(作废第三态 + 清仓份额快照)
```sql
-- 幂等 ALTER(沿用 _ensureXxxColumn 模式)
ALTER TABLE strategy_holdings ADD COLUMN void_at INTEGER; -- 作废时间(NULL=有效;非 NULL=建仓被撤=从未成立;不进历史层)
ALTER TABLE strategy_holdings ADD COLUMN closed_shares REAL; -- 清仓前份额快照(closeHolding 写入,供撤卖出段恢复活动用)
-- 活动唯一索引收窄(R-018:活动 = closed_at IS NULL AND void_at IS NULL
DROP INDEX IF EXISTS idx_active_holding;
CREATE UNIQUE INDEX IF NOT EXISTS idx_active_holding
ON strategy_holdings (strategy_id, code) WHERE closed_at IS NULL AND void_at IS NULL;
```
- 生命周期操作语义:closeHolding = shares 置 0 + closed_at + **closed_shares=清仓前份额**shares 展示仍 0R-016 Q2 语义不变);voidHolding(新增)= shares 置 0 + void_at(非清仓:建仓被撤 = 从未成立);
- 作废行不进 R-016 历史「已清仓」层(getHoldingsHistory 过滤 void_at IS NULL),保留数据可追溯。
### 12.2 归属分段表 trade_order_attributions
```sql
CREATE TABLE IF NOT EXISTS trade_order_attributions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT NOT NULL, -- → trade_orders.order_id
strategy_id TEXT NOT NULL, -- 段归属策略
holding_id INTEGER NOT NULL, -- 段锚点持仓(R-010 展开用)
code TEXT NOT NULL, -- 冗余
direction TEXT NOT NULL, -- buy/sell(冗余,撤段判向)
volume REAL NOT NULL, -- 该段已成交量(>0;总额 ≤ order.traded_volume
created_at INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_attr_order_strategy ON trade_order_attributions (order_id, strategy_id);
CREATE INDEX IF NOT EXISTS idx_attr_holding ON trade_order_attributions (holding_id);
CREATE INDEX IF NOT EXISTS idx_attr_strategy ON trade_order_attributions (strategy_id);
```
- **真相源 = 段表**trade_orders.strategy_id/holding_id 退役为冗余(保留列不删,防外部脚本 break;不再作为查询/过滤源);
- 存量迁移:段表空且 trade_orders 有单组归属 → 迁为第一段(volume = 该单 traded_volume;幂等);
- 查询适配:orders 附加归属、by-holdingR-010)、历史策略过滤(R-009)全部改经段表。
### 12.3 关键设计决策(R-018 迭代 16)
| 决策 | 结论 | 理由 |
|---|---|---|
| 归属真相源 | **分段表 trade_order_attributions**order → 多段 strategyId/holdingId/volume | 一笔委托可拆 N 段关联不同策略(R2/R4);单列两字段无法表达 |
| 数据域分界 | PositionSync/幽灵清仓**只写对账单快照**,不写 strategy_holdings | R-018 老师拍板:同步机制不伸爪子到账本;账本只由交易关联 + 手动份额操作驱动 |
| 关联驱动量 | 段 volume = 该段**已成交量** | Q4:撤单/废单不动账 |
| 撤段逆操作 | 撤 buy 段 → 减回 / 归零作废(void);撤 sell 段 → 加回 / 恢复活动仓(closed_shares | R3/R6/R7:作废 ≠ 清仓(不产生假清仓历史);恢复用 closed_shares 快照 |
| 撤段约束 | 同 holding 内**逆序撤销**(乱序返回 segment-order-conflict);跨 holding 段独立可任意撤 | 账本正确性 > 操作便利(宁可拒绝不写错账) |
| 事务 | setSegments 全量替换在**单事务**内(撤旧段 + 加新段) | 改归属原子性(R3 边界) |
### 12.4 对应实现(追加)
| 模块 | 职责 | 文件 |
|---|---|---|
| AttributionService | 归属服务:apply/revoke/setSegments(动作表判定 + 逆操作 + 事务) | src/trades/AttributionService.js(新增) |
| SqliteStore | 增列/索引重建/voidHolding/close 快照/段表 CRUD/runInTransaction/查询改段表/存量迁移 | src/storage/SqliteStore.js |
| api/trades.js | attribution-targets / attribution-set / attribution-segmentsorders 附加段 | src/api/trades.js |
| PositionSync | 删除幽灵清仓逻辑(只同步对账单快照) | src/position/PositionSync.js |
| 回归测试 | 分界/动作表/撤段/迁移/软提示 | scripts/test-r018-attribution.mjs(新增) |
## 13. 约束条目(引用)
- 技术约束-012:数据存储遵循本文件(SQLite 存储设计,含交易记录表 §10、归属分段存储 §12);
- 技术约束-011:测试/回归脚本禁止在真实数据上执行写操作(独立数据目录); - 技术约束-011:测试/回归脚本禁止在真实数据上执行写操作(独立数据目录);
- 技术约束-017(变更 1,2026-09-08):幽灵自动清仓退役,PositionSync 只同步对账单快照;
- 本文件为新领域设计约束,后续数据存储变更以本文件为最终依据。 - 本文件为新领域设计约束,后续数据存储变更以本文件为最终依据。
@@ -0,0 +1,120 @@
# 技术实现方案:07-交易记录本地存储(SQLite)+ 策略关联
> 依据:PLAN-008 需求:R-009(Q1-Q8 定稿)| 设计约束:技术约束-012(数据存储设计)、技术约束-011(测试隔离)
## 技术选型
- **存储**:沿用 SqliteStorenode:sqlite)——新增 trade_orders + trade_fills 两表(R-009 Q1);
- **同步**:服务端 TradeSync 模块(定时 60s + 启动预热 + 前端今日轮询写穿;UPSERT 幂等);
- **关联**:外键链推导(Q3 老师定稿):成交→委托(order_id 外键)→策略/holding(委托时间 join strategy_holdings 生命周期窗口)——两表零冗余 strategy_id/holding_id
- **历史查询**api/trades.js 新增 trades/history 端点(查本地 SQLite);
- **前端**TradeRecordsTab 加策略过滤 + 历史范围查本地。
## 表结构(数据存储设计.md §10 落实)
```sql
-- 交易委托(委托主行;order_id 唯一,UPSERT 幂等;零冗余 strategy_id/holding_id
CREATE TABLE IF NOT EXISTS trade_orders (
order_id TEXT PRIMARY KEY, -- m_strOrderSysID
trade_date TEXT NOT NULL, -- 交易日 YYYYMMDDm_strInsertDate
code TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
exchange TEXT NOT NULL DEFAULT '',
direction TEXT NOT NULL DEFAULT '', -- buy / sell
direction_code INTEGER,
opt_name TEXT NOT NULL DEFAULT '',
status INTEGER,
order_volume REAL NOT NULL DEFAULT 0,
traded_volume REAL NOT NULL DEFAULT 0,
limit_price REAL NOT NULL DEFAULT 0,
traded_price REAL NOT NULL DEFAULT 0,
amount REAL NOT NULL DEFAULT 0,
insert_date TEXT NOT NULL DEFAULT '',
insert_time TEXT NOT NULL DEFAULT '',
insert_ts INTEGER NOT NULL, -- 派生:insert_date+insert_time 合成毫秒时间戳(join 持仓窗口用)
cancel_info TEXT NOT NULL DEFAULT '',
error_msg TEXT NOT NULL DEFAULT '',
fetched_at INTEGER NOT NULL -- 同步时间戳
);
-- 交易成交(trade_id 唯一,order_id 外键关联委托;零冗余 strategy_id/holding_id
CREATE TABLE IF NOT EXISTS trade_fills (
trade_id TEXT PRIMARY KEY, -- m_strTradeID
order_id TEXT NOT NULL, -- → trade_orders.order_id
trade_date TEXT NOT NULL,
code TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
exchange TEXT NOT NULL DEFAULT '',
direction TEXT NOT NULL DEFAULT '',
direction_code INTEGER,
opt_name TEXT NOT NULL DEFAULT '',
price REAL NOT NULL DEFAULT 0,
volume REAL NOT NULL DEFAULT 0,
amount REAL NOT NULL DEFAULT 0,
commission_rate_wan REAL NOT NULL DEFAULT 0,
trade_time TEXT NOT NULL DEFAULT '',
fetched_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_trade_orders_date ON trade_orders (trade_date);
CREATE INDEX IF NOT EXISTS idx_trade_orders_ts ON trade_orders (insert_ts);
CREATE INDEX IF NOT EXISTS idx_trade_fills_order ON trade_fills (order_id);
CREATE INDEX IF NOT EXISTS idx_trade_fills_date ON trade_fills (trade_date);
```
**策略归属推导(查询期,FK 链)**
- 委托归属:`trade_orders JOIN strategy_holdings ON o.code=h.code AND h.created_at <= o.insert_ts AND (h.closed_at IS NULL OR o.insert_ts < h.closed_at)`
- 一码多策略消歧:命中多笔持仓时取 shares 最大者(确定性规则);
- 成交归属 = 所属委托归属(fill → order → holding → strategy);
- 无命中 → 未关联(strategy_id 返回 null)。
## 架构设计
```
SqliteStore(改)
├─ init() 建表:+trade_orders/trade_fills + 索引
├─ upsertOrders(orders) / upsertFills(fills) # UPSERT 幂等
├─ getOrderHistory({start,end,code,strategyId,direction}) # 委托 + 策略归属推导
├─ getFillHistory({start,end,code,strategyId,direction}) # 成交(按委托归属 join)
└─ close()
DataStore(改)
└─ 委托交易记录方法(upsertTradeOrders/upsertTradeFills/queryTradeHistory
TradeSync(新增)
├─ start() : 启动预热同步一次 + 60s 定时
├─ syncNow() : 拉今日 orders+trades → upsertOrders/upsertFills(幂等)
└─ stop() : 清理定时器
api/trades.js(改)
└─ trades/history → storage.queryTradeHistory(时间段/code/策略/方向过滤)
index.js(改)
└─ 装配 TradeSyncdispose 停止)+ storage 注入 api runtime
client/views/TradeRecordsTab.jsx(改)
├─ 策略过滤下拉(全部/各策略/未关联)——今日实时过滤前端、历史走服务端过滤
└─ 历史范围:调 trades/history 查本地库(不再占位)
```
## 实现步骤
1. **文档骨架**PLAN-008 + 迭代 07(本步);
2. **设计约束**:数据存储设计.md §10 + 技术方案约束新增条目;
3. **SqliteStore**:建表 + upsertOrders/upsertFills + getOrderHistory/getFillHistory(策略归属 join);
4. **DataStore**:委托交易记录方法;
5. **TradeSync**:定时同步(60s + 启动预热 + UPSERT 幂等);
6. **api/trades.js**trades/history 端点;
7. **index.js**:装配 TradeSync + storage 注入 runtime
8. **前端**TradeRecordsTab 策略过滤 + 历史本地展示;
9. **构建测试**pnpm run build + typecheck + 独立数据目录回归(技术约束-011);
10. **验收 + 复盘**
## 涉及设计约束
| 约束 | 内容 |
|---|---|
| 技术约束-012 | 数据存储遵循数据存储设计.md(本迭代增补 §10 交易记录表设计) |
| 技术约束-011 | 测试/回归脚本禁止在真实数据上写操作:ODL_TEST_DATA_DIR 独立数据目录 |
| 技术约束-001/003/004/010 | 数据源抽象 / 直连 REST / webServer 自开路由 / 服务端中转+前端轮询(沿用) |
| 产品约束-007 | 方向语义红买绿卖(沿用 R-007) |
@@ -0,0 +1,61 @@
# 迭代复盘:07-交易记录本地存储(SQLite)+ 策略关联
> 复盘日期:2026-09-01 | 迭代状态:**已完成(验收通过)**
> 关联需求:R-009(交易记录本地存储 + 策略关联,已定稿)
> 关联计划:PLAN-008(计划-交易记录本地存储SQLite与策略关联)
## 结果
迭代 07 达成:QMT 当日交易数据(委托 + 成交)本地持久化到 SQLitetrade_orders + trade_fills 两表,跨日积累成历史库),按外键链(成交→委托→策略→holding)与策略体系关联,支持按策略过滤 / 复盘交易;交易记录 tab 历史范围从「接口开发中」占位切换为本地历史查询。
## 过程事实
1. **需求定稿(R-009**:老师提出 → AI 登记(讨论中)提出 Q1-Q8 → 老师修正关联方式(外键链 + 两表零冗余)→ 确认定稿进入迭代 07;
2. **表结构**trade_orders(委托主行,order_id 主键 + insert_ts 派生时间列)+ trade_fills(成交明细,trade_id 主键、order_id 关联委托)两表;**零冗余 strategy_id/holding_id**Q3 老师定稿);
3. **策略归属推导(FK 链)**:查询期委托时间(insert_tsjoin strategy_holdings 生命周期窗口(created_at ≤ t < closed_at)推导;一码多策略取份额最大;未命中=未关联;attachStrategyAttribution 供今日实时委托附加归属;
4. **TradeSync 同步**:启动预热 + 60s 定时拉当日 orders+trades → UPSERT 落库(幂等,状态覆盖更新);前端今日轮询命中 orders 端点时写穿(机会式);
5. **本地历史查询**api/trades.js 新增 trades/history 端点(时间段/code/策略/方向过滤,策略过滤走 FK 链 join);
6. **前端**TradeRecordsTab 加策略过滤下拉(全部/各策略/未关联)+ 历史范围查本地库(不再占位);
7. **验证**:typecheck 通过、构建成功、独立数据目录回归测试 17/17 通过 + TradeSync 集成测试 5/5 通过(幂等/归属推导/过滤/写穿)。
## 经验教训(复盘沉淀)
### 1. 时间基准必须一致(本次踩坑)
- **教训**insert_ts 合成先用 Date.UTC(),而 strategy_holdings.created_at 是本地 Date.now() —— 时区差导致持仓窗口匹配失败(回归测试 6 项失败);
- **沉淀**:同一库内时间戳必须同一基准(本地时间);跨模块时间比较前先核对基准。
### 2. 测试数据要构造真实时间窗
- 第一次归属推导测试失败是因为用「当前时间」建仓(23:32)而委托是 09:30 —— 窗口本就不覆盖,代码是对的、测试数据不对;
- **沉淀**:生命周期窗口类测试必须用 SQL 精确控制 created_at/closed_at,模拟真实时间关系。
### 3. 大改组件优先整体重写(前端)
- 迭代中对 TradeRecordsTab 做多次定点替换时部分替换未生效,产生中间态(引用未定义组件);
- **沉淀**:结构性大改(新增过滤/分支重构)直接整文件重写更稳,避免局部替换残留。
### 4. QMT 委托/成交 code 无后缀 + 无独立交易日字段(真实数据发现)
- **发现**:重启后启动预热同步的真实委托/成交,code 为无后缀 `001330`(持仓体系是 `001330.SZ`),且委托无 `m_strTradeDate`(交易日 = `m_strInsertDate`)—— 直接导致 FK 链 join 匹配不上(博纳影业被判未关联)+ 历史按时间段过滤漏委托;
- **修复**QmtBridgeRestDataSource 加 `normalizeInstrumentCode`(6 位数字 + 交易所后缀;SH/SZ/BJ;无交易所按首位推断 6/9→SH、0/3→SZ;已带后缀保留)+ mapOrder 补 tradeDateinsertDate 兜底)+ mapTrade code 归一化;
- **沉淀**:QMT 各接口的证券代码格式不一致(委托/成交无后缀、持仓带后缀),语义化映射层必须统一归一化;交易「日」概念在委托接口 = insertDate(无独立 tradeDate 字段)。
### 5. 迁移持仓 created_at = 迁移时间戳 → FK 链窗口失真(真实数据发现 + 方案 A 修正)
- **发现**:迁移自 JSON 的持仓 created_at 全部是迁移时刻时间戳(2026-09-01 17:36),晚于当日真实交易时间 → 当日委托 join 窗口不匹配 → 全部判「未关联」;
- **决策(方案 A2026-09-01 老师确认)**:当前持仓(closed_at IS NULL)只做同 code 匹配(不要求 created_at ≤ 委托时间,现在持有=当日交易可归);已清仓(closed_at 非空)才按时间窗口(created_at ≤ t < closed_at)判断;
- **实现**_resolveStrategyAttribution 窗口条件分支;回归测试覆盖迁移时间戳场景(17/17 通过);
- **沉淀**:迁移数据的 created_at 语义 = 迁移时间而非真实建仓时间,时间窗口类推导必须考虑该失真;当前持仓用「存在即归属」更贴合业务语义。
### 6. 归属判定改「手动设置」(R-009 二次定稿,老师拍板)
- **发现**:算法推导(时间窗 + 份额最大)无法区分一票多策略(300057.SZ 同时分属 grid-supermarket 与 manual-t 各 1000 股,明天有成交不知道该归谁);
- **决策(老师二次定稿 Q1-Q4)**:归属由用户在**交易记录 tab 手动设置**(全手动选、可随时改、以最终为准);trade_orders 冗余存 strategy_id + holding_id**UPSERT 不覆盖归属列**(手动指定为插件逻辑);
- **实现**trade_orders 加 strategy_id/holding_id 列(存量库 ALTER 迁移,插件启动时执行,只读打开容忍);setOrderAttribution + getAttributionCandidates(候选 = 该 code 当前持仓策略)+ api 两端点(orders/set-attribution、orders/attribution-candidates);前端 TradeRecordsTab 归属列下拉(候选含策略名 + 份额);
- **验证**:37 项测试通过(核心 14 含 UPSERT 不覆盖归属/归属可改/候选列表/持久化过滤);
- **沉淀**:算法只能给候选,归属是人的决定(符合「人机合一」目标-007);手工指定的数据不能被自动同步覆盖——UPSERT 语义要区分「系统字段」与「用户字段」。
## 遗留/后续
1. **QMT Bridge 历史接口**:老师完善后,今日实时可扩展为历史接口查询(本地库仍为兜底/积累);
2. **手动修正策略关联入口**:本期未做(自动推导 + 未关联兜底已满足复盘),后续按需;
3. **导出/清理**:本地历史库持续积累,导出/清理管理界面后续迭代;
4. **盘中行情写 SQLite 验证**(迭代 06 遗留):开盘后确认行情防抖写回 store.dbupdated_at / mtime)。
@@ -0,0 +1,28 @@
# 迭代目标:07-交易记录本地存储(SQLite)+ 策略关联
> 迭代编号:07 | 创建:2026-09-01 状态:进行中
> 依据计划:PLAN-008 需求:R-009(已定稿,2026-09-01
## 目标描述
将 QMT 当日交易数据(委托 + 成交)本地持久化到 SQLitetrade_orders + trade_fills 两表,跨日积累成历史库),并按外键链(成交→委托→策略→holding)与插件策略体系关联,支持按策略过滤 / 复盘交易;同时解决 R-007 历史范围「接口开发中」占位问题(本地积累后历史可查)。
## 目标分解
1. **两表落地**trade_orders(委托主行)+ trade_fills(成交明细),两表零冗余 strategy_id/holding_id,委托表含派生时间列 insert_ts(join 持仓窗口用);
2. **定时同步**:服务端 TradeSync(启动预热 + 60s 定时 + UPSERT 幂等);
3. **外键链关联**:策略/持仓归属 = 委托时间 join strategy_holdings 生命周期窗口推导(一码多策略取份额最大,未命中=未关联);
4. **本地历史查询**trades/history 端点(时间段/code/策略/方向过滤);
5. **前端**:策略过滤下拉 + 历史范围查本地库。
## 讨论过程
- 2026-09-01 老师提出需求(在 SQLite 添加交易记录表,存储与策略关联的交易记录);
- 2026-09-01 AI 登记 R-009(讨论中),提出 Q1-Q8 技术建议(两表结构/同步机制/策略关联/历史查询/积累范围/UI/状态更新/文档约束);
- 2026-09-01 老师修正(Q3):关联链 = 成交→委托→策略→holding,两表零冗余,查询期按持仓生命周期窗口推导;
- 2026-09-01 老师确认 Q1-Q8 定稿,R-009 转已定稿,进入本迭代。
## 对老师的配合需求
- 无阻塞依赖;QMT Bridge 历史接口未就绪不影响本迭代(本地历史库 = 插件运行期逐日积累);
- 验收时可提供当日真实交易数据做端到端验证(沿用 R-007 模式)。
@@ -0,0 +1,26 @@
# 验收标准:07-交易记录本地存储(SQLite)+ 策略关联
## 验收标准线
1. **两表落地**store.db 存在 trade_ordersorder_id 主键 + insert_ts 派生列)+ trade_fillstrade_id 主键、order_id 关联委托)两表;**两表均无 strategy_id / holding_id 列**(零冗余,Q3 老师定稿);
2. **同步落库**:启动预热 + 60s 定时同步今日 orders+trades 入库;UPSERT 幂等(重复同步不产生重复行、已存在委托状态/成交量覆盖更新);
3. **策略归属推导(FK 链)**:查询时委托时间 join strategy_holdings 生命周期窗口(created_at ≤ t < closed_at)得到策略/持仓归属;一码多策略取份额最大;无命中=未关联;历史委托归属稳定(清仓/删策略转历史不删行);
4. **本地历史查询**trades/history 端点按 { start, end, code, strategyId, direction } 过滤返回 { orders, fills }(策略过滤走 FK 链 join);
5. **前端**:交易记录 tab 加策略过滤下拉(全部/各策略/未关联);历史范围展示本地库数据(不再「接口开发中」占位);今日仍走 QMT 实时 + 写穿本地;
6. **不回归**:今日实时展示(单表合并/费用/排序/展开)与持仓/策略/行情/连接配置功能正常;
7. **测试隔离**:回归测试在独立数据目录(ODL_TEST_DATA_DIR)执行,真实数据目录无写操作(技术约束-011)。
## 验收方法
- 独立数据目录(ODL_TEST_DATA_DIR)构造今日委托/成交样例 → 启动插件 → 确认同步落库、UPSERT 幂等(重复同步行数不变、状态覆盖);
- 构造持仓生命周期样例(当前持仓 + 已清仓)→ 确认策略归属推导正确(时间窗口命中、一码多策略取份额最大、未关联兜底);
- API 实测:trades/history 按时间段/code/策略/方向过滤返回正确;
- 前端:策略过滤下拉生效(今日实时 + 历史本地);历史范围展示本地数据;
- 回归:今日实时展示与现有功能正常;
- 手动核对真实数据目录:store.db 中 trade_orders/trade_fills 持续积累,无 JSON 文件新生。
## 验收目标
- 交易记录本地持久化闭环:当日实时 + 跨日历史本地可查;
- 外键链策略关联正确:按策略过滤 / 复盘交易就绪(R-007 Q10 落地);
- 设计约束同步落地:数据存储设计.md §10 + 技术方案约束新增条目。
@@ -0,0 +1,71 @@
# 技术实现方案:08-策略持仓行展开关联交易记录(Holding → 交易汇总)
> 依据:PLAN-009 需求:R-010(Q1-Q3 定稿)| 设计约束:技术约束-012、技术约束-011(测试隔离)
## 技术选型
- 复用 R-009 的 holding_id 关联(trade_orders.holding_id);
- 服务端:strategy-positions 附加 holding_id + trades/by-holding 端点;
- 前端:StrategyTab 持仓行展开(懒加载内嵌汇总表)。
## 数据流
```
策略持仓 tab
load() → strategy-positions(每行含 holding_id
用户点击持仓行 → 展开 → trades/by-holding?holdingId=13 → 委托汇总列表
懒加载:不展开不请求
```
## 实现细节
### 1. PositionManager.getStrategyPositions 附加 holding_id
```js
async getStrategyPositions(strategyId) {
const [positions, dataset, holdings] = await Promise.all([
this.getAllPositions(),
this.storage.getDataset(strategyId),
this.storage.getCurrentHoldings(strategyId), // 含 holding_id
]);
const shareMap = new Map(dataset.map(d => [d.code, d.shares]));
const holdingMap = new Map(holdings.map(h => [h.code, h.holdingId]));
return positions.map(p => {
const shares = shareMap.get(p.code) ?? 0;
return shares > 0 ? { ...p, shares, holdingId: holdingMap.get(p.code) ?? null } : null;
}).filter(Boolean);
}
```
### 2. SqliteStore.getOrdersByHolding
```js
getOrdersByHolding(holdingId) {
this.init();
return this.db.prepare(
'SELECT * FROM trade_orders WHERE holding_id=? ORDER BY insert_ts DESC'
).all(holdingId).map(r => this._mapOrderRow(r));
}
```
### 3. api/trades.jstrades/by-holding 端点
```js
case 'trades/by-holding':
return await storage.getTradeOrdersByHolding(args.holdingId);
```
### 4. StrategyTab 展开 UI
- 持仓行加展开箭头(类似交易记录 tab);
- 展开时调 trades/by-holding,内嵌小表格显示委托汇总:
时间 / 方向 / 状态 / 委托量 / 成交量 / 均价 / 金额 / 费用;
- 只显示委托汇总(ORDER_STATUS 中文映射复用);
- 懒加载:展开时才请求,折叠清空。
## 涉及设计约束
| 约束 | 内容 |
|---|---|
| 技术约束-012 | 数据存储遵循数据存储设计.md(复用 trade_orders.holding_id |
| 技术约束-011 | 测试用独立数据目录(ODL_TEST_DATA_DIR |
@@ -0,0 +1,38 @@
# 迭代复盘:08-策略持仓行展开关联交易记录(Holding → 交易汇总)
> 复盘日期:2026-09-02 | 迭代状态:**已完成(老师确认)**
> 关联需求:R-010(策略持仓行展开关联交易记录,已定稿)
> 关联计划:PLAN-009(计划-策略持仓行展开关联交易记录)
## 结果
迭代 08 达成:策略持仓 tab 每个持仓行(Holding)可展开,展开显示与该 holding 关联的交易记录(委托汇总,不分笔成交);持仓行新增成本价(avgPrice)+ 最后一笔成交价(lastTradePrice)两列;神之一手全部 tab 激活时隐藏 AI 对话输入框(纯 CSS)。实现「持仓 ↔ 交易」双向追溯。
## 过程事实
1. **需求定稿(R-010**:老师提出持仓行展开看关联交易(只要委托汇总)→ AI 登记 Q1-Q3(附加 holding_id / by-holding 端点 / 展开 UI)→ 老师确认 → 补充 Q4(成本价 + 最后一笔成交价,取最新有成交的 tradedPrice,无则默认成本价);
2. **服务端**strategy-positions 附加 holding_idPositionManager 查 strategy_holdings+ 计算 avgPrice/lastTradePrice(查该 holding 关联委托,取最新有成交的 tradedPrice);SqliteStore.getOrdersByHolding + DataStore 委托;api trades/by-holding 端点;
3. **前端**StrategyTab 持仓行展开(箭头 + 懒加载 + 内嵌委托汇总表,含交易日/时间列);成本价/最后一笔成交价两列;
4. **隐藏输入框**:借鉴 dsh-context 插件的纯 CSS 方案(:has(.odl-root) 命中时隐藏 composer)——神之一手 tab 激活时隐藏输入框,chat/其他 tab 正常显示;
5. **验证**:回归测试(by-holding 查询 6 项 + lastTradePrice 计算 4 项)通过、真实数据验证(001330.SZ holding 13 → 博纳影业卖出委托;成本价 6.84 / 最后成交 6.00)、构建 + typecheck 通过。
## 经验教训(复盘沉淀)
### 1. 隐藏宿主 UI 优先借鉴成熟插件方案
- 曾深挖 DSH composer chain / selector 机制(复杂度高、依赖内部 store),后经老师提示参考 dsh-context 插件——它用一行纯 CSS(:has() 选择器)实现「特定 tab 激活时隐藏输入框」,零宿主改动、零风险;
- **沉淀**:改宿主 UI 前先看同类插件怎么做的;纯 CSS :has() 是最优雅的 view 条件渲染方案。
### 2. 前端声明顺序 TDZ(本次多次踩坑)
- 迭代 07/08 多次遇到「Cannot access X before initialization」(TDZ):useEffect 依赖数组/回调在渲染时求值,引用了后声明的 const;
- **沉淀**React 组件内所有 useCallback/useEffect 的依赖引用必须在其声明之后;新增代码时严格核对声明顺序,构建后浏览器验证。
### 3. 服务端 vs 客户端生效机制
- 服务端改动(PositionManager/API/SqliteStore)需重启 DSH 生效;客户端 bundle 刷新即载(rev 变化自动同步);
- **沉淀**:改完先确认服务端端点/响应是新行为,再让老师重启。
## 遗留/后续
1. **持仓展开的交易汇总**:当前只显示该 holding 关联的委托汇总;后续可按 holding 聚合复盘(目标-006);
2. **关注列表 tab**:仍为占位(PlaceholderTab),后续迭代实现;
3. **全部持仓 tab**:未做展开(老师只要求策略持仓 tab);如需可复用同样模式;
4. **迭代 07 遗留**:QMT Bridge 历史接口(老师完善后接入)、盘中行情写 SQLite 验证(开盘后确认)。
@@ -0,0 +1,24 @@
# 迭代目标:08-策略持仓行展开关联交易记录(Holding → 交易汇总)
> 迭代编号:08 | 创建:2026-09-02 状态:进行中
> 依据计划:PLAN-009 需求:R-010(已定稿,2026-09-02
## 目标描述
策略持仓 tab 每个持仓行(Holding)可展开,展开显示与该 holding 关联的交易记录(仅委托汇总,不分笔成交),实现「持仓 ↔ 交易」双向追溯(R-009 反向)。
## 目标分解
1. **strategy-positions 附加 holding_id**:持仓行带出关联锚点;
2. **trades/by-holding 端点**:按 holding_id 查委托汇总;
3. **持仓行展开 UI**:展开显示委托汇总表(懒加载)。
## 讨论过程
- 2026-09-02 老师提出需求(持仓行展开看关联交易,只要委托汇总);
- 2026-09-02 AI 登记 R-010(讨论中),提出 Q1-Q3(附加 holding_id / by-holding 端点 / 展开 UI);
- 2026-09-02 老师确认 Q1-Q3 定稿,进入本迭代。
## 对老师的配合需求
- 无阻塞依赖;验收时用真实持仓(如 001330.SZ holding 13)验证展开效果。
@@ -0,0 +1,23 @@
# 验收标准:08-策略持仓行展开关联交易记录(Holding → 交易汇总)
## 验收标准线
1. **strategy-positions 附加 holding_id**:每个持仓行含 holding_id(同策略同 code 当前持仓);
2. **trades/by-holding 端点**:按 holding_id 返回该 holding 的委托汇总(时间/方向/状态/量/价/金额/费用;无成交委托也显示);
3. **持仓行展开**:策略持仓 tab 每行可展开(箭头指示),展开显示委托汇总表;
4. **仅汇总不分笔**:展开内容只显示委托汇总,无分笔成交明细;
5. **懒加载**:不展开不请求 trades/by-holding
6. **不回归**:策略持仓份额操作(添加/移出/全部移入)、交易记录 tab、行情等现有功能正常;
7. **测试隔离**:回归测试在独立数据目录执行(技术约束-011)。
## 验收方法
- 独立数据目录构造持仓 + 委托(holding_id 关联)→ 启动 → 验证 strategy-positions 含 holding_id、trades/by-holding 返回正确;
- 真实数据:网格超市 tab 展开 001330.SZholding 13)→ 看到博纳影业卖出委托汇总;
- 前端:展开/折叠、懒加载、汇总表列正确;
- 回归:份额操作、交易记录、行情不回归。
## 验收目标
- 持仓 ↔ 交易双向追溯闭环(R-009 交易→持仓 + 本迭代持仓→交易);
- 为按 holding 复盘交易铺路(目标-006)。
@@ -0,0 +1,82 @@
# 技术实现方案:09-Tab 设置统一管理
> 迭代编号:09 依据:PLAN-010、R-011、产品约束-009、UI约束-003、技术约束-014
## 1. 数据模型(src/settings.js
### 1.1 tabs 有序数组(新 schema
```js
tabs: z.array(z.object({
id: z.string().required(), // 唯一 id'tab-all-positions' / 'tab-strategy-<id>'
kind: z.enum(['builtin', 'strategy']).required(),
refKey: z.string().optional(), // builtin 专用:'allPositions' | 'tradeRecords' | 'watchlist'
refId: z.string().optional(), // strategy 专用:策略 id
name: z.string().required(), // 展示名(策略行 join 时刷新)
visible: z.boolean().default(true),
order: z.number().default(0),
})).default(DEFAULT_TABS)
```
### 1.2 归一化迁移(读取时)
getTabs(scope) 逻辑:
- 读取 settings.tabs:若为数组 → 直接返回(按 order 排序);
- 若为旧布尔对象(或缺失)→ 生成默认内置三条(全部持仓/交易记录/关注列表,保留原显隐值),策略按 strategies 旧 order 接续追加(旧 hidden → visible=true);
- 首次写入时落库归一化后的数组(幂等,不重复迁移)。
### 1.3 strategies 收窄
strategies schema 去掉 visible/order(仅 {id, name});getStrategies 不再排序/过滤 visible;策略展示名统一由 getTabs join strategies 得出(Q4 自动跟随改名)。
## 2. API 层(src/api/strategies.js
| 端点 | 变更 |
|---|---|
| tabs | 返回合并后的完整 tabs 数组(含策略行 name join |
| tabs/update | 整表更新 { tabs }(顺序 + 显隐);策略行 name 由服务端 join 刷新,客户端可只传 id 顺序 |
| strategies/add | 联动 append tab 条目(末尾,order = max+1 |
| strategies/remove | 联动删除对应 tab 条目(refId === 策略 id |
| strategies/move | **废弃**(从 METHODS 移除) |
| strategies/update | 仅剩重命名(联动刷新 tabs 中策略行 name) |
## 3. 客户端注册(src/client/index.js
合并 registerGeneralTabs + registerStrategyTabs → registerAllFromTabs(tabs)
```js
const sorted = tabs.slice().sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
for (const t of sorted) {
if (t.visible === false) continue;
const render = t.kind === 'builtin' ? GENERAL_RENDER[t.refKey] : (props) => createElement(StrategyTab, { ...props, strategyId: t.refId, strategyName: t.name });
// slots.register conversation.view, order: t.order
}
```
- GENERAL_RENDER = { allPositions, tradeRecords, watchlist } 映射常量;
- 移除硬编码 order 10/11/12 与 13+ 间隔(直接用 t.order);
- fetchTabConfig → fetchTabs 返回数组。
## 4. 设置页 UIsrc/client/views/SettingsSection.jsx
### 4.1 「Tab 设置」子 tab
- 子 tab 导航:general → tabs(更名),标签「Tab 设置」;
- 表格列:拖动手柄(≡)| 名称(内置行带「内置」徽标、策略行带「策略」徽标)| 显示(Switch);
- **无重命名/删除按钮(任何行)**;
- 拖动(原生 DnD):
- tr draggableonDragStart 记 idonDragOver 阻止默认 + 计算插入位;onDrop 重排数组;
- 落点 → 立即调 tabs/update(整表提交)→ 提示「已保存,刷新页面后生效」;
- 显隐开关:切换 → 立即调 tabs/update → 提示。
### 4.2 策略分组瘦身
- 移除排序箭头列(↑↓)与显示列(Switch);
- 保留:新增输入框 + 新增按钮 / 重命名 / 删除(确认弹窗 + 份额回未分配沿用);
- 顶部加提示:顺序与显示请在「Tab 设置」中调整。
## 5. 兼容与风险
- **迁移**:读取归一化幂等;旧策略 visible=false 迁移后 visible=trueQ3,产品约束-009 一致);
- **名称 join**tabs 存 refId,展示时 join strategies;策略改名后 tabs 行 name 自动跟随(Q4);
- **注册**:refresh = 刷新页面重载插件(无事件机制,沿用);
- **RPC**strategies/move 废弃需同步移除客户端调用(settings UI 不再有箭头);
- **回归**:技术约束-011 —— 写操作测试用独立数据目录(ODL_TEST_DATA_DIR / dataDir 参数)。
@@ -0,0 +1,41 @@
# 迭代复盘:09-Tab 设置统一管理(内置 + 策略分组,显示/隐藏 + 拖动排序)
> 复盘日期:2026-09-02 | 迭代状态:**已完成(老师确认)**
> 关联需求:R-011(Tab 设置:统一管理所有 tab,已定稿 Q1-Q5)
> 关联计划:PLAN-010(计划-Tab设置统一管理)
## 结果
迭代 09 达成:设置页「通用设置」升级为「Tab 设置」,成为所有会话 tab(系统内置 + 策略分组)**唯一的顺序与显隐入口**:两类 tab 混排一张表,每行 = 拖动手柄(原生 HTML5 DnD)+ 名称(内置带「内置」徽标、策略带「策略」徽标)+ 显示/隐藏开关;**任何 tab 均不支持重命名与删除**;拖动落点立即持久化;策略分组子 tab 瘦身为 新增/重命名/删除。
## 过程事实
1. **需求定稿(R-011**:老师提出设置页默认 tab 与策略 tab 一起排序(内置标记、不可重命名/删除)→ AI 分析现状(两套数据、两套 order 空间,内置恒在策略前)→ 提出统一 tabs 有序数组方案 → 老师确认 D1-D7 + Q1-Q5(拖动落点立即持久化 / 新增追加末尾 / 旧隐藏策略迁移后显示 / 名称 join 自动跟随 / 全部 tab 禁重命名删除);
2. **数据层(settings.js**:tabs 从布尔对象 → 统一有序数组(kind: builtin|strategy + refKey/refId + visible + order);strategies 收窄为 {id, name};旧格式读取时静默归一化迁移(schema union 兼容存量 + normalizeTabs 转换);addStrategy/removeStrategy 联动 tabs
3. **API 层(strategies.js**tabs/update 语义改整表(顺序 + 显隐);strategies/add 联动追加 tab(末尾);strategies/remove 联动删除 tab 条目;废弃 strategies/move
4. **客户端注册(client/index.js**:合并 registerGeneralTabs + registerStrategyTabs 为统一注册(读 tabs 数组按 order 排序、过滤 visiblebuiltin 走内置 render、strategy 走 StrategyTab),移除硬编码 order 间隔(10/11/12 与 13+);
5. **设置页 UISettingsSection.jsx**:「Tab 设置」子 tab(混排表格 + 徽标 + 显隐开关 + 拖动排序 + 落点立即持久化);策略分组瘦身(移除排序箭头与显隐开关,加指引提示);
6. **验证**:回归测试 35 项全通过(scripts/test-r011-tabs.mjs 数据层 23 项 + test-r011-api.mjs API 层 12 项)+ 真实 schema resolve 验证(旧配置归一化正确)+ 构建 + typecheck 通过。
## 经验教训(复盘沉淀)
### 1. schemastery 无 z.enum / .optional()schema 兼容存量用 z.union 双分支
- schemastery 只有 z.union([z.const(...), ...]) 无 z.enum,可选字段不调 .required() 即可;
- **关键**DSH settings 的 resolve 会用 schema 校验存量 user 层——新 schema 直接替换会导致旧配置(布尔对象 tabs)校验失败、插件启动报错;
- **沉淀**settings schema 变更必须先验证存量兼容——用 z.union 双分支(旧格式 + 新格式),旧值通过校验、读取时再归一化;改动 settings schema 前跑 schema-resolve 测试。
### 2. 读取时归一化 + schema 双分支 = 无感迁移
- 本次没有写一次性迁移脚本,而是「schema 接受旧格式 + normalizeTabs 读取时转新」:存量用户升级零操作、零报错;
- 归一化结果在首次写操作(updateTabs/add/remove)时落库为数组,之后自然持久化;
- **沉淀**settings 结构变更优先「schema 兼容 + 读取归一化」而非迁移脚本,符合 R-006/R-008 的迁移经验(幂等、无需用户操作)。
### 3. 前端字符串与 JSX 引号冲突
- 写 JSX 组件代码时,代码里同时有 JS 单双引号与 JSX 属性引号,用模板字符串包整段时内部反引号/插值冲突,多次报错;
- **沉淀**:批量生成代码用数组 join(每行独立字符串),避免模板字符串嵌套转义地狱。
## 遗留/后续
1. **Tab 设置拖动交互**:当前实现为整行 draggable(原生 DnD),落点插入到目标行位置;后续可优化为拖动手柄专属拖拽 + 拖拽中的视觉反馈(插入线);
2. **关注列表 tab**:仍为占位(PlaceholderTab),后续迭代实现;
3. **迁移落库时机**:旧配置首次读取不落库(纯读),首次写操作时才持久化新数组——如老师希望启动即落库可加一次性迁移(当前行为无感知差异,可接受);
4. **多 DSH 实例/多用户**:tabs 顺序为全局设置(非按会话),符合现状(strategies 亦为全局)。
@@ -0,0 +1,26 @@
# 迭代目标:09-Tab 设置统一管理(内置 + 策略分组,显示/隐藏 + 拖动排序)
> 迭代编号:09 | 创建:2026-09-02 状态:进行中
> 依据计划:PLAN-010 需求:R-011(已定稿,2026-09-02Q1-Q5 确认)
## 目标描述
将设置页「通用设置」升级为「Tab 设置」,成为所有会话 tab(系统内置 + 策略分组)**唯一的顺序与显隐入口**:两类 tab 混排一张表,每行 = 拖动手柄 + 名称 + 显示/隐藏开关;任何 tab 均不支持重命名与删除;拖动落点立即持久化。
## 目标分解
1. **数据模型统一**settings.js):tabs 布尔对象 → 有序数组,旧格式自动归一化;strategies 收窄为 {id, name}
2. **API 层**api/strategies.js):tabs/update 整表(顺序 + 显隐);strategies/add 联动追加 tabstrategies/remove 联动删除 tab;废弃 strategies/move
3. **客户端注册**client/index.js):合并为统一注册,读 tabs 数组(order 排序 + visible 过滤),移除硬编码 order;
4. **设置页 UI**SettingsSection.jsx):「Tab 设置」子 tab(混排 + 徽标 + 显隐开关 + 拖动排序)+ 策略分组瘦身(仅 新增/重命名/删除 + 指引提示)。
## 讨论过程
- 2026-09-02 老师提出需求:设置页默认几个 tab 与策略 tab 一起排序,内置标记、不可重命名/删除;
- 2026-09-02 AI 分析现状(两套数据/两套 order 空间),提出统一 tabs 有序数组方案;
- 2026-09-02 老师确认 D1-D7Tab 设置只做 显示/隐藏 + 排序(拖动),策略命名/删除留策略分组;
- 2026-09-02 老师确认 Q1-Q5:拖动落点立即持久化 / 新增追加末尾 / 旧隐藏策略迁移后显示 / 名称 join 自动跟随 / 全部 tab 禁重命名删除,R-011 定稿。
## 对老师的配合需求
- 验收时验证:拖动排序落点立即生效(刷新后 tab 栏按新顺序);显隐开关切换后刷新生效;策略分组瘦身后的新增/重命名/删除仍正常;老配置自动迁移无报错。
@@ -0,0 +1,26 @@
# 验收标准:09-Tab 设置统一管理
> 迭代编号:09 | 依据:PLAN-010 验收要点 + R-011 定稿
## 验收标准线
1. 「通用设置」子 tab 更名为「Tab 设置」,表格混排内置 + 策略全部 tab;
2. 内置行带「内置」徽标、策略行带「策略」徽标;
3. 每行可拖动排序,落点**立即持久化**(刷新后会话 tab 栏按新顺序,内置与策略可交错);
4. 每行有显示/隐藏开关,切换立即持久化(刷新后隐藏的 tab 消失/显示的出现);
5. **任何行均无重命名/删除按钮**
6. 策略分组子 tab 只留 新增/重命名/删除,无排序箭头/显隐开关,有指引提示;
7. 新增策略出现在 Tab 设置列表末尾;删除策略后对应 tab 条目消失(份额回未分配沿用);
8. 老配置(布尔对象 tabs + 策略 order/visible)读取自动归一化,无报错、无数据丢失;
9. 会话 tab 栏按新顺序与显隐注册;
10. 现有功能不回归(全部持仓/交易记录/策略持仓/QMT 切换/删除策略份额回退)。
## 验收方法
- 构建:pnpm run build + typecheck 通过;
- 单元/回归脚本:独立数据目录跑策略 CRUD + tabs 读写(技术约束-011);
- 手动(需 DSH 运行 + 老师确认):设置页 Tab 设置拖动排序 / 显隐切换 / 策略分组瘦身 / 老配置迁移;会话 tab 栏顺序与显隐验证。
## 验收目标
- 全部 10 条验收标准线通过,迭代 09 标记「验收通过」,R-011 实现状态更新,归档流程走查。
@@ -0,0 +1,43 @@
# 技术实现方案:10-UI 适配 DSH 主题
> 迭代编号:10 依据:PLAN-011、R-012
## 1. 主题机制(已查明)
- 宿主深色主题挂 `body[data-ds-dark-theme]`
- 宿主注入 `--dsw-*` CSS 变量(定义在宿主 runtime,随 light/dark/system 切换);
- 插件内联 style 直接引用 `var(--dsw-alias-xxx)` 即可自动适配。
## 2. 语义映射(实施基准,来自 R-012)
```
背景: #fff(表面) → var(--dsw-alias-bg-layer-1)
遮罩: rgba(0,0,0,.4) → var(--dsw-alias-bg-mask-1)
hover面: #f5f5f5 → var(--dsw-alias-interactive-bg-hover)
激活绿底: #e8f5e9/#f1f8f2 → color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)
错误红底: #fdecea → color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent)
信息蓝底: #e3f2fd → color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent)
主文字: #333/#222 → var(--dsw-alias-label-primary)
次文字: #555/#666 → var(--dsw-alias-label-secondary)
弱文字: #888/#999/#aaa → var(--dsw-alias-label-tertiary)
蓝字: #1565c0 → var(--dsw-alias-state-business-primary)
细线: #eee → var(--dsw-alias-border-l1)
描边: #ddd/#ccc → var(--dsw-alias-border-l2)
红(涨/错/删): #d32f2f/#c62828 → var(--dsw-alias-state-error-primary)
绿(跌/成/主): #2e7d32 → var(--dsw-alias-state-success-primary)
实心按钮字: #fff → var(--dsw-alias-button-contrast-fill)
```
## 3. 替换细则
- 替换范围:inline style 对象、模板字符串中的颜色字面量;
- `color-mix(in srgb, var(--xxx) 10%, transparent)` 用于需要「淡底 + 文字同色」的提示/激活态;
- 带透明度的原色(如 rgba(255,215,0,.5) 价格闪动高亮)保留动画语义,改 `color-mix(in srgb, var(--dsw-alias-state-warn-primary) 50%, transparent)`
- 注释中色值仅作说明可保留(不影响运行,验收时以无运行色为准);
- 每文件替换后 `node --check` 校验 JSX 语法。
## 4. 风险
- **语义偏差**:个别颜色无法精确定位语义 → 保留原色并在验收清单标注,交老师确认;
- **color-mix 兼容性**:现代浏览器(Chrome 111+/Safari 16.2+)支持,DSH 目标为 Chromium 系,可接受;
- **fallback**`var(--xxx, <原色>)` 提供兜底,宿主 token 缺失时视觉不变(更安全)。
@@ -0,0 +1,38 @@
# 迭代复盘:10-UI 适配 DSH 主题(浅色 / 深色 / 跟随系统)
> 复盘日期:2026-09-02 | 迭代状态:**已完成(老师确认)**
> 关联需求:R-012(UI 适配 DSH 主题,已定稿:暂定跟随系统)
> 关联计划:PLAN-011(计划-UI主题适配)
## 结果
迭代 10 达成:神之一手客户端 11 个文件共 141 处硬编码颜色全部替换为宿主 `--dsw-*` token(含 fallback),插件在 DSH 浅色 / 深色 / 跟随系统主题下自动适配;不自行维护主题偏好(跟随宿主);仅色值 token 化,布局与交互不变。
## 过程事实
1. **需求定稿(R-012**:老师提出适配 DSH 浅/深/系统主题 → AI 查明宿主机制(body[data-ds-dark-theme] + --dsw-* token,深色挂 data-ds-dark-theme、alias token 双值定义)→ 老师确认「暂定跟随系统」(D1-D5 + 语义映射表);
2. **替换实施**PriceCell / LoadState / Toast / PlaceholderTab / RangeSelector / AllPositionsTab / StrategyTab / TradeRecordsTab / QmtConnectionChip / SettingsSection 共 10 文件(+market 目录无颜色);
3. **语义映射执行**:白底→bg-layer-1、淡灰底→bg-layer-2、hover→interactive-bg-hover、主/次/弱文字→label-primary/secondary/tertiary、边框→border-l1/l2/l3/l4、红(涨/删/错)→state-error-primary、绿(跌/成/激活)→state-success-primary、蓝(信息/业务)→state-business-primary、实心按钮字→button-contrast-fill、遮罩→bg-mask-1、阴影→shadow-lv3
4. **淡色底**:激活/提示底色用 color-mix(in srgb, var(--语义色) 10-12%, transparent),深浅主题自适应;
5. **验证**typecheck + build 通过;headless Chrome 实证宿主 token 系统完整(238 处 dsw-alias 引用、浅/深双值定义、data-ds-dark-theme 选择器);残留硬编码色 = 0。
## 经验教训(复盘沉淀)
### 1. 宿主主题机制:body[data-ds-dark-theme] + --dsw-* token(已实证)
- DSH 主题不是 data-theme 属性切换,而是宿主在深色时给 body 挂 `data-ds-dark-theme`token 以「alias 链 → static 值」双主题注入(light: neutral-bluish-00 白系;dark: neutral-bluish-875 深系);
- **沉淀**:插件适配宿主主题只须引用 `var(--dsw-alias-xxx, fallback)`fallback 保证 token 缺失时浅色可用;不要自建主题偏好。
### 2. var() 带 fallback 是安全的迁移策略
- 每处替换写成 `var(--dsw-alias-xxx, #原色)`:宿主 token 定义齐全时自动适配;万一某 token 缺失(宿主版本差异),退回原浅色值不破相;
- **沉淀**:对宿主 token 的依赖一律带 fallback,兼容宿主版本演进。
### 3. 批量替换的 edit 冲突处理
- 多个相同 style 片段(如表头、输入框、删除按钮)导致 old_string 多处匹配:用 replace_all 处理真正相同的模式,或用带上下文的更精确 old_string
- **沉淀**:批量替换前先 grep 去重确认唯一性,相同模式直接用 replace_all,不同上下文逐条处理。
## 遗留/后续
1. **语义色待老师验收**:深色下个别语义色(state-error 红 / state-success 绿在深色底的对比度、紫/蓝徽标)观感需老师切主题确认;若个别不满意可后续加 `--odl-*` 覆盖(D5 暂缓项);
2. **color-mix 兼容性**:现代 Chromium 支持;若遇旧内核浏览器个别淡底失效,fallback 无(color-mix 无 fallback 语法)——可后续降级处理;
3. **Toast 样式**:随宿主语义色变化,实心绿/红底 + 白字在深色下对比度已由 token 保证;
4. **shadows**boxShadow 用了 shadow-lv3 token(宿主完整 shadow 值),个别较浅卡片阴影在深色下可能几乎不可见——可后续微调。
@@ -0,0 +1,18 @@
# 迭代目标:10-UI 适配 DSH 主题(浅色 / 深色 / 跟随系统)
> 迭代编号:10 | 创建:2026-09-02 状态:进行中
> 依据计划:PLAN-011 需求:R-012(已定稿,2026-09-02,暂定跟随系统)
## 目标描述
神之一手客户端 UI 全部硬编码色值替换为宿主 `--dsw-*` token,使插件在 DSH 浅色 / 深色 / 跟随系统主题下均可读、协调,随主题自动切换。
## 目标分解
1. 10 个文件 141 处硬编码色按语义映射替换为宿主 token / color-mix
2. 涨跌红涨绿跌 → 宿主 state-error/success;主按钮/徽标/提示底色 → 宿主语义色;
3. 构建 + typecheck + 深浅主题人工验收。
## 对老师的配合需求
- 验收:DSH 设置切换 浅色/深色/跟随系统,检查各页面可读性与协调性。
@@ -0,0 +1,22 @@
# 验收标准:10-UI 适配 DSH 主题
> 迭代编号:10 | 依据:PLAN-011 验收要点 + R-012
## 验收标准线
1. 浅色主题下插件各页面观感与现状基本一致(无突兀色差);
2. 深色主题下所有页面可读(背景/文字/边框/按钮/涨跌/徽标/提示/下拉菜单协调);
3. DSH 设置切换 浅色/深色/跟随系统 实时生效;
4. 涨跌色(红涨绿跌)在深浅两主题下均醒目可辨;
5. 运行代码无残留硬编码色(#xxx / rgb / rgba),注释可留;
6. 布局与交互不变(仅色值);
7. build + typecheck 通过。
## 验收方法
- build + typecheck
- 老师切 DSH 浅/深主题人工检查:设置页(Tab 设置/策略分组/QMT 卡片)、全部持仓、交易记录、策略持仓、会话头部 QMT chip 下拉、Toast。
## 验收目标
- 7 条验收线通过,迭代 10 标记「验收通过」,R-012 更新实现状态。
@@ -0,0 +1,140 @@
# 技术实现方案:11-策略自定义字段配置
> 迭代编号:11 依据:PLAN-012 + R-013(目标数据模型 / 端到端示例)+ 技术约束-015 / 产品约束-010 / UI约束-005
## 1. 数据层
### 1.1 settings.strategies 扩展 configSchemasrc/settings.js
```js
// strategySchema 中 strategies 项扩展:
strategies: z.array(z.object({
id: z.string().required(),
name: z.string().required(),
configSchema: z.array(z.object({
key: z.string().required(), // 稳健标识(英文 slug
label: z.string(), // UI 展示名(可中文)
type: z.union([z.const('text'), z.const('number'), z.const('boolean'), z.const('enum')]).required(),
enum: z.array(z.string()), // 仅 type=enum 时使用
def: z.any(), // 默认值(text=string / number=number / boolean=boolean / enum=枚举项)
unit: z.string().default(''), // 单位(2026-09-02 加:可为空;如 % / 元 / 手;展示时拼在值后)
})).default([]), // 缺省 []
})).default(DEFAULT_STRATEGIES),
```
- 读取归一化:旧项无 configSchema → 补默认空数组(读取时或 schema default 保证);
- addStrategy 返回值默认带 configSchema: []renameStrategy / updateStrategies 整表更新天然携带;
- 兼容:settings 旧数据({id,name})经 schemastery default/union 归一化,无迁移脚本。
### 1.2 strategy_holdings 新增 values 列 + 读写(src/storage/SqliteStore.js
```js
// init() 内补列(幂等,沿用 _ensureTradeAttributionColumns 模式;只读连接容忍)
_ensureHoldingValuesColumn() {
const cols = new Set(this.db.prepare('PRAGMA table_info(strategy_holdings)').all().map(c => c.name));
if (!cols.has('values')) this.db.exec('ALTER TABLE strategy_holdings ADD COLUMN values TEXT');
}
readValues(holdingId) // SELECT values FROM strategy_holdings WHERE holding_id=? → JSON.parse ?? null
writeValues(holdingId, values) // UPDATE strategy_holdings SET values=? WHERE holding_id=? → JSON.stringify
```
- openHolding/addShares/reduceShares/closeHolding **不碰 values 列**(份额生命周期与字段值独立);
- getCurrentHoldings / getHoldingHistory 的 _mapHolding 附加 values 字段(解析 JSON)。
## 2. API 层(src/api/strategies.js
```js
// 既有:strategy-positions → manager.getStrategyPositions(strategyId)
// PositionManager 返回项附加 valuesfrom SqliteStore 映射)
// 新增端点:
case 'holdings/values-update':
return await manager.updateHoldingValues(args.holdingId, args.values);
```
```js
// src/position/PositionManager.js —— updateHoldingValues(holdingId, values)
// 1) 定位 holdings 行(须存在且当前持仓 closed_at IS NULL;否则 404
// 2) 服务端校验(按所属策略 configSchema):
// number → Number.isFinite(Number(v))enum → enum.includes(v)boolean → typeof v === 'boolean'
// 未定义 key 的额外键放行(可扩展);空值/缺省可写入
// 3) 校验失败抛 { code: 'field-validation' },成功 writeValues 并返回 { holdingId, values }
```
## 3. 设置页 UIsrc/client/views/SettingsSection.jsx
- 「策略分组」子 tab 策略行加展开手柄(▶/▸ toggle,样式沿用 expand 惯例);
- 展开区:
- 字段列表表格:展示名 | key | 类型 | 枚举选项 | 默认值 | 操作(删除)—— 空态提示「尚未配置自定义字段」;
- 添加/编辑字段表单:字段名(label)→ 自动生成 key(拼音 slug,复用 generateStrategyId 思路)/ 可手改校验唯一、类型下拉(文本/数字/布尔/枚举)、枚举选项(type=enum 时逗号分隔输入)、默认值(按类型渲染输入);
- 行内编辑/删除:编辑回填表单,删除后保存生效;
- 保存:整表提交 strategies/update(映射为 { ...s, configSchema }),成功 Toast + 刷新(沿用「刷新页面后生效」机制)。
## 4. 策略持仓 tab UIsrc/client/views/StrategyTab.jsx
> 2026-09-02 演进(老师选 A + 列化):自定义字段**不再放展开区**,直接作为表格列展示;值编辑 = **点击字段单元格内联编辑**;展开区(R-010)仅保留交易明细。
- 自定义字段列按列配置(§7)渲染,取值 = values[key] ?? defboolean→开/关,空→—);
- 点击字段单元格(有 holding 锚点行)进入内联编辑:text/number=输入框(Enter 保存 / Esc 取消 / blur 保存)、boolean=开关(点击即提交切换)、enum=下拉(选择即提交);
- 保存调 one-divine-lot/holdings/values-update(按该行现有 values 合并,仅改本字段;空值删键回退 def);成功刷新,失败 Toast;
- 单元格点击 stopPropagation,不与行展开(tr onClick)冲突;无 holding 锚点(未分配)的字段列不可编辑(虚线标识可点);
## 5. 回归脚本(scripts/test-r013-custom-fields.mjs
- ODL_TEST_DATA_DIR 独立数据目录(技术约束-011);
- 用例:addStrategy 带 configSchema(四类型)→ openHolding ×2 同策略不同 code → writeValues 各自值 → 读回校验 → 校验拦截(数字非法 / 枚举越界)→ 旧策略无 configSchema 持仓行 readValues 为 NULL
- 断言存储与读取一致、校验拒绝。
## 7. 表格列显隐配置(迭代 11 范围扩展,2026-09-02 老师确认)
> 背景:R-013 落地后老师要求——策略持仓表的**列可显示/隐藏配置**:不限于自定义字段列,还包含持仓表原有基础数据列;除 代码/名称/操作(+展开箭头)外全部支持;**每策略独立配置**;入口 = 策略持仓 tab 顶部「列设置」;支持**列排序**。并入本次迭代。
### 7.1 数据模型(定稿)
```js
// settings 新增 strategyColumns(仅存与默认不同的覆盖;读取时归一化)
strategyColumns: {
[strategyId]: [
{ key: 'lastPrice', visible: true }, // 基础数据列(可配置区)
{ key: 'pctChange', visible: true },
{ key: 'lastClose', visible: false },
{ key: 'avgPrice', visible: true },
{ key: 'lastTradePrice', visible: true },
{ key: 'shares', visible: true },
{ key: 'gridGap', visible: true }, // 自定义字段列(key = configSchema.key
{ key: 'gridAmount', visible: true },
],
}
```
**列分类**
- **固定列**:展开箭头 | 代码 | 名称(恒显、锁最前)| 操作(锁最后,含 全部移入/移出)—— 不可配置;
- **可配置区**:基础数据列(现价/涨幅/昨收/成本价/最后一笔成交价/份额)+ 该策略全部自定义字段列(key=configSchema.key)—— 显隐 + 顺序可配置;
- 自定义字段列**存在性**仍由 configSchema 决定(策略未配该字段则无此列);configSchema 增删字段 → 列配置读取时自动跟随(新增字段默认追加末尾、删除字段自然移除)。
**显隐单一数据源**(老师确认 2026-09-02):visible **只存在 strategyColumns**configSchema **不加 visible**;展开区编辑区恒显示该策略全部字段(编辑入口需全量),表格列显隐由列配置单独管——避免双显隐源打架。
**默认值/兼容**:未配置 strategyColumns 的策略 → 归一化为 基础列全显(默认序)+ configSchema 字段列全显(configSchema 序);老数据缺字段 → 补默认。
### 7.2 涉及改动
1. **src/settings.js**:基础列目录常量 COLUMN_METAkey/label);strategySchema + strategyColumns;读取归一化 normalizeStrategyColumns(基础列 + 该策略 configSchema 字段合成全列 → 应用覆盖 → 返回有序可见列);更新 API;
2. **src/api/strategies.js**:新增 strategy-columns(读归一化结果)/ strategy-columns/update {strategyId, columns}
3. **src/client/views/StrategyTab.jsx**:表格动态列化——表头/行按列配置渲染(固定列 + 可见可配置列),行数据映射每列取值;
4. **新组件 ColumnSettingsPopover**(views/):tab 顶部「列设置」按钮 → 弹层:可配置列列表(标签 + 显隐勾选 + 拖动排序)+ 立即持久化 strategy-columns/update
5. 展开区(§4)不再显示自定义字段(已列化),仅保留交易明细;字段值编辑 = 表格字段单元格点击内联编辑(§4 演进);
6. 回归脚本 + typecheck/build。
### 7.3 列取值(StrategyTab 渲染参考)
| 列 key | 取值 |
|---|---|
| lastPrice / lastClose / pctChange | getPrice(code) 行情 |
| avgPrice / lastTradePrice | p 的字段(QMT 成本价 / R-010 最后一笔成交价) |
| shares | p.shares |
| 自定义字段 key | p.values?.[key] ?? def(展示同展开区:boolean→开/关,空→—) |
## 6. 验证
- build + typecheck 通过;
- 老师人工验收(见验收标准)。
@@ -0,0 +1,75 @@
# 程序结构设计:11-策略自定义字段配置(含结构归类优化)
> 迭代编号:11 | 2026-09-02 | 依据:技术约束-016(目录归类)、R-013、PLAN-012
## 背景:结构审查发现与优化
2026-09-02 结构审查发现:src/component/ 平铺 10 个服务端模块,偏离迭代 01/02 程序结构设计的语义分域(data-source/、position/、storage/);`component` 命名语义含混(UI 组件实际在 client/views/);归类演进无文档记录;存在死代码。
## 优化动作(2026-09-02 落地)
1. **归类还原**component/ 平铺 → 按功能域分目录:
```
src/
├── index.js # 插件入口(组装各模块)
├── settings.js # 设置管理(策略 + tabs + QMT 连接配置)
├── api/ # 服务端 HTTP APIwebServer /odl/api/*),按领域拆子文件
│ ├── index.js # 路由入口(合并各领域 + 405/404/500
│ ├── common.js # 公共工具
│ ├── positions.js # 持仓域
│ ├── strategies.js # 策略/份额/tabs 域
│ ├── qmt-connections.js # QMT 连接配置域
│ ├── market.js # 行情域
│ └── trades.js # 交易记录域
├── data-source/ # 数据源抽象层(技术约束-001/003)
│ ├── QmtBridgeRestDataSource.js # QMT Bridge REST 适配器
│ ├── data-source-types.js # 统一数据模型(Position/DataSource 接口)
│ └── QmtHealthMonitor.js # QMT 连接健康检查(数据源健康缓存)
├── storage/ # 存储层(R-008 SQLite
│ ├── SqliteStore.js # SQLite 存储封装(node:sqlite
│ └── DataStore.js # 存储门面(对外兼容 API + 迁移)
├── position/ # 分仓业务逻辑
│ └── PositionManager.js # 持仓↔策略份额分配、聚合视图
├── market/ # 行情域
│ ├── MarketDataHub.js # 行情缓存(内存 + 落盘 + 查询)
│ └── MarketFeed.js # 行情获取(WS + REST 轮询 + prime
├── trades/ # 交易域
│ └── TradeSync.js # 交易记录定时同步(QMT → SQLite)
└── client/ # 客户端(UI)
├── index.js # 客户端入口:tab/settings 注册
├── market/ # MarketDataProvider.jsx(行情 context
└── views/ # React 组件(StrategyTab / SettingsSection / Toast 等)
```
2. **死代码清理**:删除 src/component/AllocationStorage.js(旧 allocations.json 存储,0 引用);删除 DataStore.setDataset/removeDatasetPositionManager 适配单票生命周期后无调用方);
3. **构建与脚本同步**tsdown.config.ts entry 更新为新目录 globscripts/*.mjs 导入路径更新;
4. **验证**typecheck + build 通过;test-r009 回归 14/14 通过(独立数据目录)。
## 归类规则(沉淀为技术约束-016)
- 服务端代码禁止平铺,按功能域分目录:data-source / storage / position / market / trades
- api/ 按领域拆子文件;client/ 仅放 UIviews/ 组件 + market/ provider);
- 文件命名 = 类名 PascalCase + .js/.jsx
- 新增服务端模块必须先落对应域目录,无合适域时先讨论补域,不得回退平铺。
## R-013 新增改动落点(本迭代实施)
```
src/settings.js # +configSchema(策略 schema 扩展)
src/storage/SqliteStore.js # +_ensureHoldingValuesColumn + readValues/writeValues
src/position/PositionManager.js # +updateHoldingValues(按 configSchema 校验)
src/api/strategies.js # +holdings/values-update 端点;strategy-positions 附加 values
src/client/views/SettingsSection.jsx # 「策略分组」策略行展开配置字段
src/client/views/StrategyTab.jsx # 持仓行展开区「自定义字段」编辑
scripts/test-r013-custom-fields.mjs # 回归脚本(独立数据目录)
```
## 后续动作(2026-09-02 结构约束对齐)
归类重构落地后,将活动文档中的旧路径引用同步到新结构(历史档案按追加式原则不改写):
- docs/03-设计约束/数据存储设计.md:三处「对应实现」表 12 处 src/component/* → 新域目录(storage/position/market/trades);
- docs/05-需求池/R-013.md:涉及改动面 SqliteStore → src/storage/
- docs/02-计划/计划-策略自定义字段配置.md:文件树 → storage/SqliteStore.js
- 本迭代技术实现方案:1.2 节标题路径 + §2 PositionManager 路径标注 → 新目录;
- 保留不改:迭代 04 说明/复盘、R-008 归档、旧计划中的 component/ 描述(历史事实)。
@@ -0,0 +1,48 @@
# 迭代复盘:11-策略自定义字段配置(定义随策略,值落库 + 列显隐)
> 复盘日期:2026-09-02 | 迭代状态:**已完成(老师确认)**
> 关联需求:R-013(策略自定义字段配置,已定稿 Q1-Q4 + D6)
> 关联计划:PLAN-012(计划-策略自定义字段配置)
## 结果
迭代 11 达成:策略支持**自定义字段**(定义随策略 configSchema 存 settings 不落库,值落 strategy_holdings.values JSON 列),字段作为策略持仓表**列**展示,支持**列显隐/排序**(每策略独立配置)、**单元格点击编辑**(含单位、可编辑视觉记号)。配置页「策略分组」策略行展开配置字段。旧策略(无 configSchema)行为与现状一致。
## 过程事实
1. **需求定稿(R-013**:老师提出每个策略可加自定义字段 → 多轮讨论收敛(定义不落库随策略取 / 值落库 / 类型化 text·number·boolean·enum / 枚举有用 / 旧策略兼容 Q4 / 值编辑入口 D6)→ 老师确认定稿,端到端示例入档;
2. **范围扩展(列显隐,老师 2026-09-02)**:字段升级为持仓表列 + 统一显隐管理(不限于自定义字段,含基础数据列);除 代码/名称/操作 外全部可配置;每策略独立;入口 = tab 顶部「列设置」;支持列排序 → visible 单一数据源(strategyColumnsconfigSchema 不加 visible)老师确认;
3. **实现**settings.jsconfigSchema + COLUMN_META + strategyColumns + 归一化)、SqliteStorevalues 列幂等 ALTER + readValues/writeValues)、DataStore(透传)、PositionManagerupdateHoldingValues 按 configSchema 校验 + getStrategyPositions 附 values)、apiholdings/values-update + strategy-columns 端点)、index.js(注入 getStrategySchema);
4. **UI**StrategyFieldsEditor(设置页字段定义编辑器)、StrategyTab 动态列化 + 列设置弹层 ColumnSettingsPopover + 单元格点击编辑 FieldCellEditor;自定义字段从展开交易明细移出(老师指示,字段已列化);
5. **UI 细节(老师验收反馈迭代)**:✎ 可编辑记号(值后)、编辑态 ✓/✕ 确认按钮、切换编辑列宽稳定(输入框内容宽度)、数字输入框统一 5 位宽、字段 unit 单位(可为空,展示拼值后);
6. **修复**:配置页白屏(SettingsSection Fragment/StrategyFieldsEditor 未导入 → 运行时 ReferenceErrorcheckJs:false 未捕获);z.dict dts 推断引用 cosmokit(显式类型注解);
7. **验证**typecheck + build 通过;回归 test-r013-custom-fields 21/21、test-r013-columns 16/16、test-r011-tabs 23/23、test-r009 14/14
8. **提交**aac16b9R-013 完整实现 + 列显隐 + 单元格编辑),已推送 origin/main。
## 经验教训(复盘沉淀)
### 1. JSX 运行时错误 vs typecheck 盲区(白屏根因)
- 项目 checkJs:false → .jsx 不校验未导入标识符;`<Fragment>` 未 import / 组件未 import 只在运行时 ReferenceError → 整个组件树崩溃白屏;
- **沉淀**:jsx 改动必须自查「使用的 JSX 大写标签是否有 import/定义」(typecheck 不兜底);已建议加 client 构建期 lint 或保持自查清单。
### 2. z.dict 的 dts 推断陷阱
- schemastery z.dict 返回值类型引用 cosmokit Dict → dts 生成报「cannot be named without reference」;
- **沉淀**settings schema 用 z.dict 时给变量加显式 Schema 类型注解(@type import('@deepseek-ai/schemastery').Schema<any>)规避。
### 3. SQLite 关键字列名
- values 是 SQLite 保留字(INSERT VALUES)→ ALTER/SELECT/UPDATE 裸写报语法错误;
- **沉淀**:列名需双引号 `"values"`(PRAGMA 检测用裸名 OK);若可改名优先避开关键字。
### 4. .map(this._mapHolding) 的 this 丢失
- 方法内新增调用 this._parseValues 后,`.map(this._mapHolding)` 裸传方法引用 → this 丢失 TypeError
- **沉淀**:map 回调若依赖 this,改模块级纯函数或 `(row) => this._mapHolding(row)`
### 5. 编辑态控件宽度 vs 表格列宽稳定
- 固定宽输入框(90px)进入编辑使列宽跳变;改内容宽度(ch 随文本)+ 数字固定 5 位宽 + 紧凑 ✓/✕ 后变化最小;
- **沉淀**:表格内联编辑控件宽度应贴近展示态(内容宽度/位数基准),按钮用紧凑 inline(浮层会溢出遮挡相邻单元格)。
## 遗留/后续
1. **T-006 数据池 + 策略表格动态字段配置**(需求池起草):本迭代的列显隐/字段列化是其「表格列配置」子集的落地,数据池中间层仍未做——后续如需「列名→数据池字段映射/多源字段」再评估;
2. **列排序交互**:本期列设置用 ↑↓ 箭头(零依赖);如老师要 Tab 设置同款拖拽可后续换 DnD;
3. **值校验服务端已实现**,前端编辑即时提示靠服务端错误 Toast——如需前端预校验可后续加。
@@ -0,0 +1,21 @@
# 迭代目标:11-策略自定义字段配置(定义随策略,值落库)
> 迭代编号:11 | 创建:2026-09-02 状态:进行中
> 依据计划:PLAN-012 需求:R-013(已定稿,2026-09-02,老师确认 Q1-Q4 + D6
## 目标描述
给策略增加**可自定义、可扩展**的字段能力:每个策略在设置页「策略分组」子 tab 配置自定义字段定义(configSchemakey/label/type/enum/默认值,随策略定义存 settings 不落库);该策略下每个持仓(strategy_holdings 行)按所属策略的定义存取一份键值对值(新增 values JSON 列落库);自定义字段作为策略持仓表**列**展示,点击单元格内联编辑值(列显隐与顺序每策略独立配置)。旧策略(无定义)行为与现状完全一致。
## 目标分解
1. 数据层:settings.strategies 扩展 configSchemaschemasterytype union text|number|boolean|enum),读取归一化(旧项缺省 []);strategy_holdings 新增 values TEXT 列(幂等 ALTER+ readValues/writeValues
2. API 层:strategy-positions 每行附 values;新增 holdings/values-update {holdingId, values} 写回(服务端按 configSchema 校验:数字/枚举/布尔);
3. 设置页 UI:「策略分组」子 tab 策略行可展开 → 字段列表 + 添加/编辑/删除字段表单 + 保存;
4. 策略持仓 UI:自定义字段列表格列展示 + 单元格点击内联编辑(四类型);列显隐/顺序每策略独立配置(顶部「列设置」);
5. 回归脚本(独立数据目录,技术约束-011)+ 验证 + 验收复核。
## 对老师的配合需求
- 验收:设置页配置字段定义(四种类型各一 + 枚举)→ 策略持仓 tab 持仓行展开编辑值 → 重启验证持久化;
- 提供:是否接受「旧策略无定义」行为样(默认接受,Q4 已确认)。
@@ -0,0 +1,22 @@
# 验收标准:11-策略自定义字段配置
> 迭代编号:11 | 依据:PLAN-012 验收要点 + R-013
## 验收标准线
1. 设置页「策略分组」:策略行可展开,添加 / 编辑 / 删除字段(四种类型:文本/数字/布尔/枚举,含枚举选项与默认值)保存后生效,重启后定义仍在;
2. 策略持仓 tab:自定义字段以**列**形式展示(仅该策略配置了字段时),点击字段单元格内联编辑(文本/数字=输入框、布尔=开关、枚举=下拉),Enter/选择/开关即保存,刷新后仍在(已落库);展开区仅交易明细,不再含自定义字段;
3. 同策略多行各有各的值(600719 与 300057 互不影响);不同策略字段集互不影响;
4. 旧策略(无 configSchema):持仓行不渲染字段区,现有操作(加仓/减仓/清仓/展开交易记录)与现状完全一致;
5. 服务端校验生效:数字填非数字、枚举填选项外 → 拒绝保存并 Toast 提示;空值/缺省允许;
6. 兼容迁移:既有 strategy_holdings 数据行(无 values)补列后为 NULL,读取正常,不报错;
7. build + typecheck 通过;回归脚本(test-r013-custom-fields.mjs,独立数据目录)全绿。
## 验收方法
- 回归脚本跑通(隔离数据目录);
- 老师人工验收:设置页配置网格超市 4 字段 → 策略持仓 tab 两个持仓行展开分别编辑值 → 另配长线持有 2 字段观察互不影响 → 手动做T(旧策略)确认无字段区 → 重启后确认定义与值均保留。
## 验收目标
- 7 条验收线通过,迭代 11 标记「验收通过」,R-013 更新实现状态(已实现),归档。
@@ -0,0 +1,85 @@
# 技术实现方案:12-持仓内存快照
> 迭代编号:12 依据:PLAN-013 + R-014(老师四问拍板)+ 技术约束-010/012/016;新增 技术约束-017
## 1. PositionSyncsrc/position/PositionSync.js,新增)
### 1.1 数据结构与生命周期
```js
class PositionSync {
snapshot = []; // 内存快照:Position[]mapPosition 语义化输出;只读约定)
syncedAt = 0; // 最近成功同步毫秒时间戳(0 = 从未成功)
_ghostMiss = new Map(); // 幽灵防抖:code → 连续消失轮数
_lastAccountId = null; // 账户身份守卫
stats = { syncCount, failCount, lastError, closedGhosts, lastSyncedAt };
}
```
- `start()`:立即预热一次(syncNow,失败仅 warn+ `setInterval(10s)``stop()` 清定时器,**内存快照保留**(stop 后读方仍可消费最后快照);
- 对外读:`getSnapshot()`(数组本体,只读约定)/`getSyncedAt()`/`backfill(positions)`(读穿透回填入口,仅非空回填);
- `syncNow()` 可手动调用(**不检查 mounted**——测试与热切换后手动刷新均可用;仅 syncing 防重入)。
### 1.2 同步一轮(syncNow
```
mounted 不拦手动同步 → syncing 防重入
positions = dataSource.getPositions() // 全量
非数组 → 抛错(走失败分支)
空数组 → 双重确认:isAvailable() && getAsset().accountId
双通过 → 接受为「真清仓」(接受空快照)
否则 → 视为 QMT 异常(未登录/半可用),保留旧快照,failCount++
成功 → snapshot = positions(整体替换);syncedAt = nowstats.syncCount++
→ _autoCloseGhosts(snapshot)(失败不影响快照)
任何异常 → failCount++ / lastError 记录;不动内存(读方继续消费旧快照)
```
### 1.3 幽灵持仓自动清仓(_autoCloseGhosts,老师拍板「同步时自动清仓」)
- 判定对象:本地 `getCurrentHoldings()`(全部策略、closed_at IS NULL)中 **code 不在本轮快照**的条目;
- 防抖:连续 3 轮(`ghostRounds=3`,约 30s)消失才清仓;快照复现 → 计数复位;
- 清仓动作:该 code **全部策略**的当前持仓 `closeHolding(strategyId, code)`shares=0 + closed_at=now**不物理删除**,历史保留语义不变);
- **账户身份守卫**(防误清核心):
- 每轮顺带 `getAsset().accountId`;身份未知 → 当轮**跳过判定**(不累计不清零,保守);
- accountId 变化(R-004 连接热切换/换账户)→ **清空计数 + 当轮直接跳过**(旧账户快照不可信);
- 部分减持不触发(QMT 仍有该 code)——账实差额由前端「未分配为负」暴露,属产品已知行为;
- 单码清仓失败:保留计数,下轮重试。
## 2. 读路径切换(src/position/PositionManager.js
```js
constructor({ dataSource, storage, getStrategySchema, positionSync }) // positionSync 可选注入
async getAllPositions() {
if (!this.positionSync) return this.dataSource.getPositions(); // 未注入 → 旧穿透(兼容)
const cached = this.positionSync.getSnapshot();
if (cached.length > 0) return cached; // 主路径:读快照
const positions = await this.dataSource.getPositions(); // 读穿透:当场拉一次
this.positionSync.backfill(positions); // 回填(仅非空)
return positions; // QMT 也挂 → 抛错(前端 LoadState 重试)
}
```
- strategy-positions / unallocated / summary 三个接口全部经 getAllPositions**自动切换,前端零改动**;
- 闸门校验(addToStrategy 不超实盘)改用快照总量——与页面显示的未分配数**同源同鲜度**,自洽。
## 3. 装配(src/index.js
```js
const positionSync = new PositionSync({ runtime: { dataSource, storage }, logger });
positionSync.start();
const manager = new PositionManager({ dataSource, storage, positionSync, getStrategySchema });
registerApi(ctx, { ..., positionSync });
// disposepositionSync.stop()(在 marketFeed.stop 之后、tradeSync.stop 之前)
```
## 4. 回归脚本(scripts/test-position-sync.mjs,纯内存 mock
- **不落库不碰真实数据目录**(内存快照方案下技术约束-011 天然满足);
- mock:可编程 dataSourcepositions/accountId/healthy 可变状态)+ 内存 storagegetCurrentHoldings/closeHolding 记录调用);
- 34 项用例:基本流 / 失败保留快照 / 空快照四分支(health 挂、身份未知、双通过、有旧快照)/ 幽灵防抖(3 轮关闭、复现复位、账户切换重置、身份丢失跳过+恢复后关闭)/ 读路径(快照零 QMT 调用、读穿透回填、未注入兼容、QMT 挂抛错)/ getStrategyPositions 组装回归(shares/holdingId/lastTradePrice/values/ 定时器冒烟。
## 5. 验证
- typecheck + build 通过;test-position-sync 34/34test-r013-custom-fields 21/21(未注入 positionSync 兼容性证明);
- 老师人工验收(见验收标准)。
@@ -0,0 +1,46 @@
# 迭代复盘:12-持仓内存快照(服务端 10s 定时同步,请求不再穿透 QMT)
> 复盘日期:2026-09-02 | 迭代状态:**已实施,待老师人工验收**
> 关联需求:R-014(持仓内存快照,已定稿)
> 关联计划:PLAN-013(计划-持仓内存快照)
## 结果
迭代 12 达成:服务端建全量持仓**内存快照**(PositionSync10s 定时全量同步,**不落库**),策略持仓 / 全部持仓 / 未分配三个接口改读快照为准;同步失败保留上次快照(QMT 抖动不再白屏);空快照双重确认(health + 账户身份);快照为空读穿透兜底;**幽灵持仓自动清仓**(连续 3 轮消失 + 账户身份守卫防误清)。前端零改动,SQLite 零 schema 变更。
## 过程事实
1. **起因(架构梳理讨论)**:老师要求梳理持仓/实盘数据存储与同步机制 → 梳理结论「本地数据准确性靠打开 tab 时与实盘当场对账,无对账任务、无修正、无告警」+ 幽灵持仓盲区(QMT 卖光的票本地账本仍记着,UI 隐身)→ 老师提出优化方向(服务端缓存 + 10s 同步 + 策略持仓改读缓存);
2. **方案反转(落库 → 内存)**AI 初版方案建 positions_cache 表(惯性抄 market_quotes_cache / trade_fills 先例);老师质疑是否可复用 strategy_holdings 或放内存 → AI 论证修正:**判断落库的标准是「数据能否随时一次调用重拿全」**,持仓快照满足,落库反引入「过期快照冒充实时的说谎风险」;strategy_holdings 是账本不可掺对账单(holding_id 是交易归属锚点)→ 定稿纯内存方案;
3. **四问拍板**(老师,2026-09-02):① 内存不落库;② 读穿透兜底(首启/QMT 从未连上时当场拉一次并回填);③ 幽灵持仓同步时自动清仓(加防抖护栏);④ 同步时间前端本轮不显示;
4. **实现**PositionSync(快照 + 定时 + 空快照双确认 + 幽灵清仓防抖 + 账户守卫 + stats)、PositionManager.getAllPositions 读快照 + 读穿透 + 未注入兼容、index.js 装配、回归脚本 34 项;
5. **修复两处**(测试驱动发现):syncNow 的 mounted 守卫拦住了手动同步(测试直接调用返回 null)→ 移除该守卫(mounted 只管定时循环,手动同步/热切换后刷新不受限);账户切换守卫从「重置后当轮继续计数」收紧为「当轮直接跳过」(旧账户快照不可信);
6. **验证**typecheck + build 通过;test-position-sync 34/34test-r013-custom-fields 21/21(未注入 positionSync 旧行为兼容证明);
7. **文档链补全**(先实施后补档):R-014 + PLAN-013 + 迭代三件套 + 技术约束-017 + 数据存储设计.md §11;修正一处归档操作(R-014 未验收先写了 已完成/,按规范移回需求池根目录)。
## 经验教训(复盘沉淀)
### 1. 「抄先例」要过适用性检查
- 行情缓存落库(重启首屏有价)与交易落库(QMT 只有当日数据,不存即丢)各有硬理由;持仓快照两个理由都不占。判断标准沉淀:**外部可重取的实时投影不落库**(落库换不来任何能力,只带来说谎风险);
- 数据该放哪一层,先问三个问题:不可再生吗?重启首屏依赖吗?有读放大或复杂查询需求吗?——全否 → 内存。
### 2. 账本与对账单分离
- strategy_holdings(人为分配、生命周期、holding_id 锚点)与实盘持仓快照(外部投影、易变)是两类数据;复用同一张表会污染交易归属链。合并的诱惑来自「都是持仓」,分辨的依据是「谁写、谁信、活了多久」。
### 3. 幽灵清仓的误清防线 = 防抖 × 身份守卫
- 单纯「消失即清仓」在 QMT 半可用(未登录返回空)和账户热切换两个场景都会误清;连续 3 轮 + accountId 守卫(未知跳过 / 切换重置跳过)把误清窗口压到可忽略;
- 沉淀:**对「删/清」类自动化动作,默认加两道独立护栏再上**。
### 4. mounted 守卫的语义边界
- 定时器服务的 mounted 应只表达「定时循环是否运行」,不应拦截手动触发(syncNow/sync 类方法);否则测试、手动刷新、热切换后的即时同步全被误伤。
### 5. 先实施后补档的可行边界
- 本迭代代码在讨论中当场落地、文档事后补全;补档过程顺畅依赖两点:讨论中老师拍板结论明确(四问有记录)+ 实现严格按结论执行无偏移。若讨论结论含糊,不允许先实施(文档先行约束不破)。
## 遗留/后续
1. **部分减持的自动修正**(未分配负数仍靠 UI 暴露,人工「移出」修正):如需「检测账实不符 → 提示一键按实盘修正」可另立需求;
2. **同步时间前端显示**:老师拍板本轮不做;如持仓滞后感知需要,可加「更新于 HH:mm:ss」小标注(后端 syncedAt 已就绪);
3. **既有观察点未动**(另行登记评估):getStrategyPositions 的 N+1 委托查询(可改 IN 一次查);src/api/trades.js orders 端点绕 DataStore 门面直摸 sqlite.db(层级破洞);calcOrderFees 数据源/前端双实现(可收敛);数据存储设计.md §9.1 库文件名(one-divine-lot.db vs 实际 store.db)等历史偏差。
## 验收状态更新(2026-09-08
> 老师 2026-09-08 归档指令:确认验收并归档;R-014 已按归档清单移入 05-需求池/已完成/。
@@ -0,0 +1,26 @@
# 迭代目标:12-持仓内存快照(服务端 10s 定时同步,请求不再穿透 QMT)
> 迭代编号:12 | 创建:2026-09-02 | 状态:已实施,待验收
> 依据计划:PLAN-013 需求:R-014(已定稿,2026-09-02,老师逐项拍板 4 问)
## 目标描述
消除「QMT 抖动 → 持仓页面白屏」:服务端建全量持仓**内存快照**(不落库,老师拍板),PositionSync 每 10 秒与 QMT 全量同步一次,以快照为持仓数据的唯一读取源;策略持仓 / 全部持仓 / 未分配三个接口不再在请求时穿透 QMT;同步失败保留上次快照;快照为空读穿透兜底;幽灵持仓(QMT 已卖光、本地账本仍记着的条目)在同步循环中自动清仓(带防抖护栏)。
## 目标分解
1. **PositionSync**src/position/PositionSync.js):启动预热一次 + setInterval(10s) 全量拉 /trade/positions → 格式校验 → 整体替换内存快照;失败记 stats 不动内存;空快照双重确认(isAvailable + getAsset accountId);
2. **读路径切换**PositionManager.getAllPositions):有注入 positionSync → 读快照(空则读穿透回填);未注入 → 旧穿透行为(兼容既有构造点,如 test-r013);
3. **幽灵自动清仓**(同步循环内):本地当前持仓 code ∉ QMT 快照,连续 3 轮 → 全部策略 closeHolding 转历史;护栏:accountId 未知当轮跳过、账户切换当轮重置跳过、单码清仓失败保留计数下轮重试;
4. **装配**index.js):start + dispose + 注入 manager/api runtime
5. **回归脚本**scripts/test-position-sync.mjs,纯内存 mock 34 项)+ typecheck + build + 既有回归(r013)。
## 背景事实(本迭代为「先实施后补档」)
- 迭代代码于 2026-09-02 架构梳理讨论中当场实施(老师指令「开工吧」),讨论中老师四问拍板(落库质疑改内存 / 读穿透 / 幽灵自动清仓 / 不显示同步时间);
- 文档链(R-014 / PLAN-013 / 本迭代三件套 / 技术约束-017 / 数据存储设计 §11)事后补全,实现与讨论结论一致。
## 对老师的配合需求
- **人工验收**:真实环境重载插件 → QMT 正常时开策略持仓/全部持仓 tab 数据正常 → 停掉 QMT Bridge 刷新页面(数据应保留上次快照而非白屏)→ 恢复 QMT → 在券商端卖出某只票全部持仓,约 30s 后确认本地持仓行自动转历史;
- 验收通过后:R-014 移入 已完成/ 归档、迭代 12 标记验收通过。
@@ -0,0 +1,22 @@
# 验收标准:12-持仓内存快照
> 迭代编号:12 | 依据:PLAN-013 验收要点 + R-014
## 验收标准线
1. **自动化**typecheck + build 通过;回归脚本 test-position-sync.mjs 全绿(34 项:基本流 / 失败保留快照 / 空快照双重确认四分支 / 幽灵清仓防抖与账户守卫 / 读穿透兜底 / 未注入兼容 / 组装回归 / 定时冒烟);既有回归 test-r013-custom-fields.mjs 全绿(21 项,兼容性证明);
2. **读路径**:插件启动日志出现「PositionSync 启动」;QMT 正常时打开策略持仓 / 全部持仓 tab,数据与改造前一致(行集合 / 份额 / 成本价 / 最后一笔成交价 / 自定义字段值);连续切 tab 不产生对 QMT 的 /trade/positions 新调用(服务端日志无新增穿透请求);
3. **抗抖**:停掉 QMT Bridge 后刷新页面,持仓 tab 仍显示**最后一次快照数据**(不再白屏);恢复 QMT 后 ≤10s 快照自动恢复最新;
4. **幽灵自动清仓**:在券商端卖出某只票全部持仓后,约 30s(3 轮同步)内,该票在全部策略下的当前持仓自动转历史(strategy_holdings 出现 closed_at,不物理删除);服务端日志出现「PositionSync 幽灵清仓」warn
5. **防误清**:QMT 短暂返回空(如未登录)时本地持仓不被清空(保留旧快照);连接热切换到另一账户时旧计数重置,无误清;
6. **零 schema 变更**SQLite 无新表、strategy_holdings 结构不变;前端代码零改动。
## 验收方法
- 自动化项由 AI 执行并出具结果(已完成:34/34 + 21/21 + typecheck + build);
- 2~5 项老师真实环境人工验收:重载插件 → 正常浏览持仓 tab → 停 QMT 验证抗抖 → 恢复 → 券商端清仓一票验证幽灵自动转历史;
- 全部通过后:迭代 12 标记「验收通过」,R-014 归档至 已完成/。
## 验收目标
- 6 条验收线通过,迭代 12 标记「验收通过」,R-014 更新实现状态(已实现)并归档。
@@ -0,0 +1,106 @@
# 技术实现方案:13-盘口内存快照
> 迭代编号:13 依据:PLAN-014 + R-015(老师拍板:方案 A 替换 / WS 移除 / 纯内存 DROP / watch 维持现状 / 指示灯推荐逻辑)
## 1. 数据源扩展(QmtBridgeRestDataSource.getTicks
```js
/** 批量盘口(GET /data/tick?codes=a,b,c)→ { [code]: snapshot }(保留原始字段 + updatedAt */
async getTicks(codes) {
// j.ok && j.data → data 原样返回(tick 结构已有语义字段 lastPrice/lastClose/open/high/low/... + time/timetag
// 每 snapshot 统一附加 updatedAt = Date.now()(服务端收到时刻,价龄判断基准)
}
```
- 首次启用 /data/tick 的适配层通道(此前 MarketDataHub 自己 fetch,绕过了适配层——本轮修正层级);
- 涨停跌停走既有 getInstrument(code)(首次投入使用):upStopPrice/downStopPrice/preClose。
## 2. QuoteHubsrc/market/QuoteHub.js,新)
```js
class QuoteHub {
quotes = new Map(); // code → snapshot(全量 tick 字段 + updatedAt;只读约定)
instruments = new Map(); // code → { upStopPrice, downStopPrice, preClose, tradingDay }(交易日内存缓存)
watchCodes = new Set(); // 关注集合(维持现状只进不出,无上限——老师拍板)
stats = { syncCount, failCount, lastError, lastSyncedAt, restCount, watchSize };
}
```
- `getQuotes(codes)`:内存命中 + miss 读穿透(dataSource.getTicks 补拉并回填,等价旧三级命中的内存→REST 两级,磁盘层删除);
- `getQuote(code)`:单码便捷;`watch(codes)`:关注集合登记(QuoteSync prime/刷新、api 查询共用);
- `ingest(data, { source })`:写入统一入口(source 标签:rest|instrument——WS 将来回归时加 source 即可,入口不变);
- 合约信息:`getInstrumentCached(code, tradingDay)`——QuoteSync 判定交易日变化后失效重拉(涨停跌停当天不变)。
## 3. QuoteSyncsrc/market/QuoteSync.js,新;与 PositionSync 同构)
```
start(): prime(持仓 code 盘口,走 hub.watch + getTicks 批量)+ setInterval(5s)
每轮:codes = [...hub.watchCodes](维持现状)
分批 50 → dataSource.getTicks(batch) → hub.ingest(data, {source:'rest'})
watch 中无合约缓存或 tradingDay 变化的 code → getInstrument 懒拉(失败不阻塞本轮)
失败:stats.failCount++ / lastError**内存不动(保留上次价)**
setBaseUrl(url):热切换 → 清空 instruments 缓存(新账户/新市场环境)+ 立即 prime 一次
stop(): 清定时器;内存保留
getSyncedAt() / getStatus(): 指示灯数据源
```
- 对称性:PositionSync10s/持仓)/ QuoteSync5s/盘口)/ TradeSync(60s/交易)三域同构,全部「失败保留、stats 可观测、手动 syncNow 可触发」。
## 4. 存储清理(market_quotes_cache 退役,老师拍板 DROP
- SqliteStoreSCHEMA_SQL 删建表;新增 `DROP TABLE IF EXISTS market_quotes_cache`(init 内幂等执行,存量库清理);删 getMarketQuote/getMarketQuotes/setMarketQuotes/_mapQuotemigrateJson 删行情迁移段;isEmpty 只看 strategy_holdings
- DataStore:删 loadMarketreturn this 残迹,warmup bug 根源)/getMarketQuote/getMarketQuotes/setMarketQuotes/marketLoaded
- 存量 store.market.json 的 .bak 不动(历史备份无碍)。
## 5. APIsrc/api/market.js + 新 sync-status
```
market-snapshot { codes } → { [code]: { lastPrice, lastClose, upStopPrice, downStopPrice, updatedAt, ...tick 字段 } }
sync-status → {
position: { syncedAt, ageMs, fresh: bool, snapshotSize, failCount, lastError, periodMs },
quote: { syncedAt, ageMs, fresh: bool, snapshotSize, watchSize, failCount, lastError, periodMs },
qmt: { ...qmtHealthMonitor.getStatus() }
}
sync-now { domain: 'position' | 'quote' } → 各 Sync.syncNow()(手动触发,指示灯点击用)
market-stats 端点删除(并入 sync-status
```
- 状态判定:fresh = syncedAt 在 3×周期内(持仓 30s / 盘口 15s);前端按 ageMs 二次校准显示。
## 6. 前端(SyncIndicators.jsx 新 + 两处清理)
- **SyncIndicators**(挂 QmtConnectionChip 内 QmtHealthIndicator 右侧):两圆点「持」「行」;
- 10s 轮询 sync-status;点击圆点 → sync-now({domain}) → 刷新;
- 颜色:green(fresh) / yellow(stale) / gray(never)title 含同步时间、快照量、失败数、lastError;
- 样式复用 QmtHealthIndicator 圆点(10px 圆 + glow),色值走 --dsw-* token
- MarketDataProvider:删 `wsInfo: null` 残留;轮询/注册逻辑不变(getByCodes 签名兼容);
- QmtConnectionChip:挂载 SyncIndicators(健康灯右侧)。
## 7. 装配(src/index.js
```js
const marketHub = new QuoteHub({ logger });
const marketFeed = new QuoteSync({ hub: marketHub, runtime: { settings, dataSource }, logger });
marketFeed.start(startup.baseUrl ?? config?.qmtBaseUrl);
// disposemarketFeed.stop()(保留旧变量名,改动最小;注释标注新职责)
```
- registerApi 入参 marketHub/marketFeed 对象形态不变(api/market.js 内部改用新方法);
- QmtHealthMonitor 不动。
## 8. 回归脚本(scripts/test-quote-sync.mjs,纯内存 mock
- mock:可编程 dataSourceticks/instrument/positions 可变状态);
- 用例:prime + 5s 刷新基本流 / 失败保留上次价 / watch 空不空转 / 读穿透回填 / 涨停跌停按交易日缓存与日切失效 / 热切换清合约缓存 / sync-status 状态推导(绿黄灰边界)/ 存储清理(DROP 幂等:SqliteStore 临时目录建旧结构库 → init 后表消失且持仓数据无损)。
## 9. 策略持仓表行情列(老师确认并入本轮)
- **settings.js**COLUMN_META 追加 4 项(均 `defaultVisible: false`)——`{ key:'upStopPrice', label:'涨停价' } / { key:'downStopPrice', label:'跌停价' } / { key:'open', label:'今开' } / { key:'high', label:'最高' }`normalizeStrategyColumns 两处适配:默认列构造带 defaultVisible,未覆盖列的缺省显隐从恒 true 改为 `defaultVisible !== false`(存量 strategyColumns 无需迁移——新列缺省即隐藏);
- **StrategyTab.renderDataCell** switch 增 4 case`getPrice(p.code)?.upStopPrice / downStopPrice / open / high`,复用 fmtPrice 纯价格展示(数据缺失显示 —,与现价列同行为);
- 列设置弹层零改动(自动从 COLUMN_META 出现在列表,kind=base 标「数据」);
- 取值来源:market-snapshot 下发的 tick 字段 + 合约信息(upStopPrice/downStopPrice 来自 QuoteSync 交易日缓存),经 MarketDataProvider prices map join 到行。
## 10. 验证
- typecheck + buildtest-quote-sync 全绿;存量回归(test-position-sync 34 项 + test-r013 21 项)全绿;
- 老师人工验收(见验收标准)。
@@ -0,0 +1,43 @@
# 迭代复盘:13-盘口内存快照(QuoteSync/QuoteHub + 同步指示灯)
> 复盘日期:2026-09-02 | 迭代状态:**已实施,待老师人工验收**
> 关联需求:R-015 关联计划:PLAN-014
## 结果
迭代 13 达成:盘口数据与持仓对称收敛——QuoteSync(取数:prime + 5s REST + 涨停跌停按交易日缓存)/ QuoteHub(存查:内存快照 + watch 集合 + 读穿透 + getQuote 单一入口)替换 MarketFeed/MarketDataHub(方案 A 删除重写);WS 通路移除;market_quotes_cache 表 DROP 退役(含 loadMarket 残迹清理,warmup bug 随之消灭);新增涨停/跌停字段(/data/instrument 首次启用);会话头部新增持仓/盘口同步指示灯(绿黄灰 + 点击即同步 + sync-status 端点吸收 market-stats)。
## 过程事实
1. **讨论驱动逐项拍板**2026-09-02,五项):① 替换 vs 包壳 → 方案 A 替换(DataStore.loadMarket 残迹为前车之鉴);② WS 移除(无生效结论 + 全市场推送被过滤 + REST 5s 已覆盖;ingest 留来源标签口子);③ **纯内存不落库**(老师纠正 AI 的「表加两列」方案:缓存表整个退役,价格单一入口 = QuoteHub;AI 复以落库三问复核通过——warmup bug 复现证明落库从未真正生效);④ watchCodes 维持现状不收缩不加限(WS 移除后膨胀仅轻微浪费,切回旧 tab 价格秒显是免费福利);⑤ 指示灯(老师提出):绿黄灰三态不引入红(红留 QMT 健康灯表达连接层)、点击即 syncNow、**PriceCell 灰点方案取消**(管道健康全局灯表达,个股停牌属数据语义另议);
2. **warmup bug 实锤**:讨论中复现 DataStore.loadMarket() 返回 DataStore 实例自身 → Object.entries 枚举出 sqlite/loaded/marketLoaded 三个内部属性 → 「重启首屏有价」自上线起从未生效,一直靠 getByCodes 磁盘兜底路径撑着——成为「不落库」决策的关键证据;
3. **实现**:getTicks 适配层通道(修正旧 hub 绕过适配层自 fetch 的层级破洞)、QuoteSync/QuoteHub、SqliteStore DROP + 行情方法删除、DataStore 行情方法删除、market-snapshot 扩展字段、sync-status/sync-now 端点、SyncIndicators 组件、wsInfo 清理;
4. **验证**typecheck + buildtest-quote-sync 全绿;存量回归 test-position-sync 34/34、test-r013 21/21
5. **文档链**R-015 + PLAN-014 + 迭代三件套 + 技术约束-018 + 产品约束-012 + 技术约束-010/012 变更(本轮讨论后统一落)。
## 经验教训(复盘沉淀)
### 1. 「缓存落库」的隐性成本会以 bug 形式讨债
- market_quotes_cache 三件套(loadMarket 残迹 / warmup 假工作 / isEmpty 与 migrateJson 的行情耦合)活了三代迭代才被 root cause——落库换来的「重启首屏有价」实际从未工作,而读穿透 1 秒内就能补齐;
- 沉淀:**给「可再生缓存」落库前,先验证它的读取路径真的被走到**(监控 stats.diskHitCount 之类),否则就是无人受益的死重 + 未来 bug 的温床。
### 2. 同域两个类(Feed/Hub)的「分工」挡不住职责漂移
- MarketDataHub 本该只管存查,却自己 fetch REST(绕适配层)、管 watch 过滤、管落库防抖——「取」与「存」的边界在实践中糊掉;
- 沉淀:类职责用「它消费谁、被谁消费」检查:QuoteHub 只消费 QuoteSync/dataSource 补拉,只被 api/前端消费;出现第三条边即重构信号。
### 3. 全局状态灯 > 局部灰点
- 老师把 PriceCell 灰点讨论升级为「同步指示灯」,一并覆盖持仓+盘口两域——数据管道健康是全局属性,表达在全局位置(会话头部)比逐格标记更正确,且复用既有健康灯心智;
- 沉淀:故障可视化先问「这是单个数据的错,还是管道的错」——后者永远放全局。
### 4. 交易日是行情静态数据的正确缓存键
- 涨停/跌停/昨收「当天不变」的知识决定缓存粒度:按交易日缓存 + 日切失效,而不是跟 5s 同步周期走(省 99% 的合约请求);
- 沉淀:为同步数据分类「每周期变(tick)/每日变(合约静态)/每次变(手动归属)」,各自匹配刷新节奏。
## 遗留/后续
1. **个股停牌标识**:QMT 正常但单票不出 tick 时价格陈旧且无提示——属数据语义,老师确认另议;
2. **WS 回归路径**ingest 已留 source 标签;T-005 草稿维持起草,需要亚秒级行情时再议订阅契约;
3. **指示灯扩展位**sync-status 已按域结构化(position/quote/qmt),未来交易域(TradeSync 60s)可加第三盏灯,前端加一行渲染;
4. **前端 getQuote 消费扩展**:涨停/跌停已随 market-snapshot 下发,UI 尚无展示列(如需「接近涨停提示」另立需求)。
## 验收状态更新(2026-09-08
> 老师 2026-09-08 归档指令:确认验收并归档;R-015 已按归档清单移入 05-需求池/已完成/。
@@ -0,0 +1,29 @@
# 迭代目标:13-盘口内存快照(QuoteSync/QuoteHub + 同步指示灯)
> 迭代编号:13 | 创建:2026-09-02 | 状态:已实施,待验收
> 依据计划:PLAN-014 需求:R-015(已定稿,2026-09-02
## 目标描述
把盘口(市场行情)数据收敛为与持仓(迭代 12)对称的「定时同步 + 内存快照」模式:新建 QuoteSync(取数)与 QuoteHub(存查)两个工具类,`market_quotes_cache` 表退役(DROP),价格单一入口 = QuoteHub;扩展涨停价/跌停价(/data/instrument 按交易日缓存);会话头部新增「持仓/盘口」同步指示灯(绿黄灰,点击即同步)。WS 数据通路本轮移除(从无生效结论,老师拍板)。
## 目标分解
1. **QuoteSync**src/market/QuoteSync.js):启动 prime(持仓盘口)+ 5s 定时刷 watch 集合(分批 50)+ 涨停跌停按交易日懒拉缓存(watch 新 code → /data/instrument,日切失效)+ 热切换重置 + stats;失败保留内存不动;WS 不迁移;
2. **QuoteHub**src/market/QuoteHub.js):内存快照 Map + watchCodes Set(维持现状只进不出)+ 合约信息缓存 + 读穿透(走 dataSource.getTicks,不再自 fetch)+ 防抖 3s 写盘逻辑删除(无落库)+ 对外 getQuote/getQuotes/getByCodes/watch/stats
3. **数据源**QmtBridgeRestDataSource 新增 getTicks(codes)/data/tick 批量语义化,带 updatedAt/time 原始时间);
4. **存储清理**SqliteStore 删行情表方法 + `DROP TABLE IF EXISTS market_quotes_cache`(幂等)+ migrateJson 行情段/isEmpty 联动收缩;DataStore 删 loadMarket/getMarketQuote(s)/setMarketQuotes
5. **API**market-snapshot 值对象扩展 {lastPrice,lastClose,upStopPrice,downStopPrice,updatedAt};新增 sync-status(持仓/盘口/QMT 三域状态,吸收 market-stats+ sync-now {domain: position|quote}
6. **前端**SyncIndicators 组件(QmtHealthIndicator 旁两圆点:持●行●,10s 轮询 sync-status,绿黄灰 + 悬停详情 + 点击调 sync-now);MarketDataProvider 删 wsInfo 残留;QmtConnectionChip 挂载;
7. **回归**scripts/test-quote-sync.mjs(纯内存 mock+ typecheck + build + 存量回归;
8. **策略持仓表行情列**(老师确认并入):列设置新增 涨停价/跌停价/今开/最高 4 列(COLUMN_META 登记,kind=basedefaultVisible: false 默认隐藏);renderDataCell 取值 getPrice(code)?.{upStopPrice|downStopPrice|open|high};纯价格展示(无距离百分比);归一化缺省显隐跟随 defaultVisible
## 指示灯规格(老师采纳 AI 推荐逻辑)
- 阈值:持仓 30s 内绿 / 超过黄、从未灰(周期 10s);盘口 15s 内绿 / 超过黄、从未灰(周期 5s)——**绿黄灰三态,不引入红**(红留给 QMT 健康灯表达连接层故障,同步灯表达数据层陈旧);
- 悬停 title:状态 + 最近同步时间 + 快照量 + 失败次数 + lastError 摘要;
- 点击:立即触发该域 syncNow(不等下个周期),灯自动刷新。
## 对老师的配合需求
- 人工验收(真实环境):重载插件 → 指示灯绿、悬停信息正确 → 停 QMT 后两灯渐黄且页面价格保持旧值 → 恢复 QMT ≤5s 回绿 → 点击指示灯立即同步 → 涨停/跌停列数据正确(对比券商软件)。
@@ -0,0 +1,23 @@
# 验收标准:13-盘口内存快照
> 迭代编号:13 | 依据:PLAN-014 验收要点 + R-015
## 验收标准线
1. **自动化**typecheck + build 通过;test-quote-sync.mjs 全绿(基本流/失败保留/读穿透/交易日缓存/热切换/状态推导/DROP 幂等);存量回归 test-position-sync 34/34、test-r013 21/21
2. **存储退役**:插件启动后存量库中 market_quotes_cache 表被 DROP(幂等,重启不报错);持仓数据不受影响;代码中无 DataStore.loadMarket / getMarketQuote(s) / setMarketQuotes 残留;MarketFeed/MarketDataHub 文件已删除;
3. **价格功能**:重载插件后持仓/策略 tab 现价、涨幅正常(≤5s 更新);**涨停/跌停数据**在 market-snapshot 返回中正确(与券商软件对照);QMT 挂掉时页面价格保持旧值(不空白),恢复后 ≤5s 自动追上;
3b. **行情列**:策略持仓 tab 列设置出现 涨停价/跌停价/今开/最高 4 个新列(默认隐藏,勾选后显示且顺序可调),纯价格展示,无数据的票显示 —;
4. **指示灯**:会话头部出现「持」「行」两圆点——正常绿(悬停:同步时间/快照量/失败数);停 QMT 后持仓灯 30s 内、盘口灯 15s 内变黄(数据仍显示旧值);恢复后回绿;**点击圆点立即触发对应域同步**;
5. **WS 清理**:日志无「MarketFeed 连接 ws://」;market-stats 端点不再存在(sync-status 替代);前端 wsInfo 残留已删;
6. **零 schema 破坏**strategy_holdings / trade_orders / trade_fills 三表结构与数据完好。
## 验收方法
- 自动化项 AI 执行出具结果;
- 2~5 老师真实环境人工验收:重载插件 → 查看指示灯与悬停信息 → 对照券商涨停跌停价 → 停/启 QMT Bridge 验证抗抖与回绿 → 点击指示灯验证即时同步;
- 全部通过后:迭代 13 标记「验收通过」,R-015 归档 已完成/。
## 验收目标
- 6 条验收线通过,迭代 13 标记「验收通过」,R-015 更新实现状态(已实现)并归档。
@@ -0,0 +1,79 @@
# 技术实现方案:14-策略tab历史持仓展示
> 迭代编号:14 依据:PLAN-015 + R-016(老师拍板:Q2 显示 0 / 其余按建议)
## 1. 存储层(SqliteStore.getHoldingsHistory
```js
/** 某策略已清仓持仓(closed_at 非空),closed_at ≥ sinceMs,按 closed_at DESC */
getHoldingsHistory(strategyId, { sinceMs } = {}) {
// SELECT holding_id, strategy_id, code, shares, created_at, closed_at, "values"
// FROM strategy_holdings
// WHERE strategy_id=? AND closed_at IS NOT NULL [AND closed_at >= ?]
// ORDER BY closed_at DESC
// 返回 _mapHolding 同构(holdingId/strategyId/code/shares/createdAt/closedAt/values
}
```
- 只读新增;**零写路径变更**closeHolding 维持 shares=0 + closed_atQ2 定稿);
- sinceMs 缺省 = 不过滤(内部能力预留;API 层始终传值);
- 与 getHoldingHistory(code) 并存:后者是全表按 code 查(R-008 遗留),不满足本需求维度,不复用不改动。
## 2. 门面层(DataStore
```js
async getHoldingsHistory(strategyId, opts) { await this._ensure(); return this.sqlite.getHoldingsHistory(strategyId, opts); }
```
## 3. APIsrc/api/strategies.js 新 method
- method`strategy-holdings/history`
- args`{ strategyId: string, range: 'week'|'month'|'quarter'|'halfYear'|'year' }`
- range → sinceMs 换算(服务端):week=7d / month=30d / quarter=90d / halfYear=182d / year=365d(自然日近似,回溯窗口展示用途足够,精确日历边界无业务含义);
- 缺 strategyId 或 range 非法 → 参数错误(code: 'bad-request');
- 返回行附加 name 兜底:按 holdingId 查 trade_orders 取最新一笔的 nameSQL 单查,无则 null)——前端直接展示,免二次请求。
## 4. 前端(StrategyTab.jsx
- 状态:`showHistory`(默认 false)、`historyRange`(默认 'week')、`historyRows``historyLoading`
- 头部按钮区:「历史持仓」toggle 按钮 + 开启时显示范围下拉(近一周/近1个月/近3个月/近半年/近1年);
- 数据流:开启或切范围 → call('one-divine-lot/strategy-holdings/history', { strategyId, range });缓存按 range 键保留,关闭仅隐藏;
- 渲染:rowsForRender = 当前持仓行 +showHistory ? 历史行 : []);历史行标识 `isHistory: true`
- 行样式:opacity/灰色调(--dsw-alias-label-tertiary+ 徽标「已清仓 YYYY-MM-DD」;
- 单元格:代码/名称/清仓时间(YYYY-MM-DD HH:mm/持有天数(ceil((closedAt-createdAt)/86400000),同日=1);shares 列显示 DB 原值(=0Q2);行情类列(lastPrice/pctChange/lastClose/avgPrice/lastTradePrice/upStop/downStop/open/high)一律 —(不注册行情、不取价);自定义字段列:p.values 只读展示,禁点击编辑(isHistory 不进入 FieldCellEditor 分支);
- 展开:复用 toggleExpand(code, holdingId) → trades/by-holding(历史行同样有效);
- 排序:当前行维持现状(涨幅排序仅作用于当前行集合);历史行固定清仓时间 DESC 追加在后;
- 计数:标题(N 只)= 当前持仓数,不含历史行。
## 5. 回归脚本(scripts/test-r016-history.mjs
- ODL_TEST_DATA_DIR 临时目录(技术约束-011,不碰真实数据);
- 覆盖:建仓→清仓→history 可查 / 范围过滤边界(7d/30d…)/ 排序 / 策略隔离 / 未清仓行不出现 / API 参数校验 / name 兜底。
## 6. 验证链
typecheck → build → test-r016 → 存量回归(test-position-sync 34 / test-r013 21 / test-quote-sync)→ 人工验收清单。
## 7. 二轮补充实现:今日已清仓默认层(2026-09-07 老师拍板 Q8-Q9
> 在首轮(§1-6)基础上追加;首轮实现保持可追溯,新增改动如下。
### 7.1 服务端(src/api/strategies.js
- range 新增 today**特判本地自然日零点**localDayStartMssetHours(0,0,0,0)),sinceMs = 本地今天 00:00;不落入 HISTORY_RANGE_MS 回溯毫秒档(那 5 档语义 = now − 范围,today 语义 = 自然日边界,两类不可混);
- 参数校验错误信息扩为 today|week|month|quarter|halfYear|year;其余逻辑(strategy_id 过滤 + name 兜底 + DESC 排序)与 5 档完全复用——**SqliteStore/DataStore 零改动**sinceMs 本就通用)。
### 7.2 前端(src/client/views/StrategyTab.jsx
- 状态:historyRows 缓存键新增 today(与 5 档并存互不干扰);
- **数据流**loadHistory 定义提前到 load 之前;load() 的 finally 里调 loadHistory('today')——进 tab 即拉 + 每次持仓重载(移出/加仓/编辑后 load())都刷新今日已清仓层,当天清仓转历史后 tab 内不因此少行;
- **渲染合成(rowsToRender**分两层:
- L1 = histRowsFor('today') —— **恒显示**showHistory 关也显示);当前持仓行后追加,清仓时间 DESC;
- L2 = showHistory 开时追加 histRowsFor(historyRange)**按 holdingId 去重**(剔除已含于 L1 的行,避免今天清仓 + 开启近一周出现两行);
- 交互文案:开关 title 更新为「显示/隐藏今天之前已清仓的历史持仓(当天已清仓的持仓始终显示)」;文件头注释同步;
- 标题计数(N 只)仍只算当前持仓,两层已清仓行不计入。
### 7.3 回归(scripts/test-r016-history.mjs
- 新增 [4b] range=today:昨天 23:59:59 清仓不出现 / 今天 00:00:01 清仓出现 / 不含 40 天前行 / today 行 name 兜底同样生效 / range 非法校验不受 today 引入影响;
- 验证链复跑:test-r016 24/24 + 存量回归全绿(见迭代复盘)。
@@ -0,0 +1,40 @@
# 迭代复盘:14-策略tab历史持仓展示
> 复盘日期:2026-09-07 | 迭代状态:**已实施,待老师人工验收**
> 关联需求:R-016 关联计划:PLAN-015
## 结果
迭代 14 达成:两个策略 tab(做T / 网格超市)title 旁新增「历史持仓」开关 + 清仓时间范围筛选(近一周默认 / 近 1 个月 / 近 3 个月 / 近半年 / 近 1 年);已清仓持仓行灰显 + 「已清仓」徽标(含清仓日期)追加在当前持仓行后(closed_at DESC),展开复用 R-010 查看关联交易——补上「清仓即失联」的操作追溯缺口。当前持仓快照路径(R-014 语义)与所有写路径(closeHolding 置 shares=0Q2 拍板)零改动。
## 过程事实
1. **讨论驱动定稿**2026-09-07Q1-Q7 老师逐项拍板):Q2 为本轮唯一改判——AI 建议改 closeHolding 保留清仓时份额,老师否决:「显示 0 就好了,即使显示了清仓时的 shares 也没多少意义,主要目标是追溯这次持仓相关的历史操作」——需求核心从「数据完整性」校正为「操作追溯锚点」,方案随之收敛为零写路径变更;其余六问均按 AI 建议定稿;
2. **实现**(严格按 PLAN-015 步骤):SqliteStore.getHoldingsHistorystrategy_id + closed_at IS NOT NULL + sinceMs 过滤 + DESC,只读新增)→ DataStore 门面透传 → strategy-holdings/history 端点(range 五档服务端换算自然日近似 + name 关联委托兜底 + 参数校验 bad-request)→ StrategyTab 开关/范围下拉/历史行渲染;
3. **实现中发现并修复一个真实隐患**:R-010 展开状态与交易缓存原以 code 为键——同码「当前行 + 历史行」并存(清仓后重新买入)时两行会同时展开、缓存串行;本轮把展开状态/缓存/行键统一收敛为唯一 rowKey(当前行 = code,历史行 = 'h-' + holdingId),toggleExpand 签名同步变更(当前调用方仅 tbody 一处);
4. **验证**test-r016-history.mjs 19/19(清仓转历史可查 / 5 档范围边界 / 排序 / 策略隔离 / 未清仓不出现 / 参数校验 / name 兜底 / 端点分发零破坏);typecheck 通过;存量回归 test-position-sync 35/35、test-r013 21/21、test-quote-sync 27/27build 通过(client bundle 164KB wrapped);
5. **文档链**R-016 定稿 + PLAN-015 + 迭代三件套 + 技术约束-019 + 需求池索引(R-016 登记;顺带补登遗漏的 R-015 行);
6. **部署态排查(老师首验无数据,2026-09-07)**:实锤定位 = **运行中的 DSH 服务端进程仍是旧代码**——同进程 /odl/api/summary 正常应答、新端点返回 unknown methodcurl 实测);客户端 bundle 因插件 symlink 直连源码 lib/ 而被刷新(按钮已出现),服务端模块注册表却停留在进程启动时;处置 = ① loadHistory 失败从静默吞空改为 Toast 可见化(静默兜底=排障盲区,本轮教训)并重新构建(test-r016 19/19 复跑通过);② 服务端生效依赖 DSH web 进程重启(插件 symlink 方式安装,无需重装,重启即取新 lib)。
## 经验教训(复盘沉淀)
### 1. 需求的「核心目标」要听老师的定调,不要替老师拔高
- AI 在 Q2 提议改 closeHolding 语义以「保留清仓时份额」,出发点是账本完整性;老师一句话把需求核心定调为「追溯历史操作」——份额数字本身没有追溯价值,holding_id 锚点 + 关联委托才有;
- 沉淀:讨论中 AI 的方案建议要标明「我认定的价值假设」,让老师有机会否定假设本身,而不只是选项。
### 2. 复用既有交互时,键的唯一性要先审后用
- 展开状态按 code 键控在「一码一行」时代是隐含正确的;历史行引入打破了「一码一行」不变量——同类隐含假设(缓存 Map、React key、loading 标记)都要在数据形态变化时重新过一遍;
- 沉淀:向既有列表引入第二类行时,先列出所有「按 X 键控」的状态,逐个判断是否需要升级为行唯一键。
### 3. 只读需求的实现应当「零写路径」可断言
- 本轮全程未触碰任何 INSERT/UPDATE 语句与语义(closeHolding 分毫未动),回归脚本专门加了「strategy-positions 分发不变」断言——历史查询与当前查询的隔离在端点层就已成立(独立 method),不需要靠约定;
- 沉淀:「只读功能」的范围声明落成验收断言(分发不变 + 写路径 diff 为空),比口头承诺可靠。
## 遗留/后续
1. **归属候选不纳入清仓持仓**R-016 关联观察,2026-09-07 大连热电实际遇到):清仓后当日委托无法再通过下拉关联到对应 holding——是否立独立需求(候选纳入近期清仓持仓)待老师定;
2. **历史行成本价/收益推导**:本期不做(边界定稿);若后续要做,数据源 = 关联委托成交记录,属展示层推导,不动存储;
3. **清仓方式标记**(手动 vs 幽灵自动):数据未区分,本期不做;若要做需 closeHolding 增加来源标记(涉及写路径,另立需求);
4. **R-015 / 迭代 13 人工验收**仍待老师确认(与本迭代无依赖)。
## 验收状态更新(2026-09-08
> 老师 2026-09-08 归档指令:确认验收并归档;R-016 已按归档清单移入 05-需求池/已完成/。
@@ -0,0 +1,29 @@
# 迭代目标:14-策略tab历史持仓展示
> 迭代编号:14 | 创建:2026-09-07 | 状态:已实施,待验收
> 依据计划:PLAN-015 需求:R-016(已定稿,2026-09-07Q1-Q7 老师拍板)
## 目标描述
两个策略 tab(做T / 网格超市)title 旁添加「历史持仓」功能:① 显示/隐藏已清仓历史持仓的开关;② 显示历史时默认展示近一周清仓的持仓,并提供 近 1 个月 / 近 3 个月 / 近半年 / 近 1 年 范围选项。历史持仓行复用 R-010 行展开查看关联交易记录——补上「清仓即失联」的操作追溯缺口(核心目标:追溯该持仓相关的历史操作,老师 2026-09-07 定调)。
## 目标分解
1. **存储查询**SqliteStore.getHoldingsHistory):closed_at 非空 + closed_at ≥ sinceMs + strategy_id 过滤,按 closed_at DESC;只读新增,不动任何写路径(closeHolding 置 shares=0 语义维持,Q2 老师拍板「显示 0 就好」);
2. **门面透传**DataStore.getHoldingsHistory);
3. **API 端点**strategy-holdings/history):strategyId 必填 + range ∈ week|month|quarter|halfYear|year(服务端换算 sinceMs;week 为前端默认但服务端不设隐式默认,参数缺失报错);
4. **前端**StrategyTab):头部按钮区加「历史持仓」开关 + 范围下拉(开启时显示,默认 week);懒加载 + 范围切换重拉 + 关闭隐藏(缓存保留);历史行追加在当前持仓行后(清仓时间降序)、灰显 + 「已清仓」徽标、标题计数不含历史行;历史行字段 = 代码/名称(关联委托 name 兜底)/份额(0)/清仓时间/持有天数,行情列 —,自定义字段列只读;行展开复用 R-010;
5. **回归**scripts/test-r016-history.mjsODL_TEST_DATA_DIR 临时目录)+ typecheck + build + 存量回归三脚本。
## 二轮补充(2026-09-07 验收前追加:今日已清仓默认层,Q8-Q9 老师拍板)
- **补充诉求**:当天(本地自然日 00:00 起)已清仓的持仓**默认显示**;「历史持仓」开关只控制**今天之前**的历史——历史持仓关 ≠ 当天已清仓也被藏起(首轮「进 tab 默认只看当前」使当天清仓行当场不可见,补上该缺口);
- **实现增量**:① 服务端 range='today' 特判本地自然日零点(不落回溯毫秒档);② 前端 rowsToRender 分两层——L1 今日已清仓恒显示(缓存键 'today',随每次持仓重载刷新)+ L2 历史范围层受开关控制,层间按 holdingId 去重;③ 视觉/计数维持首轮(灰显+徽标、计数不含已清仓行);
- 影响面:只动只读查询 + 前端渲染合成;**零写路径变更**(closeHolding 语义仍不动);
- 回归:test-r016 追加 range=today 自然日边界用例(24/24)。
## 对老师/主理人的配合需求
- 实现完成后需老师真实环境人工验收(开关/范围切换/历史行展示/展开复盘/与真实清仓数据对照);
- 二轮补充后重点验收:**「历史持仓」关时当天清仓的持仓仍显示**、「历史持仓」开时今天之前的行按范围显示、层间无重复行;
- 验收通过后本迭代标记「验收通过」,R-016 进入归档流程。
@@ -0,0 +1,38 @@
# 验收标准:14-策略tab历史持仓展示
> 迭代编号:14 | 依据:PLAN-015 验收要点 + R-016
## 验收标准线
1. **自动化**typecheck + build 通过;test-r016-history.mjs 全绿(清仓转历史可查 / 5 档范围过滤与边界 / closed_at DESC 排序 / 策略隔离 / 未清仓行不出现 / 参数校验 / name 兜底);存量回归 test-position-sync、test-r013、test-quote-sync 全绿;
2. **入口与默认态**:做T / 网格超市两个 tab title 旁出现「历史持仓」开关;默认关闭 = 与现状完全一致(进 tab 无历史行);开启后默认范围 = 近一周;**全部持仓 tab 无此功能**;
3. **范围筛选**:5 档(近一周/近1个月/近3个月/近半年/近1年)切换即重拉;用 2026-09-07 大连热电真实数据验证——holding_id=15(做T,当日 09:32:35 幽灵清仓)在近一周内可见、切近 1 个月仍可见;
4. **历史行展示**:灰显 + 「已清仓 YYYY-MM-DD」徽标;清仓时间/持有天数正确;份额显示 0(Q2);行情列(现价/涨幅/涨停跌停/今开/最高/成本/最后成交价)显示 —;名称有则显示、无则 —;标题计数(N 只)不含历史行;当前持仓在前、历史行按清仓时间降序在后;
5. **展开复盘**:历史行展开可见该 holding 关联委托(大连热电 09:31:26 卖出 1000 股 @7.42 应出现);自定义字段历史行只读(无编辑入口);
6. **零破坏**strategy-positions / unallocated / summary 等现有端点行为不变;当前持仓行展示与现状一致;无任何写路径变更(closeHolding 语义不变)。
## 验收方法
- 自动化项 AI 执行出具结果;
- 2~5 老师真实环境人工验收:重载插件 → 两个策略 tab 开关历史持仓 → 切换范围 → 对照大连热电当日清仓数据 → 展开历史行核对关联委托;
- 全部通过后:迭代 14 标记「验收通过」,R-016 归档 已完成/。
## 验收目标
- 6 条验收线通过,迭代 14 标记「验收通过」,R-016 更新实现状态(已实现)并归档。
## 二轮补充验收线(2026-09-07 Q8-Q9 追加:今日已清仓默认层)
7. **今日已清仓默认显示(核心)**:「历史持仓」**关**时,策略 tab 仍显示 当前持仓 + 今天(本地自然日 00:00 起)已清仓的持仓行;当天卖出/幽灵清仓的持仓行不因开关关闭而消失;
8. **开关只控更早历史**:「历史持仓」开时,追加显示今天之前各范围(近一周/近1个月/近3个月/近半年/近1年)已清仓行——近一周范围**不含**今天已显示的行(层间按 holdingId 去重,无重复行);
9. **边界与展示**:昨天 23:59:59 清仓的行在开关关时不可见(属更早历史);今日行沿用灰显 + 已清仓徽标;标题计数(N 只)仍不含已清仓行;行情列 — / 份额 0 / 只读字段 / 行展开复用 R-010 均维持首轮;
10. **自动化**test-r016 新增 range=today 用例全绿(24/24);typecheck + build + 存量回归(test-position-sync / test-r013 / test-quote-sync)全绿。
## 验收方法(二轮补充)
- 自动化项 AI 执行出具结果(已执行:test-r016 24/24、position-sync 35、r013 21、quote-sync 27、typecheck 0、r017 12);
- 老师真实环境人工验收:刷新页面 → 在不开启「历史持仓」的情况下核对当天已清仓持仓仍显示 → 开启开关核对今天之前的行按范围出现且与今日行无重复 → 对照大连热电当日清仓数据。
## 验收目标
- 首轮 6 条 + 二轮 4 条验收线通过后,迭代 14 标记「验收通过」,R-016 归档 已完成/。
@@ -0,0 +1,29 @@
# 技术实现方案:15-归属候选纳入清仓持仓
> 迭代编号:15 依据:PLAN-016 + R-017
## 1. 存储层(getAttributionCandidates 扩展,门面透传不变)
```sql
SELECT holding_id, strategy_id, shares, closed_at FROM strategy_holdings
WHERE code=? AND (closed_at IS NULL OR closed_at >= ?) -- ? = Date.now() - 7d
ORDER BY (closed_at IS NULL) DESC, shares DESC, closed_at DESC
```
- 返回行新增 closed: boolean / closedAt: number|null(消费方:api strategies.js 同方法直接受益——注意 attribution-candidates 端点在 trades.js,调 DataStore.getTradeAttributionCandidates 透传,零改动);
- 窗口常量 7 * 86400000(与 R-016 默认范围一致)。
## 2. UITradeRecordsTab.jsx AttributionSelect 重构)
- 锚点键:currentKey = order.holdingId != null ? String(order.holdingId) : ''holdingId 是 R-009 定稿的归属锚点;strategyId 在同策略多轮持仓时会撞值);
- handleSelect:按 String(holdingId) 找候选 → setAttribution(orderId, code, cand.strategyId, cand.holdingId)__unassigned__ → 双 null
- 占位选项:已归属但候选缺失(清仓超 7 天)→ 「当前归属({strategyId})」,value=currentKey,防 select 悬空显示第一项误导;
- 清仓候选文本后缀「(已清仓 MM-DD)」(fmtClosed 本地 helper);select maxWidth 120→150。
## 3. 数据修复记录(一次性,非功能)
2026-09-07 大连热电卖出委托 645001623 经 live set-attribution 通道回填 strategy_id=manual-t / holding_id=15curl 实测 ok:trueorders/by-holding 回读确认)。当日归属曾被清为未关联的原因:候选下拉无做T项时老师误触「未关联」入口——本迭代修复后此路径消除。
## 4. 验证链
test-r017-candidates.mjs 12 断言 + typecheck + build + 存量回归(r016 19 / position-sync 35 / r013 21 / quote-sync 27)。
@@ -0,0 +1,36 @@
# 迭代复盘:15-归属候选纳入清仓持仓
> 复盘日期:2026-09-07 | 迭代状态:**已实施,待老师人工验收**
> 关联需求:R-017 关联计划:PLAN-016
## 结果
迭代 15 达成:归属候选 = 当前持仓 + 近 7 天清仓持仓(closed 标记 + 「已清仓 MM-DD」后缀),下拉锚点改 holdingId 键控(同策略多轮持仓不撞值)+ 「当前归属」占位防悬空;清仓后当日委托可补关联。另:当日大连热电卖出委托(645001623)的归属已经 set-attribution 通道修复回 manual-t/holding 15(一次性数据修复,非功能)。
## 过程事实
1. **数据取证先行**:老师报告「无法关联」时,DB 真相 = 委托归属已被清为 null/null(非从未设置)+ 候选接口只剩网格超市——「归属丢失」与「候选缺失」两个问题一次取证分清;
2. **当日修复走既有通道**set-attribution 本就无候选限制(Q4 可随时改语义),curl 修复 + 回读验证,不新造工具;
3. **实现**SqliteStore.getAttributionCandidates WHERE/ORDER 扩展 + closed/closedAt 字段(门面/api 零改动透传);AttributionSelect 从 strategyId 键控重构为 holdingId 键控 + 占位 + 后缀;
4. **验证**test-r017 12/12typecheck 通过;存量回归 r016 19/19、position-sync 35/35、r013 21/21、quote-sync 27/27build 通过。
## 经验教训(复盘沉淀)
### 1. 「关联」的锚点键要在多轮持仓出现时重新审视
- R-009 时代下拉 value 用 strategyId(一码一策略一行时无碍);R-016 历史行引入后同码可同时存在「当前行 + 历史行」、同策略可有多轮清仓——strategyId 键撞值成为真实 bug 面,holdingId 才是归属锚点(R-009 表结构早已如此,UI 层滞后);
- 沉淀:外键语义(表里存什么)和 UI 键(下拉传什么)要对齐审查,表结构升级时 UI 不自动跟上。
### 2. 「数据问题」与「能力缺失」要分层处置
- 老师当下要的是「这笔委托关联回去」(数据问题)——走既有 API 通道立即修复,不等迭代;「以后不再发生」(能力缺失)——立项 R-017 走流程。两层都做但互不阻塞。
### 3. 候选缺失时的 UI 应该显示「当前归属」而不是悬空
- select value 不在任何 option 里时浏览器表现不可控(显示空白或第一项),第一项恰是「未关联」时极易诱导误操作(本次归属被清的可能路径);
- 沉淀:受控 select 的 value 必须保证总有对应 option——数据缺口用占位 option 补,不让浏览器自作主张。
## 遗留/后续
1. **候选窗口(7 天)与 R-016 范围档是两套口径**:当前均满足场景;若老师后续要统一(候选也用 5 档范围),另议小改;
2. **自动归属推导**_resolveStrategyAttribution 时间窗)仍在(技术约束-013),但 Q3 全手动原则下仅为内部能力,未暴露 UI;维持现状;
3. R-015 / 迭代 13、R-016 / 迭代 14 的人工验收仍待老师确认。
## 验收状态更新(2026-09-08
> 功能被 R-018(迭代 16)语义取代退役,按取代归档(无需独立验收);R-017 已按归档清单移入 05-需求池/已完成/。
@@ -0,0 +1,19 @@
# 迭代目标:15-归属候选纳入清仓持仓
> 迭代编号:15 | 创建:2026-09-07 | 状态:已实施,待验收
> 依据计划:PLAN-016 需求:R-017(已定稿,2026-09-07
## 目标描述
交易记录 tab 的归属候选(orders/attribution-candidates)纳入近 7 天清仓的持仓(closed 标记),下拉锚点改 holdingId 键控——修复「清仓后当日委托无法补关联」(2026-09-07 大连热电做T实际发生:盘中归属被清后下拉已无手动做T候选)。
## 目标分解
1. **存储**SqliteStore.getAttributionCandidates):WHERE 扩展为 closed_at IS NULL OR closed_at ≥ 7 天前;返回行附 closed/closedAt;排序 当前持仓(shares DESC) 在前 → 清仓行 closed_at DESC
2. **UI**AttributionSelect):value 改 String(holdingId);点选按 holdingId 找候选取 strategyId+holdingId 写入;清仓选项加「(已清仓 MM-DD)」后缀;已归属但候选缺失时显示「当前归属」占位(防 select 值悬空);title 提示更新;
3. **回归**scripts/test-r017-candidates.mjs12 断言:候选构成/标记/窗口边界/排序/隔离/端点透传)。
## 对老师/主理人的配合需求
- 重启 DSH web 进程(服务端代码更新)+ 刷新页面后人工验收;
- 验收通过后迭代 15 标记「验收通过」,R-017 归档。
@@ -0,0 +1,20 @@
# 验收标准:15-归属候选纳入清仓持仓
> 迭代编号:15 | 依据:PLAN-016 验收要点 + R-017
## 验收标准线
1. **自动化**typecheck + build 通过;test-r017-candidates.mjs 12/12;存量回归 r016 / position-sync / r013 / quote-sync 全绿;
2. **候选构成**(真实环境):交易记录 tab 大连热电今日卖出委托的归属下拉 = 网格超市(当前持仓)+ 手动做T(已清仓 09-07)两项;
3. **补关联**:点选「手动做T(已清仓 09-07)」→ 保存成功提示 → 行内归属显示手动做T且下拉保持选中;做T策略 tab 历史持仓行展开可见该笔卖出(holding 15 关联);
4. **可逆**:切回「未关联」→ 双 null 持久化;再切回做T → 恢复;
5. **占位**:归属存在但候选缺失的场景显示「当前归属(…)」占位(可用超 7 天清仓行验证,或观察现有数据)。
## 验收方法
- 自动化项 AI 执行出具结果;2~5 老师重启 DSH web + 刷新页面人工验收;
- 全部通过后迭代 15 标记「验收通过」,R-017 归档 已完成/。
## 验收目标
- 5 条验收线通过,迭代 15 标记「验收通过」,R-017 更新实现状态并归档。
@@ -0,0 +1,141 @@
# 技术实现方案:16-交易关联驱动持仓份额动态调整
> 迭代编号:16 依据:PLAN-017 + R-018(已定稿)
## 0. 核心不变量(数据域分界,R-018 §〇)
- PositionSync(含幽灵清仓)**只写对账单域**(进程内快照),永不写 strategy_holdings
- strategy_holdings 写操作只来自:**归属服务(交易关联)** + **手动份额操作**add-shares/remove-shares/move-all-shares/策略删除清空,现状保留);
- 账本行转历史(closed_at)唯一途径:卖出段关联把份额减到 0(closeHolding);**幽灵清仓代码删除**。
## 1. 存储地基(阶段B
### 1.1 strategy_holdings 增列(幂等 ALTER,沿用 _ensureXxxColumn 模式)
```sql
ALTER TABLE strategy_holdings ADD COLUMN void_at INTEGER; -- 作废时间(NULL=有效;非 NULL=建仓被撤=从未成立)
ALTER TABLE strategy_holdings ADD COLUMN closed_shares REAL; -- 清仓前份额快照(closeHolding 写入;供撤卖出段恢复活动用)
```
- 活动判定收敛:`closed_at IS NULL AND void_at IS NULL`
- 唯一索引重建(活动唯一收窄至两态皆空):
```sql
DROP INDEX IF EXISTS idx_active_holding;
CREATE UNIQUE INDEX IF NOT EXISTS idx_active_holding
ON strategy_holdings (strategy_id, code) WHERE closed_at IS NULL AND void_at IS NULL;
```
- closeHolding 变更:置 shares=0 + closed_at=now + **closed_shares=清仓前份额**shares 列仍置 0,R-016 Q2 展示语义不变);
- 新增 voidHolding(strategyId, code):置 shares=0 + void_at=now(不改 closed_at);作废行不进 R-016 历史层(getHoldingsHistory 过滤 void_at IS NULL);
- _mapHolding 增 voidAt / closedShares 字段(读方法 select 补列)。
### 1.2 归属分段表 trade_order_attributions
```sql
CREATE TABLE IF NOT EXISTS trade_order_attributions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id TEXT NOT NULL, -- → trade_orders.order_id
strategy_id TEXT NOT NULL, -- 段归属策略(冗余,过滤快)
holding_id INTEGER NOT NULL, -- 段锚点持仓(R-010 展开用)
code TEXT NOT NULL, -- 冗余(by-holding 反查/防呆)
direction TEXT NOT NULL, -- buy/sell 冗余(撤段判向)
volume REAL NOT NULL, -- 该段已成交量(>0;总额 ≤ order.traded_volume
created_at INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_attr_order_strategy ON trade_order_attributions (order_id, strategy_id);
CREATE INDEX IF NOT EXISTS idx_attr_holding ON trade_order_attributions (holding_id);
CREATE INDEX IF NOT EXISTS idx_attr_strategy ON trade_order_attributions (strategy_id);
```
- **真相源 = 段表**trade_orders.strategy_id/holding_id 两列**退役为冗余**(不再作为查询/过滤源;保留列不删,防外部脚本 break;写入不再维护);既有单组归属迁移为第一段(volume=该单 traded_volume,幂等:段表空且 order 有归属才迁);
- 撤销语义支持 R-010 by-holding、R-009 history 策略过滤全部改经段表 join。
## 2. 归属服务(阶段C,新增 src/trades/AttributionService.js
> 命名遵守技术约束-016src/trades/ 交易域)。接口同步(node:sqlite DatabaseSync);归属服务持 `storage.sqlite` + DataStore 门面方法。
### 2.1 段级 apply(一段关联)
输入:{ orderId, code, direction, volume(已成交量) } + 目标 strategyId。判定动作表(R-018 §3.3):
| direction | strategy 活动仓(closed/void 均空) | 动作 |
|---|---|---|
| buy | 有 | addShares(strategyId, code, volume) → holding 取活动仓 |
| buy | 无 | openHolding(strategyId, code, volume) → 新仓(不复活历史/作废行) |
| sell | 有 且 shares ≥ volume | reduceShares;减后 0 → closeHolding |
| sell | 有 且 shares < volume | **拒绝**(报错 code='segment-exceeds',由 UI 自动截断后再提交;服务端不做隐式截断——避免「半生效」状态与部分关联提示脱节) |
| sell | 无 | 拒绝(code='no-active-holding',关联失败提示,R1 |
成功 → 写段表(order_id,strategy_id,holding_id,code,direction,volume,now)。买入建仓时 holding_id=openHolding 返回;买入加仓/卖出取活动仓 holding_id。
### 2.2 段级 revoke(撤一段)
输入:{ order_id, strategy_id }(唯一键)。逆操作(R3,粒度=段):
- **撤 buy 段**:查段 → holding。
- holding 活动(closed/void 空):
- shares ≥ 段量 → reduceShares(段量)**减后 shares=0** → voidHolding(作废,非 closeHoldingR3-1/R6 语义:建仓被撤=从未成立,无真实卖出→作废,不产生假清仓历史);
- shares < 段量(说明该段之后已有卖出段作用于同一 holding)→ **拒绝**code='segment-order-conflict',提示先撤更晚的段);
- holding 已 closed/void → 该 buy 段本就不应撤销成功(段应随原操作撤)→ 拒绝;
- **撤 sell 段**:查段 → holding。
- holding 仍活动(该 sell 只是减仓)→ addShares(段量) 加回;
- holding 已 closed 且 **closed_shares == 段量**(该段就是清仓段,且其后无新仓)→ **恢复活动**closed_at=NULL、shares=closed_shares、closed_shares=NULL
- holding 已 closed 且 closed_shares > 段量(部分减仓后另段清仓)→ 拒绝(先撤更晚段);已 closed 且同 code 新活动仓已存在(close 后又建仓)→ 拒绝(无法恢复,避免撞唯一索引;R-018 §3.4 边界);
- 成功后删段表行。
> 实现约束(写入 R-018 边界):**段撤销按「holding 内逆序」支持**;跨 holding 的段(拆单分给多策略)互相独立可任意撤(老师 R7 场景)。乱序撤同 holding 的段返回 segment-order-conflict,提示先撤更晚段——宁可拒绝不写错账(账本正确性 > 操作便利)。
### 2.3 全量替换 setSegments(改归属 = 撤旧段 + 加新段,原子)
输入:{ orderId, segments: [{ strategyId, volume }] }volume>0;总额 ≤ order.traded_volume,允许 < = 部分关联)。
流程(单事务):
1. 读 ordertrade_orderscode/direction/traded_volume);读段表现状;
2. diff:将被删除段逐个 revoke(同 2.2 冲突规则)→ 冲突则整体回滚并报错;
3. 新增/变更段逐个 apply(同 2.1)→ 任一失败整体回滚;
4. COMMIT 后返回 { segments: 当前全部段 }order 附加)。
> 事务:SqliteStore 暴露 `runInTransaction(fn)`BEGIN/COMMIT/ROLLBACK 包裹;同步 API 直接 exec)。
### 2.4 查询
- getOrderAttributions(orderId):段列表(含 strategyName 由 settings 附名);
- orders 列表附加归属:批量查段表按 order 聚合;
- getOrdersByHolding(holdingId):改经段表 join trade_ordersR-010 展示);
- 历史策略过滤(getOrderHistory/getFillHistory strategyId):改经段表(fill→order→段表 strategy_id 集合;`__unassigned__` = 无任何段);
- getHoldingsHistory:过滤 void_at IS NULL(作废不进历史层)+ closed 逻辑不变。
## 3. 候选与 API(阶段D
- **orders/attribution-candidates 退役 R-017 7 天语义** → 改 `orders/attribution-targets`{ code } → 全部策略(settings strategies+ 每策略该 code 当前活动份额 activityShares0=无仓)+ 该 order 已有关联段数提示(供 UI「指向后判定」的前置展示;**不预筛**,指向后由服务端判定)。策略级(Q1),holdingId 不再作下拉键;
- **orders/attribution-set**{ orderId, segments } → 归属服务 setSegments(替代 set-attributionset-attribution 端点删除或保留为单段包装——删,避免双写源混乱;既有数据迁移已覆盖);
- **orders/attribution-segments**{ orderId } → 读当前段(UI 展开/编辑用);
- 今日 orders 端点:order 行附 `segments: [{strategyId,strategyName,holdingId,volume}]`(取代原单组 strategyId/holdingId 附加;兼容字段保留首段值)。
## 4. UI 份额分配器(阶段ETradeRecordsTab
- AttributionSelect 退役 → **AttributionAllocator**
- 头部:委托已成交量 / 已关联 Σ / 待分配余额(醒目色);
- 一段 = 策略下拉(attribution-targets+ 份额输入(默认=余额全量,可改小;sell 时 max=min(余额, activityShares) 自动钳制)+ 「添加段」;
- 段列表(每段:策略名 + 量 + 「撤」按钮 → 调 attribution-set 去掉该段,即全量重交剩余段;或独立 revoke 端点——为原子性走 attribution-set 全量重交);
- 已关联/部分关联/未关联三态展示 + 保存即生效(每步操作即时 setSegments);
- 余额为 0 全关联提示;余额 >0 显「未分配剩余」提示(允许部分关联);
- 撤销冲突(segment-order-conflict)→ toast 展示后端提示。
- TradeRecordsTab order 行「归属」列 = 段 chips(点开进分配器)。
## 5. 分界 + 软提示(阶段A)
- PositionSync:删除 `_autoCloseGhosts`/ghostRounds/_ghostMiss/_fetchAccountId 调用链、stats.closedGhosts;类注释更新(只同步对账单快照);保留空快照双重确认与读穿透(对账单域行为不变)。test-position-sync.mjs 相应改断言(幽灵清仓→不再写账本:mock 断言 storage.closeHolding 未被调用);
- **漏关联软提示(T3)**:新端点 `positions/orphan-hints`:账本活动行(closed/void 均空)∩ QMT 快照无此 code → 返回 [{strategyId,code,name,shares,holdingId}]**只读**,不动账本);交易记录 tab 顶部横幅提示(N 个持仓对账单已无此票,请去关联卖出单或移出)。阈值:全部孤儿都列(不设 7 天——账本无快照即提示,软提示不自动写)。
## 6. 涉及文件
- src/position/PositionSync.js(删幽灵清仓)、scripts/test-position-sync.mjs(改断言)
- src/storage/SqliteStore.js(增列/索引重建/voidHolding/close 快照/段表 CRUD/runInTransaction/查询改段表/迁移)、src/storage/DataStore.js(门面透传)
- src/trades/AttributionService.js(新增:apply/revoke/setSegments
- src/api/trades.jsattribution-targets/set/segmentsorders 附加段)、src/api/strategies.jsstrategy-holdings/history 过滤 void
- src/client/views/TradeRecordsTab.jsxAttributionAllocator 替换 AttributionSelect
- scripts/test-r018-attribution.mjs(新增回归)
- 设计约束:技术约束-017/产品约束-011/数据存储设计.md §11 变更记录 + §12 追加
## 7. 验证链
test-r018-attribution.mjs(分界/动作表/撤段逆序/迁移/软提示/API+ test-position-sync 更新 + 存量回归(r017 改/ r016 / r013 / quote-sync+ typecheck + build。
@@ -0,0 +1,41 @@
# 迭代复盘:16-交易关联驱动持仓份额动态调整
> 复盘日期:2026-09-08 迭代状态:**验收通过(2026-09-08)** —— 口径:自动化为主体 + 只读走查 + 真实使用顺带确认(老师拍板,见验收标准.md)
> 关联需求:R-018(已定稿)| 关联计划:PLAN-017
## 结果
迭代 16 达成(R-018 全范围):**数据域分界落地**——幽灵清仓退役(PositionSync 不再写 strategy_holdings 账本);**归属升级为账本写操作**——策略级候选 + 统一份额分配器(委托拆 0..N 段 × 策略 × 量,余额默认全量可改小、允许部分关联)+ 买卖联动账本(买入加仓/建新仓不复活旧行、卖出减仓/归零清仓、仓不足拒绝、无仓拒绝)+ 撤段逆操作(撤建仓买入=作废第三态不产生假清仓历史、撤加仓=减回归零作废、撤卖出=恢复活动仓+份额加回,粒度=段、改归属原子)+ 归属分段存储(trade_order_attributions,真相源)+ R-017 7 天候选退役(holdingId 键控/占位安全逻辑由策略级 targets 承担)+ 漏关联软提示(只读)。存量归属(trade_orders 单列)自动迁移为段表第一段。
验证:新回归 test-r018-attribution 29/29test-position-sync 29/29(幽灵退役断言重写);存量回归 r016 24 / r017 12(语义修订)/ r013 21 / r009 14 / r009-api 13(端点更新)/ r009-sync 8 / r009-normalize 11 / r011-api 12 / r011-tabs 23 / quote-sync 27 全绿;typecheck + build 通过。
## 过程事实
1. **文档链先行**:讨论收敛 → R-018 定稿 → PLAN-017 → 迭代 16 三件套 → 设计约束变更(技术约束-017 / 产品约束-011 / 数据存储设计 §11-13);
2. **阶段A 分界**PositionSync 删 `_autoCloseGhosts`/ghostRounds/账户守卫链/stats.closedGhosts,只保留对账单快照同步(含空快照双重确认与读穿透);软提示新端点 positions/orphan-hints(账本活动行 ∩ 快照缺失 code,只读);
3. **阶段B 存储**strategy_holdings 增 void_at/closed_shares(幂等 ALTER + 活动唯一索引重建收窄至两态皆空——索引创建移出 SCHEMA_SQL 到补列之后,防旧库无列建索引崩溃,quote-sync 回归暴露);closeHolding 记 closed_sharesshares 仍置 0R-016 Q2 展示语义不变);voidHolding/reopenHolding/getHoldingById;新建 trade_order_attributions 段表;存量归属迁移段表第一段(幂等);
4. **阶段C 归属服务**src/trades/AttributionService.js 新增):apply(动作表判定)/ revoke(撤段逆操作,同 holding 逆序、跨 holding 独立)/ setSegments(全量替换、单事务原子);setOrderAttribution 兼容封装 = 替换语义(清旧段设新段);
5. **阶段D API**attribution-targets(策略级 + activityShares/ attribution-set / attribution-segmentsorders 与 trades/history 返回附加 segmentsR-017 attribution-candidates/set-attribution 端点退役;strategy-holdings/history name 兜底改段表 join
6. **阶段E UI**AttributionSelect → AttributionCell + AttributionEditor(余额头 + 段 chips 可撤 + 策略下拉/量输入/截断提示 + 全部撤除/完成);交易记录 tab 顶部漏关联软提示横幅;
7. **阶段F 回归**:新脚本 test-r018-attribution;更新 position-sync(幽灵退役断言)、r0177 天候选退役语义)、r009-api(新端点)、r016-history(段表造归属)。
## 经验教训(复盘沉淀)
### 1. 语义迁移要逐调用点核对 + 用旧回归脚本当哨兵
R-018 把「归属真相源」从 trade_orders 单列迁到段表,牵动 r009/r016/r017/verify-real 一堆脚本与 strategies handler。靠跑存量回归暴露了两类坑:① quote-sync 的旧结构库建库路径在 SCHEMA_SQL 含新列索引时崩溃(索引引用尚不存在的列)——列迁移与索引重建必须同序;② 测试里「UPDATE trade_orders SET holding_id」直改冗余列的方式在真相源切换后不再生效(r016 name 兜底失败)——测试自身要改经段表造数据。沉淀:**存储层语义变更时,先跑全部存量回归锁定断裂点,再逐点核对(skill §5 语义迁移核对)**。
### 2. 「作废」与「清仓」是两个生命周期终态,不能用同一标记
撤建仓买入段若走 closeHolding 会造出假的「已清仓」历史行(R-016 历史层会显示、统计会含)——老师 R3-1 拍板新增 void 第三态。沉淀:**「从未成立」与「曾经成立后卖出」在账本语义上必须区分**,UI 历史层只认 close。
### 3. 服务端不做隐式截断,让 UI 截断并提示余额
卖出段 > 活动仓时服务端直接拒绝(segment-exceeds),由 UI 自动截为可容纳量并醒目提示「余额保留」——避免服务端悄悄改用户意图造成「半生效」状态与提示脱节。沉淀:**涉及钱/仓位的写接口要显式,宁可报错回滚也不要隐式修正**。
### 4. 数据域分界用一句话可执行规则落地
「幽灵清仓是一个同步机制,爪子不许伸到账本」落到代码 = PositionSync 构造里不再持有 storage 写能力路径 + 回归断言 closeHolding 零调用。沉淀:**架构性拍板要落到「哪个模块能写哪张表」的显式约束,并用测试钉死**。
## 遗留/后续
1. **r013-columns 测试漂移(既有,非本迭代引入)**:断言「默认 8 列」但 COLUMN_META 自 R-015 加 4 行情列后实际 14 列(4 默认隐藏)——R-015 后未同步,建议另立小修(列显隐功能未破坏,纯测试预期过期);
2. **R-016/R-017 文档同步**:R-017 迭代 15 复盘所述「候选窗口」语义已被 R-018 取代(7 天候选退役),R-017 需求文档待标注退役(R-018 需求文档已含 T2 决策,归档时统一);
3. **幽灵清仓退役的旧数据影响**:存量库中过去被幽灵清仓转历史的行保持 closed 不变(不回溯);新语义下账本只随交易关联/手动操作走;
4. **UI 待老师人工验收项**:多段拆单交互、撤段恢复活动、作废行不显历史层、软提示横幅、策略持仓 tab 不因 QMT 卖光自动消失(须关联卖出单才 close)。
@@ -0,0 +1,29 @@
# 迭代目标:16-交易关联驱动持仓份额动态调整
> 迭代编号:16 | 创建:2026-09-08 状态:实施中
> 依据计划:PLAN-017 需求:R-018(已定稿,2026-09-08 老师逐项拍板)
## 目标描述
把「交易归属」从纯标签升级为**账本写操作**,落实 R-018 的**数据域分界**:幽灵清仓等同步机制只作用于「全部持仓」对账单域,不再写 strategy_holdings 账本;策略持仓份额只由**交易关联分配器 + 手动份额操作**驱动。
范围:策略级候选 + 统一份额分配器(委托已成交量拆 0..N 段 × 策略)+ 买卖联动账本(买入加仓/建新仓、卖出减仓/归零清仓、仓不足截断续分、无仓失败提示)+ 撤段逆操作(撤建仓买入=作废第三态、撤加仓=减回归零作废、撤卖出=恢复活动+份额加回,粒度=段)+ 归属分段存储(trade_order_attributions+ R-017 7 天候选退役 + 漏关联软提示 + PositionSync 删幽灵清仓逻辑。
## 目标分解(阶段)
1. **阶段A 数据域分界**PositionSync 删除幽灵清仓账本写(_autoCloseGhosts 不再 closeHolding),只保留对账单快照同步;漏关联软提示检测基础;
2. **阶段B 存储地基**strategy_holdings 增 `void_at` + `closed_shares`(幂等补列,close 时记录清仓前份额供撤段恢复);新建 `trade_order_attributions` 段表;trade_orders 原两列退役为冗余(读取改经段表);存量单组归属迁移为第一段;
3. **阶段C 归属服务**apply/revoke 判定动作表(买/卖 × 活动仓状态)+ 撤段逆操作 + 原子性(同事务撤旧段+加新段);
4. **阶段D 候选与 API**orders/attribution-candidates 回归「策略级」(R-017 7 天候选退役;holdingId 键控/占位安全逻辑保留);orders/attribution-set(全量替换式)+ attribution-segments(读);
5. **阶段E UI 分配器**TradeRecordsTab 归属入口改策略下拉 + 段量 + 段列表(每段可撤);
6. **阶段F 回归**:新回归脚本(分界/分配器动作/撤段/迁移/软提示)+ 存量回归 + typecheck + build。
## 讨论过程(摘要)
2026-09-08 讨论收敛全部决策:Q1 策略级候选 / Q2 拆段 / Q3 保留建仓逻辑(份额基于 QMT 持仓创建)/ Q4 已成交量 / Q5 新买入=新仓不复活;R1 无仓卖出关联失败 / R2 分段存储 / R3 撤段逆操作(作废第三态+恢复活动)/ R4 统一分配器(不分子买卖、余额默认全量可改小、允许部分关联)/ R5 买入可拆段 / R6 撤加仓归零作废 / R7 段级取消;数据域分界(幽灵清仓不写账本);T1 关闭(无 30s 竞态);T2 R-017 退役;T3 漏关联软提示。
## 对老师/主理人的配合需求
- 重启 DSH web 进程(服务端代码更新)+ 刷新页面人工验收;
- 验收通过后迭代 16 标记「验收通过」,R-018 归档 + 需求池索引更新;
- 迭代 12(R-014 幽灵清仓条款)、迭代 15(R-017 退役)为既有功能变更,人工验收时一并确认新行为符合预期。
@@ -0,0 +1,24 @@
# 验收标准:16-交易关联驱动持仓份额动态调整
> 迭代编号:16 | 依据:PLAN-017 验收要点 + R-018
> 验收状态:**已通过(2026-09-08)** —— 口径修订:自动化为主体 + 只读走查;实盘操作类验证项降为「真实使用顺带确认」(老师拍板:实盘不可为验收而操作,边用边发现问题再修)
## 验收标准线
1. **数据域分界(阶段A**PositionSync 不再写 strategy_holdings——幽灵清仓逻辑删除,同步只维护对账单内存快照;回归脚本断言 storage.closeHolding 不被 PositionSync 调用;
2. **存储地基(阶段B**strategy_holdings 增 void_at/closed_shares 列(幂等);活动唯一索引收窄至两态皆空;closeHolding 记录 closed_sharesshares 仍置 0);voidHolding 置 void_at;段表 trade_order_attributions 建表 + 存量单组归属迁移为第一段(幂等);
3. **份额分配器动作表(阶段C**:买入段→加仓/建新仓(不复活旧行);卖出段→减仓/归零清仓、仓不足拒绝(segment-exceeds)、无仓拒绝(no-active-holding);撤买入段→减回/归零作废(void,不产生假清仓历史);撤卖出段→加回/恢复活动仓(closed_shares 恢复);跨 holding 段可任意撤、同 holding 乱序撤拒绝(segment-order-conflict)——全量替换 setSegments 原子(失败整体回滚);
4. **API(阶段D**attribution-targets(策略级 + activityShares);attribution-setsegments 全量替换);attribution-segments(读);今日 orders / trades/history 附加 segmentsR-017 7 天候选退役(attribution-candidates/set-attribution 端点退役,候选改由 targets 承担);
5. **软提示(T3**positions/orphan-hints 返回账本活动行 ∩ QMT 快照缺失的 code(只读不写);
6. **自动化**test-r018-attribution.mjs 29/29 全绿 + test-position-sync 更新后 29/29 + 存量回归(r016 24 / r017 12 / r013 21 / r009 14 / r009-api 13 / r009-sync 8 / r009-normalize 11 / r011-api 12 / r011-tabs 23 / quote-sync 27)全绿 + typecheck + build 通过。
## 验收方法(2026-09-08 修订,老师拍板)
- **主体 = 自动化回归**:上述标准线 1-6 由回归脚本 + typecheck + build 机器验证(AI 已执行并出具结果);实盘为真实资金,不可为验收做买卖操作;
- **人工只读走查**(老师 2026-09-08 已重启并做一次关联操作确认 OK):分配器界面(余额头/策略下拉带仓数/段 chips)显示正常;仅只读观察,不做实盘写操作;
- **顺带确认项(不主动验收,真实使用自然发生时观察)**:卖出全清后策略持仓行不再 30s 自动消失(幽灵退役);顶部漏关联软提示出现;撤段恢复/作废行为——边用边发现问题再修。
## 验收目标
- 标准线 1-6(自动化)通过 → 迭代 16 标记「验收通过」;R-018 归档 已完成/ + 需求池索引实现状态更新;
- 实盘行为类验证项以「真实使用顺带确认」持续跟进(老师口径:先标记成功,后面边用边发现问题再修)。
@@ -0,0 +1,76 @@
# 迭代 17 UI 交互设计:策略会话(讨论依据控件)
> 依据:R-021(合并原 R-019+R-021,已定稿)+ 产品逻辑设计(同迭代)| 日期:2026-09-08 | 状态:**设计稿(D 问题待拍板)**
> 视觉基线:UI约束-004--dsw-* token/ UI约束-001(头部 chip 形态)/ 现有 QmtConnectionChip 交互模式;复用 Toast、外点关闭、RPC(useRpc) 机制。
## 1. 交互总览(一句话)
对话视图输入区上方有一枚**「讨论依据」chip**(仿 DSH workspace chip:未选=「讨论依据:请选择 ▾」;已选=「依据:做T ▾」+刷新);选择策略 → 服务端生成数据摘要 → 注入并在对话流顶部出现一条**数据依据条目**,之后即可基于该策略数据讨论。
## 2. 控件布局(示意)
```
┌─ 对话视图 ──────────────────────────────┐
│ …对话消息流… │
│ [数据依据条 ▾ 做T · 快照 14:32 · 3仓/42笔] ← 选后注入显示(可展开全文)
│ ─────────────────────────────────────── │
│ [依据:做T ▾] [↻] │ ← accessory 行(输入框上方左侧)
│ ┌ 输入框(composer)────────────────┐ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
## 3. chip 状态机
| 状态 | 外观 | 交互 |
|---|---|---|
| 未加载 | 不显示(静默,与 QMT chip 一致) | — |
| 未选择 | `讨论依据:请选择 ▾`tertiary 色) | 点开 → 策略下拉(空态见 §5) |
| 生成中 | `依据:做T …`opacity .6,右侧小 spinner/省略) | 不可再点(防抖) |
| 已选择 | `依据:做T ▾` + 右侧「↻ 刷新」小钮 | 点 chip 换策略;点 ↻ 刷新注入 |
| 策略已删除 | `依据:做T(已删除)▾`warning 色) | 下拉选其它/清除 |
| 错误 | toast 错误 + chip 保留原态 | 可重试 |
## 4. 交互细节
1. **选择策略(F1 主流程)**
- 点击 chip → 下拉菜单(与 QmtConnectionChip 同一套 Menu 视觉):
- 标题区「以哪个策略讨论?」;
- 策略项:每项 = 策略名 + 副行「当前 N 只持仓 · 最近交易 …」;
- 底部「清除依据」(仅已选时出现);当前依据项前打 ✓;
- 点击某策略 → chip 进入生成中 → 服务端取摘要 → 注入会话 + 对话流顶部出现依据条目 → toast.success(`已载入「做T」数据依据:3 持仓 / 42 笔交易(快照 14:32:05`)。
2. **注入条目(D-1 建议形态)**
- 视觉:淡色卡片/引用样式(bg-layer 弱化 + 左侧竖条 accent),标题行「⛭ 神之一手数据依据」+ `做T · 快照 2026-09-08 14:32:05`,行 2 摘要「当前持仓 3 只 / 历史交易 42 笔(含已清仓)」;
- 默认折叠,点击展开全文(markdown 摘要内容);
- 多依据/多刷新并存时按时间堆叠,最近一次在消息流最新处;不伪装用户/助手消息(若通道仅支持文本消息,则呈现为最顶部一段普通消息文本——实现以调研为准)。
3. **换策略**:chip 下拉选另一策略 → 同"生成中→注入"流程;chip 文案与已注入条目更新。
4. **刷新依据**:点 chip 右侧 ↻ → 生成新快照并注入新条目(含新快照时间);toast「已更新依据(快照 14:35:10)」。不会删除旧条目。
5. **清除依据**:下拉「清除依据」→ chip 复位「请选择」;不清除历史注入条目。
6. **键盘/可及**:Esc 或外点关闭下拉;chip 提供 title 提示「以某个策略的数据为依据开始讨论(只读)」。
7. **只读表达**:选择器下拉仅含策略与其状态信息,无任何写/操作入口;文案提示"会话只读,不执行交易操作"(依赖尾注与依据条目样式即可,不打断输入)。
## 5. 空态与边界 UI
| 场景 | UI |
|---|---|
| 无策略可配置 | 下拉空态:「暂无策略,请到 设置 → 神之一手 → 策略分组 添加」+「去设置」快捷键位 |
| 策略持仓为空 | 摘要条目持仓区显示「当前无持仓」;交易摘要照常 |
| 行情缺失 | 摘要中现价列显示 —,条目副行提示「部分行情缺失(--)」,不阻塞 |
| 生成失败/服务不可用 | toast.error(原因)chip 回原态可重试 |
| 已在数据 tab.odl-root)| 不显示该控件(现状 CSS 隐藏 composer 的区域一致,仅聊天视图展示) |
## 6. 组件落点(初拟,技术方案细化)
- 客户端新组件 `StrategyBasisChip.jsx`(chip + 下拉 + 刷新),复刻 QmtConnectionChip 的结构(rootRef/外点关闭/useRpc/useToast);
- 注入条目组件 `DataBasisCard.jsx`(折叠/展开 markdown 展示);
- 服务端新只读端点(`one-divine-lot/strategies/basis-summary`)生成摘要(产品逻辑 §4 规格);
- 挂载位置:D-4(对话 accessory vs 会话头部)拍板后确定 slot 与可见性策略。
## 7. D 问题(AI 建议项,与产品逻辑设计共享,待老师拍板)
| # | 问题 | AI 建议 |
|---|---|---|
| D-1 | 注入条目呈现形态 | 折叠「数据依据」卡片,点开全文(见 §4.2) |
| D-2 | 交易摘要聚合粒度 | 按持仓聚合 + 已清仓标注;如需近期逐笔再加近 N 笔 |
| D-3 | 依据跨刷新持久化 | 会话级不持久化(重开重选) |
| D-4 | chip 位置 | 对话视图输入框上方 accessory 行左侧(仿 workspace chip 位置);备选=会话头部与 QMT chip 并排 |
@@ -0,0 +1,131 @@
# 迭代 17 产品逻辑设计:策略会话
> 依据:R-021(合并原 R-019+R-021,已定稿)| 日期:2026-09-08 | 状态:**设计稿(D 问题待老师拍板后定稿)**
## 0. 一句话逻辑
> 会话 = 用户选定「讨论策略」→ 系统把该策略**当前数据**(当前持仓 + 全历史关联交易摘要)作为会话依据注入 → 之后人机围绕该数据复盘/分析;依据可随时更换/刷新;只读。
## 1. 概念与数据域
| 概念 | 定义 | 数据来源 |
|---|---|---|
| 策略 | settings 中的策略定义(id/name,做T、网格超市…) | settings.strategies(现有) |
| 策略当前持仓(活动行) | 该策略账本中未清仓的 holdingshares>0 活动) | SqliteStore strategy_holdingsR-018 数据域:账本为唯一权威) |
| 行情现价/涨跌 | holding code 的实时价格 | QuoteHub 内存快照(读穿透;不可得=—) |
| 关联交易记录 | 归属该策略/持仓的委托(含已清仓历史,trade_orders 归属列 + 历史库) | SqliteStore trade_orders/trade_fillsR-009/R-018 |
| 数据依据(basis) | 一次注入到会话的"策略当前数据摘要快照" | 服务端按需生成(本设计) |
**关键语义(沿用 R-018 数据域分界)**:持仓展示 = 账本(活动行);交易 = 归属账本(历史全量);行情 = 内存快照。摘要生成**只读**这些域,不写账本。
## 2. 会话级状态
策略会话不需要跨会话持久存储(设计建议,见 D-3);会话内 UI 状态:
- `basis: none | { strategyId, strategyName, injectedAt }`
- `basisState: idle | generating | done | error`
- 注入后的摘要内容随会话消息流存在(可折叠"数据依据"条目),不单独存储。
## 3. 核心流程
### 3.1 选择策略 → 注入依据(主流程)
1. 对话视图(聊天)中,用户在「讨论依据」控件里选一个策略;
2. 客户端调服务端 `strategies/:id/basis-summary`(只读)生成摘要文本(规格见 §4);
3. 摘要通过**宿主注入通道**进入当前会话上下文(技术通道调研待定,见 §6);同屏在对话流顶部呈现「数据依据」条目;
4. toast 反馈:`已载入「做T」数据依据(3 持仓 / 42 笔交易,快照 14:32:05`
5. 用户开始提问复盘/分析;模型以摘要(及会话中后续补充)为依据回答。
### 3.2 换策略
- 重开选择器选另一策略 → 再次执行 3.1(新摘要作为新的依据条目追加;chip 切换为新策略)。旧依据条目保留在历史中(模型自然以最近/明确引用的为准)。
### 3.3 刷新依据
- 已选状态下 chip 提供「刷新」:重新生成**当前时刻**摘要并注入(盘中数据变化后想拿最新)。
- 语义:每次刷新=新快照依据条目(含新快照时间),不覆盖旧条目。
### 3.4 清除依据
- 选择器提供「清除依据」:回到无依据普通会话(chip 复位为「讨论依据:未选择」);已注入的历史条目仍在消息流中,后续对话不再视为策略依据会话。
### 3.5 边界
| 场景 | 行为 |
|---|---|
| 无可选策略(未配置) | 选择器显示空态「暂无策略,请到 设置→神之一手→策略分组 添加」 |
| 会话进行中该策略被删除/改名 | chip 下次加载策略列表时按 id 对齐:找不到=显示「策略已删除」+ 可清除;改名后按 id 正常(显示新名);历史依据条目保留 |
| 策略当前持仓为空 / 无行情 | 摘要照常生成:持仓区显示"当前无持仓";现价列显示 —;交易摘要照常(历史) |
| 账本只读 | 摘要生成与注入路径无任何写操作;UI 无操作按钮(区别于数据 tab 的手动操作) |
| 未选策略的普通聊天 | 与现状完全一致,不受影响(策略会话是可选增强,不锁定会话) |
## 4. 摘要规格(服务端 `basis-summary` 输出,markdown 文本)
结构(自上而下):
1. **快照头**`【神之一手数据依据】策略:做T | 快照:2026-09-08 14:32:05 数据为只读快照,非实时`
2. **当前持仓**(活动行,逐行):code 名称|份额|可用|成本价|现价(— 缺失)|市值|浮动盈亏|持仓天数;尾部合计(N 只 / 总市值 / 总盈亏)。现价/市值缺行情时给—,不阻塞。
3. **交易摘要(该策略全历史关联委托,聚合)**:总笔数/买入累计/卖出累计(按归属 holding 聚合):
- 每 holding 一段:code 名称|状态(持有中/已清仓 清仓日)|买卖笔数|累计买入(量/额)|累计卖出(量/额)|当前份额;
- 已清仓 holding 标注清仓日期与最后动作摘要;
- 排序:持有中在前(按成本日/名称),已清仓在后(按清仓日倒序);
- **规模上限**holding 超过 20 行时按最新清仓倒序截断并注明「更多历史见 交易记录 tab」。
4. **尾注**:只读提示「以上为策略数据快照;本会话只读,不执行任何交易/账本操作」。
字段精确取数与空值规则在技术实现方案细化。
## 5. 非目标(本期明确不做)
- 宿主工作区/目录注册(原 R-019 R3,已并入 R-021);
- 账本写/交易执行、任何操作按钮;
- 按需取数工具(AI 自行调用业务接口);
- 复盘产物落盘 / 复盘管理(含 DB)——未来 T-002 方向;
- 通用视图(全部持仓/交易记录)作依据;
- 关注列表作为依据源。
## 6. 技术通道(设计假设,待调研确认)
注入的可行通道候选:① 宿主 injected-context/上下文节点(可折叠条目、非用户消息)→ 首选;② 会话 send(文本消息形态进会话)→ 兜底(呈现为一条前置依据消息)。**渲染与动作方案以调研结果为准**(本期设计保留两种形态的表达,UI 交互设计按"数据依据条目"统一描述)。
## 0.5 产品逻辑总图(2026-09-08 老师确认:画像 / 模型 / 主线 / 分期草案)
### 目标交易者画像(Q-画像,2026-09-08 确认)
- **混合交易模式的多策略分仓管理**:同一账户下并存多种交易风格的策略分组(当前:手动做T + 程序化网格超市;未来:情绪流交易风格的管理逻辑);
- 即"策略 = 分仓 + 交易风格/规则的容器";系统须能承载 手动主观 + 自动化规则 + 未来情绪流 三类玩法(彼此数据/规则/复盘口径不同)。
### 交易员工作模型(知识库归纳,作为产品逻辑基准)
- **日循环**:盘前计划 → 盘中执行/监控 → 盘后记账核对 → 复盘总结 → 明日计划;
- **策略循环**:规则制定 → 按规则执行 → 规则校验(复盘)→ 规则修订;
- **复盘闭环**:重建事实 → 归因 → 对照规则 → 提炼改进;**复盘原料 = 决策时的依据记录**(知识库共识:没有决策留痕的复盘只是猜)。
### AI 分工定位(人机合一,只读)
- **盘中 = AI 参谋台**(状态聚合 / 规则触发提醒 / 轻问答 / 决策留痕 / 人扣扳机执行),快、准、不打断;
- **盘后 = AI 复盘主持人**(自动重建事实线 → 引导人过结论 → 结论留痕),人不被 AI 下"对错"裁决;
- 决策依据记录(盘中一键留痕:理由 + 当时快照)是产品逻辑主线之一(2026-09-08 老师确认)——复盘有据的前提。
### 能力地图(现状 → 缺口)
- 已具备"事实记录层":对账单快照 / 策略账本(唯一权威)/ 交易记录+归属 / 行情快照 / 策略 configSchema(可承载规则参数);
- 缺口(按承接排序):①复盘事实线入口+可讨论会话(R-021 合并需求,本迭代)②决策依据留痕(盘中)③规则触发提醒 ④盘后自动日结/统计 ⑤复盘管理库(决策日志+复盘记录结构化存储检索)。
### 分期草案(2026-09-08 老师认可分步实现;**各 Phase 具体内容待讨论细化**)
| 阶段 | 主题 | 交付设想(草案) |
|---|---|---|
| Phase 0(迭代 17 | 复盘会话闭环 | R-021 策略会话复盘(原 R-019+R-021 合并;策略数据依据 + 粒度复盘上下文,事实线 → 会话讨论 → 结论在会话) |
| Phase 1 | 盘中辅助(雏形) | 决策依据一键留痕(理由+快照落库)+ 规则触发/偏离提醒雏形(configSchema × 行情)+ 会话内盘中快查 |
| Phase 2 | 盘后日结与统计 | 自动日结(当日各策略事实线+盈亏+异常)→ 一键入会话复盘;周期统计(轮次收益/胜率/策略健康) |
| Phase 3 | 复盘管理库 | 决策日志+复盘结论结构化沉淀/检索/关联(DB),对接目标-006;情绪流风格管理逻辑另立项 |
## 0.6 Phase 0 范围确认(2026-09-08 老师确认:就这样)
- **Phase 0 = 复盘会话闭环,范围 = R-021 单一合并需求(原 R-019 + R-0212026-09-08 老师指令合并、逻辑整合入 R-021)**;
- **复盘单元建模(A 拍板)**:引入"轮次 round"概念(holding 之下的交易周期聚合层):
- 网格超市:一次买入触发 + 对应卖出配对 = 一轮(每格触发记一轮);
- 手动做T:一次开仓 → 清仓 = 一轮;
- 支持人工合并/拆分修正(先人工可调);
- round 承接"复盘对象"的最小粒度,委托/段为 round 内明细;
- **行情口径(C 拍板)**:本期事实线 = 交易记录 + 持仓史 + 现价对照(无历史 K 线/分时);历史行情背景 Phase 3 复盘库时再评估;
- **决策日志预留(拍板)**:Phase 0 设计为"决策依据记录"留数据模型位置(时刻/理由/类别/当时快照/关联 round 或 holding),Phase 1 实现,不返工;
- **风格差异**:复盘口径按策略类型可扩展(手动/网格,未来情绪流预留策略类型维度)。
## 7. D 问题(AI 建议项,待老师拍板)
| # | 问题 | AI 建议 |
|---|---|---|
| D-1 | 注入条目的呈现形态 | 对话流顶部「数据依据」条目:默认折叠为一行(图标+策略+快照时间+行数摘要),点开看全文;不伪装成用户消息 |
| D-2 | 交易摘要聚合粒度 | 按持仓聚合(每持仓一段汇总 + 已清仓标注),不逐笔铺开(避免注入过长);如老师要"近期逐笔"可加近 N 笔小表 |
| D-3 | 依据选择是否跨刷新持久化 | 本期**会话级不持久化**(刷新需重选;历史注入条目仍在消息流),与 R-016 会话级偏好一致、不加存储;复盘管理未来再考虑 |
| D-4 | 选择控件挂载位置 | 对话视图输入框上方 accessory 行左侧(仿 DSH workspace chip 位置),仅聊天视图可见;替代方案=会话头部(与 QMT chip 并排)待老师定 |
@@ -0,0 +1,39 @@
# 迭代 17:策略会话(以策略当前数据为依据的会话讨论)
## 目标(一句话)
让 AI 会话可"以策略为 Workspace":对话开始前选「讨论策略」,预注入该策略**当前数据摘要**作为会话依据,AI 与老师围绕该策略持仓/交易做复盘与分析讨论;只读、会话中可换/刷新依据。
## 目标描述
- 老师原始诉求「新需求,让 AI 会话可以以策略为 Workspace」经四轮讨论定稿(R-0192026-09-08;同日老师指令 R-019 并入 R-021 合并为单一需求):
- **改造现有 DSH 对话窗口**,不引入宿主工作区/目录;
- 会话的数据 = ①开始前/过程中注入的策略当前数据摘要 + ②会话过程本身产生的对话;复盘管理(可能带 DB 沉淀)为神之一手未来模块,本期不做;
- 注入口径:当前持仓明细 + 该策略全历史关联交易聚合摘要(含已清仓);
- 只读(Q4=A);预注入不挂工具(R2-Q1=A);产物不落盘(R2-Q4)。
- **本迭代分阶段推进**:先设计(产品逻辑 + UI 交互逻辑,老师 2026-09-08 指令)→ 宿主注入通道调研 → 技术实现方案 → 实现与验收。
## 讨论过程
| 日期 | 轮次 | 要点 |
|---|---|---|
| 2026-09-08 | 需求讨论 Q1-Q4 | 语义收敛:非物理目录;数据为依据;会话内只读 |
| 2026-09-08 | 需求讨论 R2 | 预注入;范围 R2-Q3=A;产物暂不落 |
| 2026-09-08 | 需求讨论 R3 | 参考 DSH 工作区模式的"开始前选策略"体验,但**不用宿主工作区** |
| 2026-09-08 | 需求定稿 F1/F2 | 首条消息前「讨论策略」选择 + 会话中可换/刷新;注入口径维持 R2-Q3=A |
| 2026-09-08 | 老师指令 | 立项迭代:先做产品逻辑、UI 交互逻辑设计 |
## 本期(设计阶段)交付
- `产品逻辑设计.md`(数据与行为逻辑、摘要规格、状态与边界)
- `UI交互设计.md`(交互流程、控件、视觉与文案、状态机)
- D 系列设计问题(AI 建议项)→ 老师拍板 → 设计定稿
## 后续阶段(设计定稿后)
- 宿主注入通道技术调研 → 技术实现方案.md → 实现 → 验收标准.md + 验收(是否拆分独立迭代由老师定)。
## 对老师配合的请求
1. 拍板 D1-D4(产品/UI 设计中的 AI 建议项,见设计文档末);
2. 实现验收需真实会话环境人工目视。
@@ -0,0 +1,68 @@
# 迭代 18 技术实现方案:QMT Bridge MCP 能力内建
> 依据:R-0202026-09-08 定稿,Q1-Q6 + 老师澄清:移除三方 dsh-skill-mcp-panel、官方 dsh-mcp-client 可依赖)| PLAN-018
> 日期:2026-09-08
## 目标架构
神之一手在 apply 内**动态挂载 DSH 官方 @deepseek-ai/dsh-mcp-client**(依赖官方库、管理在插件内),把 QMT Bridge 的 MCP 工具注册给模型;MCP 实例生命周期(挂/切/卸)随 QMT 连接配置的增删改与激活切换联动;状态出口 = 设置页 + 会话头部指示灯。REST 直连架构不变(技术约束-003)。
```
神之一手 apply(ctx)
├─ settingsQMT 连接配置:list + activeId + defaultId [已有 R-004]
├─ dataSourceREST 直连,baseUrl 随激活热切换) [已有]
└─ QmtMcpManager(新增 src/mcp/QmtMcpManager.js
├─ 动态 import(@deepseek-ai/dsh-mcp-client) → ctx.plugin(module, config)
│ config = { serverName: QMT_Bridge_MCP, transport: streamable-http,
│ url: 激活 baseUrl + /mcp, failOnStartupError:false,
│ reconnect: {enabled, 500ms→30s, 10次} } ← 与宿主现受管块同参(Q5)
├─ 生命周期:start(启动挂载激活连接)/ resync(url)(激活·编辑地址·删除联动)
├─ 状态:probe()SDK listTools 握手:connected + 工具数 + 延迟 / error
│ getStatus() → { state, serverName, url, mounted, toolCount, error, latencyMs, checkedAt }
└─ dispose:卸载 fiberctx.effect 释放)
```
## 关键事实(已实证)
- `@deepseek-ai/dsh-mcp-client@0.1.1-rc.2` 已加入 dependencies;从插件 lib 可 import,模块导出 {name:mcp-client, inject:[tools], apply, Config},可 ctx.plugin() 动态挂载(mcp_router 同款实证);
- serverName 存活实例内必须唯一 → 移三方受管块前插件实例与宿主实例不可并存(实施时序见 PLAN-018 注意事项);
- dsh-mcp-client 不暴露连接状态查询 API → 状态用**独立 SDK 探测**(@modelcontextprotocol/sdk Clientinitialize + tools/list → 工具数/延迟),与 dsh-mcp-client 自管连接互不干扰;
- 宿主现 QMT_Bridge_MCP 受管块 = 三方 dsh-skill-mcp-panel 写入,连接与注册实为官方 dsh-mcp-client。
## 改动清单
| 文件 | 改动 |
|---|---|
| package.json | dependencies + @deepseek-ai/dsh-mcp-client@0.1.1-rc.2 + @modelcontextprotocol/sdk@1.30.0(已加) |
| src/mcp/QmtMcpManager.js(新增) | MCP 实例管理:动态挂载/卸载 + resync + probe/getStatus + dispose |
| src/index.js | 实例化 QmtMcpManager、start、ctx.effect 释放、传入 registerApi runtime |
| src/api/index.js | runtime 增 mcpManager(分发 handleQmt/handleMarket |
| src/api/qmt-connections.js | METHODS + mcp-statusactivate/update(地址变)/remove 热切换点 + mcpManager.resync(url) |
| src/api/market.js | sync-status 增加 mcp 域(读 mcpManager.getStatus |
| src/client/views/SyncIndicators.jsx | 三灯 → 四灯:+「MCP」(绿=已连接 / 黄=重连中 / 红=断开 / 灰=未启用;点击刷新) |
| src/client/views/SettingsSection.jsx | QMT 连接配置子 tab 顶部 MCP 状态条 + 「检查 MCP」按钮(手动触发 mcp-status refresh |
## MCP 状态语义(对齐产品约束-012 指示灯体系)
| state | 灯色 | 说明 |
|---|---|---|
| connected | 绿 | 已连接 + N 工具(probe listTools 成功) |
| connecting | 黄 | 挂载中 / 重连中 |
| error | 红 | 不可达 / 失败(含原因) |
| disabled | 灰 | 无激活连接(未启用) |
## 实现步骤
1. QmtMcpManager(挂/卸/resync/probe/getStatus/dispose);
2. index.js 接线 + registerApi runtime
3. apimcp-status + sync-status.mcp + 热切换联动;
4. 客户端:SyncIndicators 四灯 + 设置页 MCP 状态条;
5. 构建(pnpm build)验证;
6. 宿主迁移(独立步骤,需老师确认 + editing-cordis-compositions 流程):卸载 dsh-skill-mcp-panel + 移除 cordis.patch.yml 受管块 → 重载 → 验证工具来自神之一手;
7. 验收。
## 风险与注意
- 同 serverName 冲突:插件实例与宿主受管块不可并存(先插件后移除,或先移除后插件接管,二选一窗口);
- ctx.plugin 子 fiber 需要 tools 服务:one-divine-lot inject 列表评估(对齐 mcp_router inject [webServer, tools]);
- dsh-mcp-client 初始连接失败不抛(failOnStartupError:false),但**重复 serverName 会抛** → 挂载错误必须 catch,插件其余功能不受影响。
@@ -0,0 +1,53 @@
# 迭代复盘:18-QMTBridgeMCP内建
> 复盘日期:2026-09-08 迭代状态:**验收通过(2026-09-08)** —— 口径:服务端自动化验收全绿 + MCP 工具端到端实测 + UI 接线核验(构建产物含新控件)+ 真实调用顺带确认(老师参与验收:重启宿主 / 指令修正 / MCP 实测)
> 关联需求:R-020(已定稿,2026-09-08)| 关联计划:PLAN-018
## 结果
迭代 18 达成(R-020 全范围):**QMT Bridge MCP 能力内建**——神之一手在 apply 内动态挂载 DSH 官方 @deepseek-ai/dsh-mcp-clientserverName 固定 QMT_Bridge_MCPurl 自动派生自激活连接 baseUrl+/mcp;挂载/切换/卸载/状态全由神之一手插件内管理);**三方依赖移除**dsh-skill-mcp-panel 卸载 + 宿主 cordis.patch.yml 受管块清空);MCP 生命周期随 QMT 连接配置增删改与激活切换联动;状态出口 ×2(设置页「QMT 连接配置」MCP 状态条 + 会话头部指示灯区新增「MCP」灯);失败不崩(failOnStartupError:false + reconnect + 挂载 catch)。REST 直连架构不变(技术约束-003)。
## 验收证据(2026-09-08 实测)
| # | 验收项 | 结果 |
|---|---|---|
| 1 | 移除宿主受管块后工具仍可用 | ✅ cordis.patch.yml 清空 []、panel 已卸载;mcp__QMT_Bridge_MCP__qmt_list_apis 实际调用成功(QMT Bridge v2.0.0 返回) |
| 2 | MCP url 随激活自动派生 | ✅ 100.110.38.78:8610/mcp(激活 baseUrl+/mcpQ3 |
| 3 | 激活切换联动 | ✅ 切 QMT_CYY → 192.168.3.43:8610/mcp connected;切回 GEMWIN 恢复 |
| 4 | 删除激活自动切默认 + MCP 重挂 | ✅ 删死地址连接 → 自动切默认 GEMWIN → MCP 恢复 connected + 10 工具 |
| 5 | 失败不崩(Q5) | ✅ 激活死地址 → state=errorfetch failed),插件其余功能正常 |
| 6 | mcp-statusrefresh=检测按钮语义) | ✅ refresh:true 即时探测 connected + 10 |
| 7 | sync-status.mcp 域(头部灯数据源) | ✅ state/url/mounted/toolCount/latencyMs/checkedAt 齐全 |
| 8 | 客户端接线 | ✅ build 产物含「MCP」指示灯 + 设置页「检测 MCP」按钮 + header.actions/settings.section 注册 |
| 9 | MCP 工具端到端实测 | ✅ qmt_kline 取 000333.SZ 近一个月日 K30 根 OHLCV,区间 -4.27% |
| 10 | 构建 | ✅ typecheck + pnpm build 通过(lib/mcp/QmtMcpManager.js 产出) |
## 过程事实
1. **需求讨论定稿**:老师指令(2026-09-08)→ R-020 登记 → Q1-Q6 逐项拍板(只挂激活连接 / 移除三方 / url 自动派生 / 双状态出口 / 失败重连采纳 / 不做通用 MCP 管理);
2. **老师指令修正(关键)**:AI 一度把「动态挂载官方 dsh-mcp-client」误读为要自研协议,先撤依赖后澄清——老师明确:不要的是**三方 dsh-skill-mcp-panel****官方 @deepseek-ai/dsh-mcp-client 可以依赖**;文档同步更正(R-020 / PLAN-018 / 迭代目标 / 索引);
3. **依赖可行性实证(PLAN-018 步骤 1)**@deepseek-ai/dsh-mcp-client@0.1.1-rc.2registry 有)加为 dependencies,从插件 lib 实证可 import,模块导出 {name:mcp-client, inject:[tools], apply, Config} → ctx.plugin() 动态挂载可行(mcp_router 同款路径);@modelcontextprotocol/sdk@1.30.0 供独立状态探测(dsh-mcp-client 不暴露状态 API);
4. **实现**src/mcp/QmtMcpManager.js(挂/卸/resync/probe/getStatus/dispose);index.js 接线(start + dispose + registerApi runtime);api/qmt-connections.jsmcp-status + activate/update/remove 三热切换点 resync);api/market.jssync-status.mcp 域);客户端 SyncIndicators(四灯 + MCP+ SettingsSectionMCP 状态条 + 检测按钮);tsdown 增 mcp 域入口;
5. **宿主迁移(老师执行,验收方确认)**cordis.patch.yml 受管块清空 + dsh-skill-mcp-panel 卸载 + 宿主重启 → 神之一手实例接管;
6. **验收**:服务端 8 项 + MCP 实测 + 构建全绿(上表);
7. **提交推送**c700f2f(迭代1816 files+ 9382ace(迭代17 遗留文档补提交)→ origin/main。
## 经验教训(复盘沉淀)
### 1. 「去掉对三方插件的依赖」要先问清:依赖边界到底指哪一层
AI 把老师「不希望依赖三方插件来加入 MCP 能力」误推到「也不能依赖 DSH 官方 mcp-client → 要自研 MCP 协议」,先动手撤了官方依赖、准备重造轮子;老师澄清后回撤。沉淀:**需求里「不依赖 X」先精确定位 X 是谁(管理面板 vs 执行插件),并确认官方能力是否允许复用;架构岔路口先问一句,别先动手拆**。
### 2. 动态挂载子插件 = ctx.plugin(module, config),前提是模块在插件依赖树内可解析
mcp_router 实证了「import 官方模块 → ctx.plugin()」路径;one-divine-lot 照做时关键前置是把官方模块加进自己的 dependencies(而非 peer 裸声明),并用 createRequire 从插件 lib 路径实证解析成功再动代码。沉淀:**动态挂载依赖模块时,先验证「插件运行路径能解析到模块」这个前置条件(技术约束-003 语义外的新增实践)**。
### 3. 状态查询与连接管理分离:dsh-mcp-client 不暴露状态 API
官方插件只管连接与工具注册,无状态查询口 → 用 @modelcontextprotocol/sdk 独立只读探测(initialize + tools/list 拿工具数/延迟),与官方自管连接互不干扰。沉淀:**外部能力库若不开状态口,用协议级只读探测补状态出口,不试图扒内部字段**。
### 4. 验收期间临时造数要小心「先清后补」
用死地址测失败语义时两次 add 同名 TEST-DEAD 未清理(返回结构读错导致 id 取空),留下两条残留连接;发现后立即清理、用正确返回结构重测。沉淀:**验收脚本对临时数据的增删要成对且校验返回值;测试前后核对连接列表与激活态**。
## 关联
- R-020(已定稿 → 已实现 → 本迭代验收通过);宿主三方 dsh-skill-mcp-panel 已移除(其「技能/MCP」设置菜单与受管块职责移交完成)
- 新增依赖:@deepseek-ai/dsh-mcp-client@0.1.1-rc.2、@modelcontextprotocol/sdk@1.30.0
- 若需新增设计约束条目(如「插件内建 MCP 客户端注册规范」)——本迭代未新增,留待后续讨论(R-020 定稿时标注"如需…于定稿时补充"未触发)
@@ -0,0 +1,19 @@
# 迭代 18QMT Bridge MCP 能力内建(去三方插件依赖)
## 目标(一句话)
神之一手插件在 apply 内动态挂载 DSH 官方 dsh-mcp-clientserverName 固定 QMT_Bridge_MCPurl 自动派生自激活连接 baseUrl+/mcp,挂载/切换/卸载/状态均由神之一手插件内管理),MCP 能力不再依赖三方 dsh-skill-mcp-panel(卸载其插件 + 移除宿主 cordis.patch.yml 受管块)。
## 目标描述
- 依据 R-0202026-09-08 定稿,Q1-Q6+ PLAN-018
- Q1 只挂激活连接;Q2 移除三方块由神之一手管理;Q3 url 自动派生;Q4 状态出口=设置页卡片检查 + 会话头部 MCP 指示灯;Q5 失败不崩+重连;Q6 不做通用 MCP 管理;
- REST 直连架构不变(技术约束-003)。
## 讨论过程
| 日期 | 轮次 | 要点 |
|---|---|---|
| 2026-09-08 | 需求讨论 Q1-Q6 | R-020 定稿(见 R-020.md |
| 2026-09-08 | 立项 | 老师:「启动,这个不是一个大需求」→ 快速迭代 |
| 2026-09-08 | 指令澄清 | 老师修正:不要的是三方 dsh-skill-mcp-panel;依赖官方 @deepseek-ai/dsh-mcp-client 没问题 → 方案定为官方库动态挂载 + 移除三方面板 |
@@ -0,0 +1,10 @@
# 迭代 18 验收标准:QMT Bridge MCP 能力内建
> 验收方法/验收目标见 PLAN-018「验收标准 / 验收方法」逐条。
1. 移除宿主 cordis.patch.yml QMT_Bridge_MCP 受管块后,模型仍能用 mcp__QMT_Bridge_MCP__qmt_* 工具(来源=神之一手动态挂载,工具集一致);
2. 激活连接切换 → MCP 实例 url 跟随;编辑/删除激活连接 → 同步重挂无残留;
3. 连接列表为空 → 不挂实例(mcp-status=disabled),其余功能不受影响;
4. QMT 不可达 → 插件不崩、MCP 灯红、恢复后自动重连绿灯;
5. 设置页连接卡片 MCP 状态 + 检查按钮;会话头部 MCP 指示灯四灯齐全;
6. 现有功能无回归;typecheck + build 通过。
@@ -0,0 +1,45 @@
# 技术实现方案:19-指示灯状态自适应探测
> 迭代编号:19 依据:R-022 + R-023 | 涉及约束:产品约束-012(指示灯体系)、技术约束-016、技术约束-020/021(本迭代新增)
## 方案总览
两只连接类状态缓存统一改「**结果驱动 setTimeout 自适应链**」:每次探测完成(成功/失败/异常)后按结果排下一轮。
| 维度 | QmtHealthMonitorR-022 | QmtMcpManagerR-023 |
|---|---|---|
| 健康/已连接 | intervalMs 默认 5 分钟(稳态,原设计) | intervalMs 默认 5 分钟 |
| 异常/未知/未连接 | retryMs 默认 10 秒快速重试 | retryMs 默认 10 秒快速重试 |
| 探测目标 | 激活 baseUrl + /healthGET | 激活 baseUrl + /mcpSDK Client 握手,只读) |
| 停止调度 | stop()mounted 守卫) | 无激活连接 / dispose() / unmount() |
| 并发防护 | _inFlight 去重 | _probing 去重 |
| 前端/API | 零改动(读缓存秒回) | 零改动(getStatus 读缓存秒回) |
## 实现步骤
### R-022 QmtHealthMonitorsrc/data-source/QmtHealthMonitor.js
1. 常量 HEALTH_RETRY_MS=10s;构造注入 { intervalMs, retryMs }(测试用短间隔,对齐 QuoteSync 先例);
2. 固定 setInterval(5min) → _nextDelay()/_scheduleNext()setTimeout 链,探测完成重排);
3. resolveActiveBaseUrl 移入 try:解析失败也落 healthy:false 缓存走快速重试
(原实现解析在 try 外,抛错场景灯会灰且不自动重试——顺带修复的隐性缺陷);
4. start() 立即探测不变;stop() 用 clearTimeout + mounted 守卫。
### R-023 QmtMcpManagersrc/mcp/QmtMcpManager.js
1. 常量 PROBE_RETRY_MS=10s;构造注入 { intervalMs, retryMs }
2. probe() 结果写缓存后 _scheduleNext()target 为空早退置 disabled 并取消定时器;
3. unmount() 取消定时器;dispose() 置 _disposed + 取消 + 卸载;_probing 防并发;
4. 探测本体(SDK Client connect/initialize/tools/list)与挂载/重连逻辑零改动。
### 回归
- scripts/test-health-monitor.mjs10 项:启动即探测/异常快速重试自动恢复/健康稳态不空转/stop 停止调度…)
- scripts/test-mcp-status.mjs12 项:无激活连接不调度/异常快速重试自动恢复/已连接稳态不空转/dispose 停止调度/节奏选择;
SDK 握手不可纯内存 mock,守护调度机制:stub probe 翻转状态 + 触达真实 _scheduleNext
## 涉及文件
- src/data-source/QmtHealthMonitor.js、src/mcp/QmtMcpManager.js(核心改动)
- scripts/test-health-monitor.mjs、scripts/test-mcp-status.mjs(新增回归)
- lib/*build 再产物,部署用)
@@ -0,0 +1,45 @@
# 迭代复盘:19-指示灯状态自适应探测
## 结论
达成(R-022 + R-023 全范围):会话头部两只「连接类」指示灯(QMT连接 / MCP)统一改为
**结果驱动自适应探测**——健康/已连接维持 5 分钟稳态(原设计不变),异常/未知每 10 秒快速重试;
QMT Bridge 与其 MCP 服务启动/恢复后两灯 ≤20s 自动回绿,不再等满 5 分钟 / 不再永不恢复。
前端指示灯与 sync-status / qmt-health / mcp-status 端点零改动(读缓存秒回语义不变)。
## 事实记录
### R-022 QMT 健康灯(src/data-source/QmtHealthMonitor.js
- 根因(已证实):健康缓存固定 5 分钟探测一次激活 /health;其余三灯有高频自愈回路
(持仓 10s / 行情 5s / MCP 客户端重连)。Bridge 在两次探测间启动/恢复时缓存停留在上次失败结果。
- 变更:setInterval(5min) → setTimeout 自适应链(_nextDelay/_scheduleNext,结果驱动);
HEALTH_RETRY_MS=10s;构造注入 { intervalMs, retryMs }resolveActiveBaseUrl 移入 try
(解析失败也落 healthy:false 走快速重试,原实现该场景灯灰且不自动重试);_inFlight 防并发保留。
### R-023 MCP 状态灯(src/mcp/QmtMcpManager.js
- 根因(已证实):状态缓存只在 挂载/切换/手动探测 时刷新,无定时回路;dsh-mcp-client 自带 reconnect
只恢复真实连接(fiber 层),不刷新插件状态缓存。
- 变更:probe() 结果写缓存后 _scheduleNext()PROBE_RETRY_MS=10s;构造注入 { intervalMs, retryMs }
无激活连接早退置 disabled 并取消定时器;unmount()/dispose() 停止调度;_probing 防并发;
探测本体与挂载/重连逻辑零改动。
### 验证
- scripts/test-health-monitor.mjs 10/10 绿;scripts/test-mcp-status.mjs 12/12 绿(调度机制守护);
- pnpm typecheck 0 错;pnpm build 通过(lib 两文件均含自适应逻辑);
- 存量回归 test-quote-sync / test-position-sync 均 0 退出。
## 待老师确认事项
1. R-022 / R-023 定稿确认(边界:健康/已连接稳态 5 分钟不变 / 异常 10s 重试 / 不新增配置 / API 前端零改动);
2. 真实环境人工验收(验收标准.md 第 8 条):停/启 QMT Bridge → QMT 灯与 MCP 灯 ≤20s 自动回绿、
点击仍即时探测、插件重载后缓存秒回。
## 经验沉淀(候选)
- 定时轮询类功能的恢复感知要与故障探测解耦:固定低频定时器做稳态,异常态必须用快速重试把「恢复」
也纳入自愈回路——灯类状态"看起来没生效"的根因通常是恢复路径的响应时延,而不是探测本身失效。
- 「外层有自管重连」不等于「状态缓存会自愈」:重连只恢复真实连接,展示层缓存若无自己的刷新回路,
必须单独加状态自适应探测(R-023 教训)。
@@ -0,0 +1,32 @@
# 迭代目标:19-指示灯状态自适应探测(QMT 健康灯 + MCP 灯)
## 目标
会话头部两只「连接类」指示灯(QMT连接 / MCP)改为**结果驱动自适应探测**:
健康/已连接时维持 5 分钟稳态探测(低开销),异常/未知时每 10 秒快速重试,
使 QMT Bridge 与其 MCP 服务启动/恢复后指示灯 ≤20s 自动回绿,不再等满 5 分钟。
## 目标描述
- **背景**:两只灯的数据分别来自 QmtHealthMonitor / QmtMcpManager 的内存状态缓存,刷新机制均为
固定/缺失低频定时回路——QMT 健康缓存固定每 5 分钟探测一次(R-022),MCP 状态缓存只在
挂载/切换/手动探测时刷新、无定时回路(R-023)。持仓(10s)/ 行情(5s)灯靠各自定时同步秒级自愈;
唯独两只连接灯恢复感知滞后,老师实测反馈「QMT连接灯一直没亮」「MCP 灯也不自动恢复」(2026-09-09)。
- **范围**:只改 QmtHealthMonitorR-022)与 QmtMcpManagerR-023)的探测调度;前端指示灯组件、
sync-status / qmt-health / mcp-status 端点零改动;健康稳态间隔 5 分钟不变;不新增配置项。
- **验收线**:两灯异常/未知态 ≤10s 重试;健康/已连接态维持 5 分钟不空转;服务恢复后缓存回正常态
沿快速重试被感知;无激活连接与 dispose 后停止调度。对应需求 R-022 + R-023。
## 目标讨论过程
1. 老师报告症状(2026-09-09):Bridge 启动后其他三灯自动点亮,QMT 灯不亮 → 定位为健康缓存固定 5 分钟探测。
2. AI 提议自适应节奏(健康 5min / 异常 10s),老师确认方向,迭代 19 实施(R-022)。
3. 老师复查发现 MCP 灯同样不自动恢复(R-023)→ 确认 QmtMcpManager 状态缓存无定时回路,
dsh-mcp-client 重连只恢复真实连接、不刷新本类缓存 → 同模式加自适应探测,并入本次迭代。
4. 决策点见 R-022 / R-023 决策记录。
## 对老师(项目主理人)的配合需求
- 确认 R-022 / R-023 定稿与迭代 19 验收(核实标准见验收标准.md);
- 真实环境人工验收:停/启 QMT Bridge → QMT 灯 ≤20s 回绿、MCP 灯 ≤20s 回绿;点击灯/「检测 MCP」仍即时探测;
插件重载后灯按缓存秒回并正常。
@@ -0,0 +1,29 @@
# 验收标准:19-指示灯状态自适应探测
## 验收标准线
| # | 标准 | 判定 |
|---|---|---|
| 1 | QMT 健康:异常/未知态按 retryMs(默认 10s)快速重试 | test-health-monitor [2]:≥2 次快速重试且缓存 healthy=false |
| 2 | QMT 健康:Bridge 恢复后 ≤ 一个重试周期缓存回 healthy=true(灯回绿),无需等 5 分钟 | 同脚本 [2] |
| 3 | QMT 健康:健康态维持稳态间隔(默认 5 分钟)不空转 | 同脚本 [3] |
| 4 | QMT 健康:stop() 后停止自动调度 | 同脚本 [4] |
| 5 | MCP:无激活连接置 disabled 且不调度(真实 probe 早退不触 SDK | test-mcp-status [1] |
| 6 | MCP:异常态按 retryMs 快速重试;MCP 服务恢复后自动探测到 connected(灯回绿) | 同脚本 [2] |
| 7 | MCP:已连接态按 intervalMs 稳态不空转;dispose() 后停止调度;_nextDelay 节奏正确 | 同脚本 [3][4][5] |
| 8 | 启动即探测 + 读缓存秒回语义不变(QMT 健康 / MCP 状态) | 两脚本 [1]sync-status/qmt-health/mcp-status 端点零改动 |
| 9 | 构建产物(lib/)含两处自适应逻辑;typecheck 0 错;存量不回归 | pnpm typecheck + pnpm buildtest-quote-sync / test-position-sync 通过 |
## 验收方法
- 自动化:node scripts/test-health-monitor.mjs10/10+ node scripts/test-mcp-status.mjs12/12)全绿;
pnpm typecheckpnpm build;存量回归脚本 0 退出。
- 人工(老师真实环境):重启插件后四灯状态秒回;停 QMT Bridge → QMT 灯 ≤10s 转红、MCP 灯 ≤10s 转红、
持仓/行情灯按 30s/15s 阈值转黄;**重新启动 QMT Bridge → QMT 灯与 MCP 灯 ≤20s 自动回绿**(本次修复核心);
点击 QMT 灯 /「检测 MCP」仍即时探测并刷新悬停详情。
## 验收目标
修复确认:QMT Bridge 及其 MCP 服务启动/恢复后,会话头部 QMT连接灯与 MCP 灯自动回绿(≤20s),
与其余两盏数据灯恢复节奏一致;健康/已连接稳态无额外探测开销;前端与 API 行为不变。
验收通过后 R-022 / R-023 定稿并归档(老师确认)。
@@ -0,0 +1,88 @@
# 技术实现方案:20-策略列设置拖拽排序
## 总体思路
在 ColumnSettingsPopover.jsx 内复用 Tab 设置(SettingsSection.jsx TabSettings)已验证的
**原生 HTML5 Drag & Drop** 模式:行 draggable + onDragStart/onDragOver/onDrop + 落点高亮 + drop 重排,
零第三方依赖。持久化沿用现有 strategy-columns/update 整表覆盖 RPC,把「保存」语义从手动按钮改为
每次变更(drop / 勾选 / ↑↓)立即提交。
## 改动点(单文件:src/client/views/ColumnSettingsPopover.jsx
### 1. 状态
- 已有 `dragKey`(未被使用,正好用于 DnD):当前被拖行的 column.key
- 新增 `overKey`:当前悬停落点的 column.key(用于高亮);
- `saving` 保持:正在写库时禁止再次拖动/勾选。
### 2. 持久化函数(替换原 save)
- 新增 `persist(nextList, msg)`:提交整表 `{key, visible}` → strategy-columns/update
成功 Toast 提示 + onSaved() 刷新;失败 Toast 错误 + 本地回滚(还原为服务端最新列配置)。
- 勾选显隐:切完立即 persist(不再等「保存」按钮);
- ↑↓ 箭头:移动完立即 persist;
- drop 重排:重排完立即 persist(对齐 Tab 设置)。
### 3. 拖拽事件(照搬 TabSettings 模式)
```jsx
<div key={c.key} draggable={!saving}
onDragStart={(e) => { setDragKey(c.key); e.dataTransfer.effectAllowed = 'move'; }}
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setOverKey(c.key); }}
onDragLeave={() => { if (overKey === c.key) setOverKey(null); }}
onDrop={(e) => { e.preventDefault(); handleDrop(c.key); }}
style={{ cursor: saving ? 'default' : 'grab',
background: overKey === c.key ? 'color-mix(in srgb, var(--dsw-alias-state-business-primary, #1565c0) 12%, transparent)' : 'transparent' }}>
<span></span>
<button></button><button></button>
<input type="checkbox" ... />
...
</div>
```
### 4. handleDrop(照搬 TabSettings 逻辑,key 定位)
```js
const handleDrop = (targetKey) => {
if (!dragKey || dragKey === targetKey) { setDragKey(null); setOverKey(null); return; }
const from = list.findIndex((c) => c.key === dragKey);
const to = list.findIndex((c) => c.key === targetKey);
if (from < 0 || to < 0) { setDragKey(null); setOverKey(null); return; }
const next = list.slice();
const [moved] = next.splice(from, 1);
next.splice(to, 0, moved);
setDragKey(null); setOverKey(null);
persist(next, `${moved.label}」已移动`, next);
};
```
### 5. UI 文案
- 标题下提示改为:「勾选显示列,按住 ≡ 拖动调整顺序;代码/名称/操作固定」;
- 底部按钮区:去掉「保存」,保留「关闭」;
- 每行最前加 ≡ 手柄(配色沿用 Tab 设置 `#999`/label-tertiary)。
## 边界与不做
- 服务端、API、表格渲染零改动(strategy-columns/update 已是整表覆盖语义);
- 固定列不参与排序,不进列表(现状维持);
- 不做方案 C(表格表头直接拖列头),不引入第三方 DnD 库;
- `saving` 期间禁用拖拽与勾选,防并发写。
## 回归
- scripts/test-r013-columns.mjs(列配置归一化/读写的服务端逻辑,未改动应全绿);
- npm run typecheck + buildtsdown 产物,弹层 JSX 变更需重编 web bundle);
- 页面人工验收:拖拽重排 / 落点高亮 / drop 即存 / ↑↓ 微调 / 刷新保持。
## 实现纪要(2026-09-10,老师验收反馈后修订)
> 老师反馈:初版按原方案做的是「整行高亮」(overKey → 目标行淡蓝底),拖动时无法判断会插入目标列的**前面还是后面**,
> 观感不符合习惯。定稿修订为 **间隙高亮线**:
- 落点判定:拖动悬停时读鼠标在目标行内的 **Y 坐标**getBoundingClientRect),上半 → 插入该行**之前**(线画行上缘),
下半 → 插入该行**之后**(线画行下缘);两行之间显示 3px 主色圆角高亮线(absolute 定位在行自身 padding 内,left/right 6
无负偏移无裁剪风险),指示精确插入位置。
- drop 判定:drop 事件内直接按坐标重算 before(不依赖 state 闭包,避免滞后);无操作原地(insertAt 等于原槽位)
跳过写库;移除后目标下标左移(to > from 时 to-1)。
- 状态:overKey(整行)→ overPos { idx, half }(间隙线锚点行 + 上下缘);保留 onDragEnd 清理防拖出弹层残留。
- 边界维持:固定列不参与、服务端零改动、↑↓ 兜底保留、立即持久化不变。
@@ -0,0 +1,47 @@
# 迭代复盘:20-策略列设置拖拽排序
## 结论
达成(R-024):策略 tab「列设置」弹层的列排序从 ↑↓ 箭头点按升级为**鼠标选中后拖动排序**,
复用 R-011 Tab 设置已验证的原生 HTML5 DnD 模式;老师 2026-09-10 页面确认交互直观,验收通过。
## 事实记录
### src/client/views/ColumnSettingsPopover.jsx(单文件改动,服务端零改动)
- 排序交互升级:每行新增 ≡ 拖拽手柄(整行 draggable),拖到目标位置松手即重排;↑↓ 箭头保留作兜底微调。
- **落点立即持久化**:勾选显隐 / 拖拽 / ↑↓ 任一操作立即调 strategy-columns/update 整表提交;弹层去掉「保存」
按钮只留「关闭」,Toast 反馈;saving 期间禁用拖拽与勾选防并发写;失败回滚到服务端最新配置。
- **落点指示修订(老师验收反馈)**:初版为整行高亮(overKey → 目标行淡蓝底),老师反馈无法判断插入目标列的
前/后;改为**行间间隙高亮线**——按鼠标在目标行内的 Y 坐标取上半/下半,决定线画在该行上缘(插其前)/下缘(插其后),
明确指示精确插入位置;drop 事件内按坐标重算(不依赖 state 闭包),拖回原位置不重复写库,onDragEnd 清理残留。
- 修复:间隙线渲染曾误将 style 对象直接作 React child`{gapLineStyle(i)}`),改为条件渲染 `<div style={gapLineStyle(i)}/>`
### 验证
- pnpm typecheck 0 错;pnpm build 通过(client bundle 185KB 含间隙线逻辑,grep 确认编译产物);
- test-r013-columns.mjs 回归:11 过 / 5 败 —— **与本次改动无关**git stash 基线验证同样 11/5);
失败根因:脚本仍按 R-013 时期期望「6 个基础列」,而 COLUMN_META 已有 10 列(R-015 新增涨停/跌停/今开/最高 4 个默认隐藏列),
测试期望未随迭代 13 更新的存量债务,不在本迭代范围,列为遗留事项。
### 文档
- docs/05-需求池/R-024.md:定稿 + 变更记录(整行高亮 → 间隙高亮线);
- docs/03-设计约束/UI交互约束.md:新增 UI约束-007(列表/弹层排序交互规范:间隙高亮线标准,Tab 设置是否统一待老师评估);
- docs/04-迭代记录/20-策略列设置拖拽排序/:迭代目标 + 技术实现方案(含实现纪要)+ 验收标准(10 项)。
## 遗留事项(待老师决策)
1. **test-r013-columns.mjs 期望过期**COLUMN_META 现 10 列(R-015 新增 4 行情列默认隐藏),脚本期望仍为 6 列,
默认列数相关 5 项断言失败。建议后续小迭代同步更新脚本期望(或重构为从 COLUMN_META 推导期望),本次未动。
2. **Tab 设置整行高亮是否统一为间隙高亮线**:UI约束-007 列设置弹层已用间隙线,Tab 设置(UI约束-003)仍为整行高亮;
老师反馈列设置「直观多了」——若 Tab 设置同样已存在「无法判断插入前/后」的观感问题,可同模式调整(独立小任务,本次未动)。
## 经验沉淀(候选)
- 排序类拖拽的落点指示,**间隙线(两行之间)比整行高亮更符合用户观感**:整行高亮只能表达「落在哪一行」,
无法表达「插入该行前还是后」;主流列表/表格拖拽(如 Notion、表格列排序)均用间隙指示。
- 服务端整表覆盖语义(updateStrategyColumns 整表提交)天然适配前端即时持久化,无需逐条增量接口;
「保存按钮」在每次变更即写库后失去存在意义(本迭代据此移除)。
- 回归脚本的期望值要随数据模型演进同步更新(test-r013-columns 教训:R-015 扩展 COLUMN_META 后脚本未跟随,
留下 5 项静默失败,直到本迭代回归才暴露)。
@@ -0,0 +1,30 @@
# 迭代目标:20-策略列设置拖拽排序
## 目标
策略 tab「列设置」弹层(ColumnSettingsPopover)的列排序交互从 **↑↓ 箭头点按** 升级为
**鼠标选中后拖动排序**(原生 HTML5 DnD),并对齐 Tab 设置的即时持久化体验。
## 目标描述
- **背景**:策略 tab 的可配置列(基础数据列 + 自定义字段列)在「列设置」弹层中管理,排序目前依赖
↑↓ 箭头(R-013 迭代 11 产物,当时刻意简化,代码注释自述「用箭头替代 DnD」)。老师反馈希望
改成鼠标选中后拖动排序(2026-09-10)。
- **结论**:复用 R-011 Tab 设置已验证的原生 HTML5 DnD 模式(draggable 行 + 落点高亮 + drop 重排),
零第三方依赖;落点立即持久化(strategy-columns/update 整表覆盖),弹层去掉「保存」按钮只留「关闭」。
- **范围**:仅改 src/client/views/ColumnSettingsPopover.jsx 单文件;服务端/API/表格渲染零改动;
固定列(代码/名称/操作/展开箭头)不参与排序,维持现状。
- **验收线**:拖拽可重排列顺序且落点高亮可见;drop 后立即写库(无需点保存);↑↓ 箭头仍可微调;
勾选显隐立即生效;刷新后列顺序保持。对应需求 R-024(已定稿)。
## 目标讨论过程
1. 老师反馈(2026-09-10):策略 tab 列设计可否做成鼠标选中后拖动排序。
2. AI 调研:Tab 设置(R-011)已实现同款原生 HTML5 DnDSettingsSection.jsx),可零依赖复用;
列设置弹层存在未使用的 dragKey 状态(原计划 DnD 的残留)。
3. 登记 R-024(讨论中)→ 方案草案 A/B/C → 老师确认方案 A(弹层内拖拽);
保存时机 / 箭头去留按 AI 推荐默认(立即持久化 + ↑↓ 保留兜底)。
## 对老师(项目主理人)的配合需求
- 无(纯前端交互优化,服务端零改动,验收走页面人工确认)。
@@ -0,0 +1,32 @@
# 验收标准:20-策略列设置拖拽排序
## 验收线(在哪里验收)
DSH Web GUI → 神之一手 → 任一策略 tab → 工具条「列设置」按钮 → 弹层。
页面侧人工验收;服务端逻辑零改动,跑 scripts/test-r013-columns.mjs 回归确认无破坏。
## 验收方法(逐项操作)
| # | 操作 | 预期 |
|---|---|---|
| 1 | 打开「列设置」弹层,鼠标按住某行 ≡ 手柄(含整行)拖动到另一行附近移动鼠标 | 目标行上缘/下缘显示**主色间隙高亮线**(插入线),明确指示落点在目标列之前还是之后,非整行高亮 |
| 2 | drop 后直接关闭弹层(不点任何保存) | 策略表格表头列顺序已按新顺序展示(立即持久化生效) |
| 3 | 刷新页面 / 重新打开弹层 | 列顺序保持拖拽后的结果(写库成功) |
| 4 | 点 ↑ / ↓ 箭头 | 仍可逐格微调,且立即持久化(关闭弹层后表格列顺序同步变化) |
| 5 | 勾选/取消勾选某一列 | 显隐立即生效(关闭弹层后,表格对应列隐藏/出现) |
| 6 | 拖动到目标行上半区 vs 下半区各松手一次 | 上半区 = 插入该目标列**之前**;下半区 = 插入该目标列**之后**(以落点线位置为准) |
| 7 | 拖动期间把行拖回原位置或原地松手 | 无异常,列顺序不变(不重复写库) |
| 8 | 固定列(代码/名称/操作) | 弹层列表不含固定列,不参与拖拽,恒显示(维持现状) |
| 9 | 自定义字段列(某策略配了 configSchema) | 与基础列一样可拖拽排序、可勾选显隐 |
| 10 | 保存中(快速连续拖动) | 无并发写异常;saving 期间拖拽/勾选禁用,Toast 提示保存结果 |
## 验收目标
- 功能正交:拖拽排序 / ↑↓ 微调 / 显隐勾选 三者并存且各自立即持久化;
- 零回归:scripts/test-r013-columns.mjs 全绿、typecheck + build 通过;
- 视觉一致性:≡ 手柄沿用 Tab 设置(UI约束-003)同套样式;落点为**间隙高亮线**(主色 token,UI约束-004 无硬编码色值);
- 服务端零改动确认:git diff 无 src/api / src/settings.js 变更。
## 判定
- 老师页面人工确认 10 项全过 + 服务端回归全绿 ⇒ 迭代 20 验收通过,R-024 归档至 已完成/ 并更新索引实现状态。
@@ -0,0 +1,84 @@
# 技术实现方案:21-Tab设置排序间隙高亮统一
## 总体思路
把 SettingsSection.jsx 的 TabSettings 组件落点指示从「整行高亮(overId → 行背景 color-mix 12%)」
改为「行间间隙高亮线(overPos { idx, half } → 目标行上缘/下缘 3px 主色线)」,与 ColumnSettingsPopover.jsx
(R-024)逐点对齐;其余交互(≡ 手柄、开关、立即持久化、徽标、saving 禁用)不动。
## 改动点(单文件:src/client/views/SettingsSection.jsx,仅 TabSettings 组件)
### 1. 状态
- `overId`(整行高亮锚点)→ `overPos { idx, half }`(间隙线锚点行下标 + 上/下缘);
- `dragId` 保留。
### 2. 悬停判定(与 R-024 全同)
```jsx
const handleOver = (e, index) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const rect = e.currentTarget.getBoundingClientRect();
const half = e.clientY - rect.top < rect.height / 2 ? 'top' : 'bottom';
setOverPos({ idx: index, half });
};
```
### 3. drop(事件内按坐标重算,不依赖 state 闭包)
```jsx
const handleDrop = async (e, index) => {
e.preventDefault();
if (!tabs || !dragId) { setDragId(null); setOverPos(null); return; }
const rect = e.currentTarget.getBoundingClientRect();
const before = e.clientY - rect.top < rect.height / 2;
let to = before ? index : index + 1;
const from = tabs.findIndex((x) => x.id === dragId);
if (from < 0 || to < 0 || to > tabs.length) { setDragId(null); setOverPos(null); return; }
if (to === from || to === from + 1) { setDragId(null); setOverPos(null); return; } // 原地跳过写库
const next = tabs.slice();
const [moved] = next.splice(from, 1);
const insertAt = to > from ? to - 1 : to;
next.splice(insertAt, 0, moved);
const ordered = next.map((x, i) => ({ ...x, order: i }));
setDragId(null);
setOverPos(null);
await persist(ordered, '「' + moved.name + '」已移动');
};
```
### 4. 行渲染(tr 上挂间隙线样式)
tr 为表格行,直接内嵌 div 会破坏表格结构(tr 只允许 td/th)——**间隙线以 td 为定位锚点绘制**:
首选在每个 td 内各画一段 3px 高亮线(结构合规、视觉连续),实现纪要记录最终选型。
```jsx
<tr
key={t.id}
draggable={!saving}
onDragStart={(e) => { setDragId(t.id); setOverPos(null); e.dataTransfer.effectAllowed = 'move'; }}
onDragOver={(e) => handleOver(e, i)}
onDrop={(e) => handleDrop(e, i)}
onDragEnd={() => { setDragId(null); setOverPos(null); }}
style={{ cursor: saving ? 'default' : 'grab' }}
>
<td style={{ position: 'relative', padding: '8px 6px' }}> {gapLine(i, 'td1')} </td>
...
</tr>
```
间隙线样式(与 R-024 全同):absolute、left/right 内缩 6、height 3、borderRadius 2、
背景 primary tokentop:0(插前)/ bottom:0(插后)。
## 边界与不做
- 服务端/API/表格列结构/持久化逻辑零改动;
- 不动「策略分组」子 tab(该处无排序交互);
- 不引入第三方 DnD 库。
## 回归
- pnpm typecheck + buildSettingsSection 变更需重编 web bundle);
- 页面人工验收:Tab 设置拖动 → 间隙线位置正确(上=前/下=后)、整行高亮消失、drop 即存、
拖回原位不重复写库、刷新页面后顺序保持。
@@ -0,0 +1,26 @@
# 迭代目标:21-Tab设置排序间隙高亮统一
## 目标
设置页「Tab 设置」的分组 tab 拖拽排序落点指示,从**整行高亮**统一为**行间间隙高亮线**,
与策略 tab 列设置弹层(R-024/UI约束-007)完全一致——明确指示插入目标行的前/后。
## 目标描述
- **背景**:迭代 20 复盘遗留第 2 项——R-024 将列设置弹层落点改为间隙高亮线后老师确认「直观多了」,
但设置页 Tab 设置(R-011/UI约束-003)仍是整行高亮(overId → 目标行淡蓝底),无法判断插入前/后,
两处观感不一致。2026-09-10 老师指令:Tab 设置也统一为间隙高亮线形式。
- **范围**:仅改 src/client/views/SettingsSection.jsx 的 TabSettings 组件落点指示;表格结构、≡ 手柄、
显隐开关、立即持久化(tabs/update)、徽标、saving 禁用全部维持不变;服务端/API 零改动。
- **验收线**:拖动时目标行上缘/下缘显示主色间隙线(上半区=插前、下半区=插后);整行高亮消失;
拖回原位不重复写库;drop 后立即持久化。对应需求 R-025(已定稿)。
## 目标讨论过程
1. 迭代 20 复盘(2026-09-10)列遗留第 2 项:Tab 设置整行高亮是否统一为间隙线,留待老师评估。
2. 老师指令(2026-09-10):设置页分组 tab 排序也改上面(列设置弹层)的拖动高亮形式 → 登记 R-025 已定稿,
迭代 21 实施。
## 对老师(项目主理人)的配合需求
- 无(纯前端交互统一,服务端零改动,验收走页面人工确认)。
@@ -0,0 +1,27 @@
# 验收标准:21-Tab设置排序间隙高亮统一
## 验收线(在哪里验收)
DSH Web GUI → 设置页 → 「Tab 设置」子 tab → 拖动任一标签行排序。
页面侧人工验收;服务端逻辑零改动。
## 验收方法(逐项操作)
| # | 操作 | 预期 |
|---|---|---|
| 1 | 打开「Tab 设置」,按住某行 ≡ 手柄(含整行)拖动到另一行附近移动鼠标 | 目标行上缘/下缘显示**主色间隙高亮线**(插入线),明确指示落点在目标 tab 之前还是之后;原整行淡蓝高亮消失 |
| 2 | 拖动到目标行上半区 vs 下半区各松手一次 | 上半区 = 插入该 tab **之前**;下半区 = 插入该 tab **之后**(以落点线位置为准) |
| 3 | drop 后落点立即生效 | Toast「已移动」,刷新页面后 tab 顺序保持(tabs/update 已写库) |
| 4 | 拖动期间把行拖回原位置或原地松手 | 无异常,顺序不变,且不触发重复写库 |
| 5 | 既有交互回归 | ≡ 手柄、显示/隐藏开关、内置/策略徽标、saving 期间拖拽禁用——全部维持原行为 |
| 6 | 策略分组子 tab | 无排序交互,不受影响(仍为 新增/重命名/删除) |
## 验收目标
- 功能对准:Tab 设置落点与列设置弹层(R-024)**同一套间隙线交互**,观感一致;
- 零回归:既有 tabs 持久化(整表 order + visible)、显隐开关、策略改名联动 name join 不受影响;
- typecheck + build 通过;无新增硬编码色值(UI约束-004)。
## 判定
- 老师页面人工确认 6 项全过 + build 通过 ⇒ 迭代 21 验收通过,R-025 归档至 已完成/ 并更新索引实现状态。
@@ -0,0 +1,164 @@
# 迭代 22 UI 交互设计:字段配置入口迁移 + 策略计算字段
> 依据:**R-027(重构·第一阶段,已定稿)** + **R-026(计算字段·第二阶段,已定稿)** + PLAN-019 日期:2026-09-10
> 状态:**已定稿**(D-1D-7 老师逐项拍板;2026-09-10 因入口迁移(R-027)同步修订;D-8~D-9 为 AI 默认项,老师可否决)
> 视觉基线:UI约束-004--dsw-* token)、UI约束-006ExpandChevron)、UI约束-007(间隙高亮线);UI约束-005 待随本次修订
> 复用:ColumnSettingsPopover(列设置)、StrategyFieldsEditor(改造为弹层内容)、ToastFieldCellEditor 不参与(formula 不可编辑)
## 1. 交互总览(一句话)
策略 tab 顶部**「字段配置」+「列设置」**两个按钮:字段配置管**字段定义**(含计算字段的公式),列设置管**列的显隐与顺序**;
字段定义保存后表格列自动跟随;计算字段列**只读**展示服务端实时算出的值(缺数据 `—`)。
> 本次修订要点(R-027):字段配置入口从「设置页 → 策略分组 → 策略行展开」**迁到策略 tab 内**,与列设置并列;设置页不再有「策略分组」子 tab。
## 2. 控件布局(示意)
### 2.1 策略 tab 顶栏
```
┌─ 策略 tab:网格超市 ─────────────────────────────────────────────┐
│ 持仓 13 只 · 今日盈亏 … [历史持仓 ▾] [字段配置] [列设置] │
│ ┌ 持仓表 ─────────────────────────────────────────────────────┐ │
│ │ | | 代码 | 名称 | 现价 | ƒ 盈亏比例 | … | 操作 | │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
- 两按钮同层级、同视觉族(现有「列设置」按钮样式),**字段配置**在左、**列设置**在右(先定义字段、再排列,顺序符合操作动线)。
### 2.2 字段配置弹层(列表 + 表单)
```
┌─ 字段配置:网格超市 ─────────────────────────────────────────┐
│ 自定义字段(5) [+ 添加字段] │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 展示名 key 类型 公式/默认 单位 操作 │ │
│ │ 网格上边界 grid_ceiling 数字 默认 11 元 编辑 删除│
│ │ 基准值 current_base 数字 默认 10 元 编辑 删除│
│ │ ƒ 盈亏比例 pctProfit 计算 (现价 ÷ 成本价 - 1)×100 % 编辑 删除│
│ └──────────────────────────────────────────────────────────┘ │
│ 字段增删后,持仓表列自动跟随(新增列默认追加末尾、删除列自动移除)│
│ [关闭] [保存字段] │
└────────────────────────────────────────────────────────────────┘
```
- 弹层形态(D-9):锚定弹层(与列设置同源视觉),宽度约 520px、最大高度约 70vh、内容超出内部滚动;
- **保存字段**按钮在弹层底部(右对齐,主色实心);**关闭**在左(次按钮);
- 有未保存改动时点「关闭」/ 外点/Esc → 二次确认「有未保存的字段改动,确定放弃?」(D-8)。
### 2.3 字段表单 · 类型 = 计算(第二阶段)
```
┌─ 添加字段 / 编辑字段 ────────────────────────────────────────────┐
│ 展示名 [盈亏比例 ] 标识 [pctProfit ] 类型 [计算 ▾] │
│ 单位 [% ] 小数位 [2] │
│ ┌ 公式 ───────────────────────────────────────────────────────┐ │
│ │ (现价 ÷ 成本价 - 1) × 100 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ [试算] 试算:平安银行 000001.SZ → 6.67 % │
│ 行情 [现价] [昨收] [今开] [最高] [最低] [成交量] [成交额] │
│ 合约 [涨停价] [跌停价] 持仓 [份额] [成本价] [最后成交价] │
│ 自定义 [网格大小] [基准值] [网格交易量] ← 点 tag 插入公式 │
│ 可用变量 行情:现价 昨收 今开 最高 最低 成交量 成交额 │
│ 合约:涨停价 跌停价 │
│ 持仓:份额 成本价 最后成交价 │
│ 自定义:网格大小 基准值 网格交易量 ← 本策略手填字段 │
│ [取消] [确定] │
└─────────────────────────────────────────────────────────────────┘
```
> 其余四类(文本/数字/布尔/枚举)表单形态不变,仅「类型」下拉多一项「计算」;选「计算」时隐藏默认值、显示公式区与小数位。
### 2.4 策略持仓表(只读计算列)
```
| | 代码 | 名称 | 现价 | 成本价 | 份额 | ƒ 盈亏比例 | ƒ 浮动盈亏 | 操作 |
|▸| 000001.SZ | 平安银行 | 11.20 | 10.50 | 1000 | 6.67 % | 700.00 元 | 全部移入/移出 |
```
- 表头 `ƒ`tertiary 色,`title="计算字段(只读)"`)+ 列名;悬停列头或单元格 title 显示公式原文(`= (现价 ÷ 成本价 - 1) × 100`);
- 单元格点击**不进入内联编辑**;手填字段列的行为不变(点击即编辑)。
### 2.5 列设置弹层(ColumnSettingsPopover
- 计算字段与手填字段同列混排(名称显示 `ƒ 盈亏比例`);显隐勾选 / 拖拽排序 / 间隙高亮线 / 立即持久化**零改动**。
## 3. 字段配置弹层状态机
| 状态 | 外观 | 交互 |
|---|---|---|
| 初始 | 列表(含公式原文列)+ 底部「保存字段」(无改动时禁用/次色) | 可添加/编辑/删除字段 |
| 有未保存改动 | 「保存字段」主色可点;关闭类动作触发放弃确认 | 保存 → 调 `strategies/schema-update` |
| 保存中 | 「保存字段」disabled(文案「保存中…」) | 防重入 |
| 保存成功 | Toast「字段已保存」+ 弹层关闭(或保持打开并刷新)+ 表格列同步跟随 | — |
| 保存失败 | Toast 错误 + 弹层不关闭 + 改动保留 | 可重试 |
| 公式校验失败(计算字段) | 表单内公式框**描红** + 错误行(`语法错误:第 8 字符处缺少运算符` / `未知变量「网格距」` | 表单不关闭,光标回公式框 |
| 试算中 | 「试算」disabled(「试算中…」) | 防重入 |
| 试算成功/失败 | 行内 `试算:平安银行 000001.SZ → 6.67 %` / error 色原因(`该策略当前无持仓` | 不阻塞保存 |
## 4. 交互细节
1. **打开入口**:点策略 tab 顶栏「字段配置」→ 弹层打开并显示**本策略**字段定义(各自 tab 管自己,Q4);数据来源沿用 `strategies` 端点(取当前策略 configSchema)。
2. **字段列表**:列 = 展示名 / key / 类型 / 公式或默认值 / 单位 / 操作(编辑、删除);计算字段无默认值,公式列等宽、单行省略、悬停全文。
3. **添加/编辑字段**:沿用现有表单(展示名 → 自动生成 key、类型下拉、枚举选项、默认值、单位)**新增**「计算」分支(见 2.3);`类型` 下拉:文本 / 数字 / 布尔 / 枚举 / **计算**
4. **删除字段**:列表内即时移除(弹层内草稿),**保存**后生效;保存后该列自动消失(归一化跟随),行内残留旧值按 R-013 既有防御隐藏。
5. **保存**:调新增单策略端点 `strategies/schema-update { strategyId, configSchema }`;成功后 Toast + 刷新当前策略数据(列与字段同步);**不写 values 列**、不动份额账本。
6. **变量插入**(计算字段,2026-09-10 老师反馈改为标签平铺):公式框下方按分组(行情 / 合约 / 持仓 / 自定义字段)**平铺变量 tag**(胶囊形、悬停显示释义),点选即插入到公式框光标处(无光标信息则追加末尾并补空格);不自弹下拉菜单。
7. **变量命名规则**(D-1 已拍板):内置变量固定中文名(现价/昨收/今开/最高/最低/成交量/成交额/涨停价/跌停价/份额/成本价/最后成交价);自定义字段以其**展示名**作变量名。
8. **保存校验(服务端)**:① 公式非空且语法合法;② 变量全部存在;③ 同策略字段展示名唯一、且不与内置变量名冲突;④ 仅引用**非公式字段**(目录层排除)。失败 → Toast + 公式框描红 + 定位文案。
9. **试算**:服务端取该策略一行真实持仓(优先份额 > 0 首行)→ 变量目录取数 → 求值 → 返回 {`code`,`name`,`value`};前端按单位/小数位格式化显示。
10. **持仓表只读渲染**:按小数位 + 单位格式化为 `6.67 %` / `700.00 元`;悬停显示公式原文。
11. **刷新语义**(已知取舍):计算列随 `strategy-positions` 轮询刷新(行情服务端 5s 更新,列值刷新粒度 = 接口轮询周期);不与 `PriceCell` 实时高亮联动。
12. **历史持仓行**:计算列显示 `—`(清仓行无「现价」语义,不做历史时点计算)。
13. **旧公式引用已删变量**D-4 已拍板):字段列表给 `⚠` 标记 + 悬停原因;运行期该列显示 `—`
14. **设置页**(第一阶段):不再有「策略分组」子 tab;Tab 设置里策略行与内置行同权(显隐 + 拖拽排序 + 间隙高亮线),**不提供**新增/重命名/删除任何入口。
15. **键盘/可及**:Esc / 外点关闭弹层(有未保存改动则确认);`ƒ``⚠` 标记均带 title 文本(不单靠颜色表意)。
## 5. 空态与边界
| 场景 | UI |
|---|---|
| 本策略尚无字段 | 弹层列表空态:「尚未配置字段 —— 点击「+ 添加字段」为本策略添加(该策略下每个持仓按此定义填写值)」 |
| 该策略无手填字段(配计算字段时) | 变量菜单「自定义」分组空态:「本策略暂无手填字段」 |
| 该策略当前无持仓 | 试算行内:「暂无可试算的持仓数据(保存不受影响)」 |
| 行情缺失 | 计算列单元格 `—`tertiary 色,不报错) |
| 计算失败(除零/非有限) | 单元格 `—` |
| 公式为空 / 未知变量 / 变量名冲突 | 拒绝保存 + 公式框描红 + 具体文案(见 §4.8) |
| 弹层有未保存改动时关闭 | 二次确认「有未保存的字段改动,确定放弃?」(D-8,AI 默认) |
| 未分配持仓 / 无 holding 锚点行 | 计算列照常显示(公式只用行情 + 持仓账本,不依赖 holding 锚点) |
| 设置页 | 仅「Tab 设置 / QMT 连接配置」两个子 tab |
## 6. 组件落点(技术方案阶段细化)
- `src/client/views/StrategyTab.jsx`:顶栏新增「字段配置」按钮(列设置旁)+ 弹层开关;计算列只读渲染(`ƒ` 表头 + title + 格式化 + `—`);
- `src/client/views/FieldConfigDialog.jsx`(**新增**):字段配置弹层容器(宽度/滚动/底部按钮/放弃确认),内容复用改造后的 StrategyFieldsEditor
- `src/client/views/StrategyFieldsEditor.jsx`(**改造**):作为弹层内容;保存回调改单策略端点;类型下拉加「计算」+ 公式分支(公式框 / 变量选择器 / 试算 / 小数位);
- `src/client/views/SettingsSection.jsx`(**改造**):移除「策略分组」子 tab(含 CRUD 与确认弹窗);
- `src/client/views/ColumnSettingsPopover.jsx`:列名 `ƒ` 前缀(仅文案);
- 服务端(技术方案阶段):`strategies` 常量 + tabs 策略条目固定 + 存储归一化;`strategies/schema-update` 单策略字段端点;变量目录 + 表达式引擎 + `strategy-positions` 现算 + 校验/试算端点;
- 零改动面:`FieldCellEditor`(formula 不进入)、列配置持久化与归一化、历史持仓路径、份额账本与交易归属。
## 7. 已拍板决策(2026-09-10 老师逐项确认)
| # | 问题 | 结论 |
|---|---|---|
| D-1 | 变量命名细则 | 内置固定中文名;自定义字段用**展示名**(新增唯一性 + 不与内置名冲突校验) |
| D-2 | 计算列视觉区分 | 表头 `ƒ` 前缀 + 悬停看公式;不加背景色 |
| D-3 | 试算预览 | 本期做 |
| D-4 | 已删变量的提示 | 字段列表 `⚠` 标记 + 悬停原因;运行期 `—` |
| D-5 | 小数位 | 数字输入 0-4,默认 2 |
| D-6 | 公式框能力 | 等宽字体 + 选择器插入;不做实时高亮/自动补全 |
| D-7 | 约束文档更正 | 一并更正 UI约束-005 + 新增计算字段约束条目 |
| D-8 | 弹层未保存改动的关闭行为 | **AI 默认**:二次确认放弃;老师可否决(备选:直接丢弃 / 禁止关闭) |
| D-9 | 弹层形态 | **AI 默认**:锚定弹层(与列设置同源视觉),~520px 宽、70vh 高、内部滚动;老师可否决(备选:右侧抽屉 / 居中模态) |
## 8. 约束与历史需求联动(实施时执行)
- **约束修订**R-027):产品约束-002/003/004(标签体系 / 标签自定义 + 显示开关场景收敛为「固定两策略 + 字段自定义」)、产品约束-009(Tab 统一管理中「策略条目动态化」条款)、UI约束-002(设置页子 tab 构成:移除策略分组)、UI约束-003(Tab 设置:策略行不再随 CRUD 增删)、**UI约束-005**(字段配置 UI 入口迁移 + 更正为「表格列化 + 单元格内联编辑」现状);
- **约束新增**R-026):拟 UI约束-008(计算字段表单 + 只读计算列)、产品功能约束(计算字段语义:不落库 / 只读 / 仅非公式变量)、技术方案约束(变量目录 + 表达式引擎 + 服务端现算);
- **历史需求取代**:R-003(策略 CRUD)→ 退役;R-011(Tab 统一管理)→ 策略条目动态化部分取代、显隐排序机制保留;R-013 → 字段模型/列机制保留、入口条款取代(均由 R-027 取代,索引标注追溯)。
## 9. 待确认/顺带发现
- 无(D-8 / D-9 为 AI 默认项,老师如有异议直接改判)。
@@ -0,0 +1,250 @@
# 技术实现方案:22-策略计算字段与策略 tab 静态化
> 依据:R-027(重构·第一阶段)+ R-026(计算字段·第二阶段)+ PLAN-019 + `UI交互设计.md`(已定稿)
> 日期:2026-09-10 | 状态:**方案稿(待老师过审后进入实现)**
> 约束依据:技术约束-003(REST 直连)/011(回归独立数据目录)/012(策略定义存储)/015configSchema)、UI约束-004/005/006/007;本次修订与新增见 §7
## 0. 总览
| 阶段 | 需求 | 一句话 | 主要落点 |
|---|---|---|---|
| 一 | R-027 | 策略 tab 静态化 + 字段配置入口迁到策略 tab 内 | `settings.js` / `api/strategies.js` / `SettingsSection.jsx` / `StrategyTab.jsx` / `StrategyFieldsEditor.jsx` |
| 二 | R-026 | 新增「计算(formula)」字段类型:中文变量公式引用服务端缓存数据集现算 | `formula/`(新)/ `PositionManager` 或 API 层附着 / `StrategyTab.jsx` / `StrategyFieldsEditor.jsx` |
**关键设计决策(本方案定)**
1. **计算在服务端 API 层附着**`strategy-positions` 返回前),不改 `PositionManager` 的份额语义;
2. **聚合口径只读内存缓存**QuoteHub.quotes / instruments),即使 miss 也**不做读穿透**——`strategy-positions` 是高频读路径,不引入网络往返;行情新鲜度由 QuoteSync 5s 覆盖持仓 code 保证;
3. **无 formula 字段的策略零开销**(不取行情、不进引擎);
4. 存储真相源单一:字段定义迁到 `strategyFields``strategies` 收窄为内置身份表(读取兼容旧定义)。
## 1. 第一阶段:静态化与入口迁移(R-027)
### 1.1 数据模型(src/settings.js
**常量(新增/收窄)**
```js
/** 内置策略(静态,R-027):身份固定,不可增删改名 */
export const BUILTIN_STRATEGIES = [
{ id: 'grid-supermarket', name: '网格超市' },
{ id: 'manual-t', name: '手动做T' },
];
```
- `DEFAULT_STRATEGIES` 退役(由 `BUILTIN_STRATEGIES` 取代,内部先行别名过渡)。
**schema 变更**
```js
strategies: z.array(z.object({ id, name, configSchema })).default([]), // 收窄:仅身份表,改为内置两项(兼容读取)
strategyFields: z.dict(z.array(fieldSchema)).default({}), // 新增:策略字段定义真相源 { [strategyId]: configSchema }
strategyColumns: // 不变
```
- `fieldSchema``{ key, label, type(text|number|boolean|enum|formula), enum[], def, unit, formula, decimals }``formula`/`decimals` 为第二阶段新增,见 §2.1)。
**读取归一化(单一真相源 + 旧数据兼容)**
```js
export function getStrategies(scope) {
const v = scope?.get() ?? {};
const legacy = new Map((v.strategies ?? []).map(s => [s.id, s.configSchema])); // 旧:定义挂 strategies[]
return BUILTIN_STRATEGIES.map(s => ({
...s,
configSchema: Array.isArray(v.strategyFields?.[s.id])
? v.strategyFields[s.id]
: (Array.isArray(legacy.get(s.id)) ? legacy.get(s.id) : []), // 旧值回填视图(首次保存即落到 strategyFields
}));
}
export function getStrategyFields(scope, strategyId) { /* 同上单策略版,未知 id → [] */ }
export async function updateStrategyFields(scope, strategyId, configSchema) {
assertBuiltinStrategy(strategyId); // 非内置 → 抛 strategy-not-found
const cur = scope.get() ?? {};
await scope.update({ ...cur, strategyFields: { ...(cur.strategyFields ?? {}), [strategyId]: normalizeFields(configSchema) } });
}
```
- 兼容:存量 `strategies[].configSchema`(本机 = 网格超市 5 字段 / 手动做T 1 字段)读取时回填,前端表单提交**全量** → 首次保存即写入 `strategyFields`**无需迁移脚本**
- 自建策略(本机实测无):读取时忽略,其 `strategy_holdings` 行保留在库(不删数据)。
**tabs 归一化(`normalizeTabs` 收窄)**
- 默认序列 = 内置 tab(3)+ 内置策略 tab(2);
- 读取:按存量 `tabs``visible/order` 应用偏好,**丢弃** `kind='strategy'``refId` 不在 `BUILTIN_STRATEGIES` 中的条目(自建策略 tab 退役);
- 新增策略 tab 条目若存量缺失(旧数据无策略 tab)→ 按默认序补齐;
- 删除:`appendStrategyTab` / `removeStrategyTab`(不再有增删场景)。
**退役清单(实施确认)**`addStrategy` / `removeStrategy` / `renameStrategy` / `updateStrategies` / `appendStrategyTab` / `removeStrategyTab` / `DEFAULT_STRATEGIES` 已删除;`generateStrategyId` / `PINYIN_MAP` **保留**QMT 连接 id 生成 `generateQmtConnectionId` 仍依赖,本轮不动)。
### 1.2 API 变更(src/api/strategies.js
| 端点 | 处理 |
|---|---|
| `strategies` | 保留(返回内置两项 + 各自 configSchema |
| `strategies/add` | **退役** |
| `strategies/remove` | **退役** |
| `strategies/update` | **退役**(整表写入不再需要;重命名随静态化取消) |
| `strategies/schema-update` | **新增**`{ strategyId, configSchema }` → 服务端校验(§2.5)→ `updateStrategyFields` → 返回该策略 `{ id, name, configSchema }` |
| `tabs` / `tabs/update` | 保留(显隐 + 排序为唯一可变项) |
| `strategy-positions` | 保留 + 第二阶段附 `computed`(§2.4 |
| `formula/trial` | **新增**(第二阶段,§2.5 |
- `STRATEGY_METHODS` 集合同步增删;`api/index.js` 头部注释同步。
- `handleStrategy` 解构补 `marketHub, dataSource`(计算字段取数用)。
### 1.3 设置页收敛(SettingsSection.jsx
- 删除子 tab 项 `{ key:'strategies', label:'策略分组' }` 与渲染分支、`StrategyGroupSettings` 组件、`StrategyFieldsEditor` 引入、新增/重命名/删除逻辑与删除确认弹窗(L208–436 区块);
- 子 tab 仅剩 `tabs` / `qmt``activeTab` 初值维持 `'tabs'`
### 1.4 字段配置弹层(前端)
**`src/client/views/FieldConfigDialog.jsx`(新增)**
- props`{ strategyId, strategyName, configSchema, onSaved, onClose }`
- 形态:锚定弹层(`width 520``maxHeight 70vh`、内部滚动),视觉沿用 `ColumnSettingsPopover`
- 底部动作:`[关闭]`(次) + `[保存字段]`(主,saving 时 disabled);
- **未保存改动**`dirty`)时 关闭/外点/Esc → 二次确认「有未保存的字段改动,确定放弃?」(D-8)。
**`src/client/views/StrategyFieldsEditor.jsx`(改造)**
- 由「设置页整表保存」改为「弹层内容 + 单策略保存」:`onSave(fields)` → 调 `one-divine-lot/strategies/schema-update { strategyId, configSchema: fields }`
- 类型下拉增 `计算`(第二阶段);`dirty` 状态上报父弹层(用于放弃确认)。
**`src/client/views/StrategyTab.jsx`(改造)**
- 顶栏「列设置」左侧新增「字段配置」按钮 → 打开 `FieldConfigDialog``onSaved``load()`(列与数据同步刷新);
- configSchema 复用既有加载(L236239)。
## 2. 第二阶段:计算字段(R-026)
### 2.1 字段模型扩展(settings.js fieldSchema
```js
{ key, label, type: '…'|'formula', enum: [], def, unit: '',
formula: z.string().default(''), // 仅 type='formula':表达式串(中文变量)
decimals: z.number().default(2) } // 仅 type='formula':显示小数位 0-4
```
- `normalizeFields``decimals` 夹取 04 整数;`formula` 去首尾空白;`type='formula'` 时忽略 `def`
- 值不落库:`strategy_holdings.values` 与 formula 字段无关(读写路径零改动)。
### 2.2 公式引擎(`src/formula/evaluator.js`,纯函数零依赖)
```js
validateFormula(expr, allowedVars) { ok: true, vars: Set<string> }
| { ok: false, error: { code: 'empty'|'syntax'|'unknown-var'|'unknown-func'|'arity', message, position } }
evaluateFormula(expr, ctx) number | null // ctx: { [中文变量名]: number|null }
evaluateFormulaCompiled(ast, ctx) number | null // 编译一次多行复用(性能)
```
- **词法**:数字(整数/小数)、标识符(`\p{L}` 起头,含中文,可含数字/下划线)、运算符 `+ - * / × ÷ ( ) ,``×``*``÷``/` 归一化);
- **语法优先级**:括号 > 一元负号 > `* / / ÷` > `+ -`;函数调用 `name(arg, …)`
- **内置函数**`round(x[, n=0])``abs(x)``min(a, b, …)``max(a, b, …)`
- **求值语义**:引用的任一变量为 `null/undefined/NaN` → 整式返回 `null`(缺值短路,不产出 NaN);除零、非法运算、结果非有限 → `null`
- **安全**:自写解析,不使用 `eval/Function`(技术约束新增条目,§7)。
### 2.3 变量目录(`src/formula/variables.js`
```js
export const VARIABLE_GROUPS = [
{ group: '行情', source: 'quote', items: [
{ name: '现价', field: 'lastPrice', desc: '最新成交价' },
{ name: '昨收', field: 'lastClose' }, { name: '今开', field: 'open' },
{ name: '最高', field: 'high' }, { name: '最低', field: 'low' },
{ name: '成交量', field: 'volume' }, { name: '成交额', field: 'amount' } ] },
{ group: '合约', source: 'instrument', items: [
{ name: '涨停价', field: 'upStopPrice' }, { name: '跌停价', field: 'downStopPrice' } ] },
{ group: '持仓', source: 'row', items: [
{ name: '份额', field: 'shares' }, { name: '成本价', field: 'avgPrice' }, { name: '最后成交价', field: 'lastTradePrice' } ] },
{ group: '自定义字段', source: 'values', items: [] }, // 运行时按策略非 formula 字段的 label 动态展开
];
export const BUILTIN_VAR_NAMES = new Set([...行情/合约/持仓 items name]);
export function buildContext({ row, quote, instrument, fieldDefs }) { [中文名]: number|null }
```
- 自定义字段变量名 = 字段 `label`;值取 `row.values?.[key]`(缺省回退 `def`);非数字值(text/enum/boolean)→ `Number(...)` 失败即 `null`
- **不收录 formula 字段**(零循环依赖)。
### 2.4 计算服务与数据通路
**`src/formula/FormulaService.js`(新增)**
```js
computeRows({ strategyId, rows, fieldDefs, marketHub }) rows 附加 computed: { [fieldKey]: number|null }
```
1.`type='formula'` 字段 → **直接返回原 rows**(零开销);
2. `codes = rows.map(r => r.code)` → 行情**只读内存缓存**`marketHub.quotes.get(code)`、合约 `marketHub.instruments.get(code)`(与 `api/market.js#projectQuote` 同口径;**不读穿透**,miss → 相关变量 null);
3. 逐行 `buildContext`(含该策略非 formula 字段的值)→ 每个 formula 字段 `evaluateFormulaCompiled` 一次;
4. 结果:`computed[field.key] = number|null`(引擎返回原始数值,格式化交前端)。
**接线**`api/strategies.js``strategy-positions` 分支)
```js
const rows = await manager.getStrategyPositions(args.strategyId);
return await formulaService.computeRows({ strategyId: args.strategyId, rows,
fieldDefs: getStrategyFields(settings, args.strategyId), marketHub });
```
- 历史持仓端点(`strategy-holdings/history`**不计算**:前端对历史行显示 `—`
### 2.5 API(校验与试算)
**`strategies/schema-update`(含字段校验)**
- 结构校验:`key` 非空且唯一(同策略内)、`label` 非空且唯一(D-1)、`type` 合法;
- formula 字段:`validateFormula(formula, allowedVars)``allowedVars` = 内置变量名 本策略**非 formula** 字段的 label
- 名称冲突:`label ∈ BUILTIN_VAR_NAMES` → 拒绝(`variable-name-conflict`);
- 失败统一抛 `{ code:'field-validation', message }`(前端 Toast + 公式框描红 + 文案)。
**`formula/trial`(新增)**
```js
// args: { strategyId, formula, decimals?, unit? }
// 1) 校验公式;2) 取该策略首行份额 > 0 的持仓;3) 取缓存行情/合约 → buildContext → 求值
// → { code, name, value: number|null, reason?: 'no-holding'|'no-data'|null }
```
### 2.6 前端渲染(StrategyTab.jsx / ColumnSettingsPopover.jsx
- 列取值:`computed?.[key]``null``—`tertiary);数值 → `toFixed(decimals)` 去尾零 + `unit` 拼接;
- 表头:formula 字段列名前缀 `ƒ`tertiary`title="计算字段(只读)"`),列头/单元格 `title` = `= <公式原文>`
- 只读:formula 列**不挂** `FieldCellEditor`、不绑编辑 onClick`fieldDef.type === 'formula'` 分支直接渲染文本);
- 列设置弹层:列名前缀 `ƒ``normalizeStrategyColumns` 的字段列附 `type` 供 UI 判断;排序/显隐零改动);
- 历史行:无 `computed``—`
## 3. 涉及文件与改动清单
```
src/settings.js # BUILTIN_STRATEGIES / strategyFields / normalizeTabs 收窄 / 退役 CRUD 辅助
src/api/strategies.js # 退役 add|remove|update;新增 schema-update、formula/trialstrategy-positions 附着 computed
src/api/index.js # 头部端点注释同步
src/formula/evaluator.js # 新增:词法/语法/求值/校验
src/formula/variables.js # 新增:变量目录 + buildContext
src/formula/FormulaService.js # 新增:批量现算(缓存只读)
src/client/views/SettingsSection.jsx # 移除「策略分组」子 tab
src/client/views/FieldConfigDialog.jsx # 新增:字段配置弹层
src/client/views/StrategyFieldsEditor.jsx# 改造:弹层内容 + 单策略保存 + formula 分支(公式框/变量选择器/试算/小数位)
src/client/views/StrategyTab.jsx # 顶栏「字段配置」按钮 + 计算列只读渲染
src/client/views/ColumnSettingsPopover.jsx # 列名 ƒ 前缀(仅文案)
scripts/test-r027-static-strategies.mjs # 新增回归(第一阶段)
scripts/test-r026-formula-fields.mjs # 新增回归(第二阶段:引擎 + 目录 + 校验 + 附着 + 试算)
scripts/test-r011-tabs.mjs # 更新(策略 tab 静态化后断言调整)
scripts/test-r013-custom-fields.mjs # 更新(改用 updateStrategyFields
```
## 4. 回归与验证
- 回归脚本均用独立数据目录(`ODL_TEST_DATA_DIR`,技术约束-011),内存 mock settings scope(沿用 test-r013 惯例);
- **test-r027**:内置策略恒两项;旧 settingsstrategies[].configSchema)读取回填;`updateStrategyFields` 单策略写入且不污染另一策略;非内置 strategyId → 拒绝;`normalizeTabs` 丢弃自建策略条目且保留 visible/order`strategies/add|remove|update` 端点已退役(unknown method);
- **test-r026**:引擎(中文变量 / 优先级 / 括号 / 一元负 / 函数 arity / 语法错误位置 / 缺值短路 / 除零 → null);变量目录(三组内置名、自定义 label 动态、formula 字段不入目录);`schema-update` 校验(未知变量 / 名称冲突 / 重复 key / 重复 label);`strategy-positions` 附着 `computed`(有/无 formula 字段两条路径);`formula/trial`(正常 / 无持仓 / 缺数据);
- `pnpm typecheck` + `pnpm build` 通过;
- 老师人工验收(见 `验收标准.md`)。
## 5. 风险与取舍
| 项 | 取舍 / 缓解 |
|---|---|
| 现算开销 | 仅含 formula 字段的策略付出;行情只读内存(无网络);行数为数十级 → 毫秒内;未直接引入缓存 |
| 行情 miss | 不读穿透 → 该行算式 `—`(等 QuoteSync 下轮 5s 刷新);换取 `strategy-positions` 路径零网络 |
| 中文标识符 | 自写词法支持 `\p{L}`;不用第三方库(均不支持中文变量) |
| 旧数据 | 读取回填 + 首次保存落 `strategyFields`;不写迁移脚本、不删库数据 |
| 自建策略 | 读取忽略 + tab 丢弃;其持仓数据保留在库(不自动清份额),需要时人工处理 |
| 双份真相源风险 | `strategies[].configSchema` **只读兼容**、写路径唯一走 `strategyFields`(避免双写打架) |
## 6. 实施顺序
1. settings 层(常量 / strategyFields / tabs 归一化 / 退役)+ test-r027
2. API 层(端点退役 + schema-update+ 设置页收敛 + 字段配置弹层 + test-r013 更新;
3. 公式引擎 + 变量目录 + FormulaService + test-r026(引擎部分);
4. schema-update 字段校验 + formula/trial + strategy-positions 附着;
5. 前端 formula 分支(公式框/选择器/试算/小数位)+ 只读计算列;
6. typecheck + build + 全量回归 → 老师人工验收 → 复盘。
## 7. 约束落地(实施时执行)
- **修订**:产品约束-002/003/004(标签体系 → 固定两策略 + 字段自定义)、产品约束-009(Tab 统一管理中去掉策略条目动态化)、UI约束-002(设置页子 tab 构成)、UI约束-003(Tab 设置策略行不再随 CRUD 变化)、UI约束-005(字段配置入口迁移 + 更正为「表格列化 + 单元格内联编辑」现状);
- **新增**:UI约束-008(计算字段表单 + 只读计算列:`ƒ` 标记 / 悬停公式 / 小数位 / `—`)、产品功能约束(计算字段语义:不落库、只读、仅非公式变量)、技术方案约束(公式引擎自写零依赖不用 eval、变量目录单一入口、现算只读内存缓存)。
@@ -0,0 +1,105 @@
# 迭代复盘:22-策略计算字段与策略 tab 静态化
## 结论
**实现完成(两阶段全范围),待老师人工验收**(尚未宣告验收通过)。事实:策略 tab 静态化 + 字段配置入口迁移(R-027)
与计算字段(R-026)均已落地;`pnpm typecheck` 0 错、`pnpm build` 通过、17 个回归脚本全绿(含新增 2 个)。
## 事实记录
### 阶段一 · 策略 tab 静态化与入口迁移(R-027)
- **src/settings.js**:新增 `BUILTIN_STRATEGIES`(内置两项:网格超市 grid-supermarket / 手动做T manual-t)、
`isBuiltinStrategy / assertBuiltinStrategy / normalizeFields`;字段定义真相源迁到 **`strategyFields`**
`getStrategies` 恒返回内置两项、缺失时兼容读取旧 `strategies[].configSchema``getStrategyFields` /
`updateStrategyFields` 为唯一读写入口);`normalizeTabs` 改为**常量派生**3 内置 tab + 2 策略 tab
只保留存量 visible/order 偏好、丢弃未知条目、补齐缺失条目),`updateTabs` 只接受内置 id
`normalizeStrategyColumns` 的字段列来源改读 `getStrategyFields`;新增 `scopeValue` 容错读取
(测试桩无 `get` 不炸);**退役** `addStrategy / removeStrategy / renameStrategy / updateStrategies /
appendStrategyTab / removeStrategyTab / DEFAULT_STRATEGIES`
- **src/api/strategies.js****退役** `strategies/add | strategies/remove | strategies/update`(调用返回 not-found);
新增 `strategies/schema-update { strategyId, configSchema }`(唯一写路径)+ `validateConfigSchema`
(结构 / key 唯一 / 展示名唯一 / 内置变量名冲突 / 枚举选项 / 公式校验,失败统一 `code='field-validation'`);
新增 `formula/variables`(变量目录)与 `formula/trial`(试算)。
- **设置页**SettingsSection.jsx):「策略分组」子 tab 整体移除(含 CRUD 与删除确认)→ 设置页仅
「Tab 设置 / QMT 连接配置」。
- **字段配置弹层**FieldConfigDialog.jsx 新建 + StrategyFieldsEditor.jsx 改造):策略 tab 顶栏
「字段配置」(列设置左侧)→ 锚定弹层(520px / 70vh / 内部滚动);字段列表 + 添加/编辑/删除 + 底部
[关闭]/[保存字段]dirty 时关闭/外点/Esc 二次确认;保存走 `strategies/schema-update`
### 阶段二 · 计算字段(R-026)
- **src/formula/evaluator.js**(自写零依赖引擎,不用 `eval/Function`):词法支持中文标识符 + 全角归一
(()+-×÷),递归下降解析(四则 / 括号 / 一元负 / round·abs·min·max;长度 ≤500、深度 ≤32);
`validateFormula` 返回 `{ok,vars}``{ok:false,error:{code:empty|syntax|unknown-var|unknown-func|arity,message,position}}`
求值**缺值短路**(任一变量 null → 整式 null)、除零/非有限 → null、**最终结果浮点归一**|v|<1e12 四舍五入 10 位,
消除 `0.7000000000000002` 类噪声;中间步骤不归一再保精度——首版曾因每步归一导致 `6.66666667` 精度劣化,已改)。
- **src/formula/variables.js**:变量目录单一入口(行情 7 项 / 合约 2 项 / 持仓账本 3 项 + 运行时按策略
非 formula 字段展示名展开「自定义字段」组);`allowedVarNames` 排除 formula 字段(零循环依赖);
`buildContext` 取数(昨收兜底合约 preClose、布尔 → 0/1、空值 → null)。
- **src/formula/FormulaService.js**`computeRows``strategy-positions` 行附 `computed`;行情/合约
**只读 QuoteHub 内存缓存(不读穿透)**;**无 formula 字段直接返回原数组(零开销)**;值不落库。
- **前端**:字段表单类型「计算」分支(等宽公式框 + 「+ 插入变量」分组菜单 + 试算 + 小数位 0-4 默认 2;
保存失败公式框描红 + 服务端文案);持仓表计算列**只读**(表头 `ƒ` + title 公式原文、按 `toFixed(decimals)` + 单位
格式化如 `6.67 %` / `700.00 元`、缺数据 `—`、点击不进内联编辑);历史持仓行 `—`;列设置弹层列名带 `ƒ`
### 实机数据核实(实施前)
- `~/.dsh/settings.yaml`:策略仅两个内置(网格超市 5 字段 / 手动做T 1 字段);
- `~/.dsh/one-divine-lot/store.db``strategy_holdings` 13 / 3 行,交易与归属数据只涉及这两个策略
**无自建策略,静态化零数据迁移**
### 验证
- `pnpm typecheck` → 0 错;`pnpm build` → 通过(lib 32 文件 + client bundle 195545 bytes`scripts/wrap-client.mjs` 正常);
- 回归脚本 **17 / 17 全绿**
- 新增 `scripts/test-r027-static-strategies.mjs` 35/35(策略常量 / 旧定义兼容 / 单策略写入 / tabs 归一化 / 端点退役);
- 新增 `scripts/test-r026-formula-fields.mjs` 48/48(引擎 / 目录 / 上下文 / 现算 / 保存校验 / 试算);
- 更新 `test-r011-tabs` 22/0、`test-r011-api` 17/0、`test-r013-custom-fields` 23/0、
`test-r013-columns` 18/0(期望值按 R-015 行情列 + R-027 真相源更新)、`test-r017-candidates` 12/0(策略 id 改用内置);
- 其余(r009 系列 / r016 / r018 / quote-sync / position-sync / health / mcp)零改动全绿。
## 偏差、顺带修复与发现
1. **`generateStrategyId` / `PINYIN_MAP` 保留**(方案原文列入退役):QMT 连接 id 生成
`generateQmtConnectionId` 仍依赖,本轮不动(已回写技术方案)。
2. **真 bug(实施中发现并修复)**`normalizeStrategyColumns` 仍读旧 `strategies[].configSchema`
字段定义迁到 `strategyFields` 后策略列会丢失字段列 → 改为 `getStrategyFields(scope, strategyId)`
教训:真相源迁移必须全量搜「读点」而不只是「写点」。
3. **兼容修复**`strategy-positions` 新增 settings 依赖后,测试里无 `get` 的 settings 桩会抛错
test-r016/test-r017 暴露)→ 新增 `scopeValue` 容错读取。
4. **顺带修复迭代 20 复盘遗留第 1 项**`test-r013-columns` 期望过期(R-015 增加 4 个默认隐藏行情列后应为
12 列)——本次随实现更新期望并补「strategyFields 优先」用例。
5. **`test-r017-candidates` 策略 id 迁移**`attribution-targets``getStrategies` 取策略表,
静态化后仅内置两项 → 测试改用 `manual-t / grid-supermarket`R-027 的必然结果,非缺陷)。
6. **交互边界修复**:字段配置弹层的「外点关闭」原以弹层面板为界,点「字段配置」按钮自身会触发放弃确认
(dirty 时甚至静默卸载)→ 改为以**锚点容器**(含两个按钮)为界。
7. **工作区背景(非本轮产出)**:仓库工作区另含迭代 19/20/21R-022/R-023/R-024/R-025)的未提交改动,
本轮未触碰其逻辑,仅在其上叠加;建议老师确认后一并提交。
## 遗留与风险
- **行情 miss 显示 `—`**:计算列取数只读内存缓存、不读穿透(技术约束-023);某 code 无快照时该格为空,
等 QuoteSync 下轮(≤5s)覆盖。若老师更希望「宁可慢也要立刻有价」,可改判为读穿透。
- **需要重载插件才能人工验收**`~/.dsh/profiles/web/node_modules/one-divine-lot` 是指向本工作区的软链,
lib 已构建完成;老师侧需重载插件(或重启 dsh web)后在页面验收(未由我启动任何服务)。
- **旧 `strategies[].configSchema` 仅只读兼容**:本机字段定义在其首次保存后落入 `strategyFields`
(不写迁移脚本、不动库数据)。
- 自建策略若未来出现:读取忽略、tab 丢弃,其 `strategy_holdings` 数据保留在库(本机无此情况)。
## 待老师确认事项(人工验收)
`验收标准.md`**A1A11**(重构)+ **B1B12**(计算字段),重点四条:
1. 设置页无「策略分组」;策略 tab 恒两个、不可增删改名,但 Tab 设置仍可显隐/拖拽排序;
2. 策略 tab 顶栏「字段配置 / 列设置」可用,字段增删保存后表格列跟随、重启后定义仍在;
3. 计算字段可用:中文变量公式 + 变量选择器 + 试算 → 持仓列只读显示结果(`ƒ` 标记 + 悬停看公式);
4. 边界:非法公式/未知变量/名称冲突被拒 + 描红;缺数据与历史行显示 `—``strategy_holdings.values` 不含计算字段键。
## 经验沉淀(候选)
- **并行委派的契约必须在开工前冻结**:本轮前端由子代理并行实现,期间服务端 `settings.js` 仍在演进
(子代理观察到 mtime 变化、并在报告里提出「疑似工作区未冻结」)——契约冻结 + 文件集互斥是并行可行性的前提。
- **真相源迁移要全量搜读点**:本次 `normalizeStrategyColumns` 差点漏改,靠既有回归脚本的红灯暴露;
遗留的旧键兼容读取(而非直接删键)让迁移零风险、零脚本。
- **测试期望也需要跟着设计演进**`test-r013-columns` 的过期期望在迭代 20 复盘就被记录却未处理,
本轮顺手清偿,避免「红灯常态化」。
@@ -0,0 +1,45 @@
# 迭代目标:22-策略计算字段与策略 tab 静态化
## 目标
两条腿,一个迭代走完:
- **第一阶段 · 重构(R-027)**:策略 tab 静态化为插件内置 tab(不可增删改名,保留显隐/排序);设置页「策略分组」移除;
字段编辑功能迁到**策略 tab 内、「列设置」旁的「字段配置」弹层**;
- **第二阶段 · 计算字段(R-026)**:在迁移后的入口上新增第五种字段类型「**计算(formula)**」——用户写中文变量公式
引用服务端缓存数据集,服务端按持仓行**实时计算**列值(不落库、列只读、缺数据 `—`)。
## 目标描述
- **第一阶段背景**:2026-09-10 老师提出重构想法——「策略 tab 不想做成动态可增减修改的了,改成静态的,和全部持仓、交易记录这些一样,
都是插件内置的 tab」「这两个策略 tab 原有的字段编辑功能,切换到主窗口的策略 tab 下面,列设置旁边,作为统一的字段配置管理」。
实机核实:仅两个内置策略(网格超市 / 手动做T)、**无自建策略**(store.db 13/3 行),静态化零数据迁移风险;需求定稿 R-027。
- **第二阶段背景**:2026-09-10 老师提出字段配置优化的下一步——新增**可计算**的字段类型(公式引用服务端缓存数据集得出结果);
需求定稿 R-026。
- **合并理由**:公式表单必须落在字段配置入口上;若先按旧入口(设置页策略分组)实现,再随重构搬迁 = 白做一遍(老师 Q6 拍板:一次改到位)。
- **本迭代节奏(老师指令 2026-09-10)**:先出计划 → 启动迭代 → **本轮只做交互设计**(不做技术方案与编码);交互设计过审后补技术方案 + 验收标准,再进入实现。
- **验收线**
- 重构:设置页无「策略分组」;策略 tab 恒两个且不可增删改名;Tab 设置仍可显隐/排序;字段配置弹层可用并落库到正确策略;
- 计算字段:四式用例(浮动盈亏 / 盈亏比例 / 距涨停 / 网格占用)在策略 tab 列上算出;计算字段不落库、列只读、非法公式被拒、缺数据不炸表。
## 目标分解
1. **交互设计**(本步):① 重构部分——策略 tab 顶栏两个按钮(字段配置 / 列设置)、字段配置弹层(列表 + 表单 + 保存)、
设置页收敛;② 计算字段部分——弹层内公式分支(公式框 + 变量选择器 + 试算 + 单位/小数位)、持仓表只读计算列、空态与错误态;
2. **技术实现方案**(交互过审后):两阶段规格——tab/存储归一化、单策略字段端点、变量目录、表达式引擎、现算通路、校验与试算、回归脚本;
3. **验收标准**(同上):两阶段各自的验收方法与判定线;
4. **实现与验收**:第一阶段(重构)→ 回归 → 第二阶段(计算字段)→ 回归 → 老师人工验收 → 复盘。
## 目标讨论过程
1. 2026-09-10 老师提出计算字段方向 → AI 摸清 R-013 现状 + 服务端缓存数据集清单 → Q1-Q10 问题清单 → 老师「定」+ 三项选择题确认 → 定稿 **R-026**
2. AI 出迭代 22 `UI交互设计.md`(公式表单落在设置页策略分组)→ 老师拍板 D-1~D-7 → 交互设计定稿;
3. 2026-09-10 老师提出**重构想法**(策略 tab 静态化 + 字段配置入口迁移)→ AI 核实实机数据(无自建策略、零迁移风险)+ 影响面分析
+ 撞位提示(公式表单落在设置页,入口一搬即废)→ 老师拍板 Q1~Q4、Q6(全采建议),Q5/Q7 由 AI 定 → 定稿 **R-027**
4. 迭代 22 结构调整为「第一阶段=重构、第二阶段=计算字段」,交互设计同步修订(公式表单移入「字段配置」弹层)。
## 对老师(项目主理人)的配合需求
- **本轮(已完成 2026-09-10**:拍板 D-1D-7(计算字段交互)与 Q1~Q6(重构方案)—— 全部采纳 AI 建议;
- **后续**:技术方案过审后,按两阶段做页面人工验收——① 设置页无策略分组、策略 tab 不可增删改名、字段配置弹层可用;
② 公式配置与试算、持仓表只读计算列、缺数据/非法公式边界。
@@ -0,0 +1,98 @@
# 验收标准:22-策略计算字段与策略 tab 静态化
> 依据:R-027(第一阶段)+ R-026(第二阶段)+ PLAN-019 + `技术实现方案.md` 日期:2026-09-10 状态:**待实施后验收**
> 验收线在哪里:本文件(分阶段清单 + 判定线);配套证据:回归脚本输出、typecheck/build 输出、老师人工页面确认
## 1. 验收目标
- **第一阶段**:策略 tab 变成**插件内置静态 tab**(不可增删改名,保留显隐/排序),设置页「策略分组」消失,字段配置入口落到**策略 tab 内、列设置旁**;
- **第二阶段**:「计算(formula)」字段类型可用——用户写中文变量公式(引用行情/合约/持仓账本/本行手填字段),服务端按持仓行**实时算**出列值,**不落库、列只读、缺数据 `—`**
- 既有能力(四类手填字段、列设置、历史持仓、份额账本、交易归属)**零回归**。
## 2. 验收标准线 · 第一阶段(R-027 重构)
| # | 验收项 | 判定 |
|---|---|---|
| A1 | 设置页子 tab 仅「Tab 设置 / QMT 连接配置」 | 页面无「策略分组」入口 |
| A2 | 无任何策略新增/重命名/删除入口 | 设置页与策略 tab 均无该入口;服务端调用 `strategies/add\|remove\|update` 返回 unknown method |
| A3 | 策略 tab 恒为「网格超市 / 手动做T」 | 重启、改配置、增删数据后仍恒两项 |
| A4 | Tab 设置中策略行与内置行同权 | 可拖拽排序(间隙高亮线)、可显隐开关;现有偏好(策略置顶 / 全部持仓·关注列表隐藏)保留 |
| A5 | 策略 tab 顶栏有「字段配置」与「列设置」两个按钮 | 位置:字段配置在左、列设置在其右 |
| A6 | 字段配置弹层显示**本策略**现有字段 | 网格超市 5 个(网格上边界/下边界/基准值/网格大小/网格交易量);手动做T 1 个(T仓成本价) |
| A7 | 弹层内可添加/编辑/删除字段并保存 | 保存走 `strategies/schema-update`;成功 Toast;另一策略定义不受影响 |
| A8 | 字段增删后表格列自动跟随 | 新增列出现在表格(列设置中可勾选/排序);删除字段后该列消失 |
| A9 | 未保存改动时关闭弹层 | 出现「确定放弃?」二次确认(D-8) |
| A10 | 重启后字段定义仍在 | 存储落 `strategyFields`(旧 `strategies[].configSchema` 首次保存后被取代,无迁移脚本) |
| A11 | 数据零影响 | `strategy_holdings` 行数(13 / 3)、交易归属、份额不变 |
## 3. 验收标准线 · 第二阶段(R-026 计算字段)
| # | 验收项 | 判定 |
|---|---|---|
| B1 | 字段类型下拉含「计算」 | 选中后隐藏默认值,出现公式框 + 变量选择器 + 试算 + 小数位(0–4,默认 2) |
| B2 | 变量选择器按分组列出可用变量 | 行情(现价/昨收/今开/最高/最低/成交量/成交额)、合约(涨停价/跌停价)、持仓(份额/成本价/最后成交价)、自定义字段(本策略手填字段的**展示名**) |
| B3 | 公式用**中文变量**且可算出结果(四式用例) | ① `(现价 - 成本价) × 份额``(现价 ÷ 成本价 - 1) × 100``(涨停价 - 现价) ÷ 现价 × 100``现价 × 份额`(或 × 网格交易量)——每行值正确(与手工核算一致) |
| B4 | 试算可用 | 点「试算」用该策略一行真实持仓算出并显示(含代码/名称/格式化结果);无持仓 → 「暂无可试算的持仓数据」 |
| B5 | 保存校验生效 | 语法错误(如 `现价 ×`)→ 拒绝 + 公式框描红 + 定位文案;未知变量 → 拒绝;字段展示名与内置变量冲突(如叫「现价」)→ 拒绝;同策略展示名重复 → 拒绝 |
| B6 | 计算列只读且可辨识 | 表头 `ƒ` 前缀;点击单元格**不进入**编辑;悬停列头/单元格显示公式原文 |
| B7 | 格式化正确 | 按小数位 + 单位显示(如 `6.67 %``700.00 元` |
| B8 | 缺数据不炸表 | 无行情 / 除零 / 非有限 / 手填字段值非数字 → 该格 `—`(不显示 NaN、不报错、其余列正常) |
| B9 | 计算字段**不落库** | `strategy_holdings.values` 无 formula 字段键;重启后公式仍在(存 settings)、值仍为实时计算 |
| B10 | 历史持仓行 | 已清仓行的计算列显示 `—`(不做历史时点计算) |
| B11 | 列设置联动 | 计算列与手填列同列混排(名称带 `ƒ` 前缀),显隐/拖拽排序正常 |
| B12 | 零回归 | 四类手填字段编辑、列设置、历史持仓开关与范围、份额操作、交易归属、Tab 设置全部照旧 |
## 4. 边界与异常用例
| 场景 | 期望 |
|---|---|
| 公式引用已删除的字段 | 保存时被拒(未知变量);已存在的旧公式运行时该列 `—`,字段列表 `⚠` 标记 + 悬停原因(D-4 |
| 公式引用另一个计算字段 | 被拒(变量目录不含 formula 字段,零循环依赖) |
| 公式为空 / 只有空格 | 拒绝保存「请填写公式」 |
| 极长公式 / 深层括号 | 不崩溃(解析器深度限制,超出 → 语法错误提示) |
| 手填字段值为文本(非数字) | 参与计算时按 `null` 处理 → `—`(不报错) |
| QMT 断开 | 该策略计算列大面积 `—`;表格其余功能正常 |
| 无 formula 字段的策略 | 行为与重构前完全一致(无额外取数开销) |
| 弹层打开时保存失败(服务端异常) | Toast 错误、弹层不关闭、改动保留可重试 |
## 5. 验收方法
1. **回归脚本**(独立数据目录,技术约束-011
- `node scripts/test-r027-static-strategies.mjs` → 全绿(内置策略常量 / 旧定义兼容 / 单策略写入 / tabs 归一化 / 端点退役);
- `node scripts/test-r026-formula-fields.mjs` → 全绿(引擎 / 目录 / 校验 / 附着 computed / 试算);
- `node scripts/test-r011-tabs.mjs``node scripts/test-r013-custom-fields.mjs`(已更新)→ 全绿;
2. **静态检查**`pnpm typecheck` + `pnpm build` 通过;
3. **老师人工验收**(按 §6 步骤逐条确认,A1–A11 + B1B12)。
## 6. 老师配合项(人工验收步骤)
1. 打开设置页 → 确认只有「Tab 设置 / QMT 连接配置」(A1/A2);
2. Tab 设置 → 拖动两个策略 tab、切换显隐(A3/A4);
3. 进入策略 tab(网格超市)→ 顶栏 `[字段配置] [列设置]` → 打开字段配置 → 看到原有 5 个字段(A5/A6);
4. 添加计算字段(例:展示名「网格占用」、类型「计算」、单位「元」、小数位 2、公式 `现价 × 份额`)→ 插入变量 → 试算 → 保存(B1/B2/B4);
5. 表格中该列出现并显示数值;悬停看公式;点击不可编辑(B3/B6/B7);
6. 再配两个计算字段验证其余用例:`(现价 - 成本价) × 份额``(现价 ÷ 成本价 - 1) × 100`(单位 %)(B3);
7. 故意写坏公式(`现价 ×`)、引用未知变量、把字段命名为「现价」→ 均应被拒绝并提示(B5);
8. 造缺数据场景(如某 code 无行情)→ 该格 `—`,表格正常(B8);
9. 清仓一只持仓 → 历史行计算列 `—`B10);
10. 重启插件 → 字段定义仍在、值重新现算(A10/B9);
11. 抽查数据库:`strategy_holdings.values` 不含 formula 键(B9)。
## 7. 判定线
**通过(全部满足)**
- A1A11 与 B1B12 全部通过;
- 回归脚本全绿 + typecheck/build 通过;
- 老师人工验收确认。
**任一即不通过**
- 计算字段写入 `values` 列(或落任何库表);
- 公式可执行表达式以外的行为(无 `eval/Function`、不可触达进程/文件能力);
- 无 formula 字段的策略出现额外取数开销或行为变化(回归);
- 既有四类字段编辑、列设置、历史持仓、份额/归属任一回归失败;
- 策略仍可被增删改名(静态化未达成)。
## 8. 明确不在本次验收范围
- 策略级聚合(跨行求和)、QMT 账户域/交易记录作为变量源、公式引用公式、历史时点计算、布尔型公式结果、公式框实时高亮/自动补全、完整 T-006 数据池;
- 自建策略的历史数据清理(本机无自建策略;如未来出现,另行讨论)。
@@ -0,0 +1,88 @@
# 技术实现方案:23-计算字段判定型扩展
> 依据:R-028(已定稿)+ R-026 既有实现(迭代 22)| 日期:2026-09-10 状态:**已实施**
## 1. 公式引擎(src/formula/evaluator.js
- **词法**:新增多字符运算符识别(`>= <= == != && ||` 优先于单字符);全角/符号归一扩展
`≥→>= ≤→<= ≠→!= >→> <→< =→=`
- **语法优先级**(低 → 高):`or``and` → 比较(`> < >= <= == !=`) → 加减 → 乘除 → 一元负 → 主;
`and`/`or` 以标识符(大小写不敏感)识别,`&&`/`||` 等价;
- **求值**:比较返回布尔;逻辑返回布尔(`!!l && !!r`);两者**缺值短路**(任一操作数为 null → null);
数字运算维持原语义(除零 → null、最终结果浮点归一);`normalizeResult` 对布尔直通;
- **新增 `inferResultKind(ast)`**:顶层为比较/逻辑 → `'boolean'``neg` 递归下钻),否则 `'number'`
- 校验(`validateFormula`)逻辑不变(变量存在性 / 函数白名单 / arity)。
## 2. 存储与校验
- **src/settings.js**`fieldSchema``resultKind: z.union([z.const('number'), z.const('boolean')]).default('number')`
`normalizeFields` 对 formula 字段归一 `resultKind`(非 `'boolean'` 一律 `'number'`),`decimals` 维持夹取 0-4
- **src/api/strategies.js#validateConfigSchema**:公式校验通过后增加**结果类型一致性**校验 ——
`declared = resultKind`(默认 number)必须等于 `inferResultKind(compileFormula(formula).ast)`
不一致抛 `field-validation`
- 声明「判定」但公式无比较 → 「结果类型选了「判定」,但公式没有比较运算(如 涨停价 > 基准值 + 1)」;
- 公式是判定但声明「数字」 → 「公式结果是判定,请把「结果类型」改为「判定」」。
## 3. 现算(src/formula/FormulaService.js
`computed[key]` 布尔直通:
```js
computed[c.key] = (typeof v === 'boolean') ? v
: ((typeof v === 'number' && Number.isFinite(v)) ? v : null);
```
其余不变(无 formula 字段零开销、只读 QuoteHub 内存缓存、不落库、历史行不计算)。
## 4. 前端
**StrategyFieldsEditor.jsx**
- 表单草稿增 `resultKind`(默认 `number`,编辑时回填);
- 「类型」行新增「结果」下拉(数字 / 判定);
- 公式框 placeholder 按结果类型切换(判定示例:`涨停价 > 基准值 + 网格大小`);
- 公式框下新增运算符提示行:`运算:+ - × ÷ 括号;判定:> < >= <= == != ,组合:and / or`
- `resultKind === 'boolean'` 时**隐藏小数位**输入;
- 列表「类型」列显示 `计算(判定)`
- **判定型试算**`value` 为布尔时显示 `试算:<名称> <代码> → ✓ 满足 / — 不满足`
**StrategyTab.jsx**
- 计算列渲染:**先判布尔**`typeof raw === 'boolean'`)—— `true``✓ <字段展示名>``state-success-primary`),
`false` / 缺值 → `—`(tertiary);否则走原数字格式化(`toFixed(decimals)` + 单位);
顺序不可颠倒(`Number(false) === 0` 会被误格式化成 `0.00`);
- 表头 `ƒ` 前缀、悬停公式原文、历史行 `—` 维持不变。
## 4.5 附:公式引擎选型论证(2026-09-10 老师提问「有没有现成的库」)
**结论:维持自写引擎**`src/formula/evaluator.js`,约 250 行 / 零依赖 / 71 条回归断言)。
| 候选 | 中文变量名 | 能力覆盖 | 体积/依赖 | 风险与维护 |
|---|---|---|---|---|
| **expr-eval** | ✗ ASCII 标识符 | 四则/比较/and·or/**三元 ?:**/属性访问/数组索引 | 小 | **CVE-2025-12735**:构造 variables 对象可致**任意代码执行**GHSA-jc85-fpwf-qm7x);上游维护停滞,社区有 fork |
| **mathjs** | ✗ 明确枚举可用字符(拉丁/希腊/字母类/数学字母数字),**不含 CJK** | 极全(矩阵/单位/复数/函数库) | 大(数百 KB 级) | 重;unicode 标识符诉求自 2015 年 issue #265 起未覆盖 CJK |
| **Jexl** | ✗ ASCII 标识符(支持 `a.b` / `a[b]` 取值) | 比较/逻辑/三元/自定义运算符 | 中 | 取值语义与「缺值短路」需自行包装 |
| **CELcel-js 等)** | ✗ ASCII 标识符 | 受限表达式语言(工业标准、可静态检查) | 中 | 语义偏严格,接入成本不低 |
| **解析器工具包**chevrotain / ohm-js | ✓(自定义词法) | 自己写语法 + 求值 | 中 | **等于仍自己实现**,只是换成语法文件 + 依赖 |
| **自写引擎(现状)** | ✓ 原生支持 | 四则/括号/比较/and·or/round·abs·min·max | 0 | 无 eval/Function、无属性访问、无数组索引、变量来自白名单目录 → **结构上无任意代码执行面** |
**为什么不换**
1. **中文变量名是产品决策**(R-026 Q2 老师拍板)。主流库标识符均限 ASCII → 引库就得加「中文名 → 占位符」映射层
(按长度降序替换防前缀冲突 + 错误位置与提示回译),**复杂度不降反升**,报错体验还变差;
2. 所需能力集很小,自写约 250 行即覆盖,且**语义完全可控**(缺值短路 → `—`、除零 → `—`、最终值浮点归一);
3. **攻击面**:表达式与变量都来自用户输入,expr-eval 恰恰因变量对象注入出过 RCE;自写引擎不做属性访问/动态函数调用(仅 4 个纯数学函数白名单),无此类面;
4. 项目为 DSH 外部插件,依赖越少越稳(现依赖仅 schemastery + MCP SDK)。
**什么时候值得换成库(判据)**
- 需要**条件分支/三元**(如三态信号「空/多/观望」)→ 自写加 `?:` 约 30 行即可,仍不必引库;
- 需要**字符串/日期/正则** → 能力超出「数值公式」,届时评估 CEL / Jexl(并重做安全评估);
- 需要**Excel 级函数库**SUM/IF/VLOOKUP…)→ 考虑 hyperformula / Formula.js,但那是产品形态变化(表格公式),应先讨论需求。
> 若后续决定改用第三方库,需同步修订**技术约束-022**(现规定「自写零依赖、禁止 eval/Function」)。
- `scripts/test-r026-formula-fields.mjs` 新增两节(第 7 节判定型 15 条 + 第 8 节端到端 8 条):
比较/and/or/全角符号/短路、`inferResultKind` 三例、结果类型一致性双向拒绝、布尔直通、
`strategy-positions` 附判定 `computed`、判定试算;
- **端到端夹具 = 真实数据**:以 2026-09-10 实盘涨跌停(积成电子 8.22/6.72 等)+ 真实持仓基准值构造三行,
断言 `可下空单 / 可下多单` 判定与实算一致;
- `npx tsc --noEmit` 0 错;`npm run build` 通过;全量 **17 个回归脚本全绿**r026 由 48 → 71 条断言)。
@@ -0,0 +1,43 @@
# 迭代复盘:23-计算字段判定型扩展
## 结论
**实现完成,待老师人工验收**。计算字段由「只出数字」扩展为「数字 / 判定」双结果类型;
网格超市两条网格规则(可下空单 / 可下多单)已可用公式表达,行为与实盘数据一致(真实验证 5 只 / 2 只)。
## 事实记录
- **引擎**src/formula/evaluator.js):多字符运算符识别 + 全角 `≥ ≤ ≠ ` 归一;
优先级 `or < and < 比较 < 加减 < 乘除`;比较/逻辑求值(缺值短路);`normalizeResult` 布尔直通;
新增 `inferResultKind(ast)`
- **存储**src/settings.js):`fieldSchema.resultKind`number|boolean,默认 number+ `normalizeFields` 归一;
- **校验**src/api/strategies.js):结果类型一致性校验(声明 vs `inferResultKind`),不一致抛 `field-validation`
- **现算**src/formula/FormulaService.js):`computed` 布尔直通;
- **前端**`StrategyFieldsEditor` 结果类型下拉 + 判定时隐藏小数位 + 运算符提示 + 判定试算(✓ 满足 / — 不满足);
`StrategyTab` 判定列渲染 `✓ 字段名`(绿)/ `—`(灰,含缺值与 `false`);列表类型显示 `计算(判定)`
- **验证**`test-r026` 48 → **71** 条(新增第 7 节判定型 15 条、第 8 节真实夹具端到端 8 条);
typecheck 0 错;build 通过(client bundle 197794 bytes);全量 17 脚本全绿。
## 设计与实现要点(沉淀)
1. **布尔必须先判**:渲染分支若先做 `Number.isFinite(Number(raw))``false` 会被当成 `0` 格式化成 `0.00`——
实现中显式把 `typeof raw === 'boolean'` 放在最前(代码注释标注);
2. **判定型「不满足」= `—` 而非「否」**:缺行情与条件不满足在视觉上同形,避免把「数据缺失」误读为「不可以」;
3. **结果类型显式声明 + 静态推断校验**:避免「公式悄悄变成判定」导致列渲染与语义脱节;
推断只看**顶层**运算(`neg` 下钻),规则简单可预期;
4. **阈值用字段引用而非字面量**`基准值 + 网格大小` 让规则随字段可调(老师 Q5 拍板)。
## 偏差
- 无功能偏差;范围按 Q1–Q5 收敛(方案 C 三态信号、`not` 取反未做,已记入「不做」)。
## 补记(2026-09-10 老师反馈)
- 字段配置弹层的变量选择从「+ 插入变量 ▾」下拉改为**标签平铺**(分组成行、点选即插入公式框光标处);
已随迭代实施并重建 bundletypecheck/build/17 脚本回归全绿)。
## 遗留
- 判定「不满足」与「缺数据」同显示为 `—`:若老师希望区分(如缺数据显示 `?` 或 tooltip「数据缺失」),
可后续加 hover 原因;
- 由老师侧重载插件后手动添加两个字段(或授权我用 `strategies/schema-update` 端点直接写入 settings)。
@@ -0,0 +1,42 @@
# 迭代目标:23-计算字段判定型扩展(含网格超市信号字段)
> 依据:**R-028(已定稿,2026-09-10** | 类型:小步迭代(R-026 能力延伸,不单列计划文档)
> 日期:2026-09-10 | 状态:**已实现,待老师人工验收**
## 目标
把计算字段从「只出数字」扩展到**判定型**(比较运算 + and/or 组合,结果类型由字段显式声明),
并用它给网格超市落地两条网格规则:**可下空单** = `涨停价 > 基准值 + 网格大小`
**可下多单** = `跌停价 < 基准值 - 网格大小`
## 目标描述
- **背景**:老师提出「当天涨停值 > 当前基准值 + 1 即可下空单 / 当天跌停值 < 当前基准值 - 1 可下多单」;
R-026 的计算字段只产数字(无比较运算),需先扩能力;
- **范围**:引擎(比较/逻辑/inferResultKind)、存储(`resultKind`)、API 校验(结果类型一致性)、
字段表单(结果类型选择 + 判定时隐藏小数位 + 运算符提示 + 判定试算)、持仓表判定列渲染(`✓ 字段名` / `—`);
- **不做**:三态信号字段(空/多/观望,方案 C)、not 一元取反、判定与数字的混合字段、策略级聚合;
- **验收线**(详见 `验收标准.md`):C1C8。
## 目标分解
1. 引擎:比较 `> < >= <= == !=`(含全角 `≥ ≤ ≠`+ 逻辑 `and/or`(含 `&& ||`+ 优先级 + 缺值短路 + `inferResultKind`
2. 存储/校验:`resultKind: number|boolean`(归一化 + 保存校验一致性);
3. 现算:`computed` 布尔直通(判定型不参与 toFixed 格式化);
4. UI:结果类型选择、判定占位与运算符提示、判定列 `✓ 字段名`(绿)/ `—`(灰)、判定试算(✓ 满足 / — 不满足);
5. 回归:`test-r026` 增补 R-028 段(23 条)+ 网格超市真实夹具端到端;
6. 文档:R-028 + 本迭代四件套 + 约束变更 3 处。
## 目标讨论过程
1. 2026-09-10 老师提出网格超市两条判定规则;
2. AI 用**真实数据**验证可行性(13 只持仓 × 今日涨跌停 × 真实基准值 → 空单 5 / 多单 2),
并指出 R-026「只出数字」的能力边界 → 提出 A/B/C 三方案;
3. 老师拍板 Q1–Q5(全采建议):A 判定型 / ✓ 与 — / 支持 and·or / 结果类型显式 / 阈值引用网格大小;
4. T-012 转正 R-028 → 本轮实施完成(typecheck + build + 17 脚本全绿)。
## 对老师(项目主理人)的配合需求
- **重载插件后**在页面添加这两个字段(字段配置 → 添加字段 → 类型「计算」→ 结果「判定」→ 公式粘贴 → 试算 → 保存);
或告知由我用端点写入;
-`验收标准.md` C1C8 人工确认。
@@ -0,0 +1,48 @@
# 验收标准:23-计算字段判定型扩展
> 依据:R-028 + `技术实现方案.md` 日期:2026-09-10 状态:**待老师人工验收**
## 1. 验收目标
判定型计算字段可用(比较 + and/or + 结果类型显式声明 + 校验一致),并用它给网格超市落地
「可下空单 / 可下多单」两列;数字型计算字段与既有能力零回归。
## 2. 验收标准线
| # | 验收项 | 判定 |
|---|---|---|
| C1 | 字段表单出现「结果」下拉(数字 / 判定) | 选「判定」后**小数位输入消失**;公式框 placeholder 变为判定示例 |
| C2 | 判定公式可保存 | 展示名「可下空单」/ 类型「计算」/ 结果「判定」/ 公式 `涨停价 > 基准值 + 网格大小` → 保存成功 |
| C3 | 判定试算 | 点「试算」显示 `试算:<名称> <代码> → ✓ 满足``— 不满足`(无持仓 → 「暂无可试算的持仓数据」) |
| C4 | 持仓列渲染 | 满足行显示 `✓ 可下空单`(绿);不满足 / 缺行情 → `—`(灰);表头 `ƒ` 前缀、悬停见公式原文 |
| C5 | 两条规则与实盘一致 | 今日上证实况下,「可下空单」应为 5 只(积成电子/天海防务/万顺新材/大连热电/华智数媒)、「可下多单」应 2 只(中国化学/TCL中环),其余两列均为 `—` |
| C6 | 逻辑组合可用 | 公式 `涨停价 > 基准值 + 网格大小 and 现价 < 基准值 + 网格上边界` 可保存并算出(and / or / `&&` / `\|\|` 均可) |
| C7 | 结果类型一致性校验 | 声明「判定」但公式为算式(`1 + 1`)→ 拒绝并提示;公式为判定但声明「数字」→ 拒绝并提示 |
| C8 | 零回归 | 数字型计算字段(如 `(现价 - 成本价) × 份额`)照常;四类手填字段、列设置、历史行 `—`、Tab 设置与静态策略均不受影响 |
## 3. 边界与异常
| 场景 | 期望 |
|---|---|
| 缺行情 / 缺合约信息(涨停跌停取不到) | 判定列 `—`(不显示 false 误导) |
| 手填字段值空(如未填基准值) | 判定列 `—` |
| 全角符号输入(`≥ ≤ ≠ ×÷` | 正常解析 |
| 公式引用另一个公式字段 | 仍被拒(零循环依赖不变) |
| 历史持仓行 | 判定列 `—` |
## 4. 验收方法
1. `node scripts/test-r026-formula-fields.mjs`**71/71**(含判定型 15 条 + 真实夹具端到端 8 条);
2. 全量回归 17 个脚本全绿;`npx tsc --noEmit` 0 错;`npm run build` 通过;
3. 老师人工验收:重载插件 → 网格超市 tab → 字段配置 → 添加上述两字段 → 对照 C1–C8(重点 C5 与实盘一致性)。
## 5. 判定线
**通过**:C1–C8 全部通过 + 回归/构建全绿 + 老师人工确认。
**任一即不通过**:判定列把 `false` 显示成数字(`0.00`);缺数据显示为 `否`(误导);结果类型声明与公式不一致仍能保存;
数字型计算字段或既有四类字段出现回归。
## 6. 不在本次范围
三态信号字段(空 / 多 / 观望,方案 C)、`not` 一元取反、判定与数字混用的单字段、策略级聚合(跨行求和)、
历史持仓行的时点判定。
+60
View File
@@ -0,0 +1,60 @@
# 需求:R-021 神之一手数据加入会话(对象目录 + 添加机制;复盘为第一用例)· 已定稿(部分暂定)
> 登记:2026-09-08 来源:老师指令(原 R-019 + R-021 合并演进)| 状态:**已定稿(暂定项见文末)**
> 归属:迭代 17-策略会话(Phase 0)| 计划:PLAN-017 | 实现状态:已立项(设计阶段)
## 需求本质(2026-09-08 再定调,老师)
> **总体是:会话(agent)可以获取"神之一手"的数据;复盘只是 agent 获取到对应数据后的一种用法。本需求要确定两件事:① 数据怎么添加到会话 ② 哪些数据可以加。**
> (策略会话、粒度复盘、未来的盘中问答/分析 都是同一能力的消费方式——主干 = 「神之一手数据 → 会话」能力。)
## 一、数据怎么添加到会话(机制)——暂定 2026-09-08
| 维度 | 暂定结论(AI 建议,老师"暂定" | 开放注记 |
|---|---|---|
| 触发动因 | **A:人挑对象加入**——在数据展示处/会话内选一个"数据对象",生成它的数据上下文进会话 | B(agent 按需取数工具)不采纳,维持 R-019"预注入不挂工具"拍板;日后盘中问答需要实时取数时再独立讨论 |
| 注入形态 | 每个对象 = 一条只读「数据上下文」(折叠条目/引用),含快照时间;同对象重复加 = 新快照追加(不覆盖旧),模型以最近为准 | 通道形态待宿主技术调研 |
| 数据时效 | 注入"加入那一刻"的只读快照;要最新 = 重新添加 | — |
## 二、哪些数据可以加(对象目录)——候选目录,首批范围暂定
### 神之一手数据对象目录(候选全量)
账户:账户资金/资产、全部持仓(对账单快照)| 策略:策略(分组下持仓与交易)| 持仓:当前持仓行、历史(已清仓)持仓| 轮次:某一轮交易周期(round,待建模)| 交易:单笔委托/成交| 关系视图:持仓 ↔ 交易| 行情:单 code 现价(随行,不单独成对象)| 汇总:策略汇总/未分配/同步健康 | 未来:决策日志/统计/复盘记录(Phase 1/3
### 首批范围(复盘闭环所需,暂定草案,待老师细调)
| # | 对象 | 加入后 agent 拿到的数据上下文(草案) | 为什么复盘需要 |
|---|---|---|---|
| P1 | 策略 | 策略定义 + 当前持仓 + 关联交易聚合摘要 | 整策略复盘/会话依据 |
| P2 | 持仓(当前/历史) | 持仓完整档案:基本信息+自定义字段+建仓至今买卖链+现价盈亏+(历史=清仓信息) | 复盘一个持仓 |
| P3 | 持仓↔交易关系 | 该持仓全部关联委托/成交(按 holding 聚合) | 追溯买卖过程 |
| P4 | 单笔委托 | 时间/方向/量/价/费用/归属 + 该持仓上下文 | 复盘某次动作 |
| P5 | 轮次 round | 一轮建→平 全动作+结果(需本轮引入 round 建模) | 复盘最小单元(网格/做T 一轮) |
| P6 | 行情随行 | 对象行内现价/涨跌等(不单独成对象;历史 K 线不在本期数据域) | 复盘需当前价对照 |
**候选后补**:账户资产 / 全部持仓对账单 / 策略汇总 / 未分配 / 同步健康(供 Phase 1 盘中辅助与 Phase 2 日结)。
## 三、用例视角(复盘仅是用法之一)
- 复盘 = 挑对象(P1-P6 任意组合,通常 P2/P3/P4/P5)→ 数据上下文进会话 → agent 基于事实线引导复盘 → 结论留会话;
- 其他用例(未来消费方式):盘中状态问答、策略分析、计划讨论——同一"对象目录 + 添加机制"。
## 定稿边界(合并两轮结论)
**做**:数据对象目录 + 添加机制(人挑对象 → 只读数据上下文注入会话);首批对象数据上下文生成;折叠条目呈现;换/刷新/叠加;round 建模(若 P5 首批保留)。
**不做**:DSH 宿主工作区;账本写与交易执行;agent 按需取数工具(暂定);复盘结论落库/统计/决策日志实现(Phase 1/2/3,预留模型位);历史 K 线注入;通用数据视图(全部持仓/交易记录)作为对象目录外的会话依据特例——(对象目录已含"全部持仓",是否启用由首批范围定)。
## 待定/开放项
1. 首批对象范围与 P1-P6 取舍(老师可增删,含是否本期引入 round=P5);
2. 宿主注入通道形态(技术调研);
3. 各对象"数据上下文"的精确字段规格(服务端实现细节,技术方案阶段);
4. 会话侧呈现细节(UI 交互设计延后,老师先前指令)。
## 关联
- 迭代 17-策略会话(Phase 0)| R-010(持仓↔交易关系载体)| 终极目标-002/006/007
- 未来:Phase 1 决策留痕/盘中辅助、Phase 2 日结统计、Phase 3 复盘库(产品逻辑设计 §0.5)
- 归档:R-019 已并入本需求(已完成/R-019.md 为讨论史存档)
## 验收
**(技术方案定稿后补入 04-迭代记录/17-策略会话/验收标准.md)**
+37
View File
@@ -0,0 +1,37 @@
# 需求:R-022 QMT 连接健康灯自适应探测(Bridge 启动/恢复后快速回绿)· 讨论中
> 登记:2026-09-09 | 来源:老师反馈(会话头部 QMT 指示灯)| 状态:**讨论中(AI 提议定稿,待老师确认)**
> 归属:迭代 19-指示灯状态自适应探测(R-022:QMT 健康灯)| 实现状态:已实现(待验收)
## 症状(老师反馈,2026-09-09
启动 QMT Bridge 后,会话头部四个指示灯中「持仓数据 / 行情数据 / MCP」几秒内自动点亮,
唯独「QMT连接」灯一直不亮(停留在红/灰态)——「QMT 连接状态自动检查好像没有生效」。
## 根因
QmtHealthMonitor 只在**插件启动时 + 每 5 分钟**探测一次激活 Bridge /health2026-09-01 迭代 04 定的
「服务端 5 分钟定时探测 + 缓存」机制),结果缓存内存;前端 10s 轮询 sync-status 只读缓存、不触发探测。
其余三灯靠高频自愈回路:PositionSync 10s / QuoteSync 5s 定时拉 Bridge、MCP 客户端自带失败重连,
Bridge 恢复后秒级回绿。唯独健康缓存要等满 5 分钟下一次探测——Bridge 在两次探测间启动/恢复时,
灯长期停留在上一次失败结果,观感即「自动检查没生效」(反向同理:Bridge 中途宕机,灯最多 5 分钟不转红)。
## 方案(AI 提议,老师拍板确认)
**自适应探测节奏(owner = QmtHealthMonitor,单点改动)**
- 健康 → 每 5 分钟探测一次(**稳态,维持原设计低开销**);
- 异常/未知(含从未探测成功)→ **每 10 秒快速重试**(与前端 sync-status 10s 轮询对齐,
与持仓/行情 5-10s 恢复节奏一致)——Bridge 启动后灯 ≤20s 自动回绿。
**边界(不做)**:前端读缓存秒回不变;API/前端零改动;健康稳态间隔不放宽不放窄(5 分钟不变);
不新增配置项(间隔为常量,测试经构造参数注入);不做 read-path 触发探测(定时器已覆盖,避免耦合)。
## 决策记录
| 项 | 结论 | 讨论 |
|---|---|---|
| 异常重试间隔 | 10s | 与前端 10s 轮询对齐;5s 会翻倍失败探测开销(每次失败探测最长挂 5s 超时) |
| 健康稳态间隔 | 5 分钟维持 | 原设计为省探测开销;异常态才快速重试,健康态不空转 |
| 防并发/调度 | setTimeout 链 + _inFlight 去重 | 沿用原防并发语义;手动探测/热切换共用同一节奏重排 |
| baseUrl 解析异常 | 落 healthy:false 缓存走快速重试 | 原实现解析在 try 外,异常会丢缓存导致灯灰且不重试 |
+36
View File
@@ -0,0 +1,36 @@
# 需求:R-023 MCP 状态灯自适应探测(MCP 服务恢复后自动回绿)· 讨论中
> 登记:2026-09-09 | 来源:老师反馈(R-022 之后实测 MCP 灯同样不自动恢复)| 状态:**讨论中(AI 提议定稿,待老师确认)**
> 归属:迭代 19-指示灯状态自适应探测 | 实现状态:已实现(待验收)
## 症状(老师反馈,2026-09-09
QMT 健康灯修复后复查发现:会话头部 MCP 指示灯也存在同样的「状态不自动恢复」——
MCP 服务(QMT Bridge /mcp)启动/恢复后,MCP 灯停留在上次失败态,不会自动回绿。
## 根因
QmtMcpManager 的**状态缓存只在 挂载 / 配置切换 / 手动探测(点击「检测 MCP」)时刷新**,无任何定时回路;
sync-status 端点每 10s 读的只是这份缓存。dsh-mcp-client 自带的 reconnect 只恢复**真实连接**
(fiber 层),并不刷新插件自己的状态缓存——所以「连接恢复了、灯还是红的」。
## 方案(AI 提议,与 R-022 同模式,owner = QmtMcpManager
**结果驱动自适应探测**(对齐 QmtHealthMonitor):
- connected → 每 5 分钟探测一次(稳态,低开销);
- 非 connectederror/异常)→ **每 10 秒快速重试**——MCP 服务恢复后灯 ≤20s 自动回绿;
- 无激活连接(url 为空)不探测不调度;dispose() 后停止调度;
- 探测仍走 SDK 独立只读握手(Client connect → initialize + tools/list),与 dsh-mcp-client 自管连接互不干扰。
**边界(不做)**:不接管/不替代 dsh-mcp-client 的重连(那是真实连接层,保持现状 Q5);
不新增配置项(间隔为常量,测试经构造参数注入);前端/API 零改动(getStatus 读缓存语义不变)。
## 决策记录
| 项 | 结论 | 讨论 |
|---|---|---|
| 非连接态重试间隔 | 10s | 与前端 sync-status 10s 轮询、R-022 QMT 灯一致;MCP 探测最坏挂 8s 超时,快失败场景成本可忽略 |
| 已连接稳态间隔 | 5 分钟 | 与 R-022 一致;MCP 握手比重(initialize+tools/list),健康态不空转 |
| 调度停止条件 | 无激活连接 / dispose / unmount | 无 url 时探测早退置 disabled 并取消定时器,避免空转 |
| 并发防护 | _probing 去重(同 QmtHealthMonitor._inFlight | 定时器与手动/热切换探测可能撞车 |
+40
View File
@@ -0,0 +1,40 @@
# 需求:R-025 设置页 Tab 设置排序落点统一为间隙高亮线 · 已定稿
> 登记:2026-09-10 | 来源:老师指令(迭代 20 复盘遗留第 2 项确认)| 状态:**已定稿(2026-09-10 老师指令)**
> 归属:迭代 21-Tab设置排序间隙高亮统一 | 实现状态:未开始
## 诉求(老师,2026-09-10
> 神之一手设置页的分组 tab 排序(Tab 设置),也改一下上面(列设置弹层)的拖动高亮显示形式。
## 背景
- R-024/迭代 20 列设置弹层拖拽落点已从整行高亮改为**行间间隙高亮线**(UI约束-007),老师确认「直观多了」;
- 复盘遗留第 2 项:设置页「Tab 设置」的排序仍是整行高亮(overId → 行淡蓝底,UI约束-003),存在同样的
「无法判断插入前/后」观感问题;老师指令统一为间隙线形式。
## 定稿结论(2026-09-10 老师指令)
设置页「Tab 设置」排序落点从整行高亮改为**行间间隙高亮线**(复用 UI约束-007 / R-024 已验证模式):
- 拖动悬停时按鼠标在目标行内的 Y 坐标(上半/下半)判定,间隙线画在目标行上缘(插其前)/下缘(插其后);
- drop 事件内按坐标重算插入位置(不依赖 state 闭包);拖回原位置(to===from 或 to===from+1)跳过写库;
- 保留 onDragEnd 清理残留;整行高亮背景(color-mix 12%)移除;
- 其余维持不变:≡ 手柄、显隐开关、落点立即持久化(tabs/update)、「刷新页面后生效」提示、内置/策略徽标、
saving 期间禁用拖拽。
## 决策记录
| 项 | 结论 | 讨论 |
|---|---|---|
| 范围 | 仅 Tab 设置(SettingsSection.jsx TabSettings)落点指示 | 老师指令;不动表格结构、不动持久化、不动开关 |
| 落点形式 | 行间间隙高亮线(上缘=前/下缘=后) | 与 R-024 弹层全同,UI约束-007 已定义 |
| 判定 | 目标行内 Y 坐标上下半区 | 与 R-024 全同 |
| 无操作 | to===from 或 to===from+1 跳过写库 | 复用 R-024 数学式 |
| 服务端/API | 零改动 | tabs/update 整表语义不变 |
| 约束更新 | UI约束-003 变更:落点从整行高亮改间隙线;UI约束-007 补「Tab 设置已统一」 | 2026-09-10 老师指令 |
## 关联
- 上游:UI约束-007(排序交互间隙线标准)、R-024(同款模式实证)、迭代 20 复盘遗留第 2 项;
- 迭代 20 复盘遗留第 1 项(test-r013-columns 期望过期)不在本需求范围。
+127
View File
@@ -0,0 +1,127 @@
# 需求:R-026 策略计算字段(公式字段)· 已定稿
> 登记:2026-09-10 来源:老师指令(2026-09-10 功能讨论「策略组字段配置优化」)| 状态:**已定稿(2026-09-10 老师确认)**
> 归属:**22-策略计算字段与策略tab静态化 · 第二阶段**(PLAN-019)| 实现状态:**已实现(2026-09-10 完成,typecheck/build/17 个回归脚本全绿),待老师人工验收**
> **入口条款修订(2026-09-10,R-027 取代)**:字段配置入口由「设置页 → 策略分组 → 策略行展开」**迁至「策略 tab 内、列设置旁的『字段配置』弹层」**;本文档 §9 配置 UI 的入口位置随 R-027 变更,字段模型 / 公式语义 / 只读列 / 列机制均不变。
> 前身:草稿 T-010(已转正,草稿文件已清理)
## 诉求(老师,2026-09-10
> 策略组字段配置优化:添加的字段,目前仅是支持用户指定值;接下来添加新的字段类型,就是**可以计算**,
> 目标是让用户**写一个公式**,公式中支持来自几个**服务端缓存的数据集**,然后通过计算得出结果。
## 背景(现状 R-013/迭代 11
- 策略字段定义 = `settings.strategies[].configSchema``{ key, label, type(text|number|boolean|enum), enum[], def, unit }`
- 字段值 = `strategy_holdings.values` JSON 列(**用户手工填写**,单元格内联编辑);
- 展示 = 持仓表列化(列显隐/顺序由 `strategyColumns` 每策略独立配置);
- 本次扩展:新增**第五种字段类型「计算(formula)」**——值不是填的,是**算的**。
## 定稿结论(2026-09-10 老师逐项确认)
### 1. 字段类型与存储语义
- `configSchema.type` 增加 `'formula'`;新增 `formula` 表达式串字段(与 `unit` 同级,随策略定义存 settings);
- **计算字段不落库**:不读写 `strategy_holdings.values`,每次读 `strategy-positions` 时**逐行实时现算**
- 表格内**只读**(点击不进内联编辑);列配置兼容零改动(formula 字段自动进列集,新增默认追加末尾)。
### 2. 公式可引用的数据集(首批 = 行情/合约 + 持仓账本 + 本行自定义字段)
| 数据集 | 来源(服务端缓存) | 可引用字段 | 刷新 |
|---|---|---|---|
| 行情盘口 | QuoteHub.quotes(内存) | 现价 / 昨收 / 今开 / 最高 / 最低 / 成交量 / 成交额 | 5s REST |
| 合约信息 | QuoteHub.instruments(内存) | 涨停价 / 跌停价 / 昨收 | 按交易日 |
| 持仓账本 | SQLite strategy_holdings | 策略份额 / 成本价 / 最后一笔成交价 | 交易关联驱动 |
| 本行自定义字段值 | strategy_holdings.values | 本行手填字段(如网格间距、目标价) | 内联编辑 |
**后补(本期不做)**:QMT 账户域持仓(全账户 volume/可用/市值,跨策略视角)、交易记录聚合(当日已成交量/买卖笔数)——用例尚不明确。
### 3. 变量命名:**中文变量名**
- 公式形如 `(现价 - 成本价) × 份额`,可读性优先;
- 配置时校验**变量名唯一**;配置 UI 提供**变量选择器**(按数据集分组,点击插入);
- 引擎需支持中文标识符(自写解析器,见 §4)。
### 4. 公式引擎:自写小型表达式解析器(零依赖)
- 能力:四则运算 + 括号 + 常见函数(round / abs / min / max);
- 选型理由:中文标识符需自定义词法,第三方库(expr-eval/mathjs/jexl)均不支持中文变量;该层未来即 T-006 数据池的表达式层,值得自有。
### 5. 计算位置与粒度:**服务端计算 + 仅行级**
- 服务端在 `strategy-positions` 返回时逐行现算随行带回(行情单一入口 QuoteHub 在服务端);前端仅渲染;
- 行级上下文 = 本行行情 + 本行持仓账本 + 本行自定义字段值 → 一个结果;
- **策略级聚合(跨行求和,如本策略总盈亏)留二期**。
### 6. 循环引用边界:公式**只允许引用非公式字段**
- 变量目录直接排除 formula 字段 → 不存在循环依赖(不做检测与拓扑排序)。
### 7. 结果类型与格式
- 首批结果**只做数字**(布尔型公式后续再议);
- 复用现有 `unit`(展示拼在值后);新增**小数位**配置(默认 2 位)。
### 8. 缺数据与错误处理
- **保存时校验**:语法 + 变量存在性(服务端编译试算一次),非法公式直接拒绝保存并提示;
- **运行期**:缺行情 / 除零 / 无法计算 → 该单元格显示 `—`(不显示 NaN、不报错、不炸整表)。
### 9. 配置 UI(设置页「策略分组」字段表单)
- 类型下拉增加「计算」→ 出现公式输入框 + 变量选择器(分组:行情 / 合约 / 持仓 / 自定义字段)+ 变量速查;
- 加分项:**试算预览**(取一行真实持仓数据现场算出结果)。
### 10. 历史持仓行(已清仓)
- 历史 tab 的公式列统一显示 `—`:清仓行无「现价」语义,**不做历史时点计算**(属另一需求,不混入本期)。
### 11. 与服务端数据池(T-006)的关系
- 本期先落**轻量变量目录**(服务端声明:数据集 → 字段清单 → 取数函数),公式引擎跑在目录之上;
- 该目录即 T-006「数据池中间层」的第一块砖,**不一次性建完整数据池**(避免过度设计)。
## 典型用例(老师视角验收样例)
| 字段 | 公式 |
|---|---|
| 浮动盈亏 | `(现价 - 成本价) × 份额` |
| 盈亏比例 | `(现价 ÷ 成本价 - 1) × 100` |
| 距涨停 | `(涨停价 - 现价) ÷ 现价 × 100` |
| 网格占用(手填+计算组合) | `现价 × 份额 × 网格间距` |
## 决策记录
| 项 | 结论 | 讨论 |
|---|---|---|
| 字段类型 | configSchema.type 加 `formula` + `formula` 串 | 老师指令(新字段类型) |
| 存储语义 | **不落库**,读接口时逐行现算 | 「计算」的本质;values 列不参与 |
| 数据集范围 | 首批 行情+合约 / 持仓账本 / 本行自定义字段值 | 选择题确认(1+2+3);QMT账户域、交易聚合后补 |
| 变量命名 | **中文变量名** + 选择器插入 | 选择题确认;配置时校验唯一 |
| 计算粒度 | **仅行级**;策略级聚合二期 | 选择题确认 |
| 计算位置 | 服务端(strategy-positions 现算随行返回) | 老师方向「引用服务端缓存数据集」;行情单一入口在服务端 |
| 公式引擎 | 自写小型解析器(零依赖,支持中文标识符) | 第三方库不支持中文变量;未来即数据池表达式层 |
| 循环引用 | 只允许引用非公式字段 | 目录层排除,无循环问题 |
| 结果类型 | 数字 + unit + 小数位(默认 2) | 布尔型后议 |
| 错误处理 | 保存校验拒绝 + 运行期显示 `—` | 不炸整表 |
| 配置 UI | 公式输入 + 变量选择器(+试算预览) | 计算字段只读不内联编辑 |
| 历史行 | 公式列显示 `—` | 不做历史时点计算 |
| 数据池关系 | 轻量变量目录先行(T-006 第一块砖) | 避免一次性建完整中间层 |
## 关联
- 上游:R-013(自定义字段配置,字段类型与列机制)/ 迭代 11(列配置 strategyColumns);
- 数据来源:R-015QuoteHub 盘口内存快照)、R-014PositionSync 账户域)、R-010(最后一笔成交价)、R-018(份额账本);
- 延展:T-006(数据池中间层草稿)——本需求的变量目录为其起点;
- 约束条目待实施时补充(拟:产品功能约束 / 技术方案约束 / UI交互约束 各一条,沿用 R-013 惯例)。
## 讨论过程
- 2026-09-10 老师提出功能方向(策略组字段配置优化 → 可计算公式字段);
- AI 摸清 R-013 现状 + 盘活服务端缓存数据集清单,提出 Q1-Q10 设计问题清单(各附建议);
- 老师「定」:Q4-Q10 按 AI 建议;
- 选择题三项确认:Q1 数据集 1+2+3、Q2 中文变量名、Q3 仅行级计算;
- 转正定稿:T-010 草稿 → R-026(草稿文件已清理,索引同步);
- 2026-09-10 迭代 22 交互设计过审:老师逐项拍板 D-1~D-7(全采纳 AI 建议)→ `docs/04-迭代记录/22-策略计算字段与策略tab静态化/UI交互设计.md` 定稿;
- 2026-09-10 老师提出重构想法(策略 tab 静态化 + 字段配置入口迁移)→ 定稿 **R-027**:本需求的**字段配置入口**随之从设置页迁到「策略 tab 内、列设置旁的『字段配置』弹层」(迭代 22 调整为「第一阶段=重构、第二阶段=本需求」);
下一步:技术实现方案 + 验收标准(本轮按老师指令未做技术方案与编码)。
+85
View File
@@ -0,0 +1,85 @@
# 需求:R-027 策略 tab 静态化 + 字段配置入口迁移 · 已定稿
> 登记:2026-09-10 来源:老师指令(2026-09-10 功能讨论「重构想法」)| 状态:**已定稿(2026-09-10 老师逐项确认)**
> 归属:**22-策略计算字段与策略tab静态化 · 第一阶段**(PLAN-019)| 实现状态:**已实现(2026-09-10 完成,typecheck/build/17 个回归脚本全绿),待老师人工验收**
> 前身:草稿 T-011(已转正,草稿文件已清理)
> **取代**R-003(策略 CRUD)、R-011(Tab 统一管理中的「策略条目动态化」部分)、R-013(字段定义入口在设置页部分)
## 诉求(老师,2026-09-10
> 1. 策略 tab 我不想做成动态可增减修改的了,改成静态的,和全部持仓、交易记录这些一样,都是插件内置的 tab;
> 2. 这两个策略 tab 原有的字段编辑功能,切换到主窗口的策略 tab 下面,列设置旁边,作为统一的字段配置管理。
## 现状核实(2026-09-10 实机,代码 + settings.yaml + store.db
- 策略仅两个内置:`grid-supermarket`(网格超市,5 字段:网格上边界/下边界/基准值/网格大小/网格交易量)、`manual-t`(手动做T1 字段:T仓成本价);
- `store.db``strategy_holdings` 13 / 3 行,`trade_orders``trade_order_attributions` 均只涉及这两个策略 → **无自建策略,零数据迁移风险**
- `settings.tabs` 现状:网格超市(0) → 手动做T(1) → 交易记录(2) → 关注列表(3,隐藏) → 全部持仓(4,隐藏);
- 现状实现:`addStrategy/removeStrategy` 联动 `appendStrategyTab/removeStrategyTab`(R-011);字段编辑 = 设置页「策略分组」子 tab 内 `StrategyFieldsEditor`,保存走 `strategies/update` 整表。
## 定稿结论
### 1. 策略 tab 静态化(Q1
- 策略**集合与名称固定**(插件内置两项,语义等同 `BUILTIN_TABS`):**不可新增、不可删除、不可重命名**;
- **保留**「Tab 设置」里的**显隐开关 + 拖拽排序**(策略 tab 与内置 tab 同权混排)——老师现有偏好(策略 tab 置顶、全部持仓/关注列表隐藏)继续有效;
- 策略 CRUD 能力与 `addStrategy / removeStrategy / appendStrategyTab / removeStrategyTab / generateStrategyId`(拼音 slug 一整套)**整体退役**。
### 2. 设置页「策略分组」子 tab 移除(Q2)
- 设置页收敛为两个子 tab:**Tab 设置 / QMT 连接配置**
- 移除策略「新增 / 重命名 / 删除」入口与删除确认弹窗。
### 3. 字段配置入口迁移(Q3 + Q4)
- 入口 = 主窗口**策略 tab 顶部、「列设置」旁的「字段配置」按钮**;每个策略 tab 管理**本策略**字段(符合「字段定义随策略」语义);
- 点击打开**独立弹层**:字段列表(展示名 / key / 类型 / 公式或默认值 / 单位 / 操作)+ 添加/编辑/删除字段 + **保存**
- 与「列设置」职责分离:列设置 = 轻量即时持久化(显隐/顺序);字段配置 = 重表单 + 保存按钮;
- 字段定义增删 → 表格列自动跟随(沿用 R-013 归一化机制,零改动)。
### 4. 保存机制(Q5,技术项 AI 定)
- 新增**单策略字段端点**(拟 `strategies/schema-update { strategyId, configSchema }`):只改本策略字段定义,避开 `strategies/update` 整表覆盖风险;
- 保存成功 → Toast + 刷新该策略数据(列与字段同步跟随)。
### 5. 存储结构简化(Q7,技术项 AI 定,技术方案阶段定形)
- `strategies` **常量化为插件内置两项**id/name 常量);
- settings 只保留可变部分:**字段定义**(按 strategyId+ **strategyColumns** 列配置覆盖;
- 存量 settingsstrategies 数组 + configSchema)读取时**归一化迁移**(不写独立迁移脚本);本次实机无自建策略,无需数据处理。
### 6. 排期(Q6
- **并入迭代 22 作第一阶段**(静态化 + 入口迁移),计算字段(R-026)作**第二阶段**——一次改到位,公式表单直接落在新入口,避免「先按设置页做一遍再整体搬迁」的返工;
- 迭代 22 的 `UI交互设计.md` **同步修订**(公式表单移入「字段配置」弹层)。
## 影响面 / 约束联动
- **约束修订**(实施时执行并记录):产品约束-002/003/004(标签体系 / 标签自定义 + 显示开关的场景收敛)、产品约束-009(Tab 统一管理中「策略条目动态化」条款)、UI约束-002(设置页子 tab 构成:移除策略分组)、UI约束-003(Tab 设置:策略行不再随 CRUD 增删)、UI约束-005(字段配置 UI 入口迁移);
- **历史需求追溯**:R-003(策略 CRUD)→ 被取代退役;R-011(Tab 统一管理)→ 策略条目动态化部分被取代,显隐/排序机制保留;R-013 → 字段模型与列机制保留,**入口条款**被取代。
## 决策记录
| 项 | 结论 | 讨论 |
|---|---|---|
| 静态化程度 | 固定不可增删改名 + **保留**显隐/排序 | Q1(采纳建议 A)|
| 设置页「策略分组」 | 整体移除 | Q2(A)|
| 入口形态 | 「列设置」旁独立「字段配置」按钮 + 独立弹层 | Q3(A)|
| 「统一」范围 | 每个策略 tab 管自己的字段 | Q4(A)|
| 保存机制 | 单策略字段端点(AI 定,技术项)| Q5 |
| 排期 | 并入迭代 22 第一阶段,一次改到位 | Q6(A)|
| 存储结构 | strategies 常量 + settings 存字段/列配置(AI 定,方案阶段定形)| Q7 |
## 讨论过程
1. 2026-09-10 老师提出重构想法(策略 tab 静态化 + 字段编辑功能迁到策略 tab 内、列设置旁);
2. AI 核实实机现状(settings.yaml 策略与字段、store.db 数据归属、tabs 顺序)→ 结论:仅两个内置策略、无自建策略,**零数据迁移风险**;
3. AI 给出影响面分析(tab 体系 / 设置页 / 字段配置 / 约束与历史需求)+ 与迭代 22 的撞位提示(公式表单落在设置页,入口一搬即废)+ 排期建议;
4. 老师逐项拍板 Q1~Q4、Q6(全采建议);Q5/Q7 为技术项由 AI 定;
5. 转正定稿:T-011 草稿 → R-027(草稿文件已清理,索引同步);迭代 22 结构与交互设计同步修订。
## 关联
- 上游:T-011 草稿、实机核实(`settings.yaml` + `store.db`);
- 联动:**R-026 / 迭代 22**(入口迁移后,计算字段的公式表单落在「字段配置」弹层内);R-013(字段模型 / 列机制沿用)、R-011(Tab 显隐排序机制沿用);
- 取代:R-003、R-011(部分)、R-013(入口条款)。
+54
View File
@@ -0,0 +1,54 @@
# 需求:R-028 判定型计算字段 + 网格超市「可下空单 / 可下多单」· 已定稿
> 登记:2026-09-10 来源:老师指令(2026-09-10)| 状态:**已定稿(2026-09-10 老师逐项确认 Q1-Q5**
> 归属:**23-计算字段判定型扩展** | 实现状态:**已实现(2026-09-10),待老师人工验收**
> 前身:草稿 T-012(已转正,草稿文件已清理)| **能力扩展**:R-026(计算字段,原「首批结果只做数字」)→ 视同修订 产品约束-014
## 诉求(老师,2026-09-10
> 我想给网格超市添加一个计算字段,即当天涨停值 > 当前基准值 + 1 ,即可以下空单;当天跌停值 < 当前基准值 - 1,可以下多单。
## 现状限制(R-026 能力边界)
计算字段原**只产出数字**(产品约束-014「首批结果只做数字」),公式能力 = 四则 + 括号 + round/abs/min/max
**没有比较运算** → 上述两条规则无法直接表达为判定列。可用变量已覆盖全部原料
`涨停价`/`跌停价` = QuoteHub 合约信息按交易日;`基准值` = 网格超市手填字段)。
## 定稿结论(2026-09-10 老师拍板)
1. **方案 A|判定型计算字段**(Q1):引擎新增比较运算与逻辑组合,字段表单新增「结果类型:数字 / 判定」;
2. **判定列显示**Q2):满足 → `✓ <字段名>`(success 绿);不满足 / 缺值 → `—`tertiary 灰);
3. **支持 and / or 组合**Q3,含 `&&` / `||` 与全角 `≥ ≤ ≠`);
4. **结果类型由字段表单显式声明**(Q4),服务端校验「声明与公式形态一致」(判定=顶层比较/逻辑,数字=其余);
5. **阈值引用「网格大小」字段**Q5):公式写 `涨停价 > 基准值 + 网格大小`(阈值可配;当前各行网格大小=1,与写死 1 等价)。
**网格超市落地字段(两条)**
| 展示名 | 类型 | 结果 | 公式 | 单位 |
|---|---|---|---|---|
| 可下空单 | 计算 | 判定 | `涨停价 > 基准值 + 网格大小` | — |
| 可下多单 | 计算 | 判定 | `跌停价 < 基准值 - 网格大小` | — |
## 可行性验证(2026-09-10,真实持仓 + QMT 合约信息)
13 只网格超市当前持仓实算:**可下空单 5 只**(积成电子 8.22>8、天海防务 7.9>7、万顺新材 7.73>7、
大连热电 8.51>8、华智数媒 9.28>8);**可下多单 2 只**(中国化学 6.68<7、TCL中环 8.2<9);其余 6 只两条均不满足。
## 决策记录
| 项 | 结论 | 讨论 |
|---|---|---|
| 结果形态 | 判定型计算字段(方案 A) | Q1(老师选 A) |
| 判定显示 | 满足 `✓ 字段名`(绿)/ 不满足 `—`(灰) | Q2 |
| 组合条件 | 支持 and / or | Q3 |
| 结果类型 | 表单显式声明,服务端校验一致性 | Q4 |
| 阈值 | 引用「网格大小」字段 | Q5 |
| 存储/现算 | 结果可为布尔(`computed[key]` 布尔直通);仍不落库 | 沿用 R-026 语义 |
| 成本 | 引擎 + 校验 + 表单 + 渲染四处小改;无新增数据通路 | — |
## 关联
- 上游:R-026 / 迭代 22(计算字段:引擎、变量目录、现算、只读列);
- 修订:产品约束-014(结果类型扩展为 数字 / 判定)、技术约束-022(引擎运算集)、技术约束-024(校验规则)、UI约束-008(判定 UI);
- 迭代:23-计算字段判定型扩展(含网格超市两字段落地);
- 使用说明(用户手册):`docs/99-其他材料/计算字段公式说明.md`(变量表 / 运算符 / 函数 / 示例 / 报错对照 / 常见问题)。
+145
View File
@@ -0,0 +1,145 @@
# R-009 交易记录本地存储(SQLite)+ 策略关联 · 已完成
> 归档日期:2026-09-01 需求状态:**已完成**
> 原索引:docs/05-需求池/需求池索引.md(主索引保留 R-009 条目,指向本归档)
> 实现迭代:07-交易记录本地存储SQLite与策略关联(验收通过,迭代复盘见 docs/04-迭代记录/07-交易记录本地存储SQLite与策略关联/迭代复盘.md)
> 关联计划:PLAN-008docs/02-计划/计划-交易记录本地存储SQLite与策略关联.md)
> 讨论记录:Q1-Q8 定稿(2026-09-01);Q3 二次修正定稿(2026-09-01):归属由用户手动设置(trade_orders 冗余 strategy_id + holding_idUPSERT 不覆盖归属列,Q1-Q4 确认)
---
> 状态:**已定稿**(2026-09-01,老师确认)| 登记日期:2026-09-01
> 来源:老师指令(2026-09-01
> 优先级:P1
> 关联:R-007(交易记录接入)、R-008SQLite 存储)、docs/03-设计约束/数据存储设计.md §9(holding_id 关联锚点预留)、迭代 06 复盘遗留项 2
## 需求描述
在 SQLite 中新增**交易记录表**(委托 + 成交),将 QMT Bridge 当日交易数据**本地持久化**(跨日积累,形成本地历史库),并建立与插件**策略体系**的关联,支持**按策略过滤 / 复盘**交易。**归属由用户在交易记录 tab 手动设置(冗余存 strategy_id + holding_id),UPSERT 不覆盖归属列。**
- 承接 R-007 的 Q5(本地持久化,本期不做)与 Q10(按策略过滤,后续迭代);
- 落实迭代 06 复盘遗留项 2:「strategy_holdings.holding_id 已就绪,可作为 trades 表关联锚点(R-007 实现时建 trades 表)」;
- 顺带解决 R-007 历史范围「接口开发中」占位问题(本地积累后历史可查)。
## 现状(代码审查 2026-09-01
| 项 | 现状 |
|---|---|
| 交易展示 | TradeRecordsTab 接 QMT 当日委托(/trade/orders+ 当日成交(/trade/trades),单表合并展示,**仅实时、不落盘** |
| 历史范围 | 非今日范围前端占位「历史数据接口开发中」(QMT Bridge 无历史接口,get_trade_detail_data 读客户端缓存仅当日) |
| SQLite | 迭代 06 落地:strategy_holdings(持仓生命周期:holding_id 自增 + created_at/closed_at+ market_quotes_cache 两表;holding_id 预留为交易关联锚点 |
| 策略 | 策略=标签(settings),持仓份额存 strategy_holdings;同码可跨多个策略(部分唯一索引仅约束同策略同码一笔当前持仓) |
## 待讨论点(Q1-Q8,AI 建议见各条)
### Q1 存储粒度与表结构(AI 建议:委托 + 成交两表)
```sql
-- 交易委托(委托主行;order_id 唯一,UPSERT 幂等;零冗余:不存 strategy_id/holding_id
CREATE TABLE IF NOT EXISTS trade_orders (
order_id TEXT PRIMARY KEY, -- m_strOrderSysID
trade_date TEXT NOT NULL, -- 交易日 YYYYMMDDm_strInsertDate
code TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
exchange TEXT NOT NULL DEFAULT '',
direction TEXT NOT NULL DEFAULT '', -- buy / sell
direction_code INTEGER,
opt_name TEXT NOT NULL DEFAULT '',
status INTEGER,
order_volume REAL NOT NULL DEFAULT 0,
traded_volume REAL NOT NULL DEFAULT 0,
limit_price REAL NOT NULL DEFAULT 0,
traded_price REAL NOT NULL DEFAULT 0,
amount REAL NOT NULL DEFAULT 0,
insert_date TEXT NOT NULL DEFAULT '',
insert_time TEXT NOT NULL DEFAULT '',
insert_ts INTEGER NOT NULL, -- 派生列:insert_date+insert_time 合成毫秒时间戳(join 持仓窗口用)
cancel_info TEXT NOT NULL DEFAULT '',
error_msg TEXT NOT NULL DEFAULT '',
fetched_at INTEGER NOT NULL -- 同步时间戳
);
-- 交易成交(成交明细;trade_id 唯一,order_id 外键关联委托;零冗余:不存 strategy_id/holding_id
CREATE TABLE IF NOT EXISTS trade_fills (
trade_id TEXT PRIMARY KEY, -- m_strTradeID
order_id TEXT NOT NULL, -- → trade_orders.order_id
trade_date TEXT NOT NULL,
code TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
exchange TEXT NOT NULL DEFAULT '',
direction TEXT NOT NULL DEFAULT '',
direction_code INTEGER,
opt_name TEXT NOT NULL DEFAULT '',
price REAL NOT NULL DEFAULT 0,
volume REAL NOT NULL DEFAULT 0,
amount REAL NOT NULL DEFAULT 0,
commission_rate_wan REAL NOT NULL DEFAULT 0,
trade_time TEXT NOT NULL DEFAULT '',
fetched_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_trade_orders_date ON trade_orders (trade_date);
CREATE INDEX IF NOT EXISTS idx_trade_orders_ts ON trade_orders (insert_ts);
CREATE INDEX IF NOT EXISTS idx_trade_fills_order ON trade_fills (order_id);
CREATE INDEX IF NOT EXISTS idx_trade_fills_date ON trade_fills (trade_date);
```
- 理由:委托 1:N 成交,两表忠实 R-007「单表合并」模型(前端 mergeOrdersAndTrades 可复用);两表能保留无成交委托(待报/已撤/废单),复盘完整;避免 JSON 压扁。
- **2026-09-01 老师修正:两表都不冗余 strategy_id / holding_id**(外键链推导,见 Q3);trade_orders 增加派生列 insert_tsinsert_date+insert_time 合成,join 持仓窗口用)。
### Q2 存储时机 / 同步机制(AI 建议:服务端定时同步 + API 写穿)
- 服务端 TradeSync 模块定时(建议 60s)拉取今日 orders+trades → UPSERT 落库(幂等);
- 前端今日轮询命中 orders/trades 端点时同步写穿(机会式);
- 插件启动时同步一次(预热今日数据);
- 理由:不依赖前端开 tab 也持续积累;UPSERT 幂等不重复。
### Q3 策略关联方式(2026-09-01 二次修正定稿:手动设置归属)
**最终定稿(老师拍板)**:关联链 = 成交(fill) → 委托(order) → 策略(strategy) → holding(持仓 ID),但**归属由用户手动设置,不靠算法推导**。
- **信息源 = 人**:交易记录 tab 加载每日交易记录,用户手动设置每笔委托的归属(下拉候选 = 该 code 的当前持仓策略 + 未关联);符合「人机合一」目标-007(AI 给候选,人做最终决定);
- **trade_orders 冗余存 strategy_id + holding_id**Q1 老师确认:加冗余字段,便于过滤/展示/复盘);
- **UPSERT 不覆盖归属列**(Q2 老师确认):归属是手动指定的、是插件逻辑,TradeSync 定时同步只更新 QMT 原始字段,保留已有归属;
- **全手动选**(Q3 老师确认):不自动预填候选值,用户明确选择后才落库;未设置前归属为空(未关联);
- **可修改**(Q4 老师确认):已设置的归属可随时改,以最终修改为准;
- 成交明细跟随委托归属(不单独设置);
- 归属推导逻辑(方案 A 时间窗 + 份额最大)**降级为候选列表来源**:一票一策略时给出该策略候选、一票多策略时给出全部候选让用户选、无持仓时仅「未关联」。
### Q4 历史查询能力(AI 建议:新增本地查询端点,策略过滤走 FK 链 join)
- 新增端点(如 trades/history):按 { start, end, code, strategyId, direction } 查本地 SQLite,返回 { orders, fills }
- strategyId 过滤 = trade_orders JOIN strategy_holdings(生命周期窗口含委托时间,取份额最大持仓)筛选 strategy_id
- 前端历史范围从「占位」切换为查本地库(数据=插件运行期间逐日积累;插件停运期间无数据,诚实记录不可补);
- 今日仍走 QMT 实时(更新鲜),同步写穿本地;QMT 不可用时可用本地兜底(可选)。
### Q5 同步范围与数据积累(AI 建议)
- 只同步「当日」数据(QMT 能力所限,无法回溯补历史);
- 数据逐日积累 = 本地历史库;不做自动清理(复盘需要历史),导出/清理后续再说。
### Q6 UI 扩展(AI 建议:策略过滤)
- TradeRecordsTab 加策略过滤下拉(全部 / 各策略 / 未关联);
- 历史范围展示本地数据(不再占位);按策略过滤对今日(实时)与历史(本地)均生效。
### Q7 委托状态更新(AI 建议:UPSERT 覆盖)
- 同一 order_id 再次同步时更新 status / 成交量等(当日委托状态流转:已报→部成→已成);
- 历史日状态 = 最后一次同步快照(当日收市时基本已终态,可接受)。
### Q8 文档与约束(后续执行)
- 数据存储设计.md 增补交易记录表设计(§10);技术方案约束新增条目;产品约束按需(策略过滤 UI)。
## 边界(初拟,待讨论收敛)
**做**:两表落地(trade_orders + trade_fills,零冗余 strategy_id/holding_id+ 定时同步落库(幂等)+ 本地历史查询端点(策略过滤 = 委托时间 join 持仓生命周期窗口)+ tab 策略过滤 + 历史范围本地展示。
**不做**(本期):手动修正策略关联入口(后续);导出/清理管理界面(后续);QMT 历史接口接入(老师完善后再接);下单/撤单操作(目标-007 另议)。
## 讨论与修正记录
- 2026-09-01 老师提出需求,AI 登记(讨论中),提出 Q1-Q8 建议待确认。
- 2026-09-01 老师反馈(Q3 关联方式修正):关联链 = 成交 → 委托 → 策略 → holding。成交只与委托相关(order_id 外键即可);委托与策略相关;策略对应具体 holding_id。**两表均不冗余 strategy_id + holding_id** —— 采用零冗余 + 查询时按持仓生命周期时间窗 join 推导(Q3 定稿)。
- 2026-09-01 老师确认 Q1-Q8 全部定稿:两表零冗余 + 外键链关联(Q3)+ 定时同步 UPSERT + 本地历史查询 + tab 策略过滤 + 只同步当日;**R-009 转已定稿,进入迭代 07 实施**。
- 2026-09-01 老师二次定稿(Q1-Q4):归属由用户在交易记录 tab **手动设置**——trade_orders 冗余存 strategy_id + holding_idQ1);**UPSERT 不覆盖归属列**(Q2,手动指定为插件逻辑);**全手动选**(Q3,不自动预填候选值);**可随时改、以最终为准**(Q4);算法推导降级为候选列表来源。
+63
View File
@@ -0,0 +1,63 @@
# R-010 策略持仓行展开关联交易记录(Holding → 交易汇总)· 已完成
> 归档日期:2026-09-02 需求状态:**已完成**
> 原索引:docs/05-需求池/需求池索引.md(主索引保留 R-010 条目,指向本归档)
> 实现迭代:08-策略持仓行展开关联交易记录(老师确认,迭代复盘见 docs/04-迭代记录/08-策略持仓行展开关联交易记录/迭代复盘.md)
> 关联计划:PLAN-009docs/02-计划/计划-策略持仓行展开关联交易记录.md)
> 讨论记录:Q1-Q3 定稿(2026-09-02),补充 Q4(成本价 + 最后一笔成交价)确认
---
> 状态:**已定稿**2026-09-02,老师确认 Q1-Q3)| 登记日期:2026-09-02
> 来源:老师指令(2026-09-02
> 优先级:P1
> 关联:R-009(交易记录本地存储 + holding_id 关联)、迭代 07
## 需求描述
策略持仓 tab(如「网格超市」「手动做T」)中,**每个持仓记录(Holding)行可展开**,展开后显示**与该 holding 关联的交易记录**(仅委托汇总,不分笔成交)。
- 展开效果类似交易记录 tab 的展开效果;
- 展开内容只显示**委托汇总**(一笔委托一行,聚合成交信息),**不显示分笔成交明细**;
- 关联依据 = trade_orders.holding_idR-009 已建立:用户手动设置归属时写入 holding_id)。
## 现状(代码审查 2026-09-02
| 项 | 现状 |
|---|---|
| 策略持仓 tab | StrategyTab.jsx:表格展示策略持仓(代码/名称/现价/策略份额/总持仓/操作),行不可展开 |
| 持仓数据 | strategy-positions 端点返回 QMT 持仓 + shares**不含 holding_id**(需附加) |
| holding 关联 | trade_orders.holding_idR-009 手动归属写入),按 holding_id 可精确查询交易 |
| 交易汇总 | trade_orders 单行即委托汇总(order_volume/traded_volume/amount/费用等),天然是汇总 |
## 待讨论点(Q1-Q3,AI 建议见各条)
### Q1 持仓行如何带出 holding_idAI 建议:strategy-positions 附加 holding_id
- 方案:PositionManager.getStrategyPositions 返回时,查 strategy_holdings 附加 holding_id(同策略同 code 的当前持仓);
- 一票多策略:当前策略下的该 code 只有一个当前持仓(idx_active_holding 约束),可精确匹配;
- 需确认:是否接受服务端附加 holding_id 到 strategy-positions 响应?
### Q2 查询端点(AI 建议:新增 trades/by-holding 端点)
- 新增端点:按 holding_id 查 trade_orders(返回委托汇总列表,含状态/方向/量/价/金额/时间);
- 复用 SqliteStore 查询(WHERE holding_id=? ORDER BY insert_ts DESC);
- 需确认:端点命名与返回结构?
### Q3 前端展开 UI(AI 建议:持仓行展开 + 内嵌汇总表)
- 展开效果:类似交易记录 tab(行点击展开/折叠,箭头指示);
- 展开内容:内嵌小表格显示该 holding 的委托汇总(时间/方向/状态/委托量/成交量/均价/金额/费用);
- 懒加载:展开时才查询 trades/by-holding(不展开不请求);
- 需确认:展示列是否与交易记录 tab 的委托汇总一致(可精简)?
### Q4 成本价 + 最后一笔成交价(2026-09-02 老师补充,已确认)
- **成本价**:持仓数据源已有 avgPrice(QMT 持仓字段,如博纳影业 6.8393),直接展示;
- **最后一笔成交价**
- 该 holding 关联交易记录(trades/by-holding)按时间取**最新一笔有实际成交**tradedVolume > 0)的委托的 tradedPrice(成交价);
- 无关联交易 / 关联委托均未成交 → 默认等于成本价(avgPrice);
- 展示位置:策略持仓 tab 持仓行加「成本价」「最后一笔成交价」两列。
## 定稿记录
- 2026-09-02 老师确认 Q1-Q3 全部定稿:strategy-positions 附加 holding_idQ1+ trades/by-holding 端点(Q2+ 持仓行展开 UI(懒加载,仅委托汇总)(Q3);**R-010 转已定稿,进入迭代 08 实施**。
- 2026-09-02 老师补充 Q4(成本价 + 最后一笔成交价)并确认规则(取最新有成交的 tradedPrice,无成交默认成本价)。
+79
View File
@@ -0,0 +1,79 @@
# R-011 Tab 设置:统一管理所有 tab(内置 + 策略分组混排,显示/隐藏 + 拖动排序) · 已完成
> 归档日期:2026-09-02 需求状态:**已完成**
> 原索引:docs/05-需求池/需求池索引.md(主索引保留 R-011 条目,指向本归档)
> 实现迭代:09-Tab设置统一管理(验收通过,迭代复盘见 docs/04-迭代记录/09-Tab设置统一管理/迭代复盘.md)
> 讨论记录:2026-09-02 定稿(Q1-Q5 老师确认);实现见迭代 09 技术实现方案/复盘
---
> 状态:**已定稿**2026-09-02,老师确认 Q1-Q5)| 登记日期:2026-09-02
> 来源:老师指令(2026-09-02,设置页 tab 化扩展的二次演进)
> 优先级:P1(设置体验优化,无数据风险)
## 需求描述
设置页的「通用设置」子 tab 升级为 **「Tab 设置」**,成为**统一管理所有会话 tab(系统内置 + 策略分组)的唯一入口**:两类 tab 混排在一张表里,每行提供**显示/隐藏开关** + 通过**拖动**调整顺序;**任何 tab 都不支持重命名与删除**(策略的命名/删除仍在「策略分组」子 tab,内置 tab 名称只读)。
## 现状(代码审查 2026-09-02
| 项 | 现状 |
|---|---|
| 内置 tab | client/index.js 的 GENERAL_TABS 硬编码 order 10/11/12(全部持仓/交易记录/关注列表),注册时排在策略之前,**不可与策略交错** |
| 策略 tab | settings.strategies 数组 {id,name,visible,order},注册时按 order 排序、从 order 13 起(永远在内置之后) |
| 显隐控制 | settings.tabs 布尔对象 {allPositions,tradeRecords,watchlist}(通用设置三个开关)+ 策略各自 visible |
| 排序交互 | 「策略分组」子 tab 用 ↑↓ 上下箭头移动策略 |
**结构性障碍**:两套数据、两套 order 空间(内置 10/11/12,策略 13+),内置永远在策略前,无法混合拖动。
## 讨论结论(2026-09-02 老师确认)
- **D1** 策略的「命名」和「删除」**不归 Tab 设置管**:仍在「策略分组」子 tab 操作;「策略分组」里原来的 ↑↓ 排序箭头与显隐开关移除(被 Tab 设置取代)。
- **D2** 子 tab 名称 = **「Tab 设置」**(不是「特指」);管理对象 = 系统内置 tab(标记「内置」)+ 策略分组 tab(标记「策略」),两类混排。
- **D3** 策略侧**同步变更**:顺序与显隐统一为**一份数据源**(tabs 有序数组);策略分组 tab 的展示与 Tab 设置完全一致;删除策略时**联动删除**统一列表中的对应条目。
- **D4** 顺序调整用**拖动**(原生 HTML5 Drag & Drop,零新依赖),不用上下箭头;**落点确认后立即持久化**(Q1)。
- **D5**2026-09-02 Q5 修订,原「去掉显隐开关」作废)**Tab 设置只含「显示/隐藏 + 排序」**:所有 tab(内置 + 策略)均有显隐开关与拖动排序;**任何 tab 均不支持重命名与删除**(Q5)。
- **D6** 老配置**自动迁移**:现网 tabs 布尔对象 + 策略自带 order/visible → 启动时静默归一化为统一数组,无需用户操作。
- **D7** 隐藏边界:接受「没有任何神之一手 tab」的状态(全部隐藏时会话窗不显示任何神之一手 tab,不视为异常)。
## 目标数据模型(定稿)
统一 settings.tabs 为**有序数组**(唯一顺序与显隐来源;策略行名称来自 strategies join,自动跟随改名):
```js
// settings.tabs(定稿)
tabs: [
{ id: 'tab-all-positions', kind: 'builtin', refKey: 'allPositions', name: '全部持仓', visible: true, order: 0 },
{ id: 'tab-trade-records', kind: 'builtin', refKey: 'tradeRecords', name: '交易记录', visible: true, order: 1 },
{ id: 'tab-watchlist', kind: 'builtin', refKey: 'watchlist', name: '关注列表', visible: true, order: 2 },
{ id: 'tab-strategy-grid-supermarket', kind: 'strategy', refId: 'grid-supermarket', name: '网格超市', visible: true, order: 3 },
{ id: 'tab-strategy-manual-t', kind: 'strategy', refId: 'manual-t', name: '手动做T', visible: true, order: 4 },
]
// strategies 收窄为定义表:{ id, name }(无 visible / order
```
- 内置行的 name 与常量对齐(防漂移),策略行的 name 来自 strategies join(改名自动跟随,因为只存 refId,Q4);
- **迁移规则**D6 + Q3):旧 tabs 布尔对象 → 生成三个内置条目(**保留原显隐**);旧策略按各自 order 接续追加为策略条目,**旧隐藏的策略迁移后变为显示**(visible=trueQ3);
- 新增策略**追加到列表末尾**(Q2)。
## 涉及改动面(技术方案,定稿)
1. **src/settings.js**:tabs 从布尔对象 → 有序数组;新增读取归一化(旧格式自动转新)+ 整表更新(order + visible);strategies 相关函数收窄(去掉 visible/order 语义);DEFAULT_TABS 升级为默认顺序数组。
2. **src/api/strategies.js**tabs/update 语义改为整表更新(顺序 + 显隐,拖动落点/开关切换各提交一次);strategies/add 联动追加 tab 条目(末尾);strategies/remove 联动删除对应 tab 条目(D3);废弃 strategies/movestrategies/update 仅剩重命名。
3. **src/client/index.js**:合并 registerGeneralTabs + registerStrategyTabs 为统一注册(读 tabs 数组按 order 排序、过滤 visiblebuiltin 走内置 render、strategy 走 StrategyTab),去掉 10/11/12 与 13+ 硬编码间隔。
4. **src/client/views/SettingsSection.jsx**
- 「Tab 设置」子 tab 取代「通用设置」:表格列出全部 tab(内置行带「内置」徽标、策略行带「策略」徽标),每行 = 拖动手柄 + 名称 + **显隐开关**;**无重命名/删除按钮(任何行)**;
- 拖动:draggable + onDragStart/onDragOver/onDrop,落点重排 → 立即调 tabs/update 持久化(Q1)→ 刷新(沿用「刷新页面后生效」机制);
- 「策略分组」子 tab 瘦身:只留 新增策略 / 重命名 / 删除(删除确认弹窗 + 份额回未分配沿用),移除排序箭头与显隐开关,加提示「顺序与显示请在「Tab 设置」中调整」。
## 定稿记录(Q1-Q52026-09-02 老师确认)
- **Q1** 拖动落点确认后**立即持久化**(每次 drop 提交整表)→ 确认。
- **Q2** 新增策略默认**追加到列表末尾** → 确认。
- **Q3** 迁移时旧「隐藏」的策略**变为显示**(visible=true)→ 确认。
- **Q4** 策略改名后 Tab 设置中的名称**自动跟随**join strategies);Tab 设置不支持改名,策略名称在「策略分组」改 → 确认。
- **Q5** Tab 设置中只有**显示/隐藏 + 排序**,**全部 tab 都不支持重命名与删除** → 确认(同时修订 D5:显隐开关保留)。
> 三要素满足(边界清楚 / 核心逻辑明确 / 老师确认 Q1-Q5),**已定稿**,可进入计划范围。
+55
View File
@@ -0,0 +1,55 @@
# R-012 UI 适配 DSH 主题(浅色 / 深色 / 跟随系统) · 已完成
> 归档日期:2026-09-02 需求状态:**已完成**
> 原索引:docs/05-需求池/需求池索引.md(主索引保留 R-012 条目,指向本归档)
> 实现迭代:10-UI主题适配(验收通过,迭代复盘见 docs/04-迭代记录/10-UI主题适配/迭代复盘.md)
> 讨论记录:2026-09-02 定稿(暂定跟随系统,老师确认);实现见迭代 10 技术实现方案/复盘
---
> 状态:**已定稿(暂定:跟随系统,语义色映射可再调)**(2026-09-02,老师确认方向「暂定跟随系统」)| 登记日期:2026-09-02
> 来源:老师指令(2026-09-02
> 优先级:P1UI 体验)
## 需求描述
神之一手插件 UI 适配 DSH 的浅色 / 深色 / 跟随系统主题:将客户端全部硬编码颜色替换为宿主主题 token(`--dsw-*` CSS 变量),插件各页面随 DSH 主题切换自动适配,不自行维护主题偏好(跟随宿主)。
## 现状(代码审查 2026-09-02
- 客户端 10 个文件共 **141 处硬编码颜色**SettingsSection.jsx 55 / StrategyTab.jsx 24 / TradeRecordsTab.jsx 21 / QmtConnectionChip.jsx 13 / RangeSelector.jsx 7 / PriceCell.jsx 5 / AllPositionsTab.jsx 4 / LoadState.jsx 3 / Toast.jsx 2 / PlaceholderTab.jsx 1
- 宿主机制(已查明):深色主题时宿主挂 `body[data-ds-dark-theme]`,并注入 `--dsw-*` token`--dsw-alias-bg-base/layer-1/layer-2``label-primary/secondary/tertiary/caption``border-l1..l4``state-success/error/business-primary``brand-primary``button-primary-fill/hover``button-contrast-fill``interactive-bg-hover/active``bg-mask-1``tooltip-bg` 等),随 light/dark/system 自动切换;
- 先例:QmtConnectionChip 部分样式已引用 `var(--dsw-alias-label-secondary, #666)`token 可用。
## 决策(2026-09-02 老师确认:暂定跟随系统)
- **D1** 主题偏好跟随宿主(light/dark/system),插件不自行监听/维护;
- **D2** 全部颜色映射到宿主 `--dsw-*` token(语义映射表见下);涨跌色 A 股红涨绿跌 → 宿主语义:涨=红=`state-error-primary`、跌=绿=`state-success-primary`(宿主在深浅主题下保证可读);
- **D3** 实心主按钮 / 徽标 / 提示底色等语义色(success/error/business)跟随宿主对应 token;淡色底用 `color-mix(in srgb, <primary> 10%, transparent)` 跟随主题;
- **D4** 仅做色值 token 化(最小改动),不动布局 / 间距 / 圆角;
- **D5** 若宿主 token 观感不满意,后续可加插件级 `--odl-*` 覆盖变量(本迭代不做,暂定)。
## 语义映射表(实施基准)
| 原色 | 语义 | 替换 |
|---|---|---|
| #fff(表面底) | 表格/卡片/弹窗/输入框/菜单/按钮白底 | var(--dsw-alias-bg-layer-1) |
| rgba(0,0,0,.4)(遮罩) | 弹窗遮罩 | var(--dsw-alias-bg-mask-1) |
| #f5f5f5(hover/静态行底) | 次级面/hover | var(--dsw-alias-interactive-bg-hover) 或 bg-layer-2(静态) |
| #e8f5e9/#f1f8f2(激活浅绿底) | 选中/激活 | color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent) |
| #fdecea(错误浅红底) | 错误提示底 | color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent) |
| #e3f2fd(信息浅蓝底) | 信息提示底 | color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent) |
| #333/#222 | 主文字 | var(--dsw-alias-label-primary) |
| #555/#666 | 次文字 | var(--dsw-alias-label-secondary) |
| #888/#999/#aaa | 弱文字/caption | var(--dsw-alias-label-tertiary) |
| #1565c0(蓝字) | 业务/信息字 | var(--dsw-alias-state-business-primary) |
| #eee(细线) | 行分隔/表头细线 | var(--dsw-alias-border-l1) |
| #ddd/#ccc(描边/强调) | 控件描边/分隔 | var(--dsw-alias-border-l2) |
| #90caf9/#ce93d8(徽标淡描边) | 徽标描边 | var(--dsw-alias-border-l3)(描边中性化,文字保留语义色) |
| #d32f2f/#c62828(红字/实心红) | 涨/错误/删除/危险 | var(--dsw-alias-state-error-primary);实心底同上,文字 var(--dsw-alias-button-contrast-fill) |
| #2e7d32(绿字/实心绿) | 跌/成功/激活/主按钮 | var(--dsw-alias-state-success-primary);实心底同上,文字 var(--dsw-alias-button-contrast-fill) |
| #fff(实心按钮上文字) | 按钮文字 | var(--dsw-alias-button-contrast-fill) |
> 实施细则:仅 inline style / 变量赋值中的颜色字面量替换;注释中的色值可保留或同步(不影响运行);个别语义不确定处保留原色并在迭代记录中列出待老师验收。
+198
View File
@@ -0,0 +1,198 @@
# R-013 策略自定义字段配置(定义随策略,值落库) · 已完成
> 归档日期:2026-09-02 需求状态:**已完成**
> 原索引:docs/05-需求池/需求池索引.md(主索引保留 R-013 条目,指向本归档)
> 实现迭代:11-策略自定义字段配置(验收通过,迭代复盘见 docs/04-迭代记录/11-策略自定义字段配置/迭代复盘.md)
> 讨论记录:2026-09-02 定稿(Q1-Q4 + D6);迭代 11 实施中扩展列显隐/单元格编辑/unit(老师确认);实现见迭代 11 技术实现方案/程序结构设计/复盘
> 状态:**已定稿**2026-09-02,老师确认 Q1-Q4)| 登记日期:2026-09-02
> 来源:老师提出(2026-09-02,策略可自定义 / 可扩展讨论;T-009 转正)
> 优先级:P1(策略管理能力扩展,无数据风险)
> 关联:T-003(策略配置文件骨架,起草 —— 本需求吸收其「策略数据结构」部分)、T-006(数据池 + 策略表格动态字段配置,起草 —— 边界:本需求=策略自定义字段,T-006=表格列配置)
## 需求描述
策略要可**自定义、可扩展**:每个策略可定义自己需要的一组**自定义字段**(带类型:文本 / 数字 / 布尔 / 枚举),该策略下的每个持仓(strategy_holdings 行)按策略定义存一份**键值对值**(JSON,可任意扩展)。字段**定义随策略走**(不落库),**值落库**strategy_holdings 新增 values JSON 列)。
## 现状(代码审查 2026-09-02
| 项 | 现状 |
|---|---|
| 策略定义 | settings.strategies = [{ id, name }]R-011 收窄),存 DSH settings |
| 持仓存储 | strategy_holdingsholding_id / strategy_id / code / shares / created_at / closed_at),无自定义字段列 |
| 定义位置 | 设置页「策略分组」子 tab:策略行内 CRUD(新增 / 重命名 / 删除),无字段配置能力 |
| 持仓渲染 | 策略持仓 tab 固定列(代码/名称/份额/现价/盈亏/操作),R-010 已支持持仓行展开看交易记录 |
## 讨论结论(2026-09-02 老师确认 Q1-Q4
- **D1**(第二轮)字段**定义不存数据库**:随策略定义走,扩展 settings.strategiesconfigSchema 数组);数据库只存值。
- **D2**(第三轮 + Q1)值 = **持仓级 JSON 键值对**strategy_holdings 新增一列 values TEXT(JSON);每行(一个策略 × 一只股票)一套值;某持仓需要哪些字段 = 看所属策略的 configSchema。
- **D3**Q2/Q3)字段**类型化**:文本 / 数字 / 布尔 / **枚举**(预设选项下拉,有用,采纳)。定义含:key / label / type / enum 选项 / 默认值。
- **D4**(第三轮)配置入口 = **设置页「策略分组」子 tab**:策略行做成**可展开**,展开区添加 / 配置字段(名称 + 类型 + 枚举选项 + 默认值)。
- **D5**Q4)兼容:旧策略无 configSchema(缺省空数组)→ 持仓行不展示自定义字段区,行为与现在完全一致;旧行 values 列缺省 NULL(空);无迁移历史值。
- **D6**2026-09-02 老师确认,2026-09-02 迭代 11 演进为列化)值编辑入口原定为**持仓行展开区**;随「自定义字段列化 + 单元格点击编辑」演进(老师选 A):字段直接作为策略持仓表**列**展示,点击字段单元格进入内联编辑(text/number 输入框、boolean 开关、enum 下拉),编辑即调 API 写回该行 values。展开区(交易明细)不再显示自定义字段。
## 目标数据模型(定稿)
```js
// settings.strategies(扩展 configSchema;不落库)
strategies: [{
id: 'grid-supermarket',
name: '网格超市',
configSchema: [
{ key: 'gridGap', label: '网格间距', type: 'text', def: '3%' },
{ key: 'gridAmount', label: '单格金额', type: 'number', def: 10000 },
{ key: 'hasStop', label: '设置止损', type: 'boolean', def: false },
{ key: 'riskLevel', label: '风险等级', type: 'enum', enum: ['低','中','高'], def: '中' },
],
}]
```
```sql
-- strategy_holdings 新增列(幂等 ALTER,沿用 _ensureTradeAttributionColumns 模式)
ALTER TABLE strategy_holdings ADD COLUMN values TEXT; -- JSON 键值对
```
```js
// 持仓行 values 示例(key 与 configSchema.key 对齐;未定义的额外键允许存在=可扩展)
{ "gridGap": "3%", "gridAmount": 10000, "hasStop": true, "riskLevel": "中" }
```
**语义要点**
- key 为稳健标识(英文 slug),label 为 UI 展示名(可中文);值 JSON 以 key 存;
- 枚举字段值校验 ∈ enum 选项;数字字段 UI 数字输入;布尔渲染开关;文本自由输入——校验在服务端写回时执行;
- 定义变更对已有值数据无破坏(旧值保留,新增字段缺省用 def,未配置字段不渲染)。
## 端到端示例(Sample,供后续推进参考)
> 场景:现有三个策略 —— 网格超市(已配自定义字段)、手动做T(未配,旧策略)、长线持有(新增策略,配另一组字段)。沿用项目现有个股:601117.SH(中国交建)、300057.SZ(万顺新材)、600719.SH(大连热电)。
### 1. 策略定义(settings.strategies,不落库)
```js
// 完整值(新增/编辑后持久化于 DSH settings
strategies: [
{
id: 'grid-supermarket',
name: '网格超市',
configSchema: [
{ key: 'gridGap', label: '网格间距', type: 'text', def: '3%' },
{ key: 'gridAmount', label: '单格金额', type: 'number', def: 10000 },
{ key: 'hasStop', label: '设置止损', type: 'boolean', def: false },
{ key: 'riskLevel', label: '风险等级', type: 'enum', enum: ['低','中','高'], def: '中' },
],
},
{ id: 'manual-t', name: '手动做T' }, // 旧策略:无 configSchema(兼容,缺省 []
{
id: 'long-term-hold',
name: '长线持有',
configSchema: [
{ key: 'targetPrice', label: '目标价', type: 'number', def: null },
{ key: 'notes', label: '持仓备注', type: 'text', def: '' },
],
},
]
```
### 2. strategy_holdings 表数据(含 values 列)
```sql
-- ALTER 幂等补列后,既有行 values 自动为 NULL(不迁移)
ALTER TABLE strategy_holdings ADD COLUMN values TEXT;
SELECT holding_id, strategy_id, code, shares, values FROM strategy_holdings WHERE closed_at IS NULL;
-- 结果示例:
-- 1 | grid-supermarket | 601117.SH | 600 | {"gridGap":"3%","gridAmount":10000,"hasStop":true,"riskLevel":"高"}
-- 2 | grid-supermarket | 300057.SZ | 1000 | {"gridGap":"5%","gridAmount":8000,"hasStop":false,"riskLevel":"低"}
-- 8 | manual-t | 600719.SH | 1000 | NULL(手动做T 未配字段 → 无值)
-- 9 | long-term-hold | 601117.SH | 200 | {"targetPrice":7.5,"notes":"周线突破再加仓"}
```
> 同策略多行各存各的值(600719 与 300057 的网格间距不同);不同策略可有完全不同的字段(long-term-hold 无 gridGap)。
### 3. API 交互示例(/odl/api 端点,RPC 通道)
```js
// ① 读策略(含 configSchema
call('one-divine-lot/strategies', {})
// 响应 value: 见 §1 完整数组(策略行自带 configSchema
// ② 配字段:整表更新 strategies(改 configSchema 随整表提交)
call('one-divine-lot/strategies/update', { strategies: [ /* §1 完整数组 */ ] })
// ③ 读某策略持仓(每行附 values)
call('one-divine-lot/strategy-positions', { args: { strategyId: 'grid-supermarket' } })
// 响应 value: [
// { holdingId: 1, code: '601117.SH', shares: 600, /* 既有列 */ values: { gridGap: '3%', gridAmount: 10000, hasStop: true, riskLevel: '高' } },
// { holdingId: 2, code: '300057.SZ', shares: 1000, /* ... */ values: { gridGap: '5%', gridAmount: 8000, hasStop: false, riskLevel: '低' } },
// ]
// ④ 写某持仓行的值(无 values → 传全量;已有 → 合并更新)
call('one-divine-lot/holdings/values-update', { args: { holdingId: 1, values: { gridGap: '4%', gridAmount: 10000, hasStop: true, riskLevel: '高' } } })
// 响应 value: { holdingId: 1, values: { gridGap: '4%', gridAmount: 10000, hasStop: true, riskLevel: '高' } }
```
### 4. UI 形态
**设置页 →「策略分组」子 tab(配置字段定义)**
```
[▸] 网格超市 [重命名] [删除]
自定义字段:
展示名 | key | 类型 | 选项(枚举) | 默认值 | 操作
网格间距 | gridGap | 文本 | - | 3% | [删除]
单格金额 | gridAmount | 数字 | - | 10000 | [删除]
设置止损 | hasStop | 布尔 | - | 关 | [删除]
风险等级 | riskLevel | 枚举 | 低/中/高 | 中 | [删除]
[+ 添加字段](名称/类型/选项/默认值 一行表单) [保存]
[▸] 手动做T ...(无字段区,保持现状)
[▸] 长线持有 ...(目标价 / 持仓备注)
```
**策略持仓 tab(迭代 11 演进:自定义字段直接为表格列,点击单元格内联编辑)**
```
代码 名称 现价 涨幅 网格间距 单格金额 设置止损 风险等级 策略份额 操作
601117.SH 中国交建 7.20 +0.56% 4% 10000 ✓ 高 600 [移出]
300057.SZ 万顺新材 5.10 -1.2% 5% 8000 ✗ 低 1000 [移出]
(点击「网格间距/单格金额/风险等级」单元格 → 输入框/下拉编辑,Enter 保存;
点击「设置止损」单元格 → 开关直接切换;空值显示 — / 默认值 def)
```
> 表格顶部另有「列设置」按钮:每策略独立配置列显隐与顺序(基础数据列 + 自定义字段列;代码/名称/操作固定)。
### 5. 渲染 / 校验逻辑参考
```js
// 渲染(策略持仓 tab):configSchema 决定字段集合,values 提供值,缺省回退 def
const schema = strategy.configSchema ?? []; // 无定义 → 不渲染字段区
const merged = schema.map((f) => ({ ...f, value: (row.values ?? {})[f.key] ?? f.def }));
// 校验(服务端 holdings/values-update 写回时)
function validateField(f, v) {
if (v === undefined || v === null || v === '') return null; // 允许空
if (f.type === 'number' && !Number.isFinite(Number(v))) return f.label + ' 需为数字';
if (f.type === 'enum' && !f.enum.includes(v)) return f.label + ' 需在选项内: ' + f.enum.join('/');
if (f.type === 'boolean' && typeof v !== 'boolean') return f.label + ' 需为布尔';
return null;
}
```
## 涉及改动面(技术方案,定稿)
1. **src/settings.js**strategySchema 的 strategies 项扩展 configSchemaschemastery array of objecttype union text|number|boolean|enum);读取归一化(旧项缺省 []);strategies/update 沿用整表更新语义。
2. **src/storage/SqliteStore.js**init() 增加 _ensureHoldingValuesColumnPRAGMA table_info → ALTER,幂等,只读容忍同 trade_orders 先例);openHolding / addShares / reduceShares / closeHolding 保持既有列行为(values 不随份额操作变动);新增 readValues(holdingId) / writeValues(holdingId, values)。
3. **src/api/strategies.js**strategy-positions 返回项附加 values;新增端点 holdings/values-update {holdingId, values} 写回;strategies/update 已可携带 configSchema(整表更新天然支持)。
4. **src/client/views/SettingsSection.jsx**:「策略分组」子 tab 策略行加展开手柄:展开区 = 字段列表(label/key/type/enum/def+ 添加/删除/编辑字段表单;保存走 strategies/update。
5. **src/client/views/StrategyTab.jsx**:持仓行展开区(R-010 基础)增加「自定义字段」区块:按该策略 configSchema 渲染输入控件(文本/数字/布尔/枚举),值来自该行 values,编辑保存调 values-update;行内表格可选加字段列。
6. 回归脚本:设置字段定义 → 批量写/改持仓行 values → 校验存储与读取(独立数据目录,技术约束-011)。
## 定稿记录(Q1-Q42026-09-02 老师确认)
- **Q1** 字段定义 = 策略级(某策略下每笔持仓需要的字段由该策略定义决定);值 = 持仓级(每行一套键值对)→ 确认。
- **Q2** 字段加类型(文本 / 数字 / 布尔 / 枚举)→ 确认。
- **Q3** 支持枚举字段(预设选项下拉)→ 确认(有用)。
- **Q4** 旧策略无自定义字段:沿用现有字段定义(不迁移、不补默认),值列缺省空 → 确认。
- **D6(值编辑入口)**:持仓行展开区编辑(老师确认 2026-09-02)→ 迭代 11 演进为**表格列 + 单元格点击编辑**(老师选 A,2026-09-02)。
> 三要素满足(边界清楚 / 核心逻辑明确 / 老师确认 Q1-Q4),**已定稿**,可进入计划范围。
+38
View File
@@ -0,0 +1,38 @@
# 需求:R-014 持仓内存快照(服务端 10s 定时同步,请求不再穿透 QMT)
> 登记:2026-09-02 | 来源:架构梳理讨论 + 老师指令 | 状态:**已定稿(老师逐项拍板 4 问)**
> 归属:迭代 12 计划:PLAN-013
> **归档头(2026-09-08)** | 需求状态:已完成 | 归档日期:2026-09-08 | 实现迭代:12-持仓内存快照(持仓内存快照)
> 讨论记录索引:R-014.md 正文;实现细节见 04-迭代记录/12-持仓内存快照/
> 归档路径:05-需求池/已完成/R-014.md
> 迭代状态标记:已实现(迭代 12,回归 34/34 + typecheck + build 通过)→ 已归档(2026-09-08 老师归档指令确认验收)
## 需求描述
原实盘持仓数据在**每次请求时穿透 QMT**(`PositionManager.getAllPositions()` 实时拉 `/trade/positions`),带来两个问题:
1. **QMT 一抖(超时/掉线),策略持仓 / 全部持仓 / 未分配三个页面当场空白**——因为拼装以 QMT 返回行为驱动表,QMT 返回空则整页无行;
2. **每次进 tab 都打一次 QMT HTTP**,且部分实盘字段(volume/available/frozenVolume/price/marketValue/profit/profitPct 七个)该页面根本不消费,属搭车字段。
老师拍板优化:**服务端建缓存(内存快照),以快照为准,每 10 秒与 QMT 同步一次;策略持仓等接口相信快照,去掉请求时穿透 QMT 的逻辑。**
## 讨论记录(2026-09-02,三问拍板)
| # | 问题 | 结论(老师拍板) |
|---|---|---|
| 1 | 落库还是内存?(AI 初版方案建 SQLite 缓存表,老师质疑) | **纯内存管理,不落库**。理由(老师追问后 AI 论证修正):持仓快照随时可用一次调用重拿全,不满足落库任一正当条件(不可再生历史 / 重启首屏依赖);落库反引入「过期快照冒充实时的说谎风险」;不复用 strategy_holdings(账本 ≠ 对账单,holding_id 是交易归属锚点,不可掺易变快照) |
| 2 | 首启缓存未预热 / QMT 从未连上时,前端请求怎么办? | **读穿透兜底**:缓存为空当场拉一次 QMT 并回填;QMT 也挂才报错(前端既有 LoadState 重试 UI 兜住) |
| 3 | QMT 里卖光的票,本地持仓记录(幽灵条目)怎么办? | **同步时自动清仓**:QMT 快照连续 3 轮(约 30s)消失的 code,本地全部策略当前持仓自动转历史(不物理删除);加防抖护栏(账户身份守卫)防误清 |
| 4 | 前端要不要显示同步时间(数据最多滞后 10s)? | **本轮不加**,前端零改动;10s 级滞后对持仓场景够用 |
## 边界
**做**:内存快照同步服务(PositionSync)、getAllPositions 改读快照 + 读穿透兜底、空快照双重确认、幽灵持仓自动清仓(防抖 + 账户守卫)、回归脚本。
**不做**:不落库(无新表、无 schema 变更);不同步时间前端显示;部分减持(QMT 仍有但变少 → 负数未分配)的自动修正(维持 UI 暴露现状,后续可另立需求);策略份额账本(strategy_holdings)的任何改动。
## 验收
`docs/04-迭代记录/12-持仓内存快照/验收标准.md`
+34
View File
@@ -0,0 +1,34 @@
# 需求:R-015 盘口数据内存化(QuoteSync/QuoteHub 统一管理)+ 数据同步指示灯
> 登记:2026-09-02 | 来源:架构演进讨论(老师逐项拍板)| 状态:**已定稿**
> 归属:迭代 13 计划:PLAN-014
> **归档头(2026-09-08)** | 需求状态:已完成 | 归档日期:2026-09-08 | 实现迭代:13-盘口内存快照(盘口数据内存化+指示灯)
> 讨论记录索引:R-015.md 正文;实现细节见 04-迭代记录/13-盘口内存快照/
> 归档路径:05-需求池/已完成/R-015.md
> 迭代状态标记:已实现(迭代 13test-quote-sync 全绿 + 存量回归通过)→ 已归档(2026-09-08 老师归档指令确认验收)
## 需求描述
持仓数据已完成「内存快照统一管理」(R-014/迭代 12)。本轮把**盘口(市场行情)数据**收敛为同一模式:
1. **QuoteSync + QuoteHub 工具类**(替换 MarketFeed/MarketDataHub,方案 A 替换不包壳):
- QuoteSync 管取数:启动 prime(持仓盘口)+ 5s REST 定时刷新(watch 集合);
- QuoteHub 管存查:内存快照 + watch 集合 + 读穿透兜底 + 对外 getQuote(s)
2. **纯内存,market_quotes_cache 表退役**(老师拍板 DROP):行情随时可重取、warmup bug 证明落库从未生效、重启 prime 1 秒内有价——三问全否不落库;价格单一入口 = QuoteHub;
3. **WS 通路去掉**(老师拍板):从无生效结论(market-stats 诊断无结论文档)、全市场推送被 watch 过滤成本高、REST 5s 已覆盖;ingest 入口来源无关留再接入口子;
4. **字段扩展**:新增涨停价/跌停价(来源 /data/instrument,按交易日内存缓存,不落库);昨收 tick 自带;getInstrument 首次投入使用;
5. **数据同步指示灯**(老师提出):会话头部 QMT 健康灯旁加「持仓/盘口」两个同步状态圆点——绿(正常)/黄(同步失败中、快照陈旧)/灰(从未同步),悬停详情(同步时间/快照量/错误),**点击 = 立即触发该域 syncNow**;新增 sync-status 端点(吸收临时诊断 market-stats);
6. **策略持仓表行情列扩展**(老师确认并入):列设置新增 涨停价/跌停价/今开/最高 4 个基础列(纯价格展示,默认隐藏,列设置勾选开启);最低/成交量/成交额暂不加(单位口径另议);距涨停百分比类衍生展示另立需求;
6. **同步失败保留上次价**(AI 定,与持仓对称);**前端不加价龄灰点**(老师采纳预判:管道健康由全局灯表达,个股停牌属数据语义另议)。
## 边界
**做**:上述 1-6watchCodes 维持现状(只进不出、无上限——去掉 WS 后膨胀仅轻微浪费,收缩不值成本)。
**不做**:不落库任何行情字段(涨停跌停也不落);不做 watch 收缩/降频;不做个股停牌标识;WS 不删除需求草稿(T-005 维持起草);PriceCell 不改动;最低/成交量/成交额列不做;距离涨停百分比衍生展示不做。
## 验收
`docs/04-迭代记录/13-盘口内存快照/验收标准.md`

Some files were not shown because too many files have changed in this diff Show More