Files
one_divine_lot/scripts/test-r009.mjs
T
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

101 lines
5.6 KiB
JavaScript
Raw 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.
/**
* 迭代 07 回归测试 v2:交易记录本地存储 + 手动归属(R-009 二次定稿)
* 独立数据目录(ODL_TEST_DATA_DIR),不触碰真实数据(技术约束-011)。
*
* 覆盖:
* 1. 建表:trade_orders 含 strategy_id/holding_id 列(冗余,Q1
* 2. UPSERT 幂等 + 不覆盖归属列(Q2)
* 3. 手动设置归属(set-attributionQ4 可改)
* 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-t300057.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);