Files
one_divine_lot/scripts/test-r017-candidates.mjs
T
kyugao 22ec732607 迭代15: 委托归属候选纳入近期清仓持仓(R-017)
- 交易记录 tab 归属候选只回当前持仓 → 清仓后当日委托失去下拉锚点无法补关联
  (2026-09-07 大连热电实际发生: 盘中归属后被清为未关联,下拉已无做T候选)
- SqliteStore.getAttributionCandidates: 候选 = 该 code 当前持仓 + 近 7 天清仓的持仓
  (closed 标记 + closedAt,排序 当前持仓在前 → 清仓行 closed_at DESC)
- AttributionSelect: 下拉 value 改 holdingId 键控(同策略多轮持仓不撞值)、
  清仓选项「(已清仓 MM-DD)」后缀、已归属但候选缺失显示「当前归属」占位
- 回归: test-r017-candidates 12/12 + 存量回归全绿
2026-09-07 18:15:20 +08:00

73 lines
3.8 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.
/**
* R-017 / 迭代 15 回归:归属候选纳入近期清仓持仓
* 独立临时数据目录(技术约束-011)。覆盖:当前持仓在前 / 近 7 天清仓行入选(closed 标记)/
* 超 7 天清仓行被滤 / 策略隔离(同 code 多轮多策略)/ 空 code / 端点透传。
*/
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: () => ({}) } }; // 策略名缺省 → fallback strategyId
try {
console.log('\n[1] 当前持仓候选(closed=false');
const h1 = 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] 近 7 天清仓行入选(closed=true),当前在前');
await storage.openHolding('s-t', '000001.SZ', 500);
await storage.closeHolding('s-t', '000001.SZ'); // 刚清仓(做T场景)
cands = await storage.getTradeAttributionCandidates('000001.SZ');
assert(cands.length === 1 && cands[0].closed === true, '刚清仓行进入候选');
assert(cands[0].holdingId != null && cands[0].closedAt > 0, '清仓行带 holdingId + closedAt');
assert(cands[0].strategyId === 's-t', '清仓行策略正确');
console.log('\n[3] 超 7 天清仓行被滤除');
const hOld = await storage.openHolding('s-t', '600000.SH', 300);
await storage.closeHolding('s-t', '600000.SH');
storage.sqlite.db.prepare('UPDATE strategy_holdings SET closed_at=? WHERE holding_id=?')
.run(Date.now() - 8 * DAY, hOld.holdingId);
cands = await storage.getTradeAttributionCandidates('600000.SH');
assert(cands.length === 0, '8 天前清仓行不在候选');
console.log('\n[4] 排序:当前持仓在前,清仓行 closed_at DESC');
await storage.openHolding('s-g', '300750.SZ', 100);
await storage.closeHolding('s-g', '300750.SZ');
const hOlder = await storage.openHolding('s-g', '300750.SZ', 200); // 复用同码再建仓再清
await storage.closeHolding('s-g', '300750.SZ');
storage.sqlite.db.prepare('UPDATE strategy_holdings SET closed_at=? WHERE holding_id=?')
.run(Date.now() - 3 * DAY, hOlder.holdingId);
cands = await storage.getTradeAttributionCandidates('300750.SZ');
assert(cands.length === 2 && cands.every((c) => c.closed), '同码两轮清仓都入选');
assert(cands[0].closedAt >= cands[1].closedAt, '清仓行按 closed_at DESC');
console.log('\n[5] 空 code / 无持仓 code');
assert((await storage.getTradeAttributionCandidates('')).length === 0, '空 code → []');
assert((await storage.getTradeAttributionCandidates('999999.SH')).length === 0, '无持仓 code → []');
console.log('\n[6] API 端点透传(含 strategyName fallback');
const api = await handleTrade('orders/attribution-candidates', { code: '000001.SZ' }, runtime);
assert(api.length === 1 && api[0].strategyName === 's-t', '端点返回候选且策略名 fallback 到 id');
} finally {
storage.close();
try { rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
}
console.log('\n=== 结果: ' + pass + ' 通过 / ' + fail + ' 失败 ===');
process.exitCode = fail > 0 ? 1 : 0;