UNPKG

n8n-nodes-fxcm

Version:
335 lines (334 loc) 12 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.FxcmClient = void 0; /* eslint-disable @typescript-eslint/no-explicit-any */ const axios_1 = __importDefault(require("axios")); const ws_1 = __importDefault(require("ws")); class FxcmClient { constructor(credentials) { this.ws = null; this.subscribers = new Map(); this.isConnected = false; this.MAX_RECONNECTION_ATTEMPTS = 5; this.INITIAL_RECONNECTION_DELAY = 1000; this.reconnectionAttempts = 0; this.WS_MESSAGE_TYPES = { PRICE_UPDATE: 'PRICE_UPDATE', POSITION_UPDATE: 'POSITION_UPDATE', ORDER_UPDATE: 'ORDER_UPDATE', ERROR: 'ERROR' }; this.credentials = credentials; this.rest = axios_1.default.create({ baseURL: `https://${credentials.accountType === 'demo' ? 'demo-api' : 'api'}.fxcm.com`, headers: { 'Authorization': `Bearer ${credentials.apiToken}`, 'Content-Type': 'application/json', 'Trading-Station-ID': credentials.tradingStationId, }, timeout: 30000, // 30 seconds timeout }); } async getHistoricalData(instrument, startDate, endDate, timeframe = 'm1' // Default to 1-minute timeframe ) { try { const response = await this.rest.get('/trading/get_historical_data', { params: { symbol: instrument, from: startDate.toISOString(), to: endDate.toISOString(), timeframe } }); return this.handleResponse(response.data); } catch (error) { throw this.handleError(error, 'Failed to get historical data'); } } validateOrderParameters(instrument, amount, stopLoss, takeProfit) { if (!instrument || typeof instrument !== 'string') { throw new Error('Invalid instrument specified'); } if (!amount || amount <= 0) { throw new Error('Amount must be greater than 0'); } if (stopLoss && stopLoss <= 0) { throw new Error('Stop loss must be greater than 0'); } if (takeProfit && takeProfit <= 0) { throw new Error('Take profit must be greater than 0'); } } // WebSocket Connection Management async connectWebSocket(onMessage) { if (this.isConnected) { return; } return new Promise((resolve, reject) => { try { const wsUrl = `wss://${this.credentials.accountType === 'demo' ? 'demo-api' : 'api'}.fxcm.com`; this.ws = new ws_1.default(wsUrl); this.ws.on('open', () => { this.isConnected = true; this.authenticate(); resolve(); }); this.ws.on('message', (data) => { try { const parsedData = JSON.parse(data.toString()); this.handleWebSocketMessage(parsedData, onMessage); } catch (error) { console.error('WebSocket message parsing error:', error); } }); this.ws.on('error', (error) => { console.error('WebSocket error:', error); this.isConnected = false; this.reconnectWebSocket(onMessage); }); this.ws.on('close', () => { console.log('WebSocket closed'); this.isConnected = false; this.reconnectWebSocket(onMessage); }); // Add connection timeout setTimeout(() => { if (!this.isConnected) { reject(new Error('WebSocket connection timeout')); } }, 10000); } catch (error) { reject(error); } }); } authenticate() { if (!this.ws) return; this.ws.send(JSON.stringify({ action: 'authenticate', token: this.credentials.apiToken, trading_station_id: this.credentials.tradingStationId, })); } async reconnectWebSocket(onMessage) { if (this.ws) { this.ws.terminate(); this.ws = null; } // Exponential backoff for reconnection let retryCount = 0; const maxRetries = 5; const tryReconnect = async () => { try { await this.connectWebSocket(onMessage); console.log('Successfully reconnected to WebSocket'); retryCount = 0; } catch (error) { retryCount++; if (retryCount < maxRetries) { const delay = Math.min(1000 * Math.pow(2, retryCount), 30000); console.log(`Reconnection attempt ${retryCount} failed. Retrying in ${delay}ms...`); setTimeout(tryReconnect, delay); } else { console.error('Max reconnection attempts reached'); } } }; tryReconnect(); } handleWebSocketMessage(data, callback) { // Handle different types of WebSocket messages if (data.type === 'PRICE_UPDATE') { this.notifyPriceSubscribers(data); } if (data.type === 'POSITION_UPDATE') { this.notifyPositionSubscribers(data); } // Always call the main callback callback(data); } // Trading Operations async placeMarketOrder(order) { try { this.validateOrderParameters(order.instrument, order.amount, order.stopLoss, order.takeProfit); const response = await this.rest.post('/trading/open_trade', { symbol: order.instrument, is_buy: order.isBuy, amount: order.amount, stop: order.stopLoss, limit: order.takeProfit, order_type: 'AtMarket', time_in_force: 'GTC' }); return this.handleResponse(response.data); } catch (error) { throw this.handleError(error, 'Failed to place market order'); } } async placeLimitOrder(order) { try { const response = await this.rest.post('/trading/create_entry_order', { symbol: order.instrument, is_buy: order.isBuy, amount: order.amount, rate: order.price, stop: order.stopLoss, limit: order.takeProfit, order_type: 'LimitEntry', time_in_force: 'GTC', }); return this.handleResponse(response.data); } catch (error) { throw this.handleError(error, 'Failed to place limit order'); } } async closePosition(positionId) { try { const response = await this.rest.post('/trading/close_trade', { trade_id: positionId, amount: 'ALL', // Close entire position }); return this.handleResponse(response.data); } catch (error) { throw this.handleError(error, 'Failed to close position'); } } async modifyPosition(positionId, stopLoss, takeProfit) { try { const response = await this.rest.post('/trading/change_trade_stop_limit', { trade_id: positionId, is_stop: stopLoss !== undefined, is_limit: takeProfit !== undefined, rate: stopLoss || takeProfit, }); return this.handleResponse(response.data); } catch (error) { throw this.handleError(error, 'Failed to modify position'); } } // Market Data Operations async getInstruments() { try { const response = await this.rest.get('/trading/get_instruments'); return this.handleResponse(response.data); } catch (error) { throw this.handleError(error, 'Failed to get instruments'); } } async getPrices(instrument) { try { const response = await this.rest.get(`/trading/get_prices/${instrument}`); return this.handleResponse(response.data); } catch (error) { throw this.handleError(error, 'Failed to get prices'); } } async subscribeToPrices(instrument, callback) { if (!this.ws) { await this.connectWebSocket(() => { }); } this.subscribers.set(`price_${instrument}`, callback); if (this.ws) { this.ws.send(JSON.stringify({ action: 'subscribe', type: 'PRICE', symbol: instrument, })); } } // Account Operations async getAccountInfo() { try { const response = await this.rest.get('/trading/get_accounts'); return this.handleResponse(response.data); } catch (error) { throw this.handleError(error, 'Failed to get account info'); } } async getPositions() { try { const response = await this.rest.get('/trading/get_open_positions'); return this.handleResponse(response.data); } catch (error) { throw this.handleError(error, 'Failed to get positions'); } } async getTransactionHistory(startDate, endDate) { try { const response = await this.rest.get('/trading/get_closed_positions', { params: { from: startDate.toISOString(), to: endDate.toISOString(), }, }); return this.handleResponse(response.data); } catch (error) { throw this.handleError(error, 'Failed to get transaction history'); } } // Helper Methods handleResponse(data) { const responseData = data; if (responseData.error) { throw new Error(responseData.error.message || 'any error occurred'); } return responseData; } handleError(error, defaultMessage) { var _a, _b; let errorMessage = defaultMessage; if (axios_1.default.isAxiosError(error) && error.response) { errorMessage = ((_b = (_a = error.response.data) === null || _a === void 0 ? void 0 : _a.error) === null || _b === void 0 ? void 0 : _b.message) || error.message || defaultMessage; } else if (error instanceof Error) { errorMessage = error.message || defaultMessage; } return new Error(errorMessage); } notifyPriceSubscribers(data) { const callback = this.subscribers.get(`price_${data.symbol}`); if (callback) { callback(data); } } notifyPositionSubscribers(data) { const callback = this.subscribers.get(`position_${data.tradeId}`); if (callback) { callback(data); } } // Cleanup disconnect() { this.isConnected = false; this.reconnectionAttempts = 0; if (this.ws) { try { this.ws.close(); } catch (error) { console.error('Error closing WebSocket:', error); } this.ws = null; } this.subscribers.clear(); } } exports.FxcmClient = FxcmClient;