迭代16: 交易关联驱动持仓份额动态调整(R-018)
This commit is contained in:
@@ -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. UI:TradeRecordsTab 归属入口改造为份额分配器(策略下拉 + 段量输入 + 余额提示 + 部分关联);
|
||||
6. 回归:新脚本(分配器动作/撤段/迁移/软提示)+ 存量回归 + typecheck + build。
|
||||
|
||||
**不做**:自动归属推导暴露;全部持仓/未分配 tab 份额联动 UI;未分配余额自动兜底;账本写自动化兜底(软提示不改账本)。
|
||||
|
||||
## 实施步骤(阶段划分,供迭代跟踪)
|
||||
|
||||
1. 阶段A:PositionSync 幽灵清仓退役 + 软提示检测基础;
|
||||
2. 阶段B:存储地基(void_at + 段表 + 迁移);
|
||||
3. 阶段C:归属服务核心(apply/revoke 动作表 + 原子性);
|
||||
4. 阶段D:策略级候选 + API(含 R-017 退役);
|
||||
5. 阶段E:UI 份额分配器;
|
||||
6. 阶段F:回归 + typecheck + build + 存量回归;
|
||||
7. 迭代复盘 + R-018 索引状态更新 + 需求归档。
|
||||
|
||||
## 验收要点
|
||||
|
||||
见 `docs/04-迭代记录/16-交易关联驱动持仓份额动态调整/验收标准.md`。
|
||||
@@ -20,7 +20,7 @@
|
||||
| 产品约束-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-02 R-013 定稿 Q1-Q4 + D6):定义随策略走、值随持仓行,类型化(含枚举),旧策略兼容 |
|
||||
| 产品约束-009 | 会话 tab 统一由「Tab 设置」管理:设置页「Tab 设置」子 tab 是**所有会话 tab(系统内置 + 策略分组)的唯一顺序与显隐入口**,两类 tab 混排;每行 = 拖动排序 + 显示/隐藏开关;**任何 tab 均不支持重命名与删除**(内置 tab 名称只读,策略命名/删除仍在「策略分组」子 tab);策略改名后 Tab 设置中的名称自动跟随(只存引用);新增策略默认追加到列表末尾;删除策略联动删除 Tab 设置中对应条目;顺序与显隐唯一数据源 = settings.tabs 有序数组 | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1-Q5 确认):Tab 设置 = 显示/隐藏 + 拖动排序(落点立即持久化),全 tab 禁重命名/删除 |
|
||||
| 产品约束-011 | 持仓页数据(策略持仓 / 全部持仓 / 未分配)以服务端 10s 内存快照为准,**接受最多 10s 滞后**(同步时间前端不显示,老师拍板);QMT 抖动/掉线时持仓页面显示**最后一次快照**而非空白;QMT 已卖光的票(连续 3 轮同步确认消失)本地持仓自动转历史(幽灵清仓,不物理删除);部分减持仅表现为「未分配为负」,不做自动修正 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-014 定稿):老师四问拍板(内存不落库 / 读穿透兜底 / 幽灵自动清仓 / 不显示同步时间) |
|
||||
| 产品约束-011 | 持仓页数据(策略持仓 / 全部持仓 / 未分配)以服务端 10s 内存快照为准,**接受最多 10s 滞后**(同步时间前端不显示,老师拍板);QMT 抖动/掉线时持仓页面显示**最后一次快照**而非空白;**幽灵自动清仓退役(变更 1,2026-09-08 R-018 数据域分界)**:同步机制不再把本地策略持仓自动转历史——策略持仓份额只由「交易关联(归属=账本写操作)」与「手动份额操作」驱动,账本转历史唯一途径 = 卖出单关联份额减至 0;对账单码消失仅表现为快照无此行 + **漏关联软提示**(只读提示去关联/移出,不自动写账本);部分减持仅表现为「未分配为负」,不做自动修正 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-014 定稿);**变更 1(2026-09-08 R-018/迭代 16 拍板)**:幽灵自动清仓退役(同步机制不写账本),策略持仓由交易关联 + 手动份额操作驱动;漏关联软提示替代自动归档 |
|
||||
| 产品约束-012 | 行情数据服务形态(R-015):现价/昨收/涨停/跌停统一由盘口内存快照提供(≤5s 更新),价格单一入口;QMT 抖动时页面价格保持旧值不空白;会话头部(QMT 健康灯旁)提供**持仓/盘口同步指示灯**——绿=同步正常(持仓 30s/盘口 15s 内)、黄=同步失败中快照陈旧、灰=从未同步;悬停显示同步时间/快照量/失败数/错误摘要;点击灯 = 立即触发该域同步;不做个股停牌标识(另议) | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-015 定稿):老师提出指示灯,采纳 AI 推荐三态/点击即同步/取消 PriceCell 灰点 |
|
||||
<!-- 示例条目(确认格式后删除):
|
||||
| 产品约束-001 | 示例:求签功能必须保证抽取结果的不可预测性 | 2026-08-26 | 生效 | - | 讨论确认:为保证公平性,抽取必须不可预测 |
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
| 技术约束-015 | 策略自定义字段存储(R-013,2026-09-02):字段定义随策略定义存 settings.strategies 扩展 configSchema([{key,label,type,enum?,def}],type ∈ text|number|boolean|enum,旧项缺省空数组);字段值落 strategy_holdings 新增 values TEXT(JSON 键值对,key 对齐 configSchema.key,允许额外键=可扩展,NULL=未配置);补列用幂等 ALTER(沿用 _ensureTradeAttributionColumns 模式,只读连接容忍);API:strategy-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 列 + 幂等补列 + 类型校验 |
|
||||
| 技术约束-014 | 会话 tab 注册与顺序显隐(R-011):客户端注册统一读 **settings.tabs 有序数组**(唯一顺序与显隐来源,内置条目 refKey + 策略条目 refId),按 order 排序、过滤 visible 后注册(builtin 走内置 render、strategy 走 StrategyTab),移除硬编码 order 间隔(原内置 10/11/12、策略 13+);settings.tabs 从布尔对象升级为有序数组,读取时对旧格式(布尔对象 + 策略自带 order/visible)静默归一化迁移(旧隐藏策略迁移后显示),写入即落库;策略定义表收窄为 {id,name}(去除 visible/order);tabs/update 语义改为整表更新(顺序 + 显隐),strategies/add 联动追加 tab 条目(末尾),strategies/remove 联动删除对应 tab 条目,废弃 strategies/move | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1-Q5 确认):统一 tabs 有序数组 + 自动迁移 + 联动增删 |
|
||||
| 技术约束-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/ 仅放 UI(views/ 组件 + market/ provider);文件命名 = 类名(PascalCase)+ .js/.jsx;新增服务端模块必须先落对应域目录,无合适域时先讨论补域,不得回退平铺 | 2026-09-02 | 生效 | - | 新增(2026-09-02 结构审查 + 优化落地):component/ 平铺还原为语义分域,删除死代码 AllocationStorage、DataStore.setDataset/removeDataset |
|
||||
| 技术约束-017 | 持仓内存快照(R-014,2026-09-02):全量实盘持仓由服务端 PositionSync **进程内内存快照**管理(启动预热 + 10s 定时全量拉 /trade/positions → 校验 → 整体替换),**不落库**(无缓存表、不复用 strategy_holdings——账本与对账单分离,holding_id 交易锚点不掺易变快照;判断标准:外部可一次调用重取全的实时投影不落库);PositionManager.getAllPositions 以快照为准(strategy-positions / unallocated / summary 三接口不再请求时穿透 QMT),快照为空读穿透兜底(当场拉一次并回填);同步失败保留上次快照(不清空不报错);空快照双重确认(/health 可用 + getAsset 账户身份可识别)才接受为真清仓;幽灵清仓:快照连续 3 轮(约 30s)消失的 code 该码全部策略当前持仓自动 closeHolding 转历史(不物理删除),账户身份守卫防误清(accountId 未知当轮跳过、切换当轮重置跳过);syncNow 允许手动调用(mounted 只管定时循环,不拦手动同步) | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-014 定稿 + 迭代 12 实施):AI 初版建缓存表方案经老师质疑反转为内存方案(落库三问:不可再生?重启首屏依赖?读放大/复杂查询?全否 → 内存) |
|
||||
| 技术约束-017 | 持仓内存快照(R-014,2026-09-02;**变更 1:2026-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 实施);**变更 1(2026-09-08 R-018/迭代 16 拍板)**:幽灵自动清仓退役(不再 closeHolding 本地账本),PositionSync 只同步对账单快照;漏关联由只读软提示承担 |
|
||||
| 技术约束-018 | 盘口内存快照(R-015,2026-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**(幂等),价格单一入口 = QuoteHub,DataStore 行情方法(loadMarket/getMarketQuote(s)/setMarketQuotes)删除;同步失败保留内存旧值;watchCodes 维持只进不出无上限;涨停/跌停/昨收不落库 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-015 定稿 + 迭代 13 实施):老师五拍板(替换/WS 移除/纯内存 DROP/watch 现状/指示灯);warmup bug 复现(loadMarket return this 残迹)为不落库关键证据 |
|
||||
|
||||
| 技术约束-019 | 历史持仓查询(R-016,2026-09-07):策略 tab 历史持仓展示走**本地库只读查询**(SqliteStore.getHoldingsHistory:strategy_id + closed_at IS NOT NULL + closed_at ≥ sinceMs,closed_at DESC),经独立端点 strategy-holdings/history 暴露;**当前持仓路径(strategy-positions / PositionSync 快照语义)不掺历史数据**(两份结果前端合并渲染);closeHolding 置 shares=0 语义维持不变(Q2 老师拍板:历史行份额显示 0,重点在追溯该持仓的历史操作而非清仓时份额);范围换算服务端做(week=7d/month=30d/quarter=90d/halfYear=182d/year=365d 自然日近似)——**变更 1(2026-09-07 二轮补充 Q8-Q9 老师拍板)**:range 新增 'today' = **本地自然日 00:00 起**(特判零点,不落回溯毫秒档),供前端「今日已清仓默认层」(恒显示、不受历史持仓开关控制);历史范围层语义收窄为**今天之前**;其余不变 | 2026-09-07 | 生效 | - | 变更 1(2026-09-07 R-016 二轮补充 Q8-Q9 老师拍板):+range=today 自然日边界,历史开关只控今天之前;首轮新增(Q1-Q7):历史行=追溯操作锚点,不动存储写路径 |
|
||||
|
||||
+66
-3
@@ -364,7 +364,7 @@ CREATE INDEX IF NOT EXISTS idx_trade_fills_date ON trade_fills (trade_date);
|
||||
| 失败策略 | 同步失败**保留上次快照**,不清空不报错 | 读方继续消费旧快照,QMT 抖动不再白屏(强于旧的穿透行为) |
|
||||
| 空快照 | 双重确认(/health 可用 + getAsset 账户身份可识别)才接受为「真清仓」,否则视为异常保留旧快照 | 防一次异常响应清空缓存 |
|
||||
| 读路径 | `getAllPositions()` 读内存快照;快照为空 → 读穿透当场拉一次并回填;QMT 也挂 → 抛错(前端 LoadState 重试) | 首启兜底;未注入 positionSync 时保持旧行为(兼容) |
|
||||
| 幽灵持仓自动清仓 | QMT 快照连续 3 轮(约 30s)消失的 code,本地全部策略当前持仓 closeHolding 转历史;**账户身份守卫**:accountId 未知当轮跳过、账户切换当轮重置跳过(防误清);部分减持不触发(负数未分配由 UI 暴露) | 老师选定;清仓转历史不物理删除,历史保留语义不变 |
|
||||
| ~~幽灵持仓自动清仓~~(**退役,2026-09-08 R-018 数据域分界**) | ~~QMT 快照连续 3 轮消失 → 本地全部策略当前持仓 closeHolding 转历史~~ → **不再写 strategy_holdings**:同步机制只作用于对账单域(老师拍板:幽灵清仓本就是一个同步机制,不可以让幽灵把爪子伸太长);账本转历史唯一途径 = 卖出单关联减至 0(R-018);对账单码消失 → 快照无此行 + 漏关联软提示(positions/orphan-hints,只读) | R-014 选定时防本地账本残留;R-018 改由交易关联驱动账本生命周期,幽灵自动归档与「账本只由关联交易改」冲突 → 退役(迭代 16 实施,见 §12) |
|
||||
| 前端 | 零改动 | 三个接口数据源自动切换 |
|
||||
|
||||
### 11.2 数据流(持仓部分,替代原「读取时组装」描述)
|
||||
@@ -383,8 +383,71 @@ CREATE INDEX IF NOT EXISTS idx_trade_fills_date ON trade_fills (trade_date);
|
||||
| PositionManager | getAllPositions 改读快照 + 读穿透兜底(未注入 sync 时兼容旧穿透) | src/position/PositionManager.js |
|
||||
| 回归测试 | 纯内存 mock 34 项(同步/失败保留/空快照/幽灵防抖/账户守卫/读穿透/组装回归) | scripts/test-position-sync.mjs |
|
||||
|
||||
## 12. 约束条目(引用)
|
||||
## 12. 归属分段存储设计(R-018 落实,2026-09-08 迭代 16 实施)
|
||||
|
||||
- 技术约束-012:数据存储遵循本文件(SQLite 存储设计,含交易记录表 §10);
|
||||
> **变更理由**(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 展示仍 0,R-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-holding(R-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-segments;orders 附加段 | src/api/trades.js |
|
||||
| PositionSync | 删除幽灵清仓逻辑(只同步对账单快照) | src/position/PositionSync.js |
|
||||
| 回归测试 | 分界/动作表/撤段/迁移/软提示 | scripts/test-r018-attribution.mjs(新增) |
|
||||
|
||||
## 13. 约束条目(引用)
|
||||
|
||||
- 技术约束-012:数据存储遵循本文件(SQLite 存储设计,含交易记录表 §10、归属分段存储 §12);
|
||||
- 技术约束-011:测试/回归脚本禁止在真实数据上执行写操作(独立数据目录);
|
||||
- 技术约束-017(变更 1,2026-09-08):幽灵自动清仓退役,PositionSync 只同步对账单快照;
|
||||
- 本文件为新领域设计约束,后续数据存储变更以本文件为最终依据。
|
||||
@@ -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)
|
||||
|
||||
> 命名遵守技术约束-016(src/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(作废,非 closeHolding;R3-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. 读 order(trade_orders:code/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_orders(R-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 当前活动份额 activityShares(0=无仓)+ 该 order 已有关联段数提示(供 UI「指向后判定」的前置展示;**不预筛**,指向后由服务端判定)。策略级(Q1),holdingId 不再作下拉键;
|
||||
- **orders/attribution-set**:{ orderId, segments } → 归属服务 setSegments(替代 set-attribution;set-attribution 端点删除或保留为单段包装——删,避免双写源混乱;既有数据迁移已覆盖);
|
||||
- **orders/attribution-segments**:{ orderId } → 读当前段(UI 展开/编辑用);
|
||||
- 今日 orders 端点:order 行附 `segments: [{strategyId,strategyName,holdingId,volume}]`(取代原单组 strategyId/holdingId 附加;兼容字段保留首段值)。
|
||||
|
||||
## 4. UI 份额分配器(阶段E,TradeRecordsTab)
|
||||
|
||||
- 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.js(attribution-targets/set/segments;orders 附加段)、src/api/strategies.js(strategy-holdings/history 过滤 void)
|
||||
- src/client/views/TradeRecordsTab.jsx(AttributionAllocator 替换 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/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 通过。
|
||||
|
||||
## 过程事实
|
||||
|
||||
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_shares(shares 仍置 0,R-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-segments;orders 与 trades/history 返回附加 segments;R-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(幽灵退役断言)、r017(7 天候选退役语义)、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_shares(shares 仍置 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-set(segments 全量替换);attribution-segments(读);今日 orders / trades/history 附加 segments;R-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,74 @@
|
||||
# 需求:R-018 交易关联驱动持仓份额动态调整(归属 = 账本写操作)
|
||||
|
||||
> 登记:2026-09-08 | 来源:老师指令(2026-09-08 新优化需求讨论)| 需求状态:**已完成**
|
||||
> 归档日期:2026-09-08 | 实现迭代:16-交易关联驱动持仓份额动态调整(验收通过 2026-09-08)
|
||||
> 讨论记录索引:R-018.md 正文(Q1-Q5/R1-R7 + 数据域分界 + T2/T3);实现细节见 04-迭代记录/16-交易关联驱动持仓份额动态调整/
|
||||
> 归档路径:本文件已移入 05-需求池/已完成/
|
||||
|
||||
> 迭代状态标记:已定稿(2026-09-08)→ 已实现(迭代 16)→ **已归档(验收通过 2026-09-08,口径=自动化为主体 + 只读走查 + 真实使用顺带确认)**
|
||||
|
||||
## 需求描述(一句话)
|
||||
|
||||
从**交易记录出发**给委托关联策略/持仓时,**自动同步调整该策略下持仓的份额**(关联即账本写操作),支持一笔委托拆多段分配给不同策略;取消/改归属时做逆操作回滚份额;账本生命周期只由关联交易 + 手动份额操作驱动,同步机制不写账本。
|
||||
|
||||
## 〇、数据域分界(最根本前提,2026-09-08 老师拍板)
|
||||
|
||||
**「全部持仓」= 对账单域**:唯一数据源 = PositionSync(QMT 快照,10s 内存同步,整体替换)。**包括幽灵清仓在内的一切同步机制只作用于对账单域**,QMT 说了算(老师原话:幽灵清仓本就是一个同步机制,不可以让幽灵把爪子伸太长)。
|
||||
|
||||
**「策略持仓」= 账本域(strategy_holdings)**:写操作只来自两条通道——
|
||||
1. **交易关联分配器**(本需求新增):建仓 / 加仓 / 减仓 / 清仓 / 作废;
|
||||
2. **手动份额操作**(+ 添加持仓 / 移出 / 全部移入):无交易来源时的兜底通道(Q3:保留现在的建仓逻辑;份额以 QMT 持仓信息为基础创建,受「已分配 ≤ 总持仓」校验)。
|
||||
|
||||
**既有行为变更**:R-014 定稿的「幽灵自动清仓」(连续 3 轮消失 → closeHolding 本地全部策略当前持仓)**不再写 strategy_holdings**;账本行转历史只由「卖出单关联 → reduceShares 归 0 → closeHolding」产生。影响既有定稿:技术约束-017(幽灵清仓条款)、产品约束-011、R-014、迭代 12(已实现待验收)需修订;PositionSync 删减幽灵清仓逻辑(变更理由 = 本次讨论,写入变更记录)。
|
||||
|
||||
## 背景与现状
|
||||
|
||||
1. **QMT 实盘持仓**(对账单):PositionSync 10s 内存快照,真实 volume/price(技术约束-017);
|
||||
2. **本地策略账本** strategy_holdings:shares + 生命周期(open/add/reduce/close),此前靠手动份额操作维护;账本 ≠ 对账单(R-014:holding_id 是交易锚点,不掺易变快照);
|
||||
3. **交易归属** trade_orders.strategy_id/holding_id:手动设置(R-009/R-017),只做过滤/展示/追溯,**不改数量**——本需求改变这一点。
|
||||
|
||||
**展示行关系(防误解)**:策略持仓 tab 当前持仓行 = **QMT 快照 ∩ 账本 shares>0**(行由对账单驱动、份额由账本驱动)。卖出全清 → QMT 无此 code → 展示行随快照消失(≤10s,与幽灵清仓无关);此时账本行仍为当前持仓(closed_at NULL),作为**待关联锚点**保留,直到卖出单被关联。
|
||||
|
||||
## 讨论记录(2026-09-08)
|
||||
|
||||
| # | 决策点 | 结论(老师拍板) |
|
||||
|---|---|---|
|
||||
| 1 | 归属候选按什么给?(Q1) | **策略级下拉**,不做预先判断(有无持仓/历史持仓),指向动作发生后按目标策略当时的活动持仓状态现场判定 |
|
||||
| 2 | 驱动量基准?(Q4) | **已成交量**(撤单/废单不动账) |
|
||||
| 3 | 交互模型?(R4 统一分配器) | 不分买卖两套流程:一笔委托已成交量可拆 0..N 段(策略×量);待分配余额 = 成交量 − 已分配段和;默认输入 = 余额全量可改小;允许部分关联(剩余保留「未关联」醒目提示);买入卖出均可拆多段 |
|
||||
| 4 | 买入关联动作?(原始第 2 条) | 该策略有活动持仓 → `addShares` 加仓;无活动持仓(含曾有历史/作废行)→ `openHolding` 建**新仓**(不复活旧行;Q5) |
|
||||
| 5 | 卖出关联动作?(原始第 3 条) | 活动仓 ≥ 段量 → `reduceShares`,归 0 → `closeHolding`;活动仓 < 段量 → **自动截断**段量=可容纳量归零清仓,余额保留继续分配;该策略无活动持仓 → 关联失败提示(R1) |
|
||||
| 6 | 撤段逆操作?(R3,粒度=段) | 撤建仓买入段 → 持仓**作废**(新增第三态,非 closeHolding,不产生假清仓历史);撤加仓买入段 → `reduceShares` 减回,归 0 且无真实卖出 → 作废;撤卖出段 → **恢复活动仓 + 份额加回**(撤销语义,区别于 Q5 新买入不接旧仓) |
|
||||
| 7 | 改归属语义? | 撤旧段 + 加新段,**原子**;撤卖出段若该 holding 已被真实卖出(非本单所致)无法恢复 → 提示不可撤 |
|
||||
| 8 | 数据域分界?(本轮核心) | 幽灵清仓等同步机制只作用于对账单域;策略持仓写操作只来自交易关联 + 手动份额操作 |
|
||||
| 9 | T1 卖出关联 vs 幽灵清仓时序竞态 | **关闭**:幽灵不写账本 → 卖出后账本行保留为待关联锚点,无 30s 竞态 |
|
||||
| 10 | T2 R-017(7 天清仓候选) | **退役确认**:新时序下清仓发生在卖出单关联之后,不存在「系统先清、用户后补」场景,候选回归「当前持仓 + 未关联」;R-017 的 holdingId 键控/占位安全逻辑保留 |
|
||||
| 11 | T3 漏关联兜底 | **软提示确认**:检测「账本有活动行 + QMT 快照无此 code」时提示老师去关联/移出;不自动写账本(不违背分界) |
|
||||
|
||||
## 核心规则汇总
|
||||
|
||||
| 触发 | 判定 | 动作 |
|
||||
|---|---|---|
|
||||
| 买入段 | 该策略该 code 有活动持仓 | `addShares`(加仓,量=段量) |
|
||||
| 买入段 | 无活动持仓(含曾有历史/作废行) | `openHolding` 建新仓(不复活旧行) |
|
||||
| 卖出段 | 活动仓 ≥ 段量 | `reduceShares`;归 0 → `closeHolding`(账本转历史唯一途径) |
|
||||
| 卖出段 | 活动仓 < 段量 | 自动截断:段量=可容纳量 → 归零清仓,余额保留继续分配 |
|
||||
| 卖出段 | 该策略无活动持仓 | 关联失败提示 |
|
||||
|
||||
撤段逆操作:撤建仓买入 → 持仓作废(第三态);撤加仓买入 → 减回归 0 作废;撤卖出 → 恢复活动仓 + 份额加回。改归属 = 撤旧段 + 加新段(原子)。
|
||||
|
||||
## 存储 / 模型调整方向
|
||||
|
||||
1. **holding 第三态**:strategy_holdings 增加作废标记(如 `void_at`);活动 = `closed_at IS NULL AND void_at IS NULL`;作废行不进 R-016 历史「已清仓」层,保留数据可追溯;活动唯一索引(同策略同 code)收窄至两态皆空;
|
||||
2. **归属单列 → 分段表**:新表(如 `trade_order_attributions`:order_id → strategy_id/holding_id/volume/direction/created_at),一笔委托 N 段;R-010 by-holding 与 R-009 history 策略过滤改经段表 join(同一单可出现在多个 holding 展开里);trade_orders 原两列退役或降级为「主归属」快照,已有单组归属迁移为第一段;
|
||||
3. **软提示(T3)**:账本有活动行 + QMT 快照无此 code 时的可见性提示(不动账本)。
|
||||
|
||||
## 边界
|
||||
|
||||
**做**:幽灵清仓账本侧退役(PositionSync 删幽灵清仓逻辑)+ 策略级候选 + 统一份额分配器(拆段/余额)+ 买卖份额联动 + 撤段逆操作(含作废第三态)+ 归属分段存储 + 查询/展示适配 + R-017 7 天候选退役(holdingId 键控/占位保留)+ 漏关联软提示 + 回归。
|
||||
|
||||
**不做**:自动归属推导仍不暴露(可另议);全部持仓 tab / 未分配 tab 份额联动 UI;跨策略「未分配余额」自动兜底;账本写操作自动化兜底(软提示不自动改账本)。
|
||||
|
||||
## 验收
|
||||
|
||||
见规划后的迭代验收标准(本需求涉及既有已实现功能变更:迭代 12 幽灵清仓语义、迭代 15 R-017 退役,验收需含存量回归)。
|
||||
@@ -28,7 +28,8 @@
|
||||
| R-014 | 持仓内存快照(服务端 10s 定时同步,请求不再穿透 QMT) | 服务端建全量持仓**内存快照**(不落库):PositionSync 启动预热 + 每 10s 全量拉 QMT → 校验 → 整体替换;同步失败保留上次快照(QMT 抖动不再白屏);空快照双重确认(/health + getAsset 账户身份)才接受为真清仓;快照为空读穿透兜底(当场拉一次并回填);**幽灵持仓自动清仓**:QMT 连续 3 轮(约 30s)消失的 code,本地全部策略当前持仓自动转历史(账户身份守卫:accountId 未知跳过、切换重置跳过;部分减持不触发);strategy-positions / unallocated / summary 三个接口改读快照,前端零改动。**2026-09-02 定稿(内存不落库 / 读穿透兜底 / 幽灵自动清仓 / 同步时间不显示,老师逐项拍板),2026-09-02 完成(回归 34/34 + r013 回归 21/21 + typecheck + build 通过),迭代 12 验收待老师人工确认** | 架构梳理讨论引出 + 老师指令(2026-09-02) | P1 | 已定稿 | 2026-09-02 | 12-持仓内存快照 | 已实现(待验收) |
|
||||
| R-015 | 盘口数据内存化(QuoteSync/QuoteHub 统一管理)+ 数据同步指示灯 | QuoteSync(取数:prime + 5s REST 刷新 + 涨停跌停按交易日缓存)/ QuoteHub(存查:内存快照 + watch 集合 + 读穿透 + 价格单一入口)替换 MarketFeed/MarketDataHub(方案 A 替换不包壳);WS 通路移除;market_quotes_cache 表退役 DROP;涨停价/跌停价字段(/data/instrument 按交易日缓存不落库);会话头部新增持仓/盘口同步指示灯(绿/黄/灰 + 悬停详情 + 点击即 syncNow)+ sync-status 端点(吸收临时诊断 market-stats);策略持仓表行情列扩展(涨停/跌停/今开/最高 4 列,默认隐藏)。**2026-09-02 定稿(五项老师逐项拍板),2026-09-02 完成(test-quote-sync 全绿 + 存量回归 34/34、21/21 + typecheck + build),迭代 13 验收待老师人工确认** | 架构演进讨论引出 + 老师指令(2026-09-02) | P1 | 已定稿 | 2026-09-02 | 13-盘口内存快照 | 已实现(待验收) |
|
||||
| R-016 | 策略 tab 历史持仓展示(显示/隐藏已清仓持仓 + 清仓时间范围筛选) | 两个策略 tab(做T/网格超市)title 旁添加:① 显示/隐藏已清仓历史持仓的开关;② 显示历史时默认展示近一周清仓的持仓,并提供 近1个月/近3个月/近半年/近1年 范围选项。背景:卖出清仓后幽灵清仓(R-014)把持仓行转历史,策略 tab 当场少行(2026-09-07 大连热电做T卖出实际发生);历史数据一直在库(strategy_holdings closed_at 非空),只缺展示入口;历史行可复用 R-010 行展开做交易复盘。**2026-09-07 定稿(Q1-Q7 老师拍板:Q2 改判「份额显示 0 就好,不改 closeHolding 语义」,其余按 AI 建议;核心目标=追溯该持仓相关的历史操作)**;历史行复用 R-010 展开做交易复盘;新增 strategy-holdings/history 端点,当前持仓路径零改动。**2026-09-07 二轮补充(Q8-Q9 老师拍板):当天(本地自然日 00:00 起)已清仓的持仓默认显示(range='today' 恒显示层),「历史持仓」开关只控制今天之前的历史(历史持仓关≠当天清仓被藏起);层间按 holdingId 去重,计数不含已清仓行维持;零写路径变更维持**。详见 R-016.md | 老师指令(2026-09-07 优化功能需求讨论) | P1 | 已定稿 | 2026-09-07 | 14-策略tab历史持仓展示 | 已实现(待验收) |
|
||||
| R-017 | 委托归属候选纳入近期清仓持仓 | 交易记录 tab 归属候选只回当前持仓——清仓后当日委托失去下拉锚点无法补关联(R-016 关联观察立项,2026-09-07 大连热电实际发生:盘中归属后被清为未关联,下拉已无做T候选)。**2026-09-07 定稿(老师指令确认方向)**:候选 = 当前持仓 + 近 7 天清仓持仓(closed 标记,选项文本「已清仓」后缀),下拉 value 改 holdingId 键控(同策略多轮不撞值),排序当前在前/清仓按 closed_at DESC;当日数据修复经 set-attribution 通道完成(非固化功能)。详见 R-017.md | 老师指令(2026-09-07) | P1 | 已定稿 | 2026-09-07 | 15-归属候选纳入清仓持仓 | 已实现(待验收) |
|
||||
| R-017 | 委托归属候选纳入近期清仓持仓 | 交易记录 tab 归属候选只回当前持仓——清仓后当日委托失去下拉锚点无法补关联(R-016 关联观察立项,2026-09-07 大连热电实际发生:盘中归属后被清为未关联,下拉已无做T候选)。**2026-09-07 定稿(老师指令确认方向)**:候选 = 当前持仓 + 近 7 天清仓持仓(closed 标记,选项文本「已清仓」后缀),下拉 value 改 holdingId 键控(同策略多轮不撞值),排序当前在前/清仓按 closed_at DESC;当日数据修复经 set-attribution 通道完成(非固化功能)。**2026-09-08 R-018 取代标注:7 天清仓候选退役(R-018 T2 拍板)**——归属候选改策略级 attribution-targets(迭代 16 实施),本需求「清仓后补关联」痛点根因(幽灵清仓抢先归档)已由数据域分界消除;holdingId 键控/占位安全逻辑保留。详见 R-017.md | 老师指令(2026-09-07) | P1 | 已定稿 | 2026-09-07 | 15-归属候选纳入清仓持仓 | 已实现(被 R-018 语义取代) |
|
||||
| R-018 | 交易关联驱动持仓份额动态调整(归属 = 账本写操作) | 从交易记录出发给委托关联策略/持仓时自动同步调整该策略下持仓份额(关联即账本写操作):策略级候选 + 统一「份额分配器」(一笔委托拆 0..N 段 × 策略×量,余额默认全量可改小,允许部分关联)+ 买入加仓/建新仓(不复活旧行)、卖出减仓/归零清仓、仓不足自动截断续分、无仓关联失败;撤段逆操作(撤建仓买入=持仓作废第三态不产生假清仓历史、撤加仓=减回归零作废、撤卖出=恢复活动仓+份额加回,粒度=段、改归属原子);**数据域分界(老师拍板):幽灵清仓等同步机制只作用于「全部持仓」对账单域,不再写 strategy_holdings 账本**——账本生命周期只由交易关联 + 手动份额操作驱动,账本转历史唯一途径=卖出单关联 closeHolding;R-017 7 天清仓候选退役(holdingId 键控/占位保留);漏关联软提示(账本有活动行+QMT 无此 code)。影响既有定稿:技术约束-017/产品约束-011/R-014/迭代12 幽灵清仓条款修订。详见 R-018.md | 老师指令(2026-09-08 新优化需求讨论) | P1 | 已定稿 | 2026-09-08 | 16-交易关联驱动持仓份额动态调整 | **已实现(已归档至 已完成/R-018.md,2026-09-08 验收通过)** |
|
||||
|
||||
## 渐进明细规划素材
|
||||
|
||||
|
||||
Reference in New Issue
Block a user