Files

27 KiB
Raw Permalink Blame History

数据存储设计(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

{
  "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(份额分配,策略为中心)

{
  "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(行情快照缓存)

{
  "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/storage/DataStore.js
PositionManager 份额业务逻辑(依赖 DataStore) src/position/PositionManager.js
MarketDataHub 行情缓存(内存 + 磁盘持久化 + 查询) src/market/MarketDataHub.js
MarketFeed 行情获取(WS + REST 定时刷新 + 启动 prime src/market/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)。

-- 策略持仓生命周期(一笔 = 一次「建仓→清仓」的完整持仓;历史保留,供交易记录关联)
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 讨论修正)
不建表 不建 tradesR-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/storage/SqliteStore.js
DataStore 持仓 + 行情持久化(委托 SqliteStore,对外 API 升级为生命周期语义) src/storage/DataStore.js
PositionManager 份额业务(适配单票生命周期:openHolding/addShares/reduceShares/closeHolding src/position/PositionManager.js
MarketDataHub 行情缓存(内存 + SQLite 持久化 + 查询) src/market/MarketDataHub.js
MarketFeed 行情获取(WS + REST 定时刷新 + 启动 prime src/market/MarketFeed.js
migrate 脚本 一次性迁移脚本 scripts/migrate-json-to-sqlite.mjs

10. 交易记录存储设计(R-009 落实,2026-09-01 迭代 07 实施)

依据:R-009Q1-Q8 定稿,2026-09-01)。在 SQLite 新增交易记录两表(trade_orders + trade_fills),QMT 当日交易数据本地持久化(跨日积累成历史库),支持按策略过滤 / 复盘交易。

10.1 表结构

-- 交易委托(委托主行;order_id 唯一,UPSERT 幂等;零冗余 strategy_id/holding_id
CREATE TABLE IF NOT EXISTS trade_orders (
  order_id       TEXT PRIMARY KEY,   -- m_strOrderSysID
  trade_date     TEXT NOT NULL,      -- 交易日 YYYYMMDDm_strInsertDate
  code           TEXT NOT NULL,
  name           TEXT NOT NULL DEFAULT '',
  exchange       TEXT NOT NULL DEFAULT '',
  direction      TEXT NOT NULL DEFAULT '',   -- buy / sell
  direction_code INTEGER,
  opt_name       TEXT NOT NULL DEFAULT '',
  status         INTEGER,
  order_volume   REAL NOT NULL DEFAULT 0,
  traded_volume  REAL NOT NULL DEFAULT 0,
  limit_price    REAL NOT NULL DEFAULT 0,
  traded_price   REAL NOT NULL DEFAULT 0,
  amount         REAL NOT NULL DEFAULT 0,
  insert_date    TEXT NOT NULL DEFAULT '',
  insert_time    TEXT NOT NULL DEFAULT '',
  insert_ts      INTEGER NOT NULL,   -- 派生:insert_date+insert_time 合成毫秒时间戳
  cancel_info    TEXT NOT NULL DEFAULT '',
  error_msg      TEXT NOT NULL DEFAULT '',
  strategy_id    TEXT,               -- 用户手动设置的策略归属(冗余,Q1 确认)
  holding_id     INTEGER,            -- 用户手动设置的持仓归属(冗余,Q1 确认)
  fetched_at     INTEGER NOT NULL    -- 同步时间戳
);

-- 交易成交(trade_id 唯一,order_id 外键关联委托;零冗余 strategy_id/holding_id
CREATE TABLE IF NOT EXISTS trade_fills (
  trade_id            TEXT PRIMARY KEY, -- m_strTradeID
  order_id            TEXT NOT NULL,    -- → trade_orders.order_id
  trade_date          TEXT NOT NULL,
  code                TEXT NOT NULL,
  name                TEXT NOT NULL DEFAULT '',
  exchange            TEXT NOT NULL DEFAULT '',
  direction           TEXT NOT NULL DEFAULT '',
  direction_code      INTEGER,
  opt_name            TEXT NOT NULL DEFAULT '',
  price               REAL NOT NULL DEFAULT 0,
  volume              REAL NOT NULL DEFAULT 0,
  amount              REAL NOT NULL DEFAULT 0,
  commission_rate_wan REAL NOT NULL DEFAULT 0,
  trade_time          TEXT NOT NULL DEFAULT '',
  fetched_at          INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_trade_orders_date  ON trade_orders (trade_date);
CREATE INDEX IF NOT EXISTS idx_trade_orders_ts    ON trade_orders (insert_ts);
CREATE INDEX IF NOT EXISTS idx_trade_fills_order  ON trade_fills (order_id);
CREATE INDEX IF NOT EXISTS idx_trade_fills_date   ON trade_fills (trade_date);

10.2 关键设计决策(R-009 Q1-Q8 定稿)

决策 结论 理由
表范围 trade_orders + trade_fills 两表 委托 1:N 成交,忠实 R-007 单表合并模型;保留无成交委托(待报/已撤/废单);避免 JSON 压扁
冗余 trade_orders 冗余 strategy_id + holding_id(用户手动设置) 老师二次定稿(Q3 修正):手动设置归属,便于过滤/展示/复盘;trade_fills 仍零冗余(跟随委托)
委托时间派生列 insert_tsinsert_date+insert_time 合成毫秒) join 持仓生命周期窗口用(created_at ≤ t < closed_at
策略/持仓归属 用户手动设置(交易记录 tab 下拉:候选 = 该 code 当前持仓策略 + 未关联);持久化到 trade_orders.strategy_id + holding_idUPSERT 不覆盖归属列;可随时改以最终为准 一票多策略无法算法区分(300057.SZ 分属 grid/manual),人为确认符合「人机合一」(2026-09-01 老师二次定稿)
同步 服务端 TradeSync:启动预热 + 60s 定时 UPSERT(幂等);前端今日轮询写穿 不依赖前端开 tab 也持续积累
代码归一化 QMT 委托/成交 code 无后缀(001330),持仓带后缀(001330.SZ)——数据源映射层统一 normalizeInstrumentCode(补交易所后缀);委托交易日 = insertDate(无独立 tradeDate 字段),tradeDate 兜底 insertDate 保证 FK 链 join 匹配 + 历史按时间段过滤正确(迭代 07 真实数据发现)
同步范围 只同步当日(QMT 无历史接口) 数据逐日积累 = 本地历史库
历史查询 trades/history 端点(时间段/code/策略/方向过滤) 复盘 + 按策略过滤
数据清理 不做自动清理(复盘需要历史) 导出/清理后续迭代

10.3 数据流

启动时:TradeSync 启动预热一次(拉今日 orders+trades → UPSERT 落库,不覆盖归属列)
实盘中:TradeSync 60s 定时同步(UPSERT 幂等,状态覆盖更新,归属列保留)
        前端今日轮询命中 orders/trades 端点 → 写穿本地(机会式)
用户:  交易记录 tab 手动设置每笔委托归属(strategy_id + holding_id 落库,可随时改)
查询时:今日 → QMT 实时(+ 本地归属);历史范围 → trades/history 查本地 SQLite(策略过滤 = 用户设置的归属)

10.4 对应实现(更新)

模块 职责 文件
SqliteStore +trade_orders/trade_fills 建表 + 交易 UPSERT/历史查询 + 策略归属推导 src/storage/SqliteStore.js
DataStore +交易记录方法(upsertTradeOrders/upsertTradeFills/queryTradeHistory src/storage/DataStore.js
TradeSync 服务端定时同步(启动预热 + 60s + UPSERT 幂等) src/trades/TradeSync.js
api/trades.js +trades/history 端点 src/api/trades.js

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 时保持旧行为(兼容)
幽灵持仓自动清仓退役,2026-09-08 R-018 数据域分界 QMT 快照连续 3 轮消失 → 本地全部策略当前持仓 closeHolding 转历史不再写 strategy_holdings:同步机制只作用于对账单域(老师拍板:幽灵清仓本就是一个同步机制,不可以让幽灵把爪子伸太长);账本转历史唯一途径 = 卖出单关联减至 0(R-018);对账单码消失 → 快照无此行 + 漏关联软提示(positions/orphan-hints,只读) R-014 选定时防本地账本残留;R-018 改由交易关联驱动账本生命周期,幽灵自动归档与「账本只由关联交易改」冲突 → 退役(迭代 16 实施,见 §12)
前端 零改动 三个接口数据源自动切换

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. 归属分段存储设计(R-018 落实,2026-09-08 迭代 16 实施)

变更理由(R-018 老师拍板):交易归属从「纯标签」(trade_orders 单组 strategy_id/holding_id,不改数量)升级为账本写操作——关联即自动调整 strategy_holdings 份额;支持一笔委托拆多段分配多策略;撤段做逆操作。归属真相源从 trade_orders 两列迁移到分段表

12.1 strategy_holdings 状态扩展(作废第三态 + 清仓份额快照)

-- 幂等 ALTER(沿用 _ensureXxxColumn 模式)
ALTER TABLE strategy_holdings ADD COLUMN void_at INTEGER;       -- 作废时间(NULL=有效;非 NULL=建仓被撤=从未成立;不进历史层)
ALTER TABLE strategy_holdings ADD COLUMN closed_shares REAL;    -- 清仓前份额快照(closeHolding 写入,供撤卖出段恢复活动用)
-- 活动唯一索引收窄(R-018:活动 = closed_at IS NULL AND void_at IS NULL
DROP INDEX IF EXISTS idx_active_holding;
CREATE UNIQUE INDEX IF NOT EXISTS idx_active_holding
  ON strategy_holdings (strategy_id, code) WHERE closed_at IS NULL AND void_at IS NULL;
  • 生命周期操作语义:closeHolding = shares 置 0 + closed_at + closed_shares=清仓前份额shares 展示仍 0R-016 Q2 语义不变);voidHolding(新增)= shares 置 0 + void_at(非清仓:建仓被撤 = 从未成立);
  • 作废行不进 R-016 历史「已清仓」层(getHoldingsHistory 过滤 void_at IS NULL),保留数据可追溯。

12.2 归属分段表 trade_order_attributions

CREATE TABLE IF NOT EXISTS trade_order_attributions (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  order_id    TEXT NOT NULL,       -- → trade_orders.order_id
  strategy_id TEXT NOT NULL,       -- 段归属策略
  holding_id  INTEGER NOT NULL,    -- 段锚点持仓(R-010 展开用)
  code        TEXT NOT NULL,       -- 冗余
  direction   TEXT NOT NULL,       -- buy/sell(冗余,撤段判向)
  volume      REAL NOT NULL,       -- 该段已成交量(>0;总额 ≤ order.traded_volume
  created_at  INTEGER NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_attr_order_strategy ON trade_order_attributions (order_id, strategy_id);
CREATE INDEX IF NOT EXISTS idx_attr_holding  ON trade_order_attributions (holding_id);
CREATE INDEX IF NOT EXISTS idx_attr_strategy ON trade_order_attributions (strategy_id);
  • 真相源 = 段表trade_orders.strategy_id/holding_id 退役为冗余(保留列不删,防外部脚本 break;不再作为查询/过滤源);
  • 存量迁移:段表空且 trade_orders 有单组归属 → 迁为第一段(volume = 该单 traded_volume;幂等);
  • 查询适配:orders 附加归属、by-holdingR-010)、历史策略过滤(R-009)全部改经段表。

12.3 关键设计决策(R-018 迭代 16)

决策 结论 理由
归属真相源 分段表 trade_order_attributionsorder → 多段 strategyId/holdingId/volume 一笔委托可拆 N 段关联不同策略(R2/R4);单列两字段无法表达
数据域分界 PositionSync/幽灵清仓只写对账单快照,不写 strategy_holdings R-018 老师拍板:同步机制不伸爪子到账本;账本只由交易关联 + 手动份额操作驱动
关联驱动量 段 volume = 该段已成交量 Q4:撤单/废单不动账
撤段逆操作 撤 buy 段 → 减回 / 归零作废(void);撤 sell 段 → 加回 / 恢复活动仓(closed_shares R3/R6/R7:作废 ≠ 清仓(不产生假清仓历史);恢复用 closed_shares 快照
撤段约束 同 holding 内逆序撤销(乱序返回 segment-order-conflict);跨 holding 段独立可任意撤 账本正确性 > 操作便利(宁可拒绝不写错账)
事务 setSegments 全量替换在单事务内(撤旧段 + 加新段) 改归属原子性(R3 边界)

12.4 对应实现(追加)

模块 职责 文件
AttributionService 归属服务:apply/revoke/setSegments(动作表判定 + 逆操作 + 事务) src/trades/AttributionService.js(新增)
SqliteStore 增列/索引重建/voidHolding/close 快照/段表 CRUD/runInTransaction/查询改段表/存量迁移 src/storage/SqliteStore.js
api/trades.js attribution-targets / attribution-set / attribution-segmentsorders 附加段 src/api/trades.js
PositionSync 删除幽灵清仓逻辑(只同步对账单快照) src/position/PositionSync.js
回归测试 分界/动作表/撤段/迁移/软提示 scripts/test-r018-attribution.mjs(新增)

13. 约束条目(引用)

  • 技术约束-012:数据存储遵循本文件(SQLite 存储设计,含交易记录表 §10、归属分段存储 §12);
  • 技术约束-011:测试/回归脚本禁止在真实数据上执行写操作(独立数据目录);
  • 技术约束-017(变更 1,2026-09-08):幽灵自动清仓退役,PositionSync 只同步对账单快照;
  • 本文件为新领域设计约束,后续数据存储变更以本文件为最终依据。