Skip to main content
Glama

MaverickMCP

by wshobson
MIT License
165
  • Apple
technical.py16.1 kB
""" Technical analysis router for MaverickMCP. This module contains all technical analysis related tools including indicators, chart patterns, and analysis functions. DISCLAIMER: All technical analysis tools are for educational purposes only. Technical indicators are mathematical calculations based on historical data and do not predict future price movements. Results should not be considered as investment advice. Always consult qualified financial professionals. """ import asyncio from concurrent.futures import ThreadPoolExecutor from datetime import UTC, datetime from typing import Any from fastmcp import FastMCP from fastmcp.server.dependencies import get_access_token from maverick_mcp.core.technical_analysis import ( analyze_bollinger_bands, analyze_macd, analyze_rsi, analyze_stochastic, analyze_trend, analyze_volume, generate_outlook, identify_chart_patterns, identify_resistance_levels, identify_support_levels, ) from maverick_mcp.core.visualization import ( create_plotly_technical_chart, plotly_fig_to_base64, ) from maverick_mcp.providers.stock_data import StockDataProvider from maverick_mcp.utils.logging import PerformanceMonitor, get_logger from maverick_mcp.utils.mcp_logging import with_logging from maverick_mcp.utils.stock_helpers import ( get_stock_dataframe_async, ) logger = get_logger("maverick_mcp.routers.technical") # Create the technical analysis router technical_router: FastMCP = FastMCP("Technical_Analysis") # Initialize data provider stock_provider = StockDataProvider() # Thread pool for blocking operations executor = ThreadPoolExecutor(max_workers=10) @with_logging("rsi_analysis") async def get_rsi_analysis( ticker: str, period: int = 14, days: int = 365 ) -> dict[str, Any]: """ Get RSI analysis for a given ticker. Args: ticker: Stock ticker symbol period: RSI period (default: 14) days: Number of days of historical data to analyze (default: 365) Returns: Dictionary containing RSI analysis """ try: # Log analysis parameters logger.info( "Starting RSI analysis", extra={"ticker": ticker, "period": period, "days": days}, ) # Fetch stock data with performance monitoring with PerformanceMonitor(f"fetch_data_{ticker}"): df = await get_stock_dataframe_async(ticker, days) # Perform RSI analysis with monitoring with PerformanceMonitor(f"rsi_calculation_{ticker}"): loop = asyncio.get_event_loop() analysis = await loop.run_in_executor(executor, analyze_rsi, df) # Log successful completion logger.info( "RSI analysis completed successfully", extra={ "ticker": ticker, "rsi_current": analysis.get("current_rsi"), "signal": analysis.get("signal"), }, ) return {"ticker": ticker, "period": period, "analysis": analysis} except Exception as e: logger.error( "Error in RSI analysis", exc_info=True, extra={"ticker": ticker, "period": period, "error_type": type(e).__name__}, ) return {"error": str(e), "status": "error"} async def get_macd_analysis( ticker: str, fast_period: int = 12, slow_period: int = 26, signal_period: int = 9, days: int = 365, ) -> dict[str, Any]: """ Get MACD analysis for a given ticker. Args: ticker: Stock ticker symbol fast_period: Fast EMA period (default: 12) slow_period: Slow EMA period (default: 26) signal_period: Signal line period (default: 9) days: Number of days of historical data to analyze (default: 365) Returns: Dictionary containing MACD analysis """ try: df = await get_stock_dataframe_async(ticker, days) analysis = analyze_macd(df) return { "ticker": ticker, "parameters": { "fast_period": fast_period, "slow_period": slow_period, "signal_period": signal_period, }, "analysis": analysis, } except Exception as e: logger.error(f"Error in MACD analysis for {ticker}: {str(e)}") return {"error": str(e), "status": "error"} async def get_support_resistance(ticker: str, days: int = 365) -> dict[str, Any]: """ Get support and resistance levels for a given ticker. Args: ticker: Stock ticker symbol days: Number of days of historical data to analyze (default: 365) Returns: Dictionary containing support and resistance levels """ try: df = await get_stock_dataframe_async(ticker, days) support = identify_support_levels(df) resistance = identify_resistance_levels(df) current_price = df["close"].iloc[-1] return { "ticker": ticker, "current_price": float(current_price), "support_levels": sorted(support), "resistance_levels": sorted(resistance), } except Exception as e: logger.error(f"Error in support/resistance analysis for {ticker}: {str(e)}") return {"error": str(e), "status": "error"} async def get_full_technical_analysis(ticker: str, days: int = 365) -> dict[str, Any]: """ Get comprehensive technical analysis for a given ticker. This tool provides a complete technical analysis including: - Trend analysis - All major indicators (RSI, MACD, Stochastic, Bollinger Bands) - Support and resistance levels - Volume analysis - Chart patterns - Overall outlook Args: ticker: Stock ticker symbol days: Number of days of historical data to analyze (default: 365) Returns: Dictionary containing complete technical analysis """ try: # Access authentication context if available (optional for this tool) # This demonstrates optional authentication - tool works without auth # but provides enhanced features for authenticated users has_premium = False try: access_token = get_access_token() if access_token is None: raise ValueError("No access token available") # Log authenticated user logger.info( f"Technical analysis requested by authenticated user: {access_token.client_id}", extra={"scopes": access_token.scopes}, ) # Check for premium features based on scopes has_premium = "premium:access" in access_token.scopes logger.info(f"Has premium: {has_premium}") except Exception: # Authentication is optional for this tool logger.debug("Technical analysis requested by unauthenticated user") df = await get_stock_dataframe_async(ticker, days) # Perform all analyses trend = analyze_trend(df) rsi_analysis = analyze_rsi(df) macd_analysis = analyze_macd(df) stoch_analysis = analyze_stochastic(df) bb_analysis = analyze_bollinger_bands(df) volume_analysis = analyze_volume(df) patterns = identify_chart_patterns(df) support = identify_support_levels(df) resistance = identify_resistance_levels(df) outlook = generate_outlook( df, str(trend), rsi_analysis, macd_analysis, stoch_analysis ) # Get current price and indicators current_price = df["close"].iloc[-1] # Compile results return { "ticker": ticker, "current_price": float(current_price), "trend": trend, "outlook": outlook, "indicators": { "rsi": rsi_analysis, "macd": macd_analysis, "stochastic": stoch_analysis, "bollinger_bands": bb_analysis, "volume": volume_analysis, }, "levels": {"support": sorted(support), "resistance": sorted(resistance)}, "patterns": patterns, "last_updated": datetime.now(UTC).isoformat(), } except Exception as e: logger.error(f"Error in technical analysis for {ticker}: {str(e)}") return {"error": str(e), "status": "error"} async def get_stock_chart_analysis(ticker: str) -> dict[str, Any]: """ Generate a comprehensive technical analysis chart. This tool creates a visual technical analysis including: - Price action with candlesticks - Moving averages - Volume analysis - Technical indicators - Support and resistance levels Args: ticker: The ticker symbol of the stock to analyze Returns: Dictionary containing the chart as properly formatted MCP image content for Claude Desktop display """ try: # Use async data fetching df = await get_stock_dataframe_async(ticker, 365) # Run the chart generation in the executor for performance loop = asyncio.get_event_loop() chart_content = await loop.run_in_executor( executor, _generate_chart_mcp_format, df, ticker ) return chart_content except Exception as e: logger.error(f"Error generating chart analysis for {ticker}: {e}") return {"error": str(e)} def _generate_chart_mcp_format(df, ticker: str) -> dict[str, Any]: """Generate chart in proper MCP content format for Claude Desktop with aggressive size optimization""" from maverick_mcp.core.technical_analysis import add_technical_indicators df = add_technical_indicators(df) # Claude Desktop has a ~100k character limit for responses # Base64 images need to be MUCH smaller - aim for ~50k chars max chart_configs = [ {"height": 300, "width": 500, "format": "jpeg"}, # Small primary {"height": 250, "width": 400, "format": "jpeg"}, # Smaller fallback {"height": 200, "width": 350, "format": "jpeg"}, # Tiny fallback {"height": 150, "width": 300, "format": "jpeg"}, # Last resort ] for config in chart_configs: try: # Generate chart with current config analysis = create_plotly_technical_chart( df, ticker, height=config["height"], width=config["width"] ) # Generate base64 data URI data_uri = plotly_fig_to_base64(analysis, format=config["format"]) # Extract base64 data without the data URI prefix if data_uri.startswith(f"data:image/{config['format']};base64,"): base64_data = data_uri.split(",", 1)[1] mime_type = f"image/{config['format']}" else: # Fallback - assume it's already base64 data base64_data = data_uri mime_type = f"image/{config['format']}" # Very conservative size limit for Claude Desktop # Response gets truncated at 100k chars, so aim for 50k max for base64 max_chars = 50000 logger.info( f"Generated chart for {ticker}: {config['width']}x{config['height']} " f"({len(base64_data):,} chars base64)" ) if len(base64_data) <= max_chars: # Try multiple formats to work around Claude Desktop bugs description = ( f"Technical analysis chart for {ticker.upper()} " f"({config['width']}x{config['height']}) showing price action, " f"moving averages, volume, RSI, and MACD indicators." ) return _return_image_with_claude_desktop_workaround( base64_data, mime_type, description, ticker ) else: logger.warning( f"Chart for {ticker} too large at {config['width']}x{config['height']} " f"({len(base64_data):,} chars > {max_chars}), trying smaller size..." ) continue except Exception as e: logger.warning(f"Failed to generate chart with config {config}: {e}") continue # If all configs failed, return error return { "content": [ { "type": "text", "text": ( f"Unable to generate suitable chart size for {ticker.upper()}. " f"The chart image is too large for Claude Desktop display limits. " f"Please use the text-based technical analysis tool instead: " f"technical_get_full_technical_analysis" ), } ] } def _return_image_with_claude_desktop_workaround( base64_data: str, mime_type: str, description: str, ticker: str ) -> dict[str, Any]: """ Return image using multiple formats to work around Claude Desktop bugs. Tries alternative MCP format first, fallback to file saving. """ import base64 as b64 import tempfile from pathlib import Path # Format 1: Alternative "source" structure (some reports of this working) try: return { "content": [ {"type": "text", "text": description}, { "type": "image", "source": { "type": "base64", "media_type": mime_type, "data": base64_data, }, }, ] } except Exception as e: logger.warning(f"Alternative image format failed: {e}") # Format 2: Try original format one more time with different structure try: return { "content": [ {"type": "text", "text": description}, {"type": "image", "data": base64_data, "mimeType": mime_type}, ] } except Exception as e: logger.warning(f"Standard image format failed: {e}") # Format 3: File-based fallback (most reliable for Claude Desktop) try: ext = mime_type.split("/")[-1] # jpeg, png, etc. # Create temp file in a standard location temp_dir = Path(tempfile.gettempdir()) / "maverick_mcp_charts" temp_dir.mkdir(exist_ok=True) chart_file = ( temp_dir / f"{ticker.lower()}_chart_{datetime.now(UTC).strftime('%Y%m%d_%H%M%S')}.{ext}" ) # Decode and save base64 to file image_data = b64.b64decode(base64_data) chart_file.write_bytes(image_data) logger.info(f"Saved chart to file: {chart_file}") return { "content": [ { "type": "text", "text": ( f"{description}\n\n" f"📁 **Chart saved to file**: `{chart_file}`\n\n" f"**To view this image:**\n" f"1. Use the filesystem MCP server if configured, or\n" f"2. Ask me to open the file location, or\n" f"3. Navigate to the file manually\n\n" f"*Note: Claude Desktop has a known issue with embedded images. " f"File-based display is the current workaround.*" ), } ] } except Exception as e: logger.error(f"File fallback also failed: {e}") return { "content": [ { "type": "text", "text": ( f"Unable to display chart for {ticker.upper()} due to " f"Claude Desktop image rendering limitations. " f"Please use the text-based technical analysis instead: " f"`technical_get_full_technical_analysis`" ), } ] }

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/wshobson/maverick-mcp'

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