diff --git a/src/api/dashboard/static/js/indicators/hurst.js b/src/api/dashboard/static/js/indicators/hurst.js index a582b93..b37c0f4 100644 --- a/src/api/dashboard/static/js/indicators/hurst.js +++ b/src/api/dashboard/static/js/indicators/hurst.js @@ -2,6 +2,8 @@ // Based on J.M. Hurst's cyclic price channel theory // Using RMA + ATR displacement method +import { INTERVALS } from '../core/constants.js'; + const SIGNAL_TYPES = { BUY: 'buy', SELL: 'sell' @@ -14,47 +16,61 @@ const SIGNAL_COLORS = { class BaseIndicator { constructor(config) { + this.config = config; this.id = config.id; this.type = config.type; this.name = config.name; this.params = config.params || {}; - this.timeframe = config.timeframe || '1m'; + this.timeframe = config.timeframe || 'chart'; this.series = []; this.visible = config.visible !== false; - this.cachedResults = null; - this.cachedMeta = null; - this.lastSignalTimestamp = null; - this.lastSignalType = null; + + if (config.cachedResults === undefined) config.cachedResults = null; + if (config.cachedMeta === undefined) config.cachedMeta = null; + if (config.cachedTimeframe === undefined) config.cachedTimeframe = null; + if (config.isFetching === undefined) config.isFetching = false; + if (config.lastProcessedTime === undefined) config.lastProcessedTime = 0; } + + get cachedResults() { return this.config.cachedResults; } + set cachedResults(v) { this.config.cachedResults = v; } + get cachedMeta() { return this.config.cachedMeta; } + set cachedMeta(v) { this.config.cachedMeta = v; } + get cachedTimeframe() { return this.config.cachedTimeframe; } + set cachedTimeframe(v) { this.config.cachedTimeframe = v; } + get isFetching() { return this.config.isFetching; } + set isFetching(v) { this.config.isFetching = v; } + get lastProcessedTime() { return this.config.lastProcessedTime; } + set lastProcessedTime(v) { this.config.lastProcessedTime = v; } } -// Calculate RMA (Rolling Moving Average - Wilder's method) -// Recreates Pine Script's ta.rma() exactly +// Optimized RMA that can start from a previous state +function calculateRMAIncremental(sourceValue, prevRMA, length) { + if (prevRMA === null || isNaN(prevRMA)) return sourceValue; + const alpha = 1 / length; + return alpha * sourceValue + (1 - alpha) * prevRMA; +} + +// Calculate RMA for a full array with stable initialization function calculateRMA(sourceArray, length) { const rma = new Array(sourceArray.length).fill(null); let sum = 0; const alpha = 1 / length; - - // PineScript implicitly rounds float lengths for SMA initialization const smaLength = Math.round(length); for (let i = 0; i < sourceArray.length; i++) { if (i < smaLength - 1) { - // Accumulate first N-1 bars sum += sourceArray[i]; } else if (i === smaLength - 1) { - // On the Nth bar, the first RMA value is the SMA sum += sourceArray[i]; rma[i] = sum / smaLength; } else { - // Subsequent bars use the RMA formula const prevRMA = rma[i - 1]; rma[i] = (prevRMA === null || isNaN(prevRMA)) - ? alpha * sourceArray[i] + ? sourceArray[i] : alpha * sourceArray[i] + (1 - alpha) * prevRMA; } } - return rma; } @@ -93,12 +109,76 @@ function calculateHurstSignal(indicator, lastCandle, prevCandle, values, prevVal return null; } +function getEffectiveTimeframe(params) { + return params.timeframe === 'chart' ? window.dashboard?.currentInterval || '1m' : params.timeframe; +} + +function intervalToSeconds(interval) { + const amount = parseInt(interval); + const unit = interval.replace(/[0-9]/g, ''); + + switch (unit) { + case 'm': return amount * 60; + case 'h': return amount * 3600; + case 'd': return amount * 86400; + case 'w': return amount * 604800; + case 'M': return amount * 2592000; + default: return 60; + } +} + +async function getCandlesForTimeframe(tf, startTime, endTime) { + const url = `/api/v1/candles?symbol=BTC&interval=${tf}&start=${startTime.toISOString()}&end=${endTime.toISOString()}&limit=5000`; + const response = await fetch(url); + if (!response.ok) { + console.error(`Failed to fetch candles for ${tf}:`, response.status, response.statusText); + return []; + } + const data = await response.json(); + // API returns newest first (desc), but indicators need oldest first (asc) + // Also convert time to numeric seconds to match targetCandles + return (data.candles || []).reverse().map(c => ({ + ...c, + time: Math.floor(new Date(c.time).getTime() / 1000) + })); +} + +/** + * Robust forward filling for MTF data. + * @param {Array} results - MTF results (e.g. 5m) + * @param {Array} targetCandles - Chart candles (e.g. 1m) + */ +function forwardFillResults(results, targetCandles) { + if (!results || results.length === 0) { + return new Array(targetCandles.length).fill(null); + } + + const filled = new Array(targetCandles.length).fill(null); + let resIdx = 0; + + for (let i = 0; i < targetCandles.length; i++) { + const targetTime = targetCandles[i].time; + + // Advance result index while next result time is <= target time + while (resIdx < results.length - 1 && results[resIdx + 1].time <= targetTime) { + resIdx++; + } + + // If the current result is valid for this target time, use it + // (result time must be <= target time) + if (results[resIdx] && results[resIdx].time <= targetTime) { + filled[i] = results[resIdx]; + } + } + + return filled; +} + export class HurstBandsIndicator extends BaseIndicator { constructor(config) { super(config); - this.lastSignalTimestamp = null; - this.lastSignalType = null; + if (!this.params.timeframe) this.params.timeframe = 'chart'; if (!this.params.markerBuyShape) this.params.markerBuyShape = 'custom'; if (!this.params.markerSellShape) this.params.markerSellShape = 'custom'; if (!this.params.markerBuyColor) this.params.markerBuyColor = '#9e9e9e'; @@ -108,59 +188,207 @@ export class HurstBandsIndicator extends BaseIndicator { } calculate(candles) { + const effectiveTf = getEffectiveTimeframe(this.params); + const lastCandle = candles[candles.length - 1]; + + // Case 1: Different timeframe (MTF) + if (effectiveTf !== window.dashboard?.currentInterval && this.params.timeframe !== 'chart') { + // If we have cached results, try to forward fill them to match the current candle count + if (this.cachedResults && this.cachedTimeframe === effectiveTf) { + // If results are stale (last result time is behind last candle time), trigger background fetch + const lastResult = this.cachedResults[this.cachedResults.length - 1]; + const needsFetch = !this.isFetching && (!lastResult || lastCandle.time > lastResult.time + (intervalToSeconds(effectiveTf) / 2)); + + if (needsFetch) { + this._fetchAndCalculateMtf(effectiveTf, candles); + } + + // If length matches exactly and params haven't changed, return + if (this.cachedResults.length === candles.length && !this.shouldRecalculate()) { + return this.cachedResults; + } + + // If length differs (e.g. new 1m candle but 5m not fetched yet), forward fill + const filled = forwardFillResults(this.cachedResults.filter(r => r !== null), candles); + this.cachedResults = filled; + return filled; + } + + // Initial fetch + if (!this.isFetching) { + this._fetchAndCalculateMtf(effectiveTf, candles); + } + return new Array(candles.length).fill(null); + } + + // Case 2: Same timeframe as chart (Incremental or Full) + // Check if we can do incremental update + if (this.cachedResults && + this.cachedResults.length > 0 && + this.cachedTimeframe === effectiveTf && + !this.shouldRecalculate() && + candles.length >= this.cachedResults.length && + candles[this.cachedResults.length - 1].time === this.cachedResults[this.cachedResults.length - 1].time) { + + // Only calculate new candles + if (candles.length > this.cachedResults.length) { + const newResults = this._calculateIncremental(candles, this.cachedResults); + this.cachedResults = newResults; + } + return this.cachedResults; + } + + // Full calculation + const results = this._calculateCore(candles); + this.cachedTimeframe = effectiveTf; + this.updateCachedMeta(this.params); + this.cachedResults = results; + return results; + } + + _calculateCore(candles) { const mcl_t = this.params.period || 30; const mcm = this.params.multiplier || 1.8; const mcl = mcl_t / 2; - - // FIX: PineScript rounds implicit floats for history references []. - // 15/2 = 7.5. Pine rounds this to 8. Math.floor gives 7. const mcl_2 = Math.round(mcl / 2); const results = new Array(candles.length).fill(null); const closes = candles.map(c => c.close); + // True Range for ATR const trArray = candles.map((d, i) => { const prevClose = i > 0 ? candles[i - 1].close : null; - const high = d.high; - const low = d.low; - - if (prevClose === null || prevClose === undefined || isNaN(prevClose)) { - return high - low; - } - return Math.max( - high - low, - Math.abs(high - prevClose), - Math.abs(low - prevClose) - ); + if (prevClose === null || isNaN(prevClose)) return d.high - d.low; + return Math.max(d.high - d.low, Math.abs(d.high - prevClose), Math.abs(d.low - prevClose)); }); const ma_mcl = calculateRMA(closes, mcl); const atr = calculateRMA(trArray, mcl); for (let i = 0; i < candles.length; i++) { - const src = closes[i]; const mcm_off = mcm * (atr[i] || 0); - const historicalIndex = i - mcl_2; const historical_ma = historicalIndex >= 0 ? ma_mcl[historicalIndex] : null; - - const centerLine = (historical_ma === null || historical_ma === undefined || isNaN(historical_ma)) ? src : historical_ma; + const centerLine = (historical_ma === null || isNaN(historical_ma)) ? closes[i] : historical_ma; results[i] = { + time: candles[i].time, upper: centerLine + mcm_off, - lower: centerLine - mcm_off + lower: centerLine - mcm_off, + ma: ma_mcl[i], // Store intermediate state for incremental updates + atr: atr[i] }; } - return results; } + + _calculateIncremental(candles, oldResults) { + const mcl_t = this.params.period || 30; + const mcm = this.params.multiplier || 1.8; + const mcl = mcl_t / 2; + const mcl_2 = Math.round(mcl / 2); + + const results = [...oldResults]; + const startIndex = oldResults.length; + + for (let i = startIndex; i < candles.length; i++) { + const close = candles[i].close; + const prevClose = candles[i-1].close; + const tr = Math.max(candles[i].high - candles[i].low, Math.abs(candles[i].high - prevClose), Math.abs(candles[i].low - prevClose)); + + const prevMA = results[i-1]?.ma; + const prevATR = results[i-1]?.atr; + + const currentMA = calculateRMAIncremental(close, prevMA, mcl); + const currentATR = calculateRMAIncremental(tr, prevATR, mcl); + + // For displaced center line, we still need the MA from i - mcl_2 + // Since i >= oldResults.length, i - mcl_2 might be in the old results + let historical_ma = null; + const historicalIndex = i - mcl_2; + if (historicalIndex >= 0) { + historical_ma = historicalIndex < startIndex ? results[historicalIndex].ma : null; // In this simple incremental, we don't look ahead + } + + const centerLine = (historical_ma === null || isNaN(historical_ma)) ? close : historical_ma; + const mcm_off = mcm * (currentATR || 0); + + results[i] = { + time: candles[i].time, + upper: centerLine + mcm_off, + lower: centerLine - mcm_off, + ma: currentMA, + atr: currentATR + }; + } + return results; + } + + async _fetchAndCalculateMtf(effectiveTf, targetCandles) { + this.isFetching = true; + try { + console.log(`[Hurst] Fetching MTF data for ${effectiveTf}...`); + const chartData = window.dashboard?.allData?.get(window.dashboard?.currentInterval) || targetCandles; + if (!chartData || chartData.length === 0) { + console.warn('[Hurst] No chart data available for timeframe fetch'); + this.isFetching = false; + return; + } + + // Calculate warmup needed (period + half width) + const mcl_t = this.params.period || 30; + const warmupBars = mcl_t * 2; // Extra buffer + const tfSeconds = intervalToSeconds(effectiveTf); + const warmupOffsetSeconds = warmupBars * tfSeconds; + + // Candles endpoint expects ISO strings or timestamps. + // chartData[0].time is the earliest candle on chart. + const startTime = new Date((chartData[0].time - warmupOffsetSeconds) * 1000); + const endTime = new Date(chartData[chartData.length - 1].time * 1000); + + const tfCandles = await getCandlesForTimeframe(effectiveTf, startTime, endTime); + if (tfCandles.length === 0) { + console.warn(`[Hurst] No candles fetched for ${effectiveTf}`); + this.isFetching = false; + return; + } + + console.log(`[Hurst] Fetched ${tfCandles.length} candles for ${effectiveTf}. Calculating...`); + const tfResults = this._calculateCore(tfCandles); + const finalResults = forwardFillResults(tfResults, targetCandles); + + // Persist results on the config object + this.cachedResults = finalResults; + this.cachedTimeframe = effectiveTf; + this.updateCachedMeta(this.params); + + console.log(`[Hurst] MTF calculation complete for ${effectiveTf}. Triggering redraw.`); + + // Trigger a redraw of the dashboard to show the new data + if (window.drawIndicatorsOnChart) { + window.drawIndicatorsOnChart(); + } + } catch (err) { + console.error('[Hurst] Error in _fetchAndCalculateMtf:', err); + } finally { + this.isFetching = false; + } + } getMetadata() { return { name: 'Hurst Bands', description: 'Cyclic price channels based on Hurst theory', inputs: [ + { + name: 'timeframe', + label: 'Timeframe', + type: 'select', + default: 'chart', + options: ['chart', ...INTERVALS], + labels: { chart: '(Main Chart)' } + }, { name: 'period', label: 'Hurst Cycle Length (mcl_t)', type: 'number', default: 30, min: 5, max: 200 }, { name: 'multiplier', label: 'Multiplier (mcm)', type: 'number', default: 1.8, min: 0.5, max: 10, step: 0.1 } ], @@ -174,6 +402,20 @@ export class HurstBandsIndicator extends BaseIndicator { displayMode: 'overlay' }; } + + shouldRecalculate() { + const effectiveTf = getEffectiveTimeframe(this.params); + return this.cachedTimeframe !== effectiveTf || + (this.cachedMeta && (this.cachedMeta.period !== this.params.period || + this.cachedMeta.multiplier !== this.params.multiplier)); + } + + updateCachedMeta(params) { + this.cachedMeta = { + period: params.period, + multiplier: params.multiplier + }; + } } export { calculateHurstSignal }; \ No newline at end of file diff --git a/src/api/dashboard/static/js/strategies/index.js b/src/api/dashboard/static/js/strategies/index.js new file mode 100644 index 0000000..a1cc6e1 --- /dev/null +++ b/src/api/dashboard/static/js/strategies/index.js @@ -0,0 +1,9 @@ +export const StrategyRegistry = {}; + +export function registerStrategy(name, strategyModule) { + StrategyRegistry[name] = strategyModule; +} + +export function getStrategy(name) { + return StrategyRegistry[name]; +} diff --git a/src/api/dashboard/static/js/strategies/ping-pong.js b/src/api/dashboard/static/js/strategies/ping-pong.js new file mode 100644 index 0000000..dbf5865 --- /dev/null +++ b/src/api/dashboard/static/js/strategies/ping-pong.js @@ -0,0 +1,612 @@ +import { getSignalFunction } from '../indicators/index.js'; + +const STORAGE_KEY = 'ping_pong_settings'; + +function getSavedSettings() { + const saved = localStorage.getItem(STORAGE_KEY); + if (!saved) return null; + try { + return JSON.parse(saved); + } catch (e) { + return null; + } +} + +export const PingPongStrategy = { + id: 'ping_pong', + name: 'Ping-Pong', + + saveSettings: function() { + const settings = { + startDate: document.getElementById('simStartDate').value, + stopDate: document.getElementById('simStopDate').value, + contractType: document.getElementById('simContractType').value, + direction: document.getElementById('simDirection').value, + autoDirection: document.getElementById('simAutoDirection').checked, + capital: document.getElementById('simCapital').value, + exchangeLeverage: document.getElementById('simExchangeLeverage').value, + maxEffectiveLeverage: document.getElementById('simMaxEffectiveLeverage').value, + posSize: document.getElementById('simPosSize').value, + tp: document.getElementById('simTP').value + }; + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + + const btn = document.getElementById('saveSimSettings'); + if (btn) { + const originalText = btn.textContent; + btn.textContent = 'Saved!'; + btn.style.color = '#26a69a'; + setTimeout(() => { + btn.textContent = originalText; + btn.style.color = ''; + }, 2000); + } + }, + + renderUI: function(activeIndicators, formatDisplayDate) { + const saved = getSavedSettings(); + + // Format initial values for display + let startDisplay = saved?.startDate || '01/01/2026 00:00'; + let stopDisplay = saved?.stopDate || ''; + + if (startDisplay.includes('T')) { + startDisplay = formatDisplayDate(new Date(startDisplay)); + } + if (stopDisplay.includes('T')) { + stopDisplay = formatDisplayDate(new Date(stopDisplay)); + } + + const renderIndicatorChecklist = (prefix) => { + if (activeIndicators.length === 0) { + return '
No active indicators on chart
'; + } + + return activeIndicators.map(ind => ` + + `).join(''); + }; + + const autoDirChecked = saved?.autoDirection === true; + const disableManualStr = autoDirChecked ? 'disabled' : ''; + + return ` +
+ + +
+ +
+ + +
+ +
+ +
+ Price > MA44: LONG (Inverse/BTC Margin)
+ Price < MA44: SHORT (Linear/USDT Margin) +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ ${renderIndicatorChecklist('open')} +
+
+ +
+ +
+ ${renderIndicatorChecklist('close')} +
+
+ `; + }, + + attachListeners: function() { + const autoCheck = document.getElementById('simAutoDirection'); + const contractSelect = document.getElementById('simContractType'); + const dirSelect = document.getElementById('simDirection'); + + if (autoCheck) { + autoCheck.addEventListener('change', (e) => { + const isAuto = e.target.checked; + contractSelect.disabled = isAuto; + dirSelect.disabled = isAuto; + }); + } + + const saveBtn = document.getElementById('saveSimSettings'); + if (saveBtn) saveBtn.addEventListener('click', this.saveSettings.bind(this)); + }, + + runSimulation: async function(activeIndicators, displayResultsCallback) { + const btn = document.getElementById('runSimulationBtn'); + btn.disabled = true; + btn.textContent = 'Preparing Data...'; + + try { + const startVal = document.getElementById('simStartDate').value; + const stopVal = document.getElementById('simStopDate').value; + + const config = { + startDate: new Date(startVal).getTime() / 1000, + stopDate: stopVal ? new Date(stopVal).getTime() / 1000 : Math.floor(Date.now() / 1000), + autoDirection: document.getElementById('simAutoDirection').checked, + contractType: document.getElementById('simContractType').value, + direction: document.getElementById('simDirection').value, + capital: parseFloat(document.getElementById('simCapital').value), + exchangeLeverage: parseFloat(document.getElementById('simExchangeLeverage').value), + maxEffectiveLeverage: parseFloat(document.getElementById('simMaxEffectiveLeverage').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 interval = window.dashboard?.currentInterval || '1d'; + + // 1. Ensure data is loaded for the range + let allCandles = window.dashboard?.allData?.get(interval) || []; + + const earliestInCache = allCandles.length > 0 ? allCandles[0].time : Infinity; + const latestInCache = allCandles.length > 0 ? allCandles[allCandles.length - 1].time : -Infinity; + + if (config.startDate < earliestInCache || config.stopDate > latestInCache) { + btn.textContent = 'Fetching from Server...'; + + let currentEndISO = new Date(config.stopDate * 1000).toISOString(); + const startISO = new Date(config.startDate * 1000).toISOString(); + let keepFetching = true; + let newCandlesAdded = false; + + while (keepFetching) { + const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${interval}&start=${startISO}&end=${currentEndISO}&limit=10000`); + const data = await response.json(); + + if (data.candles && data.candles.length > 0) { + const fetchedCandles = data.candles.reverse().map(c => ({ + time: Math.floor(new Date(c.time).getTime() / 1000), + open: parseFloat(c.open), + high: parseFloat(c.high), + low: parseFloat(c.low), + close: parseFloat(c.close), + volume: parseFloat(c.volume || 0) + })); + + allCandles = window.dashboard.mergeData(allCandles, fetchedCandles); + newCandlesAdded = true; + + // If we received 10000 candles, there might be more. We fetch again using the oldest candle's time - 1s + if (data.candles.length === 10000) { + const oldestTime = fetchedCandles[0].time; + if (oldestTime <= config.startDate) { + keepFetching = false; + } else { + currentEndISO = new Date((oldestTime - 1) * 1000).toISOString(); + btn.textContent = `Fetching older data... (${new Date(oldestTime * 1000).toLocaleDateString()})`; + } + } else { + keepFetching = false; + } + } else { + keepFetching = false; + } + } + + if (newCandlesAdded) { + window.dashboard.allData.set(interval, allCandles); + window.dashboard.candleSeries.setData(allCandles); + + btn.textContent = 'Calculating Indicators...'; + window.drawIndicatorsOnChart?.(); + await new Promise(r => setTimeout(r, 500)); + } + } + + // --- Auto-Direction: Fetch 1D candles for MA(44) --- + let dailyCandles = []; + let dailyMaMap = new Map(); // timestamp (midnight UTC) -> MA44 value + + if (config.autoDirection) { + btn.textContent = 'Fetching 1D MA(44)...'; + // Fetch 1D candles starting 45 days BEFORE the simulation start date to warm up the MA + const msPerDay = 24 * 60 * 60 * 1000; + const dailyStartISO = new Date((config.startDate * 1000) - (45 * msPerDay)).toISOString(); + const stopISO = new Date(config.stopDate * 1000).toISOString(); + + const dailyResponse = await fetch(`/api/v1/candles?symbol=BTC&interval=1d&start=${dailyStartISO}&end=${stopISO}&limit=5000`); + const dailyData = await dailyResponse.json(); + + if (dailyData.candles && dailyData.candles.length > 0) { + dailyCandles = dailyData.candles.reverse().map(c => ({ + time: Math.floor(new Date(c.time).getTime() / 1000), + close: parseFloat(c.close) + })); + + // Calculate MA(44) + const maPeriod = 44; + for (let i = maPeriod - 1; i < dailyCandles.length; i++) { + let sum = 0; + for (let j = 0; j < maPeriod; j++) { + sum += dailyCandles[i - j].close; + } + const maValue = sum / maPeriod; + // Store the MA value using the midnight UTC timestamp of that day + dailyMaMap.set(dailyCandles[i].time, maValue); + } + } else { + console.warn('[Simulation] Failed to fetch 1D candles for Auto-Direction. Falling back to manual.'); + config.autoDirection = false; + } + } + // -------------------------------------------------- + + btn.textContent = 'Simulating...'; + + // Filter candles by the exact range + const simCandles = allCandles.filter(c => c.time >= config.startDate && c.time <= config.stopDate); + + if (simCandles.length === 0) { + alert('No data available for the selected range.'); + return; + } + + // Calculate indicator signals + const indicatorSignals = {}; + for (const indId of [...new Set([...config.openIndicators, ...config.closeIndicators])]) { + const ind = activeIndicators.find(a => a.id === indId); + if (!ind) continue; + + const signalFunc = getSignalFunction(ind.type); + const results = ind.cachedResults; + + if (results && signalFunc) { + indicatorSignals[indId] = simCandles.map(candle => { + const idx = allCandles.findIndex(c => c.time === candle.time); + if (idx < 1) return null; + const values = typeof results[idx] === 'object' && results[idx] !== null ? results[idx] : { ma: results[idx] }; + const prevValues = typeof results[idx-1] === 'object' && results[idx-1] !== null ? results[idx-1] : { ma: results[idx-1] }; + return signalFunc(ind, allCandles[idx], allCandles[idx-1], values, prevValues); + }); + } + } + + // Simulation Initial State + const startPrice = simCandles[0].open; + + // We maintain a single "walletBalanceUsd" variable as the source of truth for the account size + let walletBalanceUsd = config.capital; + + // At any given time, the active margin type determines how we use this balance + // When LONG (Inverse), we theoretically buy BTC with it. + // When SHORT (Linear), we just use it as USDT. + + // Set initial state based on auto or manual + if (config.autoDirection && dailyMaMap.size > 0) { + // Find the MA value for the day before start date + const simStartDayTime = Math.floor(simCandles[0].time / 86400) * 86400; // Midnight UTC + let closestMA = Array.from(dailyMaMap.entries()) + .filter(([t]) => t <= simStartDayTime) + .sort((a,b) => b[0] - a[0])[0]; + + if (closestMA) { + const price = simCandles[0].open; + if (price > closestMA[1]) { + config.direction = 'long'; + config.contractType = 'inverse'; + } else { + config.direction = 'short'; + config.contractType = 'linear'; + } + } + } + + let equityData = { usd: [], btc: [] }; + let totalQty = 0; // Linear: BTC Contracts, Inverse: USD Contracts + let avgPrice = 0; + let avgPriceData = []; + let posSizeData = { btc: [], usd: [] }; + let trades = []; + + let currentDayStart = Math.floor(simCandles[0].time / 86400) * 86400; + + const PARTIAL_EXIT_PCT = 0.15; + const MIN_POSITION_VALUE_USD = 15; + + for (let i = 0; i < simCandles.length; i++) { + const candle = simCandles[i]; + const price = candle.close; + let actionTakenInThisCandle = false; + + // --- Auto-Direction Daily Check (Midnight UTC) --- + if (config.autoDirection) { + const candleDayStart = Math.floor(candle.time / 86400) * 86400; + if (candleDayStart > currentDayStart) { + currentDayStart = candleDayStart; + // It's a new day! Get yesterday's MA(44) + let closestMA = Array.from(dailyMaMap.entries()) + .filter(([t]) => t < currentDayStart) + .sort((a,b) => b[0] - a[0])[0]; + + if (closestMA) { + const maValue = closestMA[1]; + let newDirection = config.direction; + let newContractType = config.contractType; + + if (candle.open > maValue) { + newDirection = 'long'; + newContractType = 'inverse'; + } else { + newDirection = 'short'; + newContractType = 'linear'; + } + + // Did the trend flip? + if (newDirection !== config.direction) { + // Force close open position at candle.open (market open) + if (totalQty > 0) { + let pnlUsd = 0; + if (config.contractType === 'linear') { + pnlUsd = config.direction === 'long' ? (candle.open - avgPrice) * totalQty : (avgPrice - candle.open) * totalQty; + walletBalanceUsd += pnlUsd; + } else { // inverse + // PnL in BTC, converted back to USD + const pnlBtc = config.direction === 'long' + ? totalQty * (1/avgPrice - 1/candle.open) + : totalQty * (1/candle.open - 1/avgPrice); + // Inverse margin is BTC, so balance was in BTC. + // But we maintain walletBalanceUsd, so we just add the USD value of the PNL + pnlUsd = pnlBtc * candle.open; + walletBalanceUsd += pnlUsd; + } + + trades.push({ + type: config.direction, recordType: 'exit', time: candle.time, + entryPrice: avgPrice, exitPrice: candle.open, pnl: pnlUsd, reason: 'Force Close (Trend Flip)', + currentUsd: 0, currentQty: 0 + }); + totalQty = 0; + avgPrice = 0; + } + + // Apply flip + config.direction = newDirection; + config.contractType = newContractType; + } + } + } + } + // ------------------------------------------------ + + // 1. Check TP + if (totalQty > 0) { + let isTP = false; + let exitPrice = price; + if (config.direction === 'long') { + if (candle.high >= avgPrice * (1 + config.tp)) { + isTP = true; + exitPrice = avgPrice * (1 + config.tp); + } + } else { + if (candle.low <= avgPrice * (1 - config.tp)) { + isTP = true; + exitPrice = avgPrice * (1 - config.tp); + } + } + + if (isTP) { + let qtyToClose = totalQty * PARTIAL_EXIT_PCT; + let remainingQty = totalQty - qtyToClose; + let remainingValueUsd = config.contractType === 'linear' ? remainingQty * exitPrice : remainingQty; + let reason = 'TP (Partial)'; + + if (remainingValueUsd < MIN_POSITION_VALUE_USD) { + qtyToClose = totalQty; + reason = 'TP (Full - Min Size)'; + } + + let pnlUsd; + if (config.contractType === 'linear') { + pnlUsd = config.direction === 'long' ? (exitPrice - avgPrice) * qtyToClose : (avgPrice - exitPrice) * qtyToClose; + walletBalanceUsd += pnlUsd; + } else { + const pnlBtc = config.direction === 'long' + ? qtyToClose * (1/avgPrice - 1/exitPrice) + : qtyToClose * (1/exitPrice - 1/avgPrice); + pnlUsd = pnlBtc * exitPrice; + walletBalanceUsd += pnlUsd; + } + + totalQty -= qtyToClose; + trades.push({ + type: config.direction, recordType: 'exit', time: candle.time, + entryPrice: avgPrice, exitPrice: exitPrice, pnl: pnlUsd, reason: reason, + currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty, + currentQty: config.contractType === 'linear' ? totalQty : totalQty / price + }); + actionTakenInThisCandle = true; + } + } + + // 2. Check Close Signals + if (!actionTakenInThisCandle && totalQty > 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) { + let qtyToClose = totalQty * PARTIAL_EXIT_PCT; + let remainingQty = totalQty - qtyToClose; + let remainingValueUsd = config.contractType === 'linear' ? remainingQty * price : remainingQty; + let reason = 'Signal (Partial)'; + + if (remainingValueUsd < MIN_POSITION_VALUE_USD) { + qtyToClose = totalQty; + reason = 'Signal (Full - Min Size)'; + } + + let pnlUsd; + if (config.contractType === 'linear') { + pnlUsd = config.direction === 'long' ? (price - avgPrice) * qtyToClose : (avgPrice - price) * qtyToClose; + walletBalanceUsd += pnlUsd; + } else { + const pnlBtc = config.direction === 'long' + ? qtyToClose * (1/avgPrice - 1/price) + : qtyToClose * (1/price - 1/avgPrice); + pnlUsd = pnlBtc * price; + walletBalanceUsd += pnlUsd; + } + + totalQty -= qtyToClose; + trades.push({ + type: config.direction, recordType: 'exit', time: candle.time, + entryPrice: avgPrice, exitPrice: price, pnl: pnlUsd, reason: reason, + currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty, + currentQty: config.contractType === 'linear' ? totalQty : totalQty / price + }); + actionTakenInThisCandle = true; + } + } + + // Calculate Current Equity for Margin Check + let currentEquityUsd = walletBalanceUsd; + if (totalQty > 0) { + if (config.contractType === 'linear') { + currentEquityUsd += config.direction === 'long' ? (price - avgPrice) * totalQty : (avgPrice - price) * totalQty; + } else { + const upnlBtc = config.direction === 'long' ? totalQty * (1/avgPrice - 1/price) : totalQty * (1/price - 1/avgPrice); + currentEquityUsd += (upnlBtc * price); + } + } + + // 3. Check Open Signals + 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) { + const entryValUsd = config.posSize * config.exchangeLeverage; + const currentNotionalUsd = config.contractType === 'linear' ? totalQty * price : totalQty; + + const projectedEffectiveLeverage = (currentNotionalUsd + entryValUsd) / Math.max(currentEquityUsd, 0.0000001); + + if (projectedEffectiveLeverage <= config.maxEffectiveLeverage) { + if (config.contractType === 'linear') { + const entryQty = entryValUsd / price; + avgPrice = ((totalQty * avgPrice) + (entryQty * price)) / (totalQty + entryQty); + totalQty += entryQty; + } else { + avgPrice = (totalQty + entryValUsd) / ((totalQty / avgPrice || 0) + (entryValUsd / price)); + totalQty += entryValUsd; + } + + trades.push({ + type: config.direction, recordType: 'entry', time: candle.time, + entryPrice: price, reason: 'Entry', + currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty, + currentQty: config.contractType === 'linear' ? totalQty : totalQty / price + }); + } + } + } + + // Final Equity Recording + let finalEquityUsd = walletBalanceUsd; + if (totalQty > 0) { + if (config.contractType === 'linear') { + finalEquityUsd += config.direction === 'long' ? (price - avgPrice) * totalQty : (avgPrice - price) * totalQty; + } else { + const upnlBtc = config.direction === 'long' ? totalQty * (1/avgPrice - 1/price) : totalQty * (1/price - 1/avgPrice); + finalEquityUsd += (upnlBtc * price); + } + } + let finalEquityBtc = finalEquityUsd / price; + + equityData.usd.push({ time: candle.time, value: finalEquityUsd }); + equityData.btc.push({ time: candle.time, value: finalEquityBtc }); + + if (totalQty > 0.000001) { + avgPriceData.push({ + time: candle.time, + value: avgPrice, + color: config.direction === 'long' ? '#26a69a' : '#ef5350' // Green for long, Red for short + }); + } + posSizeData.btc.push({ time: candle.time, value: config.contractType === 'linear' ? totalQty : totalQty / price }); + posSizeData.usd.push({ time: candle.time, value: config.contractType === 'linear' ? totalQty * price : totalQty }); + } + + displayResultsCallback(trades, equityData, config, simCandles[simCandles.length-1].close, avgPriceData, posSizeData); + + } catch (error) { + console.error('[Simulation] Error:', error); + alert('Simulation failed.'); + } finally { + btn.disabled = false; + btn.textContent = 'Run Simulation'; + } + } +}; diff --git a/src/api/dashboard/static/js/ui/chart.js b/src/api/dashboard/static/js/ui/chart.js index 5e5e14a..7ac055d 100644 --- a/src/api/dashboard/static/js/ui/chart.js +++ b/src/api/dashboard/static/js/ui/chart.js @@ -4,6 +4,134 @@ import { calculateSignalMarkers } from './signal-markers.js'; import { updateIndicatorCandles } from './indicators-panel-new.js'; import { TimezoneConfig } from '../config/timezone.js'; +export class SeriesMarkersPrimitive { + constructor(markers) { + this._markers = markers || []; + this._paneViews = [new MarkersPaneView(this)]; + } + + setMarkers(markers) { + this._markers = markers; + if (this._requestUpdate) { + this._requestUpdate(); + } + } + + attached(param) { + this._chart = param.chart; + this._series = param.series; + this._requestUpdate = param.requestUpdate; + this._requestUpdate(); + } + + detached() { + this._chart = undefined; + this._series = undefined; + this._requestUpdate = undefined; + } + + updateAllViews() { + this._requestUpdate?.(); + } + + paneViews() { + return this._paneViews; + } +} + +class MarkersPaneView { + constructor(source) { + this._source = source; + } + + renderer() { + return new MarkersRenderer(this._source); + } + + zOrder() { + return 'top'; + } +} + +class MarkersRenderer { + constructor(source) { + this._source = source; + } + + draw(target) { + if (!this._source._chart || !this._source._series) return; + + target.useBitmapCoordinateSpace((scope) => { + const ctx = scope.context; + const series = this._source._series; + const chart = this._source._chart; + const markers = this._source._markers; + + // Adjust coordinates to bitmap space based on pixel ratio + const ratio = scope.horizontalPixelRatio; + + ctx.save(); + + for (const marker of markers) { + const timeCoordinate = chart.timeScale().timeToCoordinate(marker.time); + if (timeCoordinate === null) continue; + + // Figure out price coordinate + let price = marker.price || marker.value; + + // If price wasn't specified but we have the series data, grab the candle high/low + if (!price && window.dashboard && window.dashboard.allData) { + const data = window.dashboard.allData.get(window.dashboard.currentInterval); + if (data) { + const candle = data.find(d => d.time === marker.time); + if (candle) { + price = marker.position === 'aboveBar' ? candle.high : candle.low; + } + } + } + + if (!price) continue; + + const priceCoordinate = series.priceToCoordinate(price); + if (priceCoordinate === null) continue; + + const x = timeCoordinate * ratio; + const size = 5 * ratio; + const margin = 15 * ratio; + const isAbove = marker.position === 'aboveBar'; + const y = (isAbove ? priceCoordinate * ratio - margin : priceCoordinate * ratio + margin); + + ctx.fillStyle = marker.color || '#26a69a'; + ctx.beginPath(); + + const shape = marker.shape || (isAbove ? 'arrowDown' : 'arrowUp'); + + if (shape === 'arrowUp' || shape === 'triangleUp') { + ctx.moveTo(x, y - size); + ctx.lineTo(x - size, y + size); + ctx.lineTo(x + size, y + size); + } else if (shape === 'arrowDown' || shape === 'triangleDown') { + ctx.moveTo(x, y + size); + ctx.lineTo(x - size, y - size); + ctx.lineTo(x + size, y - size); + } else if (shape === 'circle') { + ctx.arc(x, y, size, 0, Math.PI * 2); + } else if (shape === 'square') { + ctx.rect(x - size, y - size, size * 2, size * 2); + } else if (shape === 'custom' && marker.text) { + ctx.font = `${Math.round(14 * ratio)}px Arial`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(marker.text, x, y); + continue; + } + ctx.fill(); + } + ctx.restore(); + }); + } +} + function formatDate(timestamp) { return TimezoneConfig.formatDate(timestamp); } @@ -91,8 +219,20 @@ constructor() { setAvgPriceData(data) { if (this.avgPriceSeries) { - this.avgPriceSeries.setData(data || []); + this.chart.removeSeries(this.avgPriceSeries); } + + // Recreate series to apply custom colors per point via LineSeries data + this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, { + lineWidth: 2, + lineStyle: LightweightCharts.LineStyle.Solid, + lastValueVisible: true, + priceLineVisible: false, + crosshairMarkerVisible: false, + title: 'Avg Price', + }); + + this.avgPriceSeries.setData(data || []); } clearAvgPriceData() { @@ -509,7 +649,12 @@ async loadNewData() { this.lastCandleTimestamp = latest.time; chartData.forEach(candle => { - if (candle.time >= lastTimestamp) { + if (candle.time >= lastTimestamp && + !Number.isNaN(candle.time) && + !Number.isNaN(candle.open) && + !Number.isNaN(candle.high) && + !Number.isNaN(candle.low) && + !Number.isNaN(candle.close)) { this.candleSeries.update(candle); } }); @@ -700,78 +845,16 @@ async loadSignals() { // 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 { - this.markerController.setMarkers(markers); - return; - } catch (e) { - console.warn('[SignalMarkers] setMarkers error:', e.message); - this.markerController = null; + // Use custom primitive for markers in v5 + try { + if (!this.markerPrimitive) { + this.markerPrimitive = new SeriesMarkersPrimitive(); + this.candleSeries.attachPrimitive(this.markerPrimitive); } + this.markerPrimitive.setMarkers(markers); + } catch (e) { + console.warn('[SignalMarkers] setMarkers primitive error:', e.message); } - - // Clear price lines - if (this.markerPriceLines) { - this.markerPriceLines.forEach(ml => { - try { this.candleSeries.removePriceLine(ml); } catch (e) {} - }); - this.markerPriceLines = []; - } - - if (markers.length === 0) return; - - // Create new marker controller - if (typeof LightweightCharts.createSeriesMarkers === 'function') { - try { - this.markerController = LightweightCharts.createSeriesMarkers(this.candleSeries, markers); - return; - } catch (e) { - console.warn('[SignalMarkers] createSeriesMarkers error:', e.message); - } - } - - // Fallback: use price lines - this.addMarkerPriceLines(markers); - } - - addMarkerPriceLines(markers) { - if (this.markerPriceLines) { - this.markerPriceLines.forEach(ml => { - try { this.candleSeries.removePriceLine(ml); } catch (e) {} - }); - } - this.markerPriceLines = []; - - const recentMarkers = markers.slice(-20); - - recentMarkers.forEach(m => { - const isBuy = m.position === 'belowBar'; - const price = isBuy ? this.getMarkerLowPrice(m.time) : this.getMarkerHighPrice(m.time); - - const priceLine = this.candleSeries.createPriceLine({ - price: price, - color: m.color, - lineWidth: 2, - lineStyle: LightweightCharts.LineStyle.Dashed, - axisLabelVisible: true, - title: m.text - }); - - this.markerPriceLines.push(priceLine); - }); - } - - getMarkerLowPrice(time) { - const candles = this.allData.get(this.currentInterval); - const candle = candles?.find(c => c.time === time); - return candle ? candle.low * 0.995 : 0; - } - - getMarkerHighPrice(time) { - const candles = this.allData.get(this.currentInterval); - const candle = candles?.find(c => c.time === time); - return candle ? candle.high * 1.005 : 0; } renderTA() { @@ -954,6 +1037,9 @@ switchTimeframe(interval) { window.clearSimulationResults?.(); window.updateTimeframeDisplay?.(); + + // Notify indicators of timeframe change for recalculation + window.onTimeframeChange?.(interval); } } diff --git a/src/api/dashboard/static/js/ui/hts-visualizer.js b/src/api/dashboard/static/js/ui/hts-visualizer.js index bebc3dc..f476ec6 100644 --- a/src/api/dashboard/static/js/ui/hts-visualizer.js +++ b/src/api/dashboard/static/js/ui/hts-visualizer.js @@ -201,8 +201,20 @@ export class HTSVisualizer { } const candleSeries = this.candleData?.series; - if (candleSeries && typeof candleSeries.setMarkers === 'function') { - candleSeries.setMarkers(markers); + if (candleSeries) { + try { + if (typeof candleSeries.setMarkers === 'function') { + candleSeries.setMarkers(markers); + } else if (typeof SeriesMarkersPrimitive !== 'undefined') { + if (!this.markerPrimitive) { + this.markerPrimitive = new SeriesMarkersPrimitive(); + candleSeries.attachPrimitive(this.markerPrimitive); + } + this.markerPrimitive.setMarkers(markers); + } + } catch (e) { + console.warn('[HTS] Error setting markers:', e); + } } return markers; diff --git a/src/api/dashboard/static/js/ui/indicators-panel-new.js b/src/api/dashboard/static/js/ui/indicators-panel-new.js index c30713d..5d2038b 100644 --- a/src/api/dashboard/static/js/ui/indicators-panel-new.js +++ b/src/api/dashboard/static/js/ui/indicators-panel-new.js @@ -112,6 +112,23 @@ export function setActiveIndicators(indicators) { window.getActiveIndicators = getActiveIndicators; +async function onTimeframeChange(newInterval) { + const indicators = getActiveIndicators(); + for (const indicator of indicators) { + if (indicator.params.timeframe === 'chart' && typeof indicator.shouldRecalculate === 'function') { + if (indicator.shouldRecalculate()) { + try { + await window.renderIndicator(indicator.id); + } catch (err) { + console.error(`[onTimeframeChange] Failed to recalculate ${indicator.name}:`, err); + } + } + } + } +} + +window.onTimeframeChange = onTimeframeChange; + // Render main panel export function renderIndicatorPanel() { const container = document.getElementById('indicatorPanel'); @@ -315,7 +332,10 @@ function renderIndicatorConfig(indicator, meta) { ${input.type === 'select' ? `` : ` r !== null && r !== undefined); - const isObjectResult = firstNonNull && typeof firstNonNull === 'object'; + // Improved detection of object-based results (multiple plots) + const firstNonNull = Array.isArray(results) ? results.find(r => r !== null && r !== undefined) : null; + let isObjectResult = firstNonNull && typeof firstNonNull === 'object' && !Array.isArray(firstNonNull); + + // Fallback: If results are all null (e.g. during warmup or MTF fetch), + // use metadata to determine if it SHOULD be an object result + if (!firstNonNull && meta.plots && meta.plots.length > 1) { + isObjectResult = true; + } + // Also check if the only plot has a specific ID that isn't just a number + if (!firstNonNull && meta.plots && meta.plots.length === 1 && meta.plots[0].id !== 'value') { + isObjectResult = true; + } let plotsCreated = 0; let dataPointsAdded = 0; @@ -842,7 +877,7 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li value = results[i]; } - if (value !== null && value !== undefined) { + if (value !== null && value !== undefined && typeof value === 'number' && Number.isFinite(value)) { if (firstDataIndex === -1) { firstDataIndex = i; } @@ -1033,12 +1068,20 @@ export function drawIndicatorsOnChart() { const instance = new IndicatorClass(ind); const meta = instance.getMetadata(); - // Store calculated results and metadata for signal calculation - const results = instance.calculate(candles); - ind.cachedResults = results; +// Store calculated results and metadata for signal calculation + let results = ind.cachedResults; + if (!results || !Array.isArray(results) || results.length !== candles.length) { + try { + results = instance.calculate(candles); + ind.cachedResults = results; + } catch (err) { + console.error(`[Indicators] Failed to calculate ${ind.name}:`, err); + results = []; + } + } ind.cachedMeta = meta; - const validResults = results.filter(r => r !== null && r !== undefined); + const validResults = Array.isArray(results) ? results.filter(r => r !== null && r !== undefined) : []; const warmupPeriod = ind.params?.period || 44; console.log(`[Indicators] ${ind.name}: ${validResults.length} valid results (need ${warmupPeriod} candles warmup)`); diff --git a/src/api/dashboard/static/js/ui/indicators-panel.js b/src/api/dashboard/static/js/ui/indicators-panel.js index e103d68..2937588 100644 --- a/src/api/dashboard/static/js/ui/indicators-panel.js +++ b/src/api/dashboard/static/js/ui/indicators-panel.js @@ -240,6 +240,7 @@ export function addIndicator(type) { // Set Hurst-specific defaults if (type === 'hurst') { + params.timeframe = 'chart'; params.markerBuyShape = 'custom'; params.markerSellShape = 'custom'; params.markerBuyColor = '#9e9e9e'; @@ -492,13 +493,17 @@ export function drawIndicatorsOnChart() { } function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) { - const results = instance.calculate(candles); + let results = instance.calculate(candles); + if (!results || !Array.isArray(results)) { + console.error(`[renderIndicatorOnPane] ${indicator.name}: Failed to get valid results (got ${typeof results})`); + return; + } indicator.series = []; const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid; const lineWidth = indicator.params._lineWidth || 1; - const firstNonNull = results?.find(r => r !== null && r !== undefined); + const firstNonNull = Array.isArray(results) ? results.find(r => r !== null && r !== undefined) : null; const isObjectResult = firstNonNull && typeof firstNonNull === 'object'; meta.plots.forEach((plot, plotIdx) => { diff --git a/src/api/dashboard/static/js/ui/markers-plugin.js b/src/api/dashboard/static/js/ui/markers-plugin.js new file mode 100644 index 0000000..8270adc --- /dev/null +++ b/src/api/dashboard/static/js/ui/markers-plugin.js @@ -0,0 +1,117 @@ +export class SeriesMarkersPrimitive { + constructor(markers) { + this._markers = markers || []; + this._paneViews = [new MarkersPaneView(this)]; + } + + setMarkers(markers) { + this._markers = markers; + if (this._requestUpdate) { + this._requestUpdate(); + } + } + + attached(param) { + this._chart = param.chart; + this._series = param.series; + this._requestUpdate = param.requestUpdate; + this._requestUpdate(); + } + + detached() { + this._chart = undefined; + this._series = undefined; + this._requestUpdate = undefined; + } + + updateAllViews() {} + + paneViews() { + return this._paneViews; + } +} + +class MarkersPaneView { + constructor(source) { + this._source = source; + } + + renderer() { + return new MarkersRenderer(this._source); + } +} + +class MarkersRenderer { + constructor(source) { + this._source = source; + } + + draw(target) { + if (!this._source._chart || !this._source._series) return; + + // Lightweight Charts v5 wraps context + const ctx = target.context; + const series = this._source._series; + const chart = this._source._chart; + const markers = this._source._markers; + + ctx.save(); + + // Ensure markers are sorted by time (usually already done) + for (const marker of markers) { + const timeCoordinate = chart.timeScale().timeToCoordinate(marker.time); + if (timeCoordinate === null) continue; + + // To position above or below bar, we need the candle data or we use the marker.value if provided + // For true aboveBar/belowBar without candle data, we might just use series.priceToCoordinate on marker.value + let price = marker.value; + // Fallbacks if no value provided (which our calculator does provide) + if (!price) continue; + + const priceCoordinate = series.priceToCoordinate(price); + if (priceCoordinate === null) continue; + + const x = timeCoordinate; + const size = 5; + const margin = 12; // Gap between price and marker + const isAbove = marker.position === 'aboveBar'; + const y = isAbove ? priceCoordinate - margin : priceCoordinate + margin; + + ctx.fillStyle = marker.color || '#26a69a'; + ctx.beginPath(); + + if (marker.shape === 'arrowUp' || (!marker.shape && !isAbove)) { + ctx.moveTo(x, y - size); + ctx.lineTo(x - size, y + size); + ctx.lineTo(x + size, y + size); + } else if (marker.shape === 'arrowDown' || (!marker.shape && isAbove)) { + ctx.moveTo(x, y + size); + ctx.lineTo(x - size, y - size); + ctx.lineTo(x + size, y - size); + } else if (marker.shape === 'circle') { + ctx.arc(x, y, size, 0, Math.PI * 2); + } else if (marker.shape === 'square') { + ctx.rect(x - size, y - size, size * 2, size * 2); + } else if (marker.shape === 'custom' && marker.text) { + ctx.font = '12px Arial'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(marker.text, x, y); + continue; + } else { + // Default triangle + if (isAbove) { + ctx.moveTo(x, y + size); + ctx.lineTo(x - size, y - size); + ctx.lineTo(x + size, y - size); + } else { + ctx.moveTo(x, y - size); + ctx.lineTo(x - size, y + size); + ctx.lineTo(x + size, y + size); + } + } + ctx.fill(); + } + ctx.restore(); + } +} diff --git a/src/api/dashboard/static/js/ui/signal-markers.js b/src/api/dashboard/static/js/ui/signal-markers.js index e80191d..3474f13 100644 --- a/src/api/dashboard/static/js/ui/signal-markers.js +++ b/src/api/dashboard/static/js/ui/signal-markers.js @@ -15,15 +15,18 @@ export function calculateSignalMarkers(candles) { console.log('[SignalMarkers] Processing indicator:', indicator.type, 'showMarkers:', indicator.params.showMarkers); - const IndicatorClass = IndicatorRegistry[indicator.type]; - if (!IndicatorClass) { - continue; + // Use cache if available + let results = indicator.cachedResults; + if (!results || !Array.isArray(results) || results.length !== candles.length) { + const IndicatorClass = IndicatorRegistry[indicator.type]; + if (!IndicatorClass) { + continue; + } + const instance = new IndicatorClass(indicator); + results = instance.calculate(candles); } - const instance = new IndicatorClass(indicator); - const results = instance.calculate(candles); - - if (!results || results.length === 0) { + if (!results || !Array.isArray(results) || results.length === 0) { continue; } diff --git a/src/api/dashboard/static/js/ui/signals-calculator.js b/src/api/dashboard/static/js/ui/signals-calculator.js index ad97c50..b1e8e62 100644 --- a/src/api/dashboard/static/js/ui/signals-calculator.js +++ b/src/api/dashboard/static/js/ui/signals-calculator.js @@ -96,14 +96,17 @@ function calculateHistoricalCrossovers(activeIndicators, candles) { activeIndicators.forEach(indicator => { const indicatorType = indicator.type || indicator.indicatorType; - // Recalculate indicator values for all candles - const IndicatorClass = IndicatorRegistry[indicatorType]; - if (!IndicatorClass) return; + // Recalculate indicator values for all candles (use cache if valid) + let results = indicator.cachedResults; + if (!results || !Array.isArray(results) || results.length !== candles.length) { + const IndicatorClass = IndicatorRegistry[indicatorType]; + if (!IndicatorClass) return; + const instance = new IndicatorClass(indicator); + results = instance.calculate(candles); + // Don't save back to cache here, let drawIndicatorsOnChart be the source of truth for cache + } - const instance = new IndicatorClass(indicator); - const results = instance.calculate(candles); - - if (!results || results.length === 0) return; + if (!results || !Array.isArray(results) || results.length === 0) return; // Find the most recent crossover by going backwards from the newest candle // candles are sorted oldest first, newest last @@ -276,7 +279,7 @@ export function calculateAllIndicatorSignals() { let results = indicator.cachedResults; let meta = indicator.cachedMeta; - if (!results || !meta || results.length !== candles.length) { + if (!results || !meta || !Array.isArray(results) || results.length !== candles.length) { const instance = new IndicatorClass(indicator); meta = instance.getMetadata(); results = instance.calculate(candles); @@ -284,8 +287,8 @@ export function calculateAllIndicatorSignals() { indicator.cachedMeta = meta; } - if (!results || results.length === 0) { - console.log('[Signals] No results for indicator:', indicator.type); + if (!results || !Array.isArray(results) || results.length === 0) { + console.log('[Signals] No valid results for indicator:', indicator.type); continue; } diff --git a/src/api/dashboard/static/js/ui/strategy-panel.js b/src/api/dashboard/static/js/ui/strategy-panel.js index d7069aa..d768c23 100644 --- a/src/api/dashboard/static/js/ui/strategy-panel.js +++ b/src/api/dashboard/static/js/ui/strategy-panel.js @@ -1,13 +1,10 @@ -import { IndicatorRegistry, getSignalFunction } from '../indicators/index.js'; +import { getStrategy, registerStrategy } from '../strategies/index.js'; +import { PingPongStrategy } from '../strategies/ping-pong.js'; + +// Register available strategies +registerStrategy('ping_pong', PingPongStrategy); let activeIndicators = []; -let simulationResults = null; -let equitySeries = null; -let equityChart = null; -let posSeries = null; -let posSizeChart = null; - -const STORAGE_KEY = 'ping_pong_settings'; function formatDisplayDate(timestamp) { if (!timestamp) return ''; @@ -20,49 +17,6 @@ function formatDisplayDate(timestamp) { return `${day}/${month}/${year} ${hours}:${minutes}`; } -function parseDisplayDate(str) { - if (!str) return null; - const regex = /^(\d{2})\/(\d{2})\/(\d{4})\s(\d{2}):(\d{2})$/; - const match = str.trim().match(regex); - if (!match) return null; - const [_, day, month, year, hours, minutes] = match; - return new Date(year, month - 1, day, hours, minutes); -} - -function getSavedSettings() { - const saved = localStorage.getItem(STORAGE_KEY); - if (!saved) return null; - try { - return JSON.parse(saved); - } catch (e) { - return null; - } -} - -function saveSettings() { - const settings = { - startDate: document.getElementById('simStartDate').value, - stopDate: document.getElementById('simStopDate').value, - contractType: document.getElementById('simContractType').value, - direction: document.getElementById('simDirection').value, - capital: document.getElementById('simCapital').value, - exchangeLeverage: document.getElementById('simExchangeLeverage').value, - maxEffectiveLeverage: document.getElementById('simMaxEffectiveLeverage').value, - posSize: document.getElementById('simPosSize').value, - tp: document.getElementById('simTP').value - }; - localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); - - const btn = document.getElementById('saveSimSettings'); - const originalText = btn.textContent; - btn.textContent = 'Saved!'; - btn.style.color = '#26a69a'; - setTimeout(() => { - btn.textContent = originalText; - btn.style.color = ''; - }, 2000); -} - export function initStrategyPanel() { window.renderStrategyPanel = renderStrategyPanel; renderStrategyPanel(); @@ -88,94 +42,23 @@ export function renderStrategyPanel() { if (!container) return; activeIndicators = window.getActiveIndicators?.() || []; - const saved = getSavedSettings(); - // Format initial values for display - let startDisplay = saved?.startDate || '01/01/2026 00:00'; - let stopDisplay = saved?.stopDate || ''; - - // If the saved value is in ISO format (from previous version), convert it - if (startDisplay.includes('T')) { - startDisplay = formatDisplayDate(new Date(startDisplay)); - } - if (stopDisplay.includes('T')) { - stopDisplay = formatDisplayDate(new Date(stopDisplay)); + // For now, we only have Ping-Pong. Later we can add a strategy selector. + const currentStrategyId = 'ping_pong'; + const strategy = getStrategy(currentStrategyId); + + if (!strategy) { + container.innerHTML = ``; + return; } container.innerHTML = ` @@ -185,319 +68,27 @@ export function renderStrategyPanel() { `; - document.getElementById('runSimulationBtn').addEventListener('click', runSimulation); - document.getElementById('saveSimSettings').addEventListener('click', saveSettings); -} - -function renderIndicatorChecklist(prefix) { - if (activeIndicators.length === 0) { - return '
No active indicators on chart
'; + // Attach strategy specific listeners (like disabling dropdowns when auto-detect is on) + if (strategy.attachListeners) { + strategy.attachListeners(); } - - return activeIndicators.map(ind => ` - - `).join(''); + + document.getElementById('runSimulationBtn').addEventListener('click', () => { + strategy.runSimulation(activeIndicators, displayResults); + }); } -async function runSimulation() { - const btn = document.getElementById('runSimulationBtn'); - btn.disabled = true; - const originalBtnText = btn.textContent; - btn.textContent = 'Preparing Data...'; - - try { - const startVal = document.getElementById('simStartDate').value; - const stopVal = document.getElementById('simStopDate').value; - - const config = { - startDate: new Date(startVal).getTime() / 1000, - stopDate: stopVal ? new Date(stopVal).getTime() / 1000 : Math.floor(Date.now() / 1000), - contractType: document.getElementById('simContractType').value, - direction: document.getElementById('simDirection').value, - capital: parseFloat(document.getElementById('simCapital').value), - exchangeLeverage: parseFloat(document.getElementById('simExchangeLeverage').value), - maxEffectiveLeverage: parseFloat(document.getElementById('simMaxEffectiveLeverage').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 interval = window.dashboard?.currentInterval || '1d'; - - // 1. Ensure data is loaded for the range - let allCandles = window.dashboard?.allData?.get(interval) || []; - - const earliestInCache = allCandles.length > 0 ? allCandles[0].time : Infinity; - const latestInCache = allCandles.length > 0 ? allCandles[allCandles.length - 1].time : -Infinity; - - if (config.startDate < earliestInCache || config.stopDate > latestInCache) { - btn.textContent = 'Fetching from Server...'; - console.log(`[Simulation] Data gap detected. Range: ${config.startDate}-${config.stopDate}, Cache: ${earliestInCache}-${latestInCache}`); - - const startISO = new Date(config.startDate * 1000).toISOString(); - const stopISO = new Date(config.stopDate * 1000).toISOString(); - - const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${interval}&start=${startISO}&end=${stopISO}&limit=10000`); - const data = await response.json(); - - if (data.candles && data.candles.length > 0) { - const fetchedCandles = data.candles.reverse().map(c => ({ - time: Math.floor(new Date(c.time).getTime() / 1000), - open: parseFloat(c.open), - high: parseFloat(c.high), - low: parseFloat(c.low), - close: parseFloat(c.close), - volume: parseFloat(c.volume || 0) - })); - - // Merge with existing data - allCandles = window.dashboard.mergeData(allCandles, fetchedCandles); - window.dashboard.allData.set(interval, allCandles); - window.dashboard.candleSeries.setData(allCandles); - - // Recalculate indicators - btn.textContent = 'Calculating Indicators...'; - window.drawIndicatorsOnChart?.(); - // Wait a bit for indicators to calculate (they usually run in background/setTimeout) - await new Promise(r => setTimeout(r, 500)); - } - } - - btn.textContent = 'Simulating...'; - - // Filter candles by the exact range - const simCandles = allCandles.filter(c => c.time >= config.startDate && c.time <= config.stopDate); - - if (simCandles.length === 0) { - alert('No data available for the selected range.'); - return; - } - - // Calculate indicator signals - const indicatorSignals = {}; - for (const indId of [...new Set([...config.openIndicators, ...config.closeIndicators])]) { - const ind = activeIndicators.find(a => a.id === indId); - if (!ind) continue; - - const signalFunc = getSignalFunction(ind.type); - const results = ind.cachedResults; - - if (results && signalFunc) { - indicatorSignals[indId] = simCandles.map(candle => { - const idx = allCandles.findIndex(c => c.time === candle.time); - if (idx < 1) return null; - const values = typeof results[idx] === 'object' && results[idx] !== null ? results[idx] : { ma: results[idx] }; - const prevValues = typeof results[idx-1] === 'object' && results[idx-1] !== null ? results[idx-1] : { ma: results[idx-1] }; - return signalFunc(ind, allCandles[idx], allCandles[idx-1], values, prevValues); - }); - } - } - - // Simulation loop - const startPrice = simCandles[0].open; - let balanceBtc = config.contractType === 'inverse' ? config.capital / startPrice : 0; - let balanceUsd = config.contractType === 'linear' ? config.capital : 0; - - let equityData = { usd: [], btc: [] }; - let totalQty = 0; // Linear: BTC, Inverse: USD Contracts - let avgPrice = 0; - let avgPriceData = []; - let posSizeData = { btc: [], usd: [] }; - let trades = []; - - const PARTIAL_EXIT_PCT = 0.15; - const MIN_POSITION_VALUE_USD = 15; - - for (let i = 0; i < simCandles.length; i++) { - const candle = simCandles[i]; - const price = candle.close; - let actionTakenInThisCandle = false; - - // 1. Check TP - if (totalQty > 0) { - let isTP = false; - let exitPrice = price; - if (config.direction === 'long') { - if (candle.high >= avgPrice * (1 + config.tp)) { - isTP = true; - exitPrice = avgPrice * (1 + config.tp); - } - } else { - if (candle.low <= avgPrice * (1 - config.tp)) { - isTP = true; - exitPrice = avgPrice * (1 - config.tp); - } - } - - if (isTP) { - let qtyToClose = totalQty * PARTIAL_EXIT_PCT; - let remainingQty = totalQty - qtyToClose; - let remainingValueUsd = config.contractType === 'linear' ? remainingQty * exitPrice : remainingQty; - let reason = 'TP (Partial)'; - - // Minimum size check - if (remainingValueUsd < MIN_POSITION_VALUE_USD) { - qtyToClose = totalQty; - reason = 'TP (Full - Min Size)'; - } - - let pnl; - if (config.contractType === 'linear') { - pnl = config.direction === 'long' ? (exitPrice - avgPrice) * qtyToClose : (avgPrice - exitPrice) * qtyToClose; - balanceUsd += pnl; - } else { - pnl = config.direction === 'long' - ? qtyToClose * (1/avgPrice - 1/exitPrice) - : qtyToClose * (1/exitPrice - 1/avgPrice); - balanceBtc += pnl; - } - - totalQty -= qtyToClose; - trades.push({ - type: config.direction, recordType: 'exit', time: candle.time, - entryPrice: avgPrice, exitPrice: exitPrice, pnl: pnl, reason: reason, - currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty, - currentQty: config.contractType === 'linear' ? totalQty : totalQty / price - }); - actionTakenInThisCandle = true; - } - } - - // 2. Check Close Signals - if (!actionTakenInThisCandle && totalQty > 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) { - let qtyToClose = totalQty * PARTIAL_EXIT_PCT; - let remainingQty = totalQty - qtyToClose; - let remainingValueUsd = config.contractType === 'linear' ? remainingQty * price : remainingQty; - let reason = 'Signal (Partial)'; - - // Minimum size check - if (remainingValueUsd < MIN_POSITION_VALUE_USD) { - qtyToClose = totalQty; - reason = 'Signal (Full - Min Size)'; - } - - let pnl; - if (config.contractType === 'linear') { - pnl = config.direction === 'long' ? (price - avgPrice) * qtyToClose : (avgPrice - price) * qtyToClose; - balanceUsd += pnl; - } else { - pnl = config.direction === 'long' - ? qtyToClose * (1/avgPrice - 1/price) - : qtyToClose * (1/price - 1/avgPrice); - balanceBtc += pnl; - } - - totalQty -= qtyToClose; - trades.push({ - type: config.direction, recordType: 'exit', time: candle.time, - entryPrice: avgPrice, exitPrice: price, pnl: pnl, reason: reason, - currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty, - currentQty: config.contractType === 'linear' ? totalQty : totalQty / price - }); - actionTakenInThisCandle = true; - } - } - - // Calculate Current Equity for Margin Check - let currentEquityBtc, currentEquityUsd; - if (config.contractType === 'linear') { - const upnlUsd = totalQty > 0 ? (config.direction === 'long' ? (price - avgPrice) : (avgPrice - price)) * totalQty : 0; - currentEquityUsd = balanceUsd + upnlUsd; - currentEquityBtc = currentEquityUsd / price; - } else { - const upnlBtc = totalQty > 0 ? (config.direction === 'long' ? totalQty * (1/avgPrice - 1/price) : totalQty * (1/price - 1/avgPrice)) : 0; - currentEquityBtc = balanceBtc + upnlBtc; - currentEquityUsd = currentEquityBtc * price; - } - - // 3. Check Open Signals - 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) { - const entryValUsd = config.posSize * config.exchangeLeverage; - const currentNotionalBtc = config.contractType === 'linear' ? totalQty : totalQty / price; - const entryNotionalBtc = entryValUsd / price; - - const projectedEffectiveLeverage = (currentNotionalBtc + entryNotionalBtc) / Math.max(currentEquityBtc, 0.0000001); - - if (projectedEffectiveLeverage <= config.maxEffectiveLeverage) { - if (config.contractType === 'linear') { - const entryQty = entryValUsd / price; - avgPrice = ((totalQty * avgPrice) + (entryQty * price)) / (totalQty + entryQty); - totalQty += entryQty; - } else { - // Inverse: totalQty is USD contracts - avgPrice = (totalQty + entryValUsd) / ((totalQty / avgPrice || 0) + (entryValUsd / price)); - totalQty += entryValUsd; - } - - trades.push({ - type: config.direction, recordType: 'entry', time: candle.time, - entryPrice: price, reason: 'Entry', - currentUsd: config.contractType === 'linear' ? totalQty * price : totalQty, - currentQty: config.contractType === 'linear' ? totalQty : totalQty / price - }); - } - } - } - - // Final Equity Recording - let finalEquityBtc, finalEquityUsd; - if (config.contractType === 'linear') { - const upnl = totalQty > 0 ? (config.direction === 'long' ? (price - avgPrice) : (avgPrice - price)) * totalQty : 0; - finalEquityUsd = balanceUsd + upnl; - finalEquityBtc = finalEquityUsd / price; - } else { - const upnl = totalQty > 0 ? (config.direction === 'long' ? totalQty * (1/avgPrice - 1/price) : totalQty * (1/price - 1/avgPrice)) : 0; - finalEquityBtc = balanceBtc + upnl; - finalEquityUsd = finalEquityBtc * price; - } - - equityData.usd.push({ time: candle.time, value: finalEquityUsd }); - equityData.btc.push({ time: candle.time, value: finalEquityBtc }); - - if (totalQty > 0.000001) avgPriceData.push({ time: candle.time, value: avgPrice }); - posSizeData.btc.push({ time: candle.time, value: config.contractType === 'linear' ? totalQty : totalQty / price }); - posSizeData.usd.push({ time: candle.time, value: config.contractType === 'linear' ? totalQty * price : totalQty }); - } - - displayResults(trades, equityData, config, simCandles[simCandles.length-1].close, avgPriceData, posSizeData); - - } catch (error) { - console.error('[Simulation] Error:', error); - alert('Simulation failed.'); - } finally { - btn.disabled = false; - btn.textContent = 'Run Simulation'; - } -} +// Keep the display logic here so all strategies can use the same rendering for results +let equitySeries = null; +let equityChart = null; +let posSeries = null; +let posSizeChart = null; +let tradeMarkers = []; function displayResults(trades, equityData, config, endPrice, avgPriceData, posSizeData) { const resultsDiv = document.getElementById('simulationResults'); resultsDiv.style.display = 'block'; - // Update main chart with avg price if (window.dashboard) { window.dashboard.setAvgPriceData(avgPriceData); } @@ -574,7 +165,6 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS // Create Charts const initCharts = () => { - // Equity Chart const equityContainer = document.getElementById('equityChart'); if (equityContainer) { equityContainer.innerHTML = ''; @@ -588,12 +178,12 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS timeVisible: true, secondsVisible: false, tickMarkFormatter: (time, tickMarkType, locale) => { - return TimezoneConfig.formatTickMark(time); + return window.TimezoneConfig ? window.TimezoneConfig.formatTickMark(time) : new Date(time * 1000).toLocaleDateString(); }, }, localization: { timeFormatter: (timestamp) => { - return TimezoneConfig.formatDate(timestamp * 1000); + return window.TimezoneConfig ? window.TimezoneConfig.formatDate(timestamp * 1000) : new Date(timestamp * 1000).toLocaleString(); }, }, handleScroll: true, @@ -611,7 +201,6 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS equityChart.timeScale().fitContent(); } - // Pos Size Chart const posSizeContainer = document.getElementById('posSizeChart'); if (posSizeContainer) { posSizeContainer.innerHTML = ''; @@ -625,12 +214,12 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS timeVisible: true, secondsVisible: false, tickMarkFormatter: (time, tickMarkType, locale) => { - return TimezoneConfig.formatTickMark(time); + return window.TimezoneConfig ? window.TimezoneConfig.formatTickMark(time) : new Date(time * 1000).toLocaleDateString(); }, }, localization: { timeFormatter: (timestamp) => { - return TimezoneConfig.formatDate(timestamp * 1000); + return window.TimezoneConfig ? window.TimezoneConfig.formatDate(timestamp * 1000) : new Date(timestamp * 1000).toLocaleString(); }, }, handleScroll: true, @@ -651,7 +240,6 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS if (label) label.textContent = 'Position Size (USD)'; } - // Sync Time Scales if (equityChart && posSizeChart) { let isSyncing = false; @@ -667,7 +255,6 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS posSizeChart.timeScale().subscribeVisibleTimeRangeChange(() => syncCharts(posSizeChart, equityChart)); } - // Sync to Main Chart on Click const syncToMain = (param) => { if (!param.time || !window.dashboard || !window.dashboard.chart) return; @@ -675,7 +262,6 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS const currentRange = timeScale.getVisibleRange(); if (!currentRange) return; - // Calculate current width to preserve zoom level const width = currentRange.to - currentRange.from; const halfWidth = width / 2; @@ -691,12 +277,10 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS setTimeout(initCharts, 100); - // Toggle Logic resultsDiv.querySelectorAll('.toggle-btn').forEach(btn => { btn.addEventListener('click', (e) => { const unit = btn.dataset.unit; - // Sync all toggle button groups resultsDiv.querySelectorAll(`.toggle-btn`).forEach(b => { if (b.dataset.unit === unit) b.classList.add('active'); else b.classList.remove('active'); @@ -729,8 +313,6 @@ function displayResults(trades, equityData, config, endPrice, avgPriceData, posS }); } -let tradeMarkers = []; - function toggleSimulationMarkers(trades) { if (tradeMarkers.length > 0) { clearSimulationMarkers(); @@ -744,7 +326,6 @@ function toggleSimulationMarkers(trades) { const qtyVal = t.currentQty !== undefined ? `${t.currentQty.toFixed(4)} BTC` : '0'; const sizeStr = ` (${usdVal} / ${qtyVal})`; - // Entry marker if (t.recordType === 'entry') { markers.push({ time: t.time, @@ -755,7 +336,6 @@ function toggleSimulationMarkers(trades) { }); } - // Exit marker if (t.recordType === 'exit') { markers.push({ time: t.time, @@ -767,7 +347,6 @@ function toggleSimulationMarkers(trades) { } }); - // Sort markers by time markers.sort((a, b) => a.time - b.time); if (window.dashboard) { @@ -788,4 +367,4 @@ window.clearSimulationResults = function() { const resultsDiv = document.getElementById('simulationResults'); if (resultsDiv) resultsDiv.style.display = 'none'; clearSimulationMarkers(); -}; +}; \ No newline at end of file