Skip to main content
Glama

MaverickMCP

by wshobson
MIT License
165
  • Apple
backtesting.py47.9 kB
"""MCP router for VectorBT backtesting tools with structured logging.""" from typing import Any import numpy as np from fastmcp import Context from maverick_mcp.backtesting import ( BacktestAnalyzer, StrategyOptimizer, VectorBTEngine, ) from maverick_mcp.backtesting.strategies import STRATEGY_TEMPLATES, StrategyParser from maverick_mcp.backtesting.strategies.templates import ( get_strategy_info, list_available_strategies, ) from maverick_mcp.backtesting.visualization import ( generate_equity_curve, generate_optimization_heatmap, generate_performance_dashboard, generate_trade_scatter, ) from maverick_mcp.utils.debug_utils import debug_operation from maverick_mcp.utils.logging import get_logger from maverick_mcp.utils.structured_logger import ( CorrelationIDGenerator, get_performance_logger, with_structured_logging, ) # Initialize performance logger for backtesting router performance_logger = get_performance_logger("backtesting_router") logger = get_logger(__name__) def convert_numpy_types(obj: Any) -> Any: """Recursively convert numpy types to Python native types for JSON serialization. Args: obj: Any object that might contain numpy types Returns: Object with all numpy types converted to Python native types """ import pandas as pd # Check for numpy integer types (more robust using numpy's type hierarchy) if isinstance(obj, np.integer): return int(obj) # Check for numpy floating point types elif isinstance(obj, np.floating): return float(obj) # Check for numpy boolean type elif isinstance(obj, np.bool_ | bool) and hasattr(obj, "item"): return bool(obj) # Check for numpy complex types elif isinstance(obj, np.complexfloating): return complex(obj) # Handle numpy arrays elif isinstance(obj, np.ndarray): return obj.tolist() # Handle pandas Series elif isinstance(obj, pd.Series): return obj.tolist() # Handle pandas DataFrame elif isinstance(obj, pd.DataFrame): return obj.to_dict("records") # Handle NaN/None values elif pd.isna(obj): return None # Handle other numpy scalars with .item() method elif hasattr(obj, "item") and hasattr(obj, "dtype"): try: return obj.item() except Exception: return str(obj) # Recursively handle dictionaries elif isinstance(obj, dict): return {key: convert_numpy_types(value) for key, value in obj.items()} # Recursively handle lists and tuples elif isinstance(obj, list | tuple): return [convert_numpy_types(item) for item in obj] # Try to handle custom objects with __dict__ elif hasattr(obj, "__dict__") and not isinstance(obj, type): try: return convert_numpy_types(obj.__dict__) except Exception: return str(obj) else: # Return as-is for regular Python types return obj def setup_backtesting_tools(mcp): """Set up VectorBT backtesting tools for MCP. Args: mcp: FastMCP instance """ @mcp.tool() @with_structured_logging("run_backtest", include_performance=True, log_params=True) @debug_operation("run_backtest", enable_profiling=True, symbol="backtest_symbol") async def run_backtest( ctx: Context, symbol: str, strategy: str = "sma_cross", start_date: str | None = None, end_date: str | None = None, initial_capital: float = 10000.0, fast_period: str | int | None = None, slow_period: str | int | None = None, period: str | int | None = None, oversold: str | float | None = None, overbought: str | float | None = None, signal_period: str | int | None = None, std_dev: str | float | None = None, lookback: str | int | None = None, threshold: str | float | None = None, z_score_threshold: str | float | None = None, breakout_factor: str | float | None = None, ) -> dict[str, Any]: """Run a VectorBT backtest with specified strategy and parameters. Args: symbol: Stock symbol to backtest strategy: Strategy type (sma_cross, rsi, macd, bollinger, momentum, etc.) start_date: Start date (YYYY-MM-DD), defaults to 1 year ago end_date: End date (YYYY-MM-DD), defaults to today initial_capital: Starting capital for backtest Strategy-specific parameters passed as individual arguments (e.g., fast_period=10, slow_period=20) Returns: Comprehensive backtest results including metrics, trades, and analysis Examples: run_backtest("AAPL", "sma_cross", fast_period=10, slow_period=20) run_backtest("TSLA", "rsi", period=14, oversold=30, overbought=70) """ from datetime import datetime, timedelta # Default date range if not end_date: end_date = datetime.now().strftime("%Y-%m-%d") if not start_date: start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") # Convert string parameters to appropriate types def convert_param(value, param_type): """Convert string parameter to appropriate type.""" if value is None: return None if isinstance(value, str): try: if param_type is int: return int(value) elif param_type is float: return float(value) except (ValueError, TypeError) as e: raise ValueError( f"Invalid {param_type.__name__} value: {value}" ) from e return value # Build parameters dict from provided arguments with type conversion param_map = { "fast_period": convert_param(fast_period, int), "slow_period": convert_param(slow_period, int), "period": convert_param(period, int), "oversold": convert_param(oversold, float), "overbought": convert_param(overbought, float), "signal_period": convert_param(signal_period, int), "std_dev": convert_param(std_dev, float), "lookback": convert_param(lookback, int), "threshold": convert_param(threshold, float), "z_score_threshold": convert_param(z_score_threshold, float), "breakout_factor": convert_param(breakout_factor, float), } # Get default parameters for strategy if strategy in STRATEGY_TEMPLATES: parameters = dict(STRATEGY_TEMPLATES[strategy]["parameters"]) # Override with provided non-None parameters for param_name, param_value in param_map.items(): if param_value is not None: parameters[param_name] = param_value else: # Use only provided parameters for unknown strategies parameters = {k: v for k, v in param_map.items() if v is not None} # Initialize engine engine = VectorBTEngine() # Run backtest results = await engine.run_backtest( symbol=symbol, strategy_type=strategy, parameters=parameters, start_date=start_date, end_date=end_date, initial_capital=initial_capital, ) # Analyze results analyzer = BacktestAnalyzer() analysis = analyzer.analyze(results) # Combine results and analysis results["analysis"] = analysis # Log business metrics if results.get("metrics"): metrics = results["metrics"] performance_logger.log_business_metric( "backtest_completion", 1, symbol=symbol, strategy=strategy, total_return=metrics.get("total_return", 0), sharpe_ratio=metrics.get("sharpe_ratio", 0), max_drawdown=metrics.get("max_drawdown", 0), total_trades=metrics.get("total_trades", 0), ) # Set correlation context for downstream operations CorrelationIDGenerator.set_correlation_id() return results @mcp.tool() @with_structured_logging( "optimize_strategy", include_performance=True, log_params=True ) @debug_operation( "optimize_strategy", enable_profiling=True, strategy="optimization_strategy" ) async def optimize_strategy( ctx: Context, symbol: str, strategy: str = "sma_cross", start_date: str | None = None, end_date: str | None = None, optimization_metric: str = "sharpe_ratio", optimization_level: str = "medium", top_n: int = 10, ) -> dict[str, Any]: """Optimize strategy parameters using VectorBT grid search. Args: symbol: Stock symbol to optimize strategy: Strategy type to optimize start_date: Start date (YYYY-MM-DD) end_date: End date (YYYY-MM-DD) optimization_metric: Metric to optimize (sharpe_ratio, total_return, win_rate, etc.) optimization_level: Level of optimization (coarse, medium, fine) top_n: Number of top results to return Returns: Optimization results with best parameters and performance metrics """ from datetime import datetime, timedelta # Default date range if not end_date: end_date = datetime.now().strftime("%Y-%m-%d") if not start_date: start_date = (datetime.now() - timedelta(days=365 * 2)).strftime("%Y-%m-%d") # Initialize engine and optimizer engine = VectorBTEngine() optimizer = StrategyOptimizer(engine) # Generate parameter grid param_grid = optimizer.generate_param_grid(strategy, optimization_level) # Run optimization results = await engine.optimize_parameters( symbol=symbol, strategy_type=strategy, param_grid=param_grid, start_date=start_date, end_date=end_date, optimization_metric=optimization_metric, top_n=top_n, ) return results @mcp.tool() async def walk_forward_analysis( ctx: Context, symbol: str, strategy: str = "sma_cross", start_date: str | None = None, end_date: str | None = None, window_size: int = 252, step_size: int = 63, ) -> dict[str, Any]: """Perform walk-forward analysis to test strategy robustness. Args: symbol: Stock symbol to analyze strategy: Strategy type start_date: Start date (YYYY-MM-DD) end_date: End date (YYYY-MM-DD) window_size: Test window size in trading days (default: 1 year) step_size: Step size for rolling window (default: 1 quarter) Returns: Walk-forward analysis results with out-of-sample performance """ from datetime import datetime, timedelta # Default date range (3 years for walk-forward) if not end_date: end_date = datetime.now().strftime("%Y-%m-%d") if not start_date: start_date = (datetime.now() - timedelta(days=365 * 3)).strftime("%Y-%m-%d") # Initialize engine and optimizer engine = VectorBTEngine() optimizer = StrategyOptimizer(engine) # Get default parameters parameters = STRATEGY_TEMPLATES.get(strategy, {}).get("parameters", {}) # Run walk-forward analysis results = await optimizer.walk_forward_analysis( symbol=symbol, strategy_type=strategy, parameters=parameters, start_date=start_date, end_date=end_date, window_size=window_size, step_size=step_size, ) return results @mcp.tool() async def monte_carlo_simulation( ctx: Context, symbol: str, strategy: str = "sma_cross", start_date: str | None = None, end_date: str | None = None, num_simulations: int = 1000, fast_period: str | int | None = None, slow_period: str | int | None = None, period: str | int | None = None, ) -> dict[str, Any]: """Run Monte Carlo simulation on backtest results. Args: symbol: Stock symbol strategy: Strategy type start_date: Start date (YYYY-MM-DD) end_date: End date (YYYY-MM-DD) num_simulations: Number of Monte Carlo simulations Strategy-specific parameters as individual arguments Returns: Monte Carlo simulation results with confidence intervals """ from datetime import datetime, timedelta # Default date range if not end_date: end_date = datetime.now().strftime("%Y-%m-%d") if not start_date: start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") # Convert string parameters to appropriate types def convert_param(value, param_type): """Convert string parameter to appropriate type.""" if value is None: return None if isinstance(value, str): try: if param_type is int: return int(value) elif param_type is float: return float(value) except (ValueError, TypeError) as e: raise ValueError( f"Invalid {param_type.__name__} value: {value}" ) from e return value # Build parameters dict from provided arguments with type conversion param_map = { "fast_period": convert_param(fast_period, int), "slow_period": convert_param(slow_period, int), "period": convert_param(period, int), } # Get parameters if strategy in STRATEGY_TEMPLATES: parameters = dict(STRATEGY_TEMPLATES[strategy]["parameters"]) # Override with provided non-None parameters for param_name, param_value in param_map.items(): if param_value is not None: parameters[param_name] = param_value else: # Use only provided parameters for unknown strategies parameters = {k: v for k, v in param_map.items() if v is not None} # Run backtest first engine = VectorBTEngine() backtest_results = await engine.run_backtest( symbol=symbol, strategy_type=strategy, parameters=parameters, start_date=start_date, end_date=end_date, ) # Run Monte Carlo simulation optimizer = StrategyOptimizer(engine) mc_results = await optimizer.monte_carlo_simulation( backtest_results=backtest_results, num_simulations=num_simulations, ) return mc_results @mcp.tool() async def compare_strategies( ctx: Context, symbol: str, strategies: list[str] | str | None = None, start_date: str | None = None, end_date: str | None = None, ) -> dict[str, Any]: """Compare multiple strategies on the same symbol. Args: symbol: Stock symbol strategies: List of strategy types to compare (defaults to all) start_date: Start date (YYYY-MM-DD) end_date: End date (YYYY-MM-DD) Returns: Comparison results with rankings and analysis """ from datetime import datetime, timedelta # Default date range if not end_date: end_date = datetime.now().strftime("%Y-%m-%d") if not start_date: start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") # Handle strategies as JSON string from some clients if isinstance(strategies, str): import json try: strategies = json.loads(strategies) except json.JSONDecodeError: # If it's not JSON, treat it as a single strategy strategies = [strategies] # Default to comparing top strategies if not strategies: strategies = ["sma_cross", "rsi", "macd", "bollinger", "momentum"] # Run backtests for each strategy engine = VectorBTEngine() results_list = [] for strategy in strategies: try: # Get default parameters parameters = STRATEGY_TEMPLATES.get(strategy, {}).get("parameters", {}) # Run backtest results = await engine.run_backtest( symbol=symbol, strategy_type=strategy, parameters=parameters, start_date=start_date, end_date=end_date, ) results_list.append(results) except Exception: # Skip failed strategies continue # Compare results analyzer = BacktestAnalyzer() comparison = analyzer.compare_strategies(results_list) return comparison @mcp.tool() async def list_strategies(ctx: Context) -> dict[str, Any]: """List all available VectorBT strategies with descriptions. Returns: Dictionary of available strategies and their information """ strategies = {} for strategy_type in list_available_strategies(): strategies[strategy_type] = get_strategy_info(strategy_type) return { "available_strategies": strategies, "total_count": len(strategies), "categories": { "trend_following": ["sma_cross", "ema_cross", "macd", "breakout"], "mean_reversion": ["rsi", "bollinger", "mean_reversion"], "momentum": ["momentum", "volume_momentum"], }, } @mcp.tool() async def parse_strategy(ctx: Context, description: str) -> dict[str, Any]: """Parse natural language strategy description into VectorBT parameters. Args: description: Natural language description of trading strategy Returns: Parsed strategy configuration with type and parameters Examples: "Buy when RSI is below 30 and sell when above 70" "Use 10-day and 20-day moving average crossover" "MACD strategy with standard parameters" """ parser = StrategyParser() config = parser.parse_simple(description) # Validate the parsed strategy if parser.validate_strategy(config): return { "success": True, "strategy": config, "message": f"Successfully parsed as {config['strategy_type']} strategy", } else: return { "success": False, "strategy": config, "message": "Could not fully parse strategy, using defaults", } @mcp.tool() async def backtest_portfolio( ctx: Context, symbols: list[str], strategy: str = "sma_cross", start_date: str | None = None, end_date: str | None = None, initial_capital: float = 10000.0, position_size: float = 0.1, fast_period: str | int | None = None, slow_period: str | int | None = None, period: str | int | None = None, ) -> dict[str, Any]: """Backtest a strategy across multiple symbols (portfolio). Args: symbols: List of stock symbols strategy: Strategy type to apply start_date: Start date (YYYY-MM-DD) end_date: End date (YYYY-MM-DD) initial_capital: Starting capital position_size: Position size per symbol (0.1 = 10%) Strategy-specific parameters as individual arguments Returns: Portfolio backtest results with aggregate metrics """ from datetime import datetime, timedelta # Default date range if not end_date: end_date = datetime.now().strftime("%Y-%m-%d") if not start_date: start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") # Convert string parameters to appropriate types def convert_param(value, param_type): """Convert string parameter to appropriate type.""" if value is None: return None if isinstance(value, str): try: if param_type is int: return int(value) elif param_type is float: return float(value) except (ValueError, TypeError) as e: raise ValueError( f"Invalid {param_type.__name__} value: {value}" ) from e return value # Build parameters dict from provided arguments with type conversion param_map = { "fast_period": convert_param(fast_period, int), "slow_period": convert_param(slow_period, int), "period": convert_param(period, int), } # Get parameters if strategy in STRATEGY_TEMPLATES: parameters = dict(STRATEGY_TEMPLATES[strategy]["parameters"]) # Override with provided non-None parameters for param_name, param_value in param_map.items(): if param_value is not None: parameters[param_name] = param_value else: # Use only provided parameters for unknown strategies parameters = {k: v for k, v in param_map.items() if v is not None} # Run backtests for each symbol engine = VectorBTEngine() portfolio_results = [] capital_per_symbol = initial_capital * position_size for symbol in symbols: try: results = await engine.run_backtest( symbol=symbol, strategy_type=strategy, parameters=parameters, start_date=start_date, end_date=end_date, initial_capital=capital_per_symbol, ) portfolio_results.append(results) except Exception: # Skip failed symbols continue if not portfolio_results: return {"error": "No symbols could be backtested"} # Aggregate portfolio metrics total_return = sum( r["metrics"]["total_return"] for r in portfolio_results ) / len(portfolio_results) avg_sharpe = sum(r["metrics"]["sharpe_ratio"] for r in portfolio_results) / len( portfolio_results ) max_drawdown = max(r["metrics"]["max_drawdown"] for r in portfolio_results) total_trades = sum(r["metrics"]["total_trades"] for r in portfolio_results) return { "portfolio_metrics": { "symbols_tested": len(portfolio_results), "total_return": total_return, "average_sharpe": avg_sharpe, "max_drawdown": max_drawdown, "total_trades": total_trades, }, "individual_results": portfolio_results, "summary": f"Portfolio backtest of {len(portfolio_results)} symbols with {strategy} strategy", } @mcp.tool() async def generate_backtest_charts( ctx: Context, symbol: str, strategy: str = "sma_cross", start_date: str | None = None, end_date: str | None = None, theme: str = "light", ) -> dict[str, str]: """Generate comprehensive charts for a backtest. Args: symbol: Stock symbol strategy: Strategy type start_date: Start date (YYYY-MM-DD) end_date: End date (YYYY-MM-DD) theme: Chart theme (light or dark) Returns: Dictionary of base64-encoded chart images """ from datetime import datetime, timedelta import pandas as pd # Default date range if not end_date: end_date = datetime.now().strftime("%Y-%m-%d") if not start_date: start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") # Run backtest engine = VectorBTEngine() # Get default parameters for the strategy from maverick_mcp.backtesting.strategies import STRATEGY_TEMPLATES parameters = STRATEGY_TEMPLATES.get(strategy, {}).get("parameters", {}) results = await engine.run_backtest( symbol=symbol, strategy_type=strategy, parameters=parameters, start_date=start_date, end_date=end_date, ) # Prepare data for charts equity_curve_data = results["equity_curve"] drawdown_data = results["drawdown_series"] # Convert to pandas Series for charting returns = pd.Series(equity_curve_data) drawdown = pd.Series(drawdown_data) trades = pd.DataFrame(results["trades"]) # Generate charts charts = { "equity_curve": generate_equity_curve( returns, drawdown, f"{symbol} {strategy} Equity Curve", theme ), "trade_scatter": generate_trade_scatter( returns, trades, f"{symbol} {strategy} Trades", theme ), "performance_dashboard": generate_performance_dashboard( results["metrics"], f"{symbol} {strategy} Performance", theme ), } return charts @mcp.tool() async def generate_optimization_charts( ctx: Context, symbol: str, strategy: str = "sma_cross", start_date: str | None = None, end_date: str | None = None, theme: str = "light", ) -> dict[str, str]: """Generate chart for strategy parameter optimization. Args: symbol: Stock symbol strategy: Strategy type start_date: Start date (YYYY-MM-DD) end_date: End date (YYYY-MM-DD) theme: Chart theme (light or dark) Returns: Dictionary of base64-encoded chart images """ from datetime import datetime, timedelta # Default date range if not end_date: end_date = datetime.now().strftime("%Y-%m-%d") if not start_date: start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") # Run optimization engine = VectorBTEngine() optimizer = StrategyOptimizer(engine) param_grid = optimizer.generate_param_grid(strategy, "medium") # Create optimization results dictionary for heatmap optimization_results = {} for param_set, results in param_grid.items(): optimization_results[str(param_set)] = { "performance": results.get("total_return", 0) } # Generate optimization heatmap heatmap = generate_optimization_heatmap( optimization_results, f"{symbol} {strategy} Parameter Optimization", theme ) return {"optimization_heatmap": heatmap} # ============ ML-ENHANCED STRATEGY TOOLS ============ @mcp.tool() async def run_ml_strategy_backtest( ctx: Context, symbol: str, strategy_type: str = "ml_predictor", start_date: str | None = None, end_date: str | None = None, initial_capital: float = 10000.0, train_ratio: float = 0.8, model_type: str = "random_forest", n_estimators: int = 100, max_depth: int | None = None, learning_rate: float = 0.01, adaptation_method: str = "gradient", ) -> dict[str, Any]: """Run backtest using ML-enhanced strategies. Args: symbol: Stock symbol to backtest strategy_type: ML strategy type (ml_predictor, adaptive, ensemble, regime_aware) start_date: Start date (YYYY-MM-DD) end_date: End date (YYYY-MM-DD) initial_capital: Initial capital amount train_ratio: Ratio of data for training (0.0-1.0) Strategy-specific parameters passed as individual arguments Returns: Backtest results with ML-specific metrics """ from datetime import datetime, timedelta from maverick_mcp.backtesting.strategies.ml import ( AdaptiveStrategy, MLPredictor, RegimeAwareStrategy, StrategyEnsemble, ) from maverick_mcp.backtesting.strategies.templates import ( SimpleMovingAverageStrategy, ) # Default date range if not end_date: end_date = datetime.now().strftime("%Y-%m-%d") if not start_date: start_date = (datetime.now() - timedelta(days=730)).strftime( "%Y-%m-%d" ) # 2 years for ML # Get historical data engine = VectorBTEngine() data = await engine.get_historical_data(symbol, start_date, end_date) # Enhanced data validation for ML strategies min_total_data = 200 # Minimum total data points for ML strategies if len(data) < min_total_data: return { "error": f"Insufficient data for ML strategy: {len(data)} < {min_total_data} required" } # Split data for training/testing split_idx = int(len(data) * train_ratio) train_data = data.iloc[:split_idx] test_data = data.iloc[split_idx:] # Validate split data sizes min_train_data = 100 min_test_data = 50 if len(train_data) < min_train_data: return { "error": f"Insufficient training data: {len(train_data)} < {min_train_data} required" } if len(test_data) < min_test_data: return { "error": f"Insufficient test data: {len(test_data)} < {min_test_data} required" } logger.info( f"ML backtest data split: {len(train_data)} training, {len(test_data)} testing samples" ) try: # Create ML strategy based on type if strategy_type == "ml_predictor": ml_strategy = MLPredictor( model_type=model_type, n_estimators=n_estimators, max_depth=max_depth, ) # Train the model training_metrics = ml_strategy.train(train_data) elif strategy_type == "adaptive" or strategy_type == "online_learning": # online_learning is an alias for adaptive strategy base_strategy = SimpleMovingAverageStrategy() ml_strategy = AdaptiveStrategy( base_strategy, learning_rate=learning_rate, adaptation_method=adaptation_method, ) training_metrics = { "adaptation_method": adaptation_method, "strategy_alias": strategy_type, } elif strategy_type == "ensemble": # Create ensemble with basic strategies base_strategies = [ SimpleMovingAverageStrategy({"fast_period": 10, "slow_period": 20}), SimpleMovingAverageStrategy({"fast_period": 5, "slow_period": 15}), ] ml_strategy = StrategyEnsemble(base_strategies) training_metrics = {"ensemble_size": len(base_strategies)} elif strategy_type == "regime_aware": base_strategies = { 0: SimpleMovingAverageStrategy( {"fast_period": 5, "slow_period": 20} ), # Bear 1: SimpleMovingAverageStrategy( {"fast_period": 10, "slow_period": 30} ), # Sideways 2: SimpleMovingAverageStrategy( {"fast_period": 20, "slow_period": 50} ), # Bull } ml_strategy = RegimeAwareStrategy(base_strategies) # Fit regime detector ml_strategy.fit_regime_detector(train_data) training_metrics = {"n_regimes": len(base_strategies)} else: return {"error": f"Unsupported ML strategy type: {strategy_type}"} # Generate signals on test data entry_signals, exit_signals = ml_strategy.generate_signals(test_data) # Run backtest analysis on test period analyzer = BacktestAnalyzer() backtest_results = await analyzer.run_vectorbt_backtest( data=test_data, entry_signals=entry_signals, exit_signals=exit_signals, initial_capital=initial_capital, ) # Add ML-specific metrics ml_metrics = { "strategy_type": strategy_type, "training_period": len(train_data), "testing_period": len(test_data), "train_test_split": train_ratio, "training_metrics": training_metrics, } # Add strategy-specific analysis if hasattr(ml_strategy, "get_feature_importance"): ml_metrics["feature_importance"] = ml_strategy.get_feature_importance() if hasattr(ml_strategy, "get_regime_analysis"): ml_metrics["regime_analysis"] = ml_strategy.get_regime_analysis() if hasattr(ml_strategy, "get_strategy_weights"): ml_metrics["strategy_weights"] = ml_strategy.get_strategy_weights() backtest_results["ml_metrics"] = ml_metrics # Convert all numpy types before returning return convert_numpy_types(backtest_results) except Exception as e: return {"error": f"ML backtest failed: {str(e)}"} @mcp.tool() async def train_ml_predictor( ctx: Context, symbol: str, start_date: str | None = None, end_date: str | None = None, model_type: str = "random_forest", target_periods: int = 5, return_threshold: float = 0.02, n_estimators: int = 100, max_depth: int | None = None, min_samples_split: int = 2, ) -> dict[str, Any]: """Train an ML predictor model for trading signals. Args: symbol: Stock symbol to train on start_date: Start date for training data end_date: End date for training data model_type: ML model type (random_forest) target_periods: Forward periods for target variable return_threshold: Return threshold for signal classification n_estimators, max_depth, min_samples_split: Model-specific parameters Returns: Training results and model metrics """ from datetime import datetime, timedelta from maverick_mcp.backtesting.strategies.ml import MLPredictor # Default date range (2 years for good ML training) if not end_date: end_date = datetime.now().strftime("%Y-%m-%d") if not start_date: start_date = (datetime.now() - timedelta(days=730)).strftime("%Y-%m-%d") try: # Get training data engine = VectorBTEngine() data = await engine.get_historical_data(symbol, start_date, end_date) if len(data) < 200: return { "error": "Insufficient data for ML training (minimum 200 data points)" } # Create and train ML predictor ml_predictor = MLPredictor( model_type=model_type, n_estimators=n_estimators, max_depth=max_depth, min_samples_split=min_samples_split, ) training_metrics = ml_predictor.train( data=data, target_periods=target_periods, return_threshold=return_threshold, ) # Create model parameters dictionary model_params = { "n_estimators": n_estimators, "max_depth": max_depth, "min_samples_split": min_samples_split, } # Add training details training_results = { "symbol": symbol, "model_type": model_type, "training_period": f"{start_date} to {end_date}", "data_points": len(data), "target_periods": target_periods, "return_threshold": return_threshold, "model_parameters": model_params, "training_metrics": training_metrics, } # Convert all numpy types before returning return convert_numpy_types(training_results) except Exception as e: return {"error": f"ML training failed: {str(e)}"} @mcp.tool() async def analyze_market_regimes( ctx: Context, symbol: str, start_date: str | None = None, end_date: str | None = None, method: str = "hmm", n_regimes: int = 3, lookback_period: int = 50, ) -> dict[str, Any]: """Analyze market regimes for a stock using ML methods. Args: symbol: Stock symbol to analyze start_date: Start date for analysis end_date: End date for analysis method: Detection method (hmm, kmeans, threshold) n_regimes: Number of regimes to detect lookback_period: Lookback period for regime detection Returns: Market regime analysis results """ from datetime import datetime, timedelta from maverick_mcp.backtesting.strategies.ml.regime_aware import ( MarketRegimeDetector, ) # Default date range if not end_date: end_date = datetime.now().strftime("%Y-%m-%d") if not start_date: start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") try: # Get historical data engine = VectorBTEngine() data = await engine.get_historical_data(symbol, start_date, end_date) if len(data) < lookback_period + 50: return { "error": f"Insufficient data for regime analysis (minimum {lookback_period + 50} data points)" } # Create regime detector and analyze regime_detector = MarketRegimeDetector( method=method, n_regimes=n_regimes, lookback_period=lookback_period ) # Fit regime detector regime_detector.fit_regimes(data) # Analyze regimes over time regime_history = [] regime_probabilities = [] for i in range(lookback_period, len(data)): window_data = data.iloc[i - lookback_period : i + 1] current_regime = regime_detector.detect_current_regime(window_data) regime_probs = regime_detector.get_regime_probabilities(window_data) regime_history.append( { "date": data.index[i].strftime("%Y-%m-%d"), "regime": int(current_regime), "probabilities": regime_probs.tolist(), } ) regime_probabilities.append(regime_probs) # Calculate regime statistics regimes = [r["regime"] for r in regime_history] regime_counts = {i: regimes.count(i) for i in range(n_regimes)} regime_percentages = { k: (v / len(regimes)) * 100 for k, v in regime_counts.items() } # Calculate average regime durations regime_durations = {i: [] for i in range(n_regimes)} current_regime = regimes[0] duration = 1 for regime in regimes[1:]: if regime == current_regime: duration += 1 else: regime_durations[current_regime].append(duration) current_regime = regime duration = 1 regime_durations[current_regime].append(duration) avg_durations = { k: np.mean(v) if v else 0 for k, v in regime_durations.items() } analysis_results = { "symbol": symbol, "analysis_period": f"{start_date} to {end_date}", "method": method, "n_regimes": n_regimes, "regime_names": { 0: "Bear/Declining", 1: "Sideways/Uncertain", 2: "Bull/Trending", }, "current_regime": regimes[-1] if regimes else 1, "regime_counts": regime_counts, "regime_percentages": regime_percentages, "average_regime_durations": avg_durations, "recent_regime_history": regime_history[-20:], # Last 20 periods "total_regime_switches": len( [i for i in range(1, len(regimes)) if regimes[i] != regimes[i - 1]] ), } return analysis_results except Exception as e: return {"error": f"Regime analysis failed: {str(e)}"} @mcp.tool() async def create_strategy_ensemble( ctx: Context, symbols: list[str], base_strategies: list[str] | None = None, weighting_method: str = "performance", start_date: str | None = None, end_date: str | None = None, initial_capital: float = 10000.0, ) -> dict[str, Any]: """Create and backtest a strategy ensemble across multiple symbols. Args: symbols: List of stock symbols base_strategies: List of base strategy names to ensemble weighting_method: Weighting method (performance, equal, volatility) start_date: Start date for backtesting end_date: End date for backtesting initial_capital: Initial capital per symbol Returns: Ensemble backtest results with strategy weights """ from datetime import datetime, timedelta from maverick_mcp.backtesting.strategies.ml import StrategyEnsemble from maverick_mcp.backtesting.strategies.templates import ( SimpleMovingAverageStrategy, ) # Default strategies if none provided if base_strategies is None: base_strategies = ["sma_cross", "rsi", "macd"] # Default date range if not end_date: end_date = datetime.now().strftime("%Y-%m-%d") if not start_date: start_date = (datetime.now() - timedelta(days=365)).strftime("%Y-%m-%d") try: # Create base strategy instances strategy_instances = [] for strategy_name in base_strategies: if strategy_name == "sma_cross": strategy_instances.append(SimpleMovingAverageStrategy()) elif strategy_name == "rsi": # Create RSI-based SMA strategy with different parameters strategy_instances.append( SimpleMovingAverageStrategy( {"fast_period": 14, "slow_period": 28} ) ) elif strategy_name == "macd": # Create MACD-like SMA strategy with MACD default periods strategy_instances.append( SimpleMovingAverageStrategy( {"fast_period": 12, "slow_period": 26} ) ) # Add more strategies as needed if not strategy_instances: return {"error": "No valid base strategies provided"} # Create ensemble strategy ensemble = StrategyEnsemble( strategies=strategy_instances, weighting_method=weighting_method ) # Run ensemble backtest on multiple symbols ensemble_results = [] total_return = 0 total_trades = 0 for symbol in symbols[:5]: # Limit to 5 symbols for performance try: # Get data and run backtest engine = VectorBTEngine() data = await engine.get_historical_data( symbol, start_date, end_date ) if len(data) < 100: continue # Generate ensemble signals entry_signals, exit_signals = ensemble.generate_signals(data) # Run backtest analyzer = BacktestAnalyzer() results = await analyzer.run_vectorbt_backtest( data=data, entry_signals=entry_signals, exit_signals=exit_signals, initial_capital=initial_capital, ) # Add ensemble-specific metrics results["ensemble_metrics"] = { "strategy_weights": ensemble.get_strategy_weights(), "strategy_performance": ensemble.get_strategy_performance(), } ensemble_results.append({"symbol": symbol, "results": results}) total_return += results["metrics"]["total_return"] total_trades += results["metrics"]["total_trades"] except Exception: continue if not ensemble_results: return {"error": "No symbols could be processed"} # Calculate aggregate metrics avg_return = total_return / len(ensemble_results) avg_trades = total_trades / len(ensemble_results) # Convert all numpy types before returning return convert_numpy_types( { "ensemble_summary": { "symbols_tested": len(ensemble_results), "base_strategies": base_strategies, "weighting_method": weighting_method, "average_return": avg_return, "total_trades": total_trades, "average_trades_per_symbol": avg_trades, }, "individual_results": ensemble_results, "final_strategy_weights": ensemble.get_strategy_weights(), "strategy_performance_analysis": ensemble.get_strategy_performance(), } ) except Exception as e: return {"error": f"Ensemble creation failed: {str(e)}"}

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