n8n-nodes-fxcm
Version:
n8n node for FXCM trading integration
335 lines (334 loc) • 12 kB
JavaScript
"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;