mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 08:18:07 +00:00
refactor: extract shared adapter utilities (safeInvoke + initManager)
Created src/exchanges/adapter-utils.ts with createSafeInvoke() and createInitManager() factories. Converted 7/8 adapters to use shared utilities, eliminating ~350 lines of duplicated retry/init/error-handling boilerplate. Lighter adapter kept as-is (no retry logic by design). Also translated Chinese comments in adapter.ts and order-schema.ts.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { extractMessage } from "../utils/errors";
|
||||
|
||||
/**
|
||||
* Wrap a callback so exceptions inside it are swallowed and logged.
|
||||
* Used by adapter watch* methods to prevent gateway callback errors
|
||||
* from killing the event loop.
|
||||
*/
|
||||
export function createSafeInvoke(adapterName: string) {
|
||||
return function safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
||||
const wrapped = ((...args: any[]) => {
|
||||
try {
|
||||
cb(...args);
|
||||
} catch (error) {
|
||||
console.error(`[${adapterName}] ${context} handler failed: ${extractMessage(error)}`);
|
||||
}
|
||||
}) as T;
|
||||
return wrapped;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InitManager {
|
||||
ensureInitialized(context?: string): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reusable init-once-with-retry manager.
|
||||
* Every adapter uses the same pattern: cache the init promise, retry on
|
||||
* failure with exponential back-off, and deduplicate context error logs.
|
||||
*/
|
||||
export function createInitManager(
|
||||
adapterName: string,
|
||||
doInitialize: () => Promise<void>,
|
||||
): InitManager {
|
||||
let initPromise: Promise<void> | null = null;
|
||||
const initContexts = new Set<string>();
|
||||
let retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let retryDelayMs = 3000;
|
||||
let lastInitErrorAt = 0;
|
||||
|
||||
function clearRetry(): void {
|
||||
if (retryTimer) {
|
||||
clearTimeout(retryTimer);
|
||||
retryTimer = null;
|
||||
}
|
||||
retryDelayMs = 3000;
|
||||
}
|
||||
|
||||
function handleInitError(context: string, error: unknown): void {
|
||||
const now = Date.now();
|
||||
if (now - lastInitErrorAt < 5000) return;
|
||||
lastInitErrorAt = now;
|
||||
console.error(`[${adapterName}] ${context} failed`, error);
|
||||
}
|
||||
|
||||
function scheduleRetry(): void {
|
||||
if (retryTimer) return;
|
||||
retryTimer = setTimeout(() => {
|
||||
retryTimer = null;
|
||||
if (initPromise) return;
|
||||
retryDelayMs = Math.min(retryDelayMs * 2, 60_000);
|
||||
void ensureInitialized("retry");
|
||||
}, retryDelayMs);
|
||||
}
|
||||
|
||||
function ensureInitialized(context?: string): Promise<void> {
|
||||
if (!initPromise) {
|
||||
initContexts.clear();
|
||||
initPromise = doInitialize()
|
||||
.then((value) => {
|
||||
clearRetry();
|
||||
return value;
|
||||
})
|
||||
.catch((error) => {
|
||||
handleInitError("initialize", error);
|
||||
initPromise = null;
|
||||
scheduleRetry();
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
if (context && !initContexts.has(context)) {
|
||||
initContexts.add(context);
|
||||
initPromise.catch((error) => {
|
||||
handleInitError(context, error);
|
||||
scheduleRetry();
|
||||
});
|
||||
}
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
return { ensureInitialized };
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export interface ExchangeAdapter {
|
||||
cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void>;
|
||||
cancelAllOrders(params: { symbol: string }): Promise<void>;
|
||||
getPrecision?(): Promise<ExchangePrecision | null>;
|
||||
// 连接保护相关方法(可选,仅 StandX 支持)
|
||||
// Connection protection methods (optional)
|
||||
onConnectionEvent?(listener: ConnectionEventListener): void;
|
||||
offConnectionEvent?(listener: ConnectionEventListener): void;
|
||||
onRestHealthEvent?(listener: RestHealthListener): void;
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { Order, CreateOrderParams, Depth, Ticker, Kline } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||
import { AsterGateway } from "./gateway";
|
||||
|
||||
export interface AsterCredentials {
|
||||
@@ -21,132 +21,73 @@ export class AsterExchangeAdapter implements ExchangeAdapter {
|
||||
readonly id = "aster";
|
||||
private readonly gateway: AsterGateway;
|
||||
private readonly symbol: string;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private lastInitErrorAt = 0;
|
||||
private readonly initContexts = new Set<string>();
|
||||
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private retryDelayMs = 3000;
|
||||
private readonly safeInvoke = createSafeInvoke("AsterExchangeAdapter");
|
||||
private readonly init: ReturnType<typeof createInitManager>;
|
||||
|
||||
constructor(credentials: AsterCredentials = {}) {
|
||||
this.gateway = new AsterGateway({ apiKey: credentials.apiKey, apiSecret: credentials.apiSecret });
|
||||
this.symbol = (credentials.symbol ?? process.env.TRADE_SYMBOL ?? "BTCUSDT").toUpperCase();
|
||||
this.init = createInitManager("AsterExchangeAdapter", () =>
|
||||
this.gateway.ensureInitialized(this.symbol),
|
||||
);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
||||
const wrapped = ((...args: any[]) => {
|
||||
try {
|
||||
cb(...args);
|
||||
} catch (error) {
|
||||
console.error(`[AsterExchangeAdapter] ${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(`[AsterExchangeAdapter] ${context} failed`, error);
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.ensureInitialized("watchAccount");
|
||||
void this.init.ensureInitialized("watchAccount");
|
||||
this.gateway.onAccount(this.safeInvoke("watchAccount", (snapshot) => {
|
||||
cb(snapshot);
|
||||
}));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.ensureInitialized("watchOrders");
|
||||
void this.init.ensureInitialized("watchOrders");
|
||||
this.gateway.onOrders(this.safeInvoke("watchOrders", (orders) => {
|
||||
cb(orders);
|
||||
}));
|
||||
}
|
||||
|
||||
watchDepth(symbol: string, cb: DepthListener): void {
|
||||
void this.ensureInitialized("watchDepth");
|
||||
void this.init.ensureInitialized("watchDepth");
|
||||
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", (depth: Depth) => {
|
||||
cb(depth);
|
||||
}));
|
||||
}
|
||||
|
||||
watchTicker(symbol: string, cb: TickerListener): void {
|
||||
void this.ensureInitialized("watchTicker");
|
||||
void this.init.ensureInitialized("watchTicker");
|
||||
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", (ticker: Ticker) => {
|
||||
cb(ticker);
|
||||
}));
|
||||
}
|
||||
|
||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||
void this.ensureInitialized("watchKlines");
|
||||
void this.init.ensureInitialized("watchKlines");
|
||||
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", (klines: Kline[]) => {
|
||||
cb(klines);
|
||||
}));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
await this.ensureInitialized("createOrder");
|
||||
await this.init.ensureInitialized("createOrder");
|
||||
return this.gateway.createOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrder");
|
||||
await this.init.ensureInitialized("cancelOrder");
|
||||
await this.gateway.cancelOrder({ symbol: params.symbol, orderId: Number(params.orderId) });
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrders");
|
||||
await this.init.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders({ symbol: params.symbol, orderIdList: params.orderIdList });
|
||||
}
|
||||
|
||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
await this.init.ensureInitialized("cancelAllOrders");
|
||||
await this.gateway.cancelAllOrders(params);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
import type { Order, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { BackpackGateway, type BackpackGatewayOptions } from "./gateway";
|
||||
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||
|
||||
export interface BackpackCredentials {
|
||||
apiKey?: string;
|
||||
@@ -23,11 +24,8 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
|
||||
readonly id = "backpack";
|
||||
private readonly gateway: BackpackGateway;
|
||||
private readonly symbol: string;
|
||||
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;
|
||||
private readonly safeInvoke = createSafeInvoke("BackpackExchangeAdapter");
|
||||
private readonly init: ReturnType<typeof createInitManager>;
|
||||
|
||||
constructor(credentials: BackpackCredentials = {}) {
|
||||
const apiKey = credentials.apiKey ?? process.env.BACKPACK_API_KEY;
|
||||
@@ -54,6 +52,9 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
|
||||
|
||||
this.gateway = new BackpackGateway(gatewayOptions);
|
||||
this.symbol = symbol;
|
||||
this.init = createInitManager("BackpackExchangeAdapter", () =>
|
||||
this.gateway.ensureInitialized(this.symbol),
|
||||
);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
@@ -61,118 +62,50 @@ export class BackpackExchangeAdapter implements ExchangeAdapter {
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.ensureInitialized("watchAccount");
|
||||
void this.init.ensureInitialized("watchAccount");
|
||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.ensureInitialized("watchOrders");
|
||||
void this.init.ensureInitialized("watchOrders");
|
||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||
}
|
||||
|
||||
watchDepth(_symbol: string, cb: DepthListener): void {
|
||||
void this.ensureInitialized("watchDepth");
|
||||
void this.init.ensureInitialized("watchDepth");
|
||||
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
||||
}
|
||||
|
||||
watchTicker(_symbol: string, cb: TickerListener): void {
|
||||
void this.ensureInitialized("watchTicker");
|
||||
void this.init.ensureInitialized("watchTicker");
|
||||
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
||||
}
|
||||
|
||||
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
|
||||
void this.ensureInitialized(`watchKlines:${interval}`);
|
||||
void this.init.ensureInitialized(`watchKlines:${interval}`);
|
||||
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
await this.ensureInitialized("createOrder");
|
||||
await this.init.ensureInitialized("createOrder");
|
||||
return this.gateway.createOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrder");
|
||||
await this.init.ensureInitialized("cancelOrder");
|
||||
await this.gateway.cancelOrder({ orderId: params.orderId });
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrders");
|
||||
await this.init.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders({ orderIdList: params.orderIdList });
|
||||
}
|
||||
|
||||
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
await this.init.ensureInitialized("cancelAllOrders");
|
||||
await this.gateway.cancelAllOrders();
|
||||
}
|
||||
|
||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
||||
const wrapped = ((...args: any[]) => {
|
||||
try {
|
||||
cb(...args);
|
||||
} catch (error) {
|
||||
console.error(`[BackpackExchangeAdapter] ${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) => {
|
||||
if (process.env.BACKPACK_DEBUG === "1") {
|
||||
console.error(`[BackpackExchangeAdapter] initialize succeeded`);
|
||||
}
|
||||
if (process.env.BACKPACK_DEBUG === "1") {
|
||||
console.error(`[BackpackExchangeAdapter] initialize succeeded`);
|
||||
}
|
||||
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(`[BackpackExchangeAdapter] ${context} failed`, error);
|
||||
}
|
||||
|
||||
private logError(context: string, error: unknown): void {
|
||||
if (process.env.BACKPACK_DEBUG === "1" || process.env.BACKPACK_DEBUG === "true") {
|
||||
console.error(`[BackpackExchangeAdapter] ${context} failed: ${extractMessage(error)}`);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { clearTimeout, setTimeout } from "timers";
|
||||
import type {
|
||||
AccountListener,
|
||||
DepthListener,
|
||||
@@ -10,8 +9,8 @@ import type {
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { Order, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { BinanceGateway, type BinanceGatewayOptions } from "./gateway";
|
||||
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||
|
||||
export interface BinanceCredentials {
|
||||
apiKey?: string;
|
||||
@@ -32,11 +31,8 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
|
||||
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;
|
||||
private readonly safeInvoke = createSafeInvoke("BinanceExchangeAdapter");
|
||||
private readonly init: ReturnType<typeof createInitManager>;
|
||||
|
||||
constructor(credentials: BinanceCredentials = {}) {
|
||||
const apiKey = credentials.apiKey ?? process.env.BINANCE_API_KEY;
|
||||
@@ -61,6 +57,9 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
|
||||
futuresWsUrl: credentials.futuresWsUrl,
|
||||
logger: credentials.logger,
|
||||
});
|
||||
this.init = createInitManager("BinanceExchangeAdapter", () =>
|
||||
this.gateway.ensureInitialized(this.symbol),
|
||||
);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
@@ -69,163 +68,94 @@ export class BinanceExchangeAdapter implements ExchangeAdapter {
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
const safe = this.safeInvoke("watchAccount", cb);
|
||||
void this.ensureInitialized("watchAccount")
|
||||
void this.init.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")
|
||||
void this.init.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}`)
|
||||
void this.init.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}`)
|
||||
void this.init.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}`)
|
||||
void this.init.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}`)
|
||||
void this.init.ensureInitialized(`watchFundingRate:${symbol}`)
|
||||
.then(() => {
|
||||
this.gateway.onFundingRate(symbol, safe);
|
||||
})
|
||||
.catch((error) => this.handleInitError("watchFundingRate", error));
|
||||
});
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
await this.ensureInitialized("createOrder");
|
||||
await this.init.ensureInitialized("createOrder");
|
||||
return this.gateway.createOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrder");
|
||||
await this.init.ensureInitialized("cancelOrder");
|
||||
await this.gateway.cancelOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrders");
|
||||
await this.init.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders(params);
|
||||
}
|
||||
|
||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
await this.init.ensureInitialized("cancelAllOrders");
|
||||
await this.gateway.cancelAllOrders(params);
|
||||
}
|
||||
|
||||
async getPrecision(): Promise<ExchangePrecision | null> {
|
||||
await this.ensureInitialized("getPrecision");
|
||||
await this.init.ensureInitialized("getPrecision");
|
||||
return this.gateway.getPrecision(this.symbol);
|
||||
}
|
||||
|
||||
async queryOpenOrders(): Promise<Order[]> {
|
||||
await this.ensureInitialized("queryOpenOrders");
|
||||
await this.init.ensureInitialized("queryOpenOrders");
|
||||
return this.gateway.queryOpenOrders();
|
||||
}
|
||||
|
||||
async queryAccountSnapshot() {
|
||||
await this.ensureInitialized("queryAccountSnapshot");
|
||||
await this.init.ensureInitialized("queryAccountSnapshot");
|
||||
return this.gateway.queryAccountSnapshot();
|
||||
}
|
||||
|
||||
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
|
||||
await this.ensureInitialized("changeMarginMode");
|
||||
await this.init.ensureInitialized("changeMarginMode");
|
||||
await this.gateway.changeMarginMode(params.symbol, params.marginMode);
|
||||
}
|
||||
|
||||
async forceCancelAllOrders(): Promise<boolean> {
|
||||
await this.ensureInitialized("forceCancelAllOrders");
|
||||
await this.init.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { setTimeout, clearTimeout } from "timers";
|
||||
import path from "path";
|
||||
import { createRequire } from "module";
|
||||
import type {
|
||||
@@ -10,7 +9,6 @@ import type {
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { Order, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import {
|
||||
GrvtGateway,
|
||||
type GrvtEnvironment,
|
||||
@@ -18,6 +16,7 @@ import {
|
||||
type GrvtHostsOverride,
|
||||
type GrvtSignatureProvider,
|
||||
} from "./gateway";
|
||||
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||
|
||||
export interface GrvtCredentials {
|
||||
cookie?: string;
|
||||
@@ -40,11 +39,8 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
|
||||
private readonly gateway: GrvtGateway;
|
||||
private readonly symbol: string;
|
||||
private readonly instrument: string;
|
||||
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;
|
||||
private readonly safeInvoke = createSafeInvoke("GrvtExchangeAdapter");
|
||||
private readonly init: ReturnType<typeof createInitManager>;
|
||||
private klineInterval = "1m";
|
||||
|
||||
constructor(credentials: GrvtCredentials = {}) {
|
||||
@@ -91,6 +87,9 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
|
||||
pollIntervals: credentials.pollIntervals,
|
||||
logger: credentials.logger,
|
||||
});
|
||||
this.init = createInitManager("GrvtExchangeAdapter", () =>
|
||||
this.gateway.ensureInitialized(this.klineInterval),
|
||||
);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
@@ -98,115 +97,50 @@ export class GrvtExchangeAdapter implements ExchangeAdapter {
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.ensureInitialized("watchAccount");
|
||||
void this.init.ensureInitialized("watchAccount");
|
||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.ensureInitialized("watchOrders");
|
||||
void this.init.ensureInitialized("watchOrders");
|
||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||
}
|
||||
|
||||
watchDepth(_symbol: string, cb: DepthListener): void {
|
||||
void this.ensureInitialized("watchDepth");
|
||||
void this.init.ensureInitialized("watchDepth");
|
||||
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
||||
}
|
||||
|
||||
watchTicker(_symbol: string, cb: TickerListener): void {
|
||||
void this.ensureInitialized("watchTicker");
|
||||
void this.init.ensureInitialized("watchTicker");
|
||||
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
||||
}
|
||||
|
||||
watchKlines(_symbol: string, interval: string, cb: KlineListener): void {
|
||||
this.klineInterval = interval ?? this.klineInterval;
|
||||
void this.ensureInitialized("watchKlines", this.klineInterval);
|
||||
void this.init.ensureInitialized("watchKlines");
|
||||
this.gateway.onKlines(this.safeInvoke("watchKlines", cb));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
await this.ensureInitialized("createOrder");
|
||||
await this.init.ensureInitialized("createOrder");
|
||||
return this.gateway.createOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrder");
|
||||
await this.init.ensureInitialized("cancelOrder");
|
||||
await this.gateway.cancelOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrders");
|
||||
await this.init.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders(params);
|
||||
}
|
||||
|
||||
async cancelAllOrders(_params: { symbol: string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
await this.init.ensureInitialized("cancelAllOrders");
|
||||
await this.gateway.cancelAllOrders();
|
||||
}
|
||||
|
||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
||||
const wrapped = ((...args: any[]) => {
|
||||
try {
|
||||
cb(...args);
|
||||
} catch (error) {
|
||||
console.error(`[GrvtExchangeAdapter] ${context} handler failed: ${extractMessage(error)}`);
|
||||
}
|
||||
}) as T;
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
private ensureInitialized(context?: string, interval?: string): Promise<void> {
|
||||
if (interval) {
|
||||
this.klineInterval = interval;
|
||||
}
|
||||
if (!this.initPromise) {
|
||||
this.initContexts.clear();
|
||||
this.initPromise = this.gateway
|
||||
.ensureInitialized(this.klineInterval)
|
||||
.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(`[GrvtExchangeAdapter] ${context} failed`, error);
|
||||
}
|
||||
}
|
||||
|
||||
function requireValue<T>(value: T | undefined | null, key: string): T {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { setTimeout, clearTimeout } from "timers";
|
||||
import type {
|
||||
AccountListener,
|
||||
DepthListener,
|
||||
@@ -12,6 +11,7 @@ import type {
|
||||
import type { Order, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { NadoGateway, type NadoGatewayOptions } from "./gateway";
|
||||
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||
import type { ChainEnv } from "@nadohq/shared";
|
||||
import type { Address } from "viem";
|
||||
|
||||
@@ -35,11 +35,8 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
|
||||
|
||||
private readonly gateway: NadoGateway;
|
||||
private readonly symbol: string;
|
||||
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;
|
||||
private readonly safeInvoke = createSafeInvoke("NadoExchangeAdapter");
|
||||
private readonly init: ReturnType<typeof createInitManager>;
|
||||
|
||||
constructor(credentials: NadoCredentials = {}) {
|
||||
const signerPrivateKey = credentials.signerPrivateKey ?? process.env.NADO_SIGNER_PRIVATE_KEY;
|
||||
@@ -79,6 +76,9 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
|
||||
(process.env.NADO_STOP_TRIGGER_SOURCE as NadoGatewayOptions["stopTriggerSource"] | undefined),
|
||||
logger: (context, error) => this.logError(context, error),
|
||||
});
|
||||
this.init = createInitManager("NadoExchangeAdapter", () =>
|
||||
this.gateway.ensureInitialized(this.symbol),
|
||||
);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
@@ -86,52 +86,52 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.ensureInitialized("watchAccount");
|
||||
void this.init.ensureInitialized("watchAccount");
|
||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.ensureInitialized("watchOrders");
|
||||
void this.init.ensureInitialized("watchOrders");
|
||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||
}
|
||||
|
||||
watchDepth(symbol: string, cb: DepthListener): void {
|
||||
void this.ensureInitialized(`watchDepth:${symbol}`);
|
||||
void this.init.ensureInitialized(`watchDepth:${symbol}`);
|
||||
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
|
||||
}
|
||||
|
||||
watchTicker(symbol: string, cb: TickerListener): void {
|
||||
void this.ensureInitialized(`watchTicker:${symbol}`);
|
||||
void this.init.ensureInitialized(`watchTicker:${symbol}`);
|
||||
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
|
||||
}
|
||||
|
||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`);
|
||||
void this.init.ensureInitialized(`watchKlines:${symbol}:${interval}`);
|
||||
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
|
||||
}
|
||||
|
||||
watchFundingRate(symbol: string, cb: FundingRateListener): void {
|
||||
void this.ensureInitialized(`watchFundingRate:${symbol}`);
|
||||
void this.init.ensureInitialized(`watchFundingRate:${symbol}`);
|
||||
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
await this.ensureInitialized("createOrder");
|
||||
await this.init.ensureInitialized("createOrder");
|
||||
return this.gateway.createOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrder");
|
||||
await this.init.ensureInitialized("cancelOrder");
|
||||
await this.gateway.cancelOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrders");
|
||||
await this.init.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders(params);
|
||||
}
|
||||
|
||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
await this.init.ensureInitialized("cancelAllOrders");
|
||||
await this.gateway.cancelAllOrders(params);
|
||||
}
|
||||
|
||||
@@ -144,70 +144,6 @@ export class NadoExchangeAdapter implements ExchangeAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
||||
const wrapped = ((...args: any[]) => {
|
||||
try {
|
||||
cb(...args);
|
||||
} catch (error) {
|
||||
console.error(`[NadoExchangeAdapter] ${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(`[NadoExchangeAdapter] ${context} failed`, error);
|
||||
}
|
||||
|
||||
private logError(context: string, error: unknown): void {
|
||||
const detail = extractMessage(error);
|
||||
const message = `[NadoExchangeAdapter] ${context} failed: ${detail}`;
|
||||
|
||||
@@ -13,9 +13,8 @@ export interface BaseOrderIntent {
|
||||
|
||||
export interface LimitOrderIntent extends BaseOrderIntent {
|
||||
price: number;
|
||||
// StandX TPSL 参数
|
||||
slPrice?: number; // 止损价格
|
||||
tpPrice?: number; // 止盈价格
|
||||
slPrice?: number;
|
||||
tpPrice?: number;
|
||||
}
|
||||
|
||||
export interface MarketOrderIntent extends BaseOrderIntent {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { setTimeout, clearTimeout } from "timers";
|
||||
import type {
|
||||
AccountListener,
|
||||
DepthListener,
|
||||
@@ -10,6 +9,7 @@ import type {
|
||||
import type { Order, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { ParadexGateway, type ParadexGatewayOptions } from "./gateway";
|
||||
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||
|
||||
export interface ParadexCredentials {
|
||||
privateKey?: string;
|
||||
@@ -26,11 +26,8 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
|
||||
|
||||
private readonly gateway: ParadexGateway;
|
||||
private readonly symbol: string;
|
||||
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;
|
||||
private readonly safeInvoke = createSafeInvoke("ParadexExchangeAdapter");
|
||||
private readonly init: ReturnType<typeof createInitManager>;
|
||||
|
||||
constructor(credentials: ParadexCredentials = {}) {
|
||||
const privateKey = credentials.privateKey ?? process.env.PARADEX_PRIVATE_KEY;
|
||||
@@ -54,6 +51,9 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
|
||||
});
|
||||
|
||||
this.symbol = symbol;
|
||||
this.init = createInitManager("ParadexExchangeAdapter", () =>
|
||||
this.gateway.ensureInitialized(this.symbol),
|
||||
);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
@@ -61,114 +61,50 @@ export class ParadexExchangeAdapter implements ExchangeAdapter {
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.ensureInitialized("watchAccount");
|
||||
void this.init.ensureInitialized("watchAccount");
|
||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.ensureInitialized("watchOrders");
|
||||
void this.init.ensureInitialized("watchOrders");
|
||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||
}
|
||||
|
||||
watchDepth(symbol: string, cb: DepthListener): void {
|
||||
void this.ensureInitialized(`watchDepth:${symbol}`);
|
||||
void this.init.ensureInitialized(`watchDepth:${symbol}`);
|
||||
this.gateway.onDepth(this.safeInvoke("watchDepth", cb));
|
||||
}
|
||||
|
||||
watchTicker(symbol: string, cb: TickerListener): void {
|
||||
void this.ensureInitialized(`watchTicker:${symbol}`);
|
||||
void this.init.ensureInitialized(`watchTicker:${symbol}`);
|
||||
this.gateway.onTicker(this.safeInvoke("watchTicker", cb));
|
||||
}
|
||||
|
||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||
void this.ensureInitialized(`watchKlines:${symbol}:${interval}`);
|
||||
void this.init.ensureInitialized(`watchKlines:${symbol}:${interval}`);
|
||||
this.gateway.watchKlines(interval, this.safeInvoke("watchKlines", cb));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
await this.ensureInitialized("createOrder");
|
||||
await this.init.ensureInitialized("createOrder");
|
||||
return this.gateway.createOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrder");
|
||||
await this.init.ensureInitialized("cancelOrder");
|
||||
await this.gateway.cancelOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrders");
|
||||
await this.init.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders(params);
|
||||
}
|
||||
|
||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
await this.init.ensureInitialized("cancelAllOrders");
|
||||
await this.gateway.cancelAllOrders(params);
|
||||
}
|
||||
|
||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
||||
const wrapped = ((...args: any[]) => {
|
||||
try {
|
||||
cb(...args);
|
||||
} catch (error) {
|
||||
console.error(`[ParadexExchangeAdapter] ${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(`[ParadexExchangeAdapter] ${context} failed`, error);
|
||||
}
|
||||
|
||||
private logError(context: string, error: unknown): void {
|
||||
const detail = extractMessage(error);
|
||||
if (context === "initialize" && typeof error === "string" && /initialized/i.test(error)) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { setTimeout, clearTimeout } from "timers";
|
||||
import type {
|
||||
AccountListener,
|
||||
DepthListener,
|
||||
@@ -11,8 +10,8 @@ import type {
|
||||
TickerListener,
|
||||
} from "../adapter";
|
||||
import type { Order, CreateOrderParams } from "../types";
|
||||
import { extractMessage } from "../../utils/errors";
|
||||
import { StandxGateway, type StandxGatewayOptions, type ConnectionEventListener, type ConnectionEventType } from "./gateway";
|
||||
import { createSafeInvoke, createInitManager } from "../adapter-utils";
|
||||
|
||||
export type { ConnectionEventListener, ConnectionEventType };
|
||||
|
||||
@@ -31,11 +30,8 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
||||
|
||||
private readonly gateway: StandxGateway;
|
||||
private readonly symbol: string;
|
||||
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;
|
||||
private readonly safeInvoke = createSafeInvoke("StandxExchangeAdapter");
|
||||
private readonly init: ReturnType<typeof createInitManager>;
|
||||
|
||||
constructor(credentials: StandxCredentials = {}) {
|
||||
const token = credentials.token ?? process.env.STANDX_TOKEN;
|
||||
@@ -52,6 +48,9 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
||||
signingKey: credentials.signingKey,
|
||||
logger: credentials.logger,
|
||||
});
|
||||
this.init = createInitManager("StandxExchangeAdapter", () =>
|
||||
this.gateway.ensureInitialized(this.symbol),
|
||||
);
|
||||
}
|
||||
|
||||
supportsTrailingStops(): boolean {
|
||||
@@ -59,52 +58,52 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
||||
}
|
||||
|
||||
watchAccount(cb: AccountListener): void {
|
||||
void this.ensureInitialized("watchAccount");
|
||||
void this.init.ensureInitialized("watchAccount");
|
||||
this.gateway.onAccount(this.safeInvoke("watchAccount", cb));
|
||||
}
|
||||
|
||||
watchOrders(cb: OrderListener): void {
|
||||
void this.ensureInitialized("watchOrders");
|
||||
void this.init.ensureInitialized("watchOrders");
|
||||
this.gateway.onOrders(this.safeInvoke("watchOrders", cb));
|
||||
}
|
||||
|
||||
watchDepth(symbol: string, cb: DepthListener): void {
|
||||
void this.ensureInitialized("watchDepth");
|
||||
void this.init.ensureInitialized("watchDepth");
|
||||
this.gateway.onDepth(symbol, this.safeInvoke("watchDepth", cb));
|
||||
}
|
||||
|
||||
watchTicker(symbol: string, cb: TickerListener): void {
|
||||
void this.ensureInitialized("watchTicker");
|
||||
void this.init.ensureInitialized("watchTicker");
|
||||
this.gateway.onTicker(symbol, this.safeInvoke("watchTicker", cb));
|
||||
}
|
||||
|
||||
watchKlines(symbol: string, interval: string, cb: KlineListener): void {
|
||||
void this.ensureInitialized("watchKlines");
|
||||
void this.init.ensureInitialized("watchKlines");
|
||||
this.gateway.onKlines(symbol, interval, this.safeInvoke("watchKlines", cb));
|
||||
}
|
||||
|
||||
watchFundingRate(symbol: string, cb: FundingRateListener): void {
|
||||
void this.ensureInitialized("watchFundingRate");
|
||||
void this.init.ensureInitialized("watchFundingRate");
|
||||
this.gateway.onFundingRate(symbol, this.safeInvoke("watchFundingRate", cb));
|
||||
}
|
||||
|
||||
async createOrder(params: CreateOrderParams): Promise<Order> {
|
||||
await this.ensureInitialized("createOrder");
|
||||
await this.init.ensureInitialized("createOrder");
|
||||
return this.gateway.createOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrder(params: { symbol: string; orderId: number | string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrder");
|
||||
await this.init.ensureInitialized("cancelOrder");
|
||||
await this.gateway.cancelOrder(params);
|
||||
}
|
||||
|
||||
async cancelOrders(params: { symbol: string; orderIdList: Array<number | string> }): Promise<void> {
|
||||
await this.ensureInitialized("cancelOrders");
|
||||
await this.init.ensureInitialized("cancelOrders");
|
||||
await this.gateway.cancelOrders(params);
|
||||
}
|
||||
|
||||
async cancelAllOrders(params: { symbol: string }): Promise<void> {
|
||||
await this.ensureInitialized("cancelAllOrders");
|
||||
await this.init.ensureInitialized("cancelAllOrders");
|
||||
await this.gateway.cancelAllOrders(params);
|
||||
}
|
||||
|
||||
@@ -152,17 +151,17 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
||||
* 用于验证实际挂单情况,防止取消请求丢失
|
||||
*/
|
||||
async queryOpenOrders(): Promise<Order[]> {
|
||||
await this.ensureInitialized("queryOpenOrders");
|
||||
await this.init.ensureInitialized("queryOpenOrders");
|
||||
return this.gateway.queryOpenOrders(this.symbol);
|
||||
}
|
||||
|
||||
async queryAccountSnapshot() {
|
||||
await this.ensureInitialized("queryAccountSnapshot");
|
||||
await this.init.ensureInitialized("queryAccountSnapshot");
|
||||
return this.gateway.queryAccountSnapshot();
|
||||
}
|
||||
|
||||
async changeMarginMode(params: { symbol: string; marginMode: "isolated" | "cross" }): Promise<void> {
|
||||
await this.ensureInitialized("changeMarginMode");
|
||||
await this.init.ensureInitialized("changeMarginMode");
|
||||
await this.gateway.changeMarginMode(params.symbol, params.marginMode);
|
||||
}
|
||||
|
||||
@@ -171,69 +170,7 @@ export class StandxExchangeAdapter implements ExchangeAdapter {
|
||||
* 会查询当前挂单然后取消,并验证取消成功
|
||||
*/
|
||||
async forceCancelAllOrders(): Promise<boolean> {
|
||||
await this.ensureInitialized("forceCancelAllOrders");
|
||||
await this.init.ensureInitialized("forceCancelAllOrders");
|
||||
return this.gateway.forceCancelAllOrders(this.symbol);
|
||||
}
|
||||
|
||||
private safeInvoke<T extends (...args: any[]) => void>(context: string, cb: T): T {
|
||||
const wrapped = ((...args: any[]) => {
|
||||
try {
|
||||
cb(...args);
|
||||
} catch (error) {
|
||||
console.error(`[StandxExchangeAdapter] ${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(`[StandxExchangeAdapter] ${context} failed`, error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user