mirror of
https://github.com/discountry/ritmex-bot.git
synced 2026-09-09 16:28:06 +00:00
654 lines
22 KiB
TypeScript
654 lines
22 KiB
TypeScript
import ccxt, { type Balances, type Order as CcxtOrder, type OrderBook as CcxtOrderBook, type Ticker as CcxtTicker } from "ccxt";
|
|
import type {
|
|
AsterAccountSnapshot,
|
|
AsterAccountPosition,
|
|
AsterOrder,
|
|
AsterDepth,
|
|
AsterTicker,
|
|
AsterKline,
|
|
CreateOrderParams,
|
|
OrderType,
|
|
} from "../types";
|
|
import type {
|
|
AccountListener,
|
|
OrderListener,
|
|
DepthListener,
|
|
TickerListener,
|
|
KlineListener,
|
|
} from "../adapter";
|
|
|
|
export interface BackpackGatewayOptions {
|
|
apiKey?: string;
|
|
apiSecret?: string;
|
|
password?: string;
|
|
subaccount?: string;
|
|
symbol: string;
|
|
sandbox?: boolean;
|
|
logger?: (context: string, error: unknown) => void;
|
|
}
|
|
|
|
export class BackpackGateway {
|
|
private readonly exchange: any;
|
|
private readonly symbol: string;
|
|
private marketSymbol: string;
|
|
private market: any | null = null;
|
|
private isContractMarket = false;
|
|
private readonly logger: (context: string, error: unknown) => void;
|
|
private initialized = false;
|
|
private initPromise: Promise<void> | null = null;
|
|
|
|
// Event listeners
|
|
private accountListeners = new Set<AccountListener>();
|
|
private orderListeners = new Set<OrderListener>();
|
|
private depthListeners = new Set<DepthListener>();
|
|
private tickerListeners = new Set<TickerListener>();
|
|
private klineListeners = new Set<{ interval: string; callback: KlineListener }>();
|
|
|
|
// Polling intervals
|
|
private accountPollTimer: NodeJS.Timeout | null = null;
|
|
private orderPollTimer: NodeJS.Timeout | null = null;
|
|
private depthPollTimer: NodeJS.Timeout | null = null;
|
|
private tickerPollTimer: NodeJS.Timeout | null = null;
|
|
private klinePollTimers = new Map<string, NodeJS.Timeout>();
|
|
|
|
// WebSocket streams
|
|
private wsOrderBook: any = null;
|
|
private wsTicker: any = null;
|
|
private wsKlines = new Map<string, any>();
|
|
private wsOrders: any = null;
|
|
private wsBalance: any = null;
|
|
|
|
constructor(options: BackpackGatewayOptions) {
|
|
this.symbol = options.symbol.toUpperCase();
|
|
this.marketSymbol = this.symbol;
|
|
this.logger = options.logger ?? ((context, error) => console.error(`[BackpackGateway] ${context}:`, error));
|
|
|
|
// dynamic constructor for specific exchange
|
|
this.exchange = new (ccxt as any).backpack({
|
|
apiKey: options.apiKey,
|
|
secret: options.apiSecret,
|
|
password: options.password,
|
|
subaccount: options.subaccount,
|
|
sandbox: options.sandbox ?? false,
|
|
enableRateLimit: true,
|
|
timeout: 30000,
|
|
});
|
|
}
|
|
|
|
async ensureInitialized(symbol?: string): Promise<void> {
|
|
if (this.initialized) return;
|
|
|
|
if (this.initPromise) return this.initPromise;
|
|
|
|
this.initPromise = this.doInitialize(symbol);
|
|
return this.initPromise;
|
|
}
|
|
|
|
private async doInitialize(symbol?: string): Promise<void> {
|
|
try {
|
|
await this.exchange.loadMarkets();
|
|
|
|
// Verify symbol exists
|
|
const requested = (symbol ?? this.symbol).toUpperCase();
|
|
const resolved = this.resolveMarketSymbol(requested);
|
|
if (!resolved) {
|
|
throw new Error(`Symbol ${requested} not found in Backpack markets`);
|
|
}
|
|
this.marketSymbol = resolved;
|
|
this.market = this.exchange.market(this.marketSymbol);
|
|
this.isContractMarket = Boolean(this.market?.contract);
|
|
|
|
this.initialized = true;
|
|
this.logger("initialize", `Backpack gateway initialized for ${this.marketSymbol}`);
|
|
} catch (error) {
|
|
this.logger("initialize", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private resolveMarketSymbol(requested: string): string | null {
|
|
// normalize helpers (strip non-alphanumerics for robust comparisons)
|
|
const strip = (v: string | undefined | null) => (v ?? "").toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
|
|
// Backpack uses USDC quote; accept common USD/USDT aliases in user input
|
|
const normalizeUsdAlias = (v: string) => {
|
|
const up = v.toUpperCase();
|
|
// Replace ...USD... or ...USDT... (optionally before _ or PERP or end) with USDC
|
|
// Examples: BTCUSDPERP -> BTCUSDCPERP, BTCUSD -> BTCUSDC, BTC_USDT_PERP -> BTC_USDC_PERP
|
|
return up
|
|
.replace(/USDT(?=(?:[_-]?PERP)?$)/, "USDC")
|
|
.replace(/USD(?=(?:[_-]?PERP)?$)/, "USDC");
|
|
};
|
|
|
|
const requestedWithUsdc = normalizeUsdAlias(requested);
|
|
const compactRequested = strip(requestedWithUsdc);
|
|
|
|
// 1) exact key in markets (e.g. "BTC/USDC" or "BTC/USDC:USDC")
|
|
if (this.exchange.markets[requestedWithUsdc]) return requestedWithUsdc;
|
|
|
|
// 2) direct markets_by_id lookup by exact id
|
|
const byId = (this.exchange as any).markets_by_id ?? {};
|
|
if (byId[requestedWithUsdc]) return byId[requestedWithUsdc].symbol;
|
|
|
|
// 3) flexible lookup: compare compacted forms against ids, symbols, and base+quote
|
|
const markets = Object.values(this.exchange.markets) as Array<any>;
|
|
for (const m of markets) {
|
|
const idCompact = strip(m.id as string);
|
|
const symbolCompact = strip(m.symbol as string);
|
|
const baseQuoteCompact = strip((m.base as string) + (m.quote as string));
|
|
if (idCompact === compactRequested) return m.symbol;
|
|
if (symbolCompact === compactRequested) return m.symbol;
|
|
if (baseQuoteCompact === compactRequested) return m.symbol;
|
|
}
|
|
|
|
// 4) try matching against markets_by_id keys by compacted form
|
|
for (const key of Object.keys(byId)) {
|
|
if (strip(key) === compactRequested) return byId[key].symbol;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private normalizeTimeframe(interval: string): string {
|
|
const timeframeMap: Record<string, string> = {
|
|
"1m": "1m",
|
|
"5m": "5m",
|
|
"15m": "15m",
|
|
"1h": "1h",
|
|
"4h": "4h",
|
|
"1d": "1d",
|
|
};
|
|
return timeframeMap[interval] || "1m";
|
|
}
|
|
|
|
// Event subscription methods
|
|
onAccount(callback: AccountListener): void {
|
|
this.accountListeners.add(callback);
|
|
this.startAccountPolling();
|
|
}
|
|
|
|
onOrders(callback: OrderListener): void {
|
|
this.orderListeners.add(callback);
|
|
this.startOrderPolling();
|
|
}
|
|
|
|
onDepth(callback: DepthListener): void {
|
|
this.depthListeners.add(callback);
|
|
this.startDepthPolling();
|
|
}
|
|
|
|
onTicker(callback: TickerListener): void {
|
|
this.tickerListeners.add(callback);
|
|
this.startTickerPolling();
|
|
}
|
|
|
|
watchKlines(interval: string, callback: KlineListener): void {
|
|
const normalizedInterval = this.normalizeTimeframe(interval);
|
|
this.klineListeners.add({ interval: normalizedInterval, callback });
|
|
this.startKlinePolling(normalizedInterval);
|
|
}
|
|
|
|
// Polling implementations
|
|
private startAccountPolling(): void {
|
|
if (this.accountPollTimer) return;
|
|
|
|
const poll = async () => {
|
|
try {
|
|
const accountSnapshot = await this.fetchAccountSnapshot();
|
|
|
|
for (const listener of this.accountListeners) {
|
|
listener(accountSnapshot);
|
|
}
|
|
} catch (error) {
|
|
this.logger("accountPoll", error);
|
|
}
|
|
};
|
|
|
|
poll(); // Initial fetch
|
|
this.accountPollTimer = setInterval(poll, 5000); // Poll every 5 seconds
|
|
}
|
|
|
|
private startOrderPolling(): void {
|
|
if (this.orderPollTimer) return;
|
|
|
|
const poll = async () => {
|
|
try {
|
|
const [openOrders, closedOrders] = await Promise.all([
|
|
this.exchange.fetchOpenOrders(this.marketSymbol),
|
|
this.exchange.fetchClosedOrders(this.marketSymbol, undefined, 50), // Last 50 closed orders
|
|
]);
|
|
|
|
const allOrders = [...openOrders, ...closedOrders];
|
|
const mappedOrders = allOrders.map(order => this.mapOrderToAsterOrder(order));
|
|
|
|
for (const listener of this.orderListeners) {
|
|
listener(mappedOrders);
|
|
}
|
|
} catch (error) {
|
|
this.logger("orderPoll", error);
|
|
}
|
|
};
|
|
|
|
poll(); // Initial fetch
|
|
this.orderPollTimer = setInterval(poll, 2000); // Poll every 2 seconds
|
|
}
|
|
|
|
private startDepthPolling(): void {
|
|
if (this.depthPollTimer) return;
|
|
|
|
const poll = async () => {
|
|
try {
|
|
const orderbook = await this.exchange.fetchOrderBook(this.marketSymbol, 20);
|
|
const depth = this.mapOrderBookToDepth(orderbook);
|
|
|
|
for (const listener of this.depthListeners) {
|
|
listener(depth);
|
|
}
|
|
} catch (error) {
|
|
this.logger("depthPoll", error);
|
|
}
|
|
};
|
|
|
|
poll(); // Initial fetch
|
|
this.depthPollTimer = setInterval(poll, 1000); // Poll every 1 second
|
|
}
|
|
|
|
private startTickerPolling(): void {
|
|
if (this.tickerPollTimer) return;
|
|
|
|
const poll = async () => {
|
|
try {
|
|
const ticker = await this.exchange.fetchTicker(this.marketSymbol);
|
|
const asterTicker = this.mapTickerToAsterTicker(ticker);
|
|
|
|
for (const listener of this.tickerListeners) {
|
|
listener(asterTicker);
|
|
}
|
|
} catch (error) {
|
|
this.logger("tickerPoll", error);
|
|
}
|
|
};
|
|
|
|
poll(); // Initial fetch
|
|
this.tickerPollTimer = setInterval(poll, 2000); // Poll every 2 seconds
|
|
}
|
|
|
|
private startKlinePolling(interval: string): void {
|
|
if (this.klinePollTimers.has(interval)) return;
|
|
|
|
const poll = async () => {
|
|
try {
|
|
const ohlcv = await this.exchange.fetchOHLCV(this.marketSymbol, interval, undefined, 100);
|
|
const klines = (ohlcv as number[][])
|
|
.filter((c) => Array.isArray(c) && c.length >= 6)
|
|
.map((c) => this.mapOHLCVToKline([c[0], c[1], c[2], c[3], c[4], c[5]] as [number, number, number, number, number, number], interval));
|
|
|
|
for (const listener of this.klineListeners) {
|
|
if (listener.interval === interval) {
|
|
listener.callback(klines);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
this.logger("klinePoll", error);
|
|
}
|
|
};
|
|
|
|
poll(); // Initial fetch
|
|
this.klinePollTimers.set(interval, setInterval(poll, 5000)); // Poll every 5 seconds
|
|
}
|
|
|
|
// Order management
|
|
async createOrder(params: CreateOrderParams): Promise<AsterOrder> {
|
|
await this.ensureInitialized();
|
|
|
|
// Only pass exchange-specific params in the last argument so we don't
|
|
// override ccxt's internal request mapping (e.g. side mapping for Backpack).
|
|
const symbol = this.marketSymbol;
|
|
const type = this.mapOrderTypeToCcxt(params.type);
|
|
const side = params.side.toLowerCase();
|
|
const amount = params.quantity;
|
|
const price = params.price;
|
|
|
|
const extraParams: Record<string, unknown> = {};
|
|
if (params.stopPrice !== undefined) extraParams.stopPrice = params.stopPrice;
|
|
// Map GTX (post-only) to Backpack's postOnly boolean and use GTC as TIF
|
|
if (params.timeInForce === "GTX") {
|
|
extraParams.postOnly = true;
|
|
extraParams.timeInForce = "GTC";
|
|
} else if (params.timeInForce !== undefined) {
|
|
extraParams.timeInForce = params.timeInForce; // GTC, IOC, FOK
|
|
}
|
|
// Reduce-only string boolean -> boolean per OpenAPI
|
|
if (params.reduceOnly !== undefined) {
|
|
extraParams.reduceOnly = params.reduceOnly === "true";
|
|
}
|
|
if (params.closePosition !== undefined) {
|
|
extraParams.closePosition = params.closePosition === "true";
|
|
}
|
|
|
|
const order = await this.exchange.createOrder(
|
|
symbol,
|
|
type,
|
|
side,
|
|
amount,
|
|
price,
|
|
extraParams
|
|
);
|
|
|
|
return this.mapOrderToAsterOrder(order);
|
|
}
|
|
|
|
async cancelOrder(params: { orderId: number | string }): Promise<void> {
|
|
await this.exchange.cancelOrder(params.orderId.toString(), this.marketSymbol);
|
|
}
|
|
|
|
async cancelOrders(params: { orderIdList: Array<number | string> }): Promise<void> {
|
|
await Promise.all(
|
|
params.orderIdList.map(orderId =>
|
|
this.exchange.cancelOrder(orderId.toString(), this.marketSymbol)
|
|
)
|
|
);
|
|
}
|
|
|
|
async cancelAllOrders(): Promise<void> {
|
|
try {
|
|
if (typeof (this.exchange as any).cancelAllOrders === "function") {
|
|
await (this.exchange as any).cancelAllOrders(this.marketSymbol);
|
|
return;
|
|
}
|
|
} catch {
|
|
// fall through to manual cancel
|
|
}
|
|
const open = await this.exchange.fetchOpenOrders(this.marketSymbol);
|
|
for (const o of open) {
|
|
await this.exchange.cancelOrder(o.id as string, this.marketSymbol);
|
|
}
|
|
}
|
|
|
|
// Mapping functions
|
|
private mapBalanceToAccountSnapshot(balance: Balances): AsterAccountSnapshot {
|
|
return this.mapBalanceToAccountSnapshotWithPositions(balance, []);
|
|
}
|
|
|
|
private async fetchAccountSnapshot(): Promise<AsterAccountSnapshot> {
|
|
await this.ensureInitialized();
|
|
const balancePromise = this.exchange.fetchBalance();
|
|
const positionsPromise = this.isContractMarket
|
|
? this.exchange.fetchPositions([this.marketSymbol]).catch((error: unknown) => {
|
|
this.logger("fetchPositions", error);
|
|
return [];
|
|
})
|
|
: Promise.resolve([]);
|
|
|
|
const [balance, positions] = await Promise.all([balancePromise, positionsPromise]);
|
|
return this.mapBalanceToAccountSnapshotWithPositions(balance, positions ?? []);
|
|
}
|
|
|
|
private mapBalanceToAccountSnapshotWithPositions(balance: Balances, rawPositions: any[]): AsterAccountSnapshot {
|
|
const now = Date.now();
|
|
const assets = this.normalizeAssets(balance, now);
|
|
const positions = this.normalizePositions(rawPositions, now);
|
|
|
|
const totalWalletBalance = this.sumStrings(assets.map((asset) => asset.walletBalance));
|
|
const totalUnrealizedProfit = this.sumStrings(positions.map((position) => position.unrealizedProfit ?? "0"));
|
|
const availableBalance = this.sumStrings(assets.map((asset) => asset.availableBalance));
|
|
|
|
const snapshot: AsterAccountSnapshot = {
|
|
canTrade: true,
|
|
canDeposit: true,
|
|
canWithdraw: true,
|
|
updateTime: now,
|
|
totalWalletBalance,
|
|
totalUnrealizedProfit,
|
|
positions,
|
|
assets,
|
|
};
|
|
|
|
snapshot.availableBalance = availableBalance;
|
|
snapshot.maxWithdrawAmount = availableBalance;
|
|
|
|
if (this.isContractMarket) {
|
|
const totalMarginBalance = this.addStrings(totalWalletBalance, totalUnrealizedProfit);
|
|
snapshot.totalMarginBalance = totalMarginBalance;
|
|
snapshot.totalCrossWalletBalance = totalWalletBalance;
|
|
snapshot.totalCrossUnPnl = totalUnrealizedProfit;
|
|
}
|
|
|
|
return snapshot;
|
|
}
|
|
|
|
private normalizeAssets(balance: Balances, now: number): AsterAccountSnapshot["assets"] {
|
|
const metaKeys = new Set(["free", "used", "total", "info", "timestamp", "datetime", "debt"]);
|
|
const assets: AsterAccountSnapshot["assets"] = [];
|
|
|
|
for (const [currency, value] of Object.entries(balance)) {
|
|
if (metaKeys.has(currency)) continue;
|
|
if (!value || typeof value !== "object") continue;
|
|
|
|
const walletBalance = this.toStringAmount((value as any).total ?? (value as any).free ?? "0");
|
|
const availableBalance = this.toStringAmount((value as any).free ?? "0");
|
|
|
|
assets.push({
|
|
asset: currency,
|
|
walletBalance,
|
|
availableBalance,
|
|
updateTime: now,
|
|
});
|
|
}
|
|
|
|
return assets;
|
|
}
|
|
|
|
private normalizePositions(rawPositions: any[], now: number): AsterAccountSnapshot["positions"] {
|
|
if (!Array.isArray(rawPositions)) return [];
|
|
|
|
const positions: AsterAccountSnapshot["positions"] = [];
|
|
|
|
for (const position of rawPositions) {
|
|
const info = position?.info ?? position ?? {};
|
|
const rawSymbol = position?.symbol ?? info.symbol ?? this.marketSymbol;
|
|
const rawContracts = position?.contracts ?? info.netExposureQuantity;
|
|
const derivedSide = (position?.side ?? info.side ?? this.deriveSideFromExposure(info)) ?? "long";
|
|
const rawSide = derivedSide.toString().toLowerCase();
|
|
const quantity = this.toNumber(rawContracts);
|
|
if (!quantity) continue;
|
|
|
|
const side = rawSide === "short" ? "short" : "long";
|
|
const signedQuantity = side === "short" ? -Math.abs(quantity) : Math.abs(quantity);
|
|
|
|
const normalized: AsterAccountPosition = {
|
|
symbol: rawSymbol,
|
|
positionAmt: signedQuantity.toString(),
|
|
entryPrice: this.toStringAmount(position?.entryPrice ?? info.entryPrice ?? "0"),
|
|
unrealizedProfit: this.toStringAmount(position?.unrealizedPnl ?? info.pnlUnrealized ?? "0"),
|
|
positionSide: side === "short" ? "SHORT" : "LONG",
|
|
updateTime: now,
|
|
};
|
|
|
|
const markPrice = this.toOptionalString(position?.markPrice ?? info.markPrice);
|
|
if (markPrice !== undefined) normalized.markPrice = markPrice;
|
|
|
|
const liquidationPrice = this.toOptionalString(position?.liquidationPrice ?? info.estLiquidationPrice);
|
|
if (liquidationPrice !== undefined) normalized.liquidationPrice = liquidationPrice;
|
|
|
|
const initialMargin = this.toOptionalString(position?.initialMargin ?? info.initialMargin);
|
|
if (initialMargin !== undefined) normalized.initialMargin = initialMargin;
|
|
|
|
const maintMargin = this.toOptionalString(position?.maintenanceMargin ?? info.maintenanceMargin);
|
|
if (maintMargin !== undefined) normalized.maintMargin = maintMargin;
|
|
|
|
const leverage = this.toOptionalString(position?.leverage ?? info.leverage);
|
|
if (leverage !== undefined) normalized.leverage = leverage;
|
|
|
|
normalized.marginType = "CROSSED";
|
|
|
|
positions.push(normalized);
|
|
}
|
|
|
|
return positions;
|
|
}
|
|
|
|
private deriveSideFromExposure(info: Record<string, unknown>): "long" | "short" | "flat" {
|
|
const exposure = this.toNumber(info?.netExposureNotional ?? info?.netCost ?? info?.netQuantity);
|
|
if (!exposure) return "flat";
|
|
return exposure < 0 ? "short" : "long";
|
|
}
|
|
|
|
private toStringAmount(value: unknown): string {
|
|
if (value === undefined || value === null) return "0";
|
|
if (typeof value === "string") {
|
|
if (value.trim() === "") return "0";
|
|
return value;
|
|
}
|
|
if (typeof value === "number") {
|
|
if (!Number.isFinite(value)) return "0";
|
|
return value.toString();
|
|
}
|
|
return "0";
|
|
}
|
|
|
|
private toOptionalString(value: unknown): string | undefined {
|
|
const normalized = this.toStringAmount(value);
|
|
return normalized === "0" ? undefined : normalized;
|
|
}
|
|
|
|
private toNumber(value: unknown): number {
|
|
const asString = this.toStringAmount(value);
|
|
const parsed = Number(asString);
|
|
if (!Number.isFinite(parsed)) return 0;
|
|
return parsed;
|
|
}
|
|
|
|
private sumStrings(values: string[]): string {
|
|
let total = 0;
|
|
for (const value of values) {
|
|
const parsed = Number(value);
|
|
if (!Number.isFinite(parsed)) continue;
|
|
total += parsed;
|
|
}
|
|
return total.toString();
|
|
}
|
|
|
|
private addStrings(a: string, b: string): string {
|
|
const sum = Number(a) + Number(b);
|
|
if (!Number.isFinite(sum)) return "0";
|
|
return sum.toString();
|
|
}
|
|
|
|
private mapOrderToAsterOrder(order: CcxtOrder): AsterOrder {
|
|
const side = (order.side ?? "buy").toUpperCase() as "BUY" | "SELL";
|
|
const mappedType = this.mapCcxtOrderTypeToAster(order.type);
|
|
return {
|
|
orderId: String(order.id ?? ""),
|
|
clientOrderId: (order.clientOrderId as any as string) || "",
|
|
symbol: order.symbol || this.marketSymbol,
|
|
side,
|
|
type: mappedType,
|
|
status: (order.status as any as string) || "",
|
|
price: order.price?.toString() || "0",
|
|
origQty: order.amount?.toString() || "0",
|
|
executedQty: order.filled?.toString() || "0",
|
|
stopPrice: order.stopPrice?.toString() || "0",
|
|
time: order.timestamp || Date.now(),
|
|
updateTime: order.lastUpdateTimestamp || Date.now(),
|
|
reduceOnly: false,
|
|
closePosition: false,
|
|
avgPrice: order.average?.toString(),
|
|
cumQuote: order.cost?.toString(),
|
|
};
|
|
}
|
|
|
|
private mapOrderBookToDepth(orderbook: CcxtOrderBook): AsterDepth {
|
|
return {
|
|
lastUpdateId: orderbook.nonce || Date.now(),
|
|
bids: (orderbook.bids || []).filter((t) => t && t.length >= 2).map(([price, amount]) => [String(price ?? 0), String(amount ?? 0)]),
|
|
asks: (orderbook.asks || []).filter((t) => t && t.length >= 2).map(([price, amount]) => [String(price ?? 0), String(amount ?? 0)]),
|
|
eventTime: orderbook.timestamp,
|
|
};
|
|
}
|
|
|
|
private mapTickerToAsterTicker(ticker: CcxtTicker): AsterTicker {
|
|
return {
|
|
symbol: ticker.symbol,
|
|
lastPrice: ticker.last?.toString() || "0",
|
|
openPrice: ticker.open?.toString() || "0",
|
|
highPrice: ticker.high?.toString() || "0",
|
|
lowPrice: ticker.low?.toString() || "0",
|
|
volume: ticker.baseVolume?.toString() || "0",
|
|
quoteVolume: ticker.quoteVolume?.toString() || "0",
|
|
eventTime: ticker.timestamp,
|
|
};
|
|
}
|
|
|
|
private mapOHLCVToKline(candle: [number, number, number, number, number, number], interval: string): AsterKline {
|
|
const [timestamp, open, high, low, close, volume] = candle;
|
|
return {
|
|
openTime: timestamp,
|
|
closeTime: timestamp + this.getIntervalMs(interval),
|
|
open: open.toString(),
|
|
high: high.toString(),
|
|
low: low.toString(),
|
|
close: close.toString(),
|
|
volume: volume.toString(),
|
|
numberOfTrades: 0,
|
|
};
|
|
}
|
|
|
|
private mapOrderTypeToCcxt(type: string): string {
|
|
const typeMap: Record<string, string> = {
|
|
"LIMIT": "limit",
|
|
"MARKET": "market",
|
|
"STOP_MARKET": "stop",
|
|
"TRAILING_STOP_MARKET": "trailing-stop",
|
|
};
|
|
return typeMap[type] || "limit";
|
|
}
|
|
|
|
private mapCcxtOrderTypeToAster(type: string | undefined): OrderType {
|
|
const typeMap: Record<string, OrderType> = {
|
|
"limit": "LIMIT",
|
|
"market": "MARKET",
|
|
"stop": "STOP_MARKET",
|
|
"trailing-stop": "TRAILING_STOP_MARKET",
|
|
};
|
|
return type ? (typeMap[type] ?? "LIMIT") : "LIMIT";
|
|
}
|
|
|
|
private getIntervalMs(interval: string): number {
|
|
const intervalMap: Record<string, number> = {
|
|
"1m": 60 * 1000,
|
|
"5m": 5 * 60 * 1000,
|
|
"15m": 15 * 60 * 1000,
|
|
"1h": 60 * 60 * 1000,
|
|
"4h": 4 * 60 * 60 * 1000,
|
|
"1d": 24 * 60 * 60 * 1000,
|
|
};
|
|
return intervalMap[interval] || 60 * 1000;
|
|
}
|
|
|
|
// Cleanup
|
|
destroy(): void {
|
|
if (this.accountPollTimer) {
|
|
clearInterval(this.accountPollTimer);
|
|
this.accountPollTimer = null;
|
|
}
|
|
if (this.orderPollTimer) {
|
|
clearInterval(this.orderPollTimer);
|
|
this.orderPollTimer = null;
|
|
}
|
|
if (this.depthPollTimer) {
|
|
clearInterval(this.depthPollTimer);
|
|
this.depthPollTimer = null;
|
|
}
|
|
if (this.tickerPollTimer) {
|
|
clearInterval(this.tickerPollTimer);
|
|
this.tickerPollTimer = null;
|
|
}
|
|
|
|
for (const timer of this.klinePollTimers.values()) {
|
|
clearInterval(timer);
|
|
}
|
|
this.klinePollTimers.clear();
|
|
}
|
|
}
|