Skip to main content
Glama

Groww MCP

index.ts110 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; // Configuration schema export const configSchema = z.object({ debug: z.boolean().default(false).describe("Enable debug logging"), apiKey: z.string().describe("API key for the Groww API"), }); export default function createStatelessServer({ config, }: { config: z.infer<typeof configSchema>; }) { const server = new McpServer({ name: "groww-mcp-server", version: "1.0.0", }); // Store instruments data in memory let instrumentsData: any[] = []; let instrumentsLoaded = false; // Function to download and parse instruments CSV const loadInstruments = async () => { try { if (config.debug) { console.log("Downloading instruments CSV..."); } const response = await fetch("https://growwapi-assets.groww.in/instruments/instrument.csv"); const csvData = await response.text(); // Parse CSV (simple parsing, assumes CSV is well-formed) const lines = csvData.trim().split('\n'); const headers = lines[0].split(',').map(h => h.trim()); instrumentsData = lines.slice(1).map(line => { const values = line.split(',').map(v => v.trim()); const instrument: any = {}; headers.forEach((header, index) => { instrument[header] = values[index] || ''; }); return instrument; }); instrumentsLoaded = true; if (config.debug) { console.log(`Loaded ${instrumentsData.length} instruments successfully`); } } catch (error) { console.error("Failed to load instruments:", error); instrumentsLoaded = false; } }; // Load instruments on server startup loadInstruments(); // Common headers for all API requests const getHeaders = () => ({ 'Authorization': `Bearer ${config.apiKey}`, 'Accept': 'application/json', 'X-API-VERSION': '1.0', }); // Helper function to make API requests const makeRequest = async (url: string, options: RequestInit = {}) => { const response = await fetch(url, { ...options, headers: { ...getHeaders(), ...options.headers, }, }); if (!response.ok) { let errorMessage = `API request failed: ${response.status} ${response.statusText}`; try { const errorData = await response.json(); if (errorData.error) { errorMessage += `\nAPI Error: ${errorData.error.message || 'Unknown error'} (Code: ${errorData.error.code || 'N/A'})`; } else if (errorData.message) { errorMessage += `\nError: ${errorData.message}`; } else { errorMessage += `\nResponse: ${JSON.stringify(errorData)}`; } } catch (parseError) { const responseText = await response.text(); if (responseText) { errorMessage += `\nResponse: ${responseText}`; } } throw new Error(errorMessage); } const data = await response.json(); if (data.status === 'FAILURE') { throw new Error(`Groww API Error: ${data.error?.message || 'Unknown error'} (Code: ${data.error?.code || 'N/A'})`); } return data.payload || data; }; // ==================== INSTRUMENTS ==================== server.tool( "download_instruments_csv", "Download/refresh the complete instruments CSV file from Groww containing all tradeable instruments", {}, async () => { try { await loadInstruments(); if (!instrumentsLoaded) { return { content: [{ type: "text", text: "Failed to download instruments CSV. Please try again." }], }; } const sampleInstruments = instrumentsData.slice(0, 5).map(inst => `${inst.trading_symbol} (${inst.exchange}) - ${inst.name || 'N/A'} - ${inst.instrument_type} - ${inst.segment}` ).join('\n'); return { content: [ { type: "text", text: `Downloaded instruments CSV successfully!\n\nTotal instruments loaded: ${instrumentsData.length}\n\nSample instruments:\n${sampleInstruments}\n\nColumns available: exchange, exchange_token, trading_symbol, groww_symbol, name, instrument_type, segment, series, isin, underlying_symbol, underlying_exchange_token, expiry_date, strike_price, lot_size, tick_size, freeze_quantity, is_reserved, buy_allowed, sell_allowed\n\nUse 'search_instruments' tool to find specific instruments.` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error downloading instruments CSV: ${error}` }], }; } } ); server.tool( "search_instruments", "Search for instruments by name, trading symbol, groww symbol, or other criteria from the loaded instruments data", { query: z.string().describe("Search query (name, trading symbol, groww symbol, etc.)"), exchange: z.enum(["NSE", "BSE", "ALL"]).default("ALL").describe("Filter by exchange"), segment: z.enum(["CASH", "FNO", "ALL"]).default("ALL").describe("Filter by segment"), instrument_type: z.enum(["EQ", "IDX", "FUT", "CE", "PE", "ALL"]).default("ALL").describe("Filter by instrument type"), limit: z.number().int().min(1).max(50).default(10).describe("Maximum number of results to return"), }, async ({ query, exchange, segment, instrument_type, limit }) => { try { if (!instrumentsLoaded) { return { content: [{ type: "text", text: "Instruments data not loaded. Please run 'download_instruments_csv' first." }], }; } const queryLower = query.toLowerCase(); let filtered = instrumentsData.filter(instrument => { // Text search in name, trading symbol, and groww symbol const nameMatch = (instrument.name || '').toLowerCase().includes(queryLower); const symbolMatch = (instrument.trading_symbol || '').toLowerCase().includes(queryLower); const growwSymbolMatch = (instrument.groww_symbol || '').toLowerCase().includes(queryLower); const textMatch = nameMatch || symbolMatch || growwSymbolMatch; // Exchange filter const exchangeMatch = exchange === "ALL" || instrument.exchange === exchange; // Segment filter const segmentMatch = segment === "ALL" || instrument.segment === segment; // Instrument type filter const typeMatch = instrument_type === "ALL" || instrument.instrument_type === instrument_type; return textMatch && exchangeMatch && segmentMatch && typeMatch; }); // Sort by relevance (exact matches first, then partial matches) filtered.sort((a, b) => { const aSymbolExact = (a.trading_symbol || '').toLowerCase() === queryLower; const bSymbolExact = (b.trading_symbol || '').toLowerCase() === queryLower; const aGrowwSymbolExact = (a.groww_symbol || '').toLowerCase() === queryLower; const bGrowwSymbolExact = (b.groww_symbol || '').toLowerCase() === queryLower; const aNameExact = (a.name || '').toLowerCase() === queryLower; const bNameExact = (b.name || '').toLowerCase() === queryLower; // Priority: trading_symbol exact > groww_symbol exact > name exact > others if (aSymbolExact && !bSymbolExact) return -1; if (!aSymbolExact && bSymbolExact) return 1; if (aGrowwSymbolExact && !bGrowwSymbolExact) return -1; if (!aGrowwSymbolExact && bGrowwSymbolExact) return 1; if (aNameExact && !bNameExact) return -1; if (!aNameExact && bNameExact) return 1; return (a.trading_symbol || '').localeCompare(b.trading_symbol || ''); }); const results = filtered.slice(0, limit); if (results.length === 0) { return { content: [ { type: "text", text: `No instruments found matching query: "${query}"\n\nTry:\n- Different keywords\n- Broader search terms\n- Check spelling\n- Use 'download_instruments_csv' to refresh data` } ], }; } const resultSummary = results.map((inst, index) => { const expiry = inst.expiry_date && inst.expiry_date !== '' ? ` | Expiry: ${inst.expiry_date}` : ''; const strike = inst.strike_price && inst.strike_price !== '' ? ` | Strike: ₹${inst.strike_price}` : ''; const lotSize = inst.lot_size && inst.lot_size !== '' ? ` | Lot: ${inst.lot_size}` : ''; const growwSymbol = inst.groww_symbol && inst.groww_symbol !== inst.trading_symbol ? `\n Groww Symbol: ${inst.groww_symbol}` : ''; return `${index + 1}. **${inst.trading_symbol}** (${inst.exchange})\n Name: ${inst.name || 'N/A'}\n Type: ${inst.instrument_type} | Segment: ${inst.segment}${expiry}${strike}${lotSize}${growwSymbol}\n ISIN: ${inst.isin || 'N/A'}`; }).join('\n\n'); const totalFound = filtered.length; const showingText = totalFound > limit ? `\nShowing ${limit} of ${totalFound} results. Use higher limit to see more.` : `\nFound ${totalFound} result(s).`; return { content: [ { type: "text", text: `Search Results for "${query}":\n\n${resultSummary}${showingText}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error searching instruments: ${error}` }], }; } } ); server.tool( "get_instrument_details", "Get detailed information about a specific instrument by trading symbol", { trading_symbol: z.string().describe("Trading symbol (e.g., 'RELIANCE', 'NIFTY')"), exchange: z.enum(["NSE", "BSE"]).optional().describe("Exchange (optional, will search both if not provided)"), }, async ({ trading_symbol, exchange }) => { try { if (!instrumentsLoaded) { return { content: [{ type: "text", text: "Instruments data not loaded. Please run 'download_instruments_csv' first." }], }; } const symbolUpper = trading_symbol.toUpperCase(); let instrument = instrumentsData.find(inst => inst.trading_symbol === symbolUpper && (exchange ? inst.exchange === exchange : true) ); if (!instrument) { // Try fuzzy search instrument = instrumentsData.find(inst => inst.trading_symbol?.includes(symbolUpper) && (exchange ? inst.exchange === exchange : true) ); } if (!instrument) { return { content: [ { type: "text", text: `Instrument "${trading_symbol}" not found${exchange ? ` on ${exchange}` : ''}.\n\nUse 'search_instruments' to find similar instruments.` } ], }; } const details = [ `**${instrument.trading_symbol}** (${instrument.exchange})`, `Name: ${instrument.name || 'N/A'}`, `Type: ${instrument.instrument_type} | Segment: ${instrument.segment}`, `Series: ${instrument.series || 'N/A'}`, `ISIN: ${instrument.isin || 'N/A'}`, ]; if (instrument.groww_symbol && instrument.groww_symbol !== instrument.trading_symbol) { details.splice(2, 0, `Groww Symbol: ${instrument.groww_symbol}`); } if (instrument.underlying_symbol) { details.push(`Underlying: ${instrument.underlying_symbol}`); } if (instrument.expiry_date && instrument.expiry_date !== '') { details.push(`Expiry: ${instrument.expiry_date}`); } if (instrument.strike_price && instrument.strike_price !== '') { details.push(`Strike Price: ₹${instrument.strike_price}`); } if (instrument.lot_size && instrument.lot_size !== '') { details.push(`Lot Size: ${instrument.lot_size}`); } if (instrument.tick_size && instrument.tick_size !== '') { details.push(`Tick Size: ₹${instrument.tick_size}`); } details.push(`Trading Allowed: Buy=${instrument.buy_allowed === '1' ? 'Yes' : 'No'}, Sell=${instrument.sell_allowed === '1' ? 'Yes' : 'No'}`); return { content: [ { type: "text", text: `Instrument Details:\n\n${details.join('\n')}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting instrument details: ${error}` }], }; } } ); // ==================== ORDERS ==================== server.tool( "place_order", "Place a new order in the market (stocks, F&O, etc.) - Official Groww API: POST /v1/order/create", { trading_symbol: z.string().min(1).describe("Trading Symbol of the instrument as defined by the exchange (required)"), quantity: z.number().int().positive().describe("Quantity of the instrument to order (required)"), exchange: z.enum(["NSE", "BSE"]).describe("Stock exchange (required)"), segment: z.enum(["CASH", "FNO"]).describe("Segment of the instrument such as CASH, FNO etc. (required)"), product: z.enum(["CNC", "MIS", "NRML"]).describe("Product type (required) - CNC, MIS, NRML"), order_type: z.enum(["MARKET", "LIMIT", "SL", "SL_M"]).describe("Order type (required) - MARKET, LIMIT, SL, SL_M"), transaction_type: z.enum(["BUY", "SELL"]).describe("Transaction type of the trade (required) - BUY or SELL"), validity: z.enum(["DAY"]).default("DAY").describe("Validity of the order (required) - currently only DAY is supported"), price: z.number().positive().optional().describe("Price of the instrument in rupees for Limit order (decimal) - required for LIMIT and SL orders"), trigger_price: z.number().positive().optional().describe("Trigger price in rupees for the order (decimal) - required for SL and SL_M orders"), order_reference_id: z.string().min(8).max(20).regex(/^[a-zA-Z0-9-]+$/).optional().describe("User provided 8 to 20 length alphanumeric string with at most two hyphens (-)"), }, async ({ trading_symbol, quantity, exchange, segment, product, order_type, transaction_type, validity, price, trigger_price, order_reference_id }) => { try { // Validate all required parameters are present if (!trading_symbol || !quantity || !exchange || !segment || !product || !order_type || !transaction_type || !validity) { return { content: [{ type: "text", text: "❌ Error: Missing required parameters. All of trading_symbol, quantity, exchange, segment, product, order_type, transaction_type, and validity are required." }], }; } // Generate reference ID if not provided if (!order_reference_id || order_reference_id.trim() === '') { const timestamp = Date.now().toString(); const randomSuffix = Math.random().toString(36).substring(2, 8).toUpperCase(); order_reference_id = `ORD-${timestamp.slice(-8)}-${randomSuffix}`; } // Validate order_reference_id format (at most two hyphens) const hyphenCount = (order_reference_id.match(/-/g) || []).length; if (hyphenCount > 2) { return { content: [{ type: "text", text: "❌ Error: order_reference_id can have at most two hyphens (-)" }], }; } // Validate length (8-20 characters) if (order_reference_id.length < 8 || order_reference_id.length > 20) { return { content: [{ type: "text", text: "❌ Error: order_reference_id must be between 8-20 characters" }], }; } // Validate price requirements based on order type if ((order_type === "LIMIT" || order_type === "SL") && (price === undefined || price === null)) { return { content: [{ type: "text", text: `❌ Error: Price is required for ${order_type} orders` }], }; } if ((order_type === "SL" || order_type === "SL_M") && (trigger_price === undefined || trigger_price === null)) { return { content: [{ type: "text", text: `❌ Error: Trigger price is required for ${order_type} orders` }], }; } // Build request body exactly as per official API documentation example // Order matches the example: trading_symbol, quantity, price, trigger_price, validity, exchange, segment, product, order_type, transaction_type, order_reference_id const body: Record<string, any> = { trading_symbol: trading_symbol, quantity: quantity, }; // Add price and trigger_price in the order shown in API docs (after quantity, before validity) if (price !== undefined && price !== null) { body.price = price; } if (trigger_price !== undefined && trigger_price !== null) { body.trigger_price = trigger_price; } // Continue with required fields in API docs order body.validity = validity; body.exchange = exchange; body.segment = segment; body.product = product; body.order_type = order_type; body.transaction_type = transaction_type; // Add order_reference_id (now always present - either provided or auto-generated) body.order_reference_id = order_reference_id; // Debug logging if (config.debug) { console.log("Place Order Request Body:", JSON.stringify(body, null, 2)); console.log("Request URL: https://api.groww.in/v1/order/create"); console.log("Request Headers:", JSON.stringify(getHeaders(), null, 2)); } const data = await makeRequest("https://api.groww.in/v1/order/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); // Check if reference ID was auto-generated const wasGenerated = order_reference_id.startsWith('ORD-'); return { content: [ { type: "text", text: `✅ Order placed successfully!\n\n📋 Order Response:\n• Order ID: ${data.groww_order_id}\n• Order Status: ${data.order_status}\n• Reference ID: ${data.order_reference_id || order_reference_id}${wasGenerated ? ' (auto-generated)' : ''}\n• Remark: ${data.remark}\n\n📊 Order Summary:\n• Symbol: ${trading_symbol} (${exchange})\n• Transaction: ${transaction_type} ${quantity} units\n• Price: ${price ? `₹${price}` : 'MARKET PRICE'}\n• Product: ${product} | Segment: ${segment}\n• Validity: ${validity}${trigger_price ? `\n• Trigger Price: ₹${trigger_price}` : ''}` } ], }; } catch (error) { // Enhanced error logging if (config.debug) { console.error("Place Order Error:", error); } return { content: [{ type: "text", text: `❌ Error placing order: ${error}\n\nPlease verify:\n• All required parameters are provided\n• Trading symbol exists and is correct\n• Price/trigger_price are provided for LIMIT/SL orders\n• API key is valid and has trading permissions` }], }; } } ); server.tool( "modify_order", "Modify an existing pending or open order", { groww_order_id: z.string().describe("Groww order ID to modify"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), quantity: z.number().int().positive().optional().describe("New quantity"), price: z.number().optional().describe("New price (in rupees)"), trigger_price: z.number().optional().describe("New trigger price (in rupees)"), order_type: z.enum(["MARKET", "LIMIT", "SL", "SL_M"]).optional().describe("New order type"), }, async ({ groww_order_id, segment, quantity, price, trigger_price, order_type }) => { try { const body: any = { groww_order_id, segment, }; if (quantity !== undefined) body.quantity = quantity; if (price !== undefined) body.price = price; if (trigger_price !== undefined) body.trigger_price = trigger_price; if (order_type) body.order_type = order_type; const data = await makeRequest("https://api.groww.in/v1/order/modify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); return { content: [ { type: "text", text: `Order modified successfully!\n\nOrder ID: ${data.groww_order_id}\nNew Status: ${data.order_status}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error modifying order: ${error}` }], }; } } ); server.tool( "cancel_order", "Cancel an existing pending or open order", { groww_order_id: z.string().describe("Groww order ID to cancel"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), }, async ({ groww_order_id, segment }) => { try { const data = await makeRequest("https://api.groww.in/v1/order/cancel", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ groww_order_id, segment }), }); return { content: [ { type: "text", text: `Order cancelled successfully!\n\nOrder ID: ${data.groww_order_id}\nStatus: ${data.order_status}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error cancelling order: ${error}` }], }; } } ); server.tool( "get_order_status", "Get the status of an order by Groww order ID", { groww_order_id: z.string().describe("Groww order ID"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), }, async ({ groww_order_id, segment }) => { try { const data = await makeRequest(`https://api.groww.in/v1/order/status/${groww_order_id}?segment=${segment}`); return { content: [ { type: "text", text: `Order Status:\n\nOrder ID: ${data.groww_order_id}\nStatus: ${data.order_status}\nFilled Quantity: ${data.filled_quantity}\nReference ID: ${data.order_reference_id || 'N/A'}\nRemark: ${data.remark}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting order status: ${error}` }], }; } } ); server.tool( "get_order_status_by_reference", "Get the status of an order by user reference ID", { order_reference_id: z.string().describe("User-provided order reference ID"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), }, async ({ order_reference_id, segment }) => { try { const data = await makeRequest(`https://api.groww.in/v1/order/status/reference/${order_reference_id}?segment=${segment}`); return { content: [ { type: "text", text: `Order Status:\n\nOrder ID: ${data.groww_order_id}\nStatus: ${data.order_status}\nFilled Quantity: ${data.filled_quantity}\nReference ID: ${data.order_reference_id}\nRemark: ${data.remark}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting order status: ${error}` }], }; } } ); server.tool( "get_order_list", "Get list of all orders for the day", { segment: z.enum(["CASH", "FNO"]).optional().describe("Market segment filter"), page: z.number().int().min(0).default(0).describe("Page number"), page_size: z.number().int().min(1).max(50).default(25).describe("Number of orders per page"), }, async ({ segment, page, page_size }) => { try { let url = `https://api.groww.in/v1/order/list?page=${page}&page_size=${page_size}`; if (segment) url += `&segment=${segment}`; const data = await makeRequest(url); const orderSummary = data.order_list.map((order: any) => `Order: ${order.trading_symbol} | ${order.transaction_type} ${order.quantity} @ ${order.price || 'MARKET'} | Status: ${order.order_status} | ID: ${order.groww_order_id}` ).join('\n'); return { content: [ { type: "text", text: `Order List (Page ${page + 1}):\n\n${orderSummary || 'No orders found'}\n\nTotal orders returned: ${data.order_list.length}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting order list: ${error}` }], }; } } ); server.tool( "get_order_details", "Get detailed information about a specific order", { groww_order_id: z.string().describe("Groww order ID"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), }, async ({ groww_order_id, segment }) => { try { const data = await makeRequest(`https://api.groww.in/v1/order/detail/${groww_order_id}?segment=${segment}`); return { content: [ { type: "text", text: `Order Details:\n\nOrder ID: ${data.groww_order_id}\nSymbol: ${data.trading_symbol}\nStatus: ${data.order_status}\nType: ${data.order_type}\nTransaction: ${data.transaction_type}\nQuantity: ${data.quantity}\nPrice: ₹${data.price || 'MARKET'}\nTrigger Price: ₹${data.trigger_price || 'N/A'}\nFilled: ${data.filled_quantity}\nRemaining: ${data.remaining_quantity}\nAvg Fill Price: ₹${data.average_fill_price || 'N/A'}\nExchange: ${data.exchange}\nSegment: ${data.segment}\nProduct: ${data.product}\nValidity: ${data.validity}\nCreated: ${data.created_at}\nReference ID: ${data.order_reference_id || 'N/A'}\nRemark: ${data.remark}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting order details: ${error}` }], }; } } ); server.tool( "get_order_trades", "Get all trades/executions for a specific order", { groww_order_id: z.string().describe("Groww order ID"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), page: z.number().int().min(0).default(0).describe("Page number"), page_size: z.number().int().min(1).max(50).default(50).describe("Number of trades per page"), }, async ({ groww_order_id, segment, page, page_size }) => { try { const data = await makeRequest(`https://api.groww.in/v1/order/trades/${groww_order_id}?segment=${segment}&page=${page}&page_size=${page_size}`); const tradesSummary = data.trade_list.map((trade: any) => `Trade: ${trade.trading_symbol} | ${trade.transaction_type} ${trade.quantity} @ ₹${trade.price} | Status: ${trade.trade_status} | Trade ID: ${trade.groww_trade_id} | Time: ${trade.trade_date_time}` ).join('\n'); return { content: [ { type: "text", text: `Trades for Order ${groww_order_id}:\n\n${tradesSummary || 'No trades found'}\n\nTotal trades: ${data.trade_list.length}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting order trades: ${error}` }], }; } } ); // ==================== PORTFOLIO ==================== server.tool( "get_holdings", "Get current stock holdings in DEMAT account", {}, async () => { try { const data = await makeRequest("https://api.groww.in/v1/holdings/user"); const holdingsSummary = data.holdings.map((holding: any) => `${holding.trading_symbol} (${holding.isin}) | Qty: ${holding.quantity} | Avg Price: ₹${holding.average_price} | Free: ${holding.demat_free_quantity} | Pledged: ${holding.pledge_quantity}` ).join('\n'); return { content: [ { type: "text", text: `Holdings Summary:\n\n${holdingsSummary || 'No holdings found'}\n\nTotal holdings: ${data.holdings.length}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting holdings: ${error}` }], }; } } ); server.tool( "get_positions", "Get all trading positions for the user", { segment: z.enum(["CASH", "FNO"]).optional().describe("Market segment filter"), }, async ({ segment }) => { try { let url = "https://api.groww.in/v1/positions/user"; if (segment) url += `?segment=${segment}`; const data = await makeRequest(url); const positionsSummary = data.positions.map((position: any) => `${position.trading_symbol} | Net Qty: ${position.quantity} | Net Price: ₹${position.net_price} | Credit: ${position.credit_quantity}@₹${position.credit_price} | Debit: ${position.debit_quantity}@₹${position.debit_price} | Exchange: ${position.exchange} | Product: ${position.product}` ).join('\n'); return { content: [ { type: "text", text: `Positions Summary:\n\n${positionsSummary || 'No positions found'}\n\nTotal positions: ${data.positions.length}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting positions: ${error}` }], }; } } ); server.tool( "get_position_by_symbol", "Get position for a specific trading symbol", { trading_symbol: z.string().describe("Trading symbol"), segment: z.enum(["CASH", "FNO"]).optional().describe("Market segment"), }, async ({ trading_symbol, segment }) => { try { let url = `https://api.groww.in/v1/positions/trading-symbol?trading_symbol=${trading_symbol}`; if (segment) url += `&segment=${segment}`; const data = await makeRequest(url); const positionsSummary = data.positions.map((position: any) => `${position.trading_symbol} | Net Qty: ${position.quantity} | Net Price: ₹${position.net_price} | Credit: ${position.credit_quantity}@₹${position.credit_price} | Debit: ${position.debit_quantity}@₹${position.debit_price} | Exchange: ${position.exchange} | Product: ${position.product} | CF Qty: ${position.net_carry_forward_quantity}@₹${position.net_carry_forward_price}` ).join('\n'); return { content: [ { type: "text", text: `Position for ${trading_symbol}:\n\n${positionsSummary || 'No position found'}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting position: ${error}` }], }; } } ); // ==================== MARGIN ==================== server.tool( "get_user_margin", "Get available margin details for the user", {}, async () => { try { const data = await makeRequest("https://api.groww.in/v1/margins/detail/user"); return { content: [ { type: "text", text: `Margin Details:\n\nClear Cash: ₹${data.clear_cash}\nNet Margin Used: ₹${data.net_margin_used}\nBrokerage & Charges: ₹${data.brokerage_and_charges}\nCollateral Used: ₹${data.collateral_used}\nCollateral Available: ₹${data.collateral_available}\nAdhoc Margin: ₹${data.adhoc_margin}\n\nF&O Margins:\n- Net Used: ₹${data.fno_margin_details?.net_fno_margin_used || 0}\n- Span Used: ₹${data.fno_margin_details?.span_margin_used || 0}\n- Exposure Used: ₹${data.fno_margin_details?.exposure_margin_used || 0}\n- Future Balance: ₹${data.fno_margin_details?.future_balance_available || 0}\n- Option Buy Balance: ₹${data.fno_margin_details?.option_buy_balance_available || 0}\n- Option Sell Balance: ₹${data.fno_margin_details?.option_sell_balance_available || 0}\n\nEquity Margins:\n- Net Used: ₹${data.equity_margin_details?.net_equity_margin_used || 0}\n- CNC Used: ₹${data.equity_margin_details?.cnc_margin_used || 0}\n- MIS Used: ₹${data.equity_margin_details?.mis_margin_used || 0}\n- CNC Balance: ₹${data.equity_margin_details?.cnc_balance_available || 0}\n- MIS Balance: ₹${data.equity_margin_details?.mis_balance_available || 0}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting margin details: ${error}` }], }; } } ); server.tool( "calculate_margin_requirement", "Calculate required margin for orders (single or basket)", { segment: z.enum(["CASH", "FNO"]).describe("Market segment"), orders: z.array(z.object({ trading_symbol: z.string().describe("Trading symbol"), transaction_type: z.enum(["BUY", "SELL"]).describe("Transaction type"), quantity: z.number().int().positive().describe("Quantity"), order_type: z.enum(["MARKET", "LIMIT", "SL", "SL_M"]).describe("Order type"), product: z.enum(["CNC", "MIS", "NRML"]).describe("Product type"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), price: z.number().optional().describe("Price for limit orders"), })).describe("Array of order objects to calculate margin for"), }, async ({ segment, orders }) => { try { const data = await makeRequest(`https://api.groww.in/v1/margins/detail/orders?segment=${segment}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(orders), }); return { content: [ { type: "text", text: `Margin Requirement:\n\nTotal Requirement: ₹${data.total_requirement}\nExposure Required: ₹${data.exposure_required || 0}\nSpan Required: ₹${data.span_required || 0}\nOption Buy Premium: ₹${data.option_buy_premium || 0}\nBrokerage & Charges: ₹${data.brokerage_and_charges || 0}\nCNC Margin Required: ₹${data.cash_cnc_margin_required || 0}\nMIS Margin Required: ₹${data.cash_mis_margin_required || 0}\nPhysical Delivery Margin: ₹${data.physical_delivery_margin_requirement || 0}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error calculating margin: ${error}` }], }; } } ); // ==================== LIVE DATA ==================== server.tool( "get_live_quote", "Get complete live market data for an instrument", { trading_symbol: z.string().describe("Trading symbol (e.g., 'RELIANCE', 'NIFTY')"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), }, async ({ trading_symbol, exchange, segment }) => { try { const data = await makeRequest(`https://api.groww.in/v1/live-data/quote?trading_symbol=${trading_symbol}&exchange=${exchange}&segment=${segment}`); return { content: [ { type: "text", text: `Live Quote for ${trading_symbol}:\n\nLast Price: ₹${data.last_price}\nDay Change: ₹${data.day_change} (${data.day_change_perc}%)\nOHLC: O:₹${data.ohlc?.open} H:₹${data.ohlc?.high} L:₹${data.ohlc?.low} C:₹${data.ohlc?.close}\nVolume: ${data.volume}\nBid: ₹${data.bid_price} (${data.bid_quantity})\nOffer: ₹${data.offer_price} (${data.offer_quantity})\nCircuit Limits: ₹${data.lower_circuit_limit} - ₹${data.upper_circuit_limit}\n52W Range: ₹${data.week_52_low} - ₹${data.week_52_high}\nAvg Price: ₹${data.average_price}\nMarket Cap: ₹${data.market_cap}\nTotal Buy Qty: ${data.total_buy_quantity}\nTotal Sell Qty: ${data.total_sell_quantity}\nLast Trade: ${data.last_trade_quantity} @ ${new Date(data.last_trade_time).toLocaleString()}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting live quote: ${error}` }], }; } } ); server.tool( "get_ltp", "Get Last Traded Price for multiple instruments (up to 50)", { segment: z.enum(["CASH", "FNO"]).describe("Market segment"), exchange_symbols: z.array(z.string()).max(50).describe("Array of exchange_symbol pairs like ['NSE_RELIANCE', 'BSE_SENSEX']"), }, async ({ segment, exchange_symbols }) => { try { const symbolsParam = exchange_symbols.join(','); const url = `https://api.groww.in/v1/live-data/ltp?segment=${segment}&exchange_symbols=${symbolsParam}`; if (config.debug) { console.log(`LTP Request URL: ${url}`); } const data = await makeRequest(url); if (config.debug) { console.log(`LTP Response:`, data); } const ltpSummary = Object.entries(data).map(([symbol, price]) => `${symbol}: ₹${price}` ).join('\n'); return { content: [ { type: "text", text: `Last Traded Prices:\n\n${ltpSummary}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting LTP: ${error}` }], }; } } ); server.tool( "get_ohlc", "Get OHLC data for multiple instruments (up to 50)", { segment: z.enum(["CASH", "FNO"]).describe("Market segment"), exchange_symbols: z.array(z.string()).max(50).describe("Array of exchange_symbol pairs like ['NSE_RELIANCE', 'BSE_SENSEX']"), }, async ({ segment, exchange_symbols }) => { try { const symbolsParam = exchange_symbols.join(','); const data = await makeRequest(`https://api.groww.in/v1/live-data/ohlc?segment=${segment}&exchange_symbols=${symbolsParam}`); const ohlcSummary = Object.entries(data).map(([symbol, ohlcData]: [string, any]) => `${symbol}: O:₹${ohlcData.open} H:₹${ohlcData.high} L:₹${ohlcData.low} C:₹${ohlcData.close}` ).join('\n'); return { content: [ { type: "text", text: `OHLC Data:\n\n${ohlcSummary}` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting OHLC: ${error}` }], }; } } ); // ==================== HISTORICAL DATA ==================== // Helper function to validate historical data constraints const validateHistoricalDataRequest = (interval_in_minutes: number, start_time: string, end_time: string) => { const startDate = new Date(start_time); const endDate = new Date(end_time); const now = new Date(); // Calculate duration in days const durationMs = endDate.getTime() - startDate.getTime(); const durationDays = Math.ceil(durationMs / (1000 * 60 * 60 * 24)); // Calculate how many months ago the start date is const monthsAgo = (now.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 30); // Define constraints based on interval const constraints: Record<number, { maxDuration: number, maxHistoryMonths: number | null }> = { 1: { maxDuration: 3, maxHistoryMonths: 3 }, 5: { maxDuration: 15, maxHistoryMonths: 3 }, 10: { maxDuration: 30, maxHistoryMonths: 3 }, 60: { maxDuration: 150, maxHistoryMonths: 3 }, 240: { maxDuration: 365, maxHistoryMonths: 3 }, 1440: { maxDuration: 1080, maxHistoryMonths: null }, // No limit 10080: { maxDuration: Infinity, maxHistoryMonths: null } // No limit }; const constraint = constraints[interval_in_minutes]; if (!constraint) { return { valid: false, error: `Unsupported interval: ${interval_in_minutes} minutes. Supported: 1, 5, 10, 60, 240, 1440, 10080` }; } // Check duration limit if (durationDays > constraint.maxDuration) { return { valid: false, error: `Duration too long for ${interval_in_minutes}min interval. Max: ${constraint.maxDuration} days, Requested: ${durationDays} days` }; } // Check historical data availability (except for daily and weekly) if (constraint.maxHistoryMonths && monthsAgo > constraint.maxHistoryMonths) { return { valid: false, error: `Data too old for ${interval_in_minutes}min interval. Max: ${constraint.maxHistoryMonths} months ago, Requested: ${monthsAgo.toFixed(1)} months ago` }; } return { valid: true }; }; server.tool( "get_current_date", "Get current date and time information for reference in historical data requests", {}, async () => { try { const now = new Date(); const istOffset = 5.5 * 60 * 60 * 1000; // IST is UTC+5:30 const istTime = new Date(now.getTime() + istOffset); const formatDate = (date: Date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; }; // Calculate useful date ranges for different intervals const today = new Date(istTime); today.setHours(0, 0, 0, 0); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const lastWeek = new Date(today); lastWeek.setDate(lastWeek.getDate() - 7); const lastMonth = new Date(today); lastMonth.setMonth(lastMonth.getMonth() - 1); const last3Months = new Date(today); last3Months.setMonth(last3Months.getMonth() - 3); const lastYear = new Date(today); lastYear.setFullYear(lastYear.getFullYear() - 1); return { content: [ { type: "text", text: `Current Date & Time Information:\n\nCurrent IST Time: ${formatDate(istTime)}\nCurrent UTC Time: ${formatDate(now)}\n\n📅 USEFUL DATE RANGES FOR HISTORICAL DATA:\n\nFor 1-min data (max 3 days):\n• Yesterday: ${formatDate(yesterday)} to ${formatDate(today)}\n• Last 2 days: ${formatDate(new Date(today.getTime() - 2*24*60*60*1000))} to ${formatDate(today)}\n\nFor 5-min data (max 15 days):\n• Last week: ${formatDate(lastWeek)} to ${formatDate(today)}\n• Last 15 days: ${formatDate(new Date(today.getTime() - 15*24*60*60*1000))} to ${formatDate(today)}\n\nFor hourly data (max 150 days):\n• Last month: ${formatDate(lastMonth)} to ${formatDate(today)}\n• Last 3 months: ${formatDate(last3Months)} to ${formatDate(today)}\n\nFor daily data (max 3 years):\n• Last year: ${formatDate(lastYear)} to ${formatDate(today)}\n• Max range: ${formatDate(new Date(today.getTime() - 1080*24*60*60*1000))} to ${formatDate(today)}\n\n⚠️ DATA AVAILABILITY LIMITS:\n• 1min, 5min, 10min, 1hour, 4hour: Last 3 months only\n• Daily, Weekly: Full history available\n\nMarket Hours: 9:15 AM - 3:30 PM IST (Mon-Fri)` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting current date: ${error}` }], }; } } ); server.tool( "get_historical_data", "Get historical candle data for an instrument. CONSTRAINTS: 1min(3days,3mo), 5min(15days,3mo), 10min(30days,3mo), 1hr(150days,3mo), 4hr(365days,3mo), daily(1080days,unlimited), weekly(unlimited,unlimited)", { trading_symbol: z.string().describe("Trading symbol"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), start_time: z.string().describe("Start time in 'YYYY-MM-DD HH:mm:ss' format or epoch milliseconds"), end_time: z.string().describe("End time in 'YYYY-MM-DD HH:mm:ss' format or epoch milliseconds"), interval_in_minutes: z.number().int().optional().default(5).describe("Candle interval in minutes (1, 5, 10, 60, 240, 1440, 10080)"), }, async ({ trading_symbol, exchange, segment, start_time, end_time, interval_in_minutes }) => { try { // Validate request constraints const validation = validateHistoricalDataRequest(interval_in_minutes!, start_time, end_time); if (!validation.valid) { return { content: [{ type: "text", text: `❌ CONSTRAINT VIOLATION: ${validation.error}\n\nUse 'get_current_date' tool to see valid date ranges for each interval.\n\nGroww API Limits:\n• 1min: Max 3 days per request, Last 3 months available\n• 5min: Max 15 days per request, Last 3 months available\n• 10min: Max 30 days per request, Last 3 months available\n• 1hour: Max 150 days per request, Last 3 months available\n• 4hour: Max 365 days per request, Last 3 months available\n• Daily: Max 1080 days per request, Full history available\n• Weekly: No limit, Full history available` }], }; } // Build URL according to official API docs parameter order const params = new URLSearchParams(); params.append('exchange', exchange); params.append('segment', segment); params.append('trading_symbol', trading_symbol); params.append('start_time', start_time); params.append('end_time', end_time); if (interval_in_minutes) { params.append('interval_in_minutes', interval_in_minutes.toString()); } const url = `https://api.groww.in/v1/historical/candle/range?${params.toString()}`; if (config.debug) { console.log(`Historical Data Request URL: ${url}`); } const data = await makeRequest(url); if (config.debug) { console.log(`Historical Data Response:`, JSON.stringify(data, null, 2)); } const candlesSummary = data.candles.slice(0, 10).map((candle: any[]) => { const [timestamp, open, high, low, close, volume] = candle; const date = new Date(timestamp * 1000).toLocaleString(); return `${date}: O:₹${open} H:₹${high} L:₹${low} C:₹${close} V:${volume}`; }).join('\n'); const totalCandles = data.candles.length; const showingText = totalCandles > 10 ? `\n\n... showing first 10 of ${totalCandles} candles` : ''; // Calculate data coverage const startDate = new Date(data.candles[0][0] * 1000).toLocaleDateString(); const endDate = new Date(data.candles[data.candles.length - 1][0] * 1000).toLocaleDateString(); return { content: [ { type: "text", text: `Historical Data for ${trading_symbol}:\nInterval: ${data.interval_in_minutes} minutes\nData Coverage: ${startDate} to ${endDate}\nTotal Candles: ${totalCandles}\n\n${candlesSummary}${showingText}\n\n✅ Request within API limits for ${interval_in_minutes}-minute interval` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error getting historical data: ${error}` }], }; } } ); // ==================== TECHNICAL ANALYSIS ==================== server.tool( "calculate_moving_averages", "Calculate Simple Moving Average (SMA) and Exponential Moving Average (EMA) for historical data", { trading_symbol: z.string().describe("Trading symbol"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), start_time: z.string().describe("Start time in 'YYYY-MM-DD HH:mm:ss' format"), end_time: z.string().describe("End time in 'YYYY-MM-DD HH:mm:ss' format"), interval_in_minutes: z.number().int().default(5).describe("Candle interval in minutes"), periods: z.array(z.number().int().positive()).default([5, 10, 20, 50]).describe("Periods for moving averages (e.g., [5, 10, 20, 50])"), }, async ({ trading_symbol, exchange, segment, start_time, end_time, interval_in_minutes, periods }) => { try { // Validate request constraints const validation = validateHistoricalDataRequest(interval_in_minutes, start_time, end_time); if (!validation.valid) { return { content: [{ type: "text", text: `❌ CONSTRAINT VIOLATION: ${validation.error}\n\nUse 'get_current_date' tool to see valid date ranges for each interval.` }], }; } // Get historical data first const params = new URLSearchParams(); params.append('exchange', exchange); params.append('segment', segment); params.append('trading_symbol', trading_symbol); params.append('start_time', start_time); params.append('end_time', end_time); params.append('interval_in_minutes', interval_in_minutes.toString()); const data = await makeRequest(`https://api.groww.in/v1/historical/candle/range?${params.toString()}`); if (!data.candles || data.candles.length === 0) { return { content: [{ type: "text", text: "No historical data available for the specified period." }], }; } const closes = data.candles.map((candle: any[]) => candle[4]); // close prices const timestamps = data.candles.map((candle: any[]) => candle[0]); // Calculate SMA and EMA for each period const results = periods.map(period => { if (period > closes.length) { return `${period}-period: Not enough data (need ${period}, have ${closes.length})`; } // Simple Moving Average const smaValues = []; for (let i = period - 1; i < closes.length; i++) { const sum = closes.slice(i - period + 1, i + 1).reduce((a: number, b: number) => a + b, 0); smaValues.push(sum / period); } // Exponential Moving Average const multiplier = 2 / (period + 1); const emaValues = []; emaValues[0] = closes[period - 1]; // Start with SMA for (let i = 1; i < closes.length - period + 1; i++) { emaValues[i] = (closes[period - 1 + i] * multiplier) + (emaValues[i - 1] * (1 - multiplier)); } const latestSMA = smaValues[smaValues.length - 1]; const latestEMA = emaValues[emaValues.length - 1]; const currentPrice = closes[closes.length - 1]; return `${period}-period: SMA=₹${latestSMA.toFixed(2)}, EMA=₹${latestEMA.toFixed(2)} | Current: ₹${currentPrice}`; }); const currentPrice = closes[closes.length - 1]; const latestTime = new Date(timestamps[timestamps.length - 1] * 1000).toLocaleString(); return { content: [ { type: "text", text: `Moving Averages for ${trading_symbol}:\nCurrent Price: ₹${currentPrice} (${latestTime})\nData Points: ${closes.length} candles\n\n${results.join('\n')}\n\nInterpretation:\n- SMA: Simple average of closing prices\n- EMA: Exponential average giving more weight to recent prices\n- Price above MA = Bullish trend\n- Price below MA = Bearish trend` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error calculating moving averages: ${error}` }], }; } } ); server.tool( "calculate_rsi", "Calculate Relative Strength Index (RSI) for technical analysis", { trading_symbol: z.string().describe("Trading symbol"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), start_time: z.string().describe("Start time in 'YYYY-MM-DD HH:mm:ss' format"), end_time: z.string().describe("End time in 'YYYY-MM-DD HH:mm:ss' format"), interval_in_minutes: z.number().int().default(5).describe("Candle interval in minutes"), period: z.number().int().default(14).describe("RSI period (default: 14)"), }, async ({ trading_symbol, exchange, segment, start_time, end_time, interval_in_minutes, period }) => { try { // Validate request constraints const validation = validateHistoricalDataRequest(interval_in_minutes, start_time, end_time); if (!validation.valid) { return { content: [{ type: "text", text: `❌ CONSTRAINT VIOLATION: ${validation.error}\n\nUse 'get_current_date' tool to see valid date ranges for each interval.` }], }; } const params = new URLSearchParams(); params.append('exchange', exchange); params.append('segment', segment); params.append('trading_symbol', trading_symbol); params.append('start_time', start_time); params.append('end_time', end_time); params.append('interval_in_minutes', interval_in_minutes.toString()); const data = await makeRequest(`https://api.groww.in/v1/historical/candle/range?${params.toString()}`); if (!data.candles || data.candles.length < period + 1) { return { content: [{ type: "text", text: `Not enough data for RSI calculation. Need at least ${period + 1} candles, have ${data.candles?.length || 0}.` }], }; } const closes = data.candles.map((candle: any[]) => candle[4]); // Calculate price changes const changes = []; for (let i = 1; i < closes.length; i++) { changes.push(closes[i] - closes[i - 1]); } // Calculate gains and losses const gains = changes.map(change => change > 0 ? change : 0); const losses = changes.map(change => change < 0 ? Math.abs(change) : 0); // Calculate initial averages let avgGain = gains.slice(0, period).reduce((a, b) => a + b, 0) / period; let avgLoss = losses.slice(0, period).reduce((a, b) => a + b, 0) / period; const rsiValues = []; // Calculate RSI values for (let i = period; i < changes.length; i++) { if (avgLoss === 0) { rsiValues.push(100); } else { const rs = avgGain / avgLoss; const rsi = 100 - (100 / (1 + rs)); rsiValues.push(rsi); } // Update averages for next iteration (Wilder's smoothing) avgGain = ((avgGain * (period - 1)) + gains[i]) / period; avgLoss = ((avgLoss * (period - 1)) + losses[i]) / period; } const latestRSI = rsiValues[rsiValues.length - 1]; const currentPrice = closes[closes.length - 1]; // RSI interpretation let interpretation = ""; let signal = ""; if (latestRSI >= 70) { interpretation = "🔴 OVERBOUGHT - Consider selling/taking profits"; signal = "Consider selling or booking profits"; } else if (latestRSI <= 30) { interpretation = "🟢 OVERSOLD - Consider buying/accumulating"; signal = "Consider buying or accumulating"; } else if (latestRSI >= 50) { interpretation = "🟡 BULLISH MOMENTUM - Above midline"; signal = "Upward momentum continues"; } else { interpretation = "🟡 BEARISH MOMENTUM - Below midline"; signal = "Downward momentum continues"; } return { content: [ { type: "text", text: `RSI Analysis for ${trading_symbol}:\nCurrent Price: ₹${currentPrice}\nRSI (${period}-period): ${latestRSI.toFixed(2)}\n\n${interpretation}\nSignal: ${signal}\n\nRSI Scale:\n• 0-30: Oversold (potential buy signal)\n• 30-70: Normal range\n• 70-100: Overbought (potential sell signal)\n\nNote: RSI is a momentum oscillator measuring speed and magnitude of price changes.` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error calculating RSI: ${error}` }], }; } } ); server.tool( "calculate_bollinger_bands", "Calculate Bollinger Bands for volatility analysis", { trading_symbol: z.string().describe("Trading symbol"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), start_time: z.string().describe("Start time in 'YYYY-MM-DD HH:mm:ss' format"), end_time: z.string().describe("End time in 'YYYY-MM-DD HH:mm:ss' format"), interval_in_minutes: z.number().int().default(5).describe("Candle interval in minutes"), period: z.number().int().default(20).describe("Moving average period (default: 20)"), std_dev: z.number().default(2).describe("Standard deviation multiplier (default: 2)"), }, async ({ trading_symbol, exchange, segment, start_time, end_time, interval_in_minutes, period, std_dev }) => { try { // Validate request constraints const validation = validateHistoricalDataRequest(interval_in_minutes, start_time, end_time); if (!validation.valid) { return { content: [{ type: "text", text: `❌ CONSTRAINT VIOLATION: ${validation.error}\n\nUse 'get_current_date' tool to see valid date ranges for each interval.` }], }; } const params = new URLSearchParams(); params.append('exchange', exchange); params.append('segment', segment); params.append('trading_symbol', trading_symbol); params.append('start_time', start_time); params.append('end_time', end_time); params.append('interval_in_minutes', interval_in_minutes.toString()); const data = await makeRequest(`https://api.groww.in/v1/historical/candle/range?${params.toString()}`); if (!data.candles || data.candles.length < period) { return { content: [{ type: "text", text: `Not enough data for Bollinger Bands. Need at least ${period} candles, have ${data.candles?.length || 0}.` }], }; } const closes = data.candles.map((candle: any[]) => candle[4]); // Calculate the last set of Bollinger Bands const recentCloses = closes.slice(-period); const sma = recentCloses.reduce((a: number, b: number) => a + b, 0) / period; // Calculate standard deviation const variance = recentCloses.reduce((acc: number, price: number) => acc + Math.pow(price - sma, 2), 0) / period; const standardDeviation = Math.sqrt(variance); const upperBand = sma + (std_dev * standardDeviation); const lowerBand = sma - (std_dev * standardDeviation); const currentPrice = closes[closes.length - 1]; // Band position percentage const bandPosition = ((currentPrice - lowerBand) / (upperBand - lowerBand)) * 100; // Interpretation let interpretation = ""; let signal = ""; if (currentPrice >= upperBand) { interpretation = "🔴 PRICE AT/ABOVE UPPER BAND - Potentially overbought"; signal = "Consider selling or booking profits"; } else if (currentPrice <= lowerBand) { interpretation = "🟢 PRICE AT/BELOW LOWER BAND - Potentially oversold"; signal = "Consider buying or accumulating"; } else if (bandPosition > 80) { interpretation = "🟡 PRICE NEAR UPPER BAND - Approaching resistance"; signal = "Monitor for reversal signals"; } else if (bandPosition < 20) { interpretation = "🟡 PRICE NEAR LOWER BAND - Approaching support"; signal = "Monitor for bounce signals"; } else { interpretation = "⚪ PRICE IN MIDDLE BAND RANGE - Normal volatility"; signal = "Neutral - follow the trend"; } const bandwidth = ((upperBand - lowerBand) / sma) * 100; return { content: [ { type: "text", text: `Bollinger Bands Analysis for ${trading_symbol}:\n\nCurrent Price: ₹${currentPrice.toFixed(2)}\nMiddle Band (SMA-${period}): ₹${sma.toFixed(2)}\nUpper Band (+${std_dev}σ): ₹${upperBand.toFixed(2)}\nLower Band (-${std_dev}σ): ₹${lowerBand.toFixed(2)}\n\nBand Position: ${bandPosition.toFixed(1)}%\nBandwidth: ${bandwidth.toFixed(2)}%\n\n${interpretation}\nSignal: ${signal}\n\nBollinger Bands Guide:\n• Price touching upper band = potential sell signal\n• Price touching lower band = potential buy signal\n• Band squeeze (narrow bands) = low volatility, breakout expected\n• Band expansion = high volatility period` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error calculating Bollinger Bands: ${error}` }], }; } } ); server.tool( "calculate_support_resistance", "Identify support and resistance levels from historical price data", { trading_symbol: z.string().describe("Trading symbol"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), start_time: z.string().describe("Start time in 'YYYY-MM-DD HH:mm:ss' format"), end_time: z.string().describe("End time in 'YYYY-MM-DD HH:mm:ss' format"), interval_in_minutes: z.number().int().default(60).describe("Candle interval in minutes (use longer intervals for better levels)"), min_touches: z.number().int().default(2).describe("Minimum times a level should be touched (default: 2)"), }, async ({ trading_symbol, exchange, segment, start_time, end_time, interval_in_minutes, min_touches }) => { try { // Validate request constraints const validation = validateHistoricalDataRequest(interval_in_minutes, start_time, end_time); if (!validation.valid) { return { content: [{ type: "text", text: `❌ CONSTRAINT VIOLATION: ${validation.error}\n\nUse 'get_current_date' tool to see valid date ranges for each interval.` }], }; } const params = new URLSearchParams(); params.append('exchange', exchange); params.append('segment', segment); params.append('trading_symbol', trading_symbol); params.append('start_time', start_time); params.append('end_time', end_time); params.append('interval_in_minutes', interval_in_minutes.toString()); const data = await makeRequest(`https://api.groww.in/v1/historical/candle/range?${params.toString()}`); if (!data.candles || data.candles.length < 10) { return { content: [{ type: "text", text: "Not enough historical data to identify support/resistance levels." }], }; } const candles = data.candles.map((candle: any[]) => ({ timestamp: candle[0], open: candle[1], high: candle[2], low: candle[3], close: candle[4], volume: candle[5] })); // Find pivot highs and lows const pivotHighs = []; const pivotLows = []; for (let i = 2; i < candles.length - 2; i++) { const current = candles[i]; // Pivot High: high is higher than 2 candles on each side if (current.high > candles[i-1].high && current.high > candles[i-2].high && current.high > candles[i+1].high && current.high > candles[i+2].high) { pivotHighs.push({ price: current.high, timestamp: current.timestamp }); } // Pivot Low: low is lower than 2 candles on each side if (current.low < candles[i-1].low && current.low < candles[i-2].low && current.low < candles[i+1].low && current.low < candles[i+2].low) { pivotLows.push({ price: current.low, timestamp: current.timestamp }); } } // Group similar price levels (within 1% range) const groupLevels = (pivots: any[], tolerance = 0.01) => { const groups: any[] = []; pivots.forEach(pivot => { let addedToGroup = false; for (let group of groups) { const avgPrice = group.reduce((sum: number, p: any) => sum + p.price, 0) / group.length; if (Math.abs(pivot.price - avgPrice) / avgPrice <= tolerance) { group.push(pivot); addedToGroup = true; break; } } if (!addedToGroup) { groups.push([pivot]); } }); return groups.filter(group => group.length >= min_touches).map(group => ({ price: group.reduce((sum: number, p: any) => sum + p.price, 0) / group.length, touches: group.length, lastTouch: Math.max(...group.map((p: any) => p.timestamp)) })); }; const resistanceLevels = groupLevels(pivotHighs); const supportLevels = groupLevels(pivotLows); // Sort by strength (number of touches) resistanceLevels.sort((a, b) => b.touches - a.touches); supportLevels.sort((a, b) => b.touches - a.touches); const currentPrice = candles[candles.length - 1].close; // Find nearest levels const nearestResistance = resistanceLevels.find(level => level.price > currentPrice); const nearestSupport = supportLevels.find(level => level.price < currentPrice); let summary = `Support & Resistance Analysis for ${trading_symbol}:\nCurrent Price: ₹${currentPrice.toFixed(2)}\n\n`; if (nearestResistance) { const distance = ((nearestResistance.price - currentPrice) / currentPrice * 100); summary += `🔴 NEAREST RESISTANCE: ₹${nearestResistance.price.toFixed(2)} (${distance.toFixed(1)}% above)\n Strength: ${nearestResistance.touches} touches\n Last touched: ${new Date(nearestResistance.lastTouch * 1000).toLocaleDateString()}\n\n`; } if (nearestSupport) { const distance = ((currentPrice - nearestSupport.price) / currentPrice * 100); summary += `🟢 NEAREST SUPPORT: ₹${nearestSupport.price.toFixed(2)} (${distance.toFixed(1)}% below)\n Strength: ${nearestSupport.touches} touches\n Last touched: ${new Date(nearestSupport.lastTouch * 1000).toLocaleDateString()}\n\n`; } if (resistanceLevels.length > 1) { summary += `Other Resistance Levels:\n${resistanceLevels.slice(0, 3).map(level => `• ₹${level.price.toFixed(2)} (${level.touches} touches)` ).join('\n')}\n\n`; } if (supportLevels.length > 1) { summary += `Other Support Levels:\n${supportLevels.slice(0, 3).map(level => `• ₹${level.price.toFixed(2)} (${level.touches} touches)` ).join('\n')}\n\n`; } summary += `Analysis Notes:\n• Support: Price level where buying interest emerges\n• Resistance: Price level where selling pressure increases\n• More touches = stronger level\n• Breakout above resistance or below support can signal strong moves`; return { content: [ { type: "text", text: summary } ], }; } catch (error) { return { content: [{ type: "text", text: `Error calculating support/resistance: ${error}` }], }; } } ); server.tool( "calculate_volatility_metrics", "Calculate various volatility metrics for risk assessment", { trading_symbol: z.string().describe("Trading symbol"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), start_time: z.string().describe("Start time in 'YYYY-MM-DD HH:mm:ss' format"), end_time: z.string().describe("End time in 'YYYY-MM-DD HH:mm:ss' format"), interval_in_minutes: z.number().int().default(1440).describe("Candle interval in minutes (default: 1440 for daily)"), }, async ({ trading_symbol, exchange, segment, start_time, end_time, interval_in_minutes }) => { try { // Validate request constraints const validation = validateHistoricalDataRequest(interval_in_minutes, start_time, end_time); if (!validation.valid) { return { content: [{ type: "text", text: `❌ CONSTRAINT VIOLATION: ${validation.error}\n\nUse 'get_current_date' tool to see valid date ranges for each interval.` }], }; } const params = new URLSearchParams(); params.append('exchange', exchange); params.append('segment', segment); params.append('trading_symbol', trading_symbol); params.append('start_time', start_time); params.append('end_time', end_time); params.append('interval_in_minutes', interval_in_minutes.toString()); const data = await makeRequest(`https://api.groww.in/v1/historical/candle/range?${params.toString()}`); if (!data.candles || data.candles.length < 2) { return { content: [{ type: "text", text: "Not enough data for volatility calculation." }], }; } const candles = data.candles.map((candle: any[]) => ({ open: candle[1], high: candle[2], low: candle[3], close: candle[4], volume: candle[5] })); // Calculate daily returns const returns = []; for (let i = 1; i < candles.length; i++) { const dailyReturn = (candles[i].close - candles[i-1].close) / candles[i-1].close; returns.push(dailyReturn); } // Historical Volatility (annualized) const meanReturn = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - meanReturn, 2), 0) / (returns.length - 1); const dailyVolatility = Math.sqrt(variance); const annualizedVolatility = dailyVolatility * Math.sqrt(252) * 100; // 252 trading days // Average True Range (ATR) let atrSum = 0; for (let i = 1; i < candles.length; i++) { const tr1 = candles[i].high - candles[i].low; const tr2 = Math.abs(candles[i].high - candles[i-1].close); const tr3 = Math.abs(candles[i].low - candles[i-1].close); const trueRange = Math.max(tr1, tr2, tr3); atrSum += trueRange; } const atr = atrSum / (candles.length - 1); // Price range statistics const currentPrice = candles[candles.length - 1].close; const atrPercentage = (atr / currentPrice) * 100; // VIX-like calculation (simplified) const highLowRanges = candles.map((candle: any) => (candle.high - candle.low) / candle.close); const avgHighLowRange = highLowRanges.reduce((sum: number, range: number) => sum + range, 0) / highLowRanges.length * 100; // Risk assessment let riskLevel = ""; if (annualizedVolatility < 15) { riskLevel = "🟢 LOW VOLATILITY - Conservative investment"; } else if (annualizedVolatility < 25) { riskLevel = "🟡 MODERATE VOLATILITY - Balanced risk"; } else if (annualizedVolatility < 40) { riskLevel = "🟠 HIGH VOLATILITY - Aggressive investment"; } else { riskLevel = "🔴 VERY HIGH VOLATILITY - Speculative"; } // Sharpe ratio (simplified - assuming 6% risk-free rate) const riskFreeRate = 0.06; const excessReturn = (meanReturn * 252) - riskFreeRate; const sharpeRatio = excessReturn / (dailyVolatility * Math.sqrt(252)); return { content: [ { type: "text", text: `Volatility Analysis for ${trading_symbol}:\n\nCurrent Price: ₹${currentPrice.toFixed(2)}\nData Period: ${data.start_time} to ${data.end_time}\nCandles Analyzed: ${candles.length}\n\n📊 VOLATILITY METRICS:\n• Annualized Volatility: ${annualizedVolatility.toFixed(2)}%\n• Average True Range: ₹${atr.toFixed(2)} (${atrPercentage.toFixed(2)}% of price)\n• Daily High-Low Range: ${avgHighLowRange.toFixed(2)}%\n• Sharpe Ratio: ${sharpeRatio.toFixed(2)}\n\n${riskLevel}\n\n💡 INTERPRETATION:\n• Volatility shows price movement intensity\n• Higher volatility = higher risk & potential returns\n• ATR useful for setting stop-losses\n• Sharpe ratio measures risk-adjusted returns (>1 is good)\n\nRisk Management:\n• Position size should be inverse to volatility\n• Use wider stops for high volatility stocks\n• Consider volatility when timing entries/exits` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error calculating volatility metrics: ${error}` }], }; } } ); server.tool( "calculate_macd", "Calculate MACD (Moving Average Convergence Divergence) for trend and momentum analysis", { trading_symbol: z.string().describe("Trading symbol"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), start_time: z.string().describe("Start time in 'YYYY-MM-DD HH:mm:ss' format"), end_time: z.string().describe("End time in 'YYYY-MM-DD HH:mm:ss' format"), interval_in_minutes: z.number().int().default(5).describe("Candle interval in minutes"), fast_period: z.number().int().default(12).describe("Fast EMA period (default: 12)"), slow_period: z.number().int().default(26).describe("Slow EMA period (default: 26)"), signal_period: z.number().int().default(9).describe("Signal line EMA period (default: 9)"), }, async ({ trading_symbol, exchange, segment, start_time, end_time, interval_in_minutes, fast_period, slow_period, signal_period }) => { try { // Validate request constraints const validation = validateHistoricalDataRequest(interval_in_minutes, start_time, end_time); if (!validation.valid) { return { content: [{ type: "text", text: `❌ CONSTRAINT VIOLATION: ${validation.error}\n\nUse 'get_current_date' tool to see valid date ranges for each interval.` }], }; } const params = new URLSearchParams(); params.append('exchange', exchange); params.append('segment', segment); params.append('trading_symbol', trading_symbol); params.append('start_time', start_time); params.append('end_time', end_time); params.append('interval_in_minutes', interval_in_minutes.toString()); const data = await makeRequest(`https://api.groww.in/v1/historical/candle/range?${params.toString()}`); if (!data.candles || data.candles.length < slow_period + signal_period) { return { content: [{ type: "text", text: `Not enough data for MACD calculation. Need at least ${slow_period + signal_period} candles, have ${data.candles?.length || 0}.` }], }; } const closes = data.candles.map((candle: any[]) => candle[4]); // Calculate EMAs const calculateEMA = (prices: number[], period: number) => { const multiplier = 2 / (period + 1); const ema = [prices[0]]; for (let i = 1; i < prices.length; i++) { ema[i] = (prices[i] * multiplier) + (ema[i - 1] * (1 - multiplier)); } return ema; }; const fastEMA = calculateEMA(closes, fast_period); const slowEMA = calculateEMA(closes, slow_period); // Calculate MACD line const macdLine = []; for (let i = 0; i < closes.length; i++) { macdLine.push(fastEMA[i] - slowEMA[i]); } // Calculate Signal line (EMA of MACD) const signalLine = calculateEMA(macdLine, signal_period); // Calculate Histogram const histogram = []; for (let i = 0; i < macdLine.length; i++) { histogram.push(macdLine[i] - signalLine[i]); } const currentMACD = macdLine[macdLine.length - 1]; const currentSignal = signalLine[signalLine.length - 1]; const currentHistogram = histogram[histogram.length - 1]; const prevHistogram = histogram[histogram.length - 2]; // MACD interpretation let interpretation = ""; let signal = ""; if (currentMACD > currentSignal) { if (prevHistogram < 0 && currentHistogram > 0) { interpretation = "🟢 BULLISH CROSSOVER - MACD crossed above signal line"; signal = "Strong BUY signal - Consider entering long positions"; } else { interpretation = "�� BULLISH MOMENTUM - MACD above signal line"; signal = "Upward momentum continues"; } } else { if (prevHistogram > 0 && currentHistogram < 0) { interpretation = "🔴 BEARISH CROSSOVER - MACD crossed below signal line"; signal = "Strong SELL signal - Consider exiting/shorting"; } else { interpretation = "🟡 BEARISH MOMENTUM - MACD below signal line"; signal = "Downward momentum continues"; } } const currentPrice = closes[closes.length - 1]; return { content: [ { type: "text", text: `MACD Analysis for ${trading_symbol}:\nCurrent Price: ₹${currentPrice.toFixed(2)}\n\nMACD Line: ${currentMACD.toFixed(4)}\nSignal Line: ${currentSignal.toFixed(4)}\nHistogram: ${currentHistogram.toFixed(4)}\n\n${interpretation}\nSignal: ${signal}\n\nMACD Guide:\n• MACD above Signal = Bullish momentum\n• MACD below Signal = Bearish momentum\n• Histogram above zero = Strengthening trend\n• Histogram below zero = Weakening trend\n• Crossovers provide entry/exit signals` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error calculating MACD: ${error}` }], }; } } ); server.tool( "calculate_stochastic", "Calculate Stochastic Oscillator for momentum analysis", { trading_symbol: z.string().describe("Trading symbol"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), start_time: z.string().describe("Start time in 'YYYY-MM-DD HH:mm:ss' format"), end_time: z.string().describe("End time in 'YYYY-MM-DD HH:mm:ss' format"), interval_in_minutes: z.number().int().default(5).describe("Candle interval in minutes"), k_period: z.number().int().default(14).describe("%K period (default: 14)"), d_period: z.number().int().default(3).describe("%D period (default: 3)"), }, async ({ trading_symbol, exchange, segment, start_time, end_time, interval_in_minutes, k_period, d_period }) => { try { // Validate request constraints const validation = validateHistoricalDataRequest(interval_in_minutes, start_time, end_time); if (!validation.valid) { return { content: [{ type: "text", text: `❌ CONSTRAINT VIOLATION: ${validation.error}\n\nUse 'get_current_date' tool to see valid date ranges for each interval.` }], }; } const params = new URLSearchParams(); params.append('exchange', exchange); params.append('segment', segment); params.append('trading_symbol', trading_symbol); params.append('start_time', start_time); params.append('end_time', end_time); params.append('interval_in_minutes', interval_in_minutes.toString()); const data = await makeRequest(`https://api.groww.in/v1/historical/candle/range?${params.toString()}`); if (!data.candles || data.candles.length < k_period + d_period) { return { content: [{ type: "text", text: `Not enough data for Stochastic calculation. Need at least ${k_period + d_period} candles, have ${data.candles?.length || 0}.` }], }; } const candles = data.candles.map((candle: any[]) => ({ high: candle[2], low: candle[3], close: candle[4] })); // Calculate %K values const kValues = []; for (let i = k_period - 1; i < candles.length; i++) { const period_highs = candles.slice(i - k_period + 1, i + 1).map((c: any) => c.high); const period_lows = candles.slice(i - k_period + 1, i + 1).map((c: any) => c.low); const highest_high = Math.max(...period_highs); const lowest_low = Math.min(...period_lows); const current_close = candles[i].close; const k_value = ((current_close - lowest_low) / (highest_high - lowest_low)) * 100; kValues.push(k_value); } // Calculate %D values (SMA of %K) const dValues = []; for (let i = d_period - 1; i < kValues.length; i++) { const sum = kValues.slice(i - d_period + 1, i + 1).reduce((a: number, b: number) => a + b, 0); dValues.push(sum / d_period); } const currentK = kValues[kValues.length - 1]; const currentD = dValues[dValues.length - 1]; const prevK = kValues[kValues.length - 2]; const prevD = dValues[dValues.length - 2]; // Stochastic interpretation let interpretation = ""; let signal = ""; if (currentK >= 80 && currentD >= 80) { interpretation = "🔴 OVERBOUGHT ZONE - Both %K and %D above 80"; signal = "Consider selling/taking profits"; } else if (currentK <= 20 && currentD <= 20) { interpretation = "🟢 OVERSOLD ZONE - Both %K and %D below 20"; signal = "Consider buying/accumulating"; } else if (prevK <= prevD && currentK > currentD) { interpretation = "🟢 BULLISH CROSSOVER - %K crossed above %D"; signal = "Buy signal generated"; } else if (prevK >= prevD && currentK < currentD) { interpretation = "🔴 BEARISH CROSSOVER - %K crossed below %D"; signal = "Sell signal generated"; } else if (currentK > currentD) { interpretation = "🟡 BULLISH MOMENTUM - %K above %D"; signal = "Upward momentum"; } else { interpretation = "🟡 BEARISH MOMENTUM - %K below %D"; signal = "Downward momentum"; } const currentPrice = candles[candles.length - 1].close; return { content: [ { type: "text", text: `Stochastic Analysis for ${trading_symbol}:\nCurrent Price: ₹${currentPrice.toFixed(2)}\n\n%K: ${currentK.toFixed(2)}\n%D: ${currentD.toFixed(2)}\n\n${interpretation}\nSignal: ${signal}\n\nStochastic Guide:\n• 0-20: Oversold (potential buy zone)\n• 20-80: Normal range\n• 80-100: Overbought (potential sell zone)\n• %K crossing above %D = Buy signal\n• %K crossing below %D = Sell signal` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error calculating Stochastic: ${error}` }], }; } } ); server.tool( "calculate_williams_r", "Calculate Williams %R for momentum analysis", { trading_symbol: z.string().describe("Trading symbol"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), start_time: z.string().describe("Start time in 'YYYY-MM-DD HH:mm:ss' format"), end_time: z.string().describe("End time in 'YYYY-MM-DD HH:mm:ss' format"), interval_in_minutes: z.number().int().default(5).describe("Candle interval in minutes"), period: z.number().int().default(14).describe("Williams %R period (default: 14)"), }, async ({ trading_symbol, exchange, segment, start_time, end_time, interval_in_minutes, period }) => { try { // Validate request constraints const validation = validateHistoricalDataRequest(interval_in_minutes, start_time, end_time); if (!validation.valid) { return { content: [{ type: "text", text: `❌ CONSTRAINT VIOLATION: ${validation.error}\n\nUse 'get_current_date' tool to see valid date ranges for each interval.` }], }; } const params = new URLSearchParams(); params.append('exchange', exchange); params.append('segment', segment); params.append('trading_symbol', trading_symbol); params.append('start_time', start_time); params.append('end_time', end_time); params.append('interval_in_minutes', interval_in_minutes.toString()); const data = await makeRequest(`https://api.groww.in/v1/historical/candle/range?${params.toString()}`); if (!data.candles || data.candles.length < period) { return { content: [{ type: "text", text: `Not enough data for Williams %R calculation. Need at least ${period} candles, have ${data.candles?.length || 0}.` }], }; } const candles = data.candles.map((candle: any[]) => ({ high: candle[2], low: candle[3], close: candle[4] })); // Calculate Williams %R const williamsR = []; for (let i = period - 1; i < candles.length; i++) { const period_highs = candles.slice(i - period + 1, i + 1).map((c: any) => c.high); const period_lows = candles.slice(i - period + 1, i + 1).map((c: any) => c.low); const highest_high = Math.max(...period_highs); const lowest_low = Math.min(...period_lows); const current_close = candles[i].close; const wr_value = ((highest_high - current_close) / (highest_high - lowest_low)) * -100; williamsR.push(wr_value); } const currentWR = williamsR[williamsR.length - 1]; const prevWR = williamsR[williamsR.length - 2]; // Williams %R interpretation let interpretation = ""; let signal = ""; if (currentWR >= -20) { interpretation = "🔴 OVERBOUGHT - Williams %R above -20"; signal = "Consider selling/taking profits"; } else if (currentWR <= -80) { interpretation = "🟢 OVERSOLD - Williams %R below -80"; signal = "Consider buying/accumulating"; } else if (prevWR <= -80 && currentWR > -80) { interpretation = "🟢 OVERSOLD RECOVERY - Moving out of oversold territory"; signal = "Potential buy signal"; } else if (prevWR >= -20 && currentWR < -20) { interpretation = "🔴 OVERBOUGHT DECLINE - Moving out of overbought territory"; signal = "Potential sell signal"; } else if (currentWR > -50) { interpretation = "🟡 BULLISH BIAS - Williams %R in upper range"; signal = "Upward momentum"; } else { interpretation = "🟡 BEARISH BIAS - Williams %R in lower range"; signal = "Downward momentum"; } const currentPrice = candles[candles.length - 1].close; return { content: [ { type: "text", text: `Williams %R Analysis for ${trading_symbol}:\nCurrent Price: ₹${currentPrice.toFixed(2)}\n\nWilliams %R: ${currentWR.toFixed(2)}\n\n${interpretation}\nSignal: ${signal}\n\nWilliams %R Guide:\n• 0 to -20: Overbought (potential sell zone)\n• -20 to -80: Normal range\n• -80 to -100: Oversold (potential buy zone)\n• Move above -80 from oversold = Buy signal\n• Move below -20 from overbought = Sell signal` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error calculating Williams %R: ${error}` }], }; } } ); server.tool( "calculate_adx", "Calculate ADX (Average Directional Index) for trend strength analysis", { trading_symbol: z.string().describe("Trading symbol"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), start_time: z.string().describe("Start time in 'YYYY-MM-DD HH:mm:ss' format"), end_time: z.string().describe("End time in 'YYYY-MM-DD HH:mm:ss' format"), interval_in_minutes: z.number().int().default(5).describe("Candle interval in minutes"), period: z.number().int().default(14).describe("ADX period (default: 14)"), }, async ({ trading_symbol, exchange, segment, start_time, end_time, interval_in_minutes, period }) => { try { // Validate request constraints const validation = validateHistoricalDataRequest(interval_in_minutes, start_time, end_time); if (!validation.valid) { return { content: [{ type: "text", text: `❌ CONSTRAINT VIOLATION: ${validation.error}\n\nUse 'get_current_date' tool to see valid date ranges for each interval.` }], }; } const params = new URLSearchParams(); params.append('exchange', exchange); params.append('segment', segment); params.append('trading_symbol', trading_symbol); params.append('start_time', start_time); params.append('end_time', end_time); params.append('interval_in_minutes', interval_in_minutes.toString()); const data = await makeRequest(`https://api.groww.in/v1/historical/candle/range?${params.toString()}`); if (!data.candles || data.candles.length < period * 2) { return { content: [{ type: "text", text: `Not enough data for ADX calculation. Need at least ${period * 2} candles, have ${data.candles?.length || 0}.` }], }; } const candles = data.candles.map((candle: any[]) => ({ high: candle[2], low: candle[3], close: candle[4] })); // Calculate True Range, +DM, -DM const trueRanges = []; const plusDMs = []; const minusDMs = []; for (let i = 1; i < candles.length; i++) { const current = candles[i]; const previous = candles[i - 1]; // True Range const tr1 = current.high - current.low; const tr2 = Math.abs(current.high - previous.close); const tr3 = Math.abs(current.low - previous.close); const tr = Math.max(tr1, tr2, tr3); trueRanges.push(tr); // Directional Movement const plusDM = current.high - previous.high > previous.low - current.low ? Math.max(current.high - previous.high, 0) : 0; const minusDM = previous.low - current.low > current.high - previous.high ? Math.max(previous.low - current.low, 0) : 0; plusDMs.push(plusDM); minusDMs.push(minusDM); } // Calculate smoothed averages const smoothedTR = []; const smoothedPlusDM = []; const smoothedMinusDM = []; // Initial sums let trSum = trueRanges.slice(0, period).reduce((a: number, b: number) => a + b, 0); let plusDMSum = plusDMs.slice(0, period).reduce((a: number, b: number) => a + b, 0); let minusDMSum = minusDMs.slice(0, period).reduce((a: number, b: number) => a + b, 0); smoothedTR.push(trSum); smoothedPlusDM.push(plusDMSum); smoothedMinusDM.push(minusDMSum); // Wilder's smoothing for (let i = period; i < trueRanges.length; i++) { trSum = trSum - (trSum / period) + trueRanges[i]; plusDMSum = plusDMSum - (plusDMSum / period) + plusDMs[i]; minusDMSum = minusDMSum - (minusDMSum / period) + minusDMs[i]; smoothedTR.push(trSum); smoothedPlusDM.push(plusDMSum); smoothedMinusDM.push(minusDMSum); } // Calculate DI+ and DI- const plusDI = []; const minusDI = []; for (let i = 0; i < smoothedTR.length; i++) { plusDI.push((smoothedPlusDM[i] / smoothedTR[i]) * 100); minusDI.push((smoothedMinusDM[i] / smoothedTR[i]) * 100); } // Calculate DX and ADX const dx = []; for (let i = 0; i < plusDI.length; i++) { const diSum = plusDI[i] + minusDI[i]; const diDiff = Math.abs(plusDI[i] - minusDI[i]); dx.push(diSum !== 0 ? (diDiff / diSum) * 100 : 0); } // Calculate ADX (smoothed DX) const adx = []; if (dx.length >= period) { let adxSum = dx.slice(0, period).reduce((a: number, b: number) => a + b, 0) / period; adx.push(adxSum); for (let i = period; i < dx.length; i++) { adxSum = ((adxSum * (period - 1)) + dx[i]) / period; adx.push(adxSum); } } const currentADX = adx[adx.length - 1]; const currentPlusDI = plusDI[plusDI.length - 1]; const currentMinusDI = minusDI[minusDI.length - 1]; // ADX interpretation let trendStrength = ""; let trendDirection = ""; let signal = ""; if (currentADX >= 50) { trendStrength = "🔥 VERY STRONG TREND"; } else if (currentADX >= 25) { trendStrength = "🟠 STRONG TREND"; } else if (currentADX >= 20) { trendStrength = "🟡 MODERATE TREND"; } else { trendStrength = "⚪ WEAK TREND/SIDEWAYS"; } if (currentPlusDI > currentMinusDI) { trendDirection = "🟢 BULLISH DIRECTION (+DI > -DI)"; signal = currentADX >= 25 ? "Strong uptrend - Hold long positions" : "Weak upward movement"; } else { trendDirection = "🔴 BEARISH DIRECTION (-DI > +DI)"; signal = currentADX >= 25 ? "Strong downtrend - Avoid longs/consider shorts" : "Weak downward movement"; } const currentPrice = candles[candles.length - 1].close; return { content: [ { type: "text", text: `ADX Analysis for ${trading_symbol}:\nCurrent Price: ₹${currentPrice.toFixed(2)}\n\nADX: ${currentADX.toFixed(2)}\n+DI: ${currentPlusDI.toFixed(2)}\n-DI: ${currentMinusDI.toFixed(2)}\n\nTrend Strength: ${trendStrength}\nDirection: ${trendDirection}\nSignal: ${signal}\n\nADX Guide:\n• ADX > 25: Strong trend (tradeable)\n• ADX < 20: Weak trend/sideways (avoid trend strategies)\n• +DI > -DI: Bullish trend\n• -DI > +DI: Bearish trend\n• Rising ADX: Strengthening trend\n• Falling ADX: Weakening trend` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error calculating ADX: ${error}` }], }; } } ); server.tool( "calculate_fibonacci_levels", "Calculate Fibonacci retracement and extension levels for support/resistance analysis", { trading_symbol: z.string().describe("Trading symbol"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), start_time: z.string().describe("Start time in 'YYYY-MM-DD HH:mm:ss' format"), end_time: z.string().describe("End time in 'YYYY-MM-DD HH:mm:ss' format"), interval_in_minutes: z.number().int().default(60).describe("Candle interval in minutes"), trend_direction: z.enum(["UP", "DOWN", "AUTO"]).default("AUTO").describe("Trend direction for Fibonacci calculation"), }, async ({ trading_symbol, exchange, segment, start_time, end_time, interval_in_minutes, trend_direction }) => { try { // Validate request constraints const validation = validateHistoricalDataRequest(interval_in_minutes, start_time, end_time); if (!validation.valid) { return { content: [{ type: "text", text: `❌ CONSTRAINT VIOLATION: ${validation.error}\n\nUse 'get_current_date' tool to see valid date ranges for each interval.` }], }; } const params = new URLSearchParams(); params.append('exchange', exchange); params.append('segment', segment); params.append('trading_symbol', trading_symbol); params.append('start_time', start_time); params.append('end_time', end_time); params.append('interval_in_minutes', interval_in_minutes.toString()); const data = await makeRequest(`https://api.groww.in/v1/historical/candle/range?${params.toString()}`); if (!data.candles || data.candles.length < 10) { return { content: [{ type: "text", text: "Not enough historical data for Fibonacci calculation." }], }; } const candles = data.candles.map((candle: any[]) => ({ high: candle[2], low: candle[3], close: candle[4] })); // Find the highest high and lowest low in the period const highs = candles.map((c: any) => c.high); const lows = candles.map((c: any) => c.low); const maxHigh = Math.max(...highs); const minLow = Math.min(...lows); // Auto-detect trend direction if not specified let direction = trend_direction; if (direction === "AUTO") { const firstQuarter = candles.slice(0, Math.floor(candles.length / 4)); const lastQuarter = candles.slice(-Math.floor(candles.length / 4)); const avgEarlyPrice = firstQuarter.reduce((sum: number, c: any) => sum + c.close, 0) / firstQuarter.length; const avgLatePrice = lastQuarter.reduce((sum: number, c: any) => sum + c.close, 0) / lastQuarter.length; direction = avgLatePrice > avgEarlyPrice ? "UP" : "DOWN"; } // Fibonacci ratios const fibRatios = { retracement: [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0], extension: [1.272, 1.414, 1.618, 2.0, 2.618] }; let high, low; if (direction === "UP") { low = minLow; high = maxHigh; } else { high = maxHigh; low = minLow; } const range = high - low; const currentPrice = candles[candles.length - 1].close; // Calculate Fibonacci levels const retracementLevels = fibRatios.retracement.map(ratio => { const level = direction === "UP" ? high - (range * ratio) : low + (range * ratio); const distance = Math.abs(currentPrice - level); const distancePercent = (distance / currentPrice) * 100; return { ratio: ratio, price: level, distance: distancePercent, label: `${(ratio * 100).toFixed(1)}%` }; }); const extensionLevels = fibRatios.extension.map(ratio => { const level = direction === "UP" ? high + (range * (ratio - 1)) : low - (range * (ratio - 1)); const distance = Math.abs(currentPrice - level); const distancePercent = (distance / currentPrice) * 100; return { ratio: ratio, price: level, distance: distancePercent, label: `${(ratio * 100).toFixed(1)}%` }; }); // Find nearest levels const allLevels = [...retracementLevels, ...extensionLevels]; allLevels.sort((a, b) => a.distance - b.distance); const nearestLevel = allLevels[0]; // Determine if price is at a significant Fibonacci level let atFibLevel = ""; const tolerance = 1; // 1% tolerance const significantLevel = allLevels.find(level => level.distance <= tolerance); if (significantLevel) { atFibLevel = `🎯 PRICE NEAR FIBONACCI LEVEL: ${significantLevel.label} (₹${significantLevel.price.toFixed(2)})`; } const retracementSummary = retracementLevels.map(level => `${level.label}: ₹${level.price.toFixed(2)} (${level.distance.toFixed(1)}% away)` ).join('\n'); const extensionSummary = extensionLevels.map(level => `${level.label}: ₹${level.price.toFixed(2)} (${level.distance.toFixed(1)}% away)` ).join('\n'); return { content: [ { type: "text", text: `Fibonacci Analysis for ${trading_symbol}:\nCurrent Price: ₹${currentPrice.toFixed(2)}\nTrend Direction: ${direction}\nPrice Range: ₹${low.toFixed(2)} - ₹${high.toFixed(2)}\n\n${atFibLevel}\n\n📉 RETRACEMENT LEVELS:\n${retracementSummary}\n\n📈 EXTENSION LEVELS:\n${extensionSummary}\n\nNearest Level: ${nearestLevel.label} at ₹${nearestLevel.price.toFixed(2)}\n\nFibonacci Guide:\n• 38.2% & 61.8%: Strong support/resistance\n• 50%: Psychological level\n• 78.6%: Deep retracement (trend may reverse)\n• Extensions: Profit targets in trending markets\n• Price bounces off Fibonacci levels frequently` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error calculating Fibonacci levels: ${error}` }], }; } } ); server.tool( "analyze_candlestick_patterns", "Identify common candlestick patterns for reversal and continuation signals", { trading_symbol: z.string().describe("Trading symbol"), exchange: z.enum(["NSE", "BSE"]).describe("Exchange"), segment: z.enum(["CASH", "FNO"]).describe("Market segment"), start_time: z.string().describe("Start time in 'YYYY-MM-DD HH:mm:ss' format"), end_time: z.string().describe("End time in 'YYYY-MM-DD HH:mm:ss' format"), interval_in_minutes: z.number().int().default(60).describe("Candle interval in minutes"), lookback_candles: z.number().int().default(5).describe("Number of recent candles to analyze (default: 5)"), }, async ({ trading_symbol, exchange, segment, start_time, end_time, interval_in_minutes, lookback_candles }) => { try { // Validate request constraints const validation = validateHistoricalDataRequest(interval_in_minutes, start_time, end_time); if (!validation.valid) { return { content: [{ type: "text", text: `❌ CONSTRAINT VIOLATION: ${validation.error}\n\nUse 'get_current_date' tool to see valid date ranges for each interval.` }], }; } const params = new URLSearchParams(); params.append('exchange', exchange); params.append('segment', segment); params.append('trading_symbol', trading_symbol); params.append('start_time', start_time); params.append('end_time', end_time); params.append('interval_in_minutes', interval_in_minutes.toString()); const data = await makeRequest(`https://api.groww.in/v1/historical/candle/range?${params.toString()}`); if (!data.candles || data.candles.length < lookback_candles + 2) { return { content: [{ type: "text", text: "Not enough data for candlestick pattern analysis." }], }; } const candles = data.candles.slice(-lookback_candles - 2).map((candle: any[]) => ({ open: candle[1], high: candle[2], low: candle[3], close: candle[4], volume: candle[5] })); const patterns = []; // Helper functions const isBullish = (candle: any) => candle.close > candle.open; const isBearish = (candle: any) => candle.close < candle.open; const bodySize = (candle: any) => Math.abs(candle.close - candle.open); const upperShadow = (candle: any) => candle.high - Math.max(candle.open, candle.close); const lowerShadow = (candle: any) => Math.min(candle.open, candle.close) - candle.low; const totalRange = (candle: any) => candle.high - candle.low; const isLongBody = (candle: any) => bodySize(candle) > totalRange(candle) * 0.6; const isSmallBody = (candle: any) => bodySize(candle) < totalRange(candle) * 0.3; // Check patterns for each candle position for (let i = 1; i < candles.length - 1; i++) { const prev = candles[i - 1]; const current = candles[i]; const next = i < candles.length - 1 ? candles[i + 1] : null; // Single candle patterns // Hammer/Hanging Man if (lowerShadow(current) > bodySize(current) * 2 && upperShadow(current) < bodySize(current) * 0.5) { const isHammer = isBearish(prev) && isBullish(current); const isHangingMan = isBullish(prev) && (isBullish(current) || isBearish(current)); if (isHammer) { patterns.push({ name: "🔨 HAMMER", type: "BULLISH REVERSAL", strength: "MODERATE", position: i, description: "Potential reversal from downtrend" }); } else if (isHangingMan) { patterns.push({ name: "🪝 HANGING MAN", type: "BEARISH REVERSAL", strength: "MODERATE", position: i, description: "Potential reversal from uptrend" }); } } // Doji if (bodySize(current) < totalRange(current) * 0.1) { patterns.push({ name: "✖️ DOJI", type: "REVERSAL/INDECISION", strength: "WEAK", position: i, description: "Market indecision, potential reversal" }); } // Shooting Star if (upperShadow(current) > bodySize(current) * 2 && lowerShadow(current) < bodySize(current) * 0.5 && isBullish(prev)) { patterns.push({ name: "🌟 SHOOTING STAR", type: "BEARISH REVERSAL", strength: "MODERATE", position: i, description: "Rejection at higher levels" }); } // Two candle patterns if (i < candles.length - 1) { // Bullish Engulfing if (isBearish(current) && isBullish(next) && next.open < current.close && next.close > current.open && bodySize(next) > bodySize(current)) { patterns.push({ name: "🟢 BULLISH ENGULFING", type: "BULLISH REVERSAL", strength: "STRONG", position: i + 1, description: "Strong bullish reversal signal" }); } // Bearish Engulfing if (isBullish(current) && isBearish(next) && next.open > current.close && next.close < current.open && bodySize(next) > bodySize(current)) { patterns.push({ name: "🔴 BEARISH ENGULFING", type: "BEARISH REVERSAL", strength: "STRONG", position: i + 1, description: "Strong bearish reversal signal" }); } } // Long body candles if (isLongBody(current)) { if (isBullish(current)) { patterns.push({ name: "📈 LONG BULLISH CANDLE", type: "BULLISH CONTINUATION", strength: "MODERATE", position: i, description: "Strong buying pressure" }); } else { patterns.push({ name: "📉 LONG BEARISH CANDLE", type: "BEARISH CONTINUATION", strength: "MODERATE", position: i, description: "Strong selling pressure" }); } } } const currentPrice = candles[candles.length - 1].close; const patternCount = patterns.length; let summary = ""; if (patternCount === 0) { summary = "No significant candlestick patterns detected in recent candles."; } else { const recentPatterns = patterns.slice(-3); // Show last 3 patterns summary = recentPatterns.map(pattern => `${pattern.name} (${pattern.type})\n Strength: ${pattern.strength}\n Signal: ${pattern.description}` ).join('\n\n'); } return { content: [ { type: "text", text: `Candlestick Pattern Analysis for ${trading_symbol}:\nCurrent Price: ₹${currentPrice.toFixed(2)}\nCandles Analyzed: ${lookback_candles}\nPatterns Found: ${patternCount}\n\n${summary}\n\nPattern Guide:\n• Reversal patterns: Suggest trend change\n• Continuation patterns: Suggest trend persistence\n• Single candle patterns: Weaker signals\n• Multi-candle patterns: Stronger signals\n• Confirm with volume and other indicators` } ], }; } catch (error) { return { content: [{ type: "text", text: `Error analyzing candlestick patterns: ${error}` }], }; } } ); return server.server; }

Latest Blog Posts

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/arkapravasinha/groww-mcp-server'

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