diff --git a/docs/02-计划/计划-策略自定义字段配置.md b/docs/02-计划/计划-策略自定义字段配置.md new file mode 100644 index 0000000..1c83a5a --- /dev/null +++ b/docs/02-计划/计划-策略自定义字段配置.md @@ -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 扩展 configSchema(schemastery:array of object,type 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. API:strategy-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 通过;回归脚本全绿。 diff --git a/docs/03-设计约束/UI交互约束.md b/docs/03-设计约束/UI交互约束.md index ce689ce..4ff57ee 100644 --- a/docs/03-设计约束/UI交互约束.md +++ b/docs/03-设计约束/UI交互约束.md @@ -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 | 策略自定义字段配置 UI(R-013,2026-09-02):设置页「策略分组」子 tab 策略行**可展开** → 展开区 = 字段列表(label/key/type/enum/默认值)+ 添加/编辑/删除字段表单(名称/类型/枚举选项/默认值)+ 保存(走 strategies/update 整表);策略持仓 tab 持仓行展开区(R-010 基础上)增加「自定义字段」区块:按该策略 configSchema 渲染输入控件(文本=输入框、数字=数字输入、布尔=开关、枚举=下拉),值来自该行 values,编辑即保存调 holdings/values-update,Toast 反馈;未配置字段定义的策略持仓行不渲染字段区 | 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 双值定义) | +--> \ No newline at end of file diff --git a/docs/03-设计约束/产品功能约束.md b/docs/03-设计约束/产品功能约束.md index 0782cc0..e8053aa 100644 --- a/docs/03-设计约束/产品功能约束.md +++ b/docs/03-设计约束/产品功能约束.md @@ -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 禁重命名/删除 | \ No newline at end of file diff --git a/docs/04-迭代记录/11-策略自定义字段配置/技术实现方案.md b/docs/04-迭代记录/11-策略自定义字段配置/技术实现方案.md new file mode 100644 index 0000000..3831da0 --- /dev/null +++ b/docs/04-迭代记录/11-策略自定义字段配置/技术实现方案.md @@ -0,0 +1,89 @@ +# 技术实现方案:11-策略自定义字段配置 + +> 迭代编号:11 | 依据:PLAN-012 + R-013(目标数据模型 / 端到端示例)+ 技术约束-015 / 产品约束-010 / UI约束-005 + +## 1. 数据层 + +### 1.1 settings.strategies 扩展 configSchema(src/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 返回项附加 values(from 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. 设置页 UI(src/client/views/SettingsSection.jsx) + +- 「策略分组」子 tab 策略行加展开手柄(▶/▸ toggle,样式沿用 expand 惯例); +- 展开区: + - 字段列表表格:展示名 | key | 类型 | 枚举选项 | 默认值 | 操作(删除)—— 空态提示「尚未配置自定义字段」; + - 添加/编辑字段表单:字段名(label)→ 自动生成 key(拼音 slug,复用 generateStrategyId 思路)/ 可手改校验唯一、类型下拉(文本/数字/布尔/枚举)、枚举选项(type=enum 时逗号分隔输入)、默认值(按类型渲染输入); + - 行内编辑/删除:编辑回填表单,删除后保存生效; + - 保存:整表提交 strategies/update(映射为 { ...s, configSchema }),成功 Toast + 刷新(沿用「刷新页面后生效」机制)。 + +## 4. 策略持仓 tab UI(src/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 通过; +- 老师人工验收(见验收标准)。 diff --git a/docs/04-迭代记录/11-策略自定义字段配置/程序结构设计.md b/docs/04-迭代记录/11-策略自定义字段配置/程序结构设计.md new file mode 100644 index 0000000..0b4b2c0 --- /dev/null +++ b/docs/04-迭代记录/11-策略自定义字段配置/程序结构设计.md @@ -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 API(webServer /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/removeDataset(PositionManager 适配单票生命周期后无调用方); +3. **构建与脚本同步**:tsdown.config.ts entry 更新为新目录 glob;scripts/*.mjs 导入路径更新; +4. **验证**:typecheck + build 通过;test-r009 回归 14/14 通过(独立数据目录)。 + +## 归类规则(沉淀为技术约束-016) + +- 服务端代码禁止平铺,按功能域分目录:data-source / storage / position / market / trades; +- api/ 按领域拆子文件;client/ 仅放 UI(views/ 组件 + 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 # 回归脚本(独立数据目录) +``` diff --git a/docs/04-迭代记录/11-策略自定义字段配置/迭代目标.md b/docs/04-迭代记录/11-策略自定义字段配置/迭代目标.md new file mode 100644 index 0000000..b2111bf --- /dev/null +++ b/docs/04-迭代记录/11-策略自定义字段配置/迭代目标.md @@ -0,0 +1,21 @@ +# 迭代目标:11-策略自定义字段配置(定义随策略,值落库) + +> 迭代编号:11 | 创建:2026-09-02 | 状态:进行中 +> 依据计划:PLAN-012 | 需求:R-013(已定稿,2026-09-02,老师确认 Q1-Q4 + D6) + +## 目标描述 + +给策略增加**可自定义、可扩展**的字段能力:每个策略在设置页「策略分组」子 tab 配置自定义字段定义(configSchema:key/label/type/enum/默认值,随策略定义存 settings 不落库);该策略下每个持仓(strategy_holdings 行)按所属策略的定义存取一份键值对值(新增 values JSON 列落库);策略持仓 tab 持仓行展开区按定义渲染并编辑值。旧策略(无定义)行为与现状完全一致。 + +## 目标分解 + +1. 数据层:settings.strategies 扩展 configSchema(schemastery,type 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 已确认)。 diff --git a/docs/04-迭代记录/11-策略自定义字段配置/验收标准.md b/docs/04-迭代记录/11-策略自定义字段配置/验收标准.md new file mode 100644 index 0000000..20a2f09 --- /dev/null +++ b/docs/04-迭代记录/11-策略自定义字段配置/验收标准.md @@ -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 更新实现状态(已实现),归档。 diff --git a/docs/05-需求池/R-013.md b/docs/05-需求池/R-013.md new file mode 100644 index 0000000..9153603 --- /dev/null +++ b/docs/05-需求池/R-013.md @@ -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_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/component/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),**已定稿**,可进入计划范围。 \ No newline at end of file diff --git a/docs/05-需求池/R-011.md b/docs/05-需求池/已完成/R-011.md similarity index 93% rename from docs/05-需求池/R-011.md rename to docs/05-需求池/已完成/R-011.md index 99c38ce..dcf6e9b 100644 --- a/docs/05-需求池/R-011.md +++ b/docs/05-需求池/已完成/R-011.md @@ -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 化扩展的二次演进) diff --git a/docs/05-需求池/R-012.md b/docs/05-需求池/已完成/R-012.md similarity index 90% rename from docs/05-需求池/R-012.md rename to docs/05-需求池/已完成/R-012.md index 1ab290c..ef1c3c7 100644 --- a/docs/05-需求池/R-012.md +++ b/docs/05-需求池/已完成/R-012.md @@ -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) diff --git a/docs/05-需求池/需求池索引.md b/docs/05-需求池/需求池索引.md index 70c0199..ba1ee95 100644 --- a/docs/05-需求池/需求池索引.md +++ b/docs/05-需求池/需求池索引.md @@ -22,8 +22,9 @@ | 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 | **已实现(已归档)** | | R-009 | 交易记录本地存储(SQLite)+ 策略关联 | 在 SQLite 新增交易记录表(trade_orders + trade_fills,委托/成交两表),QMT 当日交易数据本地持久化(跨日积累成历史库);**归属由用户在交易记录 tab 手动设置(trade_orders 冗余存 strategy_id + holding_id,UPSERT 不覆盖归属列,可随时改)**;本地历史查询端点(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 升级为 SQLite(node:sqlite)数据库 + 数据存储管理能力;**变更既有设计约束**(数据存储设计.md「无需数据库」决策)。**2026-09-01 定稿转正为 R-008 并完成(迭代 06)** | 已转需求 R-008 | \ No newline at end of file +| T-008 | 数据存储管理(JSON → SQLite) | 数据存储从 JSON data store 升级为 SQLite(node:sqlite)数据库 + 数据存储管理能力;**变更既有设计约束**(数据存储设计.md「无需数据库」决策)。**2026-09-01 定稿转正为 R-008 并完成(迭代 06)** | 已转需求 R-008 | +| T-009 | 策略自定义字段配置(策略细节配置) | 每个策略支持自定义字段(键值对),不同策略按自身需要添加各自的细节配置字段(如网格超市配网格间距/单格金额、长线持有配目标价/仓位上限)。**2026-09-02 定稿转正为 R-013**,草稿文件已清理 | 已转需求 R-013 | \ No newline at end of file diff --git a/scripts/migrate-json-to-sqlite.mjs b/scripts/migrate-json-to-sqlite.mjs index 9f390c8..6d6fe32 100644 --- a/scripts/migrate-json-to-sqlite.mjs +++ b/scripts/migrate-json-to-sqlite.mjs @@ -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(); \ No newline at end of file diff --git a/scripts/test-r009-api.mjs b/scripts/test-r009-api.mjs index 5be411a..fa94c02 100644 --- a/scripts/test-r009-api.mjs +++ b/scripts/test-r009-api.mjs @@ -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-')); diff --git a/scripts/test-r009-normalize.mjs b/scripts/test-r009-normalize.mjs index 0939b2a..2d9cd8e 100644 --- a/scripts/test-r009-normalize.mjs +++ b/scripts/test-r009-normalize.mjs @@ -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); \ No newline at end of file diff --git a/scripts/test-r009-sync.mjs b/scripts/test-r009-sync.mjs index 1468a7d..36dded7 100644 --- a/scripts/test-r009-sync.mjs +++ b/scripts/test-r009-sync.mjs @@ -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); \ No newline at end of file diff --git a/scripts/test-r009.mjs b/scripts/test-r009.mjs index 398e57c..88e6410 100644 --- a/scripts/test-r009.mjs +++ b/scripts/test-r009.mjs @@ -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); \ No newline at end of file diff --git a/scripts/verify-real-attribution.mjs b/scripts/verify-real-attribution.mjs index 28d71b3..708c6be 100644 --- a/scripts/verify-real-attribution.mjs +++ b/scripts/verify-real-attribution.mjs @@ -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); \ No newline at end of file diff --git a/scripts/verify-real-r009.mjs b/scripts/verify-real-r009.mjs index fb8cc8d..8de2135 100644 --- a/scripts/verify-real-r009.mjs +++ b/scripts/verify-real-r009.mjs @@ -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(); diff --git a/src/component/AllocationStorage.js b/src/component/AllocationStorage.js deleted file mode 100644 index 78d7854..0000000 --- a/src/component/AllocationStorage.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * 本地存储:分仓分配数据(allocations.json) - * - * 设计约束:产品约束-003(分仓数据本地保存并更新管理) - * QMT 只提供全量真实持仓,策略分仓分配是本地维护的元数据。 - * - * 文件位置:/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} 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} 受影响标的代码列表 - */ - 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; - } -} diff --git a/src/component/QmtBridgeRestDataSource.js b/src/data-source/QmtBridgeRestDataSource.js similarity index 100% rename from src/component/QmtBridgeRestDataSource.js rename to src/data-source/QmtBridgeRestDataSource.js diff --git a/src/component/QmtHealthMonitor.js b/src/data-source/QmtHealthMonitor.js similarity index 100% rename from src/component/QmtHealthMonitor.js rename to src/data-source/QmtHealthMonitor.js diff --git a/src/component/data-source-types.js b/src/data-source/data-source-types.js similarity index 100% rename from src/component/data-source-types.js rename to src/data-source/data-source-types.js diff --git a/src/index.js b/src/index.js index 3849279..701e80f 100644 --- a/src/index.js +++ b/src/index.js @@ -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'; diff --git a/src/component/MarketDataHub.js b/src/market/MarketDataHub.js similarity index 97% rename from src/component/MarketDataHub.js rename to src/market/MarketDataHub.js index a4db9a8..c8ca409 100644 --- a/src/component/MarketDataHub.js +++ b/src/market/MarketDataHub.js @@ -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)); } } -} +} \ No newline at end of file diff --git a/src/component/MarketFeed.js b/src/market/MarketFeed.js similarity index 100% rename from src/component/MarketFeed.js rename to src/market/MarketFeed.js diff --git a/src/component/PositionManager.js b/src/position/PositionManager.js similarity index 98% rename from src/component/PositionManager.js rename to src/position/PositionManager.js index a6bb388..f332510 100644 --- a/src/component/PositionManager.js +++ b/src/position/PositionManager.js @@ -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 }) { diff --git a/src/component/DataStore.js b/src/storage/DataStore.js similarity index 80% rename from src/component/DataStore.js rename to src/storage/DataStore.js index d0f5b08..05f7a05 100644 --- a/src/component/DataStore.js +++ b/src/storage/DataStore.js @@ -3,16 +3,14 @@ * * 迭代 06:存储引擎从 JSON data store 升级为 SQLite(node:sqlite)。 * - 数据实际存于 SqliteStore(strategy_holdings / market_quotes_cache 表); - * - 本模块保留对外兼容 API(getDataset/setDataset/removeDataset/getAllDatasets/getMarketQuotes 等), - * 上层(PositionManager / MarketDataHub)调用点不变; + * - 本模块保留对外兼容 API(getDataset/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(); diff --git a/src/component/SqliteStore.js b/src/storage/SqliteStore.js similarity index 100% rename from src/component/SqliteStore.js rename to src/storage/SqliteStore.js diff --git a/src/component/TradeSync.js b/src/trades/TradeSync.js similarity index 100% rename from src/component/TradeSync.js rename to src/trades/TradeSync.js diff --git a/tsdown.config.ts b/tsdown.config.ts index a625ff9..0a3ab79 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -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', ], }, -]); +]); \ No newline at end of file