Spaces:
Running
Running
Download index.html from At1uri/alphabacktest-studio: direct link, hf CLI and curl.
- Browser
- Download file 40 kB
-
https://huggingface.co/spaces/At1uri/alphabacktest-studio/resolve/main/index.html
- Command line
-
hf download hf://spaces/At1uri/alphabacktest-studio/index.html
-
curl -L -o index.html https://huggingface.co/spaces/At1uri/alphabacktest-studio/resolve/main/index.html
40 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AlphaBacktest Studio | Ramprasad Atluri</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.plot.ly/plotly-2.32.0.min.js"></script> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> | |
| <style> | |
| body { font-family: 'Inter', sans-serif; } | |
| code, pre, .font-mono { font-family: 'JetBrains Mono', monospace; } | |
| ::-webkit-scrollbar { width: 6px; height: 6px; } | |
| ::-webkit-scrollbar-track { background: #0f172a; } | |
| ::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #475569; } | |
| </style> | |
| </head> | |
| <body class="bg-slate-950 text-slate-100 min-h-screen flex flex-col"> | |
| <!-- Top Header Navigation --> | |
| <header class="border-b border-slate-800 bg-slate-900/80 backdrop-blur sticky top-0 z-50"> | |
| <div class="max-w-7xl mx-auto px-4 py-3 flex flex-wrap items-center justify-between gap-4"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-emerald-500 to-cyan-400 flex items-center justify-center shadow-lg shadow-emerald-500/20 text-white font-bold text-xl"> | |
| ⚡ | |
| </div> | |
| <div> | |
| <h1 class="text-lg font-bold tracking-tight text-white flex items-center gap-2"> | |
| AlphaBacktest Studio | |
| <span class="text-xs px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-medium">v1.0</span> | |
| </h1> | |
| <p class="text-xs text-slate-400">Quantitative Algorithmic Strategy Backtesting & Risk Analytics</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-3"> | |
| <a href="https://huggingface.co/spaces/At1uri/quantitative-time-series-studio" target="_blank" class="px-3 py-1.5 rounded-lg bg-slate-800/80 hover:bg-slate-700 border border-slate-700 text-xs font-medium text-slate-300 transition"> | |
| 📈 Time-Series Studio | |
| </a> | |
| <a href="https://huggingface.co/At1uri" target="_blank" class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/30 text-xs font-medium text-emerald-400 transition"> | |
| <span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span> | |
| Author: @At1uri | |
| </a> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Container --> | |
| <main class="max-w-7xl mx-auto px-4 py-6 flex-1 w-full grid grid-cols-1 lg:grid-cols-12 gap-6"> | |
| <!-- Left Strategy & Parameter Config Panel (4 Columns) --> | |
| <div class="lg:col-span-4 bg-slate-900/70 border border-slate-800 rounded-2xl p-5 shadow-xl flex flex-col space-y-5"> | |
| <div> | |
| <h2 class="text-xs font-semibold uppercase tracking-wider text-emerald-400 mb-1">Strategy & Parameters</h2> | |
| <p class="text-xs text-slate-400">Configure strategy rules, market regimes, and execution costs</p> | |
| </div> | |
| <!-- Strategy Selection --> | |
| <div class="space-y-1.5"> | |
| <label class="text-xs font-medium text-slate-300">Algorithmic Strategy</label> | |
| <select id="strategyType" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-200 focus:outline-none focus:ring-2 focus:ring-emerald-500"> | |
| <option value="ma_cross">Dual Moving Average Crossover (Trend)</option> | |
| <option value="rsi">RSI Dynamic Mean-Reversion (Oscillator)</option> | |
| <option value="bollinger">Bollinger Bands Volatility Breakout</option> | |
| <option value="macd">MACD Signal Line Crossover (Momentum)</option> | |
| </select> | |
| </div> | |
| <!-- Dynamic Strategy Parameters --> | |
| <div id="strategyParams" class="bg-slate-950/80 border border-slate-800/90 rounded-xl p-3.5 space-y-3"> | |
| <!-- Rendered dynamically via JS based on selected strategy --> | |
| </div> | |
| <!-- Market Regime & Data Selection --> | |
| <div class="space-y-1.5"> | |
| <label class="text-xs font-medium text-slate-300">Market Regime / Asset Series</label> | |
| <select id="marketRegime" class="w-full bg-slate-950 border border-slate-700 rounded-xl px-3 py-2 text-xs text-slate-200 focus:outline-none focus:ring-2 focus:ring-emerald-500"> | |
| <option value="bull">Tech Growth Equity (Sustained Bull with Pullbacks)</option> | |
| <option value="sideways">Mean-Reverting Range (Choppy / Low Trend)</option> | |
| <option value="bear">Cyclical Bear Market (High Volatility Drawdowns)</option> | |
| <option value="crypto">Crypto High-Beta Cycle (Extreme Volatility)</option> | |
| </select> | |
| </div> | |
| <!-- Time Horizon & Capital Grid --> | |
| <div class="grid grid-cols-2 gap-3"> | |
| <div class="space-y-1"> | |
| <label class="text-xs font-medium text-slate-400">Initial Capital ($)</label> | |
| <input type="number" id="initialCapital" value="100000" step="5000" class="w-full bg-slate-950 border border-slate-700 rounded-lg px-2.5 py-1.5 text-xs text-slate-200 focus:ring-1 focus:ring-emerald-500"> | |
| </div> | |
| <div class="space-y-1"> | |
| <label class="text-xs font-medium text-slate-400">History (Days)</label> | |
| <input type="number" id="historyDays" value="365" min="120" max="1000" step="30" class="w-full bg-slate-950 border border-slate-700 rounded-lg px-2.5 py-1.5 text-xs text-slate-200 focus:ring-1 focus:ring-emerald-500"> | |
| </div> | |
| <div class="space-y-1"> | |
| <label class="text-xs font-medium text-slate-400">Slippage + Fee (bps)</label> | |
| <input type="number" id="feeBps" value="5" min="0" max="50" step="1" class="w-full bg-slate-950 border border-slate-700 rounded-lg px-2.5 py-1.5 text-xs text-slate-200 focus:ring-1 focus:ring-emerald-500" title="5 basis points = 0.05% per trade"> | |
| </div> | |
| <div class="space-y-1"> | |
| <label class="text-xs font-medium text-slate-400">Stop-Loss (%)</label> | |
| <input type="number" id="stopLoss" value="5.0" min="0" max="25" step="0.5" class="w-full bg-slate-950 border border-slate-700 rounded-lg px-2.5 py-1.5 text-xs text-slate-200 focus:ring-1 focus:ring-emerald-500" title="0 to disable stop-loss"> | |
| </div> | |
| </div> | |
| <!-- Execute Button --> | |
| <button id="runBacktestBtn" class="w-full py-2.5 px-4 rounded-xl bg-gradient-to-r from-emerald-600 hover:from-emerald-500 to-cyan-500 hover:to-cyan-400 text-white font-semibold text-xs uppercase tracking-wider shadow-lg shadow-emerald-500/20 transition active:scale-[0.99] flex items-center justify-center gap-2"> | |
| <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg> | |
| Execute Algorithmic Backtest | |
| </button> | |
| <!-- Strategy Logic Summary Card --> | |
| <div class="mt-auto p-3.5 rounded-xl bg-slate-950/80 border border-slate-800 text-[11px] text-slate-400 space-y-1.5"> | |
| <p class="font-semibold text-slate-300 flex items-center gap-1.5"> | |
| <span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span> | |
| Strategy Description | |
| </p> | |
| <p id="strategyDesc" class="text-slate-400 leading-relaxed text-[11px]"></p> | |
| </div> | |
| </div> | |
| <!-- Right Analytics & Visualizations Panel (8 Columns) --> | |
| <div class="lg:col-span-8 flex flex-col space-y-5"> | |
| <!-- Institutional Performance KPI Cards --> | |
| <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3"> | |
| <div class="bg-slate-900/70 border border-slate-800 rounded-xl p-3 shadow-md"> | |
| <p class="text-[10px] text-slate-400 uppercase tracking-wider">Strategy Return</p> | |
| <p id="kpiStratRet" class="text-base font-bold font-mono text-emerald-400 mt-0.5">+0.0%</p> | |
| <span id="kpiFinalCapital" class="text-[10px] text-slate-500 font-mono">$100,000</span> | |
| </div> | |
| <div class="bg-slate-900/70 border border-slate-800 rounded-xl p-3 shadow-md"> | |
| <p class="text-[10px] text-slate-400 uppercase tracking-wider">Benchmark Return</p> | |
| <p id="kpiBenchRet" class="text-base font-bold font-mono text-slate-300 mt-0.5">+0.0%</p> | |
| <span class="text-[10px] text-slate-500 font-mono">Buy & Hold</span> | |
| </div> | |
| <div class="bg-slate-900/70 border border-slate-800 rounded-xl p-3 shadow-md"> | |
| <p class="text-[10px] text-slate-400 uppercase tracking-wider">Sharpe Ratio</p> | |
| <p id="kpiSharpe" class="text-base font-bold font-mono text-cyan-400 mt-0.5">0.00</p> | |
| <span id="kpiSortino" class="text-[10px] text-slate-400 font-mono">Sortino: 0.00</span> | |
| </div> | |
| <div class="bg-slate-900/70 border border-slate-800 rounded-xl p-3 shadow-md"> | |
| <p class="text-[10px] text-slate-400 uppercase tracking-wider">Max Drawdown</p> | |
| <p id="kpiMDD" class="text-base font-bold font-mono text-rose-400 mt-0.5">0.0%</p> | |
| <span id="kpiBenchMDD" class="text-[10px] text-slate-500 font-mono">Bench: 0.0%</span> | |
| </div> | |
| <div class="bg-slate-900/70 border border-slate-800 rounded-xl p-3 shadow-md"> | |
| <p class="text-[10px] text-slate-400 uppercase tracking-wider">Win Rate</p> | |
| <p id="kpiWinRate" class="text-base font-bold font-mono text-amber-400 mt-0.5">0.0%</p> | |
| <span id="kpiTradesCount" class="text-[10px] text-slate-500 font-mono">0 trades</span> | |
| </div> | |
| <div class="bg-slate-900/70 border border-slate-800 rounded-xl p-3 shadow-md"> | |
| <p class="text-[10px] text-slate-400 uppercase tracking-wider">Profit Factor</p> | |
| <p id="kpiProfitFactor" class="text-base font-bold font-mono text-indigo-400 mt-0.5">0.00</p> | |
| <span id="kpiAlpha" class="text-[10px] text-emerald-400 font-mono">Alpha: +0.0%</span> | |
| </div> | |
| </div> | |
| <!-- Main Visualizer Tabs --> | |
| <div class="bg-slate-900/70 border border-slate-800 rounded-2xl p-4 shadow-xl flex flex-col space-y-4"> | |
| <div class="flex items-center justify-between border-b border-slate-800 pb-3 px-1"> | |
| <div class="flex items-center space-x-2"> | |
| <button id="tabBtnEquity" class="px-3.5 py-1.5 rounded-lg bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 text-xs font-semibold transition"> | |
| 📈 Cumulative Equity & Drawdown | |
| </button> | |
| <button id="tabBtnPrice" class="px-3.5 py-1.5 rounded-lg bg-slate-800/80 text-slate-400 hover:text-slate-200 text-xs font-semibold transition"> | |
| 🎯 Asset Price & Trade Signals | |
| </button> | |
| </div> | |
| <div class="text-[11px] text-slate-500 font-mono hidden sm:block"> | |
| Slippage: <span id="displayBps">5</span> bps included | |
| </div> | |
| </div> | |
| <!-- Charts Container --> | |
| <div id="chartEquityContainer" class="w-full"> | |
| <div id="chartEquity" class="w-full h-[400px]"></div> | |
| </div> | |
| <div id="chartPriceContainer" class="w-full hidden"> | |
| <div id="chartPrice" class="w-full h-[400px]"></div> | |
| </div> | |
| </div> | |
| <!-- Trade Log Details Table --> | |
| <div class="bg-slate-900/70 border border-slate-800 rounded-2xl p-4 shadow-xl"> | |
| <div class="flex items-center justify-between mb-3 px-1"> | |
| <div> | |
| <h3 class="text-xs font-semibold uppercase tracking-wider text-slate-300">Trade Execution Ledger</h3> | |
| <p class="text-[11px] text-slate-500">Chronological transaction logs with individual PnL, exit reasons, and durations</p> | |
| </div> | |
| <span id="ledgerBadge" class="text-[11px] font-mono px-2 py-0.5 rounded bg-slate-800 text-slate-400 border border-slate-700">0 Trades</span> | |
| </div> | |
| <div class="overflow-x-auto max-h-[220px] rounded-xl border border-slate-800"> | |
| <table class="w-full text-left text-xs font-mono"> | |
| <thead class="bg-slate-950 text-slate-400 uppercase text-[10px] sticky top-0 border-b border-slate-800"> | |
| <tr> | |
| <th class="py-2 px-3">#</th> | |
| <th class="py-2 px-3">Entry Date</th> | |
| <th class="py-2 px-3">Exit Date</th> | |
| <th class="py-2 px-3">Type</th> | |
| <th class="py-2 px-3">Entry $</th> | |
| <th class="py-2 px-3">Exit $</th> | |
| <th class="py-2 px-3">Return %</th> | |
| <th class="py-2 px-3">Net PnL ($)</th> | |
| <th class="py-2 px-3">Duration</th> | |
| <th class="py-2 px-3">Exit Trigger</th> | |
| </tr> | |
| </thead> | |
| <tbody id="tradeLogBody" class="divide-y divide-slate-800/60 bg-slate-900/40 text-[11px]"> | |
| <!-- Populated via JS --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="border-t border-slate-800/80 bg-slate-900/40 py-4 mt-auto text-center text-xs text-slate-500"> | |
| <p>Developed by <a href="https://huggingface.co/At1uri" target="_blank" class="text-emerald-400 hover:underline font-medium">Ramprasad Atluri</a> • Part of the Twice-Weekly Product Release Sprint • MIT License</p> | |
| </footer> | |
| <script> | |
| // --- Data Generation Engine --- | |
| function generateMarketData(regime, nDays) { | |
| const dates = []; | |
| const prices = []; | |
| const today = new Date(); | |
| today.setDate(today.getDate() - nDays); | |
| let p = 100.0; | |
| let mu = 0.10, sigma = 0.20; | |
| if (regime === 'bull') { | |
| mu = 0.22; sigma = 0.18; | |
| } else if (regime === 'sideways') { | |
| mu = 0.02; sigma = 0.16; | |
| } else if (regime === 'bear') { | |
| mu = -0.15; sigma = 0.32; | |
| } else if (regime === 'crypto') { | |
| mu = 0.35; sigma = 0.65; | |
| } | |
| const dt = 1.0 / 252.0; | |
| // Seedable pseudo-random | |
| let seed = 42; | |
| function rnd() { | |
| seed = (seed * 16807) % 2147483647; | |
| const u1 = (seed - 1) / 2147483646; | |
| seed = (seed * 16807) % 2147483647; | |
| const u2 = (seed - 1) / 2147483646; | |
| return Math.sqrt(-2.0 * Math.log(u1 + 1e-10)) * Math.cos(2.0 * Math.PI * u2); | |
| } | |
| for (let i = 0; i < nDays; i++) { | |
| const d = new Date(today); | |
| d.setDate(d.getDate() + i); | |
| dates.push(d.toISOString().slice(0, 10)); | |
| if (i === 0) { | |
| prices.push(p); | |
| } else { | |
| const z = rnd(); | |
| let shock = (mu - 0.5 * sigma * sigma) * dt + sigma * Math.sqrt(dt) * z; | |
| if (regime === 'sideways') { | |
| const meanLvl = 100.0; | |
| const reversion = 0.15 * (meanLvl - p) * dt; | |
| p = Math.max(10.0, p + reversion + p * sigma * Math.sqrt(dt) * z); | |
| } else { | |
| p = Math.max(5.0, p * Math.exp(shock)); | |
| } | |
| prices.push(p); | |
| } | |
| } | |
| return { dates, prices }; | |
| } | |
| // --- Mathematical Indicators --- | |
| function calcEMA(data, period) { | |
| const k = 2 / (period + 1); | |
| const ema = [data[0]]; | |
| for (let i = 1; i < data.length; i++) { | |
| ema.push(data[i] * k + ema[i - 1] * (1 - k)); | |
| } | |
| return ema; | |
| } | |
| function calcSMA(data, period) { | |
| const sma = []; | |
| for (let i = 0; i < data.length; i++) { | |
| if (i < period - 1) { | |
| sma.push(null); | |
| } else { | |
| let sum = 0; | |
| for (let j = 0; j < period; j++) sum += data[i - j]; | |
| sma.push(sum / period); | |
| } | |
| } | |
| return sma; | |
| } | |
| function calcRSI(data, period = 14) { | |
| const rsi = new Array(data.length).fill(null); | |
| let gains = 0, losses = 0; | |
| for (let i = 1; i <= period; i++) { | |
| const diff = data[i] - data[i - 1]; | |
| if (diff >= 0) gains += diff; | |
| else losses -= diff; | |
| } | |
| let avgGain = gains / period; | |
| let avgLoss = losses / period; | |
| rsi[period] = 100 - (100 / (1 + (avgGain / (avgLoss || 1e-6)))); | |
| for (let i = period + 1; i < data.length; i++) { | |
| const diff = data[i] - data[i - 1]; | |
| const gain = diff >= 0 ? diff : 0; | |
| const loss = diff < 0 ? -diff : 0; | |
| avgGain = (avgGain * (period - 1) + gain) / period; | |
| avgLoss = (avgLoss * (period - 1) + loss) / period; | |
| const rs = avgGain / (avgLoss || 1e-6); | |
| rsi[i] = 100 - (100 / (1 + rs)); | |
| } | |
| return rsi; | |
| } | |
| function calcBollingerBands(data, period = 20, numStd = 2.0) { | |
| const sma = calcSMA(data, period); | |
| const upper = []; | |
| const lower = []; | |
| for (let i = 0; i < data.length; i++) { | |
| if (sma[i] === null) { | |
| upper.push(null); | |
| lower.push(null); | |
| } else { | |
| let sumSq = 0; | |
| for (let j = 0; j < period; j++) { | |
| sumSq += Math.pow(data[i - j] - sma[i], 2); | |
| } | |
| const std = Math.sqrt(sumSq / period); | |
| upper.push(sma[i] + numStd * std); | |
| lower.push(sma[i] - numStd * std); | |
| } | |
| } | |
| return { sma, upper, lower }; | |
| } | |
| // --- Dynamic Parameters UI Manager --- | |
| function renderStrategyParams() { | |
| const type = document.getElementById('strategyType').value; | |
| const container = document.getElementById('strategyParams'); | |
| const descEl = document.getElementById('strategyDesc'); | |
| if (type === 'ma_cross') { | |
| container.innerHTML = ` | |
| <div class="grid grid-cols-2 gap-3"> | |
| <div> | |
| <label class="text-[11px] text-slate-400 font-medium">Fast EMA Window</label> | |
| <input type="number" id="paramFast" value="12" min="3" max="50" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2.5 py-1 text-xs text-slate-200 mt-1"> | |
| </div> | |
| <div> | |
| <label class="text-[11px] text-slate-400 font-medium">Slow EMA Window</label> | |
| <input type="number" id="paramSlow" value="26" min="10" max="200" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2.5 py-1 text-xs text-slate-200 mt-1"> | |
| </div> | |
| </div> | |
| `; | |
| descEl.innerHTML = "Takes a <strong>Long</strong> position when the Fast EMA crosses above the Slow EMA (Golden Cross), and exits when the Fast EMA crosses below the Slow EMA (Death Cross)."; | |
| } else if (type === 'rsi') { | |
| container.innerHTML = ` | |
| <div class="grid grid-cols-3 gap-2"> | |
| <div> | |
| <label class="text-[11px] text-slate-400 font-medium">Period</label> | |
| <input type="number" id="paramRsiPeriod" value="14" min="5" max="30" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2 py-1 text-xs text-slate-200 mt-1"> | |
| </div> | |
| <div> | |
| <label class="text-[11px] text-slate-400 font-medium">Buy < (Oversold)</label> | |
| <input type="number" id="paramRsiBuy" value="30" min="10" max="45" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2 py-1 text-xs text-slate-200 mt-1"> | |
| </div> | |
| <div> | |
| <label class="text-[11px] text-slate-400 font-medium">Sell > (Overbought)</label> | |
| <input type="number" id="paramRsiSell" value="70" min="55" max="90" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2 py-1 text-xs text-slate-200 mt-1"> | |
| </div> | |
| </div> | |
| `; | |
| descEl.innerHTML = "Buys when RSI dips below the oversold threshold indicating undervalued momentum, and liquidates when RSI rises above the overbought threshold."; | |
| } else if (type === 'bollinger') { | |
| container.innerHTML = ` | |
| <div class="grid grid-cols-2 gap-3"> | |
| <div> | |
| <label class="text-[11px] text-slate-400 font-medium">Period Window</label> | |
| <input type="number" id="paramBbPeriod" value="20" min="5" max="50" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2.5 py-1 text-xs text-slate-200 mt-1"> | |
| </div> | |
| <div> | |
| <label class="text-[11px] text-slate-400 font-medium">Std Dev Multiplier</label> | |
| <input type="number" id="paramBbStd" value="2.0" step="0.1" min="1.0" max="3.5" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2.5 py-1 text-xs text-slate-200 mt-1"> | |
| </div> | |
| </div> | |
| `; | |
| descEl.innerHTML = "Mean-reversion strategy entering when the price pierces below the lower statistical band, taking profit when it touches or crosses the central moving average."; | |
| } else if (type === 'macd') { | |
| container.innerHTML = ` | |
| <div class="grid grid-cols-3 gap-2"> | |
| <div> | |
| <label class="text-[11px] text-slate-400 font-medium">Fast</label> | |
| <input type="number" id="paramMacdFast" value="12" min="5" max="25" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2 py-1 text-xs text-slate-200 mt-1"> | |
| </div> | |
| <div> | |
| <label class="text-[11px] text-slate-400 font-medium">Slow</label> | |
| <input type="number" id="paramMacdSlow" value="26" min="15" max="50" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2 py-1 text-xs text-slate-200 mt-1"> | |
| </div> | |
| <div> | |
| <label class="text-[11px] text-slate-400 font-medium">Signal</label> | |
| <input type="number" id="paramMacdSignal" value="9" min="3" max="20" class="w-full bg-slate-900 border border-slate-700 rounded-lg px-2 py-1 text-xs text-slate-200 mt-1"> | |
| </div> | |
| </div> | |
| `; | |
| descEl.innerHTML = "Calculates the difference between Fast and Slow EMAs. Enters long when MACD crosses above the 9-day Signal EMA line, and exits on bearish crossover."; | |
| } | |
| } | |
| // --- Backtesting Simulator Engine --- | |
| function runBacktest() { | |
| const strategy = document.getElementById('strategyType').value; | |
| const regime = document.getElementById('marketRegime').value; | |
| const initialCapital = parseFloat(document.getElementById('initialCapital').value) || 100000; | |
| const nDays = parseInt(document.getElementById('historyDays').value) || 365; | |
| const feeBps = parseFloat(document.getElementById('feeBps').value) || 5; | |
| const stopLossPct = (parseFloat(document.getElementById('stopLoss').value) || 0) / 100.0; | |
| const feeRate = feeBps / 10000.0; // 5 bps = 0.0005 | |
| document.getElementById('displayBps').textContent = feeBps; | |
| // 1. Generate Market | |
| const { dates, prices } = generateMarketData(regime, nDays); | |
| // 2. Precompute Indicator Signals | |
| const signals = new Array(prices.length).fill(0); // 1 = Buy Signal, -1 = Sell Signal | |
| let indicatorTraces = []; | |
| if (strategy === 'ma_cross') { | |
| const fastP = parseInt(document.getElementById('paramFast').value) || 12; | |
| const slowP = parseInt(document.getElementById('paramSlow').value) || 26; | |
| const fastEma = calcEMA(prices, fastP); | |
| const slowEma = calcEMA(prices, slowP); | |
| for (let i = 1; i < prices.length; i++) { | |
| if (fastEma[i - 1] <= slowEma[i - 1] && fastEma[i] > slowEma[i]) { | |
| signals[i] = 1; | |
| } else if (fastEma[i - 1] >= slowEma[i - 1] && fastEma[i] < slowEma[i]) { | |
| signals[i] = -1; | |
| } | |
| } | |
| indicatorTraces = [ | |
| { x: dates, y: fastEma, name: `Fast EMA (${fastP})`, line: { color: '#38bdf8', width: 1.5 } }, | |
| { x: dates, y: slowEma, name: `Slow EMA (${slowP})`, line: { color: '#a855f7', width: 1.5 } } | |
| ]; | |
| } else if (strategy === 'rsi') { | |
| const period = parseInt(document.getElementById('paramRsiPeriod').value) || 14; | |
| const buyLvl = parseFloat(document.getElementById('paramRsiBuy').value) || 30; | |
| const sellLvl = parseFloat(document.getElementById('paramRsiSell').value) || 70; | |
| const rsi = calcRSI(prices, period); | |
| for (let i = 1; i < prices.length; i++) { | |
| if (rsi[i - 1] !== null) { | |
| if (rsi[i - 1] <= buyLvl && rsi[i] > buyLvl) signals[i] = 1; | |
| else if (rsi[i - 1] >= sellLvl && rsi[i] < sellLvl) signals[i] = -1; | |
| } | |
| } | |
| } else if (strategy === 'bollinger') { | |
| const period = parseInt(document.getElementById('paramBbPeriod').value) || 20; | |
| const std = parseFloat(document.getElementById('paramBbStd').value) || 2.0; | |
| const { sma, upper, lower } = calcBollingerBands(prices, period, std); | |
| for (let i = 1; i < prices.length; i++) { | |
| if (lower[i] !== null) { | |
| if (prices[i - 1] <= lower[i - 1] && prices[i] > lower[i]) signals[i] = 1; | |
| else if (prices[i] >= sma[i]) signals[i] = -1; | |
| } | |
| } | |
| indicatorTraces = [ | |
| { x: dates, y: upper, name: `Upper Band (${std}σ)`, line: { color: 'rgba(56, 189, 248, 0.4)', dash: 'dot' } }, | |
| { x: dates, y: sma, name: `SMA (${period})`, line: { color: '#94a3b8', width: 1.2 } }, | |
| { x: dates, y: lower, name: `Lower Band (${std}σ)`, line: { color: 'rgba(56, 189, 248, 0.4)', dash: 'dot' } } | |
| ]; | |
| } else if (strategy === 'macd') { | |
| const fastP = parseInt(document.getElementById('paramMacdFast').value) || 12; | |
| const slowP = parseInt(document.getElementById('paramMacdSlow').value) || 26; | |
| const sigP = parseInt(document.getElementById('paramMacdSignal').value) || 9; | |
| const fastEma = calcEMA(prices, fastP); | |
| const slowEma = calcEMA(prices, slowP); | |
| const macdLine = fastEma.map((f, idx) => f - slowEma[idx]); | |
| const signalLine = calcEMA(macdLine, sigP); | |
| for (let i = 1; i < prices.length; i++) { | |
| if (macdLine[i - 1] <= signalLine[i - 1] && macdLine[i] > signalLine[i]) signals[i] = 1; | |
| else if (macdLine[i - 1] >= signalLine[i - 1] && macdLine[i] < signalLine[i]) signals[i] = -1; | |
| } | |
| } | |
| // 3. Execution Simulation Loop | |
| let cash = initialCapital; | |
| let shares = 0; | |
| let inPosition = false; | |
| let entryPrice = 0; | |
| let entryDate = ''; | |
| let entryIdx = 0; | |
| const strategyEquity = []; | |
| const benchmarkEquity = []; | |
| const buySignals = []; | |
| const sellSignals = []; | |
| const trades = []; | |
| const initialBenchShares = (initialCapital * (1 - feeRate)) / prices[0]; | |
| for (let i = 0; i < prices.length; i++) { | |
| const curPrice = prices[i]; | |
| // Benchmark valuation | |
| benchmarkEquity.push(initialBenchShares * curPrice); | |
| // Check active position for Stop-Loss | |
| if (inPosition && stopLossPct > 0) { | |
| const drawdownFromEntry = (entryPrice - curPrice) / entryPrice; | |
| if (drawdownFromEntry >= stopLossPct) { | |
| // Stop-Loss triggered! | |
| const proceeds = shares * curPrice * (1 - feeRate); | |
| const netPnL = proceeds - (shares * entryPrice * (1 + feeRate)); | |
| const retPct = (curPrice - entryPrice) / entryPrice * 100; | |
| cash = proceeds; | |
| trades.push({ | |
| id: trades.length + 1, | |
| entryDate, | |
| exitDate: dates[i], | |
| type: 'LONG', | |
| entryPrice, | |
| exitPrice: curPrice, | |
| retPct, | |
| netPnL, | |
| duration: i - entryIdx, | |
| trigger: 'Stop-Loss ⚠️' | |
| }); | |
| sellSignals.push({ x: dates[i], y: curPrice }); | |
| shares = 0; | |
| inPosition = false; | |
| } | |
| } | |
| // Process Strategy Signals | |
| if (!inPosition && signals[i] === 1) { | |
| // BUY | |
| shares = (cash * (1 - feeRate)) / curPrice; | |
| cash = 0; | |
| inPosition = true; | |
| entryPrice = curPrice; | |
| entryDate = dates[i]; | |
| entryIdx = i; | |
| buySignals.push({ x: dates[i], y: curPrice }); | |
| } else if (inPosition && signals[i] === -1) { | |
| // SELL / Take-Profit | |
| const proceeds = shares * curPrice * (1 - feeRate); | |
| const netPnL = proceeds - (shares * entryPrice * (1 + feeRate)); | |
| const retPct = (curPrice - entryPrice) / entryPrice * 100; | |
| cash = proceeds; | |
| trades.push({ | |
| id: trades.length + 1, | |
| entryDate, | |
| exitDate: dates[i], | |
| type: 'LONG', | |
| entryPrice, | |
| exitPrice: curPrice, | |
| retPct, | |
| netPnL, | |
| duration: i - entryIdx, | |
| trigger: 'Signal Exit 🎯' | |
| }); | |
| sellSignals.push({ x: dates[i], y: curPrice }); | |
| shares = 0; | |
| inPosition = false; | |
| } | |
| const currentPortVal = cash + (shares * curPrice); | |
| strategyEquity.push(currentPortVal); | |
| } | |
| // Close open trade at end for fair accounting | |
| if (inPosition) { | |
| const lastPrice = prices[prices.length - 1]; | |
| const proceeds = shares * lastPrice * (1 - feeRate); | |
| const netPnL = proceeds - (shares * entryPrice * (1 + feeRate)); | |
| const retPct = (lastPrice - entryPrice) / entryPrice * 100; | |
| trades.push({ | |
| id: trades.length + 1, | |
| entryDate, | |
| exitDate: dates[dates.length - 1], | |
| type: 'LONG', | |
| entryPrice, | |
| exitPrice: lastPrice, | |
| retPct, | |
| netPnL, | |
| duration: prices.length - 1 - entryIdx, | |
| trigger: 'End of Data' | |
| }); | |
| } | |
| // 4. Compute Metrics | |
| const stratFinal = strategyEquity[strategyEquity.length - 1]; | |
| const benchFinal = benchmarkEquity[benchmarkEquity.length - 1]; | |
| const stratTotalRet = ((stratFinal - initialCapital) / initialCapital) * 100; | |
| const benchTotalRet = ((benchFinal - initialCapital) / initialCapital) * 100; | |
| // Returns & Volatility | |
| const stratReturns = []; | |
| const benchReturns = []; | |
| for (let i = 1; i < strategyEquity.length; i++) { | |
| stratReturns.push((strategyEquity[i] - strategyEquity[i - 1]) / strategyEquity[i - 1]); | |
| benchReturns.push((benchmarkEquity[i] - benchmarkEquity[i - 1]) / benchmarkEquity[i - 1]); | |
| } | |
| const rfDaily = 0.04 / 252.0; | |
| const avgStratRet = stratReturns.reduce((a, b) => a + b, 0) / stratReturns.length; | |
| const stratStd = Math.sqrt(stratReturns.reduce((s, r) => s + Math.pow(r - avgStratRet, 2), 0) / (stratReturns.length - 1)); | |
| const annStratVol = stratStd * Math.sqrt(252); | |
| const sharpe = stratStd > 0 ? ((avgStratRet - rfDaily) / stratStd) * Math.sqrt(252) : 0; | |
| // Downside deviation for Sortino | |
| const downsideDiffs = stratReturns.filter(r => r < rfDaily).map(r => Math.pow(r - rfDaily, 2)); | |
| const downsideDev = Math.sqrt(downsideDiffs.reduce((a, b) => a + b, 0) / (stratReturns.length || 1)); | |
| const sortino = downsideDev > 0 ? ((avgStratRet - rfDaily) / downsideDev) * Math.sqrt(252) : 0; | |
| // Max Drawdown Strategy & Benchmark | |
| let peak = strategyEquity[0]; | |
| let maxDD = 0; | |
| const underwaterStrategy = []; | |
| for (const val of strategyEquity) { | |
| if (val > peak) peak = val; | |
| const dd = (val - peak) / peak; | |
| if (dd < maxDD) maxDD = dd; | |
| underwaterStrategy.push(dd * 100); | |
| } | |
| let bPeak = benchmarkEquity[0]; | |
| let bMaxDD = 0; | |
| for (const val of benchmarkEquity) { | |
| if (val > bPeak) bPeak = val; | |
| const dd = (val - bPeak) / bPeak; | |
| if (dd < bMaxDD) bMaxDD = dd; | |
| } | |
| // Trades statistics | |
| const winTrades = trades.filter(t => t.netPnL > 0); | |
| const lossTrades = trades.filter(t => t.netPnL <= 0); | |
| const winRate = trades.length > 0 ? (winTrades.length / trades.length) * 100 : 0; | |
| const grossProfit = winTrades.reduce((sum, t) => sum + t.netPnL, 0); | |
| const grossLoss = Math.abs(lossTrades.reduce((sum, t) => sum + t.netPnL, 0)); | |
| const profitFactor = grossLoss > 0 ? grossProfit / grossLoss : (grossProfit > 0 ? 99.9 : 0); | |
| // Update KPI DOM | |
| const stratRetEl = document.getElementById('kpiStratRet'); | |
| stratRetEl.textContent = (stratTotalRet >= 0 ? '+' : '') + stratTotalRet.toFixed(1) + '%'; | |
| stratRetEl.className = `text-base font-bold font-mono mt-0.5 ${stratTotalRet >= 0 ? 'text-emerald-400' : 'text-rose-400'}`; | |
| document.getElementById('kpiFinalCapital').textContent = `$${Math.round(stratFinal).toLocaleString()}`; | |
| const benchRetEl = document.getElementById('kpiBenchRet'); | |
| benchRetEl.textContent = (benchTotalRet >= 0 ? '+' : '') + benchTotalRet.toFixed(1) + '%'; | |
| benchRetEl.className = `text-base font-bold font-mono mt-0.5 ${benchTotalRet >= 0 ? 'text-slate-300' : 'text-rose-400/80'}`; | |
| document.getElementById('kpiSharpe').textContent = sharpe.toFixed(2); | |
| document.getElementById('kpiSortino').textContent = `Sortino: ${sortino.toFixed(2)}`; | |
| document.getElementById('kpiMDD').textContent = (maxDD * 100).toFixed(1) + '%'; | |
| document.getElementById('kpiBenchMDD').textContent = `Bench: ${(bMaxDD * 100).toFixed(1)}%`; | |
| document.getElementById('kpiWinRate').textContent = winRate.toFixed(1) + '%'; | |
| document.getElementById('kpiTradesCount').textContent = `${trades.length} trades (${winTrades.length}W / ${lossTrades.length}L)`; | |
| document.getElementById('kpiProfitFactor').textContent = profitFactor.toFixed(2); | |
| const alphaVal = stratTotalRet - benchTotalRet; | |
| const alphaEl = document.getElementById('kpiAlpha'); | |
| alphaEl.textContent = `Alpha: ${(alphaVal >= 0 ? '+' : '') + alphaVal.toFixed(1)}%`; | |
| alphaEl.className = `text-[10px] font-mono ${alphaVal >= 0 ? 'text-emerald-400' : 'text-rose-400'}`; | |
| // Update Trade Log Table | |
| document.getElementById('ledgerBadge').textContent = `${trades.length} Executed Trades`; | |
| const tbody = document.getElementById('tradeLogBody'); | |
| tbody.innerHTML = ''; | |
| if (trades.length === 0) { | |
| tbody.innerHTML = `<tr><td colspan="10" class="py-4 text-center text-slate-500">No trades triggered with current parameters. Adjust rules or lookback.</td></tr>`; | |
| } else { | |
| trades.slice().reverse().forEach(t => { | |
| const row = document.createElement('tr'); | |
| row.className = "hover:bg-slate-800/40 transition"; | |
| const pnlColor = t.netPnL >= 0 ? 'text-emerald-400' : 'text-rose-400'; | |
| row.innerHTML = ` | |
| <td class="py-2 px-3 text-slate-500">#${t.id}</td> | |
| <td class="py-2 px-3 text-slate-300">${t.entryDate}</td> | |
| <td class="py-2 px-3 text-slate-300">${t.exitDate}</td> | |
| <td class="py-2 px-3"><span class="px-1.5 py-0.5 rounded bg-emerald-500/10 text-emerald-400 text-[10px]">${t.type}</span></td> | |
| <td class="py-2 px-3 text-slate-400">$${t.entryPrice.toFixed(2)}</td> | |
| <td class="py-2 px-3 text-slate-400">$${t.exitPrice.toFixed(2)}</td> | |
| <td class="py-2 px-3 ${pnlColor} font-semibold">${(t.retPct >= 0 ? '+' : '') + t.retPct.toFixed(2)}%</td> | |
| <td class="py-2 px-3 ${pnlColor} font-semibold">${t.netPnL >= 0 ? '+$' : '-$'}${Math.abs(t.netPnL).toFixed(2)}</td> | |
| <td class="py-2 px-3 text-slate-400">${t.duration}d</td> | |
| <td class="py-2 px-3 text-[10px] text-slate-400">${t.trigger}</td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } | |
| // 5. Render Plotly Charts | |
| const plotLayout = { | |
| paper_bgcolor: 'rgba(0,0,0,0)', | |
| plot_bgcolor: 'rgba(15, 23, 42, 0.6)', | |
| font: { family: 'Inter', color: '#94a3b8', size: 11 }, | |
| margin: { l: 50, r: 25, t: 30, b: 35 }, | |
| xaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' }, | |
| yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' }, | |
| hovermode: 'x unified', | |
| legend: { orientation: 'h', x: 0, y: 1.12, font: { color: '#cbd5e1' } } | |
| }; | |
| // Chart 1: Equity Curve & Drawdown Subplot | |
| const equityTraces = [ | |
| { | |
| x: dates, | |
| y: strategyEquity, | |
| name: 'Strategy Portfolio ($)', | |
| type: 'scatter', | |
| mode: 'lines', | |
| line: { color: '#10b981', width: 2.2 } | |
| }, | |
| { | |
| x: dates, | |
| y: benchmarkEquity, | |
| name: 'Buy & Hold Benchmark ($)', | |
| type: 'scatter', | |
| mode: 'lines', | |
| line: { color: '#64748b', width: 1.8, dash: 'dash' } | |
| } | |
| ]; | |
| Plotly.newPlot('chartEquity', equityTraces, { | |
| ...plotLayout, | |
| title: { text: '<b>Cumulative Portfolio Equity Growth</b>', font: { size: 13, color: '#f8fafc' } }, | |
| yaxis: { title: 'Equity ($)', gridcolor: '#1e293b' } | |
| }, { responsive: true, displayModeBar: false }); | |
| // Chart 2: Price Action & Buy/Sell Markers | |
| const priceTraces = [ | |
| { | |
| x: dates, | |
| y: prices, | |
| name: 'Asset Price ($)', | |
| type: 'scatter', | |
| mode: 'lines', | |
| line: { color: '#38bdf8', width: 1.8 } | |
| }, | |
| ...indicatorTraces | |
| ]; | |
| if (buySignals.length > 0) { | |
| priceTraces.push({ | |
| x: buySignals.map(s => s.x), | |
| y: buySignals.map(s => s.y), | |
| mode: 'markers', | |
| name: 'BUY Entry', | |
| marker: { symbol: 'triangle-up', size: 10, color: '#22c55e', line: { width: 1, color: '#ffffff' } } | |
| }); | |
| } | |
| if (sellSignals.length > 0) { | |
| priceTraces.push({ | |
| x: sellSignals.map(s => s.x), | |
| y: sellSignals.map(s => s.y), | |
| mode: 'markers', | |
| name: 'SELL Exit', | |
| marker: { symbol: 'triangle-down', size: 10, color: '#ef4444', line: { width: 1, color: '#ffffff' } } | |
| }); | |
| } | |
| Plotly.newPlot('chartPrice', priceTraces, { | |
| ...plotLayout, | |
| title: { text: '<b>Asset Trajectory & Quantitative Trade Entries</b>', font: { size: 13, color: '#f8fafc' } }, | |
| yaxis: { title: 'Price ($)', gridcolor: '#1e293b' } | |
| }, { responsive: true, displayModeBar: false }); | |
| } | |
| // --- Tab Switching Logic --- | |
| document.getElementById('tabBtnEquity').addEventListener('click', () => { | |
| document.getElementById('chartEquityContainer').classList.remove('hidden'); | |
| document.getElementById('chartPriceContainer').classList.add('hidden'); | |
| document.getElementById('tabBtnEquity').className = "px-3.5 py-1.5 rounded-lg bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 text-xs font-semibold transition"; | |
| document.getElementById('tabBtnPrice').className = "px-3.5 py-1.5 rounded-lg bg-slate-800/80 text-slate-400 hover:text-slate-200 text-xs font-semibold transition"; | |
| Plotly.Plots.resize('chartEquity'); | |
| }); | |
| document.getElementById('tabBtnPrice').addEventListener('click', () => { | |
| document.getElementById('chartPriceContainer').classList.remove('hidden'); | |
| document.getElementById('chartEquityContainer').classList.add('hidden'); | |
| document.getElementById('tabBtnPrice').className = "px-3.5 py-1.5 rounded-lg bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 text-xs font-semibold transition"; | |
| document.getElementById('tabBtnEquity').className = "px-3.5 py-1.5 rounded-lg bg-slate-800/80 text-slate-400 hover:text-slate-200 text-xs font-semibold transition"; | |
| Plotly.Plots.resize('chartPrice'); | |
| }); | |
| // Wire Strategy dropdown | |
| document.getElementById('strategyType').addEventListener('change', () => { | |
| renderStrategyParams(); | |
| runBacktest(); | |
| }); | |
| // Wire Regime dropdown | |
| document.getElementById('marketRegime').addEventListener('change', runBacktest); | |
| // Wire Run Button | |
| document.getElementById('runBacktestBtn').addEventListener('click', runBacktest); | |
| // Initial setup | |
| renderStrategyParams(); | |
| runBacktest(); | |
| </script> | |
| </body> | |
| </html> | |