Skip to main content
Glama

Bybit MCP Server

bybit-service.ts35.8 kB
import crypto from 'crypto'; import fetch from 'node-fetch'; import dotenv from 'dotenv'; import { BybitConfig, BybitResponse, TickerData, OrderbookData, KlineData, WalletBalance, Position, OrderRequest, OrderResponse, Order, ApiKeyInfo, InstrumentInfo } from './types.js'; dotenv.config(); export class BybitService { private config: BybitConfig; private baseUrl: string; constructor() { this.config = { accessKey: process.env.ACCESS_KEY || '', secretKey: process.env.SECRET_KEY || '', demo: process.env.DEMO?.toLowerCase() === 'true', testnet: process.env.TESTNET?.toLowerCase() === 'true' }; // Determine base URL based on environment if (this.config.demo) { this.baseUrl = 'https://api-demo.bybit.com'; console.log('Using Demo environment: https://api-demo.bybit.com'); } else if (this.config.testnet) { this.baseUrl = 'https://api-testnet.bybit.com'; console.log('Using Testnet environment: https://api-testnet.bybit.com'); } else { this.baseUrl = 'https://api.bybit.com'; console.log('Using Production environment: https://api.bybit.com'); } console.log(`Initialized Bybit Service - Demo: ${this.config.demo}, Testnet: ${this.config.testnet}`); } /** * Make authenticated request to Bybit API * This uses the EXACT same signature generation as your working Cloud Functions code */ private async makeBybitRequest<T = any>( endpoint: string, method: 'GET' | 'POST' = 'GET', params: any = {}, maxRetries: number = 3 ): Promise<BybitResponse<T> | { error: string; retCode?: number }> { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const timestamp = Date.now().toString(); // Create signature - EXACT same logic as your Cloud Functions let queryString = ''; if (method === 'GET') { const sortedParams = Object.keys(params) .sort() .map(key => `${key}=${params[key]}`) .join('&'); queryString = sortedParams; } else { // For POST requests - exact Node.js JSON.stringify behavior queryString = params && Object.keys(params).length > 0 ? JSON.stringify(params) : ''; } // Signature payload - exact same format as your Cloud Functions const signaturePayload = timestamp + this.config.accessKey + queryString; const signature = crypto .createHmac('sha256', this.config.secretKey) .update(signaturePayload) .digest('hex'); const headers = { 'X-BAPI-API-KEY': this.config.accessKey, 'X-BAPI-SIGN': signature, 'X-BAPI-TIMESTAMP': timestamp, 'Content-Type': 'application/json', 'User-Agent': 'BybitMCP-Node/1.0' }; const url = method === 'GET' && queryString ? `${this.baseUrl}${endpoint}?${queryString}` : `${this.baseUrl}${endpoint}`; const requestOptions: any = { method, headers, timeout: 10000 }; if (method === 'POST' && params && Object.keys(params).length > 0) { requestOptions.body = JSON.stringify(params); } const response = await fetch(url, requestOptions); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json() as BybitResponse<T>; if (data.retCode === 0) { return data; } else { console.error(`Bybit API error: ${data.retMsg} (Code: ${data.retCode})`); return { error: data.retMsg, retCode: data.retCode }; } } catch (error: any) { if (attempt === maxRetries) { console.error(`Request failed after ${maxRetries} attempts:`, error.message); return { error: error.message }; } else { console.warn(`Request attempt ${attempt} failed, retrying:`, error.message); await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); } } } return { error: 'Max retries exceeded' }; } // Market Data Methods async getTickers(category: string, symbol: string): Promise<BybitResponse<{ list: TickerData[] }> | { error: string }> { return this.makeBybitRequest('/v5/market/tickers', 'GET', { category, symbol }); } async getOrderbook(category: string, symbol: string, limit: number = 50): Promise<BybitResponse<OrderbookData> | { error: string }> { return this.makeBybitRequest('/v5/market/orderbook', 'GET', { category, symbol, limit }); } async getKline( category: string, symbol: string, interval: string, start?: number, end?: number, limit: number = 200 ): Promise<BybitResponse<KlineData> | { error: string }> { const params: any = { category, symbol, interval, limit }; if (start) params.start = start; if (end) params.end = end; return this.makeBybitRequest('/v5/market/kline', 'GET', params); } // Account Methods async getWalletBalance(accountType: string, coin?: string): Promise<BybitResponse<{ list: WalletBalance[] }> | { error: string }> { const params: any = { accountType }; if (coin) params.coin = coin; return this.makeBybitRequest('/v5/account/wallet-balance', 'GET', params); } async getPositions(category: string, symbol?: string): Promise<BybitResponse<{ list: Position[] }> | { error: string }> { const params: any = { category }; if (symbol) { params.symbol = symbol; } else { // Add settleCoin for linear/inverse when no symbol specified if (category === 'linear') { params.settleCoin = 'USDT'; } else if (category === 'inverse') { params.settleCoin = 'BTC'; } } return this.makeBybitRequest('/v5/position/list', 'GET', params); } async getApiKeyInformation(): Promise<BybitResponse<ApiKeyInfo> | { error: string }> { return this.makeBybitRequest('/v5/user/query-api', 'GET', {}); } // Advanced Trading Methods async placeOrderWithTrailingStop( category: string, symbol: string, side: 'Buy' | 'Sell', orderType: 'Market' | 'Limit', qty: string, price?: string, trailingStop?: string, activePrice?: string, positionIdx?: string, timeInForce?: string, orderLinkId?: string ): Promise<BybitResponse<OrderResponse> | { error: string }> { // Auto-detect position mode for futures trading if positionIdx not provided if (['linear', 'inverse'].includes(category) && !positionIdx) { console.log('Auto-detecting position mode for futures trading...'); // Check current positions to determine position mode const currentPositions = await this.getPositions(category, symbol); let detectedPositionIdx = '0'; // Default to one-way mode if ('result' in currentPositions && currentPositions.result.list.length > 0) { // Check if we have separate long/short positions (hedge mode) const positions = currentPositions.result.list; const hasLongPosition = positions.some(p => p.side === 'Buy'); const hasShortPosition = positions.some(p => p.side === 'Sell'); if (hasLongPosition || hasShortPosition) { // Hedge mode detected - use appropriate position index detectedPositionIdx = side === 'Buy' ? '1' : '2'; console.log(`Hedge mode detected, using positionIdx: ${detectedPositionIdx}`); } else { console.log('One-way mode detected, using positionIdx: 0'); } } else { console.log('No existing positions, defaulting to one-way mode (positionIdx: 0)'); } positionIdx = detectedPositionIdx; } // Validate positionIdx for futures trading (after auto-detection) if (['linear', 'inverse'].includes(category)) { if (!positionIdx || !['0', '1', '2'].includes(positionIdx)) { return { error: 'Invalid positionIdx. Use 0 for one-way mode, 1 for long position, or 2 for short position in hedge mode' }; } } // Build order request with trailing stop const orderRequest: any = { category, symbol, side, orderType, qty, timeInForce: timeInForce || (orderType === 'Market' ? 'IOC' : 'GTC'), orderFilter: 'Order', isLeverage: 0 }; // Add price for limit orders if (orderType === 'Limit' && price) { orderRequest.price = price; } // Add trailing stop parameters if (trailingStop) { orderRequest.trailingStop = trailingStop; // Add active price if provided (price at which trailing stop activates) if (activePrice) { orderRequest.activePrice = activePrice; } } // Add position index for futures if (['linear', 'inverse'].includes(category)) { orderRequest.positionIdx = positionIdx; } // Add order link ID if provided if (orderLinkId) { orderRequest.orderLinkId = orderLinkId; } return this.makeBybitRequest('/v5/order/create', 'POST', orderRequest); } // Trading Methods async placeOrder(orderRequest: OrderRequest): Promise<BybitResponse<OrderResponse> | { error: string }> { // Auto-detect position mode for futures trading if positionIdx not provided if (['linear', 'inverse'].includes(orderRequest.category) && !orderRequest.positionIdx) { console.log('Auto-detecting position mode for futures trading...'); // Check current positions to determine position mode const currentPositions = await this.getPositions(orderRequest.category, orderRequest.symbol); let detectedPositionIdx = '0'; // Default to one-way mode if ('result' in currentPositions && currentPositions.result.list.length > 0) { // Check if we have separate long/short positions (hedge mode) const positions = currentPositions.result.list; const hasLongPosition = positions.some(p => p.side === 'Buy'); const hasShortPosition = positions.some(p => p.side === 'Sell'); if (hasLongPosition || hasShortPosition) { // Hedge mode detected - use appropriate position index detectedPositionIdx = orderRequest.side === 'Buy' ? '1' : '2'; console.log(`Hedge mode detected, using positionIdx: ${detectedPositionIdx}`); } else { console.log('One-way mode detected, using positionIdx: 0'); } } else { console.log('No existing positions, defaulting to one-way mode (positionIdx: 0)'); } // Set the detected position index orderRequest.positionIdx = detectedPositionIdx; } // Validate positionIdx for futures trading (after auto-detection) if (['linear', 'inverse'].includes(orderRequest.category)) { if (!orderRequest.positionIdx || !['0', '1', '2'].includes(orderRequest.positionIdx)) { return { error: 'Invalid positionIdx. Use 0 for one-way mode, 1 for long position, or 2 for short position in hedge mode' }; } } // Set defaults const requestData = { ...orderRequest, timeInForce: orderRequest.timeInForce || (orderRequest.orderType === 'Market' ? 'IOC' : 'GTC'), orderFilter: orderRequest.orderFilter || 'Order', isLeverage: orderRequest.isLeverage || 0 }; // Remove positionIdx for spot trading if (orderRequest.category === 'spot') { delete requestData.positionIdx; } return this.makeBybitRequest('/v5/order/create', 'POST', requestData); } async cancelOrder( category: string, symbol: string, orderId?: string, orderLinkId?: string, orderFilter?: string ): Promise<BybitResponse<any> | { error: string }> { const params: any = { category, symbol }; if (orderId) params.orderId = orderId; if (orderLinkId) params.orderLinkId = orderLinkId; if (orderFilter) params.orderFilter = orderFilter; return this.makeBybitRequest('/v5/order/cancel', 'POST', params); } async getOrderHistory( category: string, symbol?: string, orderId?: string, orderLinkId?: string, orderFilter?: string, orderStatus?: string, startTime?: number, endTime?: number, limit: number = 50 ): Promise<BybitResponse<{ list: Order[] }> | { error: string }> { const params: any = { category, limit }; if (symbol) params.symbol = symbol; if (orderId) params.orderId = orderId; if (orderLinkId) params.orderLinkId = orderLinkId; if (orderFilter) params.orderFilter = orderFilter; if (orderStatus) params.orderStatus = orderStatus; if (startTime) params.startTime = startTime; if (endTime) params.endTime = endTime; return this.makeBybitRequest('/v5/order/history', 'GET', params); } async getOpenOrders( category: string, symbol?: string, orderId?: string, orderLinkId?: string, orderFilter?: string, limit: number = 50 ): Promise<BybitResponse<{ list: Order[] }> | { error: string }> { const params: any = { category, limit }; if (symbol) params.symbol = symbol; if (orderId) params.orderId = orderId; if (orderLinkId) params.orderLinkId = orderLinkId; if (orderFilter) params.orderFilter = orderFilter; return this.makeBybitRequest('/v5/order/realtime', 'GET', params); } // Position Management Methods async setLeverage( category: string, symbol: string, buyLeverage: string, sellLeverage: string ): Promise<BybitResponse<any> | { error: string }> { return this.makeBybitRequest('/v5/position/set-leverage', 'POST', { category, symbol, buyLeverage, sellLeverage }); } async setTradingStop( category: string, symbol: string, takeProfit?: string, stopLoss?: string, trailingStop?: string, positionIdx?: number ): Promise<BybitResponse<any> | { error: string }> { const params: any = { category, symbol }; if (takeProfit) params.takeProfit = takeProfit; if (stopLoss) params.stopLoss = stopLoss; if (trailingStop) params.trailingStop = trailingStop; if (positionIdx !== undefined) params.positionIdx = positionIdx; return this.makeBybitRequest('/v5/position/trading-stop', 'POST', params); } async setMarginMode( category: string, symbol: string, tradeMode: number, buyLeverage: string, sellLeverage: string ): Promise<BybitResponse<any> | { error: string }> { return this.makeBybitRequest('/v5/account/set-margin-mode', 'POST', { category, symbol, tradeMode, buyLeverage, sellLeverage }); } // Utility Methods async getInstrumentsInfo( category: string, symbol: string, status?: string, baseCoin?: string ): Promise<BybitResponse<{ list: InstrumentInfo[] }> | { error: string }> { const params: any = { category, symbol }; if (status) params.status = status; if (baseCoin) params.baseCoin = baseCoin; return this.makeBybitRequest('/v5/market/instruments-info', 'GET', params); } // Position Mode Detection async detectPositionMode( category: string, symbol?: string ): Promise<{ positionMode: 'one-way' | 'hedge'; recommendedPositionIdx: string; explanation: string; currentPositions: any[]; }> { try { // Get current positions const currentPositions = await this.getPositions(category, symbol); if ('error' in currentPositions) { return { positionMode: 'one-way', recommendedPositionIdx: '0', explanation: `Could not check positions: ${currentPositions.error}. Defaulting to one-way mode.`, currentPositions: [] }; } const positions = currentPositions.result.list; if (positions.length === 0) { return { positionMode: 'one-way', recommendedPositionIdx: '0', explanation: 'No existing positions found. Defaulting to one-way mode (positionIdx: 0).', currentPositions: [] }; } // Check if we have separate long/short positions (hedge mode) const hasLongPosition = positions.some(p => p.side === 'Buy' && parseFloat(p.size) > 0); const hasShortPosition = positions.some(p => p.side === 'Sell' && parseFloat(p.size) > 0); if (hasLongPosition && hasShortPosition) { return { positionMode: 'hedge', recommendedPositionIdx: '1', // Default to long for new orders explanation: 'Hedge mode detected (both long and short positions exist). Use positionIdx: 1 for long, 2 for short.', currentPositions: positions }; } else if (hasLongPosition || hasShortPosition) { // Check if positions have positionIdx > 0 (indicating hedge mode setup) const hasHedgePositions = positions.some(p => p.positionIdx > 0); if (hasHedgePositions) { return { positionMode: 'hedge', recommendedPositionIdx: '1', explanation: 'Hedge mode detected (positions have positionIdx > 0). Use positionIdx: 1 for long, 2 for short.', currentPositions: positions }; } else { return { positionMode: 'one-way', recommendedPositionIdx: '0', explanation: 'One-way mode detected (single position type with positionIdx: 0).', currentPositions: positions }; } } else { return { positionMode: 'one-way', recommendedPositionIdx: '0', explanation: 'One-way mode detected (no active positions, but account configured for one-way).', currentPositions: positions }; } } catch (error: any) { return { positionMode: 'one-way', recommendedPositionIdx: '0', explanation: `Error detecting position mode: ${error.message}. Defaulting to one-way mode.`, currentPositions: [] }; } } // Trailing Stop Loss Methods async calculateTrailingStop( category: string, symbol: string, entryPrice: number, currentPrice: number, side: 'Buy' | 'Sell', initialStopLoss: number, trailingDistance: number, breakevenTrigger?: number, profitProtectionTrigger?: number ): Promise<{ newStopLoss: number; trailingActivated: boolean; breakevenProtection: boolean; profitProtected: number; unrealizedPnL: number; unrealizedPnLPercentage: number; recommendations: { shouldUpdateStop: boolean; newStopPrice: number; protectionLevel: 'none' | 'breakeven' | 'profit'; reasoning: string; }; calculations: { priceMovement: number; priceMovementPercentage: number; stopLossMovement: number; maxDrawdownProtection: number; profitLocked: number; }; warnings: string[]; error?: string; }> { try { const warnings: string[] = []; const isLong = side === 'Buy'; // Validate inputs if (trailingDistance <= 0) { return { newStopLoss: initialStopLoss, trailingActivated: false, breakevenProtection: false, profitProtected: 0, unrealizedPnL: 0, unrealizedPnLPercentage: 0, recommendations: { shouldUpdateStop: false, newStopPrice: initialStopLoss, protectionLevel: 'none', reasoning: 'Invalid trailing distance' }, calculations: { priceMovement: 0, priceMovementPercentage: 0, stopLossMovement: 0, maxDrawdownProtection: 0, profitLocked: 0 }, warnings: ['Trailing distance must be greater than 0'], error: 'Invalid trailing distance' }; } // Calculate price movements const priceMovement = isLong ? currentPrice - entryPrice : entryPrice - currentPrice; const priceMovementPercentage = (priceMovement / entryPrice) * 100; // Calculate unrealized PnL const unrealizedPnL = priceMovement; const unrealizedPnLPercentage = priceMovementPercentage; // Set default triggers if not provided const breakevenTriggerDistance = breakevenTrigger || (Math.abs(entryPrice - initialStopLoss) * 1.5); const profitProtectionTriggerDistance = profitProtectionTrigger || (Math.abs(entryPrice - initialStopLoss) * 2.0); let newStopLoss = initialStopLoss; let trailingActivated = false; let breakevenProtection = false; let profitProtected = 0; let protectionLevel: 'none' | 'breakeven' | 'profit' = 'none'; let reasoning = 'No trailing conditions met'; // Check if price has moved favorably enough to activate trailing const favorableMovement = isLong ? currentPrice > entryPrice : currentPrice < entryPrice; if (favorableMovement) { // Calculate potential new stop loss based on trailing distance const potentialNewStop = isLong ? currentPrice - trailingDistance : currentPrice + trailingDistance; // Only move stop loss if it's better than current stop const shouldMoveStop = isLong ? potentialNewStop > initialStopLoss : potentialNewStop < initialStopLoss; if (shouldMoveStop) { newStopLoss = potentialNewStop; trailingActivated = true; // Check for breakeven protection const breakevenReached = isLong ? currentPrice >= entryPrice + breakevenTriggerDistance : currentPrice <= entryPrice - breakevenTriggerDistance; if (breakevenReached) { const breakevenStop = entryPrice; // Move stop to breakeven if (isLong ? breakevenStop > newStopLoss : breakevenStop < newStopLoss) { newStopLoss = breakevenStop; breakevenProtection = true; protectionLevel = 'breakeven'; reasoning = 'Breakeven protection activated - stop moved to entry price'; } } // Check for profit protection const profitProtectionReached = isLong ? currentPrice >= entryPrice + profitProtectionTriggerDistance : currentPrice <= entryPrice - profitProtectionTriggerDistance; if (profitProtectionReached) { const profitProtectionStop = isLong ? entryPrice + (profitProtectionTriggerDistance * 0.5) // Lock in 50% of the trigger distance as profit : entryPrice - (profitProtectionTriggerDistance * 0.5); if (isLong ? profitProtectionStop > newStopLoss : profitProtectionStop < newStopLoss) { newStopLoss = profitProtectionStop; profitProtected = Math.abs(profitProtectionStop - entryPrice); protectionLevel = 'profit'; reasoning = `Profit protection activated - locking in ${profitProtected.toFixed(2)} profit`; } } if (protectionLevel === 'none') { reasoning = `Trailing stop activated - following price with ${trailingDistance} distance`; } } else { reasoning = 'Price moved favorably but not enough to improve stop loss'; } } else { reasoning = 'Price has not moved favorably - maintaining original stop loss'; } // Calculate additional metrics const stopLossMovement = Math.abs(newStopLoss - initialStopLoss); const maxDrawdownProtection = Math.abs(currentPrice - newStopLoss); const profitLocked = Math.max(0, isLong ? newStopLoss - entryPrice : entryPrice - newStopLoss); // Add warnings for risk management if (unrealizedPnLPercentage < -5) { warnings.push('Position is down more than 5% - consider reviewing strategy'); } if (trailingDistance > Math.abs(entryPrice - initialStopLoss) * 2) { warnings.push('Trailing distance is very large compared to initial risk - may give back too much profit'); } if (Math.abs(unrealizedPnLPercentage) > 20) { warnings.push('Large unrealized PnL - consider taking partial profits'); } // Get instrument info for price formatting const instrumentInfo = await this.getInstrumentsInfo(category, symbol); if ('result' in instrumentInfo) { const priceFilter = instrumentInfo.result.list[0].priceFilter; const tickSize = parseFloat(priceFilter.tickSize); const decimalPlaces = tickSize.toString().split('.')[1]?.length || 0; // Round new stop loss to valid tick size newStopLoss = Math.round(newStopLoss / tickSize) * tickSize; newStopLoss = parseFloat(newStopLoss.toFixed(decimalPlaces)); } return { newStopLoss, trailingActivated, breakevenProtection, profitProtected, unrealizedPnL, unrealizedPnLPercentage, recommendations: { shouldUpdateStop: newStopLoss !== initialStopLoss, newStopPrice: newStopLoss, protectionLevel, reasoning }, calculations: { priceMovement, priceMovementPercentage, stopLossMovement, maxDrawdownProtection, profitLocked }, warnings }; } catch (error: any) { return { newStopLoss: initialStopLoss, trailingActivated: false, breakevenProtection: false, profitProtected: 0, unrealizedPnL: 0, unrealizedPnLPercentage: 0, recommendations: { shouldUpdateStop: false, newStopPrice: initialStopLoss, protectionLevel: 'none', reasoning: 'Error calculating trailing stop' }, calculations: { priceMovement: 0, priceMovementPercentage: 0, stopLossMovement: 0, maxDrawdownProtection: 0, profitLocked: 0 }, warnings: [], error: error.message }; } } // Position Sizing Methods async calculatePositionSize( category: string, symbol: string, accountBalance: number, riskPercentage: number, stopLossPrice: number, currentPrice?: number, leverage?: number ): Promise<{ recommendedQty: string; maxQty: string; riskAmount: number; positionValue: number; stopLossDistance: number; riskRewardRatio?: number; takeProfitSuggestion?: number; warnings: string[]; calculations: { riskPerTrade: number; priceDistance: number; basePositionSize: number; leveragedPositionSize: number; marginRequired: number; }; error?: string; }> { try { // Get current price if not provided let price = currentPrice; if (!price) { const ticker = await this.getTickers(category, symbol); if ('error' in ticker) { return { recommendedQty: '0', maxQty: '0', riskAmount: 0, positionValue: 0, stopLossDistance: 0, warnings: [], calculations: { riskPerTrade: 0, priceDistance: 0, basePositionSize: 0, leveragedPositionSize: 0, marginRequired: 0 }, error: `Failed to get price: ${ticker.error}` }; } price = parseFloat(ticker.result.list[0].lastPrice); } // Get instrument info for validation const instrumentInfo = await this.getInstrumentsInfo(category, symbol); if ('error' in instrumentInfo) { return { recommendedQty: '0', maxQty: '0', riskAmount: 0, positionValue: 0, stopLossDistance: 0, warnings: [], calculations: { riskPerTrade: 0, priceDistance: 0, basePositionSize: 0, leveragedPositionSize: 0, marginRequired: 0 }, error: `Failed to get instrument info: ${instrumentInfo.error}` }; } const instrument = instrumentInfo.result.list[0]; const lotSizeFilter = instrument.lotSizeFilter; const minOrderQty = parseFloat(lotSizeFilter.minOrderQty); const maxOrderQty = parseFloat(lotSizeFilter.maxOrderQty); const qtyStep = parseFloat(lotSizeFilter.qtyStep); const warnings: string[] = []; const usedLeverage = leverage || 1; // Validate inputs if (riskPercentage <= 0 || riskPercentage > 100) { warnings.push('Risk percentage should be between 0.1% and 100%'); riskPercentage = Math.max(0.1, Math.min(100, riskPercentage)); } if (riskPercentage > 5) { warnings.push('Risk percentage > 5% is considered high risk'); } // Calculate risk amount const riskAmount = (accountBalance * riskPercentage) / 100; // Calculate stop loss distance const stopLossDistance = Math.abs(price - stopLossPrice); const stopLossPercentage = (stopLossDistance / price) * 100; if (stopLossDistance === 0) { return { recommendedQty: '0', maxQty: '0', riskAmount, positionValue: 0, stopLossDistance: 0, warnings: ['Stop loss price cannot equal current price'], calculations: { riskPerTrade: riskAmount, priceDistance: 0, basePositionSize: 0, leveragedPositionSize: 0, marginRequired: 0 }, error: 'Invalid stop loss price' }; } // Calculate position size based on risk // Risk Amount = Position Size * Stop Loss Distance // Position Size = Risk Amount / Stop Loss Distance const basePositionSize = riskAmount / stopLossDistance; // With leverage, we can control a larger position with less margin const leveragedPositionSize = basePositionSize * usedLeverage; // But we need to ensure we don't exceed our margin capacity const marginRequired = (leveragedPositionSize * price) / usedLeverage; // Adjust if margin required exceeds available balance let finalPositionSize = leveragedPositionSize; if (marginRequired > accountBalance * 0.8) { // Keep 20% buffer finalPositionSize = (accountBalance * 0.8 * usedLeverage) / price; warnings.push('Position size reduced due to margin constraints'); } // Round to valid quantity step let recommendedQty = Math.floor(finalPositionSize / qtyStep) * qtyStep; // Ensure minimum quantity if (recommendedQty < minOrderQty) { recommendedQty = minOrderQty; warnings.push(`Adjusted to minimum quantity: ${minOrderQty}`); } // Ensure maximum quantity if (recommendedQty > maxOrderQty) { recommendedQty = maxOrderQty; warnings.push(`Adjusted to maximum quantity: ${maxOrderQty}`); } // Format to correct decimal places const decimalPlaces = qtyStep.toString().split('.')[1]?.length || 0; const formattedQty = recommendedQty.toFixed(decimalPlaces); const maxQty = maxOrderQty.toFixed(decimalPlaces); // Calculate position value and risk-reward suggestions const positionValue = recommendedQty * price; const actualRiskAmount = recommendedQty * stopLossDistance; // Suggest take profit at 2:1 risk-reward ratio const suggestedTakeProfit = stopLossPrice > price ? price - (2 * stopLossDistance) // Short position : price + (2 * stopLossDistance); // Long position const riskRewardRatio = 2.0; // Default 2:1 ratio // Add risk management warnings if (stopLossPercentage > 10) { warnings.push(`Stop loss distance is ${stopLossPercentage.toFixed(1)}% - consider tighter stop loss`); } if (actualRiskAmount > riskAmount * 1.1) { warnings.push('Actual risk exceeds target risk due to position size constraints'); } return { recommendedQty: formattedQty, maxQty, riskAmount: actualRiskAmount, positionValue, stopLossDistance, riskRewardRatio, takeProfitSuggestion: suggestedTakeProfit, warnings, calculations: { riskPerTrade: riskAmount, priceDistance: stopLossDistance, basePositionSize, leveragedPositionSize: finalPositionSize, marginRequired: (finalPositionSize * price) / usedLeverage } }; } catch (error: any) { return { recommendedQty: '0', maxQty: '0', riskAmount: 0, positionValue: 0, stopLossDistance: 0, warnings: [], calculations: { riskPerTrade: 0, priceDistance: 0, basePositionSize: 0, leveragedPositionSize: 0, marginRequired: 0 }, error: error.message }; } } // Order Validation Methods async validateOrderQuantity( category: string, symbol: string, targetAmount: number, currentPrice?: number ): Promise<{ isValid: boolean; validatedQty: string; estimatedCost: string; adjustments: string[]; error?: string; }> { try { // Get current price if not provided let price = currentPrice; if (!price) { const ticker = await this.getTickers(category, symbol); if ('error' in ticker) { return { isValid: false, validatedQty: '0', estimatedCost: '0', adjustments: [], error: `Failed to get price: ${ticker.error}` }; } price = parseFloat(ticker.result.list[0].lastPrice); } // Get instrument info const instrumentInfo = await this.getInstrumentsInfo(category, symbol); if ('error' in instrumentInfo) { return { isValid: false, validatedQty: '0', estimatedCost: '0', adjustments: [], error: `Failed to get instrument info: ${instrumentInfo.error}` }; } const instrument = instrumentInfo.result.list[0]; const lotSizeFilter = instrument.lotSizeFilter; const minOrderQty = parseFloat(lotSizeFilter.minOrderQty); const maxOrderQty = parseFloat(lotSizeFilter.maxOrderQty); const qtyStep = parseFloat(lotSizeFilter.qtyStep); const adjustments: string[] = []; let rawOrderQty = targetAmount / price; // Round to the nearest valid quantity step let orderQty = Math.round(rawOrderQty / qtyStep) * qtyStep; // Ensure it meets minimum requirements if (orderQty < minOrderQty) { orderQty = minOrderQty; adjustments.push(`Adjusted to minimum quantity: ${orderQty}`); } // Ensure it doesn't exceed maximum if (orderQty > maxOrderQty) { orderQty = maxOrderQty; adjustments.push(`Adjusted to maximum quantity: ${orderQty}`); } // Format to the correct decimal places based on qtyStep const decimalPlaces = qtyStep.toString().split('.')[1]?.length || 0; const validatedQty = orderQty.toFixed(decimalPlaces); const estimatedCost = (orderQty * price).toFixed(2); return { isValid: true, validatedQty, estimatedCost, adjustments, }; } catch (error: any) { return { isValid: false, validatedQty: '0', estimatedCost: '0', adjustments: [], error: error.message }; } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kondisettyravi/mcp-bybit-node'

If you have feedback or need assistance with the MCP directory API, please join our Discord server