CoolFace
Apppublic

diegobeyl/backtesting

sourceHugging Faceupdated 8mo agoView on Hugging Face
2likes
app.js681 linesDownload Raw Back to static
1/**
2 * Donchian Backtesting Application
3 * Frontend JavaScript
4 */
5
6// =====================================================
7// Configuration & State
8// =====================================================
9
10const API_BASE = '';
11const WS_URL = `ws://${window.location.host}/api/ws`;
12
13let ws = null;
14let currentConfig = null;
15let isRunning = false;
16const APP_VERSION = "1.2.2";
17console.log(`Donchian Backtester v${APP_VERSION} loading...`);
18
19// =====================================================
20// DOM Elements
21// =====================================================
22
23const elements = {
24    connectionStatus: document.getElementById('connectionStatus'),
25    configForm: document.getElementById('configForm'),
26    runBtn: document.getElementById('runBtn'),
27    resetConfigBtn: document.getElementById('resetConfigBtn'),
28    progressContainer: document.getElementById('progressContainer'),
29    progressFill: document.getElementById('progressFill'),
30    progressText: document.getElementById('progressText'),
31    metricsGrid: document.getElementById('metricsGrid'),
32    // Metrics
33    metricReturn: document.getElementById('metricReturn'),
34    metricReturnPct: document.getElementById('metricReturnPct'),
35    metricSharpe: document.getElementById('metricSharpe'),
36    metricDrawdown: document.getElementById('metricDrawdown'),
37    metricWinRate: document.getElementById('metricWinRate'),
38    metricProfitFactor: document.getElementById('metricProfitFactor'),
39    metricTrades: document.getElementById('metricTrades'),
40    // Charts
41    candlestickChart: document.getElementById('candlestickChart'),
42    equityChart: document.getElementById('equityChart'),
43    tradesTableBody: document.getElementById('tradesTableBody'),
44};
45
46// =====================================================
47// WebSocket Connection
48// =====================================================
49
50function connectWebSocket() {
51    ws = new WebSocket(WS_URL);
52
53    ws.onopen = () => {
54        console.log('WebSocket connected');
55        updateConnectionStatus('connected');
56    };
57
58    ws.onclose = () => {
59        console.log('WebSocket disconnected');
60        updateConnectionStatus('disconnected');
61        // Reconnect after 3 seconds
62        setTimeout(connectWebSocket, 3000);
63    };
64
65    ws.onerror = (error) => {
66        console.error('WebSocket error:', error);
67        updateConnectionStatus('disconnected');
68    };
69
70    ws.onmessage = (event) => {
71        try {
72            const message = JSON.parse(event.data);
73            handleWebSocketMessage(message);
74        } catch (e) {
75            console.error('Failed to parse WebSocket message:', e);
76        }
77    };
78}
79
80function handleWebSocketMessage(message) {
81    switch (message.type) {
82        case 'connected':
83            if (message.config) {
84                currentConfig = message.config;
85                updateFormFromConfig(message.config);
86            }
87            break;
88
89        case 'config_updated':
90            currentConfig = message.config;
91            updateFormFromConfig(message.config);
92            showNotification('Configuration updated', 'success');
93            break;
94
95        case 'status':
96            updateProgress(message.progress, message.message);
97            break;
98
99        case 'result':
100            handleBacktestResult(message.data);
101            break;
102
103        case 'heartbeat':
104            // Keep alive
105            break;
106    }
107}
108
109function updateConnectionStatus(status) {
110    const statusDot = elements.connectionStatus.querySelector('.status-dot');
111    const statusText = elements.connectionStatus.querySelector('.status-text');
112
113    statusDot.className = 'status-dot ' + status;
114    statusText.textContent = status === 'connected' ? 'Connected' :
115        status === 'disconnected' ? 'Disconnected' : 'Connecting...';
116}
117
118// =====================================================
119// API Functions
120// =====================================================
121
122async function fetchAPI(endpoint, options = {}) {
123    const response = await fetch(`${API_BASE}/api${endpoint}`, {
124        headers: {
125            'Content-Type': 'application/json',
126            ...options.headers,
127        },
128        ...options,
129    });
130
131    if (!response.ok) {
132        const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
133        throw new Error(error.detail || 'API request failed');
134    }
135
136    return response.json();
137}
138
139async function loadSymbols() {
140    try {
141        const data = await fetchAPI('/symbols');
142        const datalist = document.getElementById('symbolList');
143        if (datalist) {
144            datalist.innerHTML = data.symbols.map(s =>
145                `<option value="${s}">`
146            ).join('');
147        }
148    } catch (error) {
149        console.error('Failed to load symbols:', error);
150    }
151}
152
153async function loadConfig() {
154    try {
155        const config = await fetchAPI('/config');
156        currentConfig = config;
157        updateFormFromConfig(config);
158    } catch (error) {
159        console.error('Failed to load config:', error);
160    }
161}
162
163async function saveConfig(config) {
164    try {
165        await fetchAPI('/config', {
166            method: 'PUT',
167            body: JSON.stringify({ config }),
168        });
169    } catch (error) {
170        console.error('Failed to save config:', error);
171        showNotification('Failed to save configuration', 'error');
172    }
173}
174
175async function runBacktest(config) {
176    try {
177        isRunning = true;
178        elements.runBtn.disabled = true;
179        elements.runBtn.innerHTML = '<span class="btn-icon">⏳</span> Running...';
180        elements.progressContainer.style.display = 'block';
181
182        const result = await fetchAPI('/backtest/run', {
183            method: 'POST',
184            body: JSON.stringify({ config }),
185        });
186
187        console.log(`[v${APP_VERSION}] Backtest result received:`, result);
188        handleBacktestResult(result);
189
190    } catch (error) {
191        console.error('Backtest error:', error);
192        showNotification('Backtest failed: ' + error.message, 'error');
193    } finally {
194        isRunning = false;
195        elements.progressContainer.style.display = 'none';
196        elements.runBtn.disabled = false;
197        elements.runBtn.innerHTML = '<span class="btn-icon">▶</span> Run Backtest';
198    }
199}
200
201async function resetConfig() {
202    try {
203        await fetchAPI('/config/reset', { method: 'POST' });
204        await loadConfig();
205        showNotification('Configuration reset to defaults', 'success');
206    } catch (error) {
207        console.error('Failed to reset config:', error);
208        showNotification('Failed to reset configuration', 'error');
209    }
210}
211
212// =====================================================
213// Form Handling
214// =====================================================
215
216function updateFormFromConfig(config) {
217    Object.keys(config).forEach(key => {
218        const element = document.getElementById(key);
219        if (element) {
220            if (element.type === 'checkbox') {
221                element.checked = config[key];
222            } else {
223                element.value = config[key];
224            }
225        }
226    });
227}
228
229function getFormConfig() {
230    const formData = new FormData(elements.configForm);
231    const config = {};
232
233    formData.forEach((value, key) => {
234        const element = document.getElementById(key);
235        if (element.type === 'number') {
236            config[key] = parseFloat(value);
237        } else if (element.type === 'checkbox') {
238            config[key] = element.checked;
239        } else {
240            config[key] = value;
241        }
242    });
243
244    // Handle checkbox that might not be in formData when unchecked
245    const checkbox = document.getElementById('stop_resets_support');
246    config.stop_resets_support = checkbox.checked;
247
248    return config;
249}
250
251// =====================================================
252// Progress Updates
253// =====================================================
254
255function updateProgress(progress, message) {
256    elements.progressFill.style.width = `${progress}%`;
257    elements.progressText.textContent = message;
258
259    if (progress >= 100) {
260        setTimeout(() => {
261            elements.progressContainer.style.display = 'none';
262        }, 1000);
263    }
264}
265
266// =====================================================
267// Results Display
268// =====================================================
269
270function handleBacktestResult(result) {
271    if (!result.success) {
272        showNotification('Backtest failed: ' + (result.error || 'Unknown error'), 'error');
273        return;
274    }
275
276    // Update metrics
277    if (result.metrics) {
278        updateMetrics(result.metrics);
279    }
280
281    // Update charts
282    if (result.chart_data) {
283        renderCandlestickChart(result.chart_data, result.config);
284    }
285
286    if (result.equity_curve) {
287        renderEquityChart(result.equity_curve);
288    }
289
290    // Update trades table
291    if (result.trades) {
292        renderTradesTable(result.trades);
293    }
294
295    showNotification(`Backtest completed in ${result.execution_time.toFixed(2)}s`, 'success');
296}
297
298function updateMetrics(metrics) {
299    console.log(`[v${APP_VERSION}] Updating metrics:`, metrics);
300    // Total Return
301    const returnValue = metrics.total_return;
302    elements.metricReturn.textContent = formatCurrency(returnValue);
303    elements.metricReturnPct.textContent = formatPercent(metrics.total_return_percent);
304
305    // Update main card styling based on profit/loss
306    const returnCard = elements.metricReturn.closest('.metric-card');
307    if (returnValue >= 0) {
308        returnCard.classList.remove('negative');
309        returnCard.classList.add('positive');
310        elements.metricReturn.className = 'metric-value positive';
311    } else {
312        returnCard.classList.remove('positive');
313        returnCard.classList.add('negative');
314        elements.metricReturn.className = 'metric-value negative';
315    }
316
317    // Sharpe Ratio
318    elements.metricSharpe.textContent = metrics.sharpe_ratio.toFixed(2);
319
320    // Max Drawdown
321    elements.metricDrawdown.textContent = formatPercent(metrics.max_drawdown_percent);
322
323    // Win Rate
324    elements.metricWinRate.textContent = formatPercent(metrics.win_rate);
325
326    // Profit Factor
327    elements.metricProfitFactor.textContent = metrics.profit_factor.toFixed(2);
328
329    // Total Trades
330    elements.metricTrades.textContent = metrics.total_trades;
331}
332
333// =====================================================
334// Chart Rendering
335// =====================================================
336
337function renderCandlestickChart(chartData, config) {
338    const ohlc = chartData.ohlc;
339    const donchian = chartData.donchian;
340    const signals = chartData.signals;
341    const structural = chartData.structural || [];
342    const stopLoss = chartData.stop_loss || [];
343
344    const traces = [];
345
346    // 1. Candlestick trace
347    traces.push({
348        type: 'candlestick',
349        x: ohlc.map(d => d.time),
350        open: ohlc.map(d => d.open),
351        high: ohlc.map(d => d.high),
352        low: ohlc.map(d => d.low),
353        close: ohlc.map(d => d.close),
354        name: config.symbol,
355        increasing: { line: { color: '#3fb950', width: 1 } },
356        decreasing: { line: { color: '#f85149', width: 1 } },
357    });
358
359    // 2. Donchian Channels (Lighter)
360    traces.push({
361        type: 'scatter',
362        mode: 'lines',
363        x: donchian.map(d => d.time),
364        y: donchian.map(d => d.high),
365        name: 'Donchian High',
366        line: { color: '#f85149', width: 1, dash: 'dot' },
367        opacity: 0.4,
368        connectgaps: false
369    });
370
371    traces.push({
372        type: 'scatter',
373        mode: 'lines',
374        x: donchian.map(d => d.time),
375        y: donchian.map(d => d.low),
376        name: 'Donchian Low',
377        line: { color: '#3fb950', width: 1, dash: 'dot' },
378        opacity: 0.4,
379        connectgaps: false
380    });
381
382    // 3. Structural Levels
383    if (structural.length > 0) {
384        traces.push({
385            type: 'scatter',
386            mode: 'lines',
387            x: structural.map(d => d.time),
388            y: structural.map(d => d.resistance),
389            name: 'Resistance',
390            line: { color: '#00fbff', width: 3, dash: 'dash' },
391            connectgaps: false
392        });
393
394        traces.push({
395            type: 'scatter',
396            mode: 'lines',
397            x: structural.map(d => d.time),
398            y: structural.map(d => d.support),
399            name: 'Support',
400            line: { color: '#ff00ff', width: 3, dash: 'dash' },
401            connectgaps: false
402        });
403    }
404
405    // 4. Stop Loss Line (Staircase)
406    if (stopLoss.length > 0) {
407        traces.push({
408            type: 'scatter',
409            mode: 'lines+markers',
410            x: stopLoss.map(d => d.time),
411            y: stopLoss.map(d => d.sl),
412            name: 'Stop Loss',
413            line: { color: '#ff9900', width: 3, shape: 'hv' },
414            marker: { size: 6, color: '#ff9900', symbol: 'circle' },
415            connectgaps: false
416        });
417    }
418
419    // 5. Entry signals
420    const entryLong = signals.filter(s => s.type === 'entry' && s.direction === 'long');
421    const entryShort = signals.filter(s => s.type === 'entry' && s.direction === 'short');
422    const exitLong = signals.filter(s => s.type === 'exit' && s.direction === 'long');
423    const exitShort = signals.filter(s => s.type === 'exit' && s.direction === 'short');
424
425    traces.push({
426        type: 'scatter',
427        mode: 'markers',
428        x: entryLong.map(s => s.time),
429        y: entryLong.map(s => s.price),
430        name: 'Long Entry',
431        marker: { symbol: 'triangle-up', size: 16, color: '#3fb950', line: { color: 'white', width: 1 } },
432    });
433
434    traces.push({
435        type: 'scatter',
436        mode: 'markers',
437        x: entryShort.map(s => s.time),
438        y: entryShort.map(s => s.price),
439        name: 'Short Entry',
440        marker: { symbol: 'triangle-down', size: 16, color: '#f85149', line: { color: 'white', width: 1 } },
441    });
442
443    traces.push({
444        type: 'scatter',
445        mode: 'markers',
446        x: exitLong.map(s => s.time),
447        y: exitLong.map(s => s.price),
448        name: 'Long Exit',
449        marker: { symbol: 'x', size: 12, color: '#ffffff', line: { color: '#3fb950', width: 1 } },
450    });
451
452    traces.push({
453        marker: { symbol: 'x', size: 12, color: '#ffffff', line: { color: '#f85149', width: 1 } },
454    });
455
456    // 6. Background shapes based on tendency
457    const shapes = [];
458    if (structural && structural.length > 0) {
459        let currentStart = structural[0].time;
460        let currentTend = structural[0].tendency;
461
462        for (let i = 1; i <= structural.length; i++) {
463            const item = structural[i];
464            const nextTend = item ? item.tendency : null;
465
466            if (nextTend !== currentTend) {
467                const currentEnd = item ? item.time : structural[structural.length - 1].time;
468
469                shapes.push({
470                    type: 'rect',
471                    xref: 'x',
472                    yref: 'paper',
473                    x0: currentStart,
474                    y0: 0,
475                    x1: currentEnd,
476                    y1: 1,
477                    fillcolor: currentTend === 1 ? 'rgba(63, 185, 80, 0.08)' : 'rgba(248, 81, 73, 0.08)',
478                    line: { width: 0 },
479                    layer: 'below'
480                });
481
482                if (item) {
483                    currentStart = item.time;
484                    currentTend = nextTend;
485                }
486            }
487        }
488    }
489
490    const layout = {
491        paper_bgcolor: '#161b22',
492        plot_bgcolor: '#0d1117',
493        font: { color: '#f0f6fc' },
494        xaxis: {
495            rangeslider: { visible: false },
496            gridcolor: '#21262d',
497            type: 'date'
498        },
499        yaxis: {
500            gridcolor: '#21262d',
501            fixedrange: false
502        },
503        legend: {
504            orientation: 'h',
505            y: 1.1,
506            bgcolor: 'rgba(0,0,0,0)'
507        },
508        margin: { l: 60, r: 20, t: 30, b: 40 },
509        hovermode: 'x unified',
510        shapes: shapes
511    };
512
513    Plotly.newPlot(elements.candlestickChart, traces, layout, { responsive: true });
514}
515
516function renderEquityChart(equityCurve) {
517    const equityTrace = {
518        type: 'scatter',
519        mode: 'lines',
520        x: equityCurve.map(d => d.time),
521        y: equityCurve.map(d => d.equity),
522        name: 'Equity',
523        line: { color: '#58a6ff', width: 2 },
524        fill: 'tozeroy',
525        fillcolor: 'rgba(88, 166, 255, 0.1)',
526    };
527
528    const layout = {
529        title: 'Equity Curve',
530        paper_bgcolor: '#161b22',
531        plot_bgcolor: '#0d1117',
532        font: { color: '#f0f6fc' },
533        xaxis: {
534            gridcolor: '#21262d',
535        },
536        yaxis: {
537            gridcolor: '#21262d',
538            title: 'Equity ($)',
539        },
540        margin: { l: 60, r: 20, t: 60, b: 40 },
541    };
542
543    Plotly.newPlot(elements.equityChart, [equityTrace], layout, { responsive: true });
544}
545
546function renderTradesTable(trades) {
547    if (trades.length === 0) {
548        elements.tradesTableBody.innerHTML = `
549            <tr><td colspan="8" class="no-data">No trades executed</td></tr>
550        `;
551        return;
552    }
553
554    elements.tradesTableBody.innerHTML = trades.map(trade => `
555        <tr>
556            <td>${formatDateTime(trade.entry_time)}</td>
557            <td>${formatDateTime(trade.exit_time)}</td>
558            <td><span class="trade-type ${trade.type.toLowerCase()}">${trade.type}</span></td>
559            <td>${formatPrice(trade.entry_price)}</td>
560            <td>${formatPrice(trade.exit_price)}</td>
561            <td>${Math.abs(trade.size).toFixed(4)}</td>
562            <td class="${trade.pnl >= 0 ? 'pnl-positive' : 'pnl-negative'}">${formatCurrency(trade.pnl)}</td>
563            <td class="${trade.return_percent >= 0 ? 'pnl-positive' : 'pnl-negative'}">${formatPercent(trade.return_percent)}</td>
564        </tr>
565    `).join('');
566}
567
568// =====================================================
569// Tab Navigation
570// =====================================================
571
572function setupTabs() {
573    const tabBtns = document.querySelectorAll('.tab-btn');
574    const panels = document.querySelectorAll('.chart-panel');
575
576    tabBtns.forEach(btn => {
577        btn.addEventListener('click', () => {
578            const tab = btn.dataset.tab;
579
580            // Update buttons
581            tabBtns.forEach(b => b.classList.remove('active'));
582            btn.classList.add('active');
583
584            // Update panels
585            panels.forEach(p => p.classList.remove('active'));
586            document.getElementById(`${tab}Panel`).classList.add('active');
587
588            // Trigger resize for Plotly charts
589            window.dispatchEvent(new Event('resize'));
590        });
591    });
592}
593
594// =====================================================
595// Utility Functions
596// =====================================================
597
598function formatCurrency(value) {
599    const isNegative = value < 0;
600    const formatted = Math.abs(value).toLocaleString('en-US', {
601        minimumFractionDigits: 2,
602        maximumFractionDigits: 2
603    });
604    return (isNegative ? '-' : '+') + '$' + formatted;
605}
606
607function formatPercent(value) {
608    return (value >= 0 ? '+' : '') + value.toFixed(2) + '%';
609}
610
611function formatPrice(value) {
612    return value.toLocaleString('en-US', {
613        minimumFractionDigits: 2,
614        maximumFractionDigits: 5
615    });
616}
617
618function formatDateTime(dateStr) {
619    const date = new Date(dateStr);
620    return date.toLocaleDateString('en-US', {
621        month: 'short',
622        day: 'numeric',
623        year: '2-digit'
624    });
625}
626
627function showNotification(message, type = 'info') {
628    // Simple alert for now - could be enhanced with toast notifications
629    console.log(`[${type.toUpperCase()}] ${message}`);
630}
631
632// =====================================================
633// Event Listeners
634// =====================================================
635
636function setupEventListeners() {
637    // Form submission
638    elements.configForm.addEventListener('submit', async (e) => {
639        e.preventDefault();
640        const config = getFormConfig();
641        await runBacktest(config);
642    });
643
644    // Reset config button
645    elements.resetConfigBtn.addEventListener('click', resetConfig);
646
647    // Auto-save config on change (debounced)
648    let saveTimeout = null;
649    elements.configForm.addEventListener('change', () => {
650        if (saveTimeout) clearTimeout(saveTimeout);
651        saveTimeout = setTimeout(() => {
652            const config = getFormConfig();
653            saveConfig(config);
654        }, 500);
655    });
656}
657
658// =====================================================
659// Initialization
660// =====================================================
661
662async function init() {
663    console.log('Initializing Donchian Backtesting App...');
664
665    // Load initial data
666    await loadSymbols();
667    await loadConfig();
668
669    // Setup UI
670    setupTabs();
671    setupEventListeners();
672
673    // Connect WebSocket
674    connectWebSocket();
675
676    console.log('App initialized');
677}
678
679// Start the app when DOM is ready
680document.addEventListener('DOMContentLoaded', init);
681