Initial commit: Arb Pulse monolith with CI and optional Fly deploy.
Real-time BTC cross-exchange arbitrage detection (Kraken, Bybit, OKX, Binance) with React dashboard, GitHub Actions CI, and documented Fly.io deploy workflow. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
908aefb2d8
commit
2e8744ccf0
@@ -0,0 +1,102 @@
|
||||
import type { ExchangeId } from "../../domain/entities/index.js";
|
||||
import { ExchangeConnector } from "./base.js";
|
||||
|
||||
interface BinanceDepthPayload {
|
||||
lastUpdateId?: number;
|
||||
bids?: [string, string][];
|
||||
asks?: [string, string][];
|
||||
}
|
||||
|
||||
interface BinanceRestDepth {
|
||||
lastUpdateId: number;
|
||||
bids: [string, string][];
|
||||
asks: [string, string][];
|
||||
}
|
||||
|
||||
const REST_URL = "https://api.binance.com/api/v3/depth?symbol=BTCUSDT&limit=10";
|
||||
const REST_POLL_MS = 500;
|
||||
|
||||
/**
|
||||
* Binance spot partial depth stream — top 10 levels @ 100ms.
|
||||
* Docs: https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams
|
||||
* Combined stream URL (no subscribe message). REST polling fills gaps when WS is down.
|
||||
*/
|
||||
export class BinanceConnector extends ExchangeConnector {
|
||||
readonly id: ExchangeId = "binance";
|
||||
protected readonly url = "wss://stream.binance.com:9443/ws/btcusdt@depth10@100ms";
|
||||
|
||||
private restTimer: NodeJS.Timeout | null = null;
|
||||
private wsLive = false;
|
||||
|
||||
protected override skipSubscribe(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override subscribeMessage(): unknown {
|
||||
return {};
|
||||
}
|
||||
|
||||
override start(): void {
|
||||
super.start();
|
||||
if (!this.wsLive) this.startRestPoll();
|
||||
}
|
||||
|
||||
override stop(): void {
|
||||
this.stopRestPoll();
|
||||
super.stop();
|
||||
}
|
||||
|
||||
protected override onConnected(): void {
|
||||
this.wsLive = true;
|
||||
this.stopRestPoll();
|
||||
}
|
||||
|
||||
protected override onDisconnected(): void {
|
||||
this.wsLive = false;
|
||||
if (!this.closed) this.startRestPoll();
|
||||
}
|
||||
|
||||
protected override handleMessage(msg: unknown): void {
|
||||
const m = msg as BinanceDepthPayload;
|
||||
if (!m.bids?.length && !m.asks?.length) return;
|
||||
this.applyDepth(m.bids ?? [], m.asks ?? []);
|
||||
this.emit(null);
|
||||
}
|
||||
|
||||
private applyDepth(bids: [string, string][], asks: [string, string][]): void {
|
||||
this.book.reset();
|
||||
for (const [price, size] of bids) {
|
||||
this.book.bids.apply(Number(price), Number(size));
|
||||
}
|
||||
for (const [price, size] of asks) {
|
||||
this.book.asks.apply(Number(price), Number(size));
|
||||
}
|
||||
}
|
||||
|
||||
private startRestPoll(): void {
|
||||
if (this.restTimer || this.closed) return;
|
||||
this.restTimer = setInterval(() => {
|
||||
if (this.wsLive || this.closed) return;
|
||||
void this.pollRest();
|
||||
}, REST_POLL_MS);
|
||||
void this.pollRest();
|
||||
}
|
||||
|
||||
private stopRestPoll(): void {
|
||||
if (!this.restTimer) return;
|
||||
clearInterval(this.restTimer);
|
||||
this.restTimer = null;
|
||||
}
|
||||
|
||||
private async pollRest(): Promise<void> {
|
||||
try {
|
||||
const res = await fetch(REST_URL, { signal: AbortSignal.timeout(4000) });
|
||||
if (!res.ok) return;
|
||||
const data = (await res.json()) as BinanceRestDepth;
|
||||
this.applyDepth(data.bids ?? [], data.asks ?? []);
|
||||
this.emit(null);
|
||||
} catch {
|
||||
/* network blip — next poll retries */
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user