迭代16: 交易关联驱动持仓份额动态调整(R-018)

This commit is contained in:
2026-09-08 11:09:05 +08:00
parent 22ec732607
commit 1223e2e74b
25 changed files with 1659 additions and 405 deletions
+9 -41
View File
@@ -56,11 +56,11 @@ console.log('[1] startup warmup + basic sync');
{
const ds = mockDataSource({ positions: [pos('600519.SH', 100), pos('300057.SZ', 200)] });
const st = mockStorage([]);
const sync = new PositionSync({ runtime: { dataSource: ds, storage: st }, ghostRounds: 3 });
const sync = new PositionSync({ runtime: { dataSource: ds, storage: st } });
const r = await sync.syncNow();
ok(sync.getSnapshot().length === 2, 'snapshot has 2 rows');
ok(sync.getSyncedAt() > 0, 'syncedAt recorded');
ok(r && r.positions === 2 && Array.isArray(r.closed), 'returns {positions, closed}');
ok(r && r.positions === 2 && r.closed === undefined, 'returns {positions} (ghost-close retired, no closed)');
ok(st.calls.closed.length === 0, 'no local holdings -> no close');
}
@@ -116,8 +116,9 @@ console.log('[3] empty snapshot double-confirmation');
ok(s4.getSnapshot().length === 1, '3d empty + identity unknown -> old snapshot kept');
}
console.log('[4] ghost holdings auto-close (debounce + account guard)');
console.log('[4] R-018 data-domain boundary: ghost-close retired (sync never writes ledger)');
{
// 账本有活动行(本地仍记 000001.SZ),QMT 快照无此 code —— 同步机制不再自动 closeHolding(分界拍板)
const holdings = [
{ holdingId: 1, strategyId: 'grid', code: '000001.SZ', shares: 100 },
{ holdingId: 2, strategyId: 'manual-t', code: '000001.SZ', shares: 50 },
@@ -125,44 +126,11 @@ console.log('[4] ghost holdings auto-close (debounce + account guard)');
];
const ds = mockDataSource({ positions: [pos('600519.SH')] });
const st = mockStorage(holdings);
const sync = new PositionSync({ runtime: { dataSource: ds, storage: st }, ghostRounds: 3 });
await sync.syncNow();
ok(st.calls.closed.length === 0, '4a round1 miss: no close');
await sync.syncNow();
ok(st.calls.closed.length === 0, '4a round2 miss: no close');
const r3 = await sync.syncNow();
ok(st.calls.closed.length === 2 && st.calls.closed.every((c) => c.code === '000001.SZ'), '4a round3: 000001.SZ closed across 2 strategies');
ok(r3.closed.length === 1 && r3.closed[0].code === '000001.SZ' && r3.closed[0].strategies.length === 2, '4a close detail returned');
ok(!st.calls.closed.some((c) => c.code === '600519.SH'), '4a code still in snapshot untouched');
const st2 = mockStorage(holdings);
const ds2 = mockDataSource({ positions: [pos('600519.SH')] });
const sync2 = new PositionSync({ runtime: { dataSource: ds2, storage: st2 }, ghostRounds: 3 });
await sync2.syncNow(); await sync2.syncNow();
ds2._positions = [pos('600519.SH'), pos('000001.SZ')];
await sync2.syncNow();
ds2._positions = [pos('600519.SH')];
await sync2.syncNow();
ok(st2.calls.closed.length === 0, '4b reappear resets debounce counter');
const st3 = mockStorage(holdings);
const ds3 = mockDataSource({ positions: [pos('600519.SH')] });
const sync3 = new PositionSync({ runtime: { dataSource: ds3, storage: st3 }, ghostRounds: 3 });
await sync3.syncNow(); await sync3.syncNow();
ds3._accountId = 'ACC-2';
await sync3.syncNow(); await sync3.syncNow(); await sync3.syncNow();
ok(st3.calls.closed.length === 0, '4c accountId change resets debounce (never reaches 3 consecutive)');
const st4 = mockStorage(holdings);
const ds4 = mockDataSource({ positions: [pos('600519.SH')] });
const sync4 = new PositionSync({ runtime: { dataSource: ds4, storage: st4 }, ghostRounds: 3 });
await sync4.syncNow(); await sync4.syncNow(); // miss x2(差 1 轮到阈值)
ds4._accountId = null; // 身份丢失
await sync4.syncNow(); await sync4.syncNow(); // 期间跳过判定,不累计
ok(st4.calls.closed.length === 0, '4d identity unknown -> skip close judgement');
ds4._accountId = 'ACC-1'; // 身份恢复
await sync4.syncNow(); // 第 3 次 miss → 达阈值清仓
ok(st4.calls.closed.length === 2, '4d identity restored -> 3rd miss closes');
const sync = new PositionSync({ runtime: { dataSource: ds, storage: st } });
for (let i = 0; i < 5; i++) await sync.syncNow(); // 远超旧 3 轮阈值
ok(st.calls.closed.length === 0, '4a multiple rounds missing: ledger NOT closed (ghost retired)');
ok(sync.getSnapshot().every((p) => p.code === '600519.SH'), '4a snapshot only reflects QMT (600519.SH)');
ok(sync.stats.closedGhosts === undefined, '4a stats.closedGhosts removed');
}
console.log('[5] PositionManager snapshot read + read-through fallback');