technicalIndicators_ema
Calculate Exponential Moving Average (EMA) for financial assets to identify trends and generate trading signals using stock symbols, time intervals, and price data.
Instructions
Exponential Moving Average (EMA)
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| symbol | Yes | The stock symbol (e.g., "IBM"). | |
| interval | Yes | Time interval (e.g., "daily", "60min", "weekly"). Check Alpha Vantage docs for valid intervals per indicator. | |
| datatype | No | Data format for the response. | json |
| month | No | Specific month for intraday intervals (YYYY-MM format). | |
| time_period | Yes | Number of data points used to calculate the indicator. | |
| series_type | Yes | The desired price type. |
Implementation Reference
- src/index.ts:998-1012 (registration)Registers the MCP tool 'technicalIndicators_ema' with server.addTool, including name, description, parameters schema from schemas.TechnicalIndicatorsTimeSeriesIndicatorParamsSchema, and an execute handler that delegates to executeAvantageTool calling the AVantage library's ema method.server.addTool({ name: "technicalIndicators_ema", description: "Exponential Moving Average (EMA)", parameters: schemas.TechnicalIndicatorsTimeSeriesIndicatorParamsSchema, execute: ( args, context // Let type be inferred ) => executeAvantageTool( "technicalIndicators_ema", args, context, (av, params) => av.technicalIndicators.ema(params) ), });
- src/schemas.ts:204-215 (schema)Defines TechnicalIndicatorsCommonIndicatorParamsSchema (base parameters: symbol, interval, etc.) extended by TechnicalIndicatorsTimeSeriesIndicatorParamsSchema adding time_period and series_type, which validates the input for the technicalIndicators_ema tool.export const TechnicalIndicatorsCommonIndicatorParamsSchema = z.object({ symbol: z.string().describe('The stock symbol (e.g., "IBM").'), interval: z.string().describe('Time interval (e.g., "daily", "60min", "weekly"). Check Alpha Vantage docs for valid intervals per indicator.'), datatype: DatatypeSchema.default('json').optional(), month: z.string().optional().describe('Specific month for intraday intervals (YYYY-MM format).'), }).describe('Common parameters for many technical indicators.') // Schema for indicators requiring time_period and series_type export const TechnicalIndicatorsTimeSeriesIndicatorParamsSchema = TechnicalIndicatorsCommonIndicatorParamsSchema.extend({ time_period: z.string().describe('Number of data points used to calculate the indicator.'), // Using string as AV API expects string series_type: SeriesTypeSchema, }).describe('Parameters for time series based technical indicators.')
- src/index.ts:38-115 (handler)Generic executeAvantageTool function that implements the tool execution logic shared by all tools: resolves API key, manages AVantage client via resourceManager, invokes av.technicalIndicators.ema(params) for this tool, stringifies the result, and handles errors.async function executeAvantageTool<TArgs, TResult>( toolName: string, args: TArgs, context: Context<Record<string, unknown> | undefined>, // Use the imported Context type directly avantageMethod: ( av: AVantage, args: TArgs ) => Promise<{ error?: boolean; reason?: string; data?: TResult }> ): Promise<string> { logger.info(`Executing '${toolName}' tool for request ID: ${context}`); logger.debug(`Args for ${toolName}: ${JSON.stringify(args)}`); // --- Authentication & Resource Management --- // Access extraArgs safely - it might be null or undefined const extraArgsApiKey = context.extraArgs?.apiKey as string | undefined; const apiKey = extraArgsApiKey || config.apiKey; if (!apiKey) { logger.error(`'${toolName}' failed: Alpha Vantage API key missing.`); throw new UserError(apiKeyErrorMessage); } logger.debug( `Using AV API key (source: ${extraArgsApiKey ? "extraArgs" : "environment"}) for ${toolName}` ); try { // Get or create AVantage instance managed by ResourceManager const av = await resourceManager.getResource<AVantage>( apiKey, // Cache key is the resolved API key "avantage_client", // Type identifier for logging async (key) => { // Factory Function logger.info( `Creating new AVantage instance for key ending ...${key.slice(-4)}` ); // AVantage library reads AV_PREMIUM from process.env internally return new AVantage(key); }, async (avInstance) => { // Cleanup Function (no-op needed for AVantage) logger.debug(`Destroying AVantage instance (no-op)`); } ); // --- Library Call --- const result = await avantageMethod(av, args); // --- Response Handling --- if (result.error) { logger.warn( `'${toolName}' failed. Reason from avantage: ${result.reason}` ); throw new UserError(result.reason || `Tool '${toolName}' failed.`); } if (result.data === undefined || result.data === null) { logger.warn(`'${toolName}' completed successfully but returned no data.`); return "null"; // Return string "null" for empty data } logger.info(`'${toolName}' completed successfully.`); // Stringify the data part of the response return JSON.stringify(result.data); } catch (error: any) { logger.error( `Error during execution of '${toolName}': ${error.message}`, error ); // If it's already a UserError, rethrow it if (error instanceof UserError) { throw error; } // Otherwise, wrap it in a UserError throw new UserError( `An unexpected error occurred while executing tool '${toolName}': ${error.message}` ); } }
- src/resource-manager.ts:21-192 (helper)ResourceManager class used by executeAvantageTool to cache and lifecycle-manage AVantage API client instances keyed by API key, ensuring efficient reuse and cleanup.export class ResourceManager { private resources: Map<string, ResourceInfo<any>> = new Map() private readonly cleanupIntervalMs: number private cleanupTimer: NodeJS.Timeout | null = null constructor(options?: { cleanupIntervalMs?: number }) { this.cleanupIntervalMs = options?.cleanupIntervalMs ?? config.resourceCleanupInterval this.startCleanupTimer() logger.info( `ResourceManager initialized. Cleanup interval: ${this.cleanupIntervalMs}ms`, ) } /** * Retrieves an existing resource or creates a new one. */ public async getResource<T>( key: string, resourceType: string, factoryFn: (key: string) => Promise<T>, cleanupFn: (resource: T) => Promise<void>, ): Promise<T> { const existingInfo = this.resources.get(key) if (existingInfo) { logger.debug( `Reusing existing resource (Type: ${existingInfo.resourceType}, Instance ID: ${existingInfo.instanceId}) for key ending with ...${key.slice(-4)}`, ) existingInfo.lastUsed = Date.now() if (existingInfo.resourceType !== resourceType) { logger.warn( `Resource type mismatch for key ${key}. Expected ${resourceType}, found ${existingInfo.resourceType}. Returning existing resource anyway.`, ) } return existingInfo.resource as T } logger.info( `Creating new resource (Type: ${resourceType}) for key ending with ...${key.slice(-4)}`, ) try { const newResource = await factoryFn(key) const instanceId = uuidv4() const newInfo: ResourceInfo<T> = { resource: newResource, lastUsed: Date.now(), instanceId: instanceId, resourceType: resourceType, cacheKey: key, cleanupFn: cleanupFn, } this.resources.set(key, newInfo) logger.info( `Successfully created resource (Type: ${resourceType}, Instance ID: ${instanceId})`, ) return newResource } catch (error) { logger.error( `Failed to create resource (Type: ${resourceType}) for key ${key}: ${error instanceof Error ? error.message : String(error)}`, error, ) throw new Error( `Resource factory function failed for type ${resourceType}: ${error instanceof Error ? error.message : String(error)}`, ) } } private startCleanupTimer(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer) } this.cleanupTimer = setInterval( () => this.cleanupInactiveResources(), this.cleanupIntervalMs, ) this.cleanupTimer.unref() logger.info('Resource cleanup timer started.') } public stopCleanupTimer(): void { if (this.cleanupTimer) { clearInterval(this.cleanupTimer) this.cleanupTimer = null logger.info('Resource cleanup timer stopped.') } } private async cleanupInactiveResources(): Promise<void> { const now = Date.now() let cleanedCount = 0 logger.debug('Running inactive resource cleanup check...') const keysToRemove: string[] = [] for (const [key, info] of this.resources.entries()) { if (now - info.lastUsed > this.cleanupIntervalMs) { keysToRemove.push(key) } } if (keysToRemove.length === 0) { logger.debug('No inactive resources found to clean up.') return } logger.info(`Found ${keysToRemove.length} inactive resource(s) to clean up.`) for (const key of keysToRemove) { const info = this.resources.get(key) // Double-check inactivity before removing, in case it was accessed again if (info && now - info.lastUsed > this.cleanupIntervalMs) { logger.info( `Cleaning up inactive resource (Type: ${info.resourceType}, Instance ID: ${info.instanceId}, Key: ...${key.slice(-4)})`, ) try { await info.cleanupFn(info.resource) this.resources.delete(key) cleanedCount++ logger.info( `Successfully cleaned up resource (Instance ID: ${info.instanceId})`, ) } catch (error) { logger.error( `Error during cleanup of resource (Instance ID: ${info.instanceId}): ${error instanceof Error ? error.message : String(error)}`, error, ) // Optionally remove from map even if cleanup failed // this.resources.delete(key); } } } if (cleanedCount > 0) { logger.info(`Finished cleanup. Removed ${cleanedCount} inactive resource(s).`) } else { logger.debug('Finished cleanup check, no resources were removed this cycle.') } } /** * Immediately destroys all managed resources. Useful for graceful shutdown. */ public async destroyAllNow(): Promise<void> { logger.warn('Destroying all managed resources immediately...') this.stopCleanupTimer() const cleanupPromises: Promise<void>[] = [] for (const [key, info] of this.resources.entries()) { logger.info( `Initiating immediate cleanup for resource (Type: ${info.resourceType}, Instance ID: ${info.instanceId}, Key: ...${key.slice(-4)})`, ) cleanupPromises.push( info .cleanupFn(info.resource) .catch((error) => logger.error( `Error during immediate cleanup of resource (Instance ID: ${info.instanceId}): ${error instanceof Error ? error.message : String(error)}`, error, ), ), ) } await Promise.allSettled(cleanupPromises) const finalCount = this.resources.size this.resources.clear() logger.warn( `Finished destroying all resources. Cleared ${finalCount} resource entries.`, ) } }