brighter-trading/src/static/charts.js

335 lines
13 KiB
JavaScript

class Charts {
constructor(idata) {
// Unpack the initialization data.
this.chart1_id = idata.chart1_id;
this.chart2_id = idata.chart2_id;
this.chart3_id = idata.chart3_id;
this.chart4_id = idata.chart4_id;
this.trading_pair = idata.trading_pair;
this.price_history = idata.price_history;
/* A list of bound charts this is necessary for maintaining a dynamic
number of charts with their position and zoom factors bound.*/
this.bound_charts=[];
// Only the main chart is created by default.
this.create_main_chart();
}
create_main_chart() {
// Pass the id of the element to create the main chart in.
// This function returns the main chart object.
this.chart_1 = this.create_chart(this.chart1_id);
// Display the trading pair as a watermark overlaying the chart.
this.addWatermark(this.chart_1, this.trading_pair);
// - Create the candle stick series for our chart
this.candleSeries = this.chart_1.addCandlestickSeries();
// Initialize the candlestick series if price_history is available
if (this.price_history && this.price_history.length > 0) {
this.candleSeries.setData(this.price_history);
console.log('Candle series init:', this.price_history);
} else {
console.error('Price history is not available or is empty.');
}
this.bind_charts(this.chart_1);
}
update_main_chart(new_candle){
// Update candlestick series
this.candleSeries.update(new_candle);
}
create_RSI_chart(){
this.chart2 = this.create_chart(this.chart2_id, 100);
this.set_priceScale(this.chart2, 0.3, 0.0);
// Put the name of the chart in a watermark in the chart.
this.addWatermark(this.chart2, 'RSI');
// Todo: Not sure how to set this
//this.chart2.applyOptions({ priceRange: {minValue:0,maxValue:100} });
this.bind_charts(this.chart2);
}
create_MACD_chart(){
this.chart3 = this.create_chart(this.chart3_id, 100);
this.addWatermark(this.chart3, 'MACD');
this.bind_charts(this.chart3);
}
create_PercentB_chart(){
this.chart4 = this.create_chart(this.chart4_id, 100);
this.set_priceScale(this.chart4, 0.3, 0.0);
// Put the name of the chart in a watermark
this.addWatermark(this.chart4, '%B');
this.bind_charts(this.chart4);
}
create_chart(target_id, height=500){
// Accepts a target element to place the chart in.
// Returns the chart object.
let container = document.getElementById(target_id);
//Create a lightweight chart object.
let chart = LightweightCharts.createChart(container, {
width: 1000,
height: height,
crosshair: {
mode: LightweightCharts.CrosshairMode.Normal,
},
priceScale: {
borderColor: 'rgba(197, 203, 206, 0.8)',
},
timeScale: {
borderColor: 'rgba(197, 203, 206, 0.8)',
timeVisible: true,
secondsVisible: false,
barSpacing: 6
},
handleScroll: true
});
return chart;
}
set_priceScale(chart, top, bottom){
chart.priceScale('right').applyOptions({
scaleMargins: {
top: top,
bottom: bottom,
},
});
}
addWatermark(chart,text){
chart.applyOptions({
watermark: {visible: true,
color: '#DBC29E',
text: text,
fontSize: 30,
fontFamily: 'Roboto',
fontStyle: 'bold',
vertAlign: 'center'
}
});
}
bind_charts(chart){
// keep a list of charts and bind all their position and spacing.
// Add (arg1) to bound_charts
this.add_to_list(chart);
// Get the number of objects in bound_charts
let bcl = Object.keys(this.bound_charts).length;
// if bound_charts has two element in it bind them
if (bcl == 2) { this.bind2charts(); }
// if bound_charts has three elements in it bind them
if (bcl == 3) { this.bind3charts(); }
// if bound_charts has four elements in it bind them
if (bcl == 4) { this.bind4charts(); }
return;
}
add_to_list(chart){
// If the chart isn't already included in the list, add it.
if ( !this.bound_charts.includes(chart) ){
this.bound_charts.push(chart);
}
}
bind2charts(){
//On change in chart 1 change chart 2
let syncHandler1 = (e) => {
// Get the barSpacing(zoom) and position of 1st chart.
let barSpacing1 = this.bound_charts[0].timeScale().getBarSpacing();
let scrollPosition1 = this.bound_charts[0].timeScale().scrollPosition();
// Apply barSpacing(zoom) and position to 2nd chart.
this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 });
}
this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncHandler1);
//On change in chart 2 change chart 1
let syncHandler2 = (e) => {
// Get the barSpacing(zoom) and position of chart 2
let barSpacing2 = this.bound_charts[1].timeScale().getBarSpacing();
let scrollPosition2 = this.bound_charts[1].timeScale().scrollPosition();
// Apply barSpacing(zoom) and position to chart 1
this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
}
this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncHandler2);
}
bind3charts(){
//On change to chart 1 change chart 2 and 3
let syncHandler = (e) => {
// Get the barSpacing(zoom) and position of chart 1
let barSpacing1 = this.bound_charts[0].timeScale().getBarSpacing();
let scrollPosition1 = this.bound_charts[0].timeScale().scrollPosition();
// Apply barSpacing(zoom) and position to new chart
this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 });
this.bound_charts[2].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 });
}
this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncHandler);
//On change to chart 2 change chart 1 and 3
let syncHandler2 = (e) => {
// Get the barSpacing(zoom) and position of chart 2
let barSpacing2 = this.bound_charts[1].timeScale().getBarSpacing();
let scrollPosition2 = this.bound_charts[1].timeScale().scrollPosition();
// Apply barSpacing(zoom) and position to chart 1 and 3
this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
this.bound_charts[2].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
}
this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncHandler2);
//On change to chart 3 change chart 1 and 2
let syncHandler3 = (e) => {
// Get the barSpacing(zoom) and position of new chart
let barSpacing2 = this.bound_charts[2].timeScale().getBarSpacing();
let scrollPosition2 = this.bound_charts[2].timeScale().scrollPosition();
// Apply barSpacing(zoom) and position to parent chart
this.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
this.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 });
}
this.bound_charts[2].timeScale().subscribeVisibleTimeRangeChange(syncHandler3);
}
bind4charts(){
// Sync all 4 charts together
let syncFromChart = (sourceIndex) => {
return (e) => {
let barSpacing = this.bound_charts[sourceIndex].timeScale().getBarSpacing();
let scrollPosition = this.bound_charts[sourceIndex].timeScale().scrollPosition();
for (let i = 0; i < 4; i++) {
if (i !== sourceIndex) {
this.bound_charts[i].timeScale().applyOptions({ rightOffset: scrollPosition, barSpacing: barSpacing });
}
}
}
}
this.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(0));
this.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(1));
this.bound_charts[2].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(2));
this.bound_charts[3].timeScale().subscribeVisibleTimeRangeChange(syncFromChart(3));
}
// Set trade markers on chart for all trades in backtest results
setTradeMarkers(trades) {
if (!this.candleSeries) {
console.warn('Candlestick series not available');
return;
}
if (!trades || trades.length === 0) {
console.log('No trades to display as markers');
return;
}
const candleData = this.price_history || [];
if (candleData.length === 0) {
console.warn('No candle data available for markers');
return;
}
// Get the time range of loaded candle data
const minCandleTime = candleData[0].time;
const maxCandleTime = candleData[candleData.length - 1].time;
console.log(`Chart data range: ${minCandleTime} to ${maxCandleTime}`);
// Build markers array from all trades
const markers = [];
trades.forEach(trade => {
const openTime = this.dateStringToUnixTimestamp(trade.open_datetime);
const closeTime = trade.close_datetime ? this.dateStringToUnixTimestamp(trade.close_datetime) : null;
// Add entry marker if within chart data range
if (openTime && openTime >= minCandleTime && openTime <= maxCandleTime) {
const matchedOpenTime = this.findNearestCandleTime(openTime, candleData);
markers.push({
time: matchedOpenTime,
position: 'belowBar',
color: '#26a69a',
shape: 'arrowUp',
text: 'BUY @ ' + (trade.open_price ? trade.open_price.toFixed(2) : '')
});
}
// Add exit marker if within chart data range
if (closeTime && closeTime >= minCandleTime && closeTime <= maxCandleTime) {
const matchedCloseTime = this.findNearestCandleTime(closeTime, candleData);
markers.push({
time: matchedCloseTime,
position: 'aboveBar',
color: '#ef5350',
shape: 'arrowDown',
text: 'SELL @ ' + (trade.close_price ? trade.close_price.toFixed(2) : '')
});
}
});
if (markers.length === 0) {
console.log('No trades fall within the loaded chart data timespan');
return;
}
// Sort markers by time (required by lightweight-charts)
markers.sort((a, b) => a.time - b.time);
console.log(`Setting ${markers.length} trade markers on chart`);
// Set markers on the candlestick series
this.candleSeries.setMarkers(markers);
}
// Clear all trade markers from chart
clearTradeMarkers() {
if (this.candleSeries) {
this.candleSeries.setMarkers([]);
console.log('Trade markers cleared');
}
}
// Find the nearest candle time in the data
findNearestCandleTime(targetTime, candleData) {
if (!candleData || candleData.length === 0) {
return targetTime;
}
let nearestTime = candleData[0].time;
let minDiff = Math.abs(targetTime - nearestTime);
for (const candle of candleData) {
const diff = Math.abs(targetTime - candle.time);
if (diff < minDiff) {
minDiff = diff;
nearestTime = candle.time;
}
// Early exit if exact match
if (diff === 0) break;
}
return nearestTime;
}
// Convert datetime string to Unix timestamp in seconds
dateStringToUnixTimestamp(dateStr) {
if (!dateStr) return null;
try {
const date = new Date(dateStr);
return Math.floor(date.getTime() / 1000);
} catch (e) {
console.warn('Failed to parse date:', dateStr, e);
return null;
}
}
}