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 通过
101 lines
5.6 KiB
JavaScript
101 lines
5.6 KiB
JavaScript
/**
|
||
* 迭代 07 回归测试 v2:交易记录本地存储 + 手动归属(R-009 二次定稿)
|
||
* 独立数据目录(ODL_TEST_DATA_DIR),不触碰真实数据(技术约束-011)。
|
||
*
|
||
* 覆盖:
|
||
* 1. 建表:trade_orders 含 strategy_id/holding_id 列(冗余,Q1)
|
||
* 2. UPSERT 幂等 + 不覆盖归属列(Q2)
|
||
* 3. 手动设置归属(set-attribution,Q4 可改)
|
||
* 4. 归属候选列表(该 code 当前持仓策略)
|
||
* 5. 历史查询按持久化归属过滤(含未关联)
|
||
*/
|
||
import { mkdtempSync, rmSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import { join } from 'node:path';
|
||
import { SqliteStore } from '../src/storage/SqliteStore.js';
|
||
|
||
const dir = mkdtempSync(join(tmpdir(), 'odl-test-r009-v2-'));
|
||
process.env.ODL_TEST_DATA_DIR = dir;
|
||
|
||
let pass = 0;
|
||
let fail = 0;
|
||
function assert(cond, msg) {
|
||
if (cond) { pass++; console.log(' ✅ ' + msg); }
|
||
else { fail++; console.log(' ❌ ' + msg); }
|
||
}
|
||
|
||
const store = new SqliteStore({ dataDir: dir });
|
||
store.init();
|
||
|
||
// 1. 建表:trade_orders 含归属列
|
||
console.log('1. 建表');
|
||
const orderCols = store.db.prepare('PRAGMA table_info(trade_orders)').all().map(c => c.name);
|
||
assert(orderCols.includes('strategy_id'), 'trade_orders 有 strategy_id 列(冗余 Q1)');
|
||
assert(orderCols.includes('holding_id'), 'trade_orders 有 holding_id 列(冗余 Q1)');
|
||
|
||
// 2. UPSERT 幂等 + 不覆盖归属列
|
||
console.log('2. UPSERT 幂等 + 不覆盖归属');
|
||
const order = {
|
||
orderId: 'O-001', tradeDate: '20260901', code: '300057.SZ', name: '万顺新材',
|
||
exchange: 'SZ', direction: 'sell', directionCode: 49, optName: '限价卖出',
|
||
status: 50, orderVolume: 100, tradedVolume: 0, limitPrice: 7, tradedPrice: 0,
|
||
amount: 0, insertDate: '20260901', insertTime: '093000',
|
||
cancelInfo: '', errorMsg: '',
|
||
};
|
||
store.upsertOrders([order]);
|
||
// 设置归属(用户手动)
|
||
store.openHolding('manual-t', '300057.SZ', 1000);
|
||
const cands = store.getAttributionCandidates('300057.SZ');
|
||
assert(cands.length === 1 && cands[0].strategyId === 'manual-t', '归属候选:当前持仓 manual-t(300057.SZ)');
|
||
const holding = store.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 = store.setOrderAttribution('O-001', 'manual-t', holding.holding_id);
|
||
assert(ok, '手动设置归属成功');
|
||
// 再次 UPSERT(模拟下次同步,状态流转)→ 归属列保留
|
||
store.upsertOrders([{ ...order, status: 56, tradedVolume: 100, tradedPrice: 7, amount: 700 }]);
|
||
const row = store.db.prepare('SELECT status, strategy_id, holding_id FROM trade_orders WHERE order_id=?').get('O-001');
|
||
assert(row.status === 56, 'UPSERT 覆盖状态(已报→已成)');
|
||
assert(row.strategy_id === 'manual-t' && row.holding_id === holding.holding_id, 'UPSERT 不覆盖归属列(strategy_id/holding_id 保留)');
|
||
|
||
// 3. 手动设置归属可改(Q4)
|
||
console.log('3. 归属可修改');
|
||
store.openHolding('grid-supermarket', '300057.SZ', 800);
|
||
const gHold = store.db.prepare('SELECT holding_id FROM strategy_holdings WHERE strategy_id=? AND code=? AND closed_at IS NULL').get('grid-supermarket', '300057.SZ');
|
||
store.setOrderAttribution('O-001', 'grid-supermarket', gHold.holding_id);
|
||
const row2 = store.db.prepare('SELECT strategy_id, holding_id FROM trade_orders WHERE order_id=?').get('O-001');
|
||
assert(row2.strategy_id === 'grid-supermarket', '归属可改为 grid-supermarket');
|
||
|
||
// 清除归属(改回未关联)
|
||
store.setOrderAttribution('O-001', null, null);
|
||
const row3 = store.db.prepare('SELECT strategy_id, holding_id FROM trade_orders WHERE order_id=?').get('O-001');
|
||
assert(row3.strategy_id === null && row3.holding_id === null, '归属可清除(未关联)');
|
||
|
||
// 4. 候选列表(一票多策略)
|
||
console.log('4. 候选列表(一票多策略)');
|
||
const cands2 = store.getAttributionCandidates('300057.SZ');
|
||
assert(cands2.length === 2, '一票多策略候选 2 个(grid 800 / manual 1000)');
|
||
assert(cands2[0].shares === 1000 && cands2[0].strategyId === 'manual-t', '候选按份额降序(manual-t 1000 在前)');
|
||
|
||
// 5. 历史查询按持久化归属过滤
|
||
console.log('5. 历史查询按持久化归属过滤');
|
||
store.upsertOrders([{
|
||
orderId: 'O-002', tradeDate: '20260901', code: '600519.SH', name: '贵州茅台',
|
||
exchange: 'SH', direction: 'buy', directionCode: 48, optName: '限价买入',
|
||
status: 56, orderVolume: 100, tradedVolume: 100, limitPrice: 1700, tradedPrice: 1700,
|
||
amount: 170000, insertDate: '20260901', insertTime: '100000',
|
||
cancelInfo: '', errorMsg: '',
|
||
}]);
|
||
// O-002 不设归属 → 未关联;O-001 设回 manual-t
|
||
store.setOrderAttribution('O-001', 'manual-t', holding.holding_id);
|
||
const byManual = store.getOrderHistory({ start: '20260901', end: '20260901', strategyId: 'manual-t' });
|
||
assert(byManual.length === 1 && byManual[0].orderId === 'O-001', '按持久化归属 manual-t 过滤命中 O-001');
|
||
const byGrid = store.getOrderHistory({ start: '20260901', end: '20260901', strategyId: 'grid-supermarket' });
|
||
assert(byGrid.length === 0, '按 grid-supermarket 过滤为空(O-001 已设 manual-t)');
|
||
const unassigned = store.getOrderHistory({ start: '20260901', end: '20260901', strategyId: '__unassigned__' });
|
||
assert(unassigned.length === 1 && unassigned[0].orderId === 'O-002', '未关联过滤命中 O-002(未设置归属)');
|
||
const all = store.getOrderHistory({ start: '20260901', end: '20260901' });
|
||
assert(all.length === 2 && all.find(o => o.orderId === 'O-001').strategyId === 'manual-t', '查询返回持久化归属字段');
|
||
|
||
store.close();
|
||
rmSync(dir, { recursive: true, force: true });
|
||
console.log('\n结果: ' + pass + ' 通过, ' + fail + ' 失败');
|
||
process.exit(fail > 0 ? 1 : 0); |