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,53 @@
|
||||
import { config } from "../config/config.js";
|
||||
import {
|
||||
EXCHANGE_IDS,
|
||||
type BestQuote,
|
||||
type ExchangeId,
|
||||
type FeedStatus,
|
||||
type OrderBook,
|
||||
} from "../../domain/entities/index.js";
|
||||
import type { IQuoteBook } from "../../domain/ports/ports.js";
|
||||
|
||||
export class OrderBookManager implements IQuoteBook {
|
||||
private books = new Map<ExchangeId, OrderBook>();
|
||||
|
||||
update(book: OrderBook): void {
|
||||
this.books.set(book.exchange, book);
|
||||
}
|
||||
|
||||
getBook(exchange: ExchangeId): OrderBook | undefined {
|
||||
return this.books.get(exchange);
|
||||
}
|
||||
|
||||
isFresh(exchange: ExchangeId, now: number): boolean {
|
||||
const book = this.books.get(exchange);
|
||||
if (!book) return false;
|
||||
return now - book.recvTs <= config.staleMs;
|
||||
}
|
||||
|
||||
private statusFor(book: OrderBook | undefined, now: number): FeedStatus {
|
||||
if (!book) return "connecting";
|
||||
const age = now - book.recvTs;
|
||||
if (age > config.staleMs * 3) return "down";
|
||||
if (age > config.staleMs) return "stale";
|
||||
return "live";
|
||||
}
|
||||
|
||||
bestQuotes(now: number): BestQuote[] {
|
||||
return EXCHANGE_IDS.map((exchange) => {
|
||||
const book = this.books.get(exchange);
|
||||
const topBid = book?.bids[0];
|
||||
const topAsk = book?.asks[0];
|
||||
return {
|
||||
exchange,
|
||||
bid: topBid?.price ?? null,
|
||||
bidQty: topBid?.qty ?? null,
|
||||
ask: topAsk?.price ?? null,
|
||||
askQty: topAsk?.qty ?? null,
|
||||
recvTs: book?.recvTs ?? null,
|
||||
status: this.statusFor(book, now),
|
||||
ageMs: book ? now - book.recvTs : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { CircuitState, Opportunity, PnlPoint, RebalanceEvent, Trade } from "../../domain/entities/index.js";
|
||||
import type { IStateStore } from "../../domain/ports/ports.js";
|
||||
import { config } from "../config/config.js";
|
||||
|
||||
export class Store implements IStateStore {
|
||||
readonly startedAt = Date.now();
|
||||
|
||||
private opportunities: Opportunity[] = [];
|
||||
private trades: Trade[] = [];
|
||||
private rebalances: RebalanceEvent[] = [];
|
||||
private pnl: PnlPoint[] = [];
|
||||
|
||||
ticksProcessed = 0;
|
||||
opportunitiesDetected = 0;
|
||||
tradesExecuted = 0;
|
||||
tradesRejected = 0;
|
||||
realizedPnl = 0;
|
||||
consecutiveLosses = 0;
|
||||
circuit: CircuitState = "running";
|
||||
tickTimeEwma = 0;
|
||||
|
||||
recordTickTime(ms: number): void {
|
||||
const alpha = 0.05;
|
||||
this.tickTimeEwma = this.tickTimeEwma === 0 ? ms : this.tickTimeEwma * (1 - alpha) + ms * alpha;
|
||||
}
|
||||
|
||||
addOpportunity(op: Opportunity): void {
|
||||
this.opportunitiesDetected += 1;
|
||||
this.opportunities.unshift(op);
|
||||
if (this.opportunities.length > config.recentEventsMax) this.opportunities.pop();
|
||||
}
|
||||
|
||||
addTrade(trade: Trade): void {
|
||||
this.tradesExecuted += 1;
|
||||
this.realizedPnl += trade.netProfit;
|
||||
this.trades.unshift(trade);
|
||||
if (this.trades.length > config.recentEventsMax) this.trades.pop();
|
||||
|
||||
if (trade.netProfit < 0) {
|
||||
this.consecutiveLosses += 1;
|
||||
} else {
|
||||
this.consecutiveLosses = 0;
|
||||
}
|
||||
|
||||
this.pnl.push({ ts: trade.ts, pnl: this.realizedPnl });
|
||||
if (this.pnl.length > config.pnlSeriesMax) this.pnl.shift();
|
||||
}
|
||||
|
||||
addRebalance(event: RebalanceEvent): void {
|
||||
this.rebalances.unshift(event);
|
||||
if (this.rebalances.length > 20) this.rebalances.pop();
|
||||
}
|
||||
|
||||
recentOpportunities(): Opportunity[] {
|
||||
return this.opportunities;
|
||||
}
|
||||
|
||||
recentTrades(): Trade[] {
|
||||
return this.trades;
|
||||
}
|
||||
|
||||
recentRebalances(): RebalanceEvent[] {
|
||||
return this.rebalances;
|
||||
}
|
||||
|
||||
pnlSeries(): PnlPoint[] {
|
||||
return this.pnl;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.opportunities = [];
|
||||
this.trades = [];
|
||||
this.rebalances = [];
|
||||
this.pnl = [];
|
||||
this.ticksProcessed = 0;
|
||||
this.opportunitiesDetected = 0;
|
||||
this.tradesExecuted = 0;
|
||||
this.tradesRejected = 0;
|
||||
this.realizedPnl = 0;
|
||||
this.consecutiveLosses = 0;
|
||||
this.circuit = "running";
|
||||
this.tickTimeEwma = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { config } from "../config/config.js";
|
||||
import { EXCHANGE_IDS, type ExchangeId, type Wallet } from "../../domain/entities/index.js";
|
||||
import type { IInventory } from "../../domain/ports/ports.js";
|
||||
|
||||
export class WalletBook implements IInventory {
|
||||
private wallets = new Map<ExchangeId, Wallet>();
|
||||
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.wallets.clear();
|
||||
for (const exchange of EXCHANGE_IDS) {
|
||||
this.wallets.set(exchange, {
|
||||
exchange,
|
||||
usdt: config.initialUsdt,
|
||||
btc: config.initialBtc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get(exchange: ExchangeId): Wallet {
|
||||
const w = this.wallets.get(exchange);
|
||||
if (!w) throw new Error(`unknown exchange wallet: ${exchange}`);
|
||||
return w;
|
||||
}
|
||||
|
||||
all(): Wallet[] {
|
||||
return EXCHANGE_IDS.map((e) => ({ ...this.get(e) }));
|
||||
}
|
||||
|
||||
maxBuyableBtc(exchange: ExchangeId, vwapWithFee: number): number {
|
||||
if (vwapWithFee <= 0) return 0;
|
||||
return this.get(exchange).usdt / vwapWithFee;
|
||||
}
|
||||
|
||||
sellableBtc(exchange: ExchangeId): number {
|
||||
return this.get(exchange).btc;
|
||||
}
|
||||
|
||||
applyBuy(exchange: ExchangeId, btc: number, quoteCost: number): void {
|
||||
const w = this.get(exchange);
|
||||
w.btc += btc;
|
||||
w.usdt -= quoteCost;
|
||||
}
|
||||
|
||||
applySell(exchange: ExchangeId, btc: number, quoteProceeds: number): void {
|
||||
const w = this.get(exchange);
|
||||
w.btc -= btc;
|
||||
w.usdt += quoteProceeds;
|
||||
}
|
||||
|
||||
applyTransfer(from: ExchangeId, to: ExchangeId, asset: "BTC" | "USDT", amount: number, fee: number): void {
|
||||
const src = this.get(from);
|
||||
const dst = this.get(to);
|
||||
if (asset === "BTC") {
|
||||
src.btc -= amount;
|
||||
dst.btc += amount - fee;
|
||||
} else {
|
||||
src.usdt -= amount;
|
||||
dst.usdt += amount - fee;
|
||||
}
|
||||
}
|
||||
|
||||
totalEquity(btcRef: number): number {
|
||||
let total = 0;
|
||||
for (const w of this.wallets.values()) total += w.usdt + w.btc * btcRef;
|
||||
return total;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user