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 = `
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;
+}