a4902bb730
需求与文档: - 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 通过
86 lines
4.1 KiB
JavaScript
86 lines
4.1 KiB
JavaScript
/**
|
||
* 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); |