迭代16: 交易关联驱动持仓份额动态调整(R-018)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* R-017 / 迭代 15 回归:归属候选纳入近期清仓持仓
|
||||
* 独立临时数据目录(技术约束-011)。覆盖:当前持仓在前 / 近 7 天清仓行入选(closed 标记)/
|
||||
* 超 7 天清仓行被滤 / 策略隔离(同 code 多轮多策略)/ 空 code / 端点透传。
|
||||
* 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';
|
||||
@@ -18,51 +18,47 @@ function assert(cond, msg) {
|
||||
const DAY = 86400000;
|
||||
const tmp = mkdtempSync(join(tmpdir(), 'odl-r017-'));
|
||||
const storage = new DataStore({ dataDir: tmp });
|
||||
const runtime = { storage, settings: { get: () => ({}) } }; // 策略名缺省 → fallback strategyId
|
||||
const runtime = {
|
||||
storage,
|
||||
settings: { get: () => ({ strategies: [{ id: 's-t', name: '做T' }, { id: 's-g', name: '网格' }] }) },
|
||||
};
|
||||
|
||||
try {
|
||||
console.log('\n[1] 当前持仓候选(closed=false)');
|
||||
const h1 = await storage.openHolding('s-t', '600719.SH', 1000);
|
||||
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] 近 7 天清仓行入选(closed=true),当前在前');
|
||||
await storage.openHolding('s-t', '000001.SZ', 500);
|
||||
await storage.closeHolding('s-t', '000001.SZ'); // 刚清仓(做T场景)
|
||||
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 === 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);
|
||||
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, '8 天前清仓行不在候选');
|
||||
assert(cands.length === 0, '作废行不进候选');
|
||||
|
||||
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');
|
||||
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[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');
|
||||
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 */ }
|
||||
|
||||
Reference in New Issue
Block a user