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
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
import { INTERVALS, COLORS } from '../core/index.js';
|
import { INTERVALS, COLORS } from '../core/index.js';
|
||||||
import { calculateAllIndicatorSignals, calculateSummarySignal } from './signals-calculator.js';
|
import { calculateAllIndicatorSignals, calculateSummarySignal } from './signals-calculator.js';
|
||||||
|
import { calculateSignalMarkers } from './signal-markers.js';
|
||||||
import { updateIndicatorCandles } from './indicators-panel-new.js';
|
import { updateIndicatorCandles } from './indicators-panel-new.js';
|
||||||
import { TimezoneConfig } from '../config/timezone.js';
|
import { TimezoneConfig } from '../config/timezone.js';
|
||||||
|
|
||||||
@ -575,6 +576,7 @@ async loadSignals() {
|
|||||||
try {
|
try {
|
||||||
this.indicatorSignals = calculateAllIndicatorSignals();
|
this.indicatorSignals = calculateAllIndicatorSignals();
|
||||||
this.summarySignal = calculateSummarySignal(this.indicatorSignals);
|
this.summarySignal = calculateSummarySignal(this.indicatorSignals);
|
||||||
|
this.updateSignalMarkers();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading signals:', error);
|
console.error('Error loading signals:', error);
|
||||||
this.indicatorSignals = [];
|
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) {
|
if (!this.taData || this.taData.error) {
|
||||||
document.getElementById('taContent').innerHTML = `<div class="ta-error">${this.taData?.error || 'No data available'}</div>`;
|
document.getElementById('taContent').innerHTML = `<div class="ta-error">${this.taData?.error || 'No data available'}</div>`;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -322,6 +322,61 @@ function renderIndicatorConfig(indicator, meta) {
|
|||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-subtitle">Signals</div>
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Show Markers</label>
|
||||||
|
<input type="checkbox" ${indicator.params.showMarkers !== false ? 'checked' : ''}
|
||||||
|
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'showMarkers', this.checked)">
|
||||||
|
</div>
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Buy Shape</label>
|
||||||
|
<select onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'markerBuyShape', this.value)">
|
||||||
|
<option value="arrowUp" ${indicator.params.markerBuyShape === 'arrowUp' || !indicator.params.markerBuyShape ? 'selected' : ''}>Arrow Up</option>
|
||||||
|
<option value="arrowDown" ${indicator.params.markerBuyShape === 'arrowDown' ? 'selected' : ''}>Arrow Down</option>
|
||||||
|
<option value="circle" ${indicator.params.markerBuyShape === 'circle' ? 'selected' : ''}>Circle</option>
|
||||||
|
<option value="square" ${indicator.params.markerBuyShape === 'square' ? 'selected' : ''}>Square</option>
|
||||||
|
<option value="triangleUp" ${indicator.params.markerBuyShape === 'triangleUp' ? 'selected' : ''}>Triangle Up</option>
|
||||||
|
<option value="triangleDown" ${indicator.params.markerBuyShape === 'triangleDown' ? 'selected' : ''}>Triangle Down</option>
|
||||||
|
<option value="custom" ${indicator.params.markerBuyShape === 'custom' ? 'selected' : ''}>Custom</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" style="width: 60px; margin-left: 5px;" value="${indicator.params.markerBuyShape === 'custom' ? (indicator.params.markerBuyCustom || '') : ''}"
|
||||||
|
placeholder="◭"
|
||||||
|
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'markerBuyCustom', this.value)">
|
||||||
|
</div>
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Buy Color</label>
|
||||||
|
<div class="color-picker">
|
||||||
|
<input type="color" id="markerBuyColor_${indicator.id}" value="${indicator.params.markerBuyColor || '#26a69a'}"
|
||||||
|
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'markerBuyColor', this.value)">
|
||||||
|
<span class="color-preview" style="background: ${indicator.params.markerBuyColor || '#26a69a'};"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Sell Shape</label>
|
||||||
|
<select onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'markerSellShape', this.value)">
|
||||||
|
<option value="arrowDown" ${indicator.params.markerSellShape === 'arrowDown' || !indicator.params.markerSellShape ? 'selected' : ''}>Arrow Down</option>
|
||||||
|
<option value="arrowUp" ${indicator.params.markerSellShape === 'arrowUp' ? 'selected' : ''}>Arrow Up</option>
|
||||||
|
<option value="circle" ${indicator.params.markerSellShape === 'circle' ? 'selected' : ''}>Circle</option>
|
||||||
|
<option value="square" ${indicator.params.markerSellShape === 'square' ? 'selected' : ''}>Square</option>
|
||||||
|
<option value="triangleUp" ${indicator.params.markerSellShape === 'triangleUp' ? 'selected' : ''}>Triangle Up</option>
|
||||||
|
<option value="triangleDown" ${indicator.params.markerSellShape === 'triangleDown' ? 'selected' : ''}>Triangle Down</option>
|
||||||
|
<option value="custom" ${indicator.params.markerSellShape === 'custom' ? 'selected' : ''}>Custom</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" style="width: 60px; margin-left: 5px;" value="${indicator.params.markerSellShape === 'custom' ? (indicator.params.markerSellCustom || '') : ''}"
|
||||||
|
placeholder="▼"
|
||||||
|
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'markerSellCustom', this.value)">
|
||||||
|
</div>
|
||||||
|
<div class="config-row">
|
||||||
|
<label>Sell Color</label>
|
||||||
|
<div class="color-picker">
|
||||||
|
<input type="color" id="markerSellColor_${indicator.id}" value="${indicator.params.markerSellColor || '#ef5350'}"
|
||||||
|
onchange="window.updateIndicatorSetting && window.updateIndicatorSetting('${indicator.id}', 'markerSellColor', this.value)">
|
||||||
|
<span class="color-preview" style="background: ${indicator.params.markerSellColor || '#ef5350'};"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<div class="section-subtitle">
|
<div class="section-subtitle">
|
||||||
Presets
|
Presets
|
||||||
@ -633,7 +688,14 @@ function addIndicator(type) {
|
|||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
_lineType: 'solid',
|
_lineType: 'solid',
|
||||||
_lineWidth: 2
|
_lineWidth: 2,
|
||||||
|
showMarkers: true,
|
||||||
|
markerBuyShape: 'arrowUp',
|
||||||
|
markerBuyColor: '#26a69a',
|
||||||
|
markerBuyCustom: '◭',
|
||||||
|
markerSellShape: 'arrowDown',
|
||||||
|
markerSellColor: '#ef5350',
|
||||||
|
markerSellCustom: '▼'
|
||||||
};
|
};
|
||||||
metadata.plots.forEach((plot, idx) => {
|
metadata.plots.forEach((plot, idx) => {
|
||||||
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
params[`_color_${idx}`] = plot.color || getDefaultColor(activeIndicators.length + idx);
|
||||||
@ -966,6 +1028,11 @@ export function drawIndicatorsOnChart() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Indicators] Error drawing indicators:', 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) {
|
function resetIndicator(id) {
|
||||||
|
|||||||
198
src/api/dashboard/static/js/ui/signal-markers.js
Normal file
198
src/api/dashboard/static/js/ui/signal-markers.js
Normal file
@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user