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:
@@ -0,0 +1,132 @@
|
||||
# 技术实现方案:06-数据存储SQLite(JSON → SQLite)
|
||||
|
||||
> 依据:PLAN-007 | 需求:R-008 | 设计约束:技术约束-012(变更)、数据存储设计.md(第 9 节变更落实)、技术约束-011(测试隔离)
|
||||
|
||||
## 技术选型
|
||||
|
||||
- **存储引擎**:node:sqlite(Node ≥22.5 内置,DatabaseSync 同步 API)——零依赖分发(技术约束-005 不受影响),experimental 风险由存储层封装隔离(D2);
|
||||
- **封装**:新增 SqliteStore 模块(init/事务/读写/close),对外暴露与 DataStore 兼容的行为,未来可切换;
|
||||
- **迁移**:一次性迁移脚本(scripts/migrate-json-to-sqlite.mjs)+ DataStore 启动检测自动迁移(旧 JSON 存在且 SQLite 空 → 自动迁移,幂等);
|
||||
- **表结构**:strategy_holdings + market_quotes_cache 两表(R-008 讨论修正:① 策略持仓为**持仓生命周期表**(自增 holding_id + created_at/closed_at),不用 JSON 列压扁;② 行情快照只存 last_price/last_close 两个具体列,表名 market_quotes_cache 明确缓存语义;不建 trades,R-007 时再建)。
|
||||
|
||||
## 表结构设计(落实数据存储设计.md 第 9 节)
|
||||
|
||||
```sql
|
||||
-- 策略持仓生命周期(一笔 = 一次「建仓→清仓」的完整持仓;历史保留,供交易记录关联)
|
||||
CREATE TABLE IF NOT EXISTS strategy_holdings (
|
||||
holding_id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增持仓编号(交易记录关联锚点;实测删除不复用)
|
||||
strategy_id TEXT NOT NULL, -- 对应 settings 中策略的 id(如 grid-supermarket,非 name)
|
||||
code TEXT NOT NULL, -- 证券代码(含后缀,如 600719.SH)
|
||||
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, -- 证券代码(含后缀,如 600719.SH)
|
||||
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 | 002129.SZ | 200 | 迁移时间戳 | NULL |
|
||||
| 3 | grid-supermarket | 300057.SZ | 1000 | 迁移时间戳 | NULL |
|
||||
| ... | ... | ... | ... | ... | ... |
|
||||
| 8 | manual-t | 600719.SH | 1000 | 迁移时间戳 | NULL |
|
||||
| 9 | manual-t | 300426.SZ | 800 | 迁移时间戳 | NULL |
|
||||
| 10 | manual-t | 600719.SH | 0 | 迁移时间戳 | 1788xxx(清仓后) |
|
||||
|
||||
**schema 语义对齐**(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 列);盘口五档等实时字段仅存内存缓存,不落盘。
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
SqliteStore(新增,node:sqlite 封装)
|
||||
├─ init() : 建库建表(CREATE TABLE IF NOT EXISTS)
|
||||
├─ migrateJson() : 旧 JSON → SQLite(迁移前备份,幂等)
|
||||
├─ 持仓生命周期 : openHolding/addShares/reduceShares/closeHolding
|
||||
├─ 持仓查询 : getCurrentHoldings/getHoldingHistory
|
||||
├─ 行情读写 : getMarketQuote/getMarketQuotes/setMarketQuotes
|
||||
└─ close() : 关闭连接(插件释放时)
|
||||
|
||||
DataStore(改造,对外 API 升级为持仓生命周期语义)
|
||||
└─ 内部委托 SqliteStore(PositionManager 按单票操作,不再整策略重写)
|
||||
|
||||
MarketDataHub(改造,持久化路径不变)
|
||||
└─ setMarketQuotes → SqliteStore.setMarketQuotes(防抖写回)
|
||||
```
|
||||
|
||||
**存储层方法集(替代原 getDataset/setDataset/removeDataset/getAllDatasets 整体读写)**:
|
||||
|
||||
| 方法 | 是什么 | 什么时候用 | SQL 要点 |
|
||||
|---|---|---|---|
|
||||
| openHolding(strategyId, code, shares) | 建仓:创建一笔新持仓 | 某策略首次分配某股票(原「从 0 添加」) | INSERT(holding_id 自增,created_at=now,closed_at=NULL) |
|
||||
| addShares(strategyId, code, shares) | 加仓:当前持仓份额累加 | 原 addToStrategy 的「该策略已有该票」分支 | UPDATE shares=shares+? WHERE closed_at IS NULL |
|
||||
| reduceShares(strategyId, code, shares) | 减仓:当前持仓份额减少 | 原 removeFromStrategy | UPDATE shares=shares-? WHERE closed_at IS NULL |
|
||||
| closeHolding(strategyId, code) | 清仓:份额归零,closed_at=now 转历史 | 原 removeFromStrategy 减到 0 / 份额清零 / 删除策略联动 | UPDATE closed_at=now WHERE closed_at IS NULL(shares 置 0) |
|
||||
| getCurrentHoldings(strategyId?) | 查当前持仓(含 code/shares/created_at) | 策略 tab 渲染 / 汇总 / 单票查询 | SELECT ... WHERE closed_at IS NULL(可加 strategy_id 过滤) |
|
||||
| getHoldingHistory(code?) | 查历史(含 closed_at,当前+历史) | 未来交易记录关联 / 复盘 | SELECT ...(可加 code 过滤) |
|
||||
|
||||
**不再保留**:removeDataset(物理删除整策略)、setDataset(整策略整体重写)——删除策略/一键清零改为 closeHolding 批量软关闭(历史保留,可关联交易记录)。
|
||||
|
||||
**PositionManager 适配**:
|
||||
- _setShares(内部整策略重写)改为按单票调 openHolding/addShares/reduceShares/closeHolding;
|
||||
- addToStrategy:无当前持仓 → openHolding;有 → addShares;
|
||||
- removeFromStrategy:减后>0 → reduceShares;减到0 → closeHolding;
|
||||
- clearStrategyShares / strategies/remove:批量 closeHolding(转历史,不物理删除);
|
||||
- getDataset/getAllDatasets 调用点改 getCurrentHoldings(对外返回 [{code, shares}] 结构不变,上层 API 无感知)。
|
||||
|
||||
## 迁移策略(D6)
|
||||
|
||||
1. **启动检测**:DataStore.load()/loadMarket() 时检测 —— 旧 JSON 文件存在 且 SQLite 空(strategy_holdings / market_quotes_cache 无数据)→ 触发自动迁移;
|
||||
2. **迁移动作**:
|
||||
- store.json → strategy_holdings 表(strategies[].dataset 平铺为行:strategy_id + code + shares,created_at=迁移时间戳,closed_at=NULL);
|
||||
- store.market.json → market_quotes_cache 表(抽取每条快照的 lastPrice/lastClose → last_price/last_close 两列);
|
||||
- 旧 allocations.json(若仍存在,R-006 遗留迁移源,code 为中心)→ strategy_holdings 表(聚合转换,created_at=迁移时间戳);
|
||||
- 迁移前将 JSON 文件备份为 *.json.bak(D7);
|
||||
3. **幂等**:SQLite 有数据即跳过迁移;迁移失败不破坏原 JSON(先读后写,写库失败不删原文件);
|
||||
4. **一次性脚本**:scripts/migrate-json-to-sqlite.mjs(独立执行,供手动/CI 使用,逻辑与启动自动迁移共用)。
|
||||
|
||||
## 涉及设计约束
|
||||
|
||||
| 约束 | 内容 |
|
||||
|---|---|
|
||||
| 技术约束-012 | 数据存储遵循数据存储设计.md:SQLite 升级后更新为遵循 SQLite 存储设计(本迭代执行变更) |
|
||||
| 技术约束-011 | 测试/回归脚本禁止在真实数据上执行写操作:ODL_TEST_DATA_DIR 独立数据目录 |
|
||||
| 技术约束-005 | 客户端 bundle 不受影响(node:sqlite 为 Node 内置,服务端使用) |
|
||||
|
||||
## 变更记录(实现期间)
|
||||
|
||||
- **2026-09-01 移除「一键清零」按钮**(老师决定):
|
||||
- 前端:策略 tab 顶部「一键清零」按钮 + 确认弹窗 + handleClearAll 移除(StrategyTab.jsx);
|
||||
- 后端:`remove-all-shares` 端点移除(api/strategies.js、api/index.js 注释);
|
||||
- 保留:`clearStrategyShares` 方法(`strategies/remove` 删除策略仍联动清份额);
|
||||
- 理由:该操作粒度尴尬(误触风险高、单票移出+删除策略已覆盖需求),实际使用价值低;
|
||||
- 影响:前端 UI 精简;API 面收缩(旧客户端若调用 remove-all-shares 将 404,本次同版本更新无兼容问题)。
|
||||
|
||||
## 实现步骤
|
||||
|
||||
1. 文档骨架:PLAN-007 + 迭代 06(本步);
|
||||
2. 约束变更:技术约束-012 更新 + 数据存储设计.md 第 9 节落实;
|
||||
3. SqliteStore:node:sqlite 封装(init/迁移/持仓生命周期/行情读写/close);
|
||||
4. DataStore 改造:持仓读写切换 SqliteStore(openHolding/addShares/reduceShares/closeHolding/getCurrentHoldings/getHoldingHistory),保留迁移检测;
|
||||
5. PositionManager 适配:整策略重写 → 单票生命周期操作;
|
||||
6. 迁移脚本:一次性迁移脚本 + 启动自动迁移(幂等);
|
||||
7. MarketDataHub 适配:行情持久化切至 market_quotes_cache 表(落盘只投影 last_price/last_close 两列);
|
||||
8. 构建测试:pnpm run build + typecheck + 独立数据目录回归测试(技术约束-011);
|
||||
9. 验收 + 复盘。
|
||||
Reference in New Issue
Block a user