feat: add oscillator pane support with lightweight-charts v5

- Upgrade CDN from v4.1.0 to v5.1.0
- Add displayMode property to indicators ('overlay' vs 'pane')
- Mark oscillators (RSI, MACD, Stoch, ATR) as pane-type indicators
- Rewrite drawIndicatorsOnChart() to render oscillators in separate draggable panes
- Update all chart.addSeries() calls to v5 API with paneIndex parameter
- Add pane layout configuration with resizable separators
This commit is contained in:
BTC Bot
2026-02-24 11:50:30 +01:00
parent 06b2a4eac4
commit 4d5b1e1416
14 changed files with 136 additions and 63 deletions

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BTC Trading Dashboard</title>
<link rel="icon" href="data:,">
<script src="https://unpkg.com/lightweight-charts@4.1.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="https://unpkg.com/lightweight-charts@5.1.0/dist/lightweight-charts.standalone.production.js"></script>
<style>
:root {
--tv-bg: #131722;

View File

@ -31,7 +31,8 @@ export class ATRIndicator extends BaseIndicator {
name: 'ATR',
description: 'Average True Range - measures market volatility',
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
plots: [{ id: 'value', color: '#795548', title: 'ATR' }]
plots: [{ id: 'value', color: '#795548', title: 'ATR' }],
displayMode: 'pane'
};
}
}

View File

@ -11,7 +11,8 @@ export class BaseIndicator {
return {
name: this.name,
inputs: [],
plots: []
plots: [],
displayMode: 'overlay'
};
}
}

View File

@ -36,7 +36,8 @@ export class BollingerBandsIndicator extends BaseIndicator {
{ id: 'upper', color: '#4caf50', title: 'Upper' },
{ id: 'middle', color: '#4caf50', title: 'Middle', lineStyle: 2 },
{ id: 'lower', color: '#4caf50', title: 'Lower' }
]
],
displayMode: 'overlay'
};
}
}

View File

@ -11,7 +11,8 @@ export class EMAIndicator extends BaseIndicator {
return {
name: 'EMA',
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }],
plots: [{ id: 'value', color: '#ff9800', title: 'EMA' }]
plots: [{ id: 'value', color: '#ff9800', title: 'EMA' }],
displayMode: 'overlay'
};
}
}

View File

@ -34,7 +34,8 @@ export class HTSIndicator extends BaseIndicator {
{ id: 'fastLow', color: '#00bcd4', title: 'Fast Low', width: 1 },
{ id: 'slowHigh', color: '#f44336', title: 'Slow High', width: 2 },
{ id: 'slowLow', color: '#f44336', title: 'Slow Low', width: 2 }
]
],
displayMode: 'overlay'
};
}
}

View File

@ -16,7 +16,8 @@ export class MAIndicator extends BaseIndicator {
{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 },
{ name: 'maType', label: 'MA Type', type: 'select', options: ['SMA', 'EMA', 'RMA', 'WMA', 'VWMA'], default: 'SMA' }
],
plots: [{ id: 'value', color: '#2962ff', title: 'MA' }]
plots: [{ id: 'value', color: '#2962ff', title: 'MA' }],
displayMode: 'overlay'
};
}
}

View File

@ -52,8 +52,9 @@ export class MACDIndicator extends BaseIndicator {
plots: [
{ id: 'macd', color: '#2196f3', title: 'MACD' },
{ id: 'signal', color: '#ff5722', title: 'Signal' },
{ id: 'histogram', color: '#607d8b', title: 'Histogram' }
]
{ id: 'histogram', color: '#607d8b', title: 'Histogram', type: 'histogram' }
],
displayMode: 'pane'
};
}
}

View File

@ -32,7 +32,10 @@ export class RSIIndicator extends BaseIndicator {
name: 'RSI',
description: 'Relative Strength Index - momentum oscillator (0-100)',
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }],
plots: [{ id: 'value', color: '#9c27b0', title: 'RSI' }]
plots: [{ id: 'value', color: '#9c27b0', title: 'RSI' }],
displayMode: 'pane',
paneMin: 0,
paneMax: 100
};
}
}

View File

@ -11,7 +11,8 @@ export class SMAIndicator extends BaseIndicator {
return {
name: 'SMA',
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }],
plots: [{ id: 'value', color: '#2962ff', title: 'SMA' }]
plots: [{ id: 'value', color: '#2962ff', title: 'SMA' }],
displayMode: 'overlay'
};
}
}

View File

@ -39,7 +39,10 @@ export class StochasticIndicator extends BaseIndicator {
plots: [
{ id: 'k', color: '#3f51b5', title: '%K' },
{ id: 'd', color: '#ff9800', title: '%D' }
]
],
displayMode: 'pane',
paneMin: 0,
paneMax: 100
};
}
}

View File

@ -61,6 +61,11 @@ export class TradingDashboard {
layout: {
background: { color: COLORS.tvBg },
textColor: COLORS.tvText,
panes: {
separatorColor: '#2a2e39',
separatorHoverColor: '#363c4e',
enableResize: true
}
},
grid: {
vertLines: { color: COLORS.tvBorder },
@ -85,7 +90,7 @@ export class TradingDashboard {
},
});
this.candleSeries = this.chart.addCandlestickSeries({
this.candleSeries = this.chart.addSeries(LightweightCharts.CandlestickSeries, {
upColor: '#ff9800',
downColor: '#ff9800',
borderUpColor: '#ff9800',
@ -94,7 +99,7 @@ export class TradingDashboard {
wickDownColor: '#ff9800',
lastValueVisible: false,
priceLineVisible: false,
});
}, 0);
this.currentPriceLine = this.candleSeries.createPriceLine({
price: 0,

View File

@ -408,6 +408,9 @@ export function removeIndicatorByIndex(index) {
removeIndicatorById(activeIndicators[index].id);
}
let indicatorPanes = new Map();
let nextPaneIndex = 1;
export function drawIndicatorsOnChart() {
if (!window.dashboard || !window.dashboard.chart) return;
@ -420,72 +423,123 @@ export function drawIndicatorsOnChart() {
const candles = window.dashboard.allData.get(window.dashboard.currentInterval);
if (!candles || candles.length === 0) return;
const lineStyleMap = { 'solid': 0, 'dotted': 1, 'dashed': 2 };
const lineStyleMap = { 'solid': LightweightCharts.LineStyle.Solid, 'dotted': LightweightCharts.LineStyle.Dotted, 'dashed': LightweightCharts.LineStyle.Dashed };
activeIndicators.forEach((indicator) => {
indicatorPanes.clear();
nextPaneIndex = 1;
const overlayIndicators = [];
const paneIndicators = [];
activeIndicators.forEach(ind => {
const IndicatorClass = IR?.[ind.type];
if (!IndicatorClass) return;
const instance = new IndicatorClass({ type: ind.type, params: ind.params, name: ind.name });
const meta = instance.getMetadata();
if (meta.displayMode === 'pane') {
paneIndicators.push({ indicator: ind, meta, instance });
} else {
overlayIndicators.push({ indicator: ind, meta, instance });
}
});
const totalPanes = 1 + paneIndicators.length;
const mainPaneHeight = paneIndicators.length > 0 ? 60 : 100;
const paneHeight = paneIndicators.length > 0 ? Math.floor(40 / paneIndicators.length) : 0;
window.dashboard.chart.panes()[0]?.setHeight(mainPaneHeight);
overlayIndicators.forEach(({ indicator, meta, instance }) => {
if (indicator.visible === false) {
indicator.series = [];
return;
}
const IndicatorClass = IR?.[indicator.type];
if (!IndicatorClass) return;
renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap);
});
const instance = new IndicatorClass({
type: indicator.type,
params: indicator.params,
name: indicator.name
});
paneIndicators.forEach(({ indicator, meta, instance }, idx) => {
if (indicator.visible === false) {
indicator.series = [];
return;
}
const results = instance.calculate(candles);
const meta = instance.getMetadata();
indicator.series = [];
const paneIndex = nextPaneIndex++;
indicatorPanes.set(indicator.id, paneIndex);
const lineStyle = lineStyleMap[indicator.params._lineType] || 0;
const lineWidth = indicator.params._lineWidth || 2;
renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap);
const firstNonNull = results?.find(r => r !== null && r !== undefined);
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
const pane = window.dashboard.chart.panes()[paneIndex];
if (pane) {
pane.setHeight(paneHeight);
}
});
meta.plots.forEach((plot, plotIdx) => {
if (isObjectResult && typeof firstNonNull[plot.id] === 'undefined') return;
updateChartLegend();
}
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) {
const results = instance.calculate(candles);
indicator.series = [];
const lineSeries = window.dashboard.chart.addLineSeries({
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
const lineWidth = indicator.params._lineWidth || 2;
const firstNonNull = results?.find(r => r !== null && r !== undefined);
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
meta.plots.forEach((plot, plotIdx) => {
if (isObjectResult && typeof firstNonNull[plot.id] === 'undefined') return;
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
const data = [];
for (let i = 0; i < candles.length; i++) {
let value;
if (isObjectResult) {
value = results[i]?.[plot.id];
} else {
value = results[i];
}
if (value !== null && value !== undefined) {
data.push({
time: candles[i].time,
value: value
});
}
}
if (data.length === 0) return;
let series;
if (plot.type === 'histogram') {
series = window.dashboard.chart.addSeries(LightweightCharts.HistogramSeries, {
color: plotColor,
priceFormat: {
type: 'price',
precision: 4,
minMove: 0.0001
},
priceLineVisible: false,
lastValueVisible: false
}, paneIndex);
} else {
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
color: plotColor,
lineWidth: plot.width || lineWidth,
lineStyle: lineStyle,
title: '',
priceLineVisible: false,
lastValueVisible: true
});
}, paneIndex);
}
const data = [];
for (let i = 0; i < candles.length; i++) {
let value;
if (isObjectResult) {
value = results[i]?.[plot.id];
} else {
value = results[i];
}
if (value !== null && value !== undefined) {
data.push({
time: candles[i].time,
value: value
});
}
}
if (data.length > 0) {
lineSeries.setData(data);
indicator.series.push(lineSeries);
}
});
series.setData(data);
indicator.series.push(series);
});
updateChartLegend();
}
/** Update the TradingView-style legend overlay on the chart */

View File

@ -291,14 +291,14 @@ export function showSimulationMarkers() {
size: 1
});
const lineSeries = window.dashboard.chart.addLineSeries({
const lineSeries = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
color: '#2196f3',
lineWidth: 1,
lastValueVisible: false,
title: '',
priceLineVisible: false,
crosshairMarkerVisible: false
});
}, 0);
lineSeries.setData([
{ time: entryTime, value: trade.entryPrice },