From b6d9bccaaf4e158285aba949903ef7603c8e1c62 Mon Sep 17 00:00:00 2001 From: Rob Date: Tue, 10 May 2022 08:12:45 -0300 Subject: [PATCH] Working and relatively glitch free. Classes are being implemented in javascript files. Python is basically one big class with no separation. --- static/charts.js | 187 +++++++++++++++++++++++++++++ static/communication.js | 79 ++++++++++++ static/data.js | 22 ++++ static/indicators.js | 258 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 546 insertions(+) create mode 100644 static/charts.js create mode 100644 static/communication.js create mode 100644 static/data.js create mode 100644 static/indicators.js diff --git a/static/charts.js b/static/charts.js new file mode 100644 index 0000000..c66f118 --- /dev/null +++ b/static/charts.js @@ -0,0 +1,187 @@ +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.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. I had to + declare it global as it is accessed through a callback.*/ + window.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(); + + //Initialise the candlestick series + this.price_history.then((ph) => { + //Initialise the candle data + this.candleSeries.setData(ph); + }) + this.bind_charts(this.chart_1); + } + + + 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_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(window.bound_charts).length; + // if bound_charts has two element in it bind them + if (bcl == 2) { this.bind2charts(); } + + // if bound_charts has two element in it bind them + if (bcl == 3) { this.bind3charts(); } + + return; + } + add_to_list(chart){ + // If the chart isn't already included in the list, add it. + if ( !window.bound_charts.includes(chart) ){ + window.bound_charts.push(chart); + } + } + + bind2charts(){ + //On change in chart 1 change chart 2 + window.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncHandler1); + function syncHandler1(e) { + // Get the barSpacing(zoom) and position of 1st chart. + let barSpacing1 = window.bound_charts[0].timeScale().getBarSpacing(); + let scrollPosition1 = window.bound_charts[0].timeScale().scrollPosition(); + // Apply barSpacing(zoom) and position to 2nd chart. + window.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 }); + } + //On change in chart 2 change chart 1 + window.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncHandler2); + function syncHandler2(e) { + // Get the barSpacing(zoom) and position of chart 2 + let barSpacing2 = window.bound_charts[1].timeScale().getBarSpacing(); + let scrollPosition2 = window.bound_charts[1].timeScale().scrollPosition(); + // Apply barSpacing(zoom) and position to chart 1 + window.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 }); + } + } + bind3charts(){ + + //On change to chart 1 change chart 2 and 3 + window.bound_charts[0].timeScale().subscribeVisibleTimeRangeChange(syncHandler); + function syncHandler(e) { + // Get the barSpacing(zoom) and position of chart 1 + let barSpacing1 = window.bound_charts[0].timeScale().getBarSpacing(); + let scrollPosition1 = window.bound_charts[0].timeScale().scrollPosition(); + // Apply barSpacing(zoom) and position to new chart + window.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 }); + window.bound_charts[2].timeScale().applyOptions({ rightOffset: scrollPosition1, barSpacing: barSpacing1 }); + } + + //On change to chart 2 change chart 1 and 3 + window.bound_charts[1].timeScale().subscribeVisibleTimeRangeChange(syncHandler2); + function syncHandler2(e) { + // Get the barSpacing(zoom) and position of chart 2 + let barSpacing2 = window.bound_charts[1].timeScale().getBarSpacing(); + let scrollPosition2 = window.bound_charts[1].timeScale().scrollPosition(); + // Apply barSpacing(zoom) and position to chart 1 and 3 + window.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 }); + window.bound_charts[2].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 }); + } + + //On change to chart 3 change chart 1 and 2 + window.bound_charts[2].timeScale().subscribeVisibleTimeRangeChange(syncHandler3); + function syncHandler3(e) { + // Get the barSpacing(zoom) and position of new chart + let barSpacing2 = window.bound_charts[2].timeScale().getBarSpacing(); + let scrollPosition2 = window.bound_charts[2].timeScale().scrollPosition(); + // Apply barSpacing(zoom) and position to parent chart + window.bound_charts[0].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 }); + window.bound_charts[1].timeScale().applyOptions({ rightOffset: scrollPosition2, barSpacing: barSpacing2 }); + } + } + + +} diff --git a/static/communication.js b/static/communication.js new file mode 100644 index 0000000..5565bf1 --- /dev/null +++ b/static/communication.js @@ -0,0 +1,79 @@ +class Communication { + constructor(interval, ocu, occ, oiu) { + // Register callbacks + this.on_candle_update = ocu; + this.on_candle_close = occ; + this.on_indctr_update = oiu; + // Open connections. + this.set_app_con(); + this.set_exchange_con(interval); + } + + candle_update(new_candle){ + this.on_candle_update(new_candle); + } + + candle_close(new_candle){ + // Send a copy of the data to the server + this.app_con.send( JSON.stringify({ message_type: "candle_data", data :new_candle })); + this.on_candle_close(new_candle); + } + + indicator_update(data){ + this.this.on_indctr_update(data); + } + + set_app_con(){ + // Create a web socket connection to our app. + this.app_con = new WebSocket('ws://localhost:5000/ws'); + this.app_con.onopen = () => this.app_con.send("Connection OK"); + + this.app_con.addEventListener('message', ev => { + if(ev.data){ + // Get the message received from server + let msg = JSON.parse(ev.data) + // Handle a request from the server + if (msg.request) { + //handle request + console.log('Received a request from the server'); + console.log(msg.request); + } + // Handle a reply from the server + if (msg.reply) { + // Handle indicator updates + if (msg.reply == 'i_updates'){ + // console.log(msg.data); + this.indicator_update(msg.data) + } + } + } + }) + } + + set_exchange_con(interval){ + let ws = "wss://stream.binance.com:9443/ws/btcusdt@kline_" + interval; + this.exchange_con = new WebSocket(ws); + + // Set the on-message call-back for the socket + this.exchange_con.onmessage = (event) => { + // Convert message to json obj + let message = JSON.parse(event.data); + // Isolate the candle data from message + let candlestick = message.k; + //console.log(message.k) + // Reformat data for lightweight charts + let new_candle={ + time: candlestick.t / 1000, + open: candlestick.o, + high: candlestick.h, + low: candlestick.l, + close: candlestick.c, + vol: candlestick.v + }; + // Update frequently updated objects + this.candle_update(new_candle); + // Update less frequently updated objects. + if (candlestick.x == true) { this.candle.close(new_candle); } + } + } +} \ No newline at end of file diff --git a/static/data.js b/static/data.js new file mode 100644 index 0000000..3d8daef --- /dev/null +++ b/static/data.js @@ -0,0 +1,22 @@ +class Data { + constructor() { + // Set the id's of the HTML elements that contain each section of the user interface. + this.chart1_id = 'chart'; + this.chart2_id = 'chart2'; + this.chart3_id = 'chart3'; + // Get and set into memory configuration and historical data from the server. + this.trading_pair = bt_data.trading_pair; + this.interval = bt_data.interval; + this.price_history = fetch('http://localhost:5000/history').then((r) => r.json()).then( (data) => { return data; } ); + this.indicators = bt_data.indicators; + // Request initialization data for the indicators. + this.indicator_data = fetch('http://localhost:5000/indicator_init').then((r) => r.json()).then( (data) => { return data; } ); + } + candle_update(){ + console.log('candle update'); + } + + candle_close(){ + console.log('candle close'); + } +} diff --git a/static/indicators.js b/static/indicators.js new file mode 100644 index 0000000..33823bb --- /dev/null +++ b/static/indicators.js @@ -0,0 +1,258 @@ +class Indicator { + constructor(name) { + // The name of the indicator. + this.name = name; + this.lines=[]; + this.hist=[]; + } + init(data){ + console.log(this.name + ': init() unimplemented.'); + } + update(data){ + console.log(this.name + ': update() unimplemented.'); + } + addHist(name, chart, color='#26a69a'){ + this.hist[name] = chart.addHistogramSeries({ + color: color, + priceFormat: { + type: 'price', + }, + priceScaleId: '', + scaleMargins: { + top: 0.0, + bottom: 0, + }, + }); + } + addLine(name, chart, color, lineWidth){ + this.lines[name] = chart.addLineSeries({ + color: color, + lineWidth: lineWidth + }); + } + setLine(name, data, value_name){ + // Initialize the data with the data object provided. + this.lines[name].setData(data); + // Isolate the last value provided and round to 2 decimals places. + let priceValue = data.at(-1).value; + this.updateDisplay(name, priceValue, value_name); + } + updateDisplay(name, priceValue, value_name){ + let rounded_value = (Math.round(priceValue * 100) / 100).toFixed(2); + // Update the data in the edit and view indicators panel + document.getElementById(this.name + '_' + value_name).value = rounded_value; + //Initialise the legend todo fix this + //set_legend_text(rounded_value, i); + } + setHist(name, data){ + this.hist[name].setData(data); + } + updateLine(name, data, value_name){ + // Update the line-set data in the chart + this.lines[name].update(data); + // Update the data in the edit and view indicators panel + this.updateDisplay(name, data, value_name); + } + updateHist(name,data){ + this.hist[name].update(data); + } +} + +class SMA extends Indicator{ + constructor(name, chart, color, lineWidth = 2) { + // Call the inherited constructor. + super(name); + + // Create a line series and append to the appropriate chart. + this.addLine('line', chart, color, lineWidth); + + // todo: pass in indicator output or something? Regiyhhster crosshair movements to the indicators output panel + //create_legend(name, chart); + } + init(data){ + this.setLine('line',data, 'value'); + } + update(data){ + this.updateLine('line', data, 'value'); + } +} + +class Linear_Regression extends SMA{ +} + +class EMA extends SMA{ +} + +class RSI extends Indicator{ + constructor(name, charts, color, lineWidth = 2) { + // Call the inherited constructor. + super(name); + // If the chart doesn't exsist create one. + if( !charts.hasOwnProperty('chart2') ) { charts.create_RSI_chart(); } + let chart = charts.chart2; + // Create a line series and append to the appropriate chart. + this.addLine('line', chart, color, lineWidth); + + // todo: pass in indicator output or somthing? Register crosshair movements to the indicators output panel + //create_legend(name, chart); + } + init(data){ + this.setLine('line',data, 'value'); + } + update(data){ + this.updateLine('line', data, 'value'); + } +} + +class MACD extends Indicator{ + constructor(name, charts, color_m, color_s, lineWidth = 2) { + // Call the inherited constructor. + super(name); + // If the chart doesn't exsist create one. + if( !charts.hasOwnProperty('chart3') ) { charts.create_MACD_chart(); } + let chart = charts.chart3; + + // Create two line series and append to the chart. + this.addLine('line_m', chart, color_m, lineWidth); + this.addLine('line_s', chart, color_s, lineWidth); + this.addHist(name, chart); + + // todo: pass in indicator output or somthing? Register crosshair movements to the indicators output panel + //create_legend(name, chart); + } + init(data){ + this.setLine('line_m',data[0], 'macd'); + this.setLine('line_s',data[1], 'signal'); + this.setHist(name, data[2]); + } + update(data){ + this.updateLine('line_m', data[0], 'macd'); + this.updateLine('line_s', data[1], 'signal'); + this.addHist(name, data[3]); + } +} + +class ATR extends Indicator{ + init(data) { + this.updateDisplay(this.name, data.at(-1).value, 'value'); + } + update(data) { + this.updateDisplay(this.name, data.value, 'value'); + + } +} + +class Volume extends Indicator{ + constructor(name, chart) { + // Call the inherited constructor. + super(name); + this.addHist(name, chart); + this.hist[name].applyOptions( { scaleMargins: { top: 0.8, bottom: 0.0} } ); + } + init(data){ + this.setHist(this.name, data); + } + update(data){ + this.updateHist(this.name, data); + } +} + +class Bolenger extends Indicator{ + constructor(name, chart, color_u, color_m, color_l, lineWidth = 2) { + // Call the inherited constructor. + super(name); + + // Create three line series and append to the chart. + this.addLine('line_u', chart, color_u, lineWidth); + this.addLine('line_m', chart, color_u, lineWidth); + this.addLine('line_l', chart, color_u, lineWidth); + } + + init(data){ + // Initialize the data with the data object provided. + this.setLine('line_u',data[0],'value1'); + this.setLine('line_m',data[1],'value2'); + this.setLine('line_l',data[2],'value3'); + } + + update(data){ + // Update the line-set data in the chart + this.line_u.update(data[0], 'value1'); + // Update the line-set data in the chart + this.line_m.update(data[1]), 'value2'; + // Update the line-set data in the chart + this.line_l.update(data[2], 'value3'); + } + +} + +class Indicators { + constructor(charts, idata) { + // Create an array to hold all the created indicators. + this.indicators = []; + // Pass a list of indicators from received from the server + // to a function that will create them. + this.create_indicators(idata.indicators, charts); + // Pass the initial indicator data to an initialize function for the indicators. + idata.indicator_data.then( (data) => { this.init_indicators(data); } ); + + + } + create_indicators(indicators, charts){ + // loop through all the indicators received from the + // server and if the are enabled and create them. + for (let name in indicators) { + + // If this indicator is hidden skip to the next one + if (!indicators[name].visible) {continue;} + + // Get the type of indicator + let i_type = indicators[name].type; + + // Call the indicator creation function + if (i_type == 'SMA') { + // The color of the line + let color = indicators[name].color; + this.indicators[name] = new SMA(name, charts.chart_1, color); + } + if (i_type == 'BOLBands') { + // The color of three lines + let color_u = bt_data.indicators[name].color_1; + let color_m = bt_data.indicators[name].color_2; + let color_l = bt_data.indicators[name].color_3; + this.indicators[name] = new Bolenger(name, charts.chart_1, color_u, color_m, color_l); + } + if (i_type == 'MACD') { + // The color of two lines + let color_m = bt_data.indicators[name].color_1; + let color_s = bt_data.indicators[name].color_2; + this.indicators[name] = new MACD(name, charts, color_m, color_s); + } + if (i_type == 'Volume') { this.indicators[name] = new Volume(name, charts.chart_1); } + if (i_type == 'ATR') { this.indicators[name] = new ATR(name); } + if (i_type == 'LREG') { + // The color of the line + let color = indicators[name].color; + this.indicators[name] = new Linear_Regression(name, charts.chart_1, color); + } + if (i_type == 'RSI') { + let color = indicators[name].color; + this.indicators[name] = new RSI(name, charts, color); + } + if (i_type == 'EMA') { + // The color of the line + let color = indicators[name].color; + this.indicators[name] = new EMA(name, charts.chart_1, color); + } + } + } + init_indicators(data){ + // Loop through all the indicators. + for (name in data){ + // Call the initialization function for each indicator. + this.indicators[name].init(data[name]['data']); + } + + } + +}