From 73ed1a092a6ae3c0ee7385f3d57d95a1a6d51123 Mon Sep 17 00:00:00 2001 From: Rob Date: Sun, 8 Sep 2024 04:04:58 -0300 Subject: [PATCH] got all the indicators working again --- src/BrighterTrades.py | 3 + src/app.py | 33 ++++-- src/indicators.py | 39 +++++-- src/static/communication.js | 38 +++++++ src/static/general.js | 9 +- src/static/indicators.js | 172 +++++++++++++++++++++++++++--- src/templates/indicators_hud.html | 121 ++++++++++----------- 7 files changed, 309 insertions(+), 106 deletions(-) diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py index 6bc5ebd..c83e6a0 100644 --- a/src/BrighterTrades.py +++ b/src/BrighterTrades.py @@ -533,6 +533,9 @@ class BrighterTrades: elif setting == 'edit_indicator': self.indicators.edit_indicator(user_name=user_name, params=params) + elif setting == 'delete_indicator': + self.indicators.delete_indicator(user_name=user_name, indicator_name=params) + elif setting == 'new_indicator': self.indicators.new_indicator(user_name=user_name, params=params) diff --git a/src/app.py b/src/app.py index 74d6982..229a3cd 100644 --- a/src/app.py +++ b/src/app.py @@ -155,23 +155,42 @@ def settings(): edit_indicator: Edits the properties of specific indicators. toggle_indicator: Enables or disables display of indicators in the viewport. - :return: None - Redirects to index. + :return: None - Redirects to index, or returns JSON for async requests. """ # Request the user_name from the client session. if not (user_name := session.get('user')): - return redirect('/') + return jsonify({"success": False, "message": "Not logged in"}), 401 # Return if the user is not logged in. if not brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'): - return redirect('/') + return jsonify({"success": False, "message": "User not logged in"}), 401 - # Get the setting that the web application wants to change. + # Get the setting that the application wants to change. + # todo: migrate everything to ajax and clean all this up!!! + params = request.form if not (setting := request.form.get('setting')): + if request.is_json: + setting = request.json.get('setting') + params = request.json.get('indicator') + if not setting: + return jsonify({"success": False, "message": "No setting provided"}), 400 + else: + # Redirect if this is a form submission (non-async request) + return redirect('/') + + try: + # Change the setting. + brighter_trades.adjust_setting(user_name=user_name, setting=setting, params=params) + + # Return success as JSON if called via an async request + if request.is_json: + return jsonify({"success": True}), 200 + + # Redirect if this is a form submission (non-async request) return redirect('/') - # Change the setting. - brighter_trades.adjust_setting(user_name=user_name, setting=setting, params=request.form) - return redirect('/') + except Exception as e: + return jsonify({"success": False, "message": str(e)}), 500 @app.route('/api/history', methods=['POST', 'GET']) diff --git a/src/indicators.py b/src/indicators.py index 2f76e2d..47d1233 100644 --- a/src/indicators.py +++ b/src/indicators.py @@ -151,6 +151,14 @@ class ATR(SMA): class BolBands(Indicator): + def __init__(self, name: str, indicator_type: str, properties: dict): + super().__init__(name, indicator_type, properties) + # Default display properties + self.properties.setdefault('period', 20) # The default period for the moving average + self.properties.setdefault('devup', 2) # Standard deviation for the upper band + self.properties.setdefault('devdn', 2) # Standard deviation for the lower band + self.properties.setdefault('ma', 0) # Moving average type (0 = Simple Moving Average in TA-Lib) + def calculate(self, candles: pd.DataFrame, user_name: str, num_results: int = 1) -> pd.DataFrame: """ Calculate the Bollinger Bands indicator for the given candles. @@ -177,11 +185,21 @@ class BolBands(Indicator): 'lower': lower }) + # Round all values at once + df = df.round({'upper': 2, 'middle': 2, 'lower': 2}) + # Slice the DataFrame to skip initial rows where the indicator might be undefined return df.iloc[self.properties['period']:] class MACD(Indicator): + def __init__(self, name: str, indicator_type: str, properties: dict): + super().__init__(name, indicator_type, properties) + # Default display properties + self.properties.setdefault('fast_p', 12) + self.properties.setdefault('slow_p', 26) + self.properties.setdefault('signal_p', 9) + def calculate(self, candles: pd.DataFrame, user_name: str, num_results: int = 1) -> pd.DataFrame: """ Calculate the MACD indicator for the given candles. @@ -207,6 +225,9 @@ class MACD(Indicator): 'hist': hist }) + # Replace NaN values with None for JSON compatibility + df = df.replace({np.nan: None}) + # Slice the DataFrame to skip initial rows where the indicator will be undefined return df.iloc[self.properties['signal_p']:] @@ -368,11 +389,7 @@ class Indicators: # attributes.update({'visible': False}) # # Set the data in indicators according to # brighter_trades.indicators.indicator_list[indicator] = attributes - # - if 'delete' in params: - indicator = params['delete'] - # This will delete in both indicators and config. - self.delete_indicator(indicator_name=indicator, user_name=user_name) + pass def new_indicator(self, user_name: str, params) -> None: """ @@ -505,10 +522,16 @@ class Indicators: """ if not indicator_name: raise ValueError("No indicator name provided.") - self.users.remove_indicator(indicator_name=indicator_name, user_name=user_name) - # Force reload from database to refresh cache - self.load_indicators(user_name=user_name) + # Get the user ID to filter the indicators belonging to the user + user_id = self.users.get_id(user_name) + + # Remove the indicator from the DataFrame where the name matches and the creator is the user + self.indicators = self.indicators[ + ~((self.indicators['name'] == indicator_name) & (self.indicators['creator'] == user_id)) + ] + + self.users.remove_indicator(indicator_name=indicator_name, user_name=user_name) def create_indicator(self, creator: str, name: str, kind: str, source: dict, properties: dict, visible: bool = True): diff --git a/src/static/communication.js b/src/static/communication.js index ea98a2d..8051fad 100644 --- a/src/static/communication.js +++ b/src/static/communication.js @@ -106,6 +106,44 @@ class Comms { } } + /** + * Sends a request to delete an indicator. + * @param {string} indicatorName - The name of the indicator to be deleted. + * @returns {Promise} - The response from the server. + */ + async deleteIndicator(indicatorName) { + try { + const response = await fetch('/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ setting: 'delete_indicator', indicator: indicatorName }) + }); + return await response.json(); + } catch (error) { + console.error('Error deleting indicator:', error); + return { success: false }; + } + } + + /** + * Sends a request to update an indicator's properties. + * @param {Object} indicatorData - An object containing the updated properties of the indicator. + * @returns {Promise} - The response from the server. + */ + async updateIndicator(indicatorData) { + try { + const response = await fetch('/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(indicatorData) + }); + return await response.json(); + } catch (error) { + console.error('Error updating indicator:', error); + return { success: false }; + } + } + /** * Sends a message to the application server. * @param {string} messageType - The type of the message. diff --git a/src/static/general.js b/src/static/general.js index 242fa35..33ea9c9 100644 --- a/src/static/general.js +++ b/src/static/general.js @@ -1,11 +1,4 @@ -// -//class Header { -// constructor() { -// this.height = height; -// } -//} - class User_Interface{ /* This contains all the code for our User interface. The code is separated into classes that maintain and @@ -45,7 +38,7 @@ class User_Interface{ /* Indicators contains methods that interact with indicator related menus and update indicator output in charts and panels. */ - this.indicators = new Indicators(); + this.indicators = new Indicators(this.data.comms); /* Register an Indicator callback method to receive updates from the data class. */ this.data.registerCallback_i_updates(this.indicators.update); diff --git a/src/static/indicators.js b/src/static/indicators.js index 850f97a..29f2254 100644 --- a/src/static/indicators.js +++ b/src/static/indicators.js @@ -26,6 +26,15 @@ class Indicator_Output { } this.legend[name].innerHTML = name + ' ' + val + ''; } + clear_legend(name) { + // Remove the legend div from the DOM + if (this.legend[name]) { + this.legend[name].remove(); // Remove the legend from the DOM + delete this.legend[name]; // Remove the reference from the object + } else { + console.warn(`Legend for ${name} not found.`); + } + } } iOutput = new Indicator_Output(); @@ -91,9 +100,9 @@ class Indicator { } 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; +// 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; } setHist(name, data) { @@ -112,6 +121,37 @@ class Indicator { updateHist(name, data) { this.hist[name].update(data); } + + removeFromChart(chart) { + // Ensure the chart object is passed + if (!chart) { + console.error("Chart object is missing."); + return; + } + + // Remove all line series associated with this indicator + for (let lineName in this.lines) { + if (this.lines[lineName]) { + chart.removeSeries(this.lines[lineName]); + delete this.lines[lineName]; + } + } + + // Remove all histogram series associated with this indicator + for (let histName in this.hist) { + if (this.hist[histName]) { + chart.removeSeries(this.hist[histName]); + delete this.hist[histName]; + } + } + + // Remove the legend from the crosshair (if any) + if (iOutput.legend[this.name]) { + iOutput.legend[this.name].remove(); + delete iOutput.legend[this.name]; + } + } + } class SMA extends Indicator { @@ -142,7 +182,7 @@ indicatorMap.set("SMA", SMA); class Linear_Regression extends SMA { // Inherits getIndicatorConfig from SMA } -indicatorMap.set("Linear_Regression", Linear_Regression); +indicatorMap.set("LREG", Linear_Regression); class EMA extends SMA { // Inherits getIndicatorConfig from SMA @@ -197,15 +237,41 @@ class MACD extends Indicator { } init(data) { - this.setLine('line_m', data[0], 'macd'); - this.setLine('line_s', data[1], 'signal'); - this.setHist(name, data[2]); + // Filter out rows where macd, signal, or hist are null + const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null); + + if (filteredData.length > 0) { + // Set the 'line_m' for the MACD line + this.setLine('line_m', filteredData.map(row => ({ + time: row.time, + value: row.macd + })), 'macd'); + + // Set the 'line_s' for the signal line + this.setLine('line_s', filteredData.map(row => ({ + time: row.time, + value: row.signal + })), 'signal'); + + // Set the histogram + this.setHist('hist', filteredData.map(row => ({ + time: row.time, + value: row.hist + }))); + } else { + console.error('No valid MACD data found.'); + } } update(data) { - this.updateLine('line_m', data[0][0], 'macd'); - this.updateLine('line_s', data[1][0], 'signal'); - this.updateHist(name, data[2][0]); + // Update the 'macd' line + this.updateLine('line_m', {time: data[0].time, value: data[0].macd }, 'macd'); + + // Update the 'signal' line + this.updateLine('line_s', { time: data[0].time, value: data[0].signal }, 'signal'); + + // Update the 'hist' (histogram) bar + this.updateHist('hist', {time: data[0].time, value: data[0].hist }); } } indicatorMap.set("MACD", MACD); @@ -264,11 +330,25 @@ class Bolenger extends Indicator { }; } - init(data) { - this.setLine('line_u', data[0], 'value'); - this.setLine('line_m', data[1], 'value2'); - this.setLine('line_l', data[2], 'value3'); - } + init(data) { + // Set the 'line_u' for the upper line + this.setLine('line_u', data.map(row => ({ + time: row.time, + value: row.upper + })), 'value'); + + // Set the 'line_m' for the middle line + this.setLine('line_m', data.map(row => ({ + time: row.time, + value: row.middle + })), 'value2'); + + // Set the 'line_l' for the lower line + this.setLine('line_l', data.map(row => ({ + time: row.time, + value: row.lower + })), 'value3'); +} update(data) { this.updateLine('line_u', data[0][0], 'value'); @@ -276,12 +356,13 @@ class Bolenger extends Indicator { this.updateLine('line_l', data[2][0], 'value3'); } } -indicatorMap.set("Bolenger", Bolenger); +indicatorMap.set("BOLBands", Bolenger); class Indicators { - constructor() { + constructor(comms) { // Contains instantiated indicators. this.i_objs = {}; + this.comms = comms; } create_indicators(indicators, charts, bt_data) { @@ -333,12 +414,68 @@ class Indicators { } } + // This updates all the indicator data update(updates){ for (name in updates){ window.UI.indicators.i_objs[name].update(updates[name]); } } + deleteIndicator(indicator, event) { + this.comms.deleteIndicator(indicator).then(response => { + if (response.success) { + const indicatorElement = event.target.closest('.indicator-row'); + indicatorElement.remove(); // Remove from DOM + + // Remove the indicator from the chart and legend + if (this.i_objs[indicator]) { + let chart; + + // Determine which chart the indicator is on, based on its type + if (indicator.includes('RSI')) { + chart = window.UI.charts.chart2; // Assume RSI is on chart2 + } else if (indicator.includes('MACD')) { + chart = window.UI.charts.chart3; // Assume MACD is on chart3 + } else { + chart = window.UI.charts.chart_1; // Default to the main chart + } + + // Pass the correct chart object when removing the indicator + this.i_objs[indicator].removeFromChart(chart); + + // Remove the indicator object from i_objs + delete this.i_objs[indicator]; + + // Optionally: Clear the legend entry + iOutput.clear_legend(indicator); + } + } else { + alert('Failed to delete the indicator.'); + } + }); + } + + // This updates a specific indicator + updateIndicator(event) { + const row = event.target.closest('.indicator-row'); + const inputs = row.querySelectorAll('input, select'); + + // Gather input data + const formObj = {}; + inputs.forEach(input => { + formObj[input.name] = input.type === 'checkbox' ? input.checked : input.value; + }); + + this.comms.updateIndicator(formObj).then(response => { + if (response.success) { + alert('Indicator updated successfully.'); + } else { + alert('Failed to update the indicator.'); + } + }); + } + + add_to_list(){ // Adds user input to a list and displays it in a HTML element. // Used in the Create indicator panel (!)Called from inline html. @@ -365,6 +502,7 @@ class Indicators { }else{ document.getElementById("new_prop_list").insertAdjacentHTML('beforeend', ',' + JSON.stringify(p)); } } + // Call to display Create new signal dialog. open_form() { // Show the form diff --git a/src/templates/indicators_hud.html b/src/templates/indicators_hud.html index cca0ecb..86a0b0d 100644 --- a/src/templates/indicators_hud.html +++ b/src/templates/indicators_hud.html @@ -1,80 +1,69 @@ -
- +
+ -
-
- -
-
- -
-
-

Indicators

-
-
-

Name

-
-
-

Type

-
-
-

Properties

-
- - {% for indicator in indicator_list %} -
- -
- - +
+
+ +
+
+ +
+
+

Indicators

+
+
+

Name

+
+

Type

+
+
+

Properties

+
+ + {% for indicator in indicator_list %} +
+ +
+ + +
-
{{indicator}}
+
{{indicator}}
- {% for property in indicator_list[indicator] %} + {% for property in indicator_list[indicator] %} {% set list1 = property.split('_') %} -
+
{% if property=='type' %} - + {% elif property=='ma' %} - - {% elif property=='color' or list1[0]=='color' %} - + + {% elif property=='color' or list1[0]=='color' %} + {% elif property=='period' %} - + {% elif property=='visible' %} - + {% elif property=='value' %} - + {% else %} - - {%endif%} -
- {% endfor %} - + + {% endif %} +
{% endfor %} - -
-
+
+ {% endfor %} + +
+
-
\ No newline at end of file +