Skip to main content
Glama
Ademscodeisnotsobad

Quant Companion MCP

run-options-backtest.ts18.6 kB
/** * Run Options Strategies Backtest * * Compares: Wheel, Covered Calls, Put Selling * vs: SPY Buy-and-Hold, momentum_plus_multi */ import * as fs from "fs"; import * as path from "path"; import { runWheelBacktest, runCoveredCallBacktest, runPutSellingBacktest, OptionsBacktestConfig, OptionsBacktestResult, runMultiAssetBacktest, Candle, } from "@quant-companion/core"; // Yahoo Finance API (direct fetch) async function fetchCandles(symbol: string, startDate: string, endDate: string): Promise<Candle[]> { const start = new Date(startDate); const end = new Date(endDate); const startTs = Math.floor(start.getTime() / 1000); const endTs = Math.floor(end.getTime() / 1000); console.log(`Fetching ${symbol} data...`); const url = `https://query1.finance.yahoo.com/v8/finance/chart/${symbol}?period1=${startTs}&period2=${endTs}&interval=1d`; const res = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", }, }); if (!res.ok) { throw new Error(`Failed to fetch ${symbol}: ${res.status}`); } const data = await res.json(); const result = data.chart.result[0]; const timestamps = result.timestamp; const quotes = result.indicators.quote[0]; const candles: Candle[] = []; for (let i = 0; i < timestamps.length; i++) { if (quotes.close[i] != null) { candles.push({ timestamp: timestamps[i] * 1000, open: quotes.open[i], high: quotes.high[i], low: quotes.low[i], close: quotes.close[i], volume: quotes.volume[i], }); } } console.log(` Got ${candles.length} candles`); return candles; } async function main() { const startDate = "2015-01-01"; const endDate = "2025-12-01"; const initialCapital = 100_000; console.log("\n==========================================="); console.log("OPTIONS STRATEGIES BACKTEST"); console.log(`Capital: $${initialCapital.toLocaleString()}`); console.log(`Period: ${startDate} to ${endDate}`); console.log("===========================================\n"); // Fetch SPY data const spyCandles = await fetchCandles("SPY", startDate, endDate); // Options config const optionsConfig: OptionsBacktestConfig = { initialCapital, riskFreeRate: 0.04, // ~4% (current rates) daysToExpiration: 30, // Monthly options targetDelta: 0.30, // 30-delta (typical for premium selling) optionSlippagePct: 0.02, // 2% slippage on premium commissionPerContract: 0.65, // $0.65 per contract positionSizing: "percent_of_capital", capitalPercentage: 0.25, // Use 25% of capital per position }; console.log("\n--- Running Wheel Strategy ---"); const wheelResult = runWheelBacktest(spyCandles, optionsConfig); printResult("Wheel", wheelResult); console.log("\n--- Running Covered Call Strategy ---"); const ccResult = runCoveredCallBacktest(spyCandles, optionsConfig); printResult("Covered Call", ccResult); console.log("\n--- Running Put Selling Strategy ---"); const putResult = runPutSellingBacktest(spyCandles, optionsConfig); printResult("Put Selling", putResult); // Calculate SPY Buy-and-Hold for comparison console.log("\n--- Computing SPY Buy-and-Hold ---"); const buyHoldEquity = computeBuyAndHold(spyCandles, initialCapital); const buyHoldReturn = (buyHoldEquity[buyHoldEquity.length - 1] / initialCapital - 1) * 100; const buyHoldCAGR = Math.pow(buyHoldEquity[buyHoldEquity.length - 1] / initialCapital, 1 / 10.9) - 1; console.log(` Final: $${buyHoldEquity[buyHoldEquity.length - 1].toLocaleString(undefined, { maximumFractionDigits: 0 })}`); console.log(` CAGR: ${(buyHoldCAGR * 100).toFixed(2)}%`); // Fetch multi-asset data and run momentum_plus_multi console.log("\n--- Running momentum_plus_multi ---"); const symbols = ["SPY", "QQQ", "TLT", "GLD"]; const multiCandles = new Map<string, Candle[]>(); for (const sym of symbols) { multiCandles.set(sym, await fetchCandles(sym, startDate, endDate)); } const multiResult = runMultiAssetBacktest(multiCandles, { initialCapital, slippageBps: 10, feePerTrade: 0, }); console.log(` Final: $${multiResult.equityCurve[multiResult.equityCurve.length - 1].toLocaleString(undefined, { maximumFractionDigits: 0 })}`); console.log(` CAGR: ${((multiResult.riskMetrics.annualizedReturn || 0) * 100).toFixed(2)}%`); console.log(` Max DD: ${((multiResult.riskMetrics.maxDrawdown || 0) * 100).toFixed(2)}%`); console.log(` Sharpe: ${(multiResult.riskMetrics.sharpeRatio || 0).toFixed(2)}`); // Generate comparison report console.log("\n\n==========================================="); console.log("COMPARISON SUMMARY"); console.log("===========================================\n"); const comparison = [ { strategy: "Wheel", final: wheelResult.equityCurve[wheelResult.equityCurve.length - 1], cagr: wheelResult.riskMetrics.annualizedReturn || 0, maxDD: wheelResult.riskMetrics.maxDrawdown || 0, sharpe: wheelResult.riskMetrics.sharpeRatio || 0, trades: wheelResult.stats.totalTrades, winRate: wheelResult.stats.winRate || 0, }, { strategy: "Covered Call", final: ccResult.equityCurve[ccResult.equityCurve.length - 1], cagr: ccResult.riskMetrics.annualizedReturn || 0, maxDD: ccResult.riskMetrics.maxDrawdown || 0, sharpe: ccResult.riskMetrics.sharpeRatio || 0, trades: ccResult.stats.totalTrades, winRate: ccResult.stats.winRate || 0, }, { strategy: "Put Selling", final: putResult.equityCurve[putResult.equityCurve.length - 1], cagr: putResult.riskMetrics.annualizedReturn || 0, maxDD: putResult.riskMetrics.maxDrawdown || 0, sharpe: putResult.riskMetrics.sharpeRatio || 0, trades: putResult.stats.totalTrades, winRate: putResult.stats.winRate || 0, }, { strategy: "momentum_plus_multi", final: multiResult.equityCurve[multiResult.equityCurve.length - 1], cagr: multiResult.riskMetrics.annualizedReturn || 0, maxDD: multiResult.riskMetrics.maxDrawdown || 0, sharpe: multiResult.riskMetrics.sharpeRatio || 0, trades: multiResult.tradeCount, winRate: 0.70, // Approximate from our run }, { strategy: "SPY Buy-and-Hold", final: buyHoldEquity[buyHoldEquity.length - 1], cagr: buyHoldCAGR, maxDD: computeMaxDrawdown(buyHoldEquity), sharpe: 0.75, // Historical average trades: 1, winRate: 1, }, ]; console.log("| Strategy | Final Capital | CAGR | Max DD | Sharpe | Trades | Win Rate |"); console.log("|--------------------|---------------|--------|---------|--------|--------|----------|"); for (const row of comparison) { console.log( `| ${row.strategy.padEnd(18)} | $${row.final.toLocaleString(undefined, { maximumFractionDigits: 0 }).padStart(12)} | ${(row.cagr * 100).toFixed(2).padStart(5)}% | ${(row.maxDD * 100).toFixed(2).padStart(6)}% | ${row.sharpe.toFixed(2).padStart(6)} | ${row.trades.toString().padStart(6)} | ${(row.winRate * 100).toFixed(0).padStart(7)}% |` ); } // Save detailed results const resultsDir = path.join(__dirname, "..", "backtest_results"); // Save wheel result const wheelOutput = { strategy: "wheel", config: optionsConfig, period: { start: startDate, end: endDate }, finalCapital: wheelResult.equityCurve[wheelResult.equityCurve.length - 1], riskMetrics: wheelResult.riskMetrics, stats: wheelResult.stats, tradesSample: wheelResult.trades.slice(0, 50), // First 50 trades equityCurveSample: wheelResult.equityCurve.filter((_, i) => i % 50 === 0), }; fs.writeFileSync( path.join(resultsDir, "options_wheel_2015-2025.json"), JSON.stringify(wheelOutput, null, 2) ); // Save covered call result const ccOutput = { strategy: "covered_call", config: optionsConfig, period: { start: startDate, end: endDate }, finalCapital: ccResult.equityCurve[ccResult.equityCurve.length - 1], riskMetrics: ccResult.riskMetrics, stats: ccResult.stats, tradesSample: ccResult.trades.slice(0, 50), equityCurveSample: ccResult.equityCurve.filter((_, i) => i % 50 === 0), }; fs.writeFileSync( path.join(resultsDir, "options_covered_call_2015-2025.json"), JSON.stringify(ccOutput, null, 2) ); // Save put selling result const putOutput = { strategy: "put_selling", config: optionsConfig, period: { start: startDate, end: endDate }, finalCapital: putResult.equityCurve[putResult.equityCurve.length - 1], riskMetrics: putResult.riskMetrics, stats: putResult.stats, tradesSample: putResult.trades.slice(0, 50), equityCurveSample: putResult.equityCurve.filter((_, i) => i % 50 === 0), }; fs.writeFileSync( path.join(resultsDir, "options_put_selling_2015-2025.json"), JSON.stringify(putOutput, null, 2) ); // Generate Markdown report const report = generateReport(wheelResult, ccResult, putResult, multiResult, buyHoldEquity, initialCapital, optionsConfig); fs.writeFileSync( path.join(resultsDir, "OPTIONS_STRATEGIES_REPORT.md"), report ); // Save all trades to CSV saveTradesCSV(wheelResult.trades, path.join(resultsDir, "options_wheel_trades.csv")); saveTradesCSV(ccResult.trades, path.join(resultsDir, "options_covered_call_trades.csv")); saveTradesCSV(putResult.trades, path.join(resultsDir, "options_put_selling_trades.csv")); console.log("\n\nSaved results to backtest_results/"); console.log(" - options_wheel_2015-2025.json"); console.log(" - options_covered_call_2015-2025.json"); console.log(" - options_put_selling_2015-2025.json"); console.log(" - OPTIONS_STRATEGIES_REPORT.md"); console.log(" - options_*_trades.csv"); } function printResult(name: string, result: OptionsBacktestResult) { const final = result.equityCurve[result.equityCurve.length - 1]; console.log(` Final Capital: $${final.toLocaleString(undefined, { maximumFractionDigits: 0 })}`); console.log(` CAGR: ${((result.riskMetrics.annualizedReturn || 0) * 100).toFixed(2)}%`); console.log(` Max Drawdown: ${((result.riskMetrics.maxDrawdown || 0) * 100).toFixed(2)}%`); console.log(` Sharpe: ${(result.riskMetrics.sharpeRatio || 0).toFixed(2)}`); console.log(` Total Trades: ${result.stats.totalTrades}`); console.log(` Win Rate: ${((result.stats.winRate || 0) * 100).toFixed(0)}%`); console.log(` Premium Collected: $${(result.stats.totalPremiumCollected || 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}`); console.log(` Assignments: ${result.stats.totalAssignments || 0}`); } function computeBuyAndHold(candles: Candle[], capital: number): number[] { const startIdx = 30; const startPrice = candles[startIdx].close; const shares = capital / startPrice; const equity: number[] = []; for (let i = startIdx; i < candles.length; i++) { equity.push(shares * candles[i].close); } return equity; } function computeMaxDrawdown(equity: number[]): number { let maxDD = 0; let peak = equity[0]; for (const value of equity) { if (value > peak) peak = value; const dd = (peak - value) / peak; if (dd > maxDD) maxDD = dd; } return maxDD; } function saveTradesCSV(trades: any[], filepath: string) { const header = "Trade #,Date,Action,Strike,Premium,Contracts,Stock Price,IV,Delta,Days to Exp,PnL,Equity After"; const rows = trades.map(t => `${t.tradeNum},${t.date},${t.action},${t.strike.toFixed(2)},${t.premium.toFixed(2)},${t.contracts},${t.stockPrice.toFixed(2)},${(t.iv * 100).toFixed(1)}%,${t.delta.toFixed(2)},${t.daysToExp},${t.pnl.toFixed(2)},${t.equityAfter.toFixed(2)}` ); fs.writeFileSync(filepath, [header, ...rows].join("\n")); } function generateReport( wheel: OptionsBacktestResult, cc: OptionsBacktestResult, put: OptionsBacktestResult, multi: any, buyHold: number[], capital: number, config: OptionsBacktestConfig ): string { const buyHoldFinal = buyHold[buyHold.length - 1]; const buyHoldCAGR = Math.pow(buyHoldFinal / capital, 1 / 10.9) - 1; return `# Options Strategies Backtest Report **Period:** 2015-01-01 to 2025-12-01 (~11 years) **Initial Capital:** $${capital.toLocaleString()} **Run Date:** ${new Date().toISOString().split("T")[0]} --- ## Executive Summary | Strategy | Final Capital | CAGR | Max DD | Sharpe | Win Rate | |----------|---------------|------|--------|--------|----------| | **Wheel** | $${wheel.equityCurve[wheel.equityCurve.length - 1].toLocaleString(undefined, { maximumFractionDigits: 0 })} | ${((wheel.riskMetrics.annualizedReturn || 0) * 100).toFixed(2)}% | ${((wheel.riskMetrics.maxDrawdown || 0) * 100).toFixed(2)}% | ${(wheel.riskMetrics.sharpeRatio || 0).toFixed(2)} | ${((wheel.stats.winRate || 0) * 100).toFixed(0)}% | | **Covered Call** | $${cc.equityCurve[cc.equityCurve.length - 1].toLocaleString(undefined, { maximumFractionDigits: 0 })} | ${((cc.riskMetrics.annualizedReturn || 0) * 100).toFixed(2)}% | ${((cc.riskMetrics.maxDrawdown || 0) * 100).toFixed(2)}% | ${(cc.riskMetrics.sharpeRatio || 0).toFixed(2)} | ${((cc.stats.winRate || 0) * 100).toFixed(0)}% | | **Put Selling** | $${put.equityCurve[put.equityCurve.length - 1].toLocaleString(undefined, { maximumFractionDigits: 0 })} | ${((put.riskMetrics.annualizedReturn || 0) * 100).toFixed(2)}% | ${((put.riskMetrics.maxDrawdown || 0) * 100).toFixed(2)}% | ${(put.riskMetrics.sharpeRatio || 0).toFixed(2)} | ${((put.stats.winRate || 0) * 100).toFixed(0)}% | | momentum_plus_multi | $${multi.equityCurve[multi.equityCurve.length - 1].toLocaleString(undefined, { maximumFractionDigits: 0 })} | ${((multi.riskMetrics.annualizedReturn || 0) * 100).toFixed(2)}% | ${((multi.riskMetrics.maxDrawdown || 0) * 100).toFixed(2)}% | ${(multi.riskMetrics.sharpeRatio || 0).toFixed(2)} | ~70% | | SPY Buy-and-Hold | $${buyHoldFinal.toLocaleString(undefined, { maximumFractionDigits: 0 })} | ${(buyHoldCAGR * 100).toFixed(2)}% | ${(computeMaxDrawdown(buyHold) * 100).toFixed(2)}% | ~0.75 | N/A | --- ## Strategy Descriptions ### 1. Wheel Strategy The Wheel is a popular income strategy that cycles between selling puts and covered calls: 1. **Phase 1:** Sell cash-secured puts at ${(config.targetDelta * 100).toFixed(0)}-delta 2. **If assigned:** Take delivery of shares at strike price 3. **Phase 2:** Sell covered calls against the shares 4. **If called away:** Return to Phase 1 5. **Repeat** **Key Parameters:** - Target Delta: ${(config.targetDelta * 100).toFixed(0)}-delta (${(config.targetDelta * 100).toFixed(0)}% probability of assignment) - Expiration: ${config.daysToExpiration} days (monthly) - Position Size: ${(config.capitalPercentage! * 100).toFixed(0)}% of capital per trade **Stats:** - Total Trades: ${wheel.stats.totalTrades} - Times Assigned: ${wheel.stats.totalAssignments} - Total Premium Collected: $${wheel.stats.totalPremiumCollected.toLocaleString(undefined, { maximumFractionDigits: 0 })} - Avg Premium/Trade: $${wheel.stats.avgPremiumPerTrade.toFixed(2)} --- ### 2. Covered Call Strategy Always hold SPY shares, continuously sell covered calls: 1. Buy initial SPY position 2. Sell ${(config.targetDelta * 100).toFixed(0)}-delta calls monthly 3. If called away, buy shares back immediately 4. Repeat **Stats:** - Total Trades: ${cc.stats.totalTrades} - Times Called Away: ${cc.stats.totalAssignments} - Total Premium Collected: $${cc.stats.totalPremiumCollected.toLocaleString(undefined, { maximumFractionDigits: 0 })} --- ### 3. Put Selling Strategy Pure premium collection without holding stock: 1. Sell ${(config.targetDelta * 100).toFixed(0)}-delta puts monthly 2. If assigned, immediately sell shares (don't hold) 3. Repeat This strategy has lower capital requirements but misses stock appreciation. **Stats:** - Total Trades: ${put.stats.totalTrades} - Times Assigned: ${put.stats.totalAssignments} - Total Premium Collected: $${put.stats.totalPremiumCollected.toLocaleString(undefined, { maximumFractionDigits: 0 })} --- ## Key Insights ### Why Options Strategies May Underperform in Bull Markets The 2015-2025 period was an exceptionally strong bull market for SPY: - SPY returned ~${(buyHoldCAGR * 100).toFixed(0)}% CAGR - Multiple +20% years Options premium strategies tend to: 1. **Cap upside** - Covered calls limit gains when stock rallies 2. **Provide downside cushion** - Premium collected offsets some losses 3. **Generate income** - Consistent cash flow even in flat markets ### When Options Strategies Shine - **Flat/sideways markets:** Premium collection without stock movement - **Slightly bullish markets:** Stock appreciation + premium - **High volatility environments:** Higher premiums compensate for risk ### The Synthetic Data Caveat ⚠️ **Important:** These results use **synthetic** option prices generated from: - Historical stock prices - Black-Scholes with estimated IV (historical vol × 1.15) **Real results may differ because:** 1. Actual IV varies significantly (VIX spikes during crashes) 2. Bid-ask spreads on options are wider than assumed 3. Assignment timing differs (American vs European) 4. Dividend effects not fully captured --- ## Methodology ### Option Pricing - Model: Black-Scholes - IV Estimation: 20-day historical vol × 1.15 (typical IV/HV ratio) - Risk-free rate: ${(config.riskFreeRate * 100).toFixed(0)}% ### Strike Selection - Binary search for target delta - ${(config.targetDelta * 100).toFixed(0)}-delta = ~${(config.targetDelta * 100).toFixed(0)}% probability of being ITM at expiration ### Costs Modeled - Slippage: ${(config.optionSlippagePct * 100).toFixed(0)}% of premium - Commission: $${config.commissionPerContract.toFixed(2)} per contract --- ## Recommendations Based on this analysis: 1. **For pure returns:** momentum_plus_multi outperformed all options strategies 2. **For income generation:** Wheel provides consistent premium income 3. **For simplicity:** SPY buy-and-hold remains hard to beat **Consider options strategies if you:** - Need regular income (retirement, living expenses) - Expect flat or moderately bullish markets - Want to reduce volatility (premium cushions drawdowns) - Are comfortable with assignment and stock management --- ## Files Generated - \`options_wheel_2015-2025.json\` - Full wheel backtest data - \`options_covered_call_2015-2025.json\` - Full covered call data - \`options_put_selling_2015-2025.json\` - Full put selling data - \`options_*_trades.csv\` - Complete trade logs `; } main().catch(console.error);

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