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"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BTC Trading Dashboard</title> <title>BTC Trading Dashboard</title>
<link rel="icon" href="data:,"> <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> <style>
:root { :root {
--tv-bg: #131722; --tv-bg: #131722;

View File

@ -31,7 +31,8 @@ export class ATRIndicator extends BaseIndicator {
name: 'ATR', name: 'ATR',
description: 'Average True Range - measures market volatility', description: 'Average True Range - measures market volatility',
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 100 }], 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 { return {
name: this.name, name: this.name,
inputs: [], inputs: [],
plots: [] plots: [],
displayMode: 'overlay'
}; };
} }
} }

View File

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

View File

@ -11,7 +11,8 @@ export class EMAIndicator extends BaseIndicator {
return { return {
name: 'EMA', name: 'EMA',
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }], 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: 'fastLow', color: '#00bcd4', title: 'Fast Low', width: 1 },
{ id: 'slowHigh', color: '#f44336', title: 'Slow High', width: 2 }, { id: 'slowHigh', color: '#f44336', title: 'Slow High', width: 2 },
{ id: 'slowLow', color: '#f44336', title: 'Slow Low', 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: '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' } { 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: [ plots: [
{ id: 'macd', color: '#2196f3', title: 'MACD' }, { id: 'macd', color: '#2196f3', title: 'MACD' },
{ id: 'signal', color: '#ff5722', title: 'Signal' }, { 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', name: 'RSI',
description: 'Relative Strength Index - momentum oscillator (0-100)', description: 'Relative Strength Index - momentum oscillator (0-100)',
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 14, min: 1, max: 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 { return {
name: 'SMA', name: 'SMA',
inputs: [{ name: 'period', label: 'Period', type: 'number', default: 44, min: 1, max: 500 }], 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: [ plots: [
{ id: 'k', color: '#3f51b5', title: '%K' }, { id: 'k', color: '#3f51b5', title: '%K' },
{ id: 'd', color: '#ff9800', title: '%D' } { id: 'd', color: '#ff9800', title: '%D' }
] ],
displayMode: 'pane',
paneMin: 0,
paneMax: 100
}; };
} }
} }

View File

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

View File

@ -408,6 +408,9 @@ export function removeIndicatorByIndex(index) {
removeIndicatorById(activeIndicators[index].id); removeIndicatorById(activeIndicators[index].id);
} }
let indicatorPanes = new Map();
let nextPaneIndex = 1;
export function drawIndicatorsOnChart() { export function drawIndicatorsOnChart() {
if (!window.dashboard || !window.dashboard.chart) return; if (!window.dashboard || !window.dashboard.chart) return;
@ -420,72 +423,123 @@ export function drawIndicatorsOnChart() {
const candles = window.dashboard.allData.get(window.dashboard.currentInterval); const candles = window.dashboard.allData.get(window.dashboard.currentInterval);
if (!candles || candles.length === 0) return; 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) { if (indicator.visible === false) {
indicator.series = []; indicator.series = [];
return; return;
} }
const IndicatorClass = IR?.[indicator.type]; renderIndicatorOnPane(indicator, meta, instance, candles, 0, lineStyleMap);
if (!IndicatorClass) return; });
paneIndicators.forEach(({ indicator, meta, instance }, idx) => {
if (indicator.visible === false) {
indicator.series = [];
return;
}
const instance = new IndicatorClass({ const paneIndex = nextPaneIndex++;
type: indicator.type, indicatorPanes.set(indicator.id, paneIndex);
params: indicator.params,
name: indicator.name
});
const results = instance.calculate(candles); renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap);
const meta = instance.getMetadata();
indicator.series = [];
const lineStyle = lineStyleMap[indicator.params._lineType] || 0; const pane = window.dashboard.chart.panes()[paneIndex];
const lineWidth = indicator.params._lineWidth || 2; if (pane) {
pane.setHeight(paneHeight);
}
});
updateChartLegend();
}
function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap) {
const results = instance.calculate(candles);
indicator.series = [];
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 firstNonNull = results?.find(r => r !== null && r !== undefined); const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
const isObjectResult = firstNonNull && typeof firstNonNull === 'object';
meta.plots.forEach((plot, plotIdx) => { const data = [];
if (isObjectResult && typeof firstNonNull[plot.id] === 'undefined') return; for (let i = 0; i < candles.length; i++) {
let value;
if (isObjectResult) {
value = results[i]?.[plot.id];
} else {
value = results[i];
}
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff'; if (value !== null && value !== undefined) {
data.push({
const lineSeries = window.dashboard.chart.addLineSeries({ 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, color: plotColor,
lineWidth: plot.width || lineWidth, lineWidth: plot.width || lineWidth,
lineStyle: lineStyle, lineStyle: lineStyle,
title: '', title: '',
priceLineVisible: false, priceLineVisible: false,
lastValueVisible: true lastValueVisible: true
}); }, paneIndex);
}
const data = [];
for (let i = 0; i < candles.length; i++) { series.setData(data);
let value; indicator.series.push(series);
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);
}
});
}); });
updateChartLegend();
} }
/** Update the TradingView-style legend overlay on the chart */ /** Update the TradingView-style legend overlay on the chart */

View File

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