Files
one_divine_lot/docs/03-设计约束/数据存储设计.md
T
kyugao dd98b4d45b 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)
2026-09-01 18:04:28 +08:00

269 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 数据存储设计(JSON Data Store Schema
> 策略数据集 + 行情缓存的 JSON data store 设计(R-006 及其扩展,2026-08-31 / 2026-09-01 定)
> 本文件是数据存储领域的**设计约束文档**,后续数据存储相关迭代以此为依据。
## 1. 设计目标
1. **数据快速展现**:DataStore 的核心目标是「数据快速展现」——份额分配、行情价格持久化,重启/刷新后首屏即有数据,不依赖每次实盘查询。
2. **标准化描述**:为每个策略定义标准化的字段描述(schema),在描述之上挂数据集。
3. **统一 JSON 操作**:数据量很少(无需数据库/Circe),用 JSON 文件 + 类 JSON Schema 统一读写与校验。
## 2. 总体架构
```
~/.dsh/one-divine-lot/
├── store.schema.json # 数据集 schema(类 JSON Schema,随代码发布,自动生成)
├── store.json # 份额分配数据(策略为中心,原子写)
└── store.market.json # 行情快照缓存(code → 最新行情,原子写)
```
**分层**
- **策略定义**id/name/visible/order):存 DSH settings~/.dsh/settings.yaml),设置页交互不变
- **份额分配**(策略 → 股票 → 股数):存 store.json(策略为中心)
- **行情快照**code → lastPrice 等):存 store.market.json(持久化缓存)
## 3. 数据文件结构与 Schema
### 3.1 store.schema.json(数据集 schema
```json
{
"version": 1,
"kind": "one-divine-lot-data-store",
"strategies": {
"description": "策略数据集:每个策略一个 dataset(份额分配)",
"type": "array",
"items": {
"strategyId": { "type": "string", "required": true, "description": "策略 id(与 settings 中的策略一致)" },
"dataset": {
"type": "array",
"description": "该策略的持仓数据集(份额分配)",
"items": {
"code": { "type": "string", "required": true, "description": "证券代码(含后缀,如 600719.SH" },
"shares": { "type": "number", "required": true, "description": "份额(股数,>0" }
}
}
}
}
}
```
### 3.2 store.json(份额分配,策略为中心)
```json
{
"version": 1,
"strategies": [
{
"strategyId": "grid-supermarket",
"dataset": [
{ "code": "600719.SH", "shares": 800 },
{ "code": "300057.SZ", "shares": 1000 }
]
},
{
"strategyId": "manual-t",
"dataset": [
{ "code": "600719.SH", "shares": 1000 }
]
}
]
}
```
**数据视角**:按策略查持仓(策略为中心),比旧 code 为中心(allocations.json)更贴合业务。
### 3.3 store.market.json(行情快照缓存)
```json
{
"version": 1,
"savedAt": 1788193491108,
"quotes": {
"600719.SH": {
"time": 1788159604000,
"timetag": "20260831 15:00:04",
"lastPrice": 7.16,
"open": 7.19,
"high": 7.24,
"low": 7.02,
"lastClose": 7.22,
"volume": 63523,
"amount": 45341300
}
}
}
```
**用途**:宿主重启后首屏快速展示上次价格(不依赖实盘订阅);实盘推送/轮询做增量更新并定期写回。
## 4. 数据流
```
启动时:
DataStore.load() → 读 store.json(份额分配)
DataStore.loadMarket() → 读 store.market.json(行情缓存,首屏有价)
MarketFeed._primePositions() → 主动拉持仓盘口 → 写缓存(服务端启动即预热最新价)
实盘中:
MarketFeed WS 推送 / REST 定时刷新 → MarketDataHub.ingest() → 内存缓存
MarketDataHub 防抖 3s → DataStore.setMarketQuotes() → store.market.json
查询时(market-snapshot):
MarketDataHub.getByCodes() → 内存 → 磁盘 → REST 补拉(三级命中)
```
## 5. 关键设计决策
| 决策 | 结论 | 理由 |
|---|---|---|
| 存储介质 | JSON 文件(不引入数据库) | 数据量很少,JSON + schema 足够 |
| 策略定义位置 | DSH settings(不迁 store | 设置页交互不变,查询简单 |
| 份额分配结构 | 策略为中心(strategies[].dataset | 贴合业务视角,schema 统一描述 |
| dataset 格式 | [{code, shares}] 数组 | 可扩展字段(未来加成本价/备注) |
| 行情缓存 | 持久化(store.market.json | DataStore 为数据快速展现存在,重启不丢价 |
| 写入 | 原子写(临时文件 + rename) | 防损坏,多次写安全 |
| 测试隔离 | ODL_TEST_DATA_DIR 环境变量或 dataDir 参数 | 防误删真实数据(技术约束-011) |
## 6. 迁移
- 旧 allocations.jsoncode 为中心)→ 新 store.json(策略为中心):**启动时自动迁移**,旧文件备份为 allocations.json.bak
- 迁移逻辑在 DataStore._migrateFromLegacy()code 为中心 → 策略为中心聚合
## 7. 对应实现
| 模块 | 职责 | 文件 |
|---|---|---|
| DataStore | 份额分配 + 行情持久化读写、迁移、schema | src/component/DataStore.js |
| PositionManager | 份额业务逻辑(依赖 DataStore | src/component/PositionManager.js |
| MarketDataHub | 行情缓存(内存 + 磁盘持久化 + 查询) | src/component/MarketDataHub.js |
| MarketFeed | 行情获取(WS + REST 定时刷新 + 启动 prime | src/component/MarketFeed.js |
## 8. 约束条目(引用)
- 技术约束-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:测试/回归脚本禁止在真实数据上执行写操作(独立数据目录);
- 本文件为新领域设计约束,后续数据存储变更以本文件为最终依据。