diff --git a/src/api/dashboard/static/css/indicators-new.css b/src/api/dashboard/static/css/indicators-new.css index 4138129..d2f00e7 100644 --- a/src/api/dashboard/static/css/indicators-new.css +++ b/src/api/dashboard/static/css/indicators-new.css @@ -646,4 +646,54 @@ /* Collapsed sidebar adjustments */ .right-sidebar.collapsed .sidebar-tabs { display: none; +} + +/* Strategy Panel Styles */ +.indicator-checklist { + max-height: 120px; + overflow-y: auto; + background: var(--tv-bg); + border: 1px solid var(--tv-border); + border-radius: 4px; + padding: 4px; + margin-top: 4px; +} +.indicator-checklist::-webkit-scrollbar { + width: 4px; +} +.indicator-checklist::-webkit-scrollbar-thumb { + background: var(--tv-border); + border-radius: 2px; +} + +.checklist-item { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 8px; + font-size: 12px; + cursor: pointer; + border-radius: 3px; +} +.checklist-item:hover { + background: var(--tv-hover); +} +.checklist-item input { + cursor: pointer; +} + +.equity-chart-container { + width: 100%; + height: 150px; + margin-top: 12px; + border-radius: 4px; + overflow: hidden; + border: 1px solid var(--tv-border); + background: var(--tv-bg); +} + +.results-actions { + display: flex; + gap: 8px; + margin-top: 12px; } \ No newline at end of file diff --git a/src/api/dashboard/static/index.html b/src/api/dashboard/static/index.html index 35c9c79..db4d05b 100644 --- a/src/api/dashboard/static/index.html +++ b/src/api/dashboard/static/index.html @@ -1480,6 +1480,7 @@ @@ -1493,6 +1494,14 @@ + + + diff --git a/src/api/dashboard/static/js/app.js b/src/api/dashboard/static/js/app.js index 33a7ba3..839bb38 100644 --- a/src/api/dashboard/static/js/app.js +++ b/src/api/dashboard/static/js/app.js @@ -8,6 +8,7 @@ import { addIndicator, removeIndicatorById } from './ui/indicators-panel-new.js'; +import { initStrategyPanel } from './ui/strategy-panel.js'; import { IndicatorRegistry } from './indicators/index.js'; import { TimezoneConfig } from './config/timezone.js'; @@ -75,6 +76,7 @@ document.addEventListener('DOMContentLoaded', async () => { restoreSidebarTabState(); initSidebarTabs(); - // Initialize indicator panel + // Initialize panels window.initIndicatorPanel(); + initStrategyPanel(); }); diff --git a/src/api/dashboard/static/js/indicators/bb.js b/src/api/dashboard/static/js/indicators/bb.js index dceb879..ae1aa5c 100644 --- a/src/api/dashboard/static/js/indicators/bb.js +++ b/src/api/dashboard/static/js/indicators/bb.js @@ -32,35 +32,38 @@ class BaseIndicator { } // Signal calculation for Bollinger Bands -function calculateBollingerBandsSignal(indicator, lastCandle, prevCandle, values) { +function calculateBollingerBandsSignal(indicator, lastCandle, prevCandle, values, prevValues) { const close = lastCandle.close; + const prevClose = prevCandle?.close; const upper = values?.upper; const lower = values?.lower; - const middle = values?.middle; + const prevUpper = prevValues?.upper; + const prevLower = prevValues?.lower; - if (!upper || !lower || !middle) { + if (!upper || !lower || prevUpper === undefined || prevLower === undefined || prevClose === undefined) { return null; } - const bandwidth = (upper - lower) / middle * 100; - - if (close <= lower) { + // BUY: Price crosses DOWN through lower band (reversal/bounce play) + if (prevClose > prevLower && close <= lower) { return { type: SIGNAL_TYPES.BUY, - strength: Math.min(50 + (lower - close) / close * 1000, 100), + strength: 70, value: close, - reasoning: `Price (${close.toFixed(2)}) at or below lower band (${lower.toFixed(2)}), bandwidth: ${bandwidth.toFixed(1)}%` + reasoning: `Price crossed DOWN through lower Bollinger Band` }; - } else if (close >= upper) { + } + // SELL: Price crosses UP through upper band (overextended play) + else if (prevClose < prevUpper && close >= upper) { return { type: SIGNAL_TYPES.SELL, - strength: Math.min(50 + (close - upper) / close * 1000, 100), + strength: 70, value: close, - reasoning: `Price (${close.toFixed(2)}) at or above upper band (${upper.toFixed(2)}), bandwidth: ${bandwidth.toFixed(1)}%` + reasoning: `Price crossed UP through upper Bollinger Band` }; - } else { - return null; } + + return null; } // Bollinger Bands Indicator class diff --git a/src/api/dashboard/static/js/indicators/hts.js b/src/api/dashboard/static/js/indicators/hts.js index 79f12ef..df15521 100644 --- a/src/api/dashboard/static/js/indicators/hts.js +++ b/src/api/dashboard/static/js/indicators/hts.js @@ -126,35 +126,41 @@ function getMA(type, candles, period, source = 'close') { } // Signal calculation for HTS -function calculateHTSSignal(indicator, lastCandle, prevCandle, values) { - const fastHigh = values?.fastHigh; - const fastLow = values?.fastLow; - const slowHigh = values?.slowHigh; +function calculateHTSSignal(indicator, lastCandle, prevCandle, values, prevValues) { const slowLow = values?.slowLow; + const slowHigh = values?.slowHigh; + const prevSlowLow = prevValues?.slowLow; + const prevSlowHigh = prevValues?.slowHigh; - if (!fastHigh || !fastLow || !slowHigh || !slowLow) { + if (!slowLow || !slowHigh || !prevSlowLow || !prevSlowHigh) { return null; } const close = lastCandle.close; + const prevClose = prevCandle?.close; - if (close > slowLow) { + if (prevClose === undefined) return null; + + // BUY: Price crosses UP through slow low + if (prevClose <= prevSlowLow && close > slowLow) { return { type: SIGNAL_TYPES.BUY, - strength: Math.min(60 + (close - slowLow) / slowLow * 500, 100), + strength: 85, value: close, - reasoning: `Price (${close.toFixed(2)}) is above slow low (${slowLow.toFixed(2)})` + reasoning: `Price crossed UP through slow low` }; - } else if (close < slowHigh) { + } + // SELL: Price crosses DOWN through slow high + else if (prevClose >= prevSlowHigh && close < slowHigh) { return { type: SIGNAL_TYPES.SELL, - strength: Math.min(60 + (slowHigh - close) / close * 500, 100), + strength: 85, value: close, - reasoning: `Price (${close.toFixed(2)}) is below slow high (${slowHigh.toFixed(2)})` + reasoning: `Price crossed DOWN through slow high` }; - } else { - return null; } + + return null; } // HTS Indicator class diff --git a/src/api/dashboard/static/js/indicators/hurst.js b/src/api/dashboard/static/js/indicators/hurst.js index b8cb4dc..7fe7c9a 100644 --- a/src/api/dashboard/static/js/indicators/hurst.js +++ b/src/api/dashboard/static/js/indicators/hurst.js @@ -63,26 +63,30 @@ function calculateHurstSignal(indicator, lastCandle, prevCandle, values, prevVal const prevClose = prevCandle?.close; const upper = values?.upper; const lower = values?.lower; + const prevUpper = prevValues?.upper; + const prevLower = prevValues?.lower; - if (!upper || !lower || prevClose === undefined) { + if (close === undefined || prevClose === undefined || !upper || !lower || !prevUpper || !prevLower) { return null; } - if (prevClose > lower && close < lower) { + // BUY: Price crosses DOWN through lower Hurst Band + if (prevClose > prevLower && close <= lower) { return { type: 'buy', strength: 75, value: close, - reasoning: `Price crossed down below lower Hurst Band (${lower.toFixed(2)}), expect bounce` + reasoning: `Price crossed DOWN through lower Hurst Band` }; } - if (prevClose > upper && close < upper) { + // SELL: Price crosses DOWN through upper Hurst Band (reversal from top) + if (prevClose > prevUpper && close <= upper) { return { type: 'sell', strength: 75, value: close, - reasoning: `Price crossed down below upper Hurst Band (${upper.toFixed(2)}), expect reversal` + reasoning: `Price crossed DOWN through upper Hurst Band` }; } diff --git a/src/api/dashboard/static/js/indicators/macd.js b/src/api/dashboard/static/js/indicators/macd.js index aa80fbc..71ff6ab 100644 --- a/src/api/dashboard/static/js/indicators/macd.js +++ b/src/api/dashboard/static/js/indicators/macd.js @@ -50,32 +50,37 @@ function calculateEMAInline(data, period) { } // Signal calculation for MACD -function calculateMACDSignal(indicator, lastCandle, prevCandle, values) { +function calculateMACDSignal(indicator, lastCandle, prevCandle, values, prevValues) { const macd = values?.macd; const signal = values?.signal; - const histogram = values?.histogram; + const prevMacd = prevValues?.macd; + const prevSignal = prevValues?.signal; - if (!macd || macd === null || !signal || signal === null) { + if (macd === undefined || macd === null || signal === undefined || signal === null || + prevMacd === undefined || prevMacd === null || prevSignal === undefined || prevSignal === null) { return null; } - let signalType, strength, reasoning; - - const prevCandleHistogram = prevCandle ? values?.histogram : null; - - if (macd > signal) { - signalType = SIGNAL_TYPES.BUY; - strength = Math.min(50 + histogram * 500, 100); - reasoning = `MACD (${macd.toFixed(2)}) is above Signal (${signal.toFixed(2)})`; - } else if (macd < signal) { - signalType = SIGNAL_TYPES.SELL; - strength = Math.min(50 + Math.abs(histogram) * 500, 100); - reasoning = `MACD (${macd.toFixed(2)}) is below Signal (${signal.toFixed(2)})`; - } else { - return null; + // BUY: MACD crosses UP through Signal line + if (prevMacd <= prevSignal && macd > signal) { + return { + type: SIGNAL_TYPES.BUY, + strength: 80, + value: macd, + reasoning: `MACD crossed UP through Signal line` + }; + } + // SELL: MACD crosses DOWN through Signal line + else if (prevMacd >= prevSignal && macd < signal) { + return { + type: SIGNAL_TYPES.SELL, + strength: 80, + value: macd, + reasoning: `MACD crossed DOWN through Signal line` + }; } - return { type: signalType, strength, value: macd, reasoning }; + return null; } // MACD Indicator class diff --git a/src/api/dashboard/static/js/indicators/moving_average.js b/src/api/dashboard/static/js/indicators/moving_average.js index ea89324..624ce69 100644 --- a/src/api/dashboard/static/js/indicators/moving_average.js +++ b/src/api/dashboard/static/js/indicators/moving_average.js @@ -114,31 +114,35 @@ function calculateVWMA(candles, period, source = 'close') { } // Signal calculation for Moving Average -function calculateMASignal(indicator, lastCandle, prevCandle, values) { +function calculateMASignal(indicator, lastCandle, prevCandle, values, prevValues) { const close = lastCandle.close; + const prevClose = prevCandle?.close; const ma = values?.ma; + const prevMa = prevValues?.ma; - if (!ma && ma !== 0) { - return null; - } + if (!ma && ma !== 0) return null; + if (prevClose === undefined || prevMa === undefined || prevMa === null) return null; - if (close > ma) { + // BUY: Price crosses UP through MA + if (prevClose <= prevMa && close > ma) { return { type: SIGNAL_TYPES.BUY, - strength: Math.min(60 + ((close - ma) / ma) * 500, 100), + strength: 80, value: close, - reasoning: `Price (${close.toFixed(2)}) is above MA (${ma.toFixed(2)})` + reasoning: `Price crossed UP through MA` }; - } else if (close < ma) { + } + // SELL: Price crosses DOWN through MA + else if (prevClose >= prevMa && close < ma) { return { type: SIGNAL_TYPES.SELL, - strength: Math.min(60 + ((ma - close) / ma) * 500, 100), + strength: 80, value: close, - reasoning: `Price (${close.toFixed(2)}) is below MA (${ma.toFixed(2)})` + reasoning: `Price crossed DOWN through MA` }; - } else { - return null; } + + return null; } // MA Indicator class diff --git a/src/api/dashboard/static/js/indicators/rsi.js b/src/api/dashboard/static/js/indicators/rsi.js index 3f42337..a67a1d3 100644 --- a/src/api/dashboard/static/js/indicators/rsi.js +++ b/src/api/dashboard/static/js/indicators/rsi.js @@ -38,42 +38,30 @@ function calculateRSISignal(indicator, lastCandle, prevCandle, values, prevValue const overbought = indicator.params?.overbought || 70; const oversold = indicator.params?.oversold || 30; - if (!rsi || rsi === null) { + if (rsi === undefined || rsi === null || prevRsi === undefined || prevRsi === null) { return null; } - let signalType, strength, reasoning; - - // BUY when RSI crosses UP through oversold band (bottom band) - // RSI was below oversold, now above oversold - if (prevRsi !== undefined && prevRsi !== null && prevRsi < oversold && rsi >= oversold) { - signalType = SIGNAL_TYPES.BUY; - strength = Math.min(50 + (rsi - oversold) * 2, 100); - reasoning = `RSI (${rsi.toFixed(2)}) crossed up through oversold level (${oversold})`; + // BUY when RSI crosses UP through oversold level + if (prevRsi < oversold && rsi >= oversold) { + return { + type: SIGNAL_TYPES.BUY, + strength: 75, + value: rsi, + reasoning: `RSI crossed UP through oversold level (${oversold})` + }; } - // SELL when RSI crosses DOWN through overbought band (top band) - // RSI was above overbought, now below overbought - else if (prevRsi !== undefined && prevRsi !== null && prevRsi > overbought && rsi <= overbought) { - signalType = SIGNAL_TYPES.SELL; - strength = Math.min(50 + (overbought - rsi) * 2, 100); - reasoning = `RSI (${rsi.toFixed(2)}) crossed down through overbought level (${overbought})`; - } - // When RSI is in oversold territory but no crossover - strong BUY - else if (rsi < oversold) { - signalType = SIGNAL_TYPES.BUY; - strength = Math.min(40 + (oversold - rsi) * 1.5, 80); - reasoning = `RSI (${rsi.toFixed(2)}) is oversold (<${oversold})`; - } - // When RSI is in overbought territory but no crossover - strong SELL - else if (rsi > overbought) { - signalType = SIGNAL_TYPES.SELL; - strength = Math.min(40 + (rsi - overbought) * 1.5, 80); - reasoning = `RSI (${rsi.toFixed(2)}) is overbought (>${overbought})`; - } else { - return null; + // SELL when RSI crosses DOWN through overbought level + else if (prevRsi > overbought && rsi <= overbought) { + return { + type: SIGNAL_TYPES.SELL, + strength: 75, + value: rsi, + reasoning: `RSI crossed DOWN through overbought level (${overbought})` + }; } - return { type: signalType, strength, value: rsi, reasoning }; + return null; } // RSI Indicator class diff --git a/src/api/dashboard/static/js/indicators/stoch.js b/src/api/dashboard/static/js/indicators/stoch.js index ca15fe1..81ad0ef 100644 --- a/src/api/dashboard/static/js/indicators/stoch.js +++ b/src/api/dashboard/static/js/indicators/stoch.js @@ -32,33 +32,38 @@ class BaseIndicator { } // Signal calculation for Stochastic -function calculateStochSignal(indicator, lastCandle, prevCandle, values) { +function calculateStochSignal(indicator, lastCandle, prevCandle, values, prevValues) { const k = values?.k; const d = values?.d; + const prevK = prevValues?.k; + const prevD = prevValues?.d; const overbought = indicator.params?.overbought || 80; const oversold = indicator.params?.oversold || 20; - if (!k || !d) { + if (k === undefined || d === undefined || prevK === undefined || prevD === undefined) { return null; } - if (k < oversold && d < oversold) { + // BUY: %K crosses UP through %D while both are oversold + if (prevK <= prevD && k > d && k < oversold) { return { type: SIGNAL_TYPES.BUY, - strength: Math.min(50 + (oversold - k) * 2, 100), + strength: 80, value: k, - reasoning: `Stochastic %K (${k.toFixed(2)}) and %D (${d.toFixed(2)}) oversold (<${oversold})` + reasoning: `Stochastic %K crossed UP through %D in oversold zone` }; - } else if (k > overbought && d > overbought) { + } + // SELL: %K crosses DOWN through %D while both are overbought + else if (prevK >= prevD && k < d && k > overbought) { return { type: SIGNAL_TYPES.SELL, - strength: Math.min(50 + (k - overbought) * 2, 100), + strength: 80, value: k, - reasoning: `Stochastic %K (${k.toFixed(2)}) and %D (${d.toFixed(2)}) overbought (>${overbought})` + reasoning: `Stochastic %K crossed DOWN through %D in overbought zone` }; - } else { - return null; } + + return null; } // Stochastic Oscillator Indicator class diff --git a/src/api/dashboard/static/js/ui/chart.js b/src/api/dashboard/static/js/ui/chart.js index 1c3defd..2f395c8 100644 --- a/src/api/dashboard/static/js/ui/chart.js +++ b/src/api/dashboard/static/js/ui/chart.js @@ -21,9 +21,20 @@ constructor() { this.indicatorSignals = []; this.summarySignal = null; this.lastCandleTimestamp = null; + this.simulationMarkers = []; this.init(); } + + setSimulationMarkers(markers) { + this.simulationMarkers = markers || []; + this.updateSignalMarkers(); + } + + clearSimulationMarkers() { + this.simulationMarkers = []; + this.updateSignalMarkers(); + } init() { this.createTimeframeButtons(); @@ -584,11 +595,18 @@ async loadSignals() { } } -updateSignalMarkers() { + updateSignalMarkers() { const candles = this.allData.get(this.currentInterval); if (!candles || candles.length === 0) return; - const markers = calculateSignalMarkers(candles); + let markers = calculateSignalMarkers(candles); + + // Merge simulation markers if present + if (this.simulationMarkers && this.simulationMarkers.length > 0) { + markers = [...markers, ...this.simulationMarkers]; + // Re-sort combined markers by time + markers.sort((a, b) => a.time - b.time); + } // If we have a marker controller, update markers through it if (this.markerController) { diff --git a/src/api/dashboard/static/js/ui/sidebar.js b/src/api/dashboard/static/js/ui/sidebar.js index 54434ac..c5f2b7c 100644 --- a/src/api/dashboard/static/js/ui/sidebar.js +++ b/src/api/dashboard/static/js/ui/sidebar.js @@ -54,6 +54,12 @@ export function switchTab(tabId) { window.drawIndicatorsOnChart(); } }, 50); + } else if (tabId === 'strategy') { + setTimeout(() => { + if (window.renderStrategyPanel) { + window.renderStrategyPanel(); + } + }, 50); } } diff --git a/src/api/dashboard/static/js/ui/strategy-panel.js b/src/api/dashboard/static/js/ui/strategy-panel.js new file mode 100644 index 0000000..8c5a6d1 --- /dev/null +++ b/src/api/dashboard/static/js/ui/strategy-panel.js @@ -0,0 +1,422 @@ +import { IndicatorRegistry, getSignalFunction } from '../indicators/index.js'; + +let activeIndicators = []; +let simulationResults = null; +let equitySeries = null; +let equityChart = null; + +export function initStrategyPanel() { + window.renderStrategyPanel = renderStrategyPanel; + renderStrategyPanel(); + + // Listen for indicator changes to update the signal selection list + const originalAddIndicator = window.addIndicator; + window.addIndicator = function(...args) { + const res = originalAddIndicator.apply(this, args); + setTimeout(renderStrategyPanel, 100); + return res; + }; + + const originalRemoveIndicator = window.removeIndicatorById; + window.removeIndicatorById = function(...args) { + const res = originalRemoveIndicator.apply(this, args); + setTimeout(renderStrategyPanel, 100); + return res; + }; +} + +export function renderStrategyPanel() { + const container = document.getElementById('strategyPanel'); + if (!container) return; + + activeIndicators = window.getActiveIndicators?.() || []; + + container.innerHTML = ` + + + + `; + + document.getElementById('runSimulationBtn').addEventListener('click', runSimulation); +} + +function renderIndicatorChecklist(prefix) { + if (activeIndicators.length === 0) { + return '
No active indicators on chart
'; + } + + return activeIndicators.map(ind => ` + + `).join(''); +} + +async function runSimulation() { + const btn = document.getElementById('runSimulationBtn'); + btn.disabled = true; + btn.textContent = 'Simulating...'; + + try { + const config = { + startDate: new Date(document.getElementById('simStartDate').value).getTime() / 1000, + direction: document.getElementById('simDirection').value, + capital: parseFloat(document.getElementById('simCapital').value), + leverage: parseFloat(document.getElementById('simLeverage').value), + posSize: parseFloat(document.getElementById('simPosSize').value), + tp: parseFloat(document.getElementById('simTP').value) / 100, + openIndicators: Array.from(document.querySelectorAll('.sim-open-check:checked')).map(el => el.dataset.id), + closeIndicators: Array.from(document.querySelectorAll('.sim-close-check:checked')).map(el => el.dataset.id) + }; + + if (config.openIndicators.length === 0) { + alert('Please choose at least one indicator for opening positions.'); + return; + } + + const candles = window.dashboard?.allData?.get(window.dashboard?.currentInterval); + if (!candles || candles.length === 0) { + alert('No candle data available.'); + return; + } + + // Filter candles by start date + const simCandles = candles.filter(c => c.time >= config.startDate); + if (simCandles.length === 0) { + alert('No data available for the selected start date.'); + return; + } + + // Calculate all indicator values and signals for the sim period + const indicatorSignals = {}; // { indicatorId: [signals per candle] } + + for (const indId of [...new Set([...config.openIndicators, ...config.closeIndicators])]) { + const ind = activeIndicators.find(a => a.id === indId); + const IndicatorClass = IndicatorRegistry[ind.type]; + const signalFunc = getSignalFunction(ind.type); + + if (IndicatorClass && signalFunc) { + const instance = new IndicatorClass(ind); + const results = instance.calculate(candles); // Calculate on FULL history for correctness + + // Map full history results to simCandles indices + const simSignals = simCandles.map(candle => { + const idx = candles.findIndex(c => c.time === candle.time); + if (idx < 1) return null; + + const res = results[idx]; + const prevRes = results[idx-1]; + const values = typeof res === 'object' ? res : { ma: res }; + const prevValues = typeof prevRes === 'object' ? prevRes : { ma: prevRes }; + + return signalFunc(ind, candles[idx], candles[idx-1], values, prevValues); + }); + + indicatorSignals[indId] = simSignals; + } + } + + // Simulation loop + let balance = config.capital; + let equity = [{ time: simCandles[0].time, value: balance }]; + let positions = []; // { entryPrice, size, type, entryTime } + let trades = []; // { type, entryTime, exitTime, entryPrice, exitPrice, pnl, result } + + for (let i = 0; i < simCandles.length; i++) { + const candle = simCandles[i]; + const price = candle.close; + + // 1. Check TP for existing positions + for (let j = positions.length - 1; j >= 0; j--) { + const pos = positions[j]; + let isClosed = false; + let exitPrice = price; + let reason = ''; + + // TP Logic + if (pos.type === 'long') { + if (candle.high >= pos.entryPrice * (1 + config.tp)) { + isClosed = true; + exitPrice = pos.entryPrice * (1 + config.tp); + reason = 'TP'; + } + } else { + if (candle.low <= pos.entryPrice * (1 - config.tp)) { + isClosed = true; + exitPrice = pos.entryPrice * (1 - config.tp); + reason = 'TP'; + } + } + + // Close Signal Logic + if (!isClosed && config.closeIndicators.length > 0) { + const hasCloseSignal = config.closeIndicators.some(id => { + const sig = indicatorSignals[id][i]; + if (!sig) return false; + + // Short: logic is inverted + if (config.direction === 'long') { + return sig.type === 'sell'; // Sell signal closes long + } else { + return sig.type === 'buy'; // Buy signal closes short + } + }); + + if (hasCloseSignal) { + isClosed = true; + reason = 'Signal'; + } + } + + if (isClosed) { + const pnl = pos.type === 'long' + ? (exitPrice - pos.entryPrice) / pos.entryPrice * pos.size * config.leverage + : (pos.entryPrice - exitPrice) / pos.entryPrice * pos.size * config.leverage; + + balance += pnl; + trades.push({ + type: pos.type, + entryTime: pos.entryTime, + exitTime: candle.time, + entryPrice: pos.entryPrice, + exitPrice: exitPrice, + pnl: pnl, + reason: reason + }); + positions.splice(j, 1); + } + } + + // 2. Check Open Signals + const hasOpenSignal = config.openIndicators.some(id => { + const sig = indicatorSignals[id][i]; + if (!sig) return false; + + if (config.direction === 'long') { + return sig.type === 'buy'; + } else { + return sig.type === 'sell'; + } + }); + + // Ping-Pong Mode: Only 1 active position allowed + // Accumulation Mode (no close indicators): Multiple positions allowed + const isAccumulation = config.closeIndicators.length === 0; + const canOpen = isAccumulation || positions.length === 0; + + if (hasOpenSignal && canOpen && balance >= config.posSize) { + positions.push({ + type: config.direction, + entryPrice: price, + size: config.posSize, + entryTime: candle.time + }); + } + + equity.push({ time: candle.time, value: balance }); + } + + displayResults(trades, equity, config); + + } catch (error) { + console.error('[Simulation] Error:', error); + alert('Simulation failed. See console for details.'); + } finally { + btn.disabled = false; + btn.textContent = 'Run Simulation'; + } +} + +function displayResults(trades, equity, config) { + const resultsDiv = document.getElementById('simulationResults'); + resultsDiv.style.display = 'block'; + + const totalTrades = trades.length; + const profitableTrades = trades.filter(t => t.pnl > 0).length; + const winRate = totalTrades > 0 ? (profitableTrades / totalTrades * 100).toFixed(1) : 0; + const totalPnl = trades.reduce((sum, t) => sum + t.pnl, 0); + const finalBalance = config.capital + totalPnl; + const roi = (totalPnl / config.capital * 100).toFixed(2); + + resultsDiv.innerHTML = ` + + `; + + // Create Equity Chart + setTimeout(() => { + const chartContainer = document.getElementById('equityChart'); + if (!chartContainer) return; + + equityChart = LightweightCharts.createChart(chartContainer, { + layout: { background: { color: '#131722' }, textColor: '#d1d4dc' }, + grid: { vertLines: { visible: false }, horzLines: { color: '#2a2e39' } }, + rightPriceScale: { borderColor: '#2a2e39' }, + timeScale: { borderColor: '#2a2e39', visible: false }, + handleScroll: false, + handleScale: false + }); + + equitySeries = equityChart.addSeries(LightweightCharts.AreaSeries, { + lineColor: totalPnl >= 0 ? '#26a69a' : '#ef5350', + topColor: totalPnl >= 0 ? 'rgba(38, 166, 154, 0.4)' : 'rgba(239, 83, 80, 0.4)', + bottomColor: 'rgba(0, 0, 0, 0)', + lineWidth: 2, + }); + + equitySeries.setData(equity); + equityChart.timeScale().fitContent(); + }, 100); + + document.getElementById('toggleTradeMarkers').addEventListener('click', () => { + toggleSimulationMarkers(trades); + }); + + document.getElementById('clearSim').addEventListener('click', () => { + resultsDiv.style.display = 'none'; + clearSimulationMarkers(); + }); +} + +let tradeMarkers = []; + +function toggleSimulationMarkers(trades) { + if (tradeMarkers.length > 0) { + clearSimulationMarkers(); + document.getElementById('toggleTradeMarkers').textContent = 'Show Markers'; + return; + } + + const markers = []; + trades.forEach(t => { + // Entry marker + markers.push({ + time: t.entryTime, + position: t.type === 'long' ? 'belowBar' : 'aboveBar', + color: t.type === 'long' ? '#2962ff' : '#9c27b0', + shape: t.type === 'long' ? 'arrowUp' : 'arrowDown', + text: `Entry ${t.type.toUpperCase()}` + }); + + // Exit marker + markers.push({ + time: t.exitTime, + position: t.type === 'long' ? 'aboveBar' : 'belowBar', + color: t.pnl >= 0 ? '#26a69a' : '#ef5350', + shape: t.type === 'long' ? 'arrowDown' : 'arrowUp', + text: `Exit ${t.reason} ($${t.pnl.toFixed(2)})` + }); + }); + + // Sort markers by time + markers.sort((a, b) => a.time - b.time); + + if (window.dashboard) { + window.dashboard.setSimulationMarkers(markers); + tradeMarkers = markers; + document.getElementById('toggleTradeMarkers').textContent = 'Hide Markers'; + } +} + +function clearSimulationMarkers() { + if (window.dashboard) { + window.dashboard.clearSimulationMarkers(); + tradeMarkers = []; + } +} + +window.clearSimulationResults = function() { + const resultsDiv = document.getElementById('simulationResults'); + if (resultsDiv) resultsDiv.style.display = 'none'; + clearSimulationMarkers(); +};