learning_ai_invt_trdg/backend/src/connectors/ccxt.ts

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 [];
}
}
}