迭代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(老师拍板: 内存不落库/读穿透兜底/幽灵自动清仓/不显示同步时间)
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# 计划:持仓内存快照(服务端 10s 定时同步,请求不再穿透 QMT)(阶段航点)
|
||||
|
||||
> 编号:PLAN-013 | 粒度:阶段航点 | 创建:2026-09-02 | 状态:**已实施(待验收)**
|
||||
> 派生自终极目标:目标-008(真实交易系统接入)、目标-001(DSH 插件形态)
|
||||
> 依据需求:**R-014(已定稿,2026-09-02,老师逐项拍板 4 问)** —— 符合入范围门槛
|
||||
> 设计约束:技术约束-010(服务端中转 + 缓存模式沿用)、技术约束-012(存储设计)、技术约束-016(分域目录);本次新增 技术约束-017
|
||||
> 补充说明:本计划为「先实施后补档」——代码随架构梳理讨论当场落地(老师指令),文档链(本计划 + 迭代三件套 + 约束)事后补全,事实以迭代复盘为准
|
||||
|
||||
## 目标
|
||||
|
||||
服务端建立全量持仓**内存快照**(PositionSync,10s 定时与 QMT 同步),策略持仓 / 全部持仓 / 未分配三个接口改读快照为准,**消除「QMT 抖动 → 持仓页面白屏」**并去掉每次进 tab 的穿透 HTTP 调用;前端零改动。
|
||||
|
||||
## 范围
|
||||
|
||||
**做**:
|
||||
1. PositionSync(src/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`。
|
||||
+33
-1
@@ -351,7 +351,39 @@ CREATE INDEX IF NOT EXISTS idx_trade_fills_date ON trade_fills (trade_date);
|
||||
| TradeSync | 服务端定时同步(启动预热 + 60s + UPSERT 幂等) | src/trades/TradeSync.js |
|
||||
| api/trades.js | +trades/history 端点 | src/api/trades.js |
|
||||
|
||||
## 11. 约束条目(引用)
|
||||
## 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 时保持旧行为(兼容) |
|
||||
| 幽灵持仓自动清仓 | QMT 快照连续 3 轮(约 30s)消失的 code,本地全部策略当前持仓 closeHolding 转历史;**账户身份守卫**:accountId 未知当轮跳过、账户切换当轮重置跳过(防误清);部分减持不触发(负数未分配由 UI 暴露) | 老师选定;清仓转历史不物理删除,历史保留语义不变 |
|
||||
| 前端 | 零改动 | 三个接口数据源自动切换 |
|
||||
|
||||
### 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. 约束条目(引用)
|
||||
|
||||
- 技术约束-012:数据存储遵循本文件(SQLite 存储设计,含交易记录表 §10);
|
||||
- 技术约束-011:测试/回归脚本禁止在真实数据上执行写操作(独立数据目录);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# 技术实现方案:12-持仓内存快照
|
||||
|
||||
> 迭代编号:12 | 依据:PLAN-013 + R-014(老师四问拍板)+ 技术约束-010/012/016;新增 技术约束-017
|
||||
|
||||
## 1. PositionSync(src/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 = now;stats.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 });
|
||||
// dispose:positionSync.stop()(在 marketFeed.stop 之后、tradeSync.stop 之前)
|
||||
```
|
||||
|
||||
## 4. 回归脚本(scripts/test-position-sync.mjs,纯内存 mock)
|
||||
|
||||
- **不落库不碰真实数据目录**(内存快照方案下技术约束-011 天然满足);
|
||||
- mock:可编程 dataSource(positions/accountId/healthy 可变状态)+ 内存 storage(getCurrentHoldings/closeHolding 记录调用);
|
||||
- 34 项用例:基本流 / 失败保留快照 / 空快照四分支(health 挂、身份未知、双通过、有旧快照)/ 幽灵防抖(3 轮关闭、复现复位、账户切换重置、身份丢失跳过+恢复后关闭)/ 读路径(快照零 QMT 调用、读穿透回填、未注入兼容、QMT 挂抛错)/ getStrategyPositions 组装回归(shares/holdingId/lastTradePrice/values)/ 定时器冒烟。
|
||||
|
||||
## 5. 验证
|
||||
|
||||
- typecheck + build 通过;test-position-sync 34/34;test-r013-custom-fields 21/21(未注入 positionSync 兼容性证明);
|
||||
- 老师人工验收(见验收标准)。
|
||||
@@ -0,0 +1,44 @@
|
||||
# 迭代复盘:12-持仓内存快照(服务端 10s 定时同步,请求不再穿透 QMT)
|
||||
|
||||
> 复盘日期:2026-09-02 | 迭代状态:**已实施,待老师人工验收**
|
||||
> 关联需求:R-014(持仓内存快照,已定稿)
|
||||
> 关联计划:PLAN-013(计划-持仓内存快照)
|
||||
|
||||
## 结果
|
||||
|
||||
迭代 12 达成:服务端建全量持仓**内存快照**(PositionSync,10s 定时全量同步,**不落库**),策略持仓 / 全部持仓 / 未分配三个接口改读快照为准;同步失败保留上次快照(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/34;test-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)等历史偏差。
|
||||
@@ -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,32 @@
|
||||
# 需求:R-014 持仓内存快照(服务端 10s 定时同步,请求不再穿透 QMT)
|
||||
|
||||
> 登记:2026-09-02 | 来源:架构梳理讨论 + 老师指令 | 状态:**已定稿(老师逐项拍板 4 问)**
|
||||
> 归属:迭代 12 | 计划:PLAN-013
|
||||
|
||||
## 需求描述
|
||||
|
||||
原实盘持仓数据在**每次请求时穿透 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`。
|
||||
@@ -25,6 +25,7 @@
|
||||
| R-011 | Tab 设置:统一管理所有 tab(内置 + 策略分组混排,显示/隐藏 + 拖动排序) | 设置页「通用设置」升级为「Tab 设置」,统一管理所有会话 tab(系统内置 + 策略分组)的唯一入口:两类混排、**拖动排序**(原生 HTML5 DnD,落点立即持久化)+ **显隐开关**;**任何 tab 均不支持重命名/删除**(策略命名/删除仍在「策略分组」子 tab);顺序/显隐统一为一份数据源(tabs 有序数组,策略行名 join strategies 自动跟随改名),删除策略联动删除对应条目;新增策略追加末尾;老配置自动迁移(旧隐藏策略迁移后显示)。**2026-09-02 定稿(Q1-Q5 确认)并完成(迭代 09 验收通过),已归档至 已完成/R-011.md** | 老师指令(2026-09-02) | P1 | 已定稿 | 2026-09-02 | 09-Tab设置统一管理 | **已实现(已归档)** |
|
||||
| R-012 | UI 适配 DSH 主题(浅色 / 深色 / 跟随系统) | 神之一手 UI 适配 DSH 浅色/深色/跟随系统主题:141 处硬编码色值替换为宿主 `--dsw-*` token,随主题自动切换;不自行维护主题偏好(**暂定跟随系统**);涨跌红涨绿跌 → 宿主 state-error/success;仅色值 token 化不动布局。**2026-09-02 定稿(暂定跟随系统)并完成(迭代 10),已归档至 已完成/R-012.md** | 老师指令(2026-09-02) | P1 | 已定稿 | 2026-09-02 | 10-UI主题适配 | **已实现(已归档)** |
|
||||
| R-013 | 策略自定义字段配置(定义随策略,值落库) | 策略可自定义、可扩展:每个策略在设置页「策略分组」子 tab 配置自定义字段(字段名/类型文本·数字·布尔·枚举/枚举项/单位 unit/默认值,configSchema 随策略定义存 settings);每个持仓行按所属策略的定义存一份键值对值(strategy_holdings 新增 values TEXT(JSON) 列);自定义字段作为持仓表列展示,列显隐/排序每策略独立配置(strategyColumns),单元格点击内联编辑。**2026-09-02 定稿(Q1-Q4 + D6)并完成(迭代 11 验收通过),已归档至 已完成/R-013.md** | 老师指令(2026-09-02) | P1 | 已定稿 | 2026-09-02 | 11-策略自定义字段配置 | **已实现(已归档)** |
|
||||
| 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-持仓内存快照 | 已实现(待验收) |
|
||||
|
||||
## 渐进明细规划素材
|
||||
|
||||
|
||||
Reference in New Issue
Block a user