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:
@@ -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
@@ -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 升级为 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();
|
||||
Reference in New Issue
Block a user