Add Binance exchange support

- Updated the environment configuration to include Binance as a selectable exchange option.
- Enhanced the README documentation to reflect the addition of Binance.
- Implemented the Binance exchange adapter and integrated it into the existing exchange framework.
- Modified the basis arbitrage strategy to support Binance alongside existing exchanges.
- Added tests to ensure proper functionality and integration of Binance within the trading system.
This commit is contained in:
discountry
2026-02-27 11:21:47 +08:00
parent 227484152d
commit 4205d5dc12
18 changed files with 2143 additions and 17 deletions
+12 -1
View File
@@ -2,12 +2,23 @@
LANG=zh
# Exchange selection
EXCHANGE=aster # Pick aster (default) or standx/grvt/lighter/backpack/paradex/nado
EXCHANGE=aster # Pick aster (default) or binance/standx/grvt/lighter/backpack/paradex/nado
# Aster API credentials
ASTER_API_KEY=
ASTER_API_SECRET=
# Binance API credentials (set when EXCHANGE=binance)
BINANCE_API_KEY=
BINANCE_API_SECRET=
BINANCE_SYMBOL=BTCUSDT # Trading symbol. Use BTCUSDT_PERP to force perpetual when ambiguous.
BINANCE_MARKET_TYPE=perp # perp | spot | auto
# BINANCE_SANDBOX=false
# BINANCE_SPOT_REST_URL=https://api.binance.com
# BINANCE_FUTURES_REST_URL=https://fapi.binance.com
# BINANCE_SPOT_WS_URL=wss://stream.binance.com:9443/ws
# BINANCE_FUTURES_WS_URL=wss://fstream.binance.com/ws
# StandX authentication (set when EXCHANGE=standx)
STANDX_TOKEN=
STANDX_SYMBOL=BTC-USD
+1 -1
View File
@@ -86,7 +86,7 @@ curl -fsSL https://github.com/discountry/ritmex-bot/raw/refs/heads/main/setup.sh
| 变量 | 说明 |
| --- | --- |
| `EXCHANGE` | 选择交易所(`aster`/`standx`/`grvt`/`lighter`/`backpack`/`paradex`/`nado` |
| `EXCHANGE` | 选择交易所(`aster`/`binance`/`standx`/`grvt`/`lighter`/`backpack`/`paradex`/`nado` |
| `TRADE_SYMBOL` | 交易对(默认 `BTCUSDT` |
| `TRADE_AMOUNT` | 单笔下单数量(标的资产计) |
| `LOSS_LIMIT` | 单笔最大亏损触发的强平额度(USDT) |
+1 -1
View File
@@ -82,7 +82,7 @@ The script installs Bun, project dependencies, collects Aster API credentials, g
| Variable | Purpose |
| --- | --- |
| `EXCHANGE` | Choose the venue (`aster` / `standx` / `grvt` / `lighter` / `backpack` / `paradex` / `nado`) |
| `EXCHANGE` | Choose the venue (`aster` / `binance` / `standx` / `grvt` / `lighter` / `backpack` / `paradex` / `nado`) |
| `TRADE_SYMBOL` | Contract symbol (defaults to `BTCUSDT`) |
| `TRADE_AMOUNT` | Order size in base asset units |
| `LOSS_LIMIT` | Max per-trade loss in USDT before forced close |
+4 -3
View File
@@ -4,7 +4,7 @@ export interface CliOptions {
strategy?: StrategyId;
silent: boolean;
help: boolean;
exchange?: "aster" | "grvt" | "lighter" | "backpack" | "paradex" | "nado" | "standx";
exchange?: "aster" | "grvt" | "lighter" | "backpack" | "paradex" | "nado" | "standx" | "binance";
}
const STRATEGY_VALUES = new Set<StrategyId>([
@@ -89,7 +89,8 @@ function assignExchange(options: CliOptions, raw: string): void {
normalized === "backpack" ||
normalized === "paradex" ||
normalized === "nado" ||
normalized === "standx"
normalized === "standx" ||
normalized === "binance"
) {
options.exchange = normalized as CliOptions["exchange"];
} else if (normalized === "gravity" || normalized === "grav" || normalized === "grv") {
@@ -99,7 +100,7 @@ function assignExchange(options: CliOptions, raw: string): void {
export function printCliHelp(): void {
// eslint-disable-next-line no-console
console.log(`Usage: bun run index.ts [--strategy <trend|swing|guardian|maker|maker-points|offset-maker|liquidity-maker|basis|grid>] [--exchange <aster|grvt|lighter|backpack|paradex|nado|standx>] [--silent]\n\n` +
console.log(`Usage: bun run index.ts [--strategy <trend|swing|guardian|maker|maker-points|offset-maker|liquidity-maker|basis|grid>] [--exchange <aster|grvt|lighter|backpack|paradex|nado|standx|binance>] [--silent]\n\n` +
`Options:\n` +
` --strategy, -s Automatically start the specified strategy without the interactive menu.\n` +
` Aliases: offset, offset-maker for the offset maker engine.\n` +
+2 -2
View File
@@ -141,8 +141,8 @@ const STRATEGY_FACTORIES: Record<StrategyId, StrategyRunner> = {
throw new Error("Basis arbitrage strategy is disabled. Set ENABLE_BASIS_STRATEGY=true to enable it.");
}
const exchangeId = resolveExchangeId();
if (exchangeId !== "aster" && exchangeId !== "nado" && exchangeId !== "standx") {
throw new Error("Basis arbitrage strategy currently only supports the Aster, Nado, and StandX exchanges");
if (exchangeId !== "aster" && exchangeId !== "nado" && exchangeId !== "standx" && exchangeId !== "binance") {
throw new Error("Basis arbitrage strategy currently only supports the Aster, Nado, StandX, and Binance exchanges");
}
const adapter = createAdapterOrThrow(basisConfig.futuresSymbol);
const engine = new BasisArbEngine(basisConfig, adapter);
+3
View File
@@ -97,6 +97,7 @@ const SYMBOL_PRIORITY_BY_EXCHANGE: Record<SupportedExchangeId, { envKeys: string
paradex: { envKeys: ["PARADEX_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC/USDC" },
nado: { envKeys: ["NADO_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-PERP" },
standx: { envKeys: ["STANDX_SYMBOL", "TRADE_SYMBOL"], fallback: "BTC-USD" },
binance: { envKeys: ["BINANCE_SYMBOL", "TRADE_SYMBOL"], fallback: "BTCUSDT" },
};
export function resolveSymbolFromEnv(explicitExchangeId?: SupportedExchangeId | string | null): string {
@@ -312,6 +313,7 @@ export const basisConfig: BasisArbConfig = {
const exchange = (process.env.EXCHANGE ?? "").trim().toLowerCase();
if (exchange === "nado") return "BTC-PERP";
if (exchange === "standx") return "BTC-USD";
if (exchange === "binance") return "BTCUSDT_PERP";
return "ASTERUSDT";
})()
),
@@ -321,6 +323,7 @@ export const basisConfig: BasisArbConfig = {
const exchange = (process.env.EXCHANGE ?? "").trim().toLowerCase();
if (exchange === "nado") return "KBTC";
if (exchange === "standx") return "BTC-USD";
if (exchange === "binance") return "BTCUSDT";
return "ASTERUSDT";
})()
),
+231
View File
@@ -0,0 +1,231 @@
import { clearTimeout, setTimeout } from "timers";
import type {
AccountListener,
DepthListener,
ExchangeAdapter,
ExchangePrecision,
FundingRateListener,
KlineListener,
OrderListener,
TickerListener,
} from "../adapter";
import type { AsterOrder, CreateOrderParams } from "../types";
import { extractMessage } from "../../utils/errors";
import { BinanceGateway, type BinanceGatewayOptions } from "./gateway";
export interface BinanceCredentials {
apiKey?: string;
apiSecret?: string;
symbol?: string;
marketType?: "spot" | "perp" | "auto";
sandbox?: boolean;
spotRestUrl?: string;
futuresRestUrl?: string;
spotWsUrl?: string;
futuresWsUrl?: string;
logger?: BinanceGatewayOptions["logger"];
}
export class BinanceExchangeAdapter implements ExchangeAdapter {
readonly id = "binance";
private readonly gateway: BinanceGateway;
private readonly symbol: string;
private readonly marketType: "spot" | "perp" | "auto";
private initPromise: Promise<void> | null = null;
private readonly initContexts = new Set<string>();
private retryTimer: ReturnType<typeof setTimeout> | null = null;
private retryDelayMs = 3000;
private lastInitErrorAt = 0;
constructor(credentials: BinanceCredentials = {}) {
const apiKey = credentials.apiKey ?? process.env.BINANCE_API_KEY;
const apiSecret = credentials.apiSecret ?? process.env.BINANCE_API_SECRET;
if (!apiKey || !apiSecret) {
throw new Error("Missing BINANCE_API_KEY or BINANCE_API_SECRET environment variable");
}
this.symbol = (credentials.symbol ?? process.env.BINANCE_SYMBOL ?? process.env.TRADE_SYMBOL ?? "BTCUSDT").trim().toUpperCase();
const modeRaw = (credentials.marketType ?? process.env.BINANCE_MARKET_TYPE ?? "perp").trim().toLowerCase();
this.marketType = modeRaw === "spot" ? "spot" : modeRaw === "auto" ? "auto" : "perp";
this.gateway = new BinanceGateway({
apiKey,
apiSecret,
symbol: this.symbol,
marketType: this.marketType,
sandbox: credentials.sandbox,
spotRestUrl: credentials.spotRestUrl,
futuresRestUrl: credentials.futuresRestUrl,
spotWsUrl: credentials.spotWsUrl,
futuresWsUrl: credentials.futuresWsUrl,
logger: credentials.logger,
});
}
supportsTrailingStops(): boolean {
return this.marketType !== "spot";
}
watchAccount(cb: AccountListener): void {
const safe = this.safeInvoke("watchAccount", cb);
void this.ensureInitialized("watchAccount")
.then(() => {
this.gateway.onAccount(safe);
})
.catch((error) => this.handleInitError("watchAccount", error));
}
watchOrders(cb: OrderListener): void {
const safe = this.safeInvoke("watchOrders", cb);
void this.ensureInitialized("watchOrders")
.then(() => {
this.gateway.onOrders(safe);
})
.catch((error) => this.handleInitError("watchOrders", error));
}
watchDepth(symbol: string, cb: DepthListener): void {
const safe = this.safeInvoke("watchDepth", cb);
void this.ensureInitialized(`watchDepth:${symbol}`)
.then(() => {
this.gateway.onDepth(symbol, safe);
})
.catch((error) => this.handleInitError("watchDepth", error));
}
watchTicker(symbol: string, cb: TickerListener): void {
const safe = this.safeInvoke("watchTicker", cb);
void this.ensureInitialized(`watchTicker:${symbol}`)
.then(() => {
this.gateway.onTicker(symbol, safe);
})
.catch((error) => this.handleInitError("watchTicker", error));
}
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
const safe = this.safeInvoke("watchKlines", cb);
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`)
.then(() => {
this.gateway.onKlines(symbol, interval, safe);
})
.catch((error) => this.handleInitError("watchKlines", error));
}
watchFundingRate(symbol: string, cb: FundingRateListener): void {
const safe = this.safeInvoke("watchFundingRate", cb);
void this.ensureInitialized(`watchFundingRate:${symbol}`)
.then(() => {
this.gateway.onFundingRate(symbol, safe);
})
.catch((error) => this.handleInitError("watchFundingRate", error));
}
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
await this.ensureInitialized("createOrder");
return this.gateway.createOrder(params);
}
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
await this.ensureInitialized("cancelOrder");
await this.gateway.cancelOrder(params);
}
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
await this.ensureInitialized("cancelOrders");
await this.gateway.cancelOrders(params);
}
async cancelAllOrders(params: { symbol: string }): Promise<void> {
await this.ensureInitialized("cancelAllOrders");
await this.gateway.cancelAllOrders(params);
}
async getPrecision(): Promise<ExchangePrecision | null> {
await this.ensureInitialized("getPrecision");
return this.gateway.getPrecision(this.symbol);
}
async queryOpenOrders(): Promise<AsterOrder[]> {
await this.ensureInitialized("queryOpenOrders");
return this.gateway.queryOpenOrders();
}
async queryAccountSnapshot() {
await this.ensureInitialized("queryAccountSnapshot");
return this.gateway.queryAccountSnapshot();
}
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
await this.ensureInitialized("changeMarginMode");
await this.gateway.changeMarginMode(params.symbol, params.marginMode);
}
async forceCancelAllOrders(): Promise<boolean> {
await this.ensureInitialized("forceCancelAllOrders");
return this.gateway.forceCancelAllOrders();
}
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
const wrapped = ((...args: any[]) => {
try {
cb(...args);
} catch (error) {
console.error(`[BinanceExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
}
}) as T;
return wrapped;
}
private ensureInitialized(context?: string): Promise<void> {
if (!this.initPromise) {
this.initContexts.clear();
this.initPromise = this.gateway
.ensureInitialized(this.symbol)
.then((value) => {
this.clearRetry();
return value;
})
.catch((error) => {
this.handleInitError("initialize", error);
this.initPromise = null;
this.scheduleRetry();
throw error;
});
}
if (context && !this.initContexts.has(context)) {
this.initContexts.add(context);
this.initPromise.catch((error) => {
this.handleInitError(context, error);
this.scheduleRetry();
});
}
return this.initPromise;
}
private scheduleRetry(): void {
if (this.retryTimer) return;
this.retryTimer = setTimeout(() => {
this.retryTimer = null;
if (this.initPromise) return;
this.retryDelayMs = Math.min(this.retryDelayMs * 2, 60_000);
void this.ensureInitialized("retry");
}, this.retryDelayMs);
}
private clearRetry(): void {
if (this.retryTimer) {
clearTimeout(this.retryTimer);
this.retryTimer = null;
}
this.retryDelayMs = 3000;
}
private handleInitError(context: string, error: unknown): void {
const now = Date.now();
if (now - this.lastInitErrorAt < 5000) return;
this.lastInitErrorAt = now;
console.error(`[BinanceExchangeAdapter] ${context} failed`, error);
}
}
File diff suppressed because it is too large Load Diff
+100
View File
@@ -0,0 +1,100 @@
import type { AsterOrder, CreateOrderParams } from "../types";
import type {
BaseOrderIntent,
ClosePositionIntent,
LimitOrderIntent,
MarketOrderIntent,
StopOrderIntent,
TrailingStopOrderIntent,
} from "../order-schema";
import { toStringBoolean } from "../order-schema";
function applyCommonFields(params: CreateOrderParams, intent: BaseOrderIntent): CreateOrderParams {
if (params.quantity === undefined) {
params.quantity = intent.quantity;
}
if (params.timeInForce === undefined && intent.timeInForce) {
params.timeInForce = intent.timeInForce;
}
if (intent.reduceOnly !== undefined) {
params.reduceOnly = toStringBoolean(intent.reduceOnly);
}
if (intent.closePosition !== undefined) {
params.closePosition = toStringBoolean(intent.closePosition);
}
return params;
}
export async function createLimitOrder(intent: LimitOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "LIMIT",
quantity: intent.quantity,
price: intent.price,
timeInForce: intent.timeInForce ?? "GTX",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createMarketOrder(intent: MarketOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createStopOrder(intent: StopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "STOP_MARKET",
quantity: intent.quantity,
stopPrice: intent.stopPrice,
timeInForce: intent.timeInForce ?? "GTC",
triggerType: intent.triggerType,
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createTrailingStopOrder(intent: TrailingStopOrderIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "TRAILING_STOP_MARKET",
quantity: intent.quantity,
activationPrice: intent.activationPrice,
callbackRate: intent.callbackRate,
timeInForce: intent.timeInForce ?? "GTC",
},
intent
);
return intent.adapter.createOrder(params);
}
export async function createClosePositionOrder(intent: ClosePositionIntent): Promise<AsterOrder> {
const params: CreateOrderParams = applyCommonFields(
{
symbol: intent.symbol,
side: intent.side,
type: "MARKET",
quantity: intent.quantity,
reduceOnly: "true",
},
intent
);
return intent.adapter.createOrder(params);
}
+9 -1
View File
@@ -6,6 +6,7 @@ import { BackpackExchangeAdapter, type BackpackCredentials } from "./backpack/ad
import { ParadexExchangeAdapter, type ParadexCredentials } from "./paradex/adapter";
import { NadoExchangeAdapter, type NadoCredentials } from "./nado/adapter";
import { StandxExchangeAdapter, type StandxCredentials } from "./standx/adapter";
import { BinanceExchangeAdapter, type BinanceCredentials } from "./binance/adapter";
export interface ExchangeFactoryOptions {
symbol: string;
@@ -17,6 +18,7 @@ export interface ExchangeFactoryOptions {
paradex?: ParadexCredentials;
nado?: NadoCredentials;
standx?: StandxCredentials;
binance?: BinanceCredentials;
}
export type SupportedExchangeId =
@@ -26,7 +28,8 @@ export type SupportedExchangeId =
| "backpack"
| "paradex"
| "nado"
| "standx";
| "standx"
| "binance";
export function resolveExchangeId(value?: string | null): SupportedExchangeId {
const fallback = (value ?? process.env.EXCHANGE ?? process.env.TRADE_EXCHANGE ?? "aster")
@@ -39,6 +42,7 @@ export function resolveExchangeId(value?: string | null): SupportedExchangeId {
if (fallback === "paradex") return "paradex";
if (fallback === "nado") return "nado";
if (fallback === "standx") return "standx";
if (fallback === "binance" || fallback === "bnb") return "binance";
return "aster";
}
@@ -49,6 +53,7 @@ export function getExchangeDisplayName(id: SupportedExchangeId): string {
if (id === "paradex") return "Paradex";
if (id === "nado") return "Nado";
if (id === "standx") return "StandX";
if (id === "binance") return "Binance";
return "AsterDex";
}
@@ -72,5 +77,8 @@ export function createExchangeAdapter(options: ExchangeFactoryOptions): Exchange
if (id === "standx") {
return new StandxExchangeAdapter({ ...options.standx, symbol: options.symbol });
}
if (id === "binance") {
return new BinanceExchangeAdapter({ ...options.binance, symbol: options.symbol });
}
return new AsterExchangeAdapter({ ...options.aster, symbol: options.symbol });
}
+10 -1
View File
@@ -15,8 +15,9 @@ import * as lighterOrders from "./lighter/order";
import * as paradexOrders from "./paradex/order";
import * as nadoOrders from "./nado/order";
import * as standxOrders from "./standx/order";
import * as binanceOrders from "./binance/order";
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex" | "nado" | "standx";
type ExchangeKey = "aster" | "backpack" | "grvt" | "lighter" | "paradex" | "nado" | "standx" | "binance";
interface ExchangeOrderHandlers {
limit(intent: LimitOrderIntent): Promise<AsterOrder>;
@@ -76,6 +77,13 @@ const handlerMap: Record<ExchangeKey, ExchangeOrderHandlers> = {
trailingStop: standxOrders.createTrailingStopOrder,
close: standxOrders.createClosePositionOrder,
},
binance: {
limit: binanceOrders.createLimitOrder,
market: binanceOrders.createMarketOrder,
stop: binanceOrders.createStopOrder,
trailingStop: binanceOrders.createTrailingStopOrder,
close: binanceOrders.createClosePositionOrder,
},
};
const knownExchanges: ExchangeKey[] = [
@@ -86,6 +94,7 @@ const knownExchanges: ExchangeKey[] = [
"paradex",
"nado",
"standx",
"binance",
];
function normalizeExchangeId(value: string | undefined | null): string | undefined {
+30
View File
@@ -6,6 +6,7 @@ import type { BackpackCredentials } from "./backpack/adapter";
import type { ParadexCredentials } from "./paradex/adapter";
import type { NadoCredentials } from "./nado/adapter";
import type { StandxCredentials } from "./standx/adapter";
import type { BinanceCredentials } from "./binance/adapter";
import { t } from "../i18n";
import type { Address } from "viem";
@@ -48,6 +49,11 @@ export function buildAdapterFromEnv(options: BuildAdapterOptions): ExchangeAdapt
return createExchangeAdapter({ exchange: id, symbol, standx: credentials });
}
if (id === "binance") {
const credentials = resolveBinanceCredentials(symbol);
return createExchangeAdapter({ exchange: id, symbol, binance: credentials });
}
return createExchangeAdapter({ exchange: id, symbol, grvt: { symbol } });
}
@@ -174,6 +180,30 @@ function resolveStandxCredentials(symbol: string): StandxCredentials {
};
}
function resolveBinanceCredentials(symbol: string): BinanceCredentials {
const apiKey = process.env.BINANCE_API_KEY;
const apiSecret = process.env.BINANCE_API_SECRET;
if (!apiKey || !apiSecret) {
throw new Error("Missing BINANCE_API_KEY or BINANCE_API_SECRET environment variable");
}
const marketTypeRaw = process.env.BINANCE_MARKET_TYPE?.trim().toLowerCase();
const marketType: BinanceCredentials["marketType"] =
marketTypeRaw === "spot" ? "spot" : marketTypeRaw === "auto" ? "auto" : "perp";
return {
apiKey,
apiSecret,
symbol: process.env.BINANCE_SYMBOL ?? symbol,
marketType,
sandbox: parseOptionalBoolean(process.env.BINANCE_SANDBOX),
spotRestUrl: process.env.BINANCE_SPOT_REST_URL ?? undefined,
futuresRestUrl: process.env.BINANCE_FUTURES_REST_URL ?? undefined,
spotWsUrl: process.env.BINANCE_SPOT_WS_URL ?? undefined,
futuresWsUrl: process.env.BINANCE_FUTURES_WS_URL ?? undefined,
};
}
function isHex32(value: string): boolean {
return /^0x[0-9a-fA-F]{64}$/.test(value.trim());
}
+2 -2
View File
@@ -313,8 +313,8 @@ const translations: Record<string, TranslationEntry> = {
"grid.direction.long": { zh: "多", en: "Long" },
"grid.direction.short": { zh: "空", en: "Short" },
"basis.onlyAster": {
zh: "期现套利策略目前仅支持 Aster / Nado / StandX 交易所。请设置 EXCHANGE=asterEXCHANGE=nadoEXCHANGE=standx 后重试。",
en: "Basis arbitrage currently supports only Aster, Nado, and StandX. Set EXCHANGE=aster, EXCHANGE=nado, or EXCHANGE=standx and retry.",
zh: "期现套利策略目前仅支持 Aster / Nado / StandX / Binance。请设置 EXCHANGE=asterEXCHANGE=nadoEXCHANGE=standx 或 EXCHANGE=binance 后重试。",
en: "Basis arbitrage currently supports Aster, Nado, StandX, and Binance. Set EXCHANGE=aster, EXCHANGE=nado, EXCHANGE=standx, or EXCHANGE=binance and retry.",
},
"basis.startFailed": {
zh: "无法启动期现套利策略: {message}",
+4 -3
View File
@@ -167,7 +167,7 @@ export class BasisArbEngine {
}
);
if (this.exchange.id === "nado" || this.exchange.id === "standx") {
if (this.exchange.id === "nado" || this.exchange.id === "standx" || this.exchange.id === "binance") {
safeSubscribe<AsterDepth>(
this.exchange.watchDepth.bind(this.exchange, this.config.spotSymbol),
(depth) => {
@@ -409,8 +409,9 @@ export class BasisArbEngine {
if (!Number.isFinite(wallet) || !Number.isFinite(available)) continue;
if (Math.abs(wallet) === 0 && Math.abs(available) === 0) continue;
if (name === "USDT0") {
futuresBalances.push({ asset: name, wallet, available });
const isTaggedFuturesAsset = /0$/.test(name);
if (isTaggedFuturesAsset) {
futuresBalances.push({ asset: name.replace(/0$/, ""), wallet, available });
continue;
}
const locked = Math.max(wallet - available, 0);
+1 -1
View File
@@ -31,7 +31,7 @@ export function BasisApp({ onExit }: BasisAppProps) {
);
useEffect(() => {
if (exchangeId !== "aster" && exchangeId !== "nado" && exchangeId !== "standx") {
if (exchangeId !== "aster" && exchangeId !== "nado" && exchangeId !== "standx" && exchangeId !== "binance") {
setError(new Error(t("basis.onlyAster")));
return;
}
+10 -1
View File
@@ -71,6 +71,13 @@ describe("BasisArbEngine", () => {
time: 2_000,
}),
};
const futuresClient = {
getPremiumIndex: vi.fn().mockResolvedValue({
fundingRate: "0.0001",
nextFundingTime: 3_600_000,
time: 2_000,
}),
};
const engine = new BasisArbEngine(
{
@@ -79,10 +86,12 @@ describe("BasisArbEngine", () => {
refreshIntervalMs: 1_000,
maxLogEntries: 10,
takerFeeRate: 0.0004,
arbAmount: 1,
},
adapter,
{
spotClient,
futuresClient,
now: () => 1_000,
}
);
@@ -111,7 +120,7 @@ describe("BasisArbEngine", () => {
const expectedNet = 1.04 * (1 - effectiveFee) - 1.05 * (1 + effectiveFee);
expect(snapshot.netSpread).toBeCloseTo(expectedNet, 6);
expect(snapshot.netSpreadBps).toBeCloseTo((expectedNet / 1.05) * 10_000, 6);
expect(snapshot.feedStatus).toEqual({ futures: true, spot: true });
expect(snapshot.feedStatus).toEqual({ futures: true, spot: true, funding: true });
expect(snapshot.opportunity).toBe(expectedNet >= 0);
engine.stop();
+7
View File
@@ -49,4 +49,11 @@ describe("resolveSymbolFromEnv", () => {
expect(resolveSymbolFromEnv("standx")).toBe("ETH-USD");
});
it("supports binance symbol defaults when explicit exchange id is provided", () => {
delete process.env.EXCHANGE;
process.env.BINANCE_SYMBOL = "ETHUSDT";
expect(resolveSymbolFromEnv("binance")).toBe("ETHUSDT");
});
});
+13
View File
@@ -5,6 +5,7 @@ import { GrvtExchangeAdapter } from "../src/exchanges/grvt/adapter";
import { BackpackExchangeAdapter } from "../src/exchanges/backpack/adapter";
import { ParadexExchangeAdapter } from "../src/exchanges/paradex/adapter";
import { StandxExchangeAdapter } from "../src/exchanges/standx/adapter";
import { BinanceExchangeAdapter } from "../src/exchanges/binance/adapter";
const ORIGINAL_ENV = { ...process.env };
@@ -32,6 +33,7 @@ describe("exchange factory", () => {
expect(resolveExchangeId("BACKPACK")).toBe("backpack");
expect(resolveExchangeId("PaRaDeX")).toBe("paradex");
expect(resolveExchangeId("StandX")).toBe("standx");
expect(resolveExchangeId("BiNaNcE")).toBe("binance");
});
it("creates grvt adapter when EXCHANGE=grvt", () => {
@@ -79,4 +81,15 @@ describe("exchange factory", () => {
expect(adapter).toBeInstanceOf(StandxExchangeAdapter);
expect(adapter.id).toBe("standx");
});
it("creates binance adapter when EXCHANGE=binance", () => {
process.env.EXCHANGE = "binance";
process.env.BINANCE_API_KEY = "api-key";
process.env.BINANCE_API_SECRET = "api-secret";
process.env.BINANCE_SYMBOL = "BTCUSDT";
const adapter = createExchangeAdapter({ symbol: "BTCUSDT" });
expect(adapter).toBeInstanceOf(BinanceExchangeAdapter);
expect(adapter.id).toBe("binance");
});
});