mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-10 00:38:07 +00:00
Feat/support binance (#22)
* add docs * 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. * Enhance README with detailed Binance exchange configuration - Added comprehensive instructions for setting up Binance as an exchange option. - Included environment variable specifications for API keys, market types, and trading symbols. - Provided examples for both perpetual and spot trading strategies. - Clarified the use of WebSocket and REST for the Binance adapter. * Enhance exchange support and testing framework - Added a new test suite for exchange contracts to ensure consistency and functionality across supported exchanges. - Refactored exchange ID handling to utilize a centralized list of supported exchanges, improving maintainability. - Updated CLI argument parsing and help documentation to reflect the new exchange structure. - Introduced utility functions for validating supported exchanges and their display names. - Enhanced the BasisApp and strategy runner to leverage the new exchange validation logic. - Added a new test command for running exchange-related tests. * Refactor exchange contract tests and update CLI commands - Removed the trailing supported exchanges set and simplified the logic for trailing stop support in the exchange contract tests. - Updated the test command for exchange contracts to exclude unnecessary tests, streamlining the testing process. - Enhanced test descriptions for clarity and improved understanding of the functionality being tested.
This commit is contained in:
@@ -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
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user