diff --git a/src/api/dashboard/static/js/indicators/hurst.js b/src/api/dashboard/static/js/indicators/hurst.js index 7fe7c9a..a582b93 100644 --- a/src/api/dashboard/static/js/indicators/hurst.js +++ b/src/api/dashboard/static/js/indicators/hurst.js @@ -70,21 +70,21 @@ function calculateHurstSignal(indicator, lastCandle, prevCandle, values, prevVal return null; } - // BUY: Price crosses DOWN through lower Hurst Band + // BUY: Price crosses DOWN through lower Hurst Band (dip entry) if (prevClose > prevLower && close <= lower) { return { type: 'buy', - strength: 75, + strength: 80, value: close, reasoning: `Price crossed DOWN through lower Hurst Band` }; } - // SELL: Price crosses DOWN through upper Hurst Band (reversal from top) + // SELL: Price crosses DOWN through upper Hurst Band (reversal entry) if (prevClose > prevUpper && close <= upper) { return { type: 'sell', - strength: 75, + strength: 80, value: close, reasoning: `Price crossed DOWN through upper Hurst Band` }; diff --git a/src/api/dashboard/static/js/ui/chart.js b/src/api/dashboard/static/js/ui/chart.js index 2f395c8..e4b8cde 100644 --- a/src/api/dashboard/static/js/ui/chart.js +++ b/src/api/dashboard/static/js/ui/chart.js @@ -604,10 +604,14 @@ async loadSignals() { // 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); } + // CRITICAL: Filter out any markers with invalid timestamps before passing to chart + markers = markers.filter(m => m && m.time !== null && m.time !== undefined && !isNaN(m.time)); + + // 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) { try { diff --git a/src/api/dashboard/static/js/ui/strategy-panel.js b/src/api/dashboard/static/js/ui/strategy-panel.js index 8c5a6d1..fa48af6 100644 --- a/src/api/dashboard/static/js/ui/strategy-panel.js +++ b/src/api/dashboard/static/js/ui/strategy-panel.js @@ -149,128 +149,141 @@ async function runSimulation() { 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); + const results = ind.cachedResults; // Use already calculated results from chart - 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 + if (results && signalFunc) { + // Map 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 }; + + // Standardize result format (some indicators return objects, some return single values) + const values = typeof res === 'object' && res !== null ? res : { ma: res }; + const prevValues = typeof prevRes === 'object' && prevRes !== null ? prevRes : { ma: prevRes }; return signalFunc(ind, candles[idx], candles[idx-1], values, prevValues); }); indicatorSignals[indId] = simSignals; + } else { + console.warn(`[Simulation] Missing cached data or signal function for ${indId}`); } } // 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 } + let totalSize = 0; // Total position size in USD + let avgPrice = 0; + let trades = []; // { type, entryTime, exitTime, entryPrice, exitPrice, pnl, reason } + const PARTIAL_EXIT_PCT = 0.15; // 15% partial exit + for (let i = 0; i < simCandles.length; i++) { const candle = simCandles[i]; const price = candle.close; + let actionTakenInThisCandle = false; - // 1. Check TP for existing positions - for (let j = positions.length - 1; j >= 0; j--) { - const pos = positions[j]; - let isClosed = false; + // 1. Check TP for total position + if (totalSize > 0) { + let isTP = 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'; + if (config.direction === 'long') { + if (candle.high >= avgPrice * (1 + config.tp)) { + isTP = true; + exitPrice = avgPrice * (1 + config.tp); } } else { - if (candle.low <= pos.entryPrice * (1 - config.tp)) { - isClosed = true; - exitPrice = pos.entryPrice * (1 - config.tp); - reason = 'TP'; + if (candle.low <= avgPrice * (1 - config.tp)) { + isTP = true; + exitPrice = avgPrice * (1 - config.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; + if (isTP) { + const amountToClose = totalSize * PARTIAL_EXIT_PCT; + const pnl = config.direction === 'long' + ? (exitPrice - avgPrice) / avgPrice * amountToClose * config.leverage + : (avgPrice - exitPrice) / avgPrice * amountToClose * config.leverage; balance += pnl; trades.push({ - type: pos.type, - entryTime: pos.entryTime, - exitTime: candle.time, - entryPrice: pos.entryPrice, + type: config.direction, + recordType: 'exit', + time: candle.time, + entryPrice: avgPrice, exitPrice: exitPrice, pnl: pnl, - reason: reason + reason: 'TP (Partial)' }); - positions.splice(j, 1); + totalSize -= amountToClose; + actionTakenInThisCandle = true; } } - - // 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 + // 2. Check Close Signals (Partial Exit) - Only if TP didn't trigger + if (!actionTakenInThisCandle && totalSize > 0 && config.closeIndicators.length > 0) { + const hasCloseSignal = config.closeIndicators.some(id => { + const sig = indicatorSignals[id][i]; + if (!sig) return false; + return config.direction === 'long' ? sig.type === 'sell' : sig.type === 'buy'; }); + + if (hasCloseSignal) { + const amountToClose = totalSize * PARTIAL_EXIT_PCT; + const pnl = config.direction === 'long' + ? (price - avgPrice) / avgPrice * amountToClose * config.leverage + : (avgPrice - price) / avgPrice * amountToClose * config.leverage; + + balance += pnl; + trades.push({ + type: config.direction, + recordType: 'exit', + time: candle.time, + entryPrice: avgPrice, + exitPrice: price, + pnl: pnl, + reason: 'Signal (Partial)' + }); + totalSize -= amountToClose; + actionTakenInThisCandle = true; + } } - equity.push({ time: candle.time, value: balance }); + // 3. Check Open Signals (Pyramiding) - Only if no exit happened in this candle + if (!actionTakenInThisCandle) { + const hasOpenSignal = config.openIndicators.some(id => { + const sig = indicatorSignals[id][i]; + if (!sig) return false; + return config.direction === 'long' ? sig.type === 'buy' : sig.type === 'sell'; + }); + + if (hasOpenSignal && balance >= config.posSize) { + const newSize = totalSize + config.posSize; + avgPrice = ((totalSize * avgPrice) + (config.posSize * price)) / newSize; + totalSize = newSize; + + trades.push({ + type: config.direction, + recordType: 'entry', + time: candle.time, + entryPrice: price, + reason: 'Entry' + }); + } + } + + // Calculate current equity (realized balance + unrealized PnL) + const unrealizedPnl = totalSize > 0 + ? (config.direction === 'long' ? (price - avgPrice) / avgPrice : (avgPrice - price) / avgPrice) * totalSize * config.leverage + : 0; + + equity.push({ time: candle.time, value: balance + unrealizedPnl }); } displayResults(trades, equity, config); @@ -380,22 +393,26 @@ function toggleSimulationMarkers(trades) { 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()}` - }); + if (t.recordType === 'entry') { + markers.push({ + time: t.time, + 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)})` - }); + if (t.recordType === 'exit') { + markers.push({ + time: t.time, + 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