# 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_holdings(holding_id / strategy_id / code / shares / created_at / closed_at),无自定义字段列 | | 定义位置 | 设置页「策略分组」子 tab:策略行内 CRUD(新增 / 重命名 / 删除),无字段配置能力 | | 持仓渲染 | 策略持仓 tab 固定列(代码/名称/份额/现价/盈亏/操作),R-010 已支持持仓行展开看交易记录 | ## 讨论结论(2026-09-02 老师确认 Q1-Q4) - **D1**(第二轮)字段**定义不存数据库**:随策略定义走,扩展 settings.strategies(configSchema 数组);数据库只存值。 - **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 项扩展 configSchema(schemastery array of object,type union text|number|boolean|enum);读取归一化(旧项缺省 []);strategies/update 沿用整表更新语义。 2. **src/storage/SqliteStore.js**:init() 增加 _ensureHoldingValuesColumn(PRAGMA 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-Q4,2026-09-02 老师确认) - **Q1** 字段定义 = 策略级(某策略下每笔持仓需要的字段由该策略定义决定);值 = 持仓级(每行一套键值对)→ 确认。 - **Q2** 字段加类型(文本 / 数字 / 布尔 / 枚举)→ 确认。 - **Q3** 支持枚举字段(预设选项下拉)→ 确认(有用)。 - **Q4** 旧策略无自定义字段:沿用现有字段定义(不迁移、不补默认),值列缺省空 → 确认。 - **D6(值编辑入口)**:持仓行展开区编辑 —— 老师确认(2026-09-02)。 > 三要素满足(边界清楚 / 核心逻辑明确 / 老师确认 Q1-Q4),**已定稿**,可进入计划范围。