Files
Mauricio Barragan 4ffce9a59e 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>
2026-07-19 11:43:12 -06:00

70 lines
3.0 KiB
Markdown

# order-book-integrity
## Purpose
Garantizar que el libro local de cada exchange refleje fielmente el estado real
del venue: truncado al depth suscrito en feeds delta con ventana top-N,
detección de libros internamente cruzados como señal de corrupción, y
re-sincronización automática en lugar de emitir datos corruptos al motor de
arbitraje.
Origen: incidente de niveles fantasma en Kraken (Linear WAY-77, change
`fix-kraken-phantom-book-levels`).
## Requirements
### Requirement: Truncado del libro local al depth suscrito en feeds delta
`BookSide` SHALL exponer una operación `truncate()` que elimine del estado
interno (no solo de la salida) todos los niveles de precio más allá de los
mejores `depth` niveles del lado (bids: precios más altos; asks: precios más
bajos). El conector de Kraken SHALL invocar el truncado en ambos lados tras
aplicar cada mensaje `update`, usando el mismo depth con el que se suscribió
al canal `book`.
#### Scenario: Nivel que sale de la ventana top-N se elimina
- **WHEN** el libro local de bids contiene `depth` niveles y un update añade un
bid con precio mejor que todos los existentes
- **THEN** tras el truncado el nivel con peor precio ya no existe en el estado
interno del `BookSide` y el tamaño del lado es exactamente `depth`
#### Scenario: Bid fantasma no sobrevive al movimiento del mercado
- **WHEN** el precio de mercado baja y sucesivos updates llenan la ventana
top-N con precios inferiores a un bid antiguo que Kraken ya no reporta
- **THEN** el bid antiguo es eliminado por truncado y el mejor bid emitido
refleja la ventana real del exchange
### Requirement: Depth del conector consistente con la suscripción
Cada conector SHALL mantener su libro local con el mismo depth que solicita en
su suscripción. El conector de Kraken SHALL usar depth 10 tanto en el mensaje
de suscripción como en su `LocalBook`.
#### Scenario: Sin niveles residuales por mismatch de depth
- **WHEN** el conector de Kraken arranca y se suscribe al canal `book`
- **THEN** el depth del `LocalBook` es igual al depth de la suscripción (10)
### Requirement: Guard de libro cruzado con re-sincronización
`ExchangeConnector` SHALL detectar antes de emitir cuando el libro normalizado
está internamente cruzado (`bids[0].price >= asks[0].price`). En ese caso el
conector MUST NOT emitir el libro a los listeners, SHALL registrar el evento en
el log, y SHALL forzar una re-sincronización (reset del libro local y
reconexión del WebSocket para recibir un snapshot fresco).
#### Scenario: Libro cruzado no llega al motor
- **WHEN** el libro local de un exchange queda con mejor bid ≥ mejor ask
- **THEN** no se emite ningún `OrderBook` a los listeners y el
`ArbitrageEngine` no evalúa ese libro
#### Scenario: Recuperación automática tras corrupción
- **WHEN** se detecta un libro cruzado
- **THEN** el conector resetea su libro local y fuerza reconexión, y tras el
snapshot de re-suscripción vuelve a emitir libros consistentes sin
intervención manual