mirror of
https://github.com/mauricioabh/arbpulse.git
synced 2026-08-02 18:27:44 +00:00
fix(exchanges): truncate Kraken book to subscribed depth + crossed-book guard (WAY-77)
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>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
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");
|
||||
});
|
||||
Reference in New Issue
Block a user