alphabacktest-studio / index.html
At1uri's picture
Upload folder using huggingface_hub
02c4dda verified
Raw History Blame Contribute Delete
40 kB
<!DOCTYPE html>
<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 &lt; (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 &gt; (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>