Skip to main content
Glama
Ademscodeisnotsobad

Quant Companion MCP

runBacktest.ts16.6 kB
/** * MCP Tool: run_backtest * * Run a backtest with a configurable strategy. */ import { z } from "zod"; import { runBacktest as coreRunBacktest, createMACrossoverSignal, createMomentumSignal, createMeanReversionSignal, createDualMomentumSignal, createVolFilteredTrendSignal, createAdaptiveMomentumSignal, createMomentumPlusSignal, computeBacktestStats, extractDetailedTrades, runMultiAssetBacktest, Candle, BacktestConfig, RiskMetrics, DetailedTradeRecord, RegimeStats, MultiAssetBacktestResult, } from "@quant-companion/core"; import { getDefaultProvider } from "../marketData"; const strategySchema = z.discriminatedUnion("type", [ z.object({ type: z.literal("ma_crossover"), fastWindow: z.number().int().positive().describe("Fast moving average period"), slowWindow: z.number().int().positive().describe("Slow moving average period"), }), z.object({ type: z.literal("momentum"), lookback: z.number().int().positive().describe("Lookback period for momentum"), threshold: z .number() .optional() .default(0) .describe("Minimum return threshold to go long"), }), z.object({ type: z.literal("mean_reversion"), period: z.number().int().positive().optional().default(14).describe("RSI period"), oversoldThreshold: z .number() .optional() .default(30) .describe("RSI level to enter long"), overboughtThreshold: z .number() .optional() .default(70) .describe("RSI level to exit"), }), z.object({ type: z.literal("dual_momentum"), lookbackDays: z .number() .int() .positive() .optional() .default(252) .describe("Momentum lookback in trading days (default 252 = ~12 months)"), }), z.object({ type: z.literal("vol_filtered_trend"), fastWindow: z.number().int().positive().optional().default(20).describe("Fast MA period"), slowWindow: z.number().int().positive().optional().default(50).describe("Slow MA period"), volLookback: z.number().int().positive().optional().default(20).describe("Volatility lookback"), maxVolThreshold: z .number() .positive() .optional() .default(0.25) .describe("Max annualized vol to enter (default 0.25 = 25%)"), }), z.object({ type: z.literal("adaptive_momentum"), shortLookback: z.number().int().positive().optional().default(60).describe("Short lookback for high vol"), longLookback: z.number().int().positive().optional().default(252).describe("Long lookback for low vol"), volThreshold: z .number() .positive() .optional() .default(0.20) .describe("Vol threshold to switch lookback (default 0.20 = 20%)"), }), z.object({ type: z.literal("momentum_plus"), longLookback: z.number().int().positive().optional().default(252).describe("12-month lookback (default 252)"), shortLookback: z.number().int().positive().optional().default(63).describe("3-month lookback (default 63)"), entryThreshold: z.number().optional().default(0).describe("Min momentum to enter (default 0)"), exitThreshold: z.number().optional().default(-0.05).describe("Exit when 12mo < this (default -5%)"), shortExitThreshold: z.number().optional().default(-0.10).describe("Exit when 3mo < this (default -10%)"), maxVolThreshold: z.number().positive().optional().default(0.30).describe("Max vol to enter (default 30%)"), }), z.object({ type: z.literal("momentum_plus_multi"), }), ]); export const runBacktestSchema = z.object({ symbol: z.string().optional().describe("Stock/ETF ticker symbol (ignored for momentum_plus_multi)"), symbols: z.array(z.string()).optional().describe("Multiple symbols for multi-asset strategies"), start: z.string().describe("Start date in YYYY-MM-DD format"), end: z.string().describe("End date in YYYY-MM-DD format"), initialCapital: z .number() .positive() .default(10000) .describe("Starting capital (default $10,000)"), strategy: strategySchema.describe("Trading strategy configuration"), slippageBps: z .number() .nonnegative() .optional() .default(10) .describe("Slippage in basis points (default 10 = 0.1%)"), feePerTrade: z .number() .nonnegative() .optional() .default(0) .describe("Fixed fee per trade (default 0)"), }); export type RunBacktestInput = z.infer<typeof runBacktestSchema>; export interface RunBacktestOutput { symbol?: string; symbols?: string[]; period: { start: string; end: string; tradingDays: number; }; initialCapital: number; finalCapital: number; strategy: RunBacktestInput["strategy"]; riskMetrics: RiskMetrics & { totalReturnPercent: number; annualizedReturnPercent: number; annualizedVolPercent: number; maxDrawdownPercent: number; }; tradeStats?: { totalTrades: number; winningTrades: number; losingTrades: number; winRate: number; avgWin: number; avgLoss: number; profitFactor: number; }; /** Regime statistics (for multi-asset strategies) */ regimeStats?: RegimeStats; /** Detailed trade log with entry/exit dates, prices, and P&L */ trades?: DetailedTradeRecord[]; equityCurveSample: number[]; timestampsSample: string[]; } export async function runBacktest( input: RunBacktestInput ): Promise<RunBacktestOutput> { const provider = getDefaultProvider(); const start = new Date(input.start); const end = new Date(input.end); if (isNaN(start.getTime())) { throw new Error(`Invalid start date: ${input.start}`); } if (isNaN(end.getTime())) { throw new Error(`Invalid end date: ${input.end}`); } // Handle multi-asset strategy if (input.strategy.type === "momentum_plus_multi") { return runMultiAssetBacktestHandler(input, provider, start, end); } // Single-asset strategy (existing behavior) const symbol = input.symbol; if (!symbol) { throw new Error("symbol is required for single-asset strategies"); } // Fetch historical data const ohlcv = await provider.getHistoricalOHLCV({ symbol, start, end, interval: "1d", }); if (ohlcv.length < 2) { throw new Error(`Not enough data for ${symbol} in the specified period`); } // Convert to Candle format const candles: Candle[] = ohlcv.map((c) => ({ timestamp: c.timestamp, open: c.open, high: c.high, low: c.low, close: c.close, volume: c.volume, })); // Create signal generator based on strategy let generateSignal: BacktestConfig["generateSignal"]; switch (input.strategy.type) { case "ma_crossover": generateSignal = createMACrossoverSignal( input.strategy.fastWindow, input.strategy.slowWindow ); break; case "momentum": generateSignal = createMomentumSignal( input.strategy.lookback, input.strategy.threshold ); break; case "mean_reversion": generateSignal = createMeanReversionSignal( input.strategy.period, input.strategy.oversoldThreshold, input.strategy.overboughtThreshold ); break; case "dual_momentum": generateSignal = createDualMomentumSignal( input.strategy.lookbackDays ); break; case "vol_filtered_trend": generateSignal = createVolFilteredTrendSignal( input.strategy.fastWindow, input.strategy.slowWindow, input.strategy.volLookback, input.strategy.maxVolThreshold ); break; case "adaptive_momentum": generateSignal = createAdaptiveMomentumSignal( input.strategy.shortLookback, input.strategy.longLookback, input.strategy.volThreshold ); break; case "momentum_plus": generateSignal = createMomentumPlusSignal( input.strategy.longLookback, input.strategy.shortLookback, input.strategy.entryThreshold, input.strategy.exitThreshold, input.strategy.shortExitThreshold, input.strategy.maxVolThreshold ); break; } // Run backtest const result = coreRunBacktest(candles, { initialCapital: input.initialCapital, generateSignal, slippageBps: input.slippageBps, feePerTrade: input.feePerTrade, }); // Compute trade statistics const tradeStats = computeBacktestStats(result); // Extract detailed trade log with dates and symbol const trades = extractDetailedTrades(result, candles, symbol); // Sample equity curve for output (max 100 points) const { equityCurveSample, timestampsSample } = sampleEquityCurve( result.equityCurve, result.timestamps ); return { symbol, period: { start: input.start, end: input.end, tradingDays: candles.length, }, initialCapital: input.initialCapital, finalCapital: result.equityCurve[result.equityCurve.length - 1], strategy: input.strategy, riskMetrics: { ...result.riskMetrics, totalReturnPercent: result.riskMetrics.totalReturn * 100, annualizedReturnPercent: result.riskMetrics.annualizedReturn * 100, annualizedVolPercent: result.riskMetrics.annualizedVol * 100, maxDrawdownPercent: result.riskMetrics.maxDrawdown * 100, }, tradeStats, trades, equityCurveSample, timestampsSample, }; } /** * Sample equity curve for output (max 100 points) */ function sampleEquityCurve( equityCurve: number[], timestamps: number[] ): { equityCurveSample: number[]; timestampsSample: string[] } { const sampleSize = Math.min(100, equityCurve.length); const step = Math.max(1, Math.floor(equityCurve.length / sampleSize)); const equityCurveSample: number[] = []; const timestampsSample: string[] = []; for (let i = 0; i < equityCurve.length; i += step) { equityCurveSample.push(equityCurve[i]); timestampsSample.push(new Date(timestamps[i]).toISOString().split("T")[0]); } // Always include the last point if ( equityCurveSample[equityCurveSample.length - 1] !== equityCurve[equityCurve.length - 1] ) { equityCurveSample.push(equityCurve[equityCurve.length - 1]); timestampsSample.push( new Date(timestamps[timestamps.length - 1]).toISOString().split("T")[0] ); } return { equityCurveSample, timestampsSample }; } /** * Handler for multi-asset backtest */ async function runMultiAssetBacktestHandler( input: RunBacktestInput, provider: ReturnType<typeof getDefaultProvider>, start: Date, end: Date ): Promise<RunBacktestOutput> { const symbols = ["SPY", "QQQ", "TLT", "GLD"]; // Fetch data for all symbols in parallel const ohlcvResults = await Promise.all( symbols.map((symbol) => provider.getHistoricalOHLCV({ symbol, start, end, interval: "1d" }) ) ); // Build candles map const candlesBySymbol = new Map<string, Candle[]>(); for (let i = 0; i < symbols.length; i++) { const symbol = symbols[i]; const ohlcv = ohlcvResults[i]; if (ohlcv.length < 253) { throw new Error( `Not enough data for ${symbol}: need at least 253 days, got ${ohlcv.length}` ); } candlesBySymbol.set( symbol, ohlcv.map((c) => ({ timestamp: c.timestamp, open: c.open, high: c.high, low: c.low, close: c.close, volume: c.volume, })) ); } // Run multi-asset backtest const result = runMultiAssetBacktest(candlesBySymbol, { symbols, initialCapital: input.initialCapital, slippageBps: input.slippageBps, feePerTrade: input.feePerTrade, }); // Sample equity curve const { equityCurveSample, timestampsSample } = sampleEquityCurve( result.equityCurve, result.timestamps ); return { symbols: result.symbols, period: { start: input.start, end: input.end, tradingDays: result.equityCurve.length, }, initialCapital: input.initialCapital, finalCapital: result.equityCurve[result.equityCurve.length - 1], strategy: input.strategy, riskMetrics: { ...result.riskMetrics, totalReturnPercent: result.riskMetrics.totalReturn * 100, annualizedReturnPercent: result.riskMetrics.annualizedReturn * 100, annualizedVolPercent: result.riskMetrics.annualizedVol * 100, maxDrawdownPercent: result.riskMetrics.maxDrawdown * 100, }, regimeStats: result.regimeStats, equityCurveSample, timestampsSample, }; } export const runBacktestDefinition = { name: "run_backtest", description: "Run a backtest on historical data with a specified trading strategy. Returns equity curve, risk metrics, trade statistics, and detailed trade log. Strategies: ma_crossover, momentum, mean_reversion, dual_momentum (12-month absolute momentum), vol_filtered_trend (trend following with vol filter), adaptive_momentum (adapts lookback to vol regime), momentum_plus (optimized dual timeframe momentum targeting 12%+ CAGR), momentum_plus_multi (multi-asset regime allocator using SPY/QQQ/TLT/GLD).", inputSchema: { type: "object", properties: { symbol: { type: "string", description: "Stock/ETF ticker symbol (e.g., SPY, QQQ). Not required for momentum_plus_multi.", }, symbols: { type: "array", items: { type: "string" }, description: "Multiple symbols for multi-asset strategies (auto-set for momentum_plus_multi)", }, start: { type: "string", description: "Start date in YYYY-MM-DD format", }, end: { type: "string", description: "End date in YYYY-MM-DD format", }, initialCapital: { type: "number", description: "Starting capital in dollars (default 10000)", }, strategy: { type: "object", description: "Strategy types: 'ma_crossover' (fastWindow, slowWindow), 'momentum' (lookback, threshold), 'mean_reversion' (period, oversoldThreshold, overboughtThreshold), 'dual_momentum' (lookbackDays=252), 'vol_filtered_trend' (fastWindow, slowWindow, volLookback, maxVolThreshold), 'adaptive_momentum' (shortLookback, longLookback, volThreshold), 'momentum_plus' (optimized: longLookback, shortLookback, entryThreshold, exitThreshold, shortExitThreshold, maxVolThreshold), 'momentum_plus_multi' (multi-asset regime allocator: SPY/QQQ/TLT/GLD)", properties: { type: { type: "string", enum: ["ma_crossover", "momentum", "mean_reversion", "dual_momentum", "vol_filtered_trend", "adaptive_momentum", "momentum_plus", "momentum_plus_multi"], }, fastWindow: { type: "number", description: "Fast MA period" }, slowWindow: { type: "number", description: "Slow MA period" }, lookback: { type: "number", description: "Lookback period (for momentum)" }, lookbackDays: { type: "number", description: "Lookback in days (for dual_momentum, default 252)" }, threshold: { type: "number", description: "Return threshold (for momentum)" }, period: { type: "number", description: "RSI period (for mean_reversion)" }, oversoldThreshold: { type: "number", description: "Oversold RSI level" }, overboughtThreshold: { type: "number", description: "Overbought RSI level" }, volLookback: { type: "number", description: "Vol calculation period (for vol_filtered_trend)" }, maxVolThreshold: { type: "number", description: "Max vol to enter (for vol_filtered_trend, default 0.25)" }, shortLookback: { type: "number", description: "Short lookback for high vol (adaptive_momentum)" }, longLookback: { type: "number", description: "Long lookback for low vol (adaptive_momentum) or 12-month (momentum_plus)" }, volThreshold: { type: "number", description: "Vol regime threshold (adaptive_momentum, default 0.20)" }, entryThreshold: { type: "number", description: "Min momentum to enter (momentum_plus, default 0)" }, exitThreshold: { type: "number", description: "Exit when 12mo below this (momentum_plus, default -0.05)" }, shortExitThreshold: { type: "number", description: "Exit when 3mo below this (momentum_plus, default -0.10)" }, }, required: ["type"], }, slippageBps: { type: "number", description: "Slippage in basis points (default 10 = 0.1%)", }, feePerTrade: { type: "number", description: "Fixed fee per trade in dollars (default 0)", }, }, required: ["start", "end", "strategy"], }, };

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/Ademscodeisnotsobad/Quant-Companion-MCP'

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