Global Macro Hedge Fund Backtester
Define Your Global Macro Strategy
Asset Universe (Illustrative)
Trading Signal (Illustrative)
Portfolio Allocation
Set Backtest Parameters
Backtest Results & Analysis
Click "Run Backtest" to see results. Ensure strategy and parameters are set.
Error: Start date must be before end date.
'; return; } // Filter data for the selected date range const dateIndexStart = sampleMarketData.SP500.findIndex(d => d.date === startDateStr); const dateIndexEnd = sampleMarketData.SP500.findIndex(d => d.date === endDateStr); if (dateIndexStart === -1 || dateIndexEnd === -1 || dateIndexStart >= dateIndexEnd) { resultsOutputDiv.innerHTML = 'Error: Invalid date range selected for available sample data.
'; return; } let portfolioValue = initialCapital; const portfolioHistory = [{ date: startDateStr, value: initialCapital }]; const tradeLog = []; let currentPositions = {}; // { asset: { units: X, entryPrice: Y } } // Loop through periods (monthly in this sample) for (let i = dateIndexStart; i < dateIndexEnd; i++) { const currentDate = sampleMarketData.SP500[i].date; const nextDate = sampleMarketData.SP500[i+1].date; // Date for price at end of period const sentimentDataPoint = sampleMarketData.SENTIMENT.find(s => s.date === currentDate); const currentSentiment = sentimentDataPoint ? sentimentDataPoint.value : 'Neutral'; let targetAssetsLong = []; let targetAssetsShort = []; // Determine target positions based on signal if (signalType === 'sentiment_follower') { if (currentSentiment === 'Positive') targetAssetsLong = selectedAssets; else if (currentSentiment === 'Negative') targetAssetsShort = selectedAssets; } else if (signalType === 'sentiment_contrarian') { if (currentSentiment === 'Positive') targetAssetsShort = selectedAssets; else if (currentSentiment === 'Negative') targetAssetsLong = selectedAssets; } // If Neutral, or no assets targeted, effectively flat or hold existing. For simplicity, we'll go flat on non-targeted. const capitalToAllocate = portfolioValue * leverage; const allocationPerAsset = targetAssetsLong.length + targetAssetsShort.length > 0 ? capitalToAllocate / (targetAssetsLong.length + targetAssetsShort.length) : 0; // Simulate trades (simplified rebalancing at start of each period) // 1. Close existing positions that are no longer targeted or need direction change for (const asset in currentPositions) { const assetDataCurrent = sampleMarketData[asset]; if (!assetDataCurrent) continue; const currentPriceData = assetDataCurrent.find(d => d.date === currentDate); if (!currentPriceData) continue; const currentPrice = currentPriceData.price; let closePosition = false; if (currentPositions[asset].units > 0 && !targetAssetsLong.includes(asset)) closePosition = true; // Was long, no longer target long if (currentPositions[asset].units < 0 && !targetAssetsShort.includes(asset)) closePosition = true; // Was short, no longer target short if (closePosition) { const pnl = currentPositions[asset].units * (currentPrice - currentPositions[asset].entryPrice); const cost = Math.abs(currentPositions[asset].units * currentPrice * transactionCostPct); portfolioValue += pnl - cost; tradeLog.push({date: currentDate, asset: asset, action: 'Close', units: currentPositions[asset].units, price: currentPrice, pnl: pnl - cost}); delete currentPositions[asset]; } } // 2. Open new/adjust positions const allTargetAssets = [...targetAssetsLong, ...targetAssetsShort]; allTargetAssets.forEach(asset => { const assetDataCurrent = sampleMarketData[asset]; if (!assetDataCurrent) return; const currentPriceData = assetDataCurrent.find(d => d.date === currentDate); if (!currentPriceData) return; const currentPrice = currentPriceData.price; const targetUnits = targetAssetsLong.includes(asset) ? (allocationPerAsset / currentPrice) : targetAssetsShort.includes(asset) ? -(allocationPerAsset / currentPrice) : 0; if (targetUnits !== (currentPositions[asset] ? currentPositions[asset].units : 0) && targetUnits !== 0) { // If existing position, close it first (simplified for this example) if (currentPositions[asset]) { const pnlExisting = currentPositions[asset].units * (currentPrice - currentPositions[asset].entryPrice); const costExisting = Math.abs(currentPositions[asset].units * currentPrice * transactionCostPct); portfolioValue += pnlExisting - costExisting; tradeLog.push({date: currentDate, asset: asset, action: 'Close (Rebalance)', units: currentPositions[asset].units, price: currentPrice, pnl: pnlExisting - costExisting}); } currentPositions[asset] = { units: targetUnits, entryPrice: currentPrice }; const costNew = Math.abs(targetUnits * currentPrice * transactionCostPct); portfolioValue -= costNew; // Deduct cost for new position tradeLog.push({date: currentDate, asset: asset, action: targetUnits > 0 ? 'Buy' : 'Sell Short', units: targetUnits, price: currentPrice, pnl: -costNew}); } else if (targetUnits === 0 && currentPositions[asset]) { // Asset no longer targeted, close position const pnl = currentPositions[asset].units * (currentPrice - currentPositions[asset].entryPrice); const cost = Math.abs(currentPositions[asset].units * currentPrice * transactionCostPct); portfolioValue += pnl - cost; tradeLog.push({date: currentDate, asset: asset, action: 'Close', units: currentPositions[asset].units, price: currentPrice, pnl: pnl - cost}); delete currentPositions[asset]; } }); // Calculate portfolio value at end of period (before next rebalancing) let endOfPeriodPortfolioValue = portfolioValue; // Start with cash value after trades for (const asset in currentPositions) { const assetDataNext = sampleMarketData[asset]; if (!assetDataNext) continue; const nextPriceData = assetDataNext.find(d => d.date === nextDate); if (!nextPriceData) continue; // Should not happen if data is aligned const nextPrice = nextPriceData.price; // Add P&L from open positions for this period // This is a simplification; in reality, cash changes from trades affect capital for other trades. // Here, we calculate change from entry price to period-end price. // The portfolioValue already reflects cash after costs. We need to add unrealized P&L. // Initial portfolioValue reflects cash. We add value of positions. // This part is tricky. Let's track cash and asset values separately. // For simplicity in this example: value change of open positions during the period. // This is a mark-to-market for the period. // The portfolioValue was adjusted for costs. Now add value of holdings. // This calculation is simplified. A full backtester has more complex cash flow. // Let's assume portfolioValue is the "cash" component after trades. // Then, at end of period, we add the value of current holdings. // No, portfolioValue should be total equity. // When a trade is made, cash decreases by cost, and asset value is acquired. // The P&L calculation at trade closure is correct. // For value at end of period: // Current cash + sum (current_units_of_asset_X * current_price_of_asset_X) // This is complex to track perfectly without a full ledger. // Simplified: The change in value of open positions from their entry in *this* period. // The portfolioValue is updated with realized P&L. // What's missing is the unrealized P&L for the current period for positions held through. // Let's adjust the main `portfolioValue` based on the change in prices of held assets. // This is still simplified. const priceAtPeriodStart = currentPositions[asset].entryPrice; // Price at which position was taken in this period endOfPeriodPortfolioValue += currentPositions[asset].units * (nextPrice - priceAtPeriodStart); } portfolioValue = endOfPeriodPortfolioValue; // Update portfolio value for next iteration portfolioHistory.push({ date: nextDate, value: portfolioValue }); } // Final Performance Metrics const finalPortfolioValue = portfolioValue; const totalReturn = (finalPortfolioValue / initialCapital - 1) * 100; const numPeriods = portfolioHistory.length -1; const years = numPeriods / 12; // Assuming monthly periods const annualizedReturn = years > 0 ? (Math.pow(finalPortfolioValue / initialCapital, 1 / years) - 1) * 100 : totalReturn; const returns = []; for (let k = 1; k < portfolioHistory.length; k++) { returns.push(portfolioHistory[k].value / portfolioHistory[k-1].value - 1); } const avgReturn = returns.reduce((sum, r) => sum + r, 0) / returns.length; const volatility = Math.sqrt(returns.reduce((sumSq, r) => sumSq + Math.pow(r - avgReturn, 2), 0) / (returns.length -1 || 1)) * Math.sqrt(12); // Annualized const riskFreeRateMonthly = Math.pow(1 + riskFreeRateAnnualPct, 1/12) - 1; const excessReturns = returns.map(r => r - riskFreeRateMonthly); const avgExcessReturn = excessReturns.reduce((sum, r) => sum + r, 0) / excessReturns.length; // Annualize avgExcessReturn for Sharpe const annualizedAvgExcessReturn = (Math.pow(1 + avgExcessReturn, 12) -1) *100; const sharpeRatio = volatility > 0 ? (annualizedAvgExcessReturn / 100) / volatility : 0; // Sharpe ratio calculation needs annualized excess return and annualized volatility. // Annualized excess return = annualized portfolio return - annualized risk-free rate const annualizedPortfolioReturnForSharpe = (Math.pow(finalPortfolioValue / initialCapital, 1 / years) - 1); const sharpeNumerator = annualizedPortfolioReturnForSharpe - riskFreeRateAnnualPct; const calculatedSharpeRatio = volatility > 0 ? sharpeNumerator / volatility : 0; let peak = initialCapital; let maxDrawdown = 0; portfolioHistory.forEach(h => { if (h.value > peak) peak = h.value; const drawdown = (peak - h.value) / peak; if (drawdown > maxDrawdown) maxDrawdown = drawdown; }); backtestResultsData = { strategyName, initialCapital, finalPortfolioValue, leverage, startDateStr, endDateStr, transactionCostPct, riskFreeRateAnnualPct, totalReturn, annualizedReturn, annualizedVolatility: volatility * 100, sharpeRatio: calculatedSharpeRatio, maxDrawdown: maxDrawdown * 100, portfolioHistory, tradeLog, selectedAssets, signalType }; displayResults(backtestResultsData); if(pdfButtonContainer) pdfButtonContainer.style.display = 'block'; // Show PDF button } function displayResults(results) { if (!resultsOutputDiv || !results) return; let html = `Results for: ${results.strategyName}
`; html += `Period: ${results.startDateStr} to ${results.endDateStr}
`; html += `Initial Capital: $${results.initialCapital.toLocaleString()}
`; html += `Final Portfolio Value: $${results.finalPortfolioValue.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
`; html += `Total Return: ${results.totalReturn.toFixed(2)}%
`; html += `Annualized Return: ${results.annualizedReturn.toFixed(2)}%
`; html += `Annualized Volatility: ${results.annualizedVolatility.toFixed(2)}%
`; html += `Sharpe Ratio (Rf=${(results.riskFreeRateAnnualPct*100).toFixed(1)}%): ${results.sharpeRatio.toFixed(2)}
`; html += `Maximum Drawdown: ${results.maxDrawdown.toFixed(2)}%
`; html += `Portfolio Value Over Time:
`; html += `| Date | Portfolio Value ($) |
|---|---|
| ${h.date} | ${h.value.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})} |
Trade Log (Simplified):
`; html += `| Date | Asset | Action | Units | Price ($) | P&L ($) |
|---|---|---|---|---|---|
| ${t.date} | ${t.asset} | ${t.action} | ${t.units.toFixed(4)} | ${t.price.toFixed(2)} | ${t.pnl.toFixed(2)} |
No trades executed.
`; } resultsOutputDiv.innerHTML = html; } if (runBacktestBtn) { runBacktestBtn.addEventListener('click', runBacktest); } // --- PDF Download --- function loadJsPdfIfNeeded(callback) { if (jsPdfLoaded) { if (callback) callback(); return; } const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'; script.onload = () => { jsPdfLoaded = true; console.log("jsPDF loaded dynamically."); if (callback) callback(); }; script.onerror = () => { console.error("Failed to load jsPDF. PDF functionality unavailable."); alert("Error: Could not load PDF library."); }; document.head.appendChild(script); } function downloadResultsAsPdf() { if (!jsPdfLoaded) { alert("PDF library not loaded. Please wait or ensure it's included."); return; } if (!backtestResultsData) { alert("No backtest results to download. Please run a backtest first."); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF({ unit: 'pt', format: 'a4' }); const r = backtestResultsData; // alias for results const pageMargin = 40; const pageWidth = doc.internal.pageSize.getWidth() - 2 * pageMargin; let y = pageMargin; function addTitle(text) { doc.setFontSize(18); doc.setFont(undefined, 'bold'); doc.setTextColor(26, 35, 126); // Primary color doc.text(text, pageMargin, y); y += 30; } function addSectionTitle(text) { if (y > doc.internal.pageSize.getHeight() - 80) { doc.addPage(); y = pageMargin; } doc.setFontSize(14); doc.setFont(undefined, 'bold'); doc.setTextColor(57, 73, 171); // Secondary color doc.text(text, pageMargin, y); y += 20; } function addLine(key, value) { if (y > doc.internal.pageSize.getHeight() - 40) { doc.addPage(); y = pageMargin; } doc.setFontSize(10); doc.setFont(undefined, 'bold'); doc.setTextColor(33,33,33); doc.text(key, pageMargin, y); doc.setFont(undefined, 'normal'); const valueText = String(value); // Ensure value is a string doc.text(valueText, pageMargin + 150, y); // Adjust x for value y += 15; } function addTable(headers, data, columnWidths) { if (y > doc.internal.pageSize.getHeight() - 100) { doc.addPage(); y = pageMargin; } doc.setFontSize(9); doc.setFillColor(57, 73, 171); doc.setTextColor(255); doc.setFont(undefined, 'bold'); let currentX = pageMargin; headers.forEach((header, i) => { doc.rect(currentX, y, columnWidths[i], 20, 'F'); doc.text(header, currentX + 5, y + 14); currentX += columnWidths[i]; }); y += 20; doc.setTextColor(33,33,33); doc.setFont(undefined, 'normal'); data.forEach(row => { if (y > doc.internal.pageSize.getHeight() - 40) { doc.addPage(); y = pageMargin; } currentX = pageMargin; row.forEach((cell, i) => { doc.rect(currentX, y, columnWidths[i], 18); const cellText = String(cell); // Ensure cell is a string const textLines = doc.splitTextToSize(cellText, columnWidths[i] - 10); doc.text(textLines, currentX + 5, y + 12); currentX += columnWidths[i]; }); y += 18; }); y += 10; } addTitle(`Backtest Report: ${r.strategyName}`); addSectionTitle("Strategy & Parameters"); addLine("Selected Assets:", r.selectedAssets.join(', ')); addLine("Signal Logic:", r.signalType.replace('_', ' ')); addLine("Initial Capital:", `$${r.initialCapital.toLocaleString()}`); addLine("Leverage:", `${r.leverage}x`); addLine("Backtest Period:", `${r.startDateStr} to ${r.endDateStr}`); addLine("Transaction Cost:", `${(r.transactionCostPct * 100).toFixed(2)}% per trade`); addLine("Annual Risk-Free Rate:", `${(r.riskFreeRateAnnualPct * 100).toFixed(1)}%`); addSectionTitle("Key Performance Metrics"); addLine("Final Portfolio Value:", `$${r.finalPortfolioValue.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}`); addLine("Total Return:", `${r.totalReturn.toFixed(2)}%`); addLine("Annualized Return:", `${r.annualizedReturn.toFixed(2)}%`); addLine("Annualized Volatility:", `${r.annualizedVolatility.toFixed(2)}%`); addLine("Sharpe Ratio:", `${r.sharpeRatio.toFixed(2)}`); addLine("Maximum Drawdown:", `${r.maxDrawdown.toFixed(2)}%`); addSectionTitle("Portfolio Value Over Time"); const portfolioHeaders = ["Date", "Portfolio Value ($)"]; const portfolioTableData = r.portfolioHistory.map(h => [h.date, h.value.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})]); addTable(portfolioHeaders, portfolioTableData, [100, 150]); if (r.tradeLog.length > 0) { addSectionTitle("Trade Log (Simplified)"); const tradeHeaders = ["Date", "Asset", "Action", "Units", "Price ($)", "P&L ($)"]; const tradeTableData = r.tradeLog.map(t => [t.date, t.asset, t.action, t.units.toFixed(4), t.price.toFixed(2), t.pnl.toFixed(2)]); addTable(tradeHeaders, tradeTableData, [70, 70, 70, 70, 70, 70]); } else { addLine("Trade Log:", "No trades executed."); } // Footer const pageCount = doc.internal.getNumberOfPages(); for (let i = 1; i <= pageCount; i++) { doc.setPage(i); doc.setFontSize(8); doc.setTextColor(150); doc.text(`Page ${i} of ${pageCount} - Global Macro Backtest Report`, pageMargin, doc.internal.pageSize.getHeight() - 20); doc.text(new Date().toLocaleString(), doc.internal.pageSize.getWidth() - pageMargin - doc.getTextWidth(new Date().toLocaleString()), doc.internal.pageSize.getHeight() - 20); } doc.save(`Backtest_Report_${r.strategyName.replace(/\s+/g, '_')}.pdf`); } if (downloadPdfBtn) { downloadPdfBtn.addEventListener('click', () => loadJsPdfIfNeeded(downloadResultsAsPdf)); } // --- Initialization --- populateDateSelects(); showTab(0); loadJsPdfIfNeeded(); // Attempt to load jsPDF early });