mirror of
https://github.com/mauricioabh/arbpulse.git
synced 2026-07-27 23:57:43 +00:00
4ffce9a59e
Kraken WS v2 book channel does not send deletes for levels evicted from its top-N window; without client-side truncation those levels lingered forever as phantom quotes, eventually crossing the local book (bid >= ask) and feeding the engine a fake permanent arbitrage (~$62.9M bogus P&L). - BookSide.truncate() removes levels beyond the best depth prices from the internal map (not just the emitted array) - KrakenConnector uses depth 10 consistently (subscription + LocalBook) and truncates both sides after every update - ExchangeConnector.emit() drops internally crossed books, logs and forces a resync (book reset + reconnect for a fresh snapshot) - Unit tests for truncation and the crossed-book guard - OpenSpec: order-book-integrity spec; change archived (2026-07-19) Refs: Linear WAY-77 Co-authored-by: Cursor <cursoragent@cursor.com>
73 lines
1.8 KiB
TypeScript
73 lines
1.8 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import type { ExchangeId, OrderBook } from "../../domain/entities/index.js";
|
|
import { ExchangeConnector } from "./base.js";
|
|
|
|
/** Minimal connector for exercising emit() without a real WebSocket. */
|
|
class TestConnector extends ExchangeConnector {
|
|
readonly id: ExchangeId = "kraken";
|
|
protected readonly url = "ws://unused";
|
|
resyncCount = 0;
|
|
|
|
constructor() {
|
|
super(5);
|
|
}
|
|
|
|
protected subscribeMessage(): unknown {
|
|
return {};
|
|
}
|
|
|
|
protected handleMessage(): void {}
|
|
|
|
protected override resync(): void {
|
|
this.resyncCount += 1;
|
|
super.resync();
|
|
}
|
|
|
|
applyLevels(bids: [number, number][], asks: [number, number][]): void {
|
|
for (const [price, qty] of bids) this.book.bids.apply(price, qty);
|
|
for (const [price, qty] of asks) this.book.asks.apply(price, qty);
|
|
}
|
|
|
|
emitNow(): void {
|
|
this.emit(null);
|
|
}
|
|
|
|
get bookSize(): number {
|
|
return this.book.bids.size + this.book.asks.size;
|
|
}
|
|
}
|
|
|
|
test("normal book is emitted to listeners", () => {
|
|
const c = new TestConnector();
|
|
const received: OrderBook[] = [];
|
|
c.onBook((b) => received.push(b));
|
|
|
|
c.applyLevels([[64560, 1]], [[64561, 1]]);
|
|
c.emitNow();
|
|
|
|
assert.equal(received.length, 1);
|
|
assert.equal(received[0]?.bids[0]?.price, 64560);
|
|
assert.equal(c.resyncCount, 0);
|
|
});
|
|
|
|
test("crossed book is not emitted and triggers resync", () => {
|
|
const c = new TestConnector();
|
|
const received: OrderBook[] = [];
|
|
c.onBook((b) => received.push(b));
|
|
|
|
// Phantom bid above the real ask: corrupted local book.
|
|
c.applyLevels(
|
|
[
|
|
[64925.9, 0.15],
|
|
[64560, 1],
|
|
],
|
|
[[64561, 1]],
|
|
);
|
|
c.emitNow();
|
|
|
|
assert.equal(received.length, 0, "corrupted book must not reach listeners");
|
|
assert.equal(c.resyncCount, 1);
|
|
assert.equal(c.bookSize, 0, "local book is reset for a fresh snapshot");
|
|
});
|