/** * R-017 / 迭代 15 回归 → R-018 / 迭代 16 语义修订:归属候选退役 7 天清仓窗口 * 新语义(R-018 T2 老师拍板):候选回归「当前活动持仓 + 未关联」;正式候选为策略级 attribution-targets。 * 覆盖:当前活动持仓候选 / 清仓/作废行不进候选 / 多策略 / 空 code / targets 端点。 */ import { DataStore } from '../src/storage/DataStore.js'; import { handleTrade } from '../src/api/trades.js'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; let pass = 0, fail = 0; function assert(cond, msg) { if (cond) { pass++; console.log(' ✅ ' + msg); } else { fail++; console.log(' ❌ ' + msg); } } const DAY = 86400000; const tmp = mkdtempSync(join(tmpdir(), 'odl-r017-')); const storage = new DataStore({ dataDir: tmp }); const runtime = { storage, settings: { get: () => ({ strategies: [{ id: 's-t', name: '做T' }, { id: 's-g', name: '网格' }] }) }, }; try { console.log('\n[1] 当前活动持仓候选(closed=false)'); await storage.openHolding('s-t', '600719.SH', 1000); await storage.openHolding('s-g', '600719.SH', 800); // 同码第二策略 let cands = await storage.getTradeAttributionCandidates('600719.SH'); assert(cands.length === 2, '同码两策略当前持仓都在候选'); assert(cands.every((c) => c.closed === false && c.closedAt === null), '当前持仓 closed=false / closedAt=null'); assert(cands[0].shares >= cands[1].shares, '当前持仓按 shares DESC'); console.log('\n[2] 清仓/作废行不进候选(R-018 退役 7 天窗口)'); const hc = await storage.openHolding('s-t', '000001.SZ', 500); await storage.closeHolding('s-t', '000001.SZ'); // 刚清仓(R-017 时代会入选,R-018 起不再) cands = await storage.getTradeAttributionCandidates('000001.SZ'); assert(cands.length === 0, '刚清仓行不进候选(7 天窗口退役)'); const hv = await storage.openHolding('s-t', '600000.SH', 300); storage.sqlite.voidHolding('s-t', '600000.SH'); cands = await storage.getTradeAttributionCandidates('600000.SH'); assert(cands.length === 0, '作废行不进候选'); console.log('\n[3] 空 code / 无持仓 code'); assert((await storage.getTradeAttributionCandidates('')).length === 0, '空 code → []'); assert((await storage.getTradeAttributionCandidates('999999.SH')).length === 0, '无持仓 code → []'); console.log('\n[4] R-018 attribution-targets(策略级候选端点)'); const hc2 = await storage.openHolding('s-t', '000002.SZ', 500); const tgt = await handleTrade('orders/attribution-targets', { code: '000002.SZ' }, runtime); assert(Array.isArray(tgt), 'targets 端点返回数组'); assert(tgt.every((x) => x.strategyId && x.activityShares != null), 'targets 每项含 strategyId + activityShares'); const rowT = tgt.find((x) => x.strategyId === 's-t'); assert(rowT && rowT.activityShares === 500 && rowT.hasActiveHolding === true, '有仓策略 activityShares=500 / hasActiveHolding=true'); const rowG = tgt.find((x) => x.strategyId === 's-g'); assert(rowG && rowG.activityShares === 0 && rowG.hasActiveHolding === false, '无仓策略 activityShares=0 / hasActiveHolding=false'); // 关闭的持仓不占 hasActiveHolding await storage.closeHolding('s-t', '000002.SZ'); const tgt2 = await handleTrade('orders/attribution-targets', { code: '000002.SZ' }, runtime); assert(tgt2.find((x) => x.strategyId === 's-t').hasActiveHolding === false, '清仓后该策略 hasActiveHolding=false'); } finally { storage.close(); try { rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ } } console.log('\n=== 结果: ' + pass + ' 通过 / ' + fail + ' 失败 ==='); process.exitCode = fail > 0 ? 1 : 0;