Files
kyugao a4902bb730 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 通过
2026-09-02 15:38:51 +08:00

86 lines
4.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* TradeSync + 手动归属集成验证(迭代 07 v2,独立数据目录)
* mock 数据源:返回今日委托/成交 → TradeSync.syncNow → 落库 → 手动设置归属 → 再同步不覆盖归属
*/
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
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;
let pass = 0, fail = 0;
function assert(cond, msg) {
if (cond) { pass++; console.log(' ✅ ' + msg); }
else { fail++; console.log(' ❌ ' + msg); }
}
const storage = new DataStore({ dataDir: dir });
await storage._ensure();
const mockDataSource = {
async getOrders() {
return [{
orderId: 'O-S1', tradeDate: '20260901', code: '300057.SZ', name: '万顺新材',
exchange: 'SZ', direction: 'sell', directionCode: 49, optName: '限价卖出',
status: 50, orderVolume: 300, tradedVolume: 0, limitPrice: 7, tradedPrice: 0,
amount: 0, insertDate: '20260901', insertTime: '093500',
cancelInfo: '', errorMsg: '',
}];
},
async getTrades() {
return [{
tradeId: 'T-S1', orderId: 'O-S1', tradeDate: '20260901', code: '300057.SZ', name: '万顺新材',
exchange: 'SZ', direction: 'sell', directionCode: 49, optName: '限价卖出',
price: 7, volume: 300, amount: 2100, commissionRateWan: 0.852, tradeTime: '093501',
}];
},
};
// 一票多策略:grid 800 + manual 1000
storage.sqlite.openHolding('grid-supermarket', '300057.SZ', 800);
storage.sqlite.openHolding('manual-t', '300057.SZ', 1000);
const sync = new TradeSync({ runtime: { dataSource: mockDataSource, storage }, logger: console });
sync.mounted = true;
const result = await sync.syncNow();
assert(result && result.orders === 1 && result.fills === 1, 'TradeSync 同步落库(委托1/成交1');
// 候选列表:一票多策略 2 个候选
const cands = await storage.getTradeAttributionCandidates('300057.SZ');
assert(cands.length === 2, '候选 2 个(一票多策略)');
// 用户手动设置归属:manual-t
const mHold = storage.sqlite.db.prepare('SELECT holding_id FROM strategy_holdings WHERE strategy_id=? AND code=? AND closed_at IS NULL').get('manual-t', '300057.SZ');
const ok = await storage.setTradeOrderAttribution('O-S1', 'manual-t', mHold.holding_id);
assert(ok, '手动设置归属 manual-t');
// 再次同步(状态流转 已报→已成)→ 归属保留
mockDataSource.getOrders = async () => [{
orderId: 'O-S1', tradeDate: '20260901', code: '300057.SZ', name: '万顺新材',
exchange: 'SZ', direction: 'sell', directionCode: 49, optName: '限价卖出',
status: 56, orderVolume: 300, tradedVolume: 300, limitPrice: 7, tradedPrice: 7,
amount: 2100, insertDate: '20260901', insertTime: '093500',
cancelInfo: '', errorMsg: '',
}];
await sync.syncNow();
const row = storage.sqlite.db.prepare('SELECT status, strategy_id, holding_id FROM trade_orders WHERE order_id=?').get('O-S1');
assert(row.status === 56, '再次同步状态更新(已成)');
assert(row.strategy_id === 'manual-t' && row.holding_id === mHold.holding_id, '再次同步不覆盖归属(manual-t 保留)');
// 历史查询按归属过滤
const byManual = await storage.getTradeOrderHistory({ start: '20260901', end: '20260901', strategyId: 'manual-t' });
assert(byManual.length === 1 && byManual[0].orderId === 'O-S1', '历史按 manual-t 过滤命中');
const byGrid = await storage.getTradeOrderHistory({ start: '20260901', end: '20260901', strategyId: 'grid-supermarket' });
assert(byGrid.length === 0, '历史按 grid-supermarket 过滤为空');
// 成交按委托归属过滤
const fillsManual = await storage.getTradeFillHistory({ start: '20260901', end: '20260901', strategyId: 'manual-t' });
assert(fillsManual.length === 1 && fillsManual[0].tradeId === 'T-S1', '成交按 manual-t 过滤命中');
storage.close();
rmSync(dir, { recursive: true, force: true });
console.log('\n结果: ' + pass + ' 通过, ' + fail + ' 失败');
process.exit(fail > 0 ? 1 : 0);