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:
@ -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;
|
||||
|
||||
@ -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'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,8 @@ export class BaseIndicator {
|
||||
return {
|
||||
name: this.name,
|
||||
inputs: [],
|
||||
plots: []
|
||||
plots: [],
|
||||
displayMode: 'overlay'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
paneIndicators.forEach(({ indicator, meta, instance }, idx) => {
|
||||
if (indicator.visible === false) {
|
||||
indicator.series = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const instance = new IndicatorClass({
|
||||
type: indicator.type,
|
||||
params: indicator.params,
|
||||
name: indicator.name
|
||||
});
|
||||
const paneIndex = nextPaneIndex++;
|
||||
indicatorPanes.set(indicator.id, paneIndex);
|
||||
|
||||
const results = instance.calculate(candles);
|
||||
const meta = instance.getMetadata();
|
||||
indicator.series = [];
|
||||
renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, lineStyleMap);
|
||||
|
||||
const lineStyle = lineStyleMap[indicator.params._lineType] || 0;
|
||||
const lineWidth = indicator.params._lineWidth || 2;
|
||||
const pane = window.dashboard.chart.panes()[paneIndex];
|
||||
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 isObjectResult = firstNonNull && typeof firstNonNull === 'object';
|
||||
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
|
||||
|
||||
meta.plots.forEach((plot, plotIdx) => {
|
||||
if (isObjectResult && typeof firstNonNull[plot.id] === 'undefined') return;
|
||||
const data = [];
|
||||
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';
|
||||
|
||||
const lineSeries = window.dashboard.chart.addLineSeries({
|
||||
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
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}, paneIndex);
|
||||
}
|
||||
|
||||
series.setData(data);
|
||||
indicator.series.push(series);
|
||||
});
|
||||
|
||||
updateChartLegend();
|
||||
}
|
||||
|
||||
/** Update the TradingView-style legend overlay on the chart */
|
||||
|
||||
@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user