feat(R-013+重构): 策略自定义字段需求文档 + 结构归类优化

需求与文档:
- R-013 策略自定义字段配置(定义随策略 configSchema 存 settings、值落
  strategy_holdings.values JSON、类型化文本/数字/布尔/枚举)定稿并进入
  PLAN-012 / 迭代 11(含目标数据模型 + 端到端示例 + 技术方案 + 验收标准)
- R-011(Tab设置)/ R-012(UI主题适配)归档至 已完成/,索引标记已归档
- 沉淀约束: 产品约束-010 / 技术约束-015 / 技术约束-016 / UI约束-005

结构重构(技术约束-016):
- src/component/ 平铺还原为语义分域目录: data-source/ storage/ position/
  market/ trades/(git rename 保留历史)
- 清理死代码: 删除 AllocationStorage.js(0 引用)、DataStore
  setDataset/removeDataset(无调用方)
- 同步更新 src/index.js / scripts/*.mjs / tsdown.config.ts 引用
- typecheck + build + test-r009 回归 14/14 通过
This commit is contained in:
2026-09-02 15:38:51 +08:00
parent f1e7e785a1
commit a4902bb730
31 changed files with 509 additions and 198 deletions
@@ -0,0 +1,58 @@
# 计划:策略自定义字段配置(定义随策略,值落库)(阶段航点)
> 编号:PLAN-012 | 粒度:阶段航点 创建:2026-09-02 状态:**执行中**
> 派生自终极目标:目标-002(策略定义能力)、目标-007(人机合一)
> 依据需求:**R-013(已定稿,2026-09-02,老师确认 Q1-Q4 + D6)** —— 符合入范围门槛
> 设计约束:技术约束-012(策略定义仍存 DSH settings / SQLite 存储)沿用;本次新增 产品约束-010、技术约束-015、UI约束-005
## 目标
策略可**自定义、可扩展**:每个策略在设置页「策略分组」子 tab 配置自己的自定义字段定义(configSchema,含类型/枚举/默认值,随策略定义存 settings 不落库);该策略下的每个持仓(strategy_holdings 行)按定义存取一份键值对值(新增 values JSON 列落库);策略持仓 tab 持仓行展开区按定义渲染并编辑值。旧策略无定义的行为与现状完全一致。
## 范围
**做**
1. settings.strategies 扩展 configSchemaschemasteryarray of objecttype union text|number|boolean|enum + enum 选项 + def 默认值),读取归一化(旧项缺省 []);
2. strategy_holdings 新增 values TEXT(JSON) 列(幂等 ALTER,沿用 _ensureTradeAttributionColumns 模式,只读容忍);新增 readValues(holdingId) / writeValues(holdingId, values);现有 openHolding/addShares/reduceShares/closeHolding 保持 values 不随份额操作变动;
3. APIstrategy-positions 每行附 values;新增 holdings/values-update {holdingId, values} 写回(服务端按 configSchema 校验:数字有限数、枚举在选项内、布尔为布尔;允许额外键=可扩展;空值/缺省可写入);
4. 设置页「策略分组」子 tab:策略行可展开 → 展开区字段列表(label/key/type/enum/def+ 添加/编辑/删除字段(字段名/类型/选项/默认值表单)+ 保存走 strategies/update(整表);
5. 策略持仓 tab:持仓行展开区(R-010 基础上)增加「自定义字段」区块:按该策略 configSchema 渲染输入控件(文本=输入框、数字=数字输入、布尔=开关、枚举=下拉),值来自该行 values,编辑即保存调 values-update,保存成功 Toast 反馈;
6. 回归脚本(独立数据目录,技术约束-011):定义字段 → 批量写/改持仓行 values → 校验存储与读取、校验过界值拒绝。
**不做**
- 字段定义落库(定义随策略定义走,D1);
- 策略级(整策略一份)值(值=持仓级,Q1);
- T-006 数据池 / 表格列动态配置(独立需求,起草中);
- T-003 策略配置文件骨架(被本需求吸收,不另做);
- 删除策略时字段定义联动处理之外的数据迁移(兼容:旧策略无定义、旧行 NULL,不迁移,Q4)。
## 涉及文件
```
src/
├── settings.js # strategySchema 扩展 configSchema + 归一化
├── component/SqliteStore.js # _ensureHoldingValuesColumn + readValues/writeValues
├── api/strategies.js # strategy-positions 附加 values + holdings/values-update 端点
├── client/views/SettingsSection.jsx # 「策略分组」子 tab 策略行展开配置字段
└── client/views/StrategyTab.jsx # 持仓行展开区「自定义字段」编辑
scripts/
└── test-r013-custom-fields.mjs # 回归脚本(独立数据目录)
```
## 实现步骤
1. **文档骨架**PLAN-012 + 迭代 11(迭代目标 / 技术实现方案 / 验收标准)+ 约束条目(产品约束-010、技术约束-015、UI约束-005)(本步);
2. **数据层**settings.js configSchema 扩展 + SqliteStore 补列/读写(幂等迁移验证);
3. **API 层**strategy-positions 附加 values + holdings/values-update(含 configSchema 校验);
4. **设置页 UI**:「策略分组」展开配置字段;
5. **策略持仓 UI**:展开区自定义字段编辑(文本/数字/布尔/枚举);
6. **回归脚本 + 验证 + 验收复盘**
## 验收要点
- 设置页「策略分组」:策略行可展开,添加/编辑/删除字段(四种类型 + 枚举选项 + 默认值)后保存,重启后定义仍在;
- 策略持仓 tab:持仓行展开可见「自定义字段」区块,四种类型控件按定义渲染,编辑值保存后刷新仍在(落库);
- 同策略多行各有各的值;不同策略字段集互不影响;
- 旧策略(无 configSchema)持仓行不出现字段区,现有操作(加仓/减仓/清仓/展开交易记录)不受影响;
- 服务端校验生效:数字填非数字、枚举填选项外拒绝并提示;
- typecheck + build 通过;回归脚本全绿。
+2 -1
View File
@@ -13,8 +13,9 @@
| UI约束-002 | 设置页「QMT 连接配置」子 tab:与「Tab 设置 / 策略分组」并列的第三个子 tab;配置以**卡片形式**展示(自适应网格 minmax(260px,1fr),激活卡片绿色描边):卡片含名称 + 激活/默认徽标 + HTTP 地址 + 行内操作(激活 / 设为默认 / 编辑 / 测试连接 / 删除)+ 新增/编辑表单(纵排两字段);删除沿用现有确认弹窗模式,删除最后一条时给出禁止提示;超时不设输入框(Q5);长文本(地址/错误信息)单行省略 + 悬停看全文,组件设 minWidth:0 防撑行 | 2026-08-29 | 生效 | - | 变更(2026-08-29 老师反馈):列表形式改卡片形式,约束组件宽度、长文本省略不换行;原定稿:设置页子 tab 形态沿用现有设置交互体系;变更(2026-09-02 R-011 定稿):「通用设置」更名为「Tab 设置」 |
| UI约束-003 | 设置页「Tab 设置」子 tab:表格列出**全部会话 tab(系统内置 + 策略分组)混排**,内置行带「内置」徽标、策略行带「策略」徽标;每行 = 拖动手柄(原生 HTML5 Drag & Drop+ 名称 + 显示/隐藏开关;**任何行均无重命名/删除按钮**;拖动落点确认后**立即持久化**(每次 drop 提交整表,沿用「刷新页面后生效」提示机制);「策略分组」子 tab 只保留 新增/重命名/删除,移除排序箭头与显隐开关,并提示「顺序与显示请在「Tab 设置」中调整」 | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1/Q4/Q5 确认):拖动排序 + 显隐开关,禁重命名/删除 |
| UI约束-005 | 策略自定义字段配置 UIR-013,2026-09-02):设置页「策略分组」子 tab 策略行**可展开** → 展开区 = 字段列表(label/key/type/enum/默认值)+ 添加/编辑/删除字段表单(名称/类型/枚举选项/默认值)+ 保存(走 strategies/update 整表);策略持仓 tab 持仓行展开区(R-010 基础上)增加「自定义字段」区块:按该策略 configSchema 渲染输入控件(文本=输入框、数字=数字输入、布尔=开关、枚举=下拉),值来自该行 values,编辑即保存调 holdings/values-updateToast 反馈;未配置字段定义的策略持仓行不渲染字段区 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-013 定稿 D6 老师确认:值编辑入口 = 持仓行展开区) |
| UI约束-004 | 神之一手 UI 适配 DSH 主题:所有颜色引用宿主 `--dsw-*` token(带 fallback `var(--dsw-alias-xxx, #原色)`),随 DSH 浅色 / 深色 / 跟随系统自动切换,不自行维护主题偏好(跟随宿主);语义映射:表面底→bg-layer-1、次级面→bg-layer-2、hover→interactive-bg-hover、主/次/弱文字→label-primary/secondary/tertiary、边框→border-l1..l4、红(涨/删/错)→state-error-primary、绿(跌/成/激活)→state-success-primary、蓝(信息/业务)→state-business-primary、实心按钮字→button-contrast-fill、遮罩→bg-mask-1、阴影→shadow-lv3;淡底用 color-mix(in srgb, var(--语义色) 10-12%, transparent);禁止硬编码色值(#hex/rgb)进运行代码 | 2026-09-02 | 生效 | - | R-012 定稿(2026-09-02,暂定跟随系统)+ 迭代 10 实施:141 处硬编码色 token 化,宿主机制实证(body[data-ds-dark-theme] + alias token 双值定义) |
<!-- 示例条目(确认格式后删除):
| UI约束-001 | 示例:求签页面必须保持单屏完整,不出现滚动 | 2026-08-26 | 生效 | - | 讨论确认:移动端优先,避免滚动打断仪式感 |
-->
-->
@@ -18,6 +18,7 @@
| 产品约束-006 | QMT 连接会话头部快捷切换:会话窗口顶栏(PTC 模式标签旁)常驻下拉控件(chip 显示 `QMT: <激活配置名>`),点开列出全部配置(激活项勾选),点选即激活并轻提示反馈;头部控件仅做切换,配置管理(增删改/测试连接/默认标记)仍在设置页子 tab | 2026-08-29 | 生效 | - | R-004 定稿(2026-08-29):Q9/Q10 确认(入口=会话头部 PTC 旁,菜单仅切换激活) |
| 产品约束-008 | 交易记录支持**按策略过滤**:交易记录 tab 提供策略过滤下拉(全部 / 各策略 / 未关联),对今日(QMT 实时)与历史(本地 SQLite)均生效;策略归属 = 委托时间 join 持仓生命周期窗口推导(一码多策略取份额最大,未命中=未关联);历史范围展示本地积累数据(不再「接口开发中」占位) | 2026-09-01 | 生效 | - | 新增(2026-09-01 R-009 定稿 + 迭代 07 实施):策略过滤 + 历史本地展示 |
| 产品约束-010 | 策略自定义字段配置:每个策略可在设置页「策略分组」子 tab 配置自定义字段定义(字段名 / 类型文本·数字·布尔·枚举 / 枚举选项 / 默认值),定义随策略存 settings(不落库);该策略下每个持仓(strategy_holdings 行)按所属策略的定义存取一份字段值(values JSON 列,持仓级键值对,key 对齐定义、允许扩展额外键);旧策略无定义时行为与现状一致(不渲染字段区、不迁移历史值) | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-013 定稿 Q1-Q4 + D6):定义随策略走、值随持仓行,类型化(含枚举),旧策略兼容 |
| 产品约束-009 | 会话 tab 统一由「Tab 设置」管理:设置页「Tab 设置」子 tab 是**所有会话 tab(系统内置 + 策略分组)的唯一顺序与显隐入口**,两类 tab 混排;每行 = 拖动排序 + 显示/隐藏开关;**任何 tab 均不支持重命名与删除**(内置 tab 名称只读,策略命名/删除仍在「策略分组」子 tab);策略改名后 Tab 设置中的名称自动跟随(只存引用);新增策略默认追加到列表末尾;删除策略联动删除 Tab 设置中对应条目;顺序与显隐唯一数据源 = settings.tabs 有序数组 | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1-Q5 确认):Tab 设置 = 显示/隐藏 + 拖动排序(落点立即持久化),全 tab 禁重命名/删除 |
<!-- 示例条目(确认格式后删除):
| 产品约束-001 | 示例:求签功能必须保证抽取结果的不可预测性 | 2026-08-26 | 生效 | - | 讨论确认:为保证公平性,抽取必须不可预测 |
@@ -23,7 +23,9 @@
| 技术约束-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 机制与内置插件注册方式 |
| 技术约束-013 | 交易记录本地存储(R-009):QMT 当日交易数据(委托/成交)由服务端 TradeSync 定时同步落 SQLite(启动预热 + 60s 定时 + UPSERT 幂等,只同步当日);trade_orders(委托主行,order_id 主键 + insert_ts 派生时间列 + **strategy_id/holding_id 手动归属列**+ trade_fills(成交明细,trade_id 主键、order_id 外键)两表;**委托归属由用户在交易记录 tab 手动设置**(候选 = 该 code 当前持仓策略 + 未关联,全手动选、可随时改、以最终为准);**UPSERT 不覆盖归属列**(手动指定为插件逻辑);本地历史查询走 trades/history 端点(策略过滤 = 用户设置的归属);今日实时仍走 QMT Bridge;QMT 委托/成交 code 无后缀、持仓带后缀 —— 数据源映射层统一 normalizeInstrumentCode 归一化;委托交易日 = insertDatetradeDate 兜底) | 2026-09-01 | 生效 | - | 新增(2026-09-01 R-009 定稿 + 迭代 07 实施):两表 + 定时同步 + 本地历史查询;变更 12026-09-01):+code 归一化 + tradeDate 兜底;变更 22026-09-01 老师二次定稿):归属改**手动设置**trade_orders 冗余 strategy_id+holding_idUPSERT 不覆盖归属列),弃算法推导 |
| 技术约束-015 | 策略自定义字段存储(R-013,2026-09-02):字段定义随策略定义存 settings.strategies 扩展 configSchema[{key,label,type,enum?,def}]type ∈ text|number|boolean|enum,旧项缺省空数组);字段值落 strategy_holdings 新增 values TEXTJSON 键值对,key 对齐 configSchema.key,允许额外键=可扩展,NULL=未配置);补列用幂等 ALTER(沿用 _ensureTradeAttributionColumns 模式,只读连接容忍);APIstrategy-positions 每行附 values,新增 holdings/values-update {holdingId, values} 写回,服务端按 configSchema 校验(number=有限数、enum=在选项内、boolean=布尔),空值/缺省可写入;持仓生命周期操作(openHolding/addShares/reduceShares/closeHolding)不碰 values 列 | 2026-09-02 | 生效 | - | 新增(2026-09-02 R-013 定稿 + PLAN-012):定义 settings + 值 SQLite 列 + 幂等补列 + 类型校验 |
| 技术约束-014 | 会话 tab 注册与顺序显隐(R-011):客户端注册统一读 **settings.tabs 有序数组**(唯一顺序与显隐来源,内置条目 refKey + 策略条目 refId),按 order 排序、过滤 visible 后注册(builtin 走内置 render、strategy 走 StrategyTab),移除硬编码 order 间隔(原内置 10/11/12、策略 13+);settings.tabs 从布尔对象升级为有序数组,读取时对旧格式(布尔对象 + 策略自带 order/visible)静默归一化迁移(旧隐藏策略迁移后显示),写入即落库;策略定义表收窄为 {id,name}(去除 visible/order);tabs/update 语义改为整表更新(顺序 + 显隐),strategies/add 联动追加 tab 条目(末尾),strategies/remove 联动删除对应 tab 条目,废弃 strategies/move | 2026-09-02 | 生效 | - | R-011 定稿(2026-09-02 Q1-Q5 确认):统一 tabs 有序数组 + 自动迁移 + 联动增删 |
| 技术约束-016 | src 目录按功能域归类(2026-09-02 结构优化):服务端代码**禁止平铺**,按职责域分目录 —— src/data-source/QmtBridgeRestDataSource + data-source-types + QmtHealthMonitor,数据源与连接健康)、src/storage/SqliteStore + DataStore,存储层)、src/position/PositionManager,分仓逻辑)、src/market/MarketDataHub + MarketFeed,行情)、src/trades/TradeSync,交易同步);api/ 按领域拆分子文件(positions/strategies/qmt-connections/market/trades),client/ 仅放 UIviews/ 组件 + market/ provider);文件命名 = 类名(PascalCase+ .js/.jsx;新增服务端模块必须先落对应域目录,无合适域时先讨论补域,不得回退平铺 | 2026-09-02 | 生效 | - | 新增(2026-09-02 结构审查 + 优化落地):component/ 平铺还原为语义分域,删除死代码 AllocationStorage、DataStore.setDataset/removeDataset |
<!-- 示例条目(确认格式后删除):
| 技术约束-001 | 示例:技术栈以 Node.js / TypeScript 为准,不引入未讨论的新框架 | 2026-08-26 | 生效 | - | 讨论确认:优先复用 DSH 既有能力,新框架需论证 |
-->
@@ -0,0 +1,89 @@
# 技术实现方案:11-策略自定义字段配置
> 迭代编号:11 依据:PLAN-012 + R-013(目标数据模型 / 端到端示例)+ 技术约束-015 / 产品约束-010 / UI约束-005
## 1. 数据层
### 1.1 settings.strategies 扩展 configSchemasrc/settings.js
```js
// strategySchema 中 strategies 项扩展:
strategies: z.array(z.object({
id: z.string().required(),
name: z.string().required(),
configSchema: z.array(z.object({
key: z.string().required(), // 稳健标识(英文 slug
label: z.string(), // UI 展示名(可中文)
type: z.union([z.const('text'), z.const('number'), z.const('boolean'), z.const('enum')]).required(),
enum: z.array(z.string()), // 仅 type=enum 时使用
def: z.any(), // 默认值(text=string / number=number / boolean=boolean / enum=枚举项)
})).default([]), // 缺省 []
})).default(DEFAULT_STRATEGIES),
```
- 读取归一化:旧项无 configSchema → 补默认空数组(读取时或 schema default 保证);
- addStrategy 返回值默认带 configSchema: []renameStrategy / updateStrategies 整表更新天然携带;
- 兼容:settings 旧数据({id,name})经 schemastery default/union 归一化,无迁移脚本。
### 1.2 strategy_holdings 新增 values 列 + 读写(src/component/SqliteStore.js
```js
// init() 内补列(幂等,沿用 _ensureTradeAttributionColumns 模式;只读连接容忍)
_ensureHoldingValuesColumn() {
const cols = new Set(this.db.prepare('PRAGMA table_info(strategy_holdings)').all().map(c => c.name));
if (!cols.has('values')) this.db.exec('ALTER TABLE strategy_holdings ADD COLUMN values TEXT');
}
readValues(holdingId) // SELECT values FROM strategy_holdings WHERE holding_id=? → JSON.parse ?? null
writeValues(holdingId, values) // UPDATE strategy_holdings SET values=? WHERE holding_id=? → JSON.stringify
```
- openHolding/addShares/reduceShares/closeHolding **不碰 values 列**(份额生命周期与字段值独立);
- getCurrentHoldings / getHoldingHistory 的 _mapHolding 附加 values 字段(解析 JSON)。
## 2. API 层(src/api/strategies.js
```js
// 既有:strategy-positions → manager.getStrategyPositions(strategyId)
// PositionManager 返回项附加 valuesfrom SqliteStore 映射)
// 新增端点:
case 'holdings/values-update':
return await manager.updateHoldingValues(args.holdingId, args.values);
```
```js
// PositionManager.updateHoldingValues(holdingId, values)
// 1) 定位 holdings 行(须存在且当前持仓 closed_at IS NULL;否则 404
// 2) 服务端校验(按所属策略 configSchema):
// number → Number.isFinite(Number(v))enum → enum.includes(v)boolean → typeof v === 'boolean'
// 未定义 key 的额外键放行(可扩展);空值/缺省可写入
// 3) 校验失败抛 { code: 'field-validation' },成功 writeValues 并返回 { holdingId, values }
```
## 3. 设置页 UIsrc/client/views/SettingsSection.jsx
- 「策略分组」子 tab 策略行加展开手柄(▶/▸ toggle,样式沿用 expand 惯例);
- 展开区:
- 字段列表表格:展示名 | key | 类型 | 枚举选项 | 默认值 | 操作(删除)—— 空态提示「尚未配置自定义字段」;
- 添加/编辑字段表单:字段名(label)→ 自动生成 key(拼音 slug,复用 generateStrategyId 思路)/ 可手改校验唯一、类型下拉(文本/数字/布尔/枚举)、枚举选项(type=enum 时逗号分隔输入)、默认值(按类型渲染输入);
- 行内编辑/删除:编辑回填表单,删除后保存生效;
- 保存:整表提交 strategies/update(映射为 { ...s, configSchema }),成功 Toast + 刷新(沿用「刷新页面后生效」机制)。
## 4. 策略持仓 tab UIsrc/client/views/StrategyTab.jsx
- 持仓行展开区(R-010 展开逻辑基础上)增加「自定义字段」区块(configSchema 非空才渲染,空则无此区块);
- 块内按 configSchema 渲染控件:text=输入框 / number=数字输入 / boolean=开关(复用 Switch 组件)/ enum=下拉;
- 值来源:strategy-positions 返回的 values;控件初值 = values[key] ?? def
- 编辑即保存:单个字段变更(或区块「保存」按钮)→ 调 one-divine-lot/holdings/values-update;成功 Toast;校验失败(服务端返回)Toast 错误并回滚输入;
- 与交易记录展开区块并存(展开区纵向分区:交易记录 / 自定义字段)。
## 5. 回归脚本(scripts/test-r013-custom-fields.mjs
- ODL_TEST_DATA_DIR 独立数据目录(技术约束-011);
- 用例:addStrategy 带 configSchema(四类型)→ openHolding ×2 同策略不同 code → writeValues 各自值 → 读回校验 → 校验拦截(数字非法 / 枚举越界)→ 旧策略无 configSchema 持仓行 readValues 为 NULL
- 断言存储与读取一致、校验拒绝。
## 6. 验证
- build + typecheck 通过;
- 老师人工验收(见验收标准)。
@@ -0,0 +1,66 @@
# 程序结构设计:11-策略自定义字段配置(含结构归类优化)
> 迭代编号:11 | 2026-09-02 | 依据:技术约束-016(目录归类)、R-013、PLAN-012
## 背景:结构审查发现与优化
2026-09-02 结构审查发现:src/component/ 平铺 10 个服务端模块,偏离迭代 01/02 程序结构设计的语义分域(data-source/、position/、storage/);`component` 命名语义含混(UI 组件实际在 client/views/);归类演进无文档记录;存在死代码。
## 优化动作(2026-09-02 落地)
1. **归类还原**component/ 平铺 → 按功能域分目录:
```
src/
├── index.js # 插件入口(组装各模块)
├── settings.js # 设置管理(策略 + tabs + QMT 连接配置)
├── api/ # 服务端 HTTP APIwebServer /odl/api/*),按领域拆子文件
│ ├── index.js # 路由入口(合并各领域 + 405/404/500
│ ├── common.js # 公共工具
│ ├── positions.js # 持仓域
│ ├── strategies.js # 策略/份额/tabs 域
│ ├── qmt-connections.js # QMT 连接配置域
│ ├── market.js # 行情域
│ └── trades.js # 交易记录域
├── data-source/ # 数据源抽象层(技术约束-001/003)
│ ├── QmtBridgeRestDataSource.js # QMT Bridge REST 适配器
│ ├── data-source-types.js # 统一数据模型(Position/DataSource 接口)
│ └── QmtHealthMonitor.js # QMT 连接健康检查(数据源健康缓存)
├── storage/ # 存储层(R-008 SQLite
│ ├── SqliteStore.js # SQLite 存储封装(node:sqlite
│ └── DataStore.js # 存储门面(对外兼容 API + 迁移)
├── position/ # 分仓业务逻辑
│ └── PositionManager.js # 持仓↔策略份额分配、聚合视图
├── market/ # 行情域
│ ├── MarketDataHub.js # 行情缓存(内存 + 落盘 + 查询)
│ └── MarketFeed.js # 行情获取(WS + REST 轮询 + prime
├── trades/ # 交易域
│ └── TradeSync.js # 交易记录定时同步(QMT → SQLite)
└── client/ # 客户端(UI)
├── index.js # 客户端入口:tab/settings 注册
├── market/ # MarketDataProvider.jsx(行情 context
└── views/ # React 组件(StrategyTab / SettingsSection / Toast 等)
```
2. **死代码清理**:删除 src/component/AllocationStorage.js(旧 allocations.json 存储,0 引用);删除 DataStore.setDataset/removeDatasetPositionManager 适配单票生命周期后无调用方);
3. **构建与脚本同步**tsdown.config.ts entry 更新为新目录 globscripts/*.mjs 导入路径更新;
4. **验证**typecheck + build 通过;test-r009 回归 14/14 通过(独立数据目录)。
## 归类规则(沉淀为技术约束-016)
- 服务端代码禁止平铺,按功能域分目录:data-source / storage / position / market / trades
- api/ 按领域拆子文件;client/ 仅放 UIviews/ 组件 + market/ provider);
- 文件命名 = 类名 PascalCase + .js/.jsx
- 新增服务端模块必须先落对应域目录,无合适域时先讨论补域,不得回退平铺。
## R-013 新增改动落点(本迭代实施)
```
src/settings.js # +configSchema(策略 schema 扩展)
src/storage/SqliteStore.js # +_ensureHoldingValuesColumn + readValues/writeValues
src/position/PositionManager.js # +updateHoldingValues(按 configSchema 校验)
src/api/strategies.js # +holdings/values-update 端点;strategy-positions 附加 values
src/client/views/SettingsSection.jsx # 「策略分组」策略行展开配置字段
src/client/views/StrategyTab.jsx # 持仓行展开区「自定义字段」编辑
scripts/test-r013-custom-fields.mjs # 回归脚本(独立数据目录)
```
@@ -0,0 +1,21 @@
# 迭代目标:11-策略自定义字段配置(定义随策略,值落库)
> 迭代编号:11 | 创建:2026-09-02 状态:进行中
> 依据计划:PLAN-012 需求:R-013(已定稿,2026-09-02,老师确认 Q1-Q4 + D6
## 目标描述
给策略增加**可自定义、可扩展**的字段能力:每个策略在设置页「策略分组」子 tab 配置自定义字段定义(configSchemakey/label/type/enum/默认值,随策略定义存 settings 不落库);该策略下每个持仓(strategy_holdings 行)按所属策略的定义存取一份键值对值(新增 values JSON 列落库);策略持仓 tab 持仓行展开区按定义渲染并编辑值。旧策略(无定义)行为与现状完全一致。
## 目标分解
1. 数据层:settings.strategies 扩展 configSchemaschemasterytype union text|number|boolean|enum),读取归一化(旧项缺省 []);strategy_holdings 新增 values TEXT 列(幂等 ALTER+ readValues/writeValues
2. API 层:strategy-positions 每行附 values;新增 holdings/values-update {holdingId, values} 写回(服务端按 configSchema 校验:数字/枚举/布尔);
3. 设置页 UI:「策略分组」子 tab 策略行可展开 → 字段列表 + 添加/编辑/删除字段表单 + 保存;
4. 策略持仓 UI:持仓行展开区(R-010 基础上)增加「自定义字段」区块,按定义渲染四种类型控件,编辑即保存;
5. 回归脚本(独立数据目录,技术约束-011)+ 验证 + 验收复核。
## 对老师的配合需求
- 验收:设置页配置字段定义(四种类型各一 + 枚举)→ 策略持仓 tab 持仓行展开编辑值 → 重启验证持久化;
- 提供:是否接受「旧策略无定义」行为样(默认接受,Q4 已确认)。
@@ -0,0 +1,22 @@
# 验收标准:11-策略自定义字段配置
> 迭代编号:11 | 依据:PLAN-012 验收要点 + R-013
## 验收标准线
1. 设置页「策略分组」:策略行可展开,添加 / 编辑 / 删除字段(四种类型:文本/数字/布尔/枚举,含枚举选项与默认值)保存后生效,重启后定义仍在;
2. 策略持仓 tab:持仓行展开区出现「自定义字段」区块(仅该策略配置了字段时),四种类型控件按定义渲染(文本=输入框、数字=数字输入、布尔=开关、枚举=下拉),编辑值保存后刷新仍在(已落库);
3. 同策略多行各有各的值(600719 与 300057 互不影响);不同策略字段集互不影响;
4. 旧策略(无 configSchema):持仓行不渲染字段区,现有操作(加仓/减仓/清仓/展开交易记录)与现状完全一致;
5. 服务端校验生效:数字填非数字、枚举填选项外 → 拒绝保存并 Toast 提示;空值/缺省允许;
6. 兼容迁移:既有 strategy_holdings 数据行(无 values)补列后为 NULL,读取正常,不报错;
7. build + typecheck 通过;回归脚本(test-r013-custom-fields.mjs,独立数据目录)全绿。
## 验收方法
- 回归脚本跑通(隔离数据目录);
- 老师人工验收:设置页配置网格超市 4 字段 → 策略持仓 tab 两个持仓行展开分别编辑值 → 另配长线持有 2 字段观察互不影响 → 手动做T(旧策略)确认无字段区 → 重启后确认定义与值均保留。
## 验收目标
- 7 条验收线通过,迭代 11 标记「验收通过」,R-013 更新实现状态(已实现),归档。
+194
View File
@@ -0,0 +1,194 @@
# R-013 策略自定义字段配置(定义随策略,值落库)
> 状态:**已定稿**2026-09-02,老师确认 Q1-Q4)| 登记日期:2026-09-02
> 来源:老师提出(2026-09-02,策略可自定义 / 可扩展讨论;T-009 转正)
> 优先级:P1(策略管理能力扩展,无数据风险)
> 关联:T-003(策略配置文件骨架,起草 —— 本需求吸收其「策略数据结构」部分)、T-006(数据池 + 策略表格动态字段配置,起草 —— 边界:本需求=策略自定义字段,T-006=表格列配置)
## 需求描述
策略要可**自定义、可扩展**:每个策略可定义自己需要的一组**自定义字段**(带类型:文本 / 数字 / 布尔 / 枚举),该策略下的每个持仓(strategy_holdings 行)按策略定义存一份**键值对值**(JSON,可任意扩展)。字段**定义随策略走**(不落库),**值落库**strategy_holdings 新增 values JSON 列)。
## 现状(代码审查 2026-09-02
| 项 | 现状 |
|---|---|
| 策略定义 | settings.strategies = [{ id, name }]R-011 收窄),存 DSH settings |
| 持仓存储 | strategy_holdingsholding_id / strategy_id / code / shares / created_at / closed_at),无自定义字段列 |
| 定义位置 | 设置页「策略分组」子 tab:策略行内 CRUD(新增 / 重命名 / 删除),无字段配置能力 |
| 持仓渲染 | 策略持仓 tab 固定列(代码/名称/份额/现价/盈亏/操作),R-010 已支持持仓行展开看交易记录 |
## 讨论结论(2026-09-02 老师确认 Q1-Q4
- **D1**(第二轮)字段**定义不存数据库**:随策略定义走,扩展 settings.strategiesconfigSchema 数组);数据库只存值。
- **D2**(第三轮 + Q1)值 = **持仓级 JSON 键值对**strategy_holdings 新增一列 values TEXT(JSON);每行(一个策略 × 一只股票)一套值;某持仓需要哪些字段 = 看所属策略的 configSchema。
- **D3**Q2/Q3)字段**类型化**:文本 / 数字 / 布尔 / **枚举**(预设选项下拉,有用,采纳)。定义含:key / label / type / enum 选项 / 默认值。
- **D4**(第三轮)配置入口 = **设置页「策略分组」子 tab**:策略行做成**可展开**,展开区添加 / 配置字段(名称 + 类型 + 枚举选项 + 默认值)。
- **D5**Q4)兼容:旧策略无 configSchema(缺省空数组)→ 持仓行不展示自定义字段区,行为与现在完全一致;旧行 values 列缺省 NULL(空);无迁移历史值。
- **D6**2026-09-02 老师确认)值编辑入口 = **策略持仓 tab 持仓行展开区**(沿 R-010 展开能力),展开后按 configSchema 渲染各字段输入控件,编辑即调 API 写回该行 values。
## 目标数据模型(定稿)
```js
// settings.strategies(扩展 configSchema;不落库)
strategies: [{
id: 'grid-supermarket',
name: '网格超市',
configSchema: [
{ key: 'gridGap', label: '网格间距', type: 'text', def: '3%' },
{ key: 'gridAmount', label: '单格金额', type: 'number', def: 10000 },
{ key: 'hasStop', label: '设置止损', type: 'boolean', def: false },
{ key: 'riskLevel', label: '风险等级', type: 'enum', enum: ['低','中','高'], def: '中' },
],
}]
```
```sql
-- strategy_holdings 新增列(幂等 ALTER,沿用 _ensureTradeAttributionColumns 模式)
ALTER TABLE strategy_holdings ADD COLUMN values TEXT; -- JSON 键值对
```
```js
// 持仓行 values 示例(key 与 configSchema.key 对齐;未定义的额外键允许存在=可扩展)
{ "gridGap": "3%", "gridAmount": 10000, "hasStop": true, "riskLevel": "中" }
```
**语义要点**
- key 为稳健标识(英文 slug),label 为 UI 展示名(可中文);值 JSON 以 key 存;
- 枚举字段值校验 ∈ enum 选项;数字字段 UI 数字输入;布尔渲染开关;文本自由输入——校验在服务端写回时执行;
- 定义变更对已有值数据无破坏(旧值保留,新增字段缺省用 def,未配置字段不渲染)。
## 端到端示例(Sample,供后续推进参考)
> 场景:现有三个策略 —— 网格超市(已配自定义字段)、手动做T(未配,旧策略)、长线持有(新增策略,配另一组字段)。沿用项目现有个股:601117.SH(中国交建)、300057.SZ(万顺新材)、600719.SH(大连热电)。
### 1. 策略定义(settings.strategies,不落库)
```js
// 完整值(新增/编辑后持久化于 DSH settings
strategies: [
{
id: 'grid-supermarket',
name: '网格超市',
configSchema: [
{ key: 'gridGap', label: '网格间距', type: 'text', def: '3%' },
{ key: 'gridAmount', label: '单格金额', type: 'number', def: 10000 },
{ key: 'hasStop', label: '设置止损', type: 'boolean', def: false },
{ key: 'riskLevel', label: '风险等级', type: 'enum', enum: ['低','中','高'], def: '中' },
],
},
{ id: 'manual-t', name: '手动做T' }, // 旧策略:无 configSchema(兼容,缺省 []
{
id: 'long-term-hold',
name: '长线持有',
configSchema: [
{ key: 'targetPrice', label: '目标价', type: 'number', def: null },
{ key: 'notes', label: '持仓备注', type: 'text', def: '' },
],
},
]
```
### 2. strategy_holdings 表数据(含 values 列)
```sql
-- ALTER 幂等补列后,既有行 values 自动为 NULL(不迁移)
ALTER TABLE strategy_holdings ADD COLUMN values TEXT;
SELECT holding_id, strategy_id, code, shares, values FROM strategy_holdings WHERE closed_at IS NULL;
-- 结果示例:
-- 1 | grid-supermarket | 601117.SH | 600 | {"gridGap":"3%","gridAmount":10000,"hasStop":true,"riskLevel":"高"}
-- 2 | grid-supermarket | 300057.SZ | 1000 | {"gridGap":"5%","gridAmount":8000,"hasStop":false,"riskLevel":"低"}
-- 8 | manual-t | 600719.SH | 1000 | NULL(手动做T 未配字段 → 无值)
-- 9 | long-term-hold | 601117.SH | 200 | {"targetPrice":7.5,"notes":"周线突破再加仓"}
```
> 同策略多行各存各的值(600719 与 300057 的网格间距不同);不同策略可有完全不同的字段(long-term-hold 无 gridGap)。
### 3. API 交互示例(/odl/api 端点,RPC 通道)
```js
// ① 读策略(含 configSchema
call('one-divine-lot/strategies', {})
// 响应 value: 见 §1 完整数组(策略行自带 configSchema
// ② 配字段:整表更新 strategies(改 configSchema 随整表提交)
call('one-divine-lot/strategies/update', { strategies: [ /* §1 完整数组 */ ] })
// ③ 读某策略持仓(每行附 values)
call('one-divine-lot/strategy-positions', { args: { strategyId: 'grid-supermarket' } })
// 响应 value: [
// { holdingId: 1, code: '601117.SH', shares: 600, /* 既有列 */ values: { gridGap: '3%', gridAmount: 10000, hasStop: true, riskLevel: '高' } },
// { holdingId: 2, code: '300057.SZ', shares: 1000, /* ... */ values: { gridGap: '5%', gridAmount: 8000, hasStop: false, riskLevel: '低' } },
// ]
// ④ 写某持仓行的值(无 values → 传全量;已有 → 合并更新)
call('one-divine-lot/holdings/values-update', { args: { holdingId: 1, values: { gridGap: '4%', gridAmount: 10000, hasStop: true, riskLevel: '高' } } })
// 响应 value: { holdingId: 1, values: { gridGap: '4%', gridAmount: 10000, hasStop: true, riskLevel: '高' } }
```
### 4. UI 形态
**设置页 →「策略分组」子 tab(配置字段定义)**
```
[▸] 网格超市 [重命名] [删除]
自定义字段:
展示名 | key | 类型 | 选项(枚举) | 默认值 | 操作
网格间距 | gridGap | 文本 | - | 3% | [删除]
单格金额 | gridAmount | 数字 | - | 10000 | [删除]
设置止损 | hasStop | 布尔 | - | 关 | [删除]
风险等级 | riskLevel | 枚举 | 低/中/高 | 中 | [删除]
[+ 添加字段](名称/类型/选项/默认值 一行表单) [保存]
[▸] 手动做T ...(无字段区,保持现状)
[▸] 长线持有 ...(目标价 / 持仓备注)
```
**策略持仓 tab → 持仓行展开区(编辑值,R-010 展开能力之上)**
```
601117.SH 中国交建 600股 现价 7.20 +0.56% [展开]
└ 交易记录(R-010 已有)
└ 自定义字段:
网格间距 [ 4% ]
单格金额 [ 10000 ]
设置止损 [ ✓ ]
风险等级 [ 高 ▼ ]
[保存]
```
### 5. 渲染 / 校验逻辑参考
```js
// 渲染(策略持仓 tab):configSchema 决定字段集合,values 提供值,缺省回退 def
const schema = strategy.configSchema ?? []; // 无定义 → 不渲染字段区
const merged = schema.map((f) => ({ ...f, value: (row.values ?? {})[f.key] ?? f.def }));
// 校验(服务端 holdings/values-update 写回时)
function validateField(f, v) {
if (v === undefined || v === null || v === '') return null; // 允许空
if (f.type === 'number' && !Number.isFinite(Number(v))) return f.label + ' 需为数字';
if (f.type === 'enum' && !f.enum.includes(v)) return f.label + ' 需在选项内: ' + f.enum.join('/');
if (f.type === 'boolean' && typeof v !== 'boolean') return f.label + ' 需为布尔';
return null;
}
```
## 涉及改动面(技术方案,定稿)
1. **src/settings.js**strategySchema 的 strategies 项扩展 configSchemaschemastery array of objecttype union text|number|boolean|enum);读取归一化(旧项缺省 []);strategies/update 沿用整表更新语义。
2. **src/component/SqliteStore.js**init() 增加 _ensureHoldingValuesColumnPRAGMA table_info → ALTER,幂等,只读容忍同 trade_orders 先例);openHolding / addShares / reduceShares / closeHolding 保持既有列行为(values 不随份额操作变动);新增 readValues(holdingId) / writeValues(holdingId, values)。
3. **src/api/strategies.js**strategy-positions 返回项附加 values;新增端点 holdings/values-update {holdingId, values} 写回;strategies/update 已可携带 configSchema(整表更新天然支持)。
4. **src/client/views/SettingsSection.jsx**:「策略分组」子 tab 策略行加展开手柄:展开区 = 字段列表(label/key/type/enum/def+ 添加/删除/编辑字段表单;保存走 strategies/update。
5. **src/client/views/StrategyTab.jsx**:持仓行展开区(R-010 基础)增加「自定义字段」区块:按该策略 configSchema 渲染输入控件(文本/数字/布尔/枚举),值来自该行 values,编辑保存调 values-update;行内表格可选加字段列。
6. 回归脚本:设置字段定义 → 批量写/改持仓行 values → 校验存储与读取(独立数据目录,技术约束-011)。
## 定稿记录(Q1-Q42026-09-02 老师确认)
- **Q1** 字段定义 = 策略级(某策略下每笔持仓需要的字段由该策略定义决定);值 = 持仓级(每行一套键值对)→ 确认。
- **Q2** 字段加类型(文本 / 数字 / 布尔 / 枚举)→ 确认。
- **Q3** 支持枚举字段(预设选项下拉)→ 确认(有用)。
- **Q4** 旧策略无自定义字段:沿用现有字段定义(不迁移、不补默认),值列缺省空 → 确认。
- **D6(值编辑入口)**:持仓行展开区编辑 —— 老师确认(2026-09-02)。
> 三要素满足(边界清楚 / 核心逻辑明确 / 老师确认 Q1-Q4),**已定稿**,可进入计划范围。
@@ -1,4 +1,13 @@
# R-011 Tab 设置:统一管理所有 tab(内置 + 策略分组混排,显示/隐藏 + 拖动排序)
# R-011 Tab 设置:统一管理所有 tab(内置 + 策略分组混排,显示/隐藏 + 拖动排序) · 已完成
> 归档日期:2026-09-02 需求状态:**已完成**
> 原索引:docs/05-需求池/需求池索引.md(主索引保留 R-011 条目,指向本归档)
> 实现迭代:09-Tab设置统一管理(验收通过,迭代复盘见 docs/04-迭代记录/09-Tab设置统一管理/迭代复盘.md)
> 讨论记录:2026-09-02 定稿(Q1-Q5 老师确认);实现见迭代 09 技术实现方案/复盘
---
> 状态:**已定稿**2026-09-02,老师确认 Q1-Q5)| 登记日期:2026-09-02
> 来源:老师指令(2026-09-02,设置页 tab 化扩展的二次演进)
@@ -1,4 +1,13 @@
# R-012 UI 适配 DSH 主题(浅色 / 深色 / 跟随系统)
# R-012 UI 适配 DSH 主题(浅色 / 深色 / 跟随系统) · 已完成
> 归档日期:2026-09-02 需求状态:**已完成**
> 原索引:docs/05-需求池/需求池索引.md(主索引保留 R-012 条目,指向本归档)
> 实现迭代:10-UI主题适配(验收通过,迭代复盘见 docs/04-迭代记录/10-UI主题适配/迭代复盘.md)
> 讨论记录:2026-09-02 定稿(暂定跟随系统,老师确认);实现见迭代 10 技术实现方案/复盘
---
> 状态:**已定稿(暂定:跟随系统,语义色映射可再调)**(2026-09-02,老师确认方向「暂定跟随系统」)| 登记日期:2026-09-02
> 来源:老师指令(2026-09-02
+5 -3
View File
@@ -22,8 +22,9 @@
| R-008 | 数据存储管理(JSON → SQLite | 数据存储从 JSON data store 升级为 SQLitenode: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-01T-008 转正) | P1 | 已定稿 | 2026-09-01 | 06-数据存储SQLite | **已实现(已归档)** |
| R-009 | 交易记录本地存储(SQLite+ 策略关联 | 在 SQLite 新增交易记录表(trade_orders + trade_fills,委托/成交两表),QMT 当日交易数据本地持久化(跨日积累成历史库);**归属由用户在交易记录 tab 手动设置(trade_orders 冗余存 strategy_id + holding_idUPSERT 不覆盖归属列,可随时改)**;本地历史查询端点(trades/history 策略过滤)+ 交易记录 tab 策略过滤与历史范围本地展示;QMT code 归一化 + tradeDate 兜底。**2026-09-01 定稿(Q1-Q8 + 二次定稿 Q1-Q4),2026-09-01 完成(迭代 07 验收通过),已归档至 已完成/R-009.md** | 老师指令(2026-09-01 | P1 | 已定稿 | 2026-09-01 | 07-交易记录本地存储SQLite与策略关联 | **已实现(已归档)** |
| R-010 | 策略持仓行展开关联交易记录(Holding → 交易汇总) | 策略持仓 tab 每个持仓行(Holding)可展开,展开显示与该 holding 关联的交易记录(仅委托汇总,不分笔成交);strategy-positions 附加 holding_id + 新增 trades/by-holding 端点 + 持仓行展开 UI + 成本价/最后一笔成交价两列 + 神之一手 tab 隐藏输入框。**2026-09-02 定稿(Q1-Q4 确认),2026-09-02 完成(老师确认),已归档至 已完成/R-010.md** | 老师指令(2026-09-02 | P1 | 已定稿 | 2026-09-02 | 08-策略持仓行展开关联交易记录 | **已实现(已归档)** |
| R-011 | Tab 设置:统一管理所有 tab(内置 + 策略分组混排,显示/隐藏 + 拖动排序) | 设置页「通用设置」升级为「Tab 设置」,统一管理所有会话 tab(系统内置 + 策略分组)的唯一入口:两类混排、**拖动排序**(原生 HTML5 DnD,落点立即持久化)+ **显隐开关**;**任何 tab 均不支持重命名/删除**(策略命名/删除仍在「策略分组」子 tab);顺序/显隐统一为一份数据源(tabs 有序数组,策略行名 join strategies 自动跟随改名),删除策略联动删除对应条目;新增策略追加末尾;老配置自动迁移(旧隐藏策略迁移后显示)。**2026-09-02 定稿(Q1-Q5 确认)并完成(迭代 09 验收通过)**,详见 docs/05-需求池/R-011.md | 老师指令(2026-09-02 | P1 | 已定稿 | 2026-09-02 | 09-Tab设置统一管理 | **已实现(迭代 09 完结** |
| R-012 | UI 适配 DSH 主题(浅色 / 深色 / 跟随系统) | 神之一手 UI 适配 DSH 浅色/深色/跟随系统主题:141 处硬编码色值替换为宿主 `--dsw-*` token,随主题自动切换;不自行维护主题偏好(**暂定跟随系统**);涨跌红涨绿跌 → 宿主 state-error/success;仅色值 token 化不动布局。**2026-09-02 定稿(暂定跟随系统)并完成(迭代 10)**,详见 docs/05-需求池/R-012.md | 老师指令(2026-09-02 | P1 | 已定稿 | 2026-09-02 | 10-UI主题适配 | **已实现(迭代 10 完结** |
| R-011 | Tab 设置:统一管理所有 tab(内置 + 策略分组混排,显示/隐藏 + 拖动排序) | 设置页「通用设置」升级为「Tab 设置」,统一管理所有会话 tab(系统内置 + 策略分组)的唯一入口:两类混排、**拖动排序**(原生 HTML5 DnD,落点立即持久化)+ **显隐开关**;**任何 tab 均不支持重命名/删除**(策略命名/删除仍在「策略分组」子 tab);顺序/显隐统一为一份数据源(tabs 有序数组,策略行名 join strategies 自动跟随改名),删除策略联动删除对应条目;新增策略追加末尾;老配置自动迁移(旧隐藏策略迁移后显示)。**2026-09-02 定稿(Q1-Q5 确认)并完成(迭代 09 验收通过),已归档至 已完成/R-011.md** | 老师指令(2026-09-02 | P1 | 已定稿 | 2026-09-02 | 09-Tab设置统一管理 | **已实现(已归档** |
| R-012 | UI 适配 DSH 主题(浅色 / 深色 / 跟随系统) | 神之一手 UI 适配 DSH 浅色/深色/跟随系统主题:141 处硬编码色值替换为宿主 `--dsw-*` token,随主题自动切换;不自行维护主题偏好(**暂定跟随系统**);涨跌红涨绿跌 → 宿主 state-error/success;仅色值 token 化不动布局。**2026-09-02 定稿(暂定跟随系统)并完成(迭代 10),已归档至 已完成/R-012.md** | 老师指令(2026-09-02 | P1 | 已定稿 | 2026-09-02 | 10-UI主题适配 | **已实现(已归档** |
| R-013 | 策略自定义字段配置(定义随策略,值落库) | 策略可自定义、可扩展:每个策略在设置页「策略分组」子 tab 配置自定义字段(字段名/类型文本·数字·布尔·枚举/默认值,configSchema 随策略定义存 settings);每个持仓行按所属策略的定义存一份键值对值(strategy_holdings 新增 values TEXT(JSON) 列);策略持仓 tab 持仓行展开区按定义渲染/编辑值。**2026-09-02 定稿(Q1-Q4 + D6 确认)并进入计划 PLAN-012(迭代 11 实施中)**,详见 docs/05-需求池/R-013.md | 老师指令(2026-09-02 | P1 | 已定稿 | 2026-09-02 | 11-策略自定义字段配置 | 实施中(迭代 11) |
## 渐进明细规划素材
@@ -38,4 +39,5 @@
| T-005 | 通过 ws 长连接做市场数据的实时反映 | WebSocket 长连接实现市场数据(持仓/行情)实时推送与展示;源自迭代 01 复盘。**2026-08-31 转正为 R-005(迭代 04 实施中)**,草稿文件:`草稿/通过ws长连接做市场数据的实时反映.md` | 已转需求 R-005 |
| T-006 | 数据池中间层 + 策略表格动态字段配置 | 服务端数据池(数据集中间层:按 code 聚合宽表、多源字段映射、填充器架构)+ 策略持仓表格动态字段配置(列配置×池行渲染)。源自 R003 讨论(2026-08-28)拆分独立,草稿文件:`草稿/数据池中间层与策略表格动态字段配置.md` | 起草 |
| T-007 | RSS 订阅管理 | 管理 RSS 订阅源(增删改查)+ 抓取聚合订阅内容,作为资讯/消息来源;支撑 目标-003 市场监控、目标-004 消息管理,与 T-004 同属消息链路。**2026-08-28 老师确认先放草稿**,草稿文件:`草稿/RSS订阅管理.md` | 起草 |
| T-008 | 数据存储管理(JSON → SQLite | 数据存储从 JSON data store 升级为 SQLitenode:sqlite)数据库 + 数据存储管理能力;**变更既有设计约束**(数据存储设计.md「无需数据库」决策)。**2026-09-01 定稿转正为 R-008 并完成(迭代 06** | 已转需求 R-008 |
| T-008 | 数据存储管理(JSON → SQLite | 数据存储从 JSON data store 升级为 SQLitenode:sqlite)数据库 + 数据存储管理能力;**变更既有设计约束**(数据存储设计.md「无需数据库」决策)。**2026-09-01 定稿转正为 R-008 并完成(迭代 06** | 已转需求 R-008 |
| T-009 | 策略自定义字段配置(策略细节配置) | 每个策略支持自定义字段(键值对),不同策略按自身需要添加各自的细节配置字段(如网格超市配网格间距/单格金额、长线持有配目标价/仓位上限)。**2026-09-02 定稿转正为 R-013**,草稿文件已清理 | 已转需求 R-013 |
+2 -2
View File
@@ -17,7 +17,7 @@
* 5. 迁移前备份 JSON 为 *.bak;迁移失败不破坏原文件。
*/
import { SqliteStore } from '../lib/component/SqliteStore.js';
import { SqliteStore } from '../lib/storage/SqliteStore.js';
async function main() {
const store = new SqliteStore({}); // dataDir 默认 ~/.dsh/one-divine-lot,或 ODL_TEST_DATA_DIR
@@ -41,4 +41,4 @@ async function main() {
}
}
main();
main();
+1 -1
View File
@@ -5,7 +5,7 @@
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DataStore } from '../src/component/DataStore.js';
import { DataStore } from '../src/storage/DataStore.js';
import { handleTrade, TRADE_METHODS } from '../src/api/trades.js';
const dir = mkdtempSync(join(tmpdir(), 'odl-api-test-'));
+2 -2
View File
@@ -1,4 +1,4 @@
import { QmtBridgeRestDataSource } from '../src/component/QmtBridgeRestDataSource.js';
import { QmtBridgeRestDataSource } from '../src/data-source/QmtBridgeRestDataSource.js';
const ds = new QmtBridgeRestDataSource({ baseUrl: 'http://x' });
let pass = 0, fail = 0;
@@ -38,4 +38,4 @@ assert(t.code === '001330.SZ', 'mapTrade code 归一化: ' + t.code);
assert(t.tradeDate === '20260901', 'mapTrade tradeDate 保留: ' + t.tradeDate);
console.log('\n结果: ' + pass + ' 通过, ' + fail + ' 失败');
process.exit(fail > 0 ? 1 : 0);
process.exit(fail > 0 ? 1 : 0);
+3 -3
View File
@@ -5,8 +5,8 @@
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { DataStore } from '../src/component/DataStore.js';
import { TradeSync } from '../src/component/TradeSync.js';
import { DataStore } from '../src/storage/DataStore.js';
import { TradeSync } from '../src/trades/TradeSync.js';
const dir = mkdtempSync(join(tmpdir(), 'odl-test-r009-sync-v2-'));
process.env.ODL_TEST_DATA_DIR = dir;
@@ -83,4 +83,4 @@ assert(fillsManual.length === 1 && fillsManual[0].tradeId === 'T-S1', '成交按
storage.close();
rmSync(dir, { recursive: true, force: true });
console.log('\n结果: ' + pass + ' 通过, ' + fail + ' 失败');
process.exit(fail > 0 ? 1 : 0);
process.exit(fail > 0 ? 1 : 0);
+2 -2
View File
@@ -12,7 +12,7 @@
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { SqliteStore } from '../src/component/SqliteStore.js';
import { SqliteStore } from '../src/storage/SqliteStore.js';
const dir = mkdtempSync(join(tmpdir(), 'odl-test-r009-v2-'));
process.env.ODL_TEST_DATA_DIR = dir;
@@ -98,4 +98,4 @@ assert(all.length === 2 && all.find(o => o.orderId === 'O-001').strategyId === '
store.close();
rmSync(dir, { recursive: true, force: true });
console.log('\n结果: ' + pass + ' 通过, ' + fail + ' 失败');
process.exit(fail > 0 ? 1 : 0);
process.exit(fail > 0 ? 1 : 0);
+2 -2
View File
@@ -2,7 +2,7 @@
* 真实库归属闭环验证(写操作测试):设置归属 → 过滤 → UPSERT 保留 → 还原
* 注意:插件进程持锁时外部写会失败;若失败说明插件在运行(正常),跳过写操作
*/
import { DataStore } from '../src/component/DataStore.js';
import { DataStore } from '../src/storage/DataStore.js';
const storage = new DataStore({ dataDir: process.env.HOME + '/.dsh/one-divine-lot' });
await storage._ensure();
@@ -45,4 +45,4 @@ try {
storage.close();
console.log('\n结果: ' + pass + ' 通过, ' + fail + ' 失败');
process.exit(fail > 0 ? 1 : 0);
process.exit(fail > 0 ? 1 : 0);
+1 -1
View File
@@ -3,7 +3,7 @@
* 手动归属模型:验证归属列(若已迁移)、历史查询、候选列表
* 注:归属列 ALTER 迁移由插件重启后自动执行(当前运行中的旧构建未含迁移逻辑)
*/
import { DataStore } from '../src/component/DataStore.js';
import { DataStore } from '../src/storage/DataStore.js';
const storage = new DataStore({ dataDir: process.env.HOME + '/.dsh/one-divine-lot' });
await storage._ensure();
-139
View File
@@ -1,139 +0,0 @@
/**
* 本地存储:分仓分配数据(allocations.json
*
* 设计约束:产品约束-003(分仓数据本地保存并更新管理)
* QMT 只提供全量真实持仓,策略分仓分配是本地维护的元数据。
*
* 文件位置:<dataDir>/allocations.json
* 结构:
* {
* "version": 1,
* "allocations": {
* "600719.SH": { "grid-supermarket": 1000, "manual-t": 800 }
* }
* }
*/
import { promises as fs } from 'node:fs';
import { dirname, join } from 'node:path';
import { homedir } from 'node:os';
const DEFAULT_DATA_DIR = join(homedir(), '.dsh', 'one-divine-lot');
const FILE_NAME = 'allocations.json';
/**
* 测试模式数据目录(防误删真实数据,2026-08-31 老师定):
* - 环境变量 ODL_TEST_DATA_DIR 设置时,存储落到独立测试目录,不影响真实 allocations.json
* - 测试脚本必须显式注入 dataDir 或设置该环境变量,禁止在真实数据上执行写操作。
*/
const TEST_DATA_DIR = process.env.ODL_TEST_DATA_DIR || '';
export class AllocationStorage {
/**
* @param {object} [options]
* @param {string} [options.dataDir] 数据目录(默认 ~/.dsh/one-divine-lot
*/
constructor({ dataDir = TEST_DATA_DIR || DEFAULT_DATA_DIR } = {}) {
this.dataDir = dataDir;
this.filePath = join(dataDir, FILE_NAME);
this.data = { version: 1, allocations: {} };
this.loaded = false;
}
/** 加载数据(首次调用时从磁盘读取) */
async load() {
if (this.loaded) return this.data;
try {
const raw = await fs.readFile(this.filePath, 'utf8');
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
this.data = {
version: parsed.version ?? 1,
allocations: parsed.allocations ?? {},
};
}
} catch (err) {
if (err.code !== 'ENOENT') {
throw new Error(`分仓数据读取失败: ${err.message}`);
}
// 文件不存在:首次使用,用默认空数据
}
this.loaded = true;
return this.data;
}
/** 保存数据到磁盘(原子写:临时文件 + rename) */
async save() {
await fs.mkdir(this.dataDir, { recursive: true });
const tmpPath = `${this.filePath}.${process.pid}.tmp`;
await fs.writeFile(tmpPath, JSON.stringify(this.data, null, 2), 'utf8');
await fs.rename(tmpPath, this.filePath);
}
/** 读取某只股票的分配份额 { strategyId: shares } */
async get(code) {
await this.load();
return { ...(this.data.allocations[code] ?? {}) };
}
/** 获取全部分仓分配 */
async getAll() {
await this.load();
return { ...this.data.allocations };
}
/**
* 设置某只股票的份额分配
* @param {string} code 证券代码
* @param {Object<string, number>} shares 策略份额映射,如 { 'grid-supermarket': 1000 }
*/
async set(code, shares) {
await this.load();
// 过滤掉 0/无效值
const cleaned = {};
for (const [sid, n] of Object.entries(shares ?? {})) {
const num = Number(n);
if (Number.isFinite(num) && num > 0) cleaned[sid] = num;
}
if (Object.keys(cleaned).length > 0) {
this.data.allocations[code] = cleaned;
} else {
delete this.data.allocations[code];
}
await this.save();
return this.get(code);
}
/** 删除某只股票的分配 */
async remove(code) {
await this.load();
delete this.data.allocations[code];
await this.save();
}
/**
* 清空某策略在所有股票下的份额(迭代 02:O3 删除策略联动;O6 一键清零按钮已移除)
* 删除该 strategyId 的所有分配,返回受影响(原本有份额)的股票代码列表
* @param {string} strategyId 策略 id
* @returns {Promise<string[]>} 受影响标的代码列表
*/
async removeStrategyShares(strategyId) {
await this.load();
const affected = [];
for (const [code, shares] of Object.entries(this.data.allocations)) {
if (shares && shares[strategyId] !== undefined) {
affected.push(code);
delete shares[strategyId];
// 该票已无任何分配 → 移除整条记录
if (Object.keys(shares).length === 0) {
delete this.data.allocations[code];
}
}
}
if (affected.length > 0) {
await this.save();
}
return affected;
}
}
+7 -7
View File
@@ -13,15 +13,15 @@
* 设计约束:技术约束-001/003、产品约束-001/002/003
*/
import { QmtBridgeRestDataSource } from './component/QmtBridgeRestDataSource.js';
import { QmtBridgeRestDataSource } from './data-source/QmtBridgeRestDataSource.js';
import { registerSettings, resolveStartupConnection } from './settings.js';
import { DataStore } from './component/DataStore.js';
import { PositionManager } from './component/PositionManager.js';
import { DataStore } from './storage/DataStore.js';
import { PositionManager } from './position/PositionManager.js';
import { registerApi } from './api/index.js';
import { MarketDataHub } from './component/MarketDataHub.js';
import { MarketFeed } from './component/MarketFeed.js';
import { QmtHealthMonitor } from './component/QmtHealthMonitor.js';
import { TradeSync } from './component/TradeSync.js';
import { MarketDataHub } from './market/MarketDataHub.js';
import { MarketFeed } from './market/MarketFeed.js';
import { QmtHealthMonitor } from './data-source/QmtHealthMonitor.js';
import { TradeSync } from './trades/TradeSync.js';
const name = 'one-divine-lot';
@@ -26,7 +26,7 @@ export class MarketDataHub {
/**
* @param {object} opts
* @param {object} [opts.logger]
* @param {import('./DataStore.js').DataStore} [opts.dataStore] 持久化存储可选提供则启用行情落盘
* @param {import('../storage/DataStore.js').DataStore} [opts.dataStore] 持久化存储可选提供则启用行情落盘
*/
constructor({ logger, dataStore } = {}) {
this.logger = logger;
@@ -162,4 +162,4 @@ export class MarketDataHub {
this.logger?.debug?.('[one-divine-lot] MarketDataHub REST 拉取失败: ' + (e?.message ?? e));
}
}
}
}
@@ -20,12 +20,12 @@
* - 对外方法签名与返回结构不变上层 api 无感知
*/
import { DataStore } from './DataStore.js';
import { DataStore } from '../storage/DataStore.js';
export class PositionManager {
/**
* @param {object} opts
* @param {import('./data-source-types.js').DataSource} opts.dataSource 数据源QMT REST
* @param {import('../data-source/data-source-types.js').DataSource} opts.dataSource 数据源QMT REST
* @param {DataStore} opts.storage 数据集存储R-006 DataStore
*/
constructor({ dataSource, storage }) {
@@ -3,16 +3,14 @@
*
* 迭代 06存储引擎从 JSON data store 升级为 SQLitenode:sqlite
* - 数据实际存于 SqliteStorestrategy_holdings / market_quotes_cache
* - 本模块保留对外兼容 APIgetDataset/setDataset/removeDataset/getAllDatasets/getMarketQuotes
* 上层PositionManager / MarketDataHub调用点不变
* - 本模块保留对外兼容 APIgetDataset/getAllDatasets/getMarketQuotes 上层PositionManager / MarketDataHub调用点不变
* - 启动时检测旧 JSON 自动迁移幂等JSON 迁移后废弃备份 .bak
*
* PositionManager 适配单票生命周期openHolding/addShares/reduceShares/closeHolding
* 本模块的整策略 setDataset/removeDataset 将不再被调用保留兼容不删除
* PositionManager 适配单票生命周期旧整策略读写 setDataset/removeDataset 已删除
* 2026-09-02 结构优化调用getDataset/getAllDatasets 仍保留供 PositionManager 使用
*/
import { SqliteStore } from './SqliteStore.js';
export class DataStore {
/**
* @param {object} [options]
@@ -97,28 +95,6 @@ export class DataStore {
return this.sqlite.getCurrentHoldings(strategyId).map((h) => ({ code: h.code, shares: h.shares }));
}
/** 设置某策略数据集(整体替换 —— 兼容旧调用,转换为逐票操作) */
async setDataset(strategyId, dataset) {
await this._ensure();
const items = Array.isArray(dataset) ? dataset.filter((x) => x && x.code && x.shares > 0) : [];
// 简单实现:清空当前持仓再重建(供旧调用方过渡;新代码走生命周期)
for (const h of this.sqlite.getCurrentHoldings(strategyId)) {
this.sqlite.closeHolding(strategyId, h.code);
}
for (const item of items) {
this.sqlite.openHolding(strategyId, item.code, item.shares);
}
return this.getDataset(strategyId);
}
/** 删除某策略数据集(兼容旧调用 —— 转历史,不物理删除) */
async removeDataset(strategyId) {
await this._ensure();
for (const h of this.sqlite.getCurrentHoldings(strategyId)) {
this.sqlite.closeHolding(strategyId, h.code);
}
}
/** 全部数据集(策略为中心,兼容旧调用) */
async getAllDatasets() {
await this._ensure();
+2 -2
View File
@@ -2,7 +2,7 @@ import { defineConfig } from 'tsdown';
export default defineConfig([
{
entry: ['src/index.js', 'src/api/index.js', 'src/settings.js', 'src/component/*.js'],
entry: ['src/index.js', 'src/api/index.js', 'src/settings.js', 'src/data-source/*.js', 'src/storage/*.js', 'src/position/*.js', 'src/market/*.js', 'src/trades/*.js'],
format: ['esm'],
outDir: 'lib',
clean: true,
@@ -21,4 +21,4 @@ export default defineConfig([
'@deepseek-ai/dsh-client-connection',
],
},
]);
]);