@copytrade/broker-fyers
Version:
Fyers broker plugin for @copytrade/unified-broker
579 lines • 27.8 kB
JavaScript
"use strict";
/**
* Fyers Service Adapter
* Adapts the existing FyersService to implement IBrokerService interface
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.FyersServiceAdapter = void 0;
const unified_broker_1 = require("@copytrade/unified-broker");
const fyersService_1 = require("./fyersService");
const symbolFormatter_1 = require("./symbolFormatter");
// Import standardized symbol services
// TODO: These should be properly exported from shared packages
// import { BrokerSymbolConverterFactory } from '../../../backend/src/services/brokerSymbolConverters/BrokerSymbolConverterFactory';
// import { symbolDatabaseService } from '../../../backend/src/services/symbolDatabaseService';
class FyersServiceAdapter extends unified_broker_1.IBrokerService {
constructor() {
super('fyers');
this.fyersService = new fyersService_1.FyersService();
}
async login(credentials) {
try {
const fyersCredentials = credentials;
// If we have an auth code, try to complete the OAuth flow
if (fyersCredentials.authCode) {
// Complete OAuth authentication
const serviceCredentials = {
clientId: fyersCredentials.clientId,
secretKey: fyersCredentials.secretKey,
redirectUri: fyersCredentials.redirectUri || '',
authCode: fyersCredentials.authCode,
accessToken: fyersCredentials.accessToken,
refreshToken: fyersCredentials.refreshToken
};
const tokenResponse = await this.fyersService.generateAccessToken(fyersCredentials.authCode, serviceCredentials);
if (tokenResponse.success) {
this.setConnected(true, fyersCredentials.clientId);
return this.createSuccessResponse('OAuth authentication completed', {
accountId: fyersCredentials.clientId,
accessToken: tokenResponse.accessToken
});
}
else {
// Auth code is invalid, fall back to generating new OAuth URL
console.log('🔄 Auth code invalid, generating new OAuth URL...');
}
}
// Generate OAuth URL (either no auth code or invalid auth code)
{
console.log('🔗 Generating OAuth URL for Fyers authentication...');
// Generate OAuth URL for authentication
const serviceCredentials = {
clientId: fyersCredentials.clientId,
secretKey: fyersCredentials.secretKey,
redirectUri: fyersCredentials.redirectUri || '',
authCode: fyersCredentials.authCode,
accessToken: fyersCredentials.accessToken,
refreshToken: fyersCredentials.refreshToken
};
const response = await this.fyersService.login(serviceCredentials);
if (!response.success && response.authUrl) {
// Return OAuth URL for frontend to handle
console.log('✅ OAuth URL generated successfully');
return {
success: false,
message: response.message || 'OAuth authentication required',
data: {
authUrl: response.authUrl
}
};
}
else {
console.error('❌ Failed to generate OAuth URL:', response.message);
return this.createErrorResponse(response.message || 'Failed to generate OAuth URL', response);
}
}
}
catch (error) {
return this.createErrorResponse(error.message || 'Login failed', error);
}
}
async logout() {
try {
const result = await this.fyersService.logout();
this.setConnected(false);
// logout returns an object, check if it indicates success
return typeof result === 'object' ? result.success === true : Boolean(result);
}
catch (error) {
console.error('Logout failed:', error);
return false;
}
}
async validateSession(accountId) {
try {
return await this.fyersService.validateSession();
}
catch (error) {
return false;
}
}
async placeOrder(orderRequest) {
const maxRetries = 3;
let lastError = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Map unified order type to Fyers-specific format
let fyersOrderType;
switch (orderRequest.orderType) {
case 'LIMIT':
fyersOrderType = 'LIMIT';
break;
case 'MARKET':
fyersOrderType = 'MARKET';
break;
case 'SL-LIMIT':
fyersOrderType = 'SL';
break;
case 'SL-MARKET':
fyersOrderType = 'SL-M';
break;
default:
fyersOrderType = 'MARKET';
}
// Map unified product type to Fyers format
const productTypeMap = {
'CNC': 'CNC',
'MIS': 'INTRADAY',
'NRML': 'MARGIN',
'BO': 'BO',
'C': 'CNC',
'M': 'INTRADAY',
'H': 'MARGIN',
'B': 'BO'
};
const fyersProductType = productTypeMap[orderRequest.productType] || orderRequest.productType;
// Convert symbol using standardized symbol system
let formattedSymbol;
try {
// First, try to find the symbol in the standardized database
const standardizedSymbol = await this.lookupStandardizedSymbol(orderRequest.symbol, orderRequest.exchange);
if (standardizedSymbol) {
// TODO: Re-enable when symbol converter is properly exported
// const converter = BrokerSymbolConverterFactory.getConverter('fyers');
// const brokerFormat = converter.convertToBrokerFormat(standardizedSymbol);
formattedSymbol = standardizedSymbol.tradingSymbol;
console.log(`🔄 Fyers standardized symbol conversion: ${orderRequest.symbol} -> ${formattedSymbol}`);
}
else {
// Fallback to legacy symbol formatter
formattedSymbol = symbolFormatter_1.FyersSymbolFormatter.formatSymbol(orderRequest.symbol, orderRequest.exchange || 'NSE');
console.log(`🔄 Fyers legacy symbol formatting: ${orderRequest.symbol} -> ${formattedSymbol}`);
}
}
catch (error) {
console.warn(`⚠️ Symbol conversion failed for ${orderRequest.symbol}, using fallback:`, error.message);
// Final fallback to Fyers equity format
formattedSymbol = symbolFormatter_1.FyersSymbolFormatter.formatEquity(orderRequest.symbol, orderRequest.exchange || 'NSE');
}
// Transform to Fyers-specific order format
const fyersOrderRequest = {
symbol: formattedSymbol,
qty: orderRequest.quantity,
type: fyersOrderType,
side: orderRequest.action, // Keep as 'BUY' | 'SELL' string
productType: fyersProductType,
limitPrice: orderRequest.price || 0,
stopPrice: orderRequest.triggerPrice || 0,
validity: (orderRequest.validity === 'GTD' ? 'DAY' : orderRequest.validity),
disclosedQty: 0,
offlineOrder: false,
stopLoss: 0,
takeProfit: 0
};
const response = await this.fyersService.placeOrder(fyersOrderRequest);
// Fyers response format: { s: 'ok'/'error', message: string, id?: string }
if (response.s === 'ok') {
return this.createSuccessResponse('Order placed successfully', {
orderId: response.id,
message: response.message,
brokerOrderId: response.id,
status: 'PLACED'
});
}
else {
// Check if this is a retryable error
const isRetryable = this.isRetryableError(response.message);
if (isRetryable && attempt < maxRetries) {
console.log(`⚠️ Fyers order placement failed (attempt ${attempt}/${maxRetries}): ${response.message}. Retrying...`);
lastError = new Error(response.message);
await this.delay(1000 * attempt); // Exponential backoff
continue;
}
return this.createErrorResponse(this.transformErrorMessage(response.message || 'Order placement failed'), {
errorType: this.categorizeError(response.message),
originalError: response.message,
attempt: attempt
});
}
}
catch (error) {
lastError = error;
// Check if this is a retryable error
const isRetryable = this.isRetryableError(error.message);
if (isRetryable && attempt < maxRetries) {
console.log(`⚠️ Fyers order placement error (attempt ${attempt}/${maxRetries}): ${error.message}. Retrying...`);
await this.delay(1000 * attempt); // Exponential backoff
continue;
}
// Handle specific error types
const errorType = this.categorizeError(error.message);
const userFriendlyMessage = this.transformErrorMessage(error.message);
return this.createErrorResponse(userFriendlyMessage, {
errorType: errorType,
originalError: error.message,
attempt: attempt
});
}
}
// If we get here, all retries failed
return this.createErrorResponse(this.transformErrorMessage(lastError?.message || 'Order placement failed after multiple attempts'), {
errorType: this.categorizeError(lastError?.message),
originalError: lastError?.message,
maxRetriesExceeded: true
});
}
async getOrderStatus(accountId, orderId) {
const maxRetries = 2;
let lastError = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Fyers doesn't have a separate getOrderStatus method, use getOrderBook
const orderBook = await this.fyersService.getOrderBook();
// orderBook is an array, not an object with orderBook property
if (!Array.isArray(orderBook)) {
throw new Error('Invalid order book response from Fyers');
}
const order = orderBook.find((o) => o.id === orderId);
if (order) {
return {
orderId: order.id || orderId,
status: order.status || 'UNKNOWN',
quantity: order.qty || 0,
filledQuantity: order.filledQty || 0,
price: order.limitPrice || 0,
averagePrice: order.avgPrice || 0,
timestamp: new Date(order.orderDateTime || Date.now())
};
}
else {
throw new Error(`Order ${orderId} not found in Fyers order book`);
}
}
catch (error) {
lastError = error;
// Check if this is a retryable error
const isRetryable = this.isRetryableError(error.message);
if (isRetryable && attempt < maxRetries) {
console.log(`⚠️ Fyers get order status failed (attempt ${attempt}/${maxRetries}): ${error.message}. Retrying...`);
await this.delay(1000 * attempt);
continue;
}
// Transform error message for user
const userFriendlyMessage = this.transformErrorMessage(error.message);
throw new Error(userFriendlyMessage);
}
}
// If we get here, all retries failed
const userFriendlyMessage = this.transformErrorMessage(lastError?.message || 'Failed to get order status after multiple attempts');
throw new Error(userFriendlyMessage);
}
async getOrderHistory(accountId) {
const maxRetries = 2;
let lastError = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Use getOrderBook for order history in Fyers
const response = await this.fyersService.getOrderBook();
// response is an array, not an object with orderBook property
if (!Array.isArray(response)) {
console.warn('Fyers order history response is not an array, returning empty array');
return [];
}
return response.map((order) => ({
orderId: order.id || '',
status: order.status || 'UNKNOWN',
quantity: order.qty || 0,
filledQuantity: order.filledQty || 0,
price: order.limitPrice || 0,
averagePrice: order.avgPrice || 0,
timestamp: new Date(order.orderDateTime || Date.now())
}));
}
catch (error) {
lastError = error;
// Check if this is a retryable error
const isRetryable = this.isRetryableError(error.message);
if (isRetryable && attempt < maxRetries) {
console.log(`⚠️ Fyers get order history failed (attempt ${attempt}/${maxRetries}): ${error.message}. Retrying...`);
await this.delay(1000 * attempt);
continue;
}
// Transform error message for user
const userFriendlyMessage = this.transformErrorMessage(error.message);
throw new Error(userFriendlyMessage);
}
}
// If we get here, all retries failed
const userFriendlyMessage = this.transformErrorMessage(lastError?.message || 'Failed to get order history after multiple attempts');
throw new Error(userFriendlyMessage);
}
async getPositions(accountId) {
const maxRetries = 2;
let lastError = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await this.fyersService.getPositions();
// response is an array, not an object with netPositions property
if (!Array.isArray(response)) {
console.warn('Fyers positions response is not an array, returning empty array');
return [];
}
return response.map((position) => ({
symbol: position.symbol || '',
quantity: position.netQty || 0,
averagePrice: position.avgPrice || 0,
currentPrice: position.ltp || 0,
pnl: position.pl || 0,
exchange: position.exchange || '',
productType: position.productType || ''
}));
}
catch (error) {
lastError = error;
// Check if this is a retryable error
const isRetryable = this.isRetryableError(error.message);
if (isRetryable && attempt < maxRetries) {
console.log(`⚠️ Fyers get positions failed (attempt ${attempt}/${maxRetries}): ${error.message}. Retrying...`);
await this.delay(1000 * attempt);
continue;
}
// Transform error message for user
const userFriendlyMessage = this.transformErrorMessage(error.message);
throw new Error(userFriendlyMessage);
}
}
// If we get here, all retries failed
const userFriendlyMessage = this.transformErrorMessage(lastError?.message || 'Failed to get positions after multiple attempts');
throw new Error(userFriendlyMessage);
}
async getQuote(symbol, exchange) {
try {
// Convert symbol using standardized symbol system
let formattedSymbol;
try {
// First, try to find the symbol in the standardized database
const standardizedSymbol = await this.lookupStandardizedSymbol(symbol, exchange);
if (standardizedSymbol) {
// TODO: Re-enable when symbol converter is properly exported
// const converter = BrokerSymbolConverterFactory.getConverter('fyers');
// const brokerFormat = converter.convertToBrokerFormat(standardizedSymbol);
formattedSymbol = standardizedSymbol.tradingSymbol;
console.log(`🔄 Fyers standardized quote symbol conversion: ${symbol} -> ${formattedSymbol}`);
}
else {
// Fallback to legacy symbol formatter
formattedSymbol = symbolFormatter_1.FyersSymbolFormatter.formatSymbol(symbol, exchange);
console.log(`🔄 Fyers legacy quote symbol formatting: ${symbol} -> ${formattedSymbol}`);
}
}
catch (error) {
console.warn(`⚠️ Quote symbol conversion failed for ${symbol}, using fallback:`, error.message);
formattedSymbol = `${exchange}:${symbol}`;
}
const response = await this.fyersService.getQuotes([formattedSymbol]);
if (!response || response.length === 0) {
throw new Error('No quote data received');
}
const quote = response[0];
if (!quote) {
throw new Error('No quote data in response');
}
return {
symbol: quote.symbol || symbol,
price: quote.ltp || 0,
change: quote.chng || 0,
changePercent: quote.chngPercent || 0,
volume: quote.volume || 0,
exchange: exchange,
timestamp: new Date()
};
}
catch (error) {
throw new Error(`Failed to get quote: ${error.message}`);
}
}
async searchSymbols(query, exchange) {
try {
// Fyers doesn't have a searchSymbols method, return empty array for now
console.warn('searchSymbols not implemented for Fyers');
return [];
}
catch (error) {
throw new Error(`Failed to search symbols: ${error.message}`);
}
}
// Fyers-specific methods that can be accessed if needed
getFyersService() {
return this.fyersService;
}
async refreshToken(credentials) {
try {
console.log('🔄 Attempting to refresh Fyers access token...');
// Set up credentials if provided
if (credentials) {
this.fyersService.setRefreshToken(credentials.refreshToken);
// Set app ID and secret key for refresh
if (credentials.appId && credentials.secretKey) {
this.fyersService.appId = credentials.appId;
this.fyersService.secretKey = credentials.secretKey;
}
}
const refreshResult = await this.fyersService.refreshAccessToken();
if (refreshResult.success) {
// Update internal state with new tokens
this.setConnected(true, credentials?.appId || this.fyersService.appId);
console.log('✅ Fyers access token refreshed successfully');
return {
success: true,
tokenInfo: {
accessToken: refreshResult.accessToken,
refreshToken: refreshResult.refreshToken,
expiryTime: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours from now
isExpired: false,
canRefresh: true
},
message: 'Access token refreshed successfully'
};
}
else {
console.error('❌ Failed to refresh Fyers access token:', refreshResult.message);
return {
success: false,
message: refreshResult.message || 'Failed to refresh access token'
};
}
}
catch (error) {
console.error('🚨 Error refreshing Fyers access token:', error);
return {
success: false,
message: error.message || 'Token refresh failed'
};
}
}
async completeAuth(authCode) {
try {
// Simplified auth completion - just store the auth code
this.setConnected(true, this.accountId);
return this.createSuccessResponse('Authentication completed', {
accountId: this.accountId,
authCode: authCode
});
}
catch (error) {
return this.createErrorResponse(error.message || 'Authentication failed', error);
}
}
// Error handling helper methods
isRetryableError(errorMessage) {
if (!errorMessage)
return false;
const message = errorMessage.toLowerCase();
// Network-related errors that can be retried
if (message.includes('network') ||
message.includes('timeout') ||
message.includes('connection') ||
message.includes('server error') ||
message.includes('service unavailable') ||
message.includes('rate limit') ||
message.includes('too many requests')) {
return true;
}
return false;
}
categorizeError(errorMessage) {
if (!errorMessage)
return 'UNKNOWN_ERROR';
const message = errorMessage.toLowerCase();
if (message.includes('token') || message.includes('unauthorized') || message.includes('authentication')) {
return 'AUTH_ERROR';
}
else if (message.includes('network') || message.includes('timeout') || message.includes('connection')) {
return 'NETWORK_ERROR';
}
else if (message.includes('rate limit') || message.includes('too many requests')) {
return 'RATE_LIMIT_ERROR';
}
else if (message.includes('insufficient') || message.includes('balance') || message.includes('margin')) {
return 'INSUFFICIENT_FUNDS';
}
else if (message.includes('invalid symbol') || message.includes('symbol not found')) {
return 'INVALID_SYMBOL';
}
else if (message.includes('market closed') || message.includes('trading hours')) {
return 'MARKET_CLOSED';
}
else if (message.includes('order') && (message.includes('rejected') || message.includes('failed'))) {
return 'ORDER_REJECTED';
}
else {
return 'BROKER_ERROR';
}
}
transformErrorMessage(errorMessage) {
if (!errorMessage)
return 'An unknown error occurred';
const message = errorMessage.toLowerCase();
// Transform technical errors to user-friendly messages
if (message.includes('token') || message.includes('unauthorized')) {
return 'Your session has expired. Please reconnect your Fyers account.';
}
else if (message.includes('network') || message.includes('timeout')) {
return 'Network connection issue. Please check your internet connection and try again.';
}
else if (message.includes('rate limit') || message.includes('too many requests')) {
return 'Too many requests. Please wait a moment and try again.';
}
else if (message.includes('insufficient') || message.includes('balance')) {
return 'Insufficient funds in your account to place this order.';
}
else if (message.includes('invalid symbol') || message.includes('symbol not found')) {
return 'Invalid trading symbol. Please check the symbol and try again.';
}
else if (message.includes('market closed') || message.includes('trading hours')) {
return 'Market is currently closed. Orders can only be placed during trading hours.';
}
else if (message.includes('order') && message.includes('rejected')) {
return 'Order was rejected by the broker. Please check order parameters and try again.';
}
else {
// Return original message if no specific transformation is available
return errorMessage;
}
}
async delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Look up standardized symbol from database
*/
async lookupStandardizedSymbol(symbol, exchange) {
try {
// TODO: Re-enable when symbol database service is properly exported
// Check if symbol database service is available and initialized
// if (!symbolDatabaseService || !symbolDatabaseService.isReady()) {
// console.warn('Symbol database service not available, using legacy formatting');
// return null;
// }
// Try to find by trading symbol first
// let standardizedSymbol = await symbolDatabaseService.getSymbolByTradingSymbol(symbol, exchange);
// if (!standardizedSymbol) {
// // Try to find by ID if the symbol looks like an ID
// if (symbol.length === 24 && /^[0-9a-fA-F]{24}$/.test(symbol)) {
// standardizedSymbol = await symbolDatabaseService.getSymbolById(symbol);
// }
// }
// return standardizedSymbol;
// Temporary fallback - return null to use legacy formatting
console.warn('Symbol database service not available, using legacy formatting');
return null;
}
catch (error) {
console.warn('Failed to lookup standardized symbol:', error);
return null;
}
}
}
exports.FyersServiceAdapter = FyersServiceAdapter;
//# sourceMappingURL=FyersServiceAdapter.js.map