Tradovate MCP Server

by alexanimal
Verified
import * as logger from "./logger.js"; import { tradovateRequest } from './auth.js'; import { contractsCache, positionsCache, ordersCache, accountsCache, fetchPositions } from './data.js'; /** * Handle get_contract_details tool */ export async function handleGetContractDetails(request: any) { const symbol = String(request.params.arguments?.symbol); if (!symbol) { throw new Error("Symbol is required"); } try { // Find contract by symbol const contract = await tradovateRequest('GET', `contract/find?name=${symbol}`); if (!contract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } return { content: [{ type: "text", text: `Contract details for ${symbol}:\n${JSON.stringify(contract, null, 2)}` }] }; } catch (error) { logger.error(`Error getting contract details for ${symbol}:`, error); // Fallback to cached data const cachedContract = Object.values(contractsCache).find(c => c.name === symbol); if (!cachedContract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } return { content: [{ type: "text", text: `Contract details for ${symbol} (cached):\n${JSON.stringify(cachedContract, null, 2)}` }] }; } } /** * Handle list_positions tool */ export async function handleListPositions(request: any) { const accountId = String(request.params.arguments?.accountId || ""); try { // Get positions from API let endpoint = 'position/list'; if (accountId) { endpoint += `?accountId=${accountId}`; } const positions = await tradovateRequest('GET', endpoint); if (!positions || positions.length === 0) { return { content: [{ type: "text", text: `No positions found${accountId ? ` for account ${accountId}` : ''}` }] }; } return { content: [{ type: "text", text: `Positions${accountId ? ` for account ${accountId}` : ''}:\n${JSON.stringify(positions, null, 2)}` }] }; } catch (error) { // Log error but attempt to retry once more before giving up logger.error("Error listing positions, retrying:", error); try { // Retry API call let endpoint = 'position/list'; if (accountId) { endpoint += `?accountId=${accountId}`; } const positions = await tradovateRequest('GET', endpoint); if (!positions || positions.length === 0) { return { content: [{ type: "text", text: `No positions found${accountId ? ` for account ${accountId}` : ''}` }] }; } return { content: [{ type: "text", text: `Positions${accountId ? ` for account ${accountId}` : ''}:\n${JSON.stringify(positions, null, 2)}` }] }; } catch (retryError) { logger.error("Error listing positions after retry:", retryError); return { content: [{ type: "text", text: `Error fetching positions: ${retryError instanceof Error ? retryError.message : String(retryError)}` }] }; } } } /** * Handle place_order tool */ export async function handlePlaceOrder(request: any) { const symbol = String(request.params.arguments?.symbol); const action = String(request.params.arguments?.action); const orderType = String(request.params.arguments?.orderType); const quantity = Number(request.params.arguments?.quantity); const price = request.params.arguments?.price ? Number(request.params.arguments.price) : undefined; const stopPrice = request.params.arguments?.stopPrice ? Number(request.params.arguments.stopPrice) : undefined; if (!symbol || !action || !orderType || !quantity) { throw new Error("Symbol, action, orderType, and quantity are required"); } // Validate order type and required parameters - moved up before any API calls if ((orderType === "Limit" || orderType === "StopLimit") && price === undefined) { throw new Error("Price is required for Limit and StopLimit orders"); } if ((orderType === "Stop" || orderType === "StopLimit") && stopPrice === undefined) { throw new Error("Stop price is required for Stop and StopLimit orders"); } try { // Find contract by symbol const contract = await tradovateRequest('GET', `contract/find?name=${symbol}`); if (!contract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } // Get account ID const accounts = await tradovateRequest('GET', 'account/list'); if (!accounts || accounts.length === 0) { throw new Error("No accounts found"); } const accountId = accounts[0].id; // Use the first account // Prepare order data const orderData = { accountId, contractId: contract.id, action, orderQty: quantity, orderType, price, stopPrice }; // Place order via API const newOrder = await tradovateRequest('POST', 'order/placeOrder', orderData); // Update orders cache ordersCache[newOrder.id.toString()] = newOrder; return { content: [{ type: "text", text: `Order placed successfully:\n${JSON.stringify(newOrder, null, 2)}` }] }; } catch (error) { logger.error("Error placing order:", error); // Fallback to cached data for simulation try { // Find contract by symbol const cachedContract = Object.values(contractsCache).find(c => c.name === symbol); if (!cachedContract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } // Get first account from cache const cachedAccount = Object.values(accountsCache)[0]; if (!cachedAccount) { return { content: [{ type: "text", text: `No accounts found in cache` }] }; } // Create simulated order const newOrderId = String(Object.keys(ordersCache).length + 1); const simulatedOrder = { id: parseInt(newOrderId), accountId: cachedAccount.id, contractId: cachedContract.id, timestamp: new Date().toISOString(), action, ordStatus: "Working", orderQty: quantity, orderType, price, stopPrice }; // Add to cache ordersCache[newOrderId] = simulatedOrder; return { content: [{ type: "text", text: `Order placed successfully (simulated):\n${JSON.stringify(simulatedOrder, null, 2)}` }] }; } catch (fallbackError) { logger.error("Error in fallback order placement:", fallbackError); return { content: [{ type: "text", text: `Failed to place order: ${error instanceof Error ? error.message : String(error)}` }] }; } } } /** * Handle modify_order tool */ export async function handleModifyOrder(request: any) { const orderId = String(request.params.arguments?.orderId); const price = request.params.arguments?.price !== undefined ? Number(request.params.arguments.price) : undefined; const stopPrice = request.params.arguments?.stopPrice !== undefined ? Number(request.params.arguments.stopPrice) : undefined; const quantity = request.params.arguments?.quantity !== undefined ? Number(request.params.arguments.quantity) : undefined; if (!orderId) { throw new Error("Order ID is required"); } try { // Find order by ID const order = await tradovateRequest('GET', `order/find?id=${orderId}`); if (!order) { return { content: [{ type: "text", text: `Order not found with ID: ${orderId}` }] }; } // Prepare modification data const modifyData: any = { orderId: parseInt(orderId) }; if (price !== undefined) modifyData.price = price; if (stopPrice !== undefined) modifyData.stopPrice = stopPrice; if (quantity !== undefined) modifyData.orderQty = quantity; // Modify order via API const updatedOrder = await tradovateRequest('POST', 'order/modifyOrder', modifyData); // Update orders cache ordersCache[orderId] = updatedOrder; return { content: [{ type: "text", text: `Order modified successfully:\n${JSON.stringify(updatedOrder, null, 2)}` }] }; } catch (error) { logger.error(`Error modifying order ${orderId}:`, error); // Fallback to cached data for simulation const cachedOrder = ordersCache[orderId]; if (!cachedOrder) { return { content: [{ type: "text", text: `Order not found with ID: ${orderId}` }] }; } // Update order in cache if (price !== undefined) cachedOrder.price = price; if (stopPrice !== undefined) cachedOrder.stopPrice = stopPrice; if (quantity !== undefined) cachedOrder.orderQty = quantity; return { content: [{ type: "text", text: `Order modified successfully (simulated):\n${JSON.stringify(cachedOrder, null, 2)}` }] }; } } /** * Handle cancel_order tool */ export async function handleCancelOrder(request: any) { const orderId = String(request.params.arguments?.orderId); if (!orderId) { throw new Error("Order ID is required"); } try { // Find order by ID const order = await tradovateRequest('GET', `order/find?id=${orderId}`); if (!order) { return { content: [{ type: "text", text: `Order not found with ID: ${orderId}` }] }; } // Cancel order via API const canceledOrder = await tradovateRequest('POST', 'order/cancelOrder', { orderId: parseInt(orderId) }); // Update orders cache ordersCache[orderId] = canceledOrder; return { content: [{ type: "text", text: `Order canceled successfully:\n${JSON.stringify(canceledOrder, null, 2)}` }] }; } catch (error) { logger.error(`Error canceling order ${orderId}:`, error); // Fallback to cached data for simulation const cachedOrder = ordersCache[orderId]; if (!cachedOrder) { return { content: [{ type: "text", text: `Order not found with ID: ${orderId}` }] }; } // Update order status in cache cachedOrder.ordStatus = "Canceled"; return { content: [{ type: "text", text: `Order canceled successfully (simulated):\n${JSON.stringify(cachedOrder, null, 2)}` }] }; } } /** * Handle liquidate_position tool */ export async function handleLiquidatePosition(request: any) { const symbol = String(request.params.arguments?.symbol); if (!symbol) { throw new Error("Symbol is required"); } try { // Find contract by symbol const contract = await tradovateRequest('GET', `contract/find?name=${symbol}`); if (!contract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } // Find position by contract ID const positions = await tradovateRequest('GET', 'position/list'); const position = positions.find((p: any) => p.contractId === contract.id); if (!position) { return { content: [{ type: "text", text: `No position found for symbol: ${symbol}` }] }; } // Liquidate position via API const liquidationResult = await tradovateRequest('POST', 'order/liquidatePosition', { accountId: position.accountId, contractId: position.contractId }); return { content: [{ type: "text", text: `Position liquidated successfully:\n${JSON.stringify(liquidationResult, null, 2)}` }] }; } catch (error) { logger.error(`Error liquidating position for ${symbol}, retrying:`, error); // Retry once before giving up try { // Find contract by symbol (retry) const contract = await tradovateRequest('GET', `contract/find?name=${symbol}`); if (!contract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } // Find position by contract ID (retry) const positions = await tradovateRequest('GET', 'position/list'); const position = positions.find((p: any) => p.contractId === contract.id); if (!position) { return { content: [{ type: "text", text: `No position found for symbol: ${symbol}` }] }; } // Liquidate position via API (retry) const liquidationResult = await tradovateRequest('POST', 'order/liquidatePosition', { accountId: position.accountId, contractId: position.contractId }); return { content: [{ type: "text", text: `Position liquidated successfully:\n${JSON.stringify(liquidationResult, null, 2)}` }] }; } catch (retryError) { logger.error(`Error liquidating position for ${symbol} after retry:`, retryError); return { content: [{ type: "text", text: `Failed to liquidate position for ${symbol}: ${retryError instanceof Error ? retryError.message : String(retryError)}` }] }; } } } /** * Handle get_account_summary tool */ export async function handleGetAccountSummary(request: any) { const accountId = String(request.params.arguments?.accountId || ""); try { // Get accounts let accounts; if (accountId) { accounts = [await tradovateRequest('GET', `account/find?id=${accountId}`)]; if (!accounts[0]) { return { content: [{ type: "text", text: `Account not found with ID: ${accountId}` }] }; } } else { accounts = await tradovateRequest('GET', 'account/list'); if (!accounts || accounts.length === 0) { return { content: [{ type: "text", text: `No accounts found` }] }; } } const account = accounts[0]; const actualAccountId = account.id; // Get cash balance const cashBalance = await tradovateRequest('POST', 'cashBalance/getCashBalanceSnapshot', { accountId: actualAccountId }); // Get positions const positions = await tradovateRequest('GET', `position/list?accountId=${actualAccountId}`); // Calculate summary const totalRealizedPnl = positions.reduce((sum: number, pos: any) => sum + pos.realizedPnl, 0); const totalOpenPnl = positions.reduce((sum: number, pos: any) => sum + pos.openPnl, 0); const summary = { account, balance: cashBalance.cashBalance, openPnl: totalOpenPnl, totalEquity: cashBalance.cashBalance + totalOpenPnl, marginUsed: cashBalance.initialMargin, availableMargin: cashBalance.cashBalance - cashBalance.initialMargin + totalOpenPnl, positionCount: positions.length }; return { content: [{ type: "text", text: `Account summary for ${account.name}:\n${JSON.stringify(summary, null, 2)}` }] }; } catch (error) { logger.error("Error getting account summary, retrying:", error); // Retry the API call once before giving up try { // Get accounts (retry) let accounts; if (accountId) { accounts = [await tradovateRequest('GET', `account/find?id=${accountId}`)]; if (!accounts[0]) { return { content: [{ type: "text", text: `Account not found with ID: ${accountId}` }] }; } } else { accounts = await tradovateRequest('GET', 'account/list'); if (!accounts || accounts.length === 0) { return { content: [{ type: "text", text: `No accounts found` }] }; } } const account = accounts[0]; const actualAccountId = account.id; // Get cash balance (retry) const cashBalance = await tradovateRequest('POST', 'cashBalance/getCashBalanceSnapshot', { accountId: actualAccountId }); // Get positions (retry) const positions = await tradovateRequest('GET', `position/list?accountId=${actualAccountId}`); // Calculate summary const totalRealizedPnl = positions.reduce((sum: number, pos: any) => sum + pos.realizedPnl, 0); const totalOpenPnl = positions.reduce((sum: number, pos: any) => sum + pos.openPnl, 0); const summary = { account, balance: cashBalance.cashBalance, openPnl: totalOpenPnl, totalEquity: cashBalance.cashBalance + totalOpenPnl, marginUsed: cashBalance.initialMargin, availableMargin: cashBalance.cashBalance - cashBalance.initialMargin + totalOpenPnl, positionCount: positions.length }; return { content: [{ type: "text", text: `Account summary for ${account.name}:\n${JSON.stringify(summary, null, 2)}` }] }; } catch (retryError) { logger.error("Error getting account summary after retry:", retryError); return { content: [{ type: "text", text: `Error getting account summary: ${retryError instanceof Error ? retryError.message : String(retryError)}` }] }; } } } /** * Handle get_market_data tool */ export async function handleGetMarketData(request: any) { const symbol = String(request.params.arguments?.symbol); const dataType = String(request.params.arguments?.dataType); const chartTimeframe = String(request.params.arguments?.chartTimeframe || "1min"); if (!symbol || !dataType) { throw new Error("Symbol and dataType are required"); } try { // Find contract by symbol const contract = await tradovateRequest('GET', `contract/find?name=${symbol}`); if (!contract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } let marketData: any; switch (dataType) { case "Quote": // Get quote data using market data API marketData = await tradovateRequest('GET', `md/getQuote?contractId=${contract.id}`, undefined, true); break; case "DOM": // Get DOM data using market data API marketData = await tradovateRequest('GET', `md/getDOM?contractId=${contract.id}`, undefined, true); break; case "Chart": // Convert timeframe to chart parameters let chartUnits; let chartLength; switch (chartTimeframe) { case "1min": chartUnits = "m"; chartLength = 1; break; case "5min": chartUnits = "m"; chartLength = 5; break; case "15min": chartUnits = "m"; chartLength = 15; break; case "30min": chartUnits = "m"; chartLength = 30; break; case "1hour": chartUnits = "h"; chartLength = 1; break; case "4hour": chartUnits = "h"; chartLength = 4; break; case "1day": chartUnits = "d"; chartLength = 1; break; default: chartUnits = "m"; chartLength = 1; } // Get chart data using market data API marketData = await tradovateRequest( 'GET', `md/getChart?contractId=${contract.id}&chartDescription=${chartLength}${chartUnits}&timeRange=3600`, undefined, true ); break; default: throw new Error(`Unsupported data type: ${dataType}`); } return { content: [{ type: "text", text: `Market data for ${symbol} (${dataType}):\n${JSON.stringify(marketData, null, 2)}` }] }; } catch (error) { logger.error(`Error getting market data for ${symbol}:`, error); // Fallback to mock data const mockContract = Object.values(contractsCache).find(c => c.name === symbol); if (!mockContract) { return { content: [{ type: "text", text: `Contract not found for symbol: ${symbol}` }] }; } let mockMarketData: any; switch (dataType) { case "Quote": mockMarketData = { symbol, bid: 5275.25, ask: 5275.50, last: 5275.25, volume: 1250000, timestamp: new Date().toISOString() }; break; case "DOM": mockMarketData = { symbol, bids: [ { price: 5275.25, size: 250 }, { price: 5275.00, size: 175 }, { price: 5274.75, size: 320 }, { price: 5274.50, size: 450 }, { price: 5274.25, size: 280 } ], asks: [ { price: 5275.50, size: 180 }, { price: 5275.75, size: 220 }, { price: 5276.00, size: 350 }, { price: 5276.25, size: 275 }, { price: 5276.50, size: 400 } ], timestamp: new Date().toISOString() }; break; case "Chart": // Generate mock chart data for the requested timeframe const now = new Date(); const bars = []; for (let i = 0; i < 10; i++) { const barTime = new Date(now); switch (chartTimeframe) { case "1min": barTime.setMinutes(now.getMinutes() - i); break; case "5min": barTime.setMinutes(now.getMinutes() - i * 5); break; case "15min": barTime.setMinutes(now.getMinutes() - i * 15); break; case "30min": barTime.setMinutes(now.getMinutes() - i * 30); break; case "1hour": barTime.setHours(now.getHours() - i); break; case "4hour": barTime.setHours(now.getHours() - i * 4); break; case "1day": barTime.setDate(now.getDate() - i); break; } const basePrice = 5275.00; const open = basePrice - i * 0.25; const high = open + Math.random() * 1.5; const low = open - Math.random() * 1.5; const close = (open + high + low) / 3; const volume = Math.floor(Math.random() * 10000) + 5000; bars.push({ timestamp: barTime.toISOString(), open, high, low, close, volume }); } mockMarketData = { symbol, timeframe: chartTimeframe, bars: bars.reverse() }; break; default: throw new Error(`Unsupported data type: ${dataType}`); } return { content: [{ type: "text", text: `Market data for ${symbol} (${dataType}) [MOCK DATA]:\n${JSON.stringify(mockMarketData, null, 2)}` }] }; } } /** * Handle list_orders tool */ export async function handleListOrders(request: any) { const accountId = String(request.params.arguments?.accountId || ""); try { // Get orders from API let endpoint = 'order/list'; if (accountId) { endpoint += `?accountId=${accountId}`; } const orders = await tradovateRequest('GET', endpoint); if (!orders || orders.length === 0) { return { content: [{ type: "text", text: `No orders found${accountId ? ` for account ${accountId}` : ''}` }] }; } return { content: [{ type: "text", text: `Orders${accountId ? ` for account ${accountId}` : ''}:\n${JSON.stringify(orders, null, 2)}` }] }; } catch (error) { // Log error but attempt to retry once more before giving up logger.error("Error listing orders, retrying:", error); try { // Retry API call let endpoint = 'order/list'; if (accountId) { endpoint += `?accountId=${accountId}`; } const orders = await tradovateRequest('GET', endpoint); if (!orders || orders.length === 0) { return { content: [{ type: "text", text: `No orders found${accountId ? ` for account ${accountId}` : ''}` }] }; } return { content: [{ type: "text", text: `Orders${accountId ? ` for account ${accountId}` : ''}:\n${JSON.stringify(orders, null, 2)}` }] }; } catch (retryError) { logger.error("Error listing orders after retry:", retryError); // Fallback to cached data const cachedOrders = Object.values(ordersCache); const filteredOrders = accountId ? cachedOrders.filter(order => order.accountId === parseInt(accountId)) : cachedOrders; if (filteredOrders.length === 0) { return { content: [{ type: "text", text: `No orders found${accountId ? ` for account ${accountId}` : ''} (cached)` }] }; } return { content: [{ type: "text", text: `Orders${accountId ? ` for account ${accountId}` : ''} (cached):\n${JSON.stringify(filteredOrders, null, 2)}` }] }; } } }