199 lines
7.5 KiB
TypeScript
199 lines
7.5 KiB
TypeScript
import * as ccxt from 'ccxt';
|
|
import { config } from '../config/index.js';
|
|
import logger from '../utils/logger.js';
|
|
import { IExchangeConnector, Candle, ExchangeCapabilities, ExchangeOrderCorrelation } from './types.js';
|
|
|
|
export class CCXTConnector implements IExchangeConnector {
|
|
private client: ccxt.Exchange;
|
|
|
|
constructor(apiKey?: string, apiSecret?: string) {
|
|
const exchangeId = config.EXCHANGE as keyof typeof ccxt;
|
|
const exchangeClass = ccxt[exchangeId] as any;
|
|
|
|
if (!exchangeClass) {
|
|
throw new Error(`Exchange ${config.EXCHANGE} not supported by CCXT`);
|
|
}
|
|
|
|
const auth: any = {};
|
|
const resolvedApiKey = apiKey || config.CCXT_API_KEY;
|
|
const resolvedApiSecret = apiSecret || config.CCXT_API_SECRET;
|
|
|
|
if (resolvedApiKey && resolvedApiKey !== 'your_key') {
|
|
auth.apiKey = resolvedApiKey;
|
|
}
|
|
if (resolvedApiSecret && resolvedApiSecret !== 'your_secret') {
|
|
auth.secret = resolvedApiSecret;
|
|
}
|
|
|
|
this.client = new exchangeClass({
|
|
...auth,
|
|
enableRateLimit: true,
|
|
});
|
|
}
|
|
|
|
public getCapabilities(): ExchangeCapabilities {
|
|
return {
|
|
fetchOpenOrders: !!this.client.has['fetchOpenOrders'],
|
|
fetchClosedOrders: !!this.client.has['fetchClosedOrders'],
|
|
shorting: !!this.client.has['createMarketBuyOrder'] && !!this.client.has['createMarketSellOrder'],
|
|
margin: !!this.client.has['margin'],
|
|
leverage: !!this.client.has['setLeverage'],
|
|
tradingWindow: false // CCXT doesn't have a unified market clock usually
|
|
};
|
|
}
|
|
|
|
async fetchOHLCV(symbol: string, timeframe: string, limit: number = 100): Promise<Candle[]> {
|
|
try {
|
|
// Translate Alpaca style '1Min' to CCXT style '1m'
|
|
let translatedTimeframe = timeframe;
|
|
if (timeframe === '1Min') translatedTimeframe = '1m';
|
|
if (timeframe === '5Min') translatedTimeframe = '5m';
|
|
if (timeframe === '15Min') translatedTimeframe = '15m';
|
|
if (timeframe === '1Hour') translatedTimeframe = '1h';
|
|
if (timeframe === '1Day') translatedTimeframe = '1d';
|
|
|
|
logger.info(`[CCXT] Fetching data for ${symbol} via ${config.EXCHANGE} (${translatedTimeframe})...`);
|
|
const candles = await this.client.fetchOHLCV(symbol, translatedTimeframe, undefined, limit);
|
|
return candles.map((c: any) => ({
|
|
timestamp: c[0],
|
|
open: c[1],
|
|
high: c[2],
|
|
low: c[3],
|
|
close: c[4],
|
|
volume: c[5],
|
|
}));
|
|
} catch (error) {
|
|
logger.error(`[CCXT] Error: ${error}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async placeOrder(
|
|
symbol: string,
|
|
side: 'buy' | 'sell',
|
|
qty: number,
|
|
type: 'market' | 'limit',
|
|
price?: number,
|
|
stopLoss?: number,
|
|
takeProfit?: number,
|
|
clientOrderId?: string,
|
|
correlation?: ExchangeOrderCorrelation
|
|
): Promise<any> {
|
|
try {
|
|
logger.info(`[CCXT] Placing ${side} ${type} order for ${qty} ${symbol}...`);
|
|
const params: any = {};
|
|
if (clientOrderId) params.clientOrderId = clientOrderId;
|
|
if (stopLoss !== undefined) params.stopLoss = stopLoss;
|
|
if (takeProfit !== undefined) params.takeProfit = takeProfit;
|
|
return await this.client.createOrder(symbol, type, side, qty, price, params);
|
|
} catch (error) {
|
|
logger.error(`[CCXT] Order Error: ${error}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
|
|
async getOrder(orderId: string, symbol?: string): Promise<any> {
|
|
try {
|
|
logger.info(`[CCXT] Fetching order ${orderId} (${symbol || 'unknown symbol'})...`);
|
|
// CCXT often requires the symbol for fetchOrder
|
|
return await this.client.fetchOrder(orderId, symbol);
|
|
} catch (error: any) {
|
|
logger.error(`[CCXT] GetOrder Error for ${orderId}: ${error.message || error}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async getPosition(symbol: string): Promise<any> {
|
|
try {
|
|
const positions = await this.client.fetchPositions([symbol]);
|
|
return positions.find(p => p.symbol === symbol) || null;
|
|
} catch (error: any) {
|
|
logger.error(`[CCXT] GetPosition Error for ${symbol}: ${error.message || error}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async isTradingWindowOpen(): Promise<boolean | null> {
|
|
// Crypto venues are effectively 24/7 for this bot's supported flow.
|
|
return true;
|
|
}
|
|
|
|
private normalizeDataSymbol(symbol?: string): string {
|
|
const candidate = String(symbol || '').toUpperCase();
|
|
if (!candidate) return candidate;
|
|
if (candidate.includes('/')) {
|
|
return candidate;
|
|
}
|
|
if (candidate.endsWith('USDT')) {
|
|
return `${candidate.slice(0, -4)}/USDT`;
|
|
}
|
|
if (candidate.endsWith('USD')) {
|
|
return `${candidate.slice(0, -3)}/USD`;
|
|
}
|
|
return candidate;
|
|
}
|
|
|
|
async cancelOrder(orderId: string, symbol?: string): Promise<boolean> {
|
|
try {
|
|
logger.info(`[CCXT] Cancelling order ${orderId} (${symbol || 'unknown symbol'})...`);
|
|
await this.client.cancelOrder(orderId, symbol);
|
|
return true;
|
|
} catch (error: any) {
|
|
logger.error(`[CCXT] CancelOrder Error for ${orderId}: ${error.message || error}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async fetchOpenOrders(symbols?: string[]): Promise<any[]> {
|
|
if (!this.client.fetchOpenOrders) return [];
|
|
try {
|
|
const orders = await this.client.fetchOpenOrders();
|
|
if (!symbols || symbols.length === 0) return orders;
|
|
|
|
const normalizedTargets = new Set(symbols.map((s) => this.normalizeDataSymbol(s)));
|
|
return orders.filter((order: any) => normalizedTargets.has(this.normalizeDataSymbol(order.symbol)));
|
|
} catch (error: any) {
|
|
logger.error(`[CCXT] Fetch open orders failed: ${error.message || error}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async fetchClosedOrders(
|
|
symbols?: string[],
|
|
options?: {
|
|
after?: Date;
|
|
limit?: number;
|
|
maxPages?: number;
|
|
}
|
|
): Promise<any[]> {
|
|
if (!this.client.fetchClosedOrders) return [];
|
|
const since = options?.after instanceof Date && Number.isFinite(options.after.getTime())
|
|
? options.after.getTime()
|
|
: undefined;
|
|
const limit = Math.max(1, Math.min(500, Math.floor(Number(options?.limit || 500))));
|
|
|
|
try {
|
|
if (!symbols || symbols.length === 0) {
|
|
return await this.client.fetchClosedOrders(undefined, since, limit);
|
|
}
|
|
|
|
const collected: any[] = [];
|
|
for (const symbol of symbols) {
|
|
try {
|
|
const rows = await this.client.fetchClosedOrders(symbol, since, limit);
|
|
if (Array.isArray(rows)) {
|
|
collected.push(...rows);
|
|
}
|
|
} catch (symbolError: any) {
|
|
logger.warn(`[CCXT] Fetch closed orders failed for ${symbol}: ${symbolError.message || symbolError}`);
|
|
}
|
|
}
|
|
return collected;
|
|
} catch (error: any) {
|
|
logger.error(`[CCXT] Fetch closed orders failed: ${error.message || error}`);
|
|
return [];
|
|
}
|
|
}
|
|
}
|