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,61 @@
|
|||||||
|
# 计划:数据存储管理(JSON → SQLite)(阶段航点)
|
||||||
|
|
||||||
|
> 编号:PLAN-007 | 粒度:阶段航点(大粒度) | 创建:2026-09-01 | 状态:**进行中**
|
||||||
|
> 派生自终极目标:目标-003(按策略监控市场)、目标-005(市场复盘)、目标-006(交易复盘)——复杂查询/多数据集统一存储是复盘与监控的数据底座
|
||||||
|
> 依据需求:**R-008(已定稿,2026-09-01,D1-D8 全部确认)** —— 符合入范围门槛
|
||||||
|
> 设计约束:技术约束-012(变更中)、数据存储设计.md(第 9 节变更预告)
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
将神之一手的数据存储从 **JSON data store**(store.json / store.market.json / store.schema.json)升级为 **SQLite 数据库**(node:sqlite):**仅存储引擎替换,对外行为不变**。引入 strategies + allocation + market_quotes 三表,策略定义仍存 DSH settings,JSON 迁移后废弃(迁移前自动备份)。
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
**做**:
|
||||||
|
1. **存储层封装**:新增 SqliteStore 模块,封装 node:sqlite(DatabaseSync)——隔离 experimental 风险,提供 init/migrate/upsert/query 能力;对外行为与 DataStore 兼容(D2);
|
||||||
|
2. **表结构**:strategy_holdings(**持仓生命周期表**:自增 holding_id + strategy_id/code/shares/created_at/closed_at,一对多「多」侧单表)+ market_quotes_cache(code → last_price/last_close 两列,行情快照缓存)两表(D4;2026-09-01 讨论修正:不用 strategies JSON 列 + allocation 冗余双表;行情不用 snapshot JSON 列;持仓加生命周期为交易记录关联铺路);
|
||||||
|
3. **策略定义位置**:仍存 DSH settings,不迁 SQLite(D5);
|
||||||
|
4. **迁移**:一次性迁移脚本(scripts/)+ 启动检测自动迁移(旧 JSON 存在且 SQLite 空 → 自动迁移,幂等,迁移前自动备份)(D6);
|
||||||
|
5. **JSON 废弃**:迁移后废弃 JSON 文件(迁移前备份)(D7);
|
||||||
|
6. **DataStore 改造**:数据读写/迁移/schema 逻辑切换至 SQLite 后端,**存储层 API 升级为持仓生命周期语义**(openHolding/addShares/reduceShares/closeHolding/getCurrentHoldings/getHoldingHistory,废弃整策略重写的 setDataset/removeDataset);
|
||||||
|
7. **MarketDataHub 适配**:行情持久化走 SQLite market_quotes_cache 表(落盘只投影 last_price/last_close 两列),防抖写回逻辑不变。
|
||||||
|
|
||||||
|
**不做**:
|
||||||
|
- 不建 trades 表(R-007 交易记录时再建,D4);
|
||||||
|
- 不做数据管理界面/导出/清理(D3:本期仅存储引擎替换);
|
||||||
|
- 不引入新依赖(node:sqlite 为 Node 内置,零依赖分发,技术约束-005 不受影响);
|
||||||
|
- 不改策略设置交互(D5);
|
||||||
|
- 不实现复杂查询 API(本期仅保证现有行为不变,查询能力为后续功能铺路)。
|
||||||
|
|
||||||
|
## 程序结构(改造后)
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── component/
|
||||||
|
│ ├── SqliteStore.js # 新增:SQLite 存储层封装(node:sqlite)
|
||||||
|
│ ├── DataStore.js # 改:读写/迁移切换至 SqliteStore(API 升级为持仓生命周期)
|
||||||
|
│ └── MarketDataHub.js # 改:行情持久化走 SqliteStore.market_quotes_cache
|
||||||
|
├── index.js # 改:实例化 SqliteStore 注入 DataStore
|
||||||
|
scripts/
|
||||||
|
└── migrate-json-to-sqlite.mjs # 新增:一次性迁移脚本(独立可执行)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实现步骤(建议顺序)
|
||||||
|
|
||||||
|
1. **文档骨架**:PLAN-007 + 迭代 06 子目录(本步);
|
||||||
|
2. **约束变更**:技术约束-012 更新 + 数据存储设计.md 第 9 节落实(SQLite 表结构设计替代 JSON schema);
|
||||||
|
3. **SqliteStore**:node:sqlite 封装(init/事务/upsert/query/备份);
|
||||||
|
4. **DataStore 改造**:load/save/行情读写切换 SqliteStore,存储层 API 升级为持仓生命周期,保留迁移检测;
|
||||||
|
5. **PositionManager 适配**:整策略重写 → 单票生命周期操作;
|
||||||
|
6. **迁移脚本**:一次性迁移脚本(JSON → SQLite)+ 启动自动迁移(幂等);
|
||||||
|
6. **MarketDataHub 适配**:行情持久化切至 market_quotes_cache 表(投影 last_price/last_close);
|
||||||
|
7. **构建 + 测试**:pnpm run build + typecheck + 独立数据目录回归测试(技术约束-011);
|
||||||
|
8. **验收**:对照迭代 06 验收标准逐条核验,记录迭代复盘。
|
||||||
|
|
||||||
|
## 验收要点
|
||||||
|
|
||||||
|
- 数据落 SQLite(one-divine-lot.db),store.json/store.market.json 迁移后废弃(迁移前备份);
|
||||||
|
- 对外行为不变:份额分配 CRUD、行情缓存查询/写回、重启后首屏有价;
|
||||||
|
- 幂等:重复启动不重复迁移;迁移失败不破坏原 JSON;
|
||||||
|
- 旧 allocations.json 迁移链路仍有效(并入 SQLite 迁移);
|
||||||
|
- 技术约束-011:回归测试用独立数据目录(ODL_TEST_DATA_DIR)。
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
| 技术约束-006 | 客户端 slots.register 的 component 必须是第二参数(register({...}, Component));settings schema 必须用 schemastery z.object() 函数式定义 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:component 位置错误致 React #130;普通对象 schema 报 schema is not a function |
|
| 技术约束-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 add;bundle patch 顶层必须是 insert 操作 | 2026-08-28 | 生效 | - | 迭代 01 复盘沉淀:pnpm add 不会更新 dsh.profile.bundles |
|
| 技术约束-007 | 插件安装用 dsh plugin add(自动 reconcile bundles),不直接用 pnpm add;bundle 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) |
|
| 技术约束-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**:存储引擎为 **SQLite(node:sqlite)**——strategy_holdings + market_quotes_cache 两表(持仓生命周期表 + 行情极简两列缓存表);存储层单票生命周期操作;策略定义仍存 DSH settings;旧 JSON(store.json / store.market.json / allocations.json)经一次性迁移脚本 + 启动自动迁移(幂等、迁移前自动备份)后废弃;仅存储引擎替换,对外行为不变 | 2026-09-01 | 生效 | - | 变更(2026-09-01 R-008 定稿 + 迭代 06 实施):JSON data store → SQLite(node: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 数据误删事故沉淀:回归测试误删大连热电/万顺新材份额分配,老师定「测试用独立数据目录」 |
|
| 技术约束-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 |
|
| 技术约束-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 机制与内置插件注册方式 |
|
| 技术约束-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 机制与内置插件注册方式 |
|
||||||
|
|||||||
@@ -144,3 +144,125 @@
|
|||||||
|
|
||||||
- 技术约束-011:测试/回归脚本禁止在真实数据上执行写操作(独立数据目录)
|
- 技术约束-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);不建 strategies(JSON 列压扁)+ 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_holdings(dataset 平铺为行,created_at=迁移时间戳,closed_at=NULL);store.market.json → market_quotes_cache(抽取 lastPrice/lastClose 两列);allocations.json → strategy_holdings(code 为中心聚合转换);迁移前 JSON 备份为 *.json.bak;
|
||||||
|
- 幂等:SQLite 有数据即跳过;迁移失败不破坏原 JSON;
|
||||||
|
- 一次性脚本:scripts/migrate-json-to-sqlite.mjs(与启动自动迁移共用逻辑)。
|
||||||
|
|
||||||
|
### 9.6 关键设计决策(更新)
|
||||||
|
|
||||||
|
| 决策 | 结论 | 理由 |
|
||||||
|
|---|---|---|
|
||||||
|
| 存储介质 | **SQLite(node:sqlite)** | 复杂查询 + 未来多数据集统一存储(R-008 D1) |
|
||||||
|
| 驱动 | node:sqlite(Node ≥22.5 内置) | 零依赖分发,experimental 风险由 SqliteStore 封装隔离(D2) |
|
||||||
|
| 持仓表 | **strategy_holdings**(持仓生命周期:holding_id 自增 + created_at/closed_at) | 关系模型正确、无 JSON 压扁、支持按 code 反查;为交易记录关联铺路(2026-09-01 讨论演进) |
|
||||||
|
| 行情表 | market_quotes_cache(code → 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:sqlite:init/迁移/持仓生命周期/行情读写/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:测试/回归脚本禁止在真实数据上执行写操作(独立数据目录);
|
||||||
|
- 本文件为新领域设计约束,后续数据存储变更以本文件为最终依据。
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
# UI 交互调用分析:06-数据存储SQLite
|
||||||
|
|
||||||
|
> 分析日期:2026-09-01 | 依据:技术实现方案.md(接口定义)
|
||||||
|
> 目的:基于 SQLite 存储层接口(strategy_holdings / market_quotes_cache + 持仓生命周期方法集),梳理**前端各操作 → 服务端 → 存储层**的完整调用链,确认对外 API 不变的前提下,存储层升级对调用流程的影响。
|
||||||
|
|
||||||
|
## 调用链路总览(升级后不变的部分)
|
||||||
|
|
||||||
|
```
|
||||||
|
前端组件(React)
|
||||||
|
│ fetch POST /odl/api/<method> body={ args }
|
||||||
|
▼
|
||||||
|
connection.jsx useRpc() ── 标准化剥离 one-divine-lot/ 前缀
|
||||||
|
▼
|
||||||
|
服务端 src/api/*.js 领域分发(positions / strategies / qmt-connections / market / trades)
|
||||||
|
▼
|
||||||
|
PositionManager / MarketDataHub / Settings(业务逻辑)
|
||||||
|
▼
|
||||||
|
DataStore(改造)── 委托 ──▶ SqliteStore(node:sqlite,新增)
|
||||||
|
│ ├─ strategy_holdings 表(持仓生命周期)
|
||||||
|
│ └─ market_quotes_cache 表(行情快照)
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键结论**:前端组件与 /odl/api/* 端点**零改动**;改动集中在服务端业务层(PositionManager 适配单票生命周期)+ 存储层(DataStore→SqliteStore)。以下逐操作分析。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作 1:策略持仓 tab 加载(读)
|
||||||
|
|
||||||
|
**触发**:进入策略 tab → `StrategyTab.load()`
|
||||||
|
|
||||||
|
**前端调用**(`StrategyTab.jsx`):
|
||||||
|
```js
|
||||||
|
Promise.all([
|
||||||
|
call('one-divine-lot/strategy-positions', { args: { strategyId } }), // 策略持仓
|
||||||
|
call('one-divine-lot/unallocated', {}), // 未分配(添加下拉候选)
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
**服务端**(`api/strategies.js`)→ `PositionManager.getStrategyPositions(strategyId)`:
|
||||||
|
- 升级前:`getAllPositions()`(QMT REST)+ `storage.getDataset(strategyId)`(整策略读 store.json)
|
||||||
|
- 升级后:`getAllPositions()` + `SqliteStore.getCurrentHoldings(strategyId)`(`SELECT ... WHERE strategy_id=? AND closed_at IS NULL`)
|
||||||
|
|
||||||
|
**存储层**:
|
||||||
|
```sql
|
||||||
|
SELECT code, shares, created_at FROM strategy_holdings
|
||||||
|
WHERE strategy_id = ? AND closed_at IS NULL
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回**:`[{ code, name, volume, shares, ... }]`(对外结构不变,上层 API 无感知)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作 2:添加持仓(建仓/加仓)
|
||||||
|
|
||||||
|
**触发**:策略 tab 添加表单 → 选标的 + 填份额 → 确认添加 → `handleAdd()`
|
||||||
|
|
||||||
|
**前端调用**(`StrategyTab.jsx`):
|
||||||
|
```js
|
||||||
|
call('one-divine-lot/add-shares', { args: { code, strategyId, shares: Number(addShares) } });
|
||||||
|
```
|
||||||
|
|
||||||
|
**服务端** → `PositionManager.addToStrategy(code, strategyId, shares)`:
|
||||||
|
- 升级前:读 code 份额 map → 校验超限 → `_setShares(code, next)`(**整策略 dataset 重写**)
|
||||||
|
- 升级后:校验逻辑不变(total / 已分配 + 新增 ≤ 总持仓),写入改为**单票生命周期**:
|
||||||
|
- 该策略该 code **无当前持仓** → `SqliteStore.openHolding(strategyId, code, shares)`(INSERT,created_at=now,closed_at=NULL)
|
||||||
|
- **已有当前持仓** → `SqliteStore.addShares(strategyId, code, shares)`(UPDATE shares += ? WHERE closed_at IS NULL)
|
||||||
|
|
||||||
|
**存储层**:
|
||||||
|
```sql
|
||||||
|
-- openHolding(建仓)
|
||||||
|
INSERT INTO strategy_holdings (strategy_id, code, shares, created_at, closed_at)
|
||||||
|
VALUES (?, ?, ?, ?, NULL);
|
||||||
|
|
||||||
|
-- addShares(加仓)
|
||||||
|
UPDATE strategy_holdings SET shares = shares + ?
|
||||||
|
WHERE strategy_id = ? AND code = ? AND closed_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回**:`getShares(code)`(`{ strategyId: shares }`,结构不变)→ 前端 `toast.success` + `load()` 刷新
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作 3:移出份额(减仓/清仓)
|
||||||
|
|
||||||
|
**触发**:策略 tab 行内「移出」→ 填数量 → 确认 → `handleRemove()`
|
||||||
|
|
||||||
|
**前端调用**(`StrategyTab.jsx`):
|
||||||
|
```js
|
||||||
|
call('one-divine-lot/remove-shares', { args: { code, strategyId, shares: Number(removeTarget.shares) } });
|
||||||
|
```
|
||||||
|
|
||||||
|
**服务端** → `PositionManager.removeFromStrategy(code, strategyId, shares)`:
|
||||||
|
- 升级前:读份额 → 校验不超移 → `_setShares`(整策略重写;减到 0 则删除该 code 条目)
|
||||||
|
- 升级后:校验不变,写入分两种:
|
||||||
|
- 减后 **> 0** → `SqliteStore.reduceShares(strategyId, code, shares)`(UPDATE shares -= ? WHERE closed_at IS NULL)
|
||||||
|
- 减到 **= 0** → `SqliteStore.closeHolding(strategyId, code)`(UPDATE closed_at=now, shares=0;**转历史,不物理删除**)
|
||||||
|
|
||||||
|
**存储层**:
|
||||||
|
```sql
|
||||||
|
-- reduceShares(减仓,仍持仓)
|
||||||
|
UPDATE strategy_holdings SET shares = shares - ?
|
||||||
|
WHERE strategy_id = ? AND code = ? AND closed_at IS NULL;
|
||||||
|
|
||||||
|
-- closeHolding(清仓,转历史)
|
||||||
|
UPDATE strategy_holdings SET shares = 0, closed_at = ?
|
||||||
|
WHERE strategy_id = ? AND code = ? AND closed_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**行为变化**:清仓数据**保留为历史**(closed_at 非 NULL),不再物理删除 —— 供未来交易记录/复盘关联(R-007)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作 4:全部移入(单票级)
|
||||||
|
|
||||||
|
**触发**:策略 tab 行内「全部移入」→ `handleMoveAllIn()`
|
||||||
|
|
||||||
|
**前端调用**(`StrategyTab.jsx`):
|
||||||
|
```js
|
||||||
|
call('one-divine-lot/move-all-shares', { args: { code, strategyId } });
|
||||||
|
```
|
||||||
|
|
||||||
|
**服务端** → `PositionManager.moveAllUnallocatedToStrategy(code, strategyId)`:
|
||||||
|
- 升级前:计算未分配份额 → `_setShares`(整策略重写,`current[strategyId] + unallocated`)
|
||||||
|
- 升级后:等价于「把未分配份额并入该策略当前持仓」→ 走生命周期:
|
||||||
|
- 无当前持仓 → `openHolding(strategyId, code, unallocated)`
|
||||||
|
- 有当前持仓 → `addShares(strategyId, code, unallocated)`
|
||||||
|
|
||||||
|
**存储层**:同操作 2 的 openHolding / addShares(shares = 原份额 + 未分配份额)
|
||||||
|
|
||||||
|
**返回**:`getShares(code)` → `toast.success` + `load()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作 5:一键清零(策略级)
|
||||||
|
|
||||||
|
**触发**:策略 tab 顶部「一键清零」→ 弹窗确认 → `handleClearAll()`
|
||||||
|
|
||||||
|
**前端调用**(`StrategyTab.jsx`):
|
||||||
|
```js
|
||||||
|
call('one-divine-lot/remove-all-shares', { args: { strategyId } });
|
||||||
|
```
|
||||||
|
|
||||||
|
**服务端** → `PositionManager.clearStrategyShares(strategyId)`:
|
||||||
|
- 升级前:`storage.getDataset(strategyId)`(读整策略)→ `storage.removeDataset(strategyId)`(**物理删除整策略 dataset**)
|
||||||
|
- 升级后:**批量 closeHolding** —— 该策略所有当前持仓转历史(不物理删除):
|
||||||
|
- 读 `getCurrentHoldings(strategyId)` 得到受影响 code 列表
|
||||||
|
- 逐笔 `closeHolding(strategyId, code)`
|
||||||
|
- 返回受影响 code 列表(对外结构不变)
|
||||||
|
|
||||||
|
**存储层**:
|
||||||
|
```sql
|
||||||
|
UPDATE strategy_holdings SET shares = 0, closed_at = ?
|
||||||
|
WHERE strategy_id = ? AND closed_at IS NULL;
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回**:`string[]`(受影响标的 code 列表)→ 前端提示「已清空策略份额(N 只标的回到未分配)」+ `load()`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作 6:删除策略(设置页联动清份额)
|
||||||
|
|
||||||
|
**触发**:设置页「策略分组」→ 删除策略 → 弹窗确认 → `handleDelete()`
|
||||||
|
|
||||||
|
**前端调用**(`SettingsSection.jsx`):
|
||||||
|
```js
|
||||||
|
call('one-divine-lot/strategies/remove', { args: { strategyId: id } });
|
||||||
|
```
|
||||||
|
|
||||||
|
**服务端**(`api/strategies.js`):
|
||||||
|
```js
|
||||||
|
await removeStrategy(settings, args.strategyId); // settings 删除策略定义
|
||||||
|
await manager.clearStrategyShares(args.strategyId); // 联动清份额
|
||||||
|
return getStrategies(settings);
|
||||||
|
```
|
||||||
|
|
||||||
|
**升级后**:`clearStrategyShares` 同样走**批量 closeHolding** —— 该策略持仓全部转历史(closed_at=now),**历史保留**(供交易记录关联),settings 中策略定义删除(策略 id 从配置消失,但 SQLite 中历史行仍带 strategy_id,可追溯)。
|
||||||
|
|
||||||
|
**存储层**:同操作 5 的批量 closeHolding。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作 7:行情现价展示(读缓存)
|
||||||
|
|
||||||
|
**触发**:任意持仓表格渲染现价列 → `useMarket().getPrice(code)`
|
||||||
|
|
||||||
|
**前端调用**(`MarketDataProvider` 轮询):
|
||||||
|
```js
|
||||||
|
// 表格上报 code → Provider 调行情接口(去重,不再自拉 positions)
|
||||||
|
registerCodes(codes);
|
||||||
|
// 周期轮询
|
||||||
|
call('one-divine-lot/market-snapshot', { args: { codes } });
|
||||||
|
```
|
||||||
|
|
||||||
|
**服务端**(`api/market.js`)→ `MarketDataHub.getByCodes()`:
|
||||||
|
- 内存缓存 → 磁盘缓存 → REST 补拉(三级命中)
|
||||||
|
- 升级后:磁盘缓存由 `store.market.json` 改为 `SqliteStore.getMarketQuotes(codes)`(读 market_quotes_cache 表)
|
||||||
|
|
||||||
|
**存储层**:
|
||||||
|
```sql
|
||||||
|
SELECT code, last_price, last_close FROM market_quotes_cache WHERE code IN (...);
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回**:`{ code: { lastPrice, lastClose } }` → PriceCell 渲染现价 + 红涨绿跌着色
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作 8:行情写回缓存(服务端后台)
|
||||||
|
|
||||||
|
**触发**:MarketDataHub.ingest() 防抖 3s 写回(非用户直接操作)
|
||||||
|
|
||||||
|
**前端**:无直接调用(服务端后台行为)
|
||||||
|
|
||||||
|
**服务端**:`MarketDataHub` → 升级后 `SqliteStore.setMarketQuotes(quotes)`:
|
||||||
|
```sql
|
||||||
|
INSERT INTO market_quotes_cache (code, last_price, last_close, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT(code) DO UPDATE SET
|
||||||
|
last_price = excluded.last_price,
|
||||||
|
last_close = excluded.last_close,
|
||||||
|
updated_at = excluded.updated_at;
|
||||||
|
```
|
||||||
|
|
||||||
|
**说明**:只落盘 lastPrice/lastClose 两列(UI 消费的现价 + 涨跌基准);盘口五档等仅内存缓存,不落盘(方案明确)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 存储层接口映射总表
|
||||||
|
|
||||||
|
| 前端操作 | /odl/api 端点 | PositionManager 方法 | SqliteStore 方法(升级) | 原 DataStore 方法(废弃) |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 策略 tab 加载 | strategy-positions | getStrategyPositions | **getCurrentHoldings** | getDataset |
|
||||||
|
| 添加(无持仓) | add-shares | addToStrategy | **openHolding** | setDataset(整策略重写) |
|
||||||
|
| 添加(有持仓) | add-shares | addToStrategy | **addShares** | setDataset |
|
||||||
|
| 移出(>0) | remove-shares | removeFromStrategy | **reduceShares** | setDataset |
|
||||||
|
| 移出(=0) | remove-shares | removeFromStrategy | **closeHolding** | setDataset(删条目) |
|
||||||
|
| 全部移入 | move-all-shares | moveAllUnallocatedToStrategy | openHolding / addShares | setDataset |
|
||||||
|
| 一键清零 | remove-all-shares | clearStrategyShares | **批量 closeHolding** | removeDataset(物理删) |
|
||||||
|
| 删除策略 | strategies/remove | removeStrategy + clearStrategyShares | 批量 closeHolding | removeDataset |
|
||||||
|
| 行情读取 | market-snapshot | MarketDataHub.getByCodes | **getMarketQuotes** | store.market.json 读 |
|
||||||
|
| 行情写回 | (后台) | MarketDataHub.ingest | **setMarketQuotes** | store.market.json 写 |
|
||||||
|
|
||||||
|
## 对前端的结论
|
||||||
|
|
||||||
|
1. **前端零改动**:所有操作仍走 /odl/api/* 端点,`useRpc` 封装、请求/响应结构不变;
|
||||||
|
2. **行为增强(前端无感)**:清仓/一键清零/删除策略后数据**转历史保留**(closed_at),而非物理删除 —— 为 R-007 交易记录关联与复盘铺路;
|
||||||
|
3. **行情落盘降维**:只落 lastPrice/lastClose 两列(响应无需关心,服务端投影);
|
||||||
|
4. **唯一可见差异**:历史持仓数据存在(未来若做「持仓历史」UI,getHoldingHistory 可直接支撑)。
|
||||||
@@ -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. 验收 + 复盘。
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 迭代复盘:06-数据存储SQLite(JSON → SQLite)
|
||||||
|
|
||||||
|
> 复盘日期:2026-09-01 | 迭代状态:**已完成(验收通过)**
|
||||||
|
> 关联需求:R-008(数据存储管理 JSON → SQLite,已定稿)
|
||||||
|
> 关联计划:PLAN-007(数据存储 SQLite 化)
|
||||||
|
|
||||||
|
## 结果
|
||||||
|
|
||||||
|
迭代 06 达成:数据存储从 JSON data store(store.json / store.market.json / store.schema.json)升级为 SQLite(store.db,node:sqlite),仅存储引擎替换、对外行为不变。真实数据迁移完成(15 条持仓 + 13 条行情),JSON 清理废弃,份额/行情/设置功能全部不回归。
|
||||||
|
|
||||||
|
## 过程事实
|
||||||
|
|
||||||
|
1. **技术选型验证**:node:sqlite(Node 22.23.1 内置,DatabaseSync,SQLite 3.51.3)实测可用;better-sqlite3 需原生编译 → 选 node:sqlite(零依赖分发,experimental 风险由 SqliteStore 封装隔离);
|
||||||
|
2. **表结构**:strategy_holdings(持仓生命周期表:holding_id 自增 + strategy_id/code/shares/created_at/closed_at + 部分唯一索引)+ market_quotes_cache(code 主键 + last_price/last_close/updated_at 两列);不建 trades(R-007 再建);
|
||||||
|
3. **SqliteStore 封装**:init/迁移/持仓生命周期(openHolding/addShares/reduceShares/closeHolding)/行情读写/close;
|
||||||
|
4. **DataStore 改造**:门面委托 SqliteStore,保留兼容 API(getDataset/getAllDatasets/行情读写),启动检测自动迁移(幂等);
|
||||||
|
5. **PositionManager 适配**:整策略 dataset 重写(_setShares)→ 单票生命周期(_applySharesForStrategy 绝对目标语义);
|
||||||
|
6. **迁移脚本**:scripts/migrate-json-to-sqlite.mjs + pnpm migrate 命令(一次性 + 启动自动迁移共用逻辑);
|
||||||
|
7. **真实迁移**:15 持仓 + 13 行情迁入 SQLite,与迁移前逐条一致;JSON 备份 .bak 后清理删除(仅留 store.db);
|
||||||
|
8. **移除「一键清零」按钮**(老师决定):前端按钮/弹窗 + 后端 remove-all-shares 端点移除;clearStrategyShares 保留(删除策略联动)。
|
||||||
|
|
||||||
|
## 经验教训(复盘沉淀)
|
||||||
|
|
||||||
|
### 1. 语义迁移必须逐方法核对(重要)
|
||||||
|
- **教训**:改造 PositionManager 时,把 addToStrategy 的「新增量」误当「绝对目标份额」传给生命周期方法,导致加仓 100 变 100 —— 真实 API 验证时发现(600719.SH 手动做T 少了 100 股),已修复并回滚数据;
|
||||||
|
- **沉淀**:方法语义变更(增量 vs 绝对量)必须显式命名(_applySharesForStrategy 接收 target 绝对目标),调用方逐处核对;改造后必须用真实 API 走一遍 CRUD 回归,不能只靠隔离单测。
|
||||||
|
|
||||||
|
### 2. node:sqlite 的锁行为
|
||||||
|
- 插件进程持有 SQLite 连接时,外部脚本再开连接写库会报 `attempt to write a readonly database`;
|
||||||
|
- **沉淀**:真实数据修正必须走插件自己的 API(或停机后操作),外部脚本只能读;测试写操作用 ODL_TEST_DATA_DIR 隔离(技术约束-011)。
|
||||||
|
|
||||||
|
### 3. 关闭连接
|
||||||
|
- 插件 dispose 时必须 storage.close()(关闭 SQLite 连接),避免资源泄漏。
|
||||||
|
|
||||||
|
### 4. 文档与实际必须一致
|
||||||
|
- 验收标准初稿写 `one-divine-lot.db`,实际实现用 `store.db` —— 归档前已修正。
|
||||||
|
|
||||||
|
## 遗留/后续
|
||||||
|
|
||||||
|
1. **盘中行情写 SQLite 验证**:本次验收时已收盘,无实时推送;开盘后应确认行情防抖写回 store.db(观察 updated_at / 文件 mtime);
|
||||||
|
2. **R-007 交易记录关联**:strategy_holdings.holding_id 已就绪,可作为 trades 表关联锚点(R-007 实现时建 trades 表);
|
||||||
|
3. **SqliteStore 迁移逻辑引用旧文件名**:migrateJson 仍检查 store.json 等(幂等跳过),保留用于新环境初始化。
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 迭代目标:06-数据存储SQLite(JSON → SQLite 存储引擎替换)
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
将神之一手的数据存储从 JSON data store(store.json / store.market.json / store.schema.json)升级为 **SQLite 数据库**(node:sqlite),**仅存储引擎替换、对外行为不变**:引入 strategy_holdings(持仓生命周期表)+ market_quotes_cache 两表,存储层升级为单票持仓生命周期(openHolding/addShares/reduceShares/closeHolding),策略定义仍存 DSH settings,一次性迁移 + 启动自动迁移(幂等),JSON 迁移后废弃(迁移前自动备份)。
|
||||||
|
|
||||||
|
## 目标描述
|
||||||
|
|
||||||
|
- 范围:R-008(已定稿 2026-09-01,D1-D8 全部确认);不新增其他功能域;
|
||||||
|
- 要解决的问题:JSON 全量读写无法支撑复杂查询/筛选(按 code/时间/策略),且未来交易记录/复盘等多数据集统一存储需要数据库底座(目标-003/005/006);
|
||||||
|
- 引用需求:**R-008(已定稿,2026-09-01)**;
|
||||||
|
- 实现方式:node:sqlite(Node ≥22.5,实测 22.23.1 可用)+ SqliteStore 封装隔离 experimental 风险 + 启动自动迁移(幂等)+ JSON 废弃(D1-D8);
|
||||||
|
- 关联设计约束:技术约束-012(变更)、数据存储设计.md(第 9 节变更预告落实)。
|
||||||
|
|
||||||
|
## 目标讨论过程
|
||||||
|
|
||||||
|
- 2026-09-01 老师提出需求:准备上 SQLite,现在基于 JSON 文件(T-008 入池);
|
||||||
|
- 2026-09-01 第一轮讨论定稿(D1-D8):动机=复杂查询+未来铺路;node:sqlite(实测可用);仅引擎替换;strategy_holdings+market_quotes_cache 两表(持仓生命周期表 + 行情极简两列缓存表);存储层单票生命周期操作;策略仍存 settings;一次性迁移脚本+启动自动迁移;JSON 迁移后废弃;P1;
|
||||||
|
- 2026-09-01 实测验证:本机 Node v22.23.1,node:sqlite 可用(DatabaseSync,内置 SQLite 3.51.3),标记 Experimental;better-sqlite3 需编译(倾向稳妥+零依赖分发 → 选 node:sqlite);
|
||||||
|
- 2026-09-01 三要素核对通过,转正为 R-008,进入迭代 06。
|
||||||
|
|
||||||
|
## 对老师的配合需求
|
||||||
|
|
||||||
|
- 确认迭代 06 计划与验收标准(PLAN-007);
|
||||||
|
- 构建安装后重启 DSH 配合验证(首次启动自动迁移真实数据);
|
||||||
|
- 验证迁移结果:store.json/store.market.json 数据正确落 SQLite(备份文件留存),份额分配/行情显示不回归;
|
||||||
|
- 验证持仓生命周期:加仓/减仓/清仓操作后当前持仓与历史(holding_id/created_at/closed_at)正确,删除策略/一键清零后历史保留。
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# 验收标准:06-数据存储SQLite(JSON → SQLite)
|
||||||
|
|
||||||
|
## 验收标准线
|
||||||
|
|
||||||
|
1. **存储介质切换**:数据落 SQLite(store.db),不再生成/更新 store.json / store.market.json / store.schema.json(迁移前备份除外);
|
||||||
|
2. **两表结构**:strategy_holdings + market_quotes_cache 两表存在且结构符合技术实现方案(strategy_holdings 持仓生命周期表:holding_id 自增 + strategy_id/code/shares/created_at/closed_at;market_quotes_cache 仅 last_price/last_close 两列,无 JSON 列);
|
||||||
|
3. **持仓生命周期正确**:openHolding/addShares/reduceShares/closeHolding 操作正确 —— 建仓生成 holding_id+created_at、加/减仓更新当前持仓 shares、清仓 closed_at 置位转历史;同策略同股票仅一笔当前持仓(部分唯一索引);删除策略/一键清零改为软关闭(历史保留);
|
||||||
|
4. **行情不回归**:行情缓存查询/写回不变 —— 启动首屏有价(从 SQLite 加载)、盘中防抖写回、三级命中(内存→SQLite→REST 补拉)行为一致;
|
||||||
|
5. **自动迁移正确**:旧 JSON(store.json / store.market.json / 旧 allocations.json 若存在)→ SQLite 数据一致(份额逐条核对、行情快照逐条核对),迁移前备份文件(*.json.bak)留存;
|
||||||
|
6. **幂等**:重复启动不重复迁移(SQLite 有数据即跳过);迁移失败不破坏原 JSON;
|
||||||
|
7. **JSON 废弃**:迁移成功后 store.json / store.market.json 不再读写(旧文件保留为备份,新数据只写 SQLite);
|
||||||
|
8. **策略定义位置**:策略(id/name/visible/order)仍存 DSH settings,设置页交互不变;
|
||||||
|
9. **不回归**:持仓/策略/行情/连接配置/交易记录等现有功能正常;
|
||||||
|
10. **测试隔离**:回归测试在独立数据目录(ODL_TEST_DATA_DIR)执行,真实数据目录无写操作(技术约束-011)。
|
||||||
|
|
||||||
|
## 验收方法
|
||||||
|
|
||||||
|
- 独立数据目录(ODL_TEST_DATA_DIR)构造旧 JSON 数据(store.json / store.market.json)→ 启动插件 → 确认自动迁移完成、SQLite 数据与 JSON 一致、备份文件留存;
|
||||||
|
- 反复重启确认幂等(不重复迁移、数据不丢);
|
||||||
|
- 通过 API 验证份额 CRUD 与行情读写行为与改造前一致(对照迭代 04/05 回归);
|
||||||
|
- 手动核对真实数据目录:迁移后 store.json / store.market.json 不再更新,store.db 为数据源;
|
||||||
|
- 回归:全部持仓 / 策略 / 行情 / QMT 连接配置 / 交易记录正常。
|
||||||
|
|
||||||
|
## 验收目标
|
||||||
|
|
||||||
|
- 存储引擎替换闭环:JSON → SQLite(迁移正确 + 幂等 + 备份),对外行为零变化;
|
||||||
|
- 为复杂查询/多数据集统一存储铺路(strategy_holdings 持仓生命周期表 + market_quotes_cache 就绪,holding_id 可作为 trades 关联锚点,trades 表 R-007 再建);
|
||||||
|
- 设计约束同步落地:技术约束-012 与数据存储设计.md 更新为 SQLite 设计。
|
||||||
@@ -61,3 +61,15 @@
|
|||||||
- [ ] 迭代目标.md 的讨论过程如何记录(详细纪要 vs 结论摘要)
|
- [ ] 迭代目标.md 的讨论过程如何记录(详细纪要 vs 结论摘要)
|
||||||
- [ ] 验收标准.md 的写法模板(标准线 / 验收方法 / 验收目标的具体写法与示例)
|
- [ ] 验收标准.md 的写法模板(标准线 / 验收方法 / 验收目标的具体写法与示例)
|
||||||
- [ ] 复盘模板(复盘时如何从记录中提炼结论)
|
- [ ] 复盘模板(复盘时如何从记录中提炼结论)
|
||||||
|
|
||||||
|
## 实现经验沉淀(2026-09-01 起,随迭代复盘累积)
|
||||||
|
|
||||||
|
> 跨迭代复用的实现经验,从各次迭代复盘中提炼,作为后续实现的隐性约束。
|
||||||
|
|
||||||
|
### 语义迁移核对(迭代 06,2026-09-01)
|
||||||
|
|
||||||
|
改造/迁移方法时,若方法语义发生变化(如「新增量」变「绝对目标量」、存储整体读写变单条生命周期),必须:
|
||||||
|
|
||||||
|
1. **显式命名语义**:如 `_applySharesForStrategy(target)` 注释标明参数为绝对目标;
|
||||||
|
2. **逐调用点核对**:每个调用点确认传入参数含义一致(增量 vs 绝对量);
|
||||||
|
3. **真实 API CRUD 回归**:不能只靠隔离单测 —— 隔离测试可能掩盖语义混淆(本次 add-shares 加仓 100 变 100 的 Bug 就是真实 API 验证才暴露)。
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# R-008 数据存储管理(JSON → SQLite)· 已完成
|
||||||
|
|
||||||
|
> 归档日期:2026-09-01 | 需求状态:**已完成**
|
||||||
|
> 原索引:docs/05-需求池/需求池索引.md(主索引保留 R-008 条目,指向本归档)
|
||||||
|
> 实现迭代:06-数据存储SQLite(验收通过,迭代复盘见 docs/04-迭代记录/06-数据存储SQLite/迭代复盘.md)
|
||||||
|
> 讨论记录:需求从 T-008 草稿转正,2026-09-01 定稿(D1-D8);实现过程中的语义迁移 Bug 修复与「一键清零」移除见迭代 06 技术实现方案变更记录
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> 状态:**已定稿**(2026-09-01,老师确认)| 登记日期:2026-09-01
|
||||||
|
> 来源:T-008 草稿转正(docs/05-需求池/草稿/T-008-数据存储管理SQLite.md)
|
||||||
|
> 优先级:P1
|
||||||
|
> 关联:docs/03-设计约束/数据存储设计.md(将变更)、技术约束-012(将变更)
|
||||||
|
|
||||||
|
## 想法描述
|
||||||
|
|
||||||
|
将神之一手的数据存储从 **JSON 文件**(store.json / store.market.json / store.schema.json)升级为 **SQLite 数据库**,并引入「数据存储管理」能力。
|
||||||
|
|
||||||
|
**背景**:当前存储基于 JSON data store(R-006,2026-08-31 定),设计约束明确「数据量很少,无需数据库」。现准备引入 SQLite,意味着数据量/查询复杂度已增长到需要数据库的程度,需重新评估该设计决策。
|
||||||
|
|
||||||
|
## 现状(代码审查 2026-09-01)
|
||||||
|
|
||||||
|
| 项 | 现状 |
|
||||||
|
|---|---|
|
||||||
|
| 存储介质 | JSON 文件:store.schema.json(schema)/ store.json(份额分配,策略为中心)/ store.market.json(行情快照缓存) |
|
||||||
|
| 存储模块 | src/component/DataStore.js(读写/迁移/schema)、MarketDataHub.js(行情缓存写回)、AllocationStorage.js(遗留旧存储,迁移源) |
|
||||||
|
| 数据流 | 启动 load → 实盘 ingest → 防抖 3s 写回 store.market.json;原子写(临时文件 + rename) |
|
||||||
|
| 依赖 | 无数据库依赖(package.json 无 sqlite) |
|
||||||
|
| 设计约束 | docs/03-设计约束/数据存储设计.md:「数据量很少(无需数据库/Circe),用 JSON 文件 + 类 JSON Schema」 |
|
||||||
|
|
||||||
|
## 动机(为什么要 SQLite)
|
||||||
|
|
||||||
|
(待老师补充确认 —— 初步推测):
|
||||||
|
- [ ] 数据量增长:行情缓存 / 交易记录 / 复盘记录等数据积累,JSON 全量读写成本变高?
|
||||||
|
- [ ] 查询需求:按 code / 时间 / 策略做复杂查询(如历史行情、交易复盘筛选),JSON 内存过滤不便?
|
||||||
|
- [ ] 写入频率:盘中行情防抖写回 + 未来更多高频写入,JSON 原子写压力大?
|
||||||
|
- [ ] 多数据集统一管理:份额 / 行情 / 交易记录 / 复盘记录统一入一个库?
|
||||||
|
|
||||||
|
## 待讨论点
|
||||||
|
|
||||||
|
- [ ] **动机确认**:老师确认上 SQLite 的具体原因(上述哪条 / 其他)——决定改造范围;
|
||||||
|
- [ ] **库选型**:better-sqlite3(同步 API,Node 原生绑定)?node:sqlite(Node 22+ 内置)?其他?(技术约束-001:复用优先,需论证)
|
||||||
|
- [ ] **schema 设计**:表结构 —— strategies / allocation(份额)/ market_quotes / trades / reviews?如何对齐现有 store.schema.json 语义?
|
||||||
|
- [ ] **迁移策略**:现有 store.json / store.market.json / 旧 allocations.json 如何迁入 SQLite(一次性迁移?启动自动迁移 + 备份?参照 R-006 迁移模式)?
|
||||||
|
- [ ] **保留还是废弃 JSON**:SQLite 落地后 JSON 文件是否完全废弃(读兼容 / 写切换 / 双写过渡)?
|
||||||
|
- [ ] **策略定义位置**:策略(id/name/visible/order)仍存 DSH settings 还是迁入 SQLite?(现有设计决策:存 settings)
|
||||||
|
- [ ] **数据存储管理能力**:本需求「数据存储管理」具体指什么 —— 数据浏览/导出?备份/恢复?清理(行情缓存膨胀治理)?还是仅存储引擎替换?
|
||||||
|
- [ ] **设计约束变更**:数据存储设计.md 的「无需数据库」决策需修订 —— 变更理由与记录;
|
||||||
|
- [ ] **依赖引入**:新增原生依赖(better-sqlite3 需编译)对插件构建/分发的影响(技术约束-005 bundle 格式);
|
||||||
|
- [ ] **优先级与排期**:机制定稿后确定。
|
||||||
|
|
||||||
|
> 成熟后按「三要素」(边界清楚 / 核心逻辑明确 / 老师确认)讨论定稿,再转正到根目录形成正式需求。
|
||||||
|
## 讨论结论(2026-09-01 第一轮)
|
||||||
|
|
||||||
|
### 已确认决策
|
||||||
|
|
||||||
|
| # | 决策点 | 结论 |
|
||||||
|
|---|---|---|
|
||||||
|
| D1 | 动机 | **需要复杂查询/筛选** + **为未来功能铺路**(多数据集统一管理);非数据量/写入压力问题 |
|
||||||
|
| D2 | 库选型 | **node:sqlite**(Node ≥22.5,实测本机 22.23.1 可用);存储层封装独立模块隔离 experimental 风险 |
|
||||||
|
| D3 | 能力范围 | **仅存储引擎替换**(JSON→SQLite),对外行为不变,最小改动;本期不做数据管理界面/导出/清理 |
|
||||||
|
| D4 | 表范围 | 本期:**strategies + allocation + market_quotes** 三表;**不建 trades 表**(R-007 时再建) |
|
||||||
|
| D5 | 策略定义位置 | **仍存 DSH settings**(不迁 SQLite,设置页交互不变) |
|
||||||
|
| D6 | 迁移策略 | **一次性迁移脚本** + **启动检测自动迁移**(检测旧 JSON 存在且 SQLite 空 → 自动迁移,幂等) |
|
||||||
|
| D7 | JSON 去留 | **迁移后废弃**(迁移前自动备份) |
|
||||||
|
| D8 | 优先级 | **P1** |
|
||||||
|
|
||||||
|
### 实测验证记录(2026-09-01)
|
||||||
|
|
||||||
|
- 本机 Node v22.23.1,`node:sqlite` 可用(`DatabaseSync`,内置 SQLite 3.51.3),但标记 **Experimental**(`ExperimentalWarning`);
|
||||||
|
- better-sqlite3 实测 ESM 加载正常,但原生模块需编译;分发需预编译产物;
|
||||||
|
- 选型考量:本项目倾向稳妥 + 零依赖分发 → node:sqlite;存储层封装(SqliteStore)隔离 experimental 风险,未来可切换。
|
||||||
|
|
||||||
|
### 设计约束变更(待执行)
|
||||||
|
|
||||||
|
- 技术约束-012(数据存储遵循 JSON data store)需**变更**:SQLite 落地后更新为遵循 SQLite 存储设计;
|
||||||
|
- docs/03-设计约束/数据存储设计.md 需修订:「无需数据库」决策 → SQLite(记录变更理由:复杂查询 + 未来功能铺路);
|
||||||
|
- 变更需在定稿时同步执行(本需求转正后)。
|
||||||
|
|
||||||
|
### 转正条件(三要素核对)
|
||||||
|
|
||||||
|
- [x] 边界清楚:仅引擎替换 + 三表 + 不建 trades + 策略仍存 settings;
|
||||||
|
- [x] 核心逻辑明确:node:sqlite + 自动迁移 + JSON 废弃(D1-D8 全部确认);
|
||||||
|
- [ ] **老师确认定稿**(待确认)—— 确认后转正为正式需求 R 编号,进入计划/迭代。
|
||||||
@@ -40,6 +40,28 @@
|
|||||||
|
|
||||||
> 实践样本:R-002 从提出到定稿经历 7+ 轮讨论,完整走完三要素后进入迭代 01。
|
> 实践样本:R-002 从提出到定稿经历 7+ 轮讨论,完整走完三要素后进入迭代 01。
|
||||||
|
|
||||||
|
## 归档与转正规范(2026-09-01 老师确认,源自 R-005/R-008 实践)
|
||||||
|
|
||||||
|
### 需求归档检查清单
|
||||||
|
|
||||||
|
需求实现完成后归档,按序执行:
|
||||||
|
|
||||||
|
1. **核实完成**:对应迭代复盘存在且标记「验收通过」,索引中实现状态为「已实现」;
|
||||||
|
2. **补归档头**:归档文件加「归档日期 / 需求状态:已完成 / 实现迭代 / 讨论记录索引」头部;
|
||||||
|
3. **移入归档**:需求文件从根目录移入 `已完成/`;
|
||||||
|
4. **更新索引**:主索引保留条目,实现状态改为「已实现(已归档)」,描述标注归档路径;
|
||||||
|
5. **双向追溯**:迭代记录与需求池互相引用(迭代复盘关联需求编号,需求归档引用迭代复盘路径)。
|
||||||
|
|
||||||
|
> 归档只追加不改写历史。
|
||||||
|
|
||||||
|
### 草稿转正清理
|
||||||
|
|
||||||
|
草稿转正为正式需求(T→R)时:
|
||||||
|
|
||||||
|
- **必须清理草稿残留的头部/模板内容**(如「状态:起草」「登记日期」旧头部),确保正式文件只有一套头部;
|
||||||
|
- 转正后删除草稿文件;
|
||||||
|
- 索引同步更新(草稿行状态 → 已转需求 R-xxx)。
|
||||||
|
|
||||||
## 目录结构(2026-08-28 老师确认)
|
## 目录结构(2026-08-28 老师确认)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
| R-005 | WS 盘中价格实时更新 | 3 个监控表格(全部持仓/手动做T/网格超市)盘中现价实时显示:服务端中转 + 行情缓存(store.market.json 持久化)+ 前端轮询读缓存;现价列 + 红涨绿跌 + 变化高亮。**2026-08-31 定稿,2026-09-01 完成(迭代 04 验收通过),已归档至 已完成/R-005.md** | 老师指令(2026-08-31,T-005 转正) | P1 | 已定稿 | 2026-09-01 | 04-WS盘中价格实时更新 | **已实现(已归档)** |
|
| R-005 | WS 盘中价格实时更新 | 3 个监控表格(全部持仓/手动做T/网格超市)盘中现价实时显示:服务端中转 + 行情缓存(store.market.json 持久化)+ 前端轮询读缓存;现价列 + 红涨绿跌 + 变化高亮。**2026-08-31 定稿,2026-09-01 完成(迭代 04 验收通过),已归档至 已完成/R-005.md** | 老师指令(2026-08-31,T-005 转正) | P1 | 已定稿 | 2026-09-01 | 04-WS盘中价格实时更新 | **已实现(已归档)** |
|
||||||
| R-004 | QMT 连接配置(多配置管理 + 会话头部快捷切换) | 设置页「QMT 连接配置」子 tab:多配置 CRUD(卡片形式)、单选激活(数据源热切换立即生效)、默认标记(启动自动激活)、测试连接(/health 代理)、删除边界处理;会话头部(PTC 模式标签旁)快捷切换 chip。**2026-08-29 定稿(Q1-Q10)并完成实现与验收(7/7 通过),已归档至 已完成/R-004.md**。决策沉淀:产品约束-005/006、技术约束-003(变更)/008/009、UI约束-001/002 | 老师指令(2026-08-28) | P1 | 已定稿 | 2026-08-29 | 03-QMT连接配置 | **已实现(已归档)** |
|
| R-004 | QMT 连接配置(多配置管理 + 会话头部快捷切换) | 设置页「QMT 连接配置」子 tab:多配置 CRUD(卡片形式)、单选激活(数据源热切换立即生效)、默认标记(启动自动激活)、测试连接(/health 代理)、删除边界处理;会话头部(PTC 模式标签旁)快捷切换 chip。**2026-08-29 定稿(Q1-Q10)并完成实现与验收(7/7 通过),已归档至 已完成/R-004.md**。决策沉淀:产品约束-005/006、技术约束-003(变更)/008/009、UI约束-001/002 | 老师指令(2026-08-28) | P1 | 已定稿 | 2026-08-29 | 03-QMT连接配置 | **已实现(已归档)** |
|
||||||
| R-007 | 交易记录接入(订单与成交数据 + 时间段查询) | 交易记录 tab 接入 QMT Bridge 当日委托(/trade/orders)与当日成交(/trade/trades):**单表合并**(委托主行 + 展开成交明细)+ **时间段查询**(今日/本周/本月 + 手动起止)+ 费用计算(佣金费率+最低5元/印花税/过户费,委托级合并计费)。**2026-09-01 定稿并完成(迭代 05 验收通过),已归档至 已完成/R-007.md** | 老师指令(2026-09-01) | P1 | 已定稿 | 2026-09-01 | 05-交易记录接入 | **已实现(已归档)** |
|
| R-007 | 交易记录接入(订单与成交数据 + 时间段查询) | 交易记录 tab 接入 QMT Bridge 当日委托(/trade/orders)与当日成交(/trade/trades):**单表合并**(委托主行 + 展开成交明细)+ **时间段查询**(今日/本周/本月 + 手动起止)+ 费用计算(佣金费率+最低5元/印花税/过户费,委托级合并计费)。**2026-09-01 定稿并完成(迭代 05 验收通过),已归档至 已完成/R-007.md** | 老师指令(2026-09-01) | P1 | 已定稿 | 2026-09-01 | 05-交易记录接入 | **已实现(已归档)** |
|
||||||
|
| R-008 | 数据存储管理(JSON → SQLite) | 数据存储从 JSON data store 升级为 SQLite(node:sqlite):仅存储引擎替换 + strategies/allocation/market_quotes 三表 + 一次性迁移脚本(启动自动迁移)+ JSON 废弃;策略仍存 DSH settings;不建 trades 表(R-007 时再建)。**2026-09-01 定稿(T-008 转正,D1-D8 确认),2026-09-01 完成(迭代 06 验收通过),已归档至 已完成/R-008.md** | 老师指令(2026-09-01,T-008 转正) | P1 | 已定稿 | 2026-09-01 | 06-数据存储SQLite | **已实现(已归档)** |
|
||||||
|
|
||||||
## 渐进明细规划素材
|
## 渐进明细规划素材
|
||||||
|
|
||||||
@@ -33,3 +34,4 @@
|
|||||||
| T-005 | 通过 ws 长连接做市场数据的实时反映 | WebSocket 长连接实现市场数据(持仓/行情)实时推送与展示;源自迭代 01 复盘。**2026-08-31 转正为 R-005(迭代 04 实施中)**,草稿文件:`草稿/通过ws长连接做市场数据的实时反映.md` | 已转需求 R-005 |
|
| T-005 | 通过 ws 长连接做市场数据的实时反映 | WebSocket 长连接实现市场数据(持仓/行情)实时推送与展示;源自迭代 01 复盘。**2026-08-31 转正为 R-005(迭代 04 实施中)**,草稿文件:`草稿/通过ws长连接做市场数据的实时反映.md` | 已转需求 R-005 |
|
||||||
| T-006 | 数据池中间层 + 策略表格动态字段配置 | 服务端数据池(数据集中间层:按 code 聚合宽表、多源字段映射、填充器架构)+ 策略持仓表格动态字段配置(列配置×池行渲染)。源自 R003 讨论(2026-08-28)拆分独立,草稿文件:`草稿/数据池中间层与策略表格动态字段配置.md` | 起草 |
|
| T-006 | 数据池中间层 + 策略表格动态字段配置 | 服务端数据池(数据集中间层:按 code 聚合宽表、多源字段映射、填充器架构)+ 策略持仓表格动态字段配置(列配置×池行渲染)。源自 R003 讨论(2026-08-28)拆分独立,草稿文件:`草稿/数据池中间层与策略表格动态字段配置.md` | 起草 |
|
||||||
| T-007 | RSS 订阅管理 | 管理 RSS 订阅源(增删改查)+ 抓取聚合订阅内容,作为资讯/消息来源;支撑 目标-003 市场监控、目标-004 消息管理,与 T-004 同属消息链路。**2026-08-28 老师确认先放草稿**,草稿文件:`草稿/RSS订阅管理.md` | 起草 |
|
| T-007 | RSS 订阅管理 | 管理 RSS 订阅源(增删改查)+ 抓取聚合订阅内容,作为资讯/消息来源;支撑 目标-003 市场监控、目标-004 消息管理,与 T-004 同属消息链路。**2026-08-28 老师确认先放草稿**,草稿文件:`草稿/RSS订阅管理.md` | 起草 |
|
||||||
|
| T-008 | 数据存储管理(JSON → SQLite) | 数据存储从 JSON data store 升级为 SQLite(node:sqlite)数据库 + 数据存储管理能力;**变更既有设计约束**(数据存储设计.md「无需数据库」决策)。**2026-09-01 定稿转正为 R-008 并完成(迭代 06)** | 已转需求 R-008 |
|
||||||
|
|||||||
Reference in New Issue
Block a user