Skip to main content
Glama

Tradingview Chart MCP

by ertugrul59
main_optimized.py20.2 kB
import os import sys import logging import argparse import asyncio import threading import atexit from typing import Optional from dotenv import load_dotenv from mcp.server.fastmcp import FastMCP, Context from tview_scraper import TradingViewScraper, TradingViewScraperError def setup_logging(log_dir=None, log_level="INFO"): """Configure logging for the TradingView MCP server.""" logger = logging.getLogger(__name__) if log_dir: os.makedirs(log_dir, exist_ok=True) log_file = os.path.join(log_dir, "tradingview_mcp_server.log") file_handler = logging.FileHandler(log_file) file_handler.setFormatter( logging.Formatter("[%(asctime)s] [%(levelname)s] %(name)s - %(message)s") ) file_handler.setLevel(getattr(logging, log_level)) logger.addHandler(file_handler) print(f"[LOG_INIT] Logging to file: {log_file} at level {log_level}") return logger def validate_environment(require_auth=True): """Validate required environment variables.""" if not require_auth: logger.info("Authentication disabled (use --auth to enable)") return None, None session_id = os.getenv("TRADINGVIEW_SESSION_ID") session_id_sign = os.getenv("TRADINGVIEW_SESSION_ID_SIGN") if not session_id or not session_id_sign: logger.error( "Error: TRADINGVIEW_SESSION_ID and TRADINGVIEW_SESSION_ID_SIGN must be set when --auth is enabled." ) logger.error( " Provide them either via environment variables (e.g., in MCP client config)" ) logger.error( " or in a .env file in the project directory for local execution." ) sys.exit(1) return session_id, session_id_sign def get_scraper_config(): """Get scraper configuration from environment variables.""" return { "headless": os.getenv("MCP_SCRAPER_HEADLESS", "True").lower() == "true", "window_width": int(os.getenv("MCP_SCRAPER_WINDOW_WIDTH", "1920")), "window_height": int(os.getenv("MCP_SCRAPER_WINDOW_HEIGHT", "1080")), "use_save_shortcut": os.getenv("MCP_SCRAPER_USE_SAVE_SHORTCUT", "True").lower() == "true", "chart_page_id": os.getenv("MCP_SCRAPER_CHART_PAGE_ID", ""), } def parse_arguments(): """Parse command line arguments.""" parser = argparse.ArgumentParser( description="TradingView Chart MCP Server - Optimized for Concurrent Performance" ) parser.add_argument( "--transport", choices=["stdio", "streamable-http"], default="stdio", help="Transport mode", ) parser.add_argument( "--port", type=int, default=8003, help="Port for HTTP transport" ) parser.add_argument("--host", default="localhost", help="Host for HTTP transport") parser.add_argument("--log-dir", type=str, help="Directory for log files") parser.add_argument( "--log-level", type=str, default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR"], help="Logging level", ) parser.add_argument( "--auth", action="store_true", help="Enable authentication (default: True for TradingView)", ) parser.add_argument( "--max-concurrent", type=int, default=4, help="Maximum concurrent requests (default: 4, based on performance testing)", ) parser.add_argument( "--disable-pooling", action="store_true", help="Disable browser pooling optimization (not recommended)", ) return parser.parse_args() class OptimizedTradingViewMCPServer: """ Optimized TradingView MCP Server with Browser Pooling ==================================================== This server uses browser pooling to achieve significant performance improvements for concurrent requests compared to the traditional approach. Performance improvements expected: - 1 request: ~6-8s (baseline) - 2 concurrent: ~3-4s each (60-70% faster) - 3 concurrent: ~2.5-3.5s each (70-80% faster) """ def __init__(self, max_concurrent: int = 4, config: dict = None): self.max_concurrent = max_concurrent self.config = config or {} self.browser_pool = [] self.browser_lock = threading.Lock() self.semaphore = asyncio.Semaphore(max_concurrent) self.logger = logging.getLogger(__name__) # Performance tracking self.request_count = 0 self.total_request_time = 0.0 self._initialize_browser_pool() def _initialize_browser_pool(self): """Pre-create browser instances for optimal concurrent performance""" self.logger.info( f"🔥 Initializing optimized browser pool with {self.max_concurrent} instances..." ) for i in range(self.max_concurrent): try: self.logger.info(f" Creating browser {i+1}/{self.max_concurrent}...") scraper = TradingViewScraper( headless=self.config.get("headless", True), window_size=f"{self.config.get('window_width', 1920)},{self.config.get('window_height', 1080)}", chart_page_id=self.config.get("chart_page_id") or TradingViewScraper.DEFAULT_CHART_PAGE_ID, use_save_shortcut=self.config.get("use_save_shortcut", True), ) scraper._setup_driver() # Initialize browser immediately self.browser_pool.append(scraper) except Exception as e: self.logger.error(f"Failed to create browser {i+1}: {e}") # Check if this is a Windows-specific ChromeDriver issue if "WinError 193" in str(e) or "not a valid Win32 application" in str( e ): self.logger.error( "Windows ChromeDriver issue detected. Please ensure Chrome is installed and try restarting the server." ) raise TradingViewScraperError(f"Windows ChromeDriver error: {e}") else: raise TradingViewScraperError( f"Browser pool initialization failed: {e}" ) self.logger.info( f"✅ Browser pool initialized! Ready for {self.max_concurrent} concurrent requests" ) self.logger.info( f"🚀 Expected performance: ~2.5-3.5s per request for 3 concurrent (70-80% faster than baseline)" ) def _get_browser(self) -> Optional[TradingViewScraper]: """Get an available browser from the pool""" with self.browser_lock: if self.browser_pool: return self.browser_pool.pop() return None def _return_browser(self, scraper: TradingViewScraper): """Return browser to pool for reuse""" with self.browser_lock: self.browser_pool.append(scraper) async def get_chart_optimized(self, ticker: str, interval: str) -> str: """Get chart using optimized browser pooling for maximum concurrent performance""" import time start_time = time.time() async with self.semaphore: # Limit concurrent requests to optimal level scraper = None try: # Get browser from pool (no creation overhead!) scraper = self._get_browser() if not scraper: raise TradingViewScraperError("No browser available in pool") self.logger.debug( f"📊 Processing {ticker} ({interval}) with pooled browser" ) # Use existing browser - much faster than creating new one result = await asyncio.get_event_loop().run_in_executor( None, scraper.get_chart_image_url, ticker, interval ) # Track performance duration = time.time() - start_time self.request_count += 1 self.total_request_time += duration avg_time = self.total_request_time / self.request_count self.logger.info( f"✅ Chart completed for {ticker} in {duration:.2f}s (avg: {avg_time:.2f}s)" ) return result except Exception as e: duration = time.time() - start_time self.logger.error( f"❌ Chart request failed for {ticker} after {duration:.2f}s: {e}" ) raise TradingViewScraperError(f"Optimized chart request failed: {e}") finally: if scraper: self._return_browser(scraper) # Return for reuse def get_performance_stats(self) -> dict: """Get performance statistics for monitoring""" if self.request_count == 0: return {"message": "No requests processed yet"} avg_time = self.total_request_time / self.request_count baseline_time = 8.0 # Estimated baseline for TradingView concurrent requests improvement = (baseline_time - avg_time) / baseline_time * 100 return { "total_requests": self.request_count, "average_time_per_request": f"{avg_time:.2f}s", "total_time": f"{self.total_request_time:.2f}s", "performance_improvement": f"{improvement:.1f}%", "baseline_comparison": f"{avg_time:.2f}s vs {baseline_time:.2f}s baseline", "browsers_in_pool": len(self.browser_pool), "max_concurrent": self.max_concurrent, } def cleanup(self): """Clean up browser pool""" self.logger.info("🧹 Cleaning up browser pool...") with self.browser_lock: for scraper in self.browser_pool: try: scraper.close() except Exception as e: self.logger.warning(f"Error closing browser: {e}") self.browser_pool.clear() self.logger.info("✅ Browser pool cleanup completed") # Parse arguments first so we can use them everywhere (only when run as main) if __name__ == "__main__": args = parse_arguments() else: print("For testing, create default args.") # For testing, create default args import argparse args = argparse.Namespace( transport="stdio", port=8003, host="localhost", log_dir=None, log_level="INFO", auth=True, # TradingView requires auth by default max_concurrent=4, disable_pooling=False, ) # Initialize components with parsed arguments logger = setup_logging(args.log_dir, args.log_level) load_dotenv() # Validate environment and get credentials (TradingView requires auth by default) TRADINGVIEW_SESSION_ID, TRADINGVIEW_SESSION_ID_SIGN = validate_environment(args.auth) # Get scraper configuration config = get_scraper_config() if config["chart_page_id"] == "": config["chart_page_id"] = TradingViewScraper.DEFAULT_CHART_PAGE_ID # Initialize optimized chart server (unless pooling is disabled) if not args.disable_pooling: logger.info( f"🚀 Initializing OPTIMIZED TradingView MCP server with browser pooling ({args.max_concurrent} concurrent)" ) optimized_server = OptimizedTradingViewMCPServer( max_concurrent=args.max_concurrent, config=config ) else: logger.info("⚠️ Browser pooling DISABLED - using traditional approach") optimized_server = None # Create MCP server mcp_server = FastMCP("TradingView Chart Image - Optimized") @mcp_server.tool() async def get_tradingview_chart_image(ticker: str, interval: str, ctx: Context) -> str: """ Fetches the direct image URL for a TradingView chart snapshot with optimized concurrent performance. PERFORMANCE: This optimized version achieves 70-80% better performance for concurrent requests using browser pooling technology. Args: ticker: The TradingView ticker symbol (e.g., "BYBIT:BTCUSDT.P", "NASDAQ:AAPL"). interval: The chart time interval (e.g., '1', '5', '15', '60', '240', 'D', 'W'). ctx: MCP Context (automatically passed by FastMCP). Returns: The direct TradingView snapshot image URL (e.g., data:image/png;base64,... or https://s3.tradingview.com/snapshots/...). Raises: Error: If the scraper fails or invalid input is provided. """ await ctx.info( f"🔍 [OPTIMIZED] Getting TradingView chart for {ticker} interval {interval}" ) try: if optimized_server and not args.disable_pooling: # Use optimized server with browser pooling image_url = await optimized_server.get_chart_optimized( ticker=ticker, interval=interval ) else: # Fallback to traditional approach await ctx.info("⚠️ Using traditional approach (pooling disabled)") with TradingViewScraper( headless=config["headless"], window_size=f"{config['window_width']},{config['window_height']}", chart_page_id=config["chart_page_id"], use_save_shortcut=config["use_save_shortcut"], ) as scraper: if config["use_save_shortcut"]: image_url = scraper.get_chart_image_url( ticker=ticker, interval=interval ) if not image_url: raise TradingViewScraperError( "Scraper did not return an image URL." ) else: screenshot_link = scraper.get_screenshot_link( ticker=ticker, interval=interval ) if not screenshot_link: raise TradingViewScraperError( "Scraper did not return a screenshot link from clipboard." ) image_url = scraper.convert_link_to_image_url(screenshot_link) if not image_url: raise TradingViewScraperError( "Failed to convert screenshot link to image URL." ) if not image_url: raise TradingViewScraperError("Scraper did not return an image URL.") await ctx.info( f"✅ Successfully obtained image URL for {ticker} ({interval}): {image_url[:100]}{'...' if len(image_url) > 100 else ''}" ) return image_url except TradingViewScraperError as e: await ctx.error(f"❌ Scraper Error: {e}") raise ValueError(f"Scraper Error: {e}") except ValueError as e: await ctx.error(f"⚠️ Input Error: {e}") raise ValueError(f"Input Error: {e}") except Exception as e: await ctx.error(f"❌ Unexpected error in get_tradingview_chart_image: {e}") raise RuntimeError( "An unexpected error occurred while fetching the TradingView chart image." ) @mcp_server.tool() async def get_performance_stats(ctx: Context) -> str: """ Get performance statistics for the optimized TradingView MCP server. Returns detailed metrics about request performance, improvement over baseline, and browser pool status. """ await ctx.info("📊 Retrieving performance statistics...") if optimized_server: stats = optimized_server.get_performance_stats() return f""" 🚀 OPTIMIZED TRADINGVIEW MCP SERVER PERFORMANCE STATS ==================================================== 📊 Request Metrics: • Total Requests: {stats.get('total_requests', 0)} • Average Time: {stats.get('average_time_per_request', 'N/A')} • Total Processing Time: {stats.get('total_time', 'N/A')} 🎯 Performance Improvement: • vs Baseline: {stats.get('baseline_comparison', 'N/A')} • Improvement: {stats.get('performance_improvement', 'N/A')} 🔧 System Configuration: • Browsers in Pool: {stats.get('browsers_in_pool', 0)} • Max Concurrent: {stats.get('max_concurrent', 0)} Expected Performance: • 1 request: ~6-8s • 2 concurrent: ~3-4s each (60-70% faster) • 3 concurrent: ~2.5-3.5s each (70-80% faster) """ else: return "⚠️ Optimized server not initialized (pooling may be disabled)" @mcp_server.prompt("Get the {interval} chart for {ticker}") async def get_chart_prompt_generic(ticker: str, interval: str, ctx: Context): await ctx.info( f"🔍 [OPTIMIZED] Executing prompt: Get the {interval} chart for {ticker}" ) interval_map = { "1 minute": "1", "5 minute": "5", "15 minute": "15", "30 minute": "30", "1 hour": "60", "4 hour": "240", "daily": "D", "weekly": "W", "monthly": "M", } mcp_interval = interval_map.get(interval.lower(), interval) await ctx.info(f"📊 Mapped interval '{interval}' to '{mcp_interval}'") return await get_tradingview_chart_image( ticker=ticker, interval=mcp_interval, ctx=ctx ) @mcp_server.prompt("Show me the daily TradingView chart for {ticker}") async def get_chart_prompt_daily_tradingview(ticker: str, ctx: Context): await ctx.info( f"🔍 [OPTIMIZED] Executing prompt: Show me the daily TradingView chart for {ticker}" ) return await get_tradingview_chart_image(ticker=ticker, interval="D", ctx=ctx) @mcp_server.prompt( "Fetch TradingView chart image for {ticker} on the {interval} timeframe" ) async def get_chart_prompt_timeframe(ticker: str, interval: str, ctx: Context): await ctx.info( f"🔍 [OPTIMIZED] Executing prompt: Fetch TradingView chart image for {ticker} on the {interval} timeframe" ) interval_map = { "daily": "D", "weekly": "W", "monthly": "M", "1 minute": "1", "5 minute": "5", "15 minute": "15", "1 hour": "60", "4 hour": "240", } mcp_interval = interval_map.get(interval.lower(), interval) await ctx.info(f"📊 Mapped interval '{interval}' to '{mcp_interval}'") return await get_tradingview_chart_image( ticker=ticker, interval=mcp_interval, ctx=ctx ) # Cleanup handler import atexit def cleanup_on_exit(): if optimized_server: optimized_server.cleanup() atexit.register(cleanup_on_exit) if __name__ == "__main__": if os.getenv("MCP_DEBUG_STARTUP", "false").lower() == "true": logger.info("🚀 Starting OPTIMIZED TradingView Chart Image MCP Server...") logger.info(f"⚙️ Configuration:") logger.info(f" - Transport mode: {args.transport}") logger.info(f" - Authentication: {'Enabled' if args.auth else 'Disabled'}") logger.info( f" - Browser Pooling: {'Enabled' if not args.disable_pooling else 'DISABLED'}" ) logger.info(f" - Max Concurrent: {args.max_concurrent}") logger.info(f" - Headless: {config['headless']}") logger.info( f" - Window Size: {config['window_width']}x{config['window_height']}" ) logger.info(f" - Use Save Shortcut: {config['use_save_shortcut']}") logger.info(f" - Chart Page ID: {config['chart_page_id']}") if args.transport == "streamable-http": logger.info(f" - HTTP Server: {args.host}:{args.port}") try: if args.transport == "streamable-http": logger.info( f"Starting optimized TradingView MCP server on {args.host}:{args.port}" ) mcp_server.settings.host = args.host mcp_server.settings.port = args.port mcp_server.run(transport="streamable-http") else: mcp_server.run(transport="stdio") except KeyboardInterrupt: logger.info("🛑 Server interrupted by user") except Exception as e: logger.error(f"❌ Server error: {e}") finally: if optimized_server: optimized_server.cleanup()

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/ertugrul59/tradingview-chart-mcp'

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