docs(迭代06): 数据存储SQLite迭代文档 + 需求归档(R-008)

- 新增 02-计划/计划-数据存储SQLite.md(PLAN-007)
- 新增 04-迭代记录/06-数据存储SQLite/ 五份文档(迭代目标/技术实现方案/验收标准/UI交互调用分析/迭代复盘)
- 归档 R-008 至 已完成/(含归档头 + 讨论记录索引),索引更新为已实现(已归档)
- 设计约束更新:技术方案约束-012 标注 SQLite 变更、数据存储设计.md 第 9 节变更预告
- 需求池说明.md 新增「归档与转正规范」;迭代记录说明.md 新增「实现经验沉淀」
- 验收标准修正 db 文件名笔误(one-divine-lot.db → store.db)
This commit is contained in:
2026-09-01 18:04:28 +08:00
parent 9b719f13ba
commit dd98b4d45b
12 changed files with 782 additions and 3 deletions
+2 -2
View File
@@ -17,11 +17,11 @@
| 技术约束-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 |
| 技术约束-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**JSON data store schema):份额分配存 store.json(策略为中心)、行情快照存 store.market.json、schema 存 store.schema.json;原子写(临时文件+rename);旧 allocations.json 启动自动迁移;DataStore 为数据快速展现存在(重启/刷新首屏有数据) | 2026-09-01 | 生效 | - | R-006 及扩展定稿(2026-08-31/09-01):JSON data store schema 标准化 + 行情持久化 |
| 技术约束-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 设计为上一版本基线 |
| 技术约束-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 |
| 技术约束-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 机制与内置插件注册方式 |
<!-- 示例条目(确认格式后删除):
| 技术约束-001 | 示例:技术栈以 Node.js / TypeScript 为准,不引入未讨论的新框架 | 2026-08-26 | 生效 | - | 讨论确认:优先复用 DSH 既有能力,新框架需论证 |
-->
-->
+122
View File
@@ -144,3 +144,125 @@
- 技术约束-011:测试/回归脚本禁止在真实数据上执行写操作(独立数据目录)
- 本文件为新领域设计约束,后续数据存储变更以本文件为最终依据
## 9. SQLite 存储设计(R-008 落实,2026-09-01 迭代 06 实施)
> **变更理由**(R-008 讨论确认):① 需要复杂查询/筛选(按 code/时间/策略,JSON 内存过滤不便);② 为未来功能铺路(交易记录/复盘/消息等统一存储)。
> **变更范围**:仅存储引擎替换(JSON → SQLite),对外行为不变(数据快速展现目标不变)。本节替代第 3 节的 JSON schema 作为存储设计;第 3 节 JSON 结构保留为上一版本基线(迁移源)。
> **2026-09-01 迭代 06 演进**:存储层从「数据集整体读写」升级为「持仓生命周期」(自增 holding_id + created_at/closed_at),为交易记录关联铺路。
### 9.1 存储介质
```
~/.dsh/one-divine-lot/
├── one-divine-lot.db # SQLite 数据库(strategy_holdings + market_quotes_cache 两表)
├── store.json.bak # 迁移前备份(原 store.json
├── store.market.json.bak # 迁移前备份(原 store.market.json
└── allocations.json.bak # 迁移前备份(原 allocations.json,若存在)
```
### 9.2 表结构
> 2026-09-01 讨论修正:策略持仓为**一对多的「多」侧单表**strategy_holdings+ **持仓生命周期**(自增 holding_id / created_at / closed_at);不建 strategiesJSON 列压扁)+ allocation(冗余)双表;「一」侧=settings 中的策略定义(D5,不迁 SQLite)。
```sql
-- 策略持仓生命周期(一笔 = 一次「建仓→清仓」的完整持仓;历史保留,供交易记录关联)
CREATE TABLE IF NOT EXISTS strategy_holdings (
holding_id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增持仓编号(交易记录关联锚点;实测删除不复用)
strategy_id TEXT NOT NULL, -- 对应 settings 策略的 id(如 grid-supermarket
code TEXT NOT NULL, -- 证券代码(含后缀)
shares REAL NOT NULL, -- 当前份额(股数,>0
created_at INTEGER NOT NULL, -- 建仓时间(毫秒时间戳)
closed_at INTEGER, -- 清仓时间(NULL=当前持仓;非 NULL=已清仓历史)
PRIMARY KEY (holding_id)
);
-- 业务约束:同一策略同一股票只允许一笔「当前持仓」(closed_at IS NULL 唯一)
CREATE UNIQUE INDEX IF NOT EXISTS idx_active_holding
ON strategy_holdings (strategy_id, code) WHERE closed_at IS NULL;
-- 行情快照缓存(code 维度一行一码;只存 UI 消费的现价 + 昨收,无 JSON 列;cache 语义=重启首屏有价)
CREATE TABLE IF NOT EXISTS market_quotes_cache (
code TEXT PRIMARY KEY,
last_price REAL NOT NULL, -- 最新价(UI 现价)
last_close REAL NOT NULL, -- 昨收(UI 涨跌着色对比基准)
updated_at INTEGER NOT NULL DEFAULT 0
);
```
**数据示例**strategy_holdings):
| holding_id | strategy_id | code | shares | created_at | closed_at |
|---|---|---|---|---|---|
| 1 | grid-supermarket | 601117.SH | 600 | 迁移时间戳 | NULL |
| 2 | grid-supermarket | 300057.SZ | 1000 | 迁移时间戳 | NULL |
| 8 | manual-t | 600719.SH | 1000 | 迁移时间戳 | NULL |
| 9 | manual-t | 600719.SH | 0 | 迁移时间戳 | 1788xxx(清仓后) |
**语义对齐**store.schema.json → SQLite):
- holding_id 自增(SQLite AUTOINCREMENT 实测删除不复用),稳定唯一,作为未来交易记录(trades)的关联外键;
- strategy_holdings.strategy_id = settings 策略的 id(英文 slug,稳定唯一标识;改名只改 name 不影响数据);
- created_at=建仓时间;closed_at=清仓时间(NULL=当前持仓);部分唯一索引保证同策略同股票仅一笔当前持仓;
- market_quotes_cache 对齐原 store.market.json 的 quotes 对象,但只抽取 lastPrice/lastClose 两个具体列入库(无 JSON 列);盘口五档等实时字段仅存内存缓存,不落盘(重启首屏只需「上次价格」)。
### 9.3 分层
- **策略定义**id/name/visible/order):仍存 DSH settings~/.dsh/settings.yaml),设置页交互不变(D5);
- **持仓份额**(策略 → 股票 → 股数 + 生命周期):存 strategy_holdings 表(一对多「多」侧,holding_id 唯一);
- **行情快照**code → lastPrice/lastClose):存 market_quotes_cache 表(极简两列)。
### 9.4 数据流(不变)
```
启动时:
DataStore.load() → 读 SQLite strategy_holdings(迁移检测:旧 JSON 存在且库空 → 自动迁移)
DataStore.loadMarket() → 读 SQLite market_quotes_cache(行情缓存,首屏有价)
MarketFeed._primePositions() → 主动拉持仓盘口 → 写缓存(服务端启动即预热最新价)
实盘中:
MarketFeed WS 推送 / REST 定时刷新 → MarketDataHub.ingest() → 内存缓存
MarketDataHub 防抖 3s → SqliteStore.setMarketQuotes() → market_quotes_cache 表(只投影 last_price/last_close
查询时(market-snapshot):
MarketDataHub.getByCodes() → 内存 → SQLite → REST 补拉(三级命中)
```
### 9.5 迁移
- 触发:启动检测 —— 旧 JSON 文件(store.json / store.market.json / allocations.json)存在 且 SQLite 空 → 自动迁移;
- 动作:store.json → strategy_holdingsdataset 平铺为行,created_at=迁移时间戳,closed_at=NULL);store.market.json → market_quotes_cache(抽取 lastPrice/lastClose 两列);allocations.json → strategy_holdingscode 为中心聚合转换);迁移前 JSON 备份为 *.json.bak
- 幂等:SQLite 有数据即跳过;迁移失败不破坏原 JSON;
- 一次性脚本:scripts/migrate-json-to-sqlite.mjs(与启动自动迁移共用逻辑)。
### 9.6 关键设计决策(更新)
| 决策 | 结论 | 理由 |
|---|---|---|
| 存储介质 | **SQLitenode:sqlite** | 复杂查询 + 未来多数据集统一存储(R-008 D1) |
| 驱动 | node:sqliteNode ≥22.5 内置) | 零依赖分发,experimental 风险由 SqliteStore 封装隔离(D2 |
| 持仓表 | **strategy_holdings**(持仓生命周期:holding_id 自增 + created_at/closed_at | 关系模型正确、无 JSON 压扁、支持按 code 反查;为交易记录关联铺路(2026-09-01 讨论演进) |
| 行情表 | market_quotes_cachecode → last_price/last_close 两列) | 只存 UI 消费的现价+昨收;无 JSON 列;盘口深度仅内存(2026-09-01 讨论修正) |
| 不建表 | 不建 trades(R-007 时再建) | 本期最小改动(D4) |
| 策略定义位置 | DSH settings(不迁 SQLite | 设置页交互不变(D5 |
| 存储层语义 | 单票生命周期(openHolding/addShares/reduceShares/closeHolding),废弃整策略重写(setDataset/removeDataset | 份额=持仓生命周期,历史保留可关联交易记录(2026-09-01 讨论演进) |
| 迁移 | 一次性脚本 + 启动自动迁移(幂等) | 参照 R-006 迁移模式(D6 |
| JSON 去留 | 迁移后废弃(迁移前自动备份) | 单一数据源(D7) |
| 能力范围 | 仅存储引擎替换,对外行为不变 | 最小改动(D3) |
| 写入 | SQLite 事务(同步 API) | 原子性由数据库保证 |
| 测试隔离 | ODL_TEST_DATA_DIR 环境变量或 dataDir 参数 | 防误删真实数据(技术约束-011) |
### 9.7 对应实现(更新)
| 模块 | 职责 | 文件 |
|---|---|---|
| SqliteStore | SQLite 存储层封装(node:sqliteinit/迁移/持仓生命周期/行情读写/close | src/component/SqliteStore.js |
| DataStore | 持仓 + 行情持久化(委托 SqliteStore,对外 API 升级为生命周期语义) | src/component/DataStore.js |
| PositionManager | 份额业务(适配单票生命周期:openHolding/addShares/reduceShares/closeHolding | src/component/PositionManager.js |
| MarketDataHub | 行情缓存(内存 + SQLite 持久化 + 查询) | src/component/MarketDataHub.js |
| MarketFeed | 行情获取(WS + REST 定时刷新 + 启动 prime | src/component/MarketFeed.js |
| migrate 脚本 | 一次性迁移脚本 | scripts/migrate-json-to-sqlite.mjs |
## 10. 约束条目(引用)
- 技术约束-012:数据存储遵循本文件(SQLite 存储设计);
- 技术约束-011:测试/回归脚本禁止在真实数据上执行写操作(独立数据目录);
- 本文件为新领域设计约束,后续数据存储变更以本文件为最终依据。