From 9d7647fde5e160093b4cd31ec788dd53ef5b7df7 Mon Sep 17 00:00:00 2001 From: DiTus Date: Mon, 2 Mar 2026 12:49:49 +0100 Subject: [PATCH] Add signal markers on main chart with configurable shapes and colors - Create signal-markers.js module to calculate crossover markers for indicators - Add marker configuration options to indicator config panel: - Show/hide markers toggle - Buy/sell shape selection (built-in or custom Unicode) - Buy/sell color pickers - Integrate markers with lightweight-charts using createSeriesMarkers API - Markers recalculate when indicators change or historical data loads --- src/api/dashboard/static/js/ui/chart.js | 84 +++++++- .../static/js/ui/indicators-panel-new.js | 69 +++++- .../dashboard/static/js/ui/signal-markers.js | 198 ++++++++++++++++++ 3 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 src/api/dashboard/static/js/ui/signal-markers.js diff --git a/src/api/dashboard/static/js/ui/chart.js b/src/api/dashboard/static/js/ui/chart.js index 6d1767a..1c3defd 100644 --- a/src/api/dashboard/static/js/ui/chart.js +++ b/src/api/dashboard/static/js/ui/chart.js @@ -1,5 +1,6 @@ import { INTERVALS, COLORS } from '../core/index.js'; import { calculateAllIndicatorSignals, calculateSummarySignal } from './signals-calculator.js'; +import { calculateSignalMarkers } from './signal-markers.js'; import { updateIndicatorCandles } from './indicators-panel-new.js'; import { TimezoneConfig } from '../config/timezone.js'; @@ -575,6 +576,7 @@ async loadSignals() { try { this.indicatorSignals = calculateAllIndicatorSignals(); this.summarySignal = calculateSummarySignal(this.indicatorSignals); + this.updateSignalMarkers(); } catch (error) { console.error('Error loading signals:', error); this.indicatorSignals = []; @@ -582,7 +584,87 @@ async loadSignals() { } } -renderTA() { +updateSignalMarkers() { + const candles = this.allData.get(this.currentInterval); + if (!candles || candles.length === 0) return; + + const markers = calculateSignalMarkers(candles); + + // 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; + } + } + + // 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() { if (!this.taData || this.taData.error) { document.getElementById('taContent').innerHTML = `
${this.taData?.error || 'No data available'}
`; return; 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 f417e49..ad81d94 100644 --- a/src/api/dashboard/static/js/ui/indicators-panel-new.js +++ b/src/api/dashboard/static/js/ui/indicators-panel-new.js @@ -322,6 +322,61 @@ function renderIndicatorConfig(indicator, meta) { ` : ''} +
+
Signals
+
+ + +
+
+ + + +
+
+ +
+ + +
+
+
+ + + +
+
+ +
+ + +
+
+
+
Presets @@ -633,7 +688,14 @@ function addIndicator(type) { const params = { _lineType: 'solid', - _lineWidth: 2 + _lineWidth: 2, + showMarkers: true, + markerBuyShape: 'arrowUp', + markerBuyColor: '#26a69a', + markerBuyCustom: '◭', + markerSellShape: 'arrowDown', + markerSellColor: '#ef5350', + markerSellCustom: '▼' }; metadata.plots.forEach((plot, idx) => { params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx); @@ -966,6 +1028,11 @@ export function drawIndicatorsOnChart() { } catch (error) { console.error('[Indicators] Error drawing indicators:', error); } + + // Update signal markers after indicators are drawn + if (window.dashboard && typeof window.dashboard.updateSignalMarkers === 'function') { + window.dashboard.updateSignalMarkers(); + } } function resetIndicator(id) { diff --git a/src/api/dashboard/static/js/ui/signal-markers.js b/src/api/dashboard/static/js/ui/signal-markers.js new file mode 100644 index 0000000..e0430ce --- /dev/null +++ b/src/api/dashboard/static/js/ui/signal-markers.js @@ -0,0 +1,198 @@ +import { IndicatorRegistry } from '../indicators/index.js'; + +export function calculateSignalMarkers(candles) { + const activeIndicators = window.getActiveIndicators?.() || []; + const markers = []; + + if (!candles || candles.length < 2) { + return markers; + } + + for (const indicator of activeIndicators) { + if (indicator.params.showMarkers === false || indicator.params.showMarkers === 'false') { + continue; + } + + console.log('[SignalMarkers] Processing indicator:', indicator.type, 'showMarkers:', indicator.params.showMarkers); + + const IndicatorClass = IndicatorRegistry[indicator.type]; + if (!IndicatorClass) { + continue; + } + + const instance = new IndicatorClass(indicator); + const results = instance.calculate(candles); + + if (!results || results.length === 0) { + continue; + } + + const indicatorMarkers = findCrossoverMarkers(indicator, candles, results); + markers.push(...indicatorMarkers); + } + + markers.sort((a, b) => a.time - b.time); + + return markers; +} + +function findCrossoverMarkers(indicator, candles, results) { + const markers = []; + const overbought = indicator.params?.overbought || 70; + const oversold = indicator.params?.oversold || 30; + const indicatorType = indicator.type; + + const buyColor = indicator.params?.markerBuyColor || '#26a69a'; + const sellColor = indicator.params?.markerSellColor || '#ef5350'; + const buyShape = indicator.params?.markerBuyShape || 'arrowUp'; + const sellShape = indicator.params?.markerSellShape || 'arrowDown'; + const buyCustom = indicator.params?.markerBuyCustom || '◭'; + const sellCustom = indicator.params?.markerSellCustom || '▼'; + + for (let i = 1; i < results.length; i++) { + const candle = candles[i]; + const prevCandle = candles[i - 1]; + const result = results[i]; + const prevResult = results[i - 1]; + + if (!result || !prevResult) continue; + + if (indicatorType === 'rsi' || indicatorType === 'stoch') { + const rsi = result.rsi ?? result; + const prevRsi = prevResult.rsi ?? prevResult; + + if (rsi === undefined || prevRsi === undefined) continue; + + if (prevRsi > overbought && rsi <= overbought) { + markers.push({ + time: candle.time, + position: 'aboveBar', + color: sellColor, + shape: sellShape === 'custom' ? '' : sellShape, + text: sellShape === 'custom' ? sellCustom : '' + }); + } + + if (prevRsi < oversold && rsi >= oversold) { + markers.push({ + time: candle.time, + position: 'belowBar', + color: buyColor, + shape: buyShape === 'custom' ? '' : buyShape, + text: buyShape === 'custom' ? buyCustom : '' + }); + } + } else if (indicatorType === 'macd') { + const macd = result.macd ?? result.MACD; + const signal = result.signal ?? result.signalLine; + const prevMacd = prevResult.macd ?? prevResult.MACD; + const prevSignal = prevResult.signal ?? prevResult.signalLine; + + if (macd === undefined || signal === undefined || prevMacd === undefined || prevSignal === undefined) continue; + + const macdAbovePrev = prevMacd > prevSignal; + const macdAboveNow = macd > signal; + + if (macdAbovePrev && !macdAboveNow) { + markers.push({ + time: candle.time, + position: 'aboveBar', + color: sellColor, + shape: sellShape === 'custom' ? '' : sellShape, + text: sellShape === 'custom' ? sellCustom : '' + }); + } + + if (!macdAbovePrev && macdAboveNow) { + markers.push({ + time: candle.time, + position: 'belowBar', + color: buyColor, + shape: buyShape === 'custom' ? '' : buyShape, + text: buyShape === 'custom' ? buyCustom : '' + }); + } + } else if (indicatorType === 'bb') { + const upper = result.upper ?? result.upperBand; + const lower = result.lower ?? result.lowerBand; + + if (upper === undefined || lower === undefined) continue; + + const priceAboveUpperPrev = prevCandle.close > (prevResult.upper ?? prevResult.upperBand); + const priceAboveUpperNow = candle.close > upper; + + if (priceAboveUpperPrev && !priceAboveUpperNow) { + markers.push({ + time: candle.time, + position: 'aboveBar', + color: sellColor, + shape: sellShape === 'custom' ? '' : sellShape, + text: sellShape === 'custom' ? sellCustom : '' + }); + } + + if (!priceAboveUpperPrev && priceAboveUpperNow) { + markers.push({ + time: candle.time, + position: 'belowBar', + color: buyColor, + shape: buyShape === 'custom' ? '' : buyShape, + text: buyShape === 'custom' ? buyCustom : '' + }); + } + + const priceBelowLowerPrev = prevCandle.close < (prevResult.lower ?? prevResult.lowerBand); + const priceBelowLowerNow = candle.close < lower; + + if (priceBelowLowerPrev && !priceBelowLowerNow) { + markers.push({ + time: candle.time, + position: 'belowBar', + color: buyColor, + shape: buyShape === 'custom' ? '' : buyShape, + text: buyShape === 'custom' ? buyCustom : '' + }); + } + + if (!priceBelowLowerPrev && priceBelowLowerNow) { + markers.push({ + time: candle.time, + position: 'aboveBar', + color: sellColor, + shape: sellShape === 'custom' ? '' : sellShape, + text: sellShape === 'custom' ? sellCustom : '' + }); + } + } else { + const ma = result.ma ?? result; + const prevMa = prevResult.ma ?? prevResult; + + if (ma === undefined || prevMa === undefined) continue; + + const priceAbovePrev = prevCandle.close > prevMa; + const priceAboveNow = candle.close > ma; + + if (priceAbovePrev && !priceAboveNow) { + markers.push({ + time: candle.time, + position: 'aboveBar', + color: sellColor, + shape: sellShape === 'custom' ? '' : sellShape, + text: sellShape === 'custom' ? sellCustom : '' + }); + } + + if (!priceAbovePrev && priceAboveNow) { + markers.push({ + time: candle.time, + position: 'belowBar', + color: buyColor, + shape: buyShape === 'custom' ? '' : buyShape, + text: buyShape === 'custom' ? buyCustom : '' + }); + } + } + } + + return markers; +}