From 89e0f8b849f03e5f39672d2dc2aae9a09d5de0eb Mon Sep 17 00:00:00 2001 From: Rob Date: Wed, 9 Oct 2024 09:09:52 -0300 Subject: [PATCH] I decoupled the comms class from a few other classes. I updated some of the ui in regard to this indicator readouts. An just about to attempt to break up the strategies class. --- requirements.txt | 19 +- src/BrighterTrades.py | 29 +- src/DataCache_v3.py | 1 - src/Users.py | 3 +- src/app.py | 4 +- src/backtesting.py | 345 ++++++++-- src/indicators.py | 1 + src/static/Strategies.js | 958 ++++++++++++-------------- src/static/backtesting.js | 192 +++++- src/static/brighterStyles.css | 18 +- src/static/charts.js | 13 +- src/static/communication.js | 102 +-- src/static/custom_blocks.js | 427 ++++++++++++ src/static/data.js | 79 ++- src/static/general.js | 42 +- src/static/indicator_blocks.js | 38 + src/static/indicators.js | 289 ++++++-- src/static/json_generators.js | 114 +++ src/static/python_generators.js | 178 +++++ src/static/signals.js | 351 ++++------ src/static/trade.js | 9 +- src/templates/backtest_popup.html | 105 +++ src/templates/backtesting_hud.html | 10 +- src/templates/index.html | 2 + src/templates/indicators_hud.html | 13 +- src/templates/new_strategy_popup.html | 38 +- src/templates/price_chart.html | 8 +- 27 files changed, 2369 insertions(+), 1019 deletions(-) create mode 100644 src/static/custom_blocks.js create mode 100644 src/static/indicator_blocks.js create mode 100644 src/static/json_generators.js create mode 100644 src/static/python_generators.js create mode 100644 src/templates/backtest_popup.html diff --git a/requirements.txt b/requirements.txt index f217fdf..30bb2d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,11 @@ -numpy==1.24.3 -flask==2.3.2 -flask_cors==3.0.10 -flask_sock==0.7.0 +numpy<2.0.0 +flask==3.0.3 config~=0.5.1 -PyYAML~=6.0 +PyYAML==6.0.2 requests==2.30.0 -pandas==2.0.1 +pandas==2.2.3 passlib~=1.7.4 -SQLAlchemy==2.0.13 -ccxt==4.3.65 -email-validator~=2.2.0 -TA-Lib~=0.4.32 -bcrypt~=4.2.0 +ccxt==4.4.8 + +pytz==2024.2 +backtrader==1.9.78.123 \ No newline at end of file diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py index 5091329..5863e7e 100644 --- a/src/BrighterTrades.py +++ b/src/BrighterTrades.py @@ -48,7 +48,8 @@ class BrighterTrades: self.strategies = Strategies(self.data, self.trades) # Object responsible for testing trade and strategies data. - self.backtester = Backtester() + self.backtester = Backtester(data_cache=self.data, strategies=self.strategies) + self.backtests = {} # In-memory storage for backtests (replace with DB access in production) def create_new_user(self, email: str, username: str, password: str) -> bool: """ @@ -219,6 +220,7 @@ class BrighterTrades: chart_view = self.users.get_chart_view(user_name=user_name) indicator_types = self.indicators.get_available_indicator_types() available_indicators = self.indicators.get_indicator_list(user_name) + exchange = self.exchanges.get_exchange(ename=chart_view.get('exchange'), uname=user_name) if not chart_view: chart_view = {'timeframe': '', 'exchange_name': '', 'market': ''} @@ -234,7 +236,9 @@ class BrighterTrades: 'exchange_name': chart_view.get('exchange_name'), 'trading_pair': chart_view.get('market'), 'user_name': user_name, - 'public_exchanges': self.exchanges.get_public_exchanges() + 'public_exchanges': self.exchanges.get_public_exchanges(), + 'intervals': exchange.intervals if exchange else [], + 'symbols': exchange.get_symbols() if exchange else {} } return js_data @@ -569,6 +573,12 @@ class BrighterTrades: """ Return a JSON object of all the trades in the trades instance.""" return self.trades.get_trades('dict') + def delete_backtest(self, msg_data): + """ Delete an existing backtest. """ + backtest_name = msg_data.get('name') + if backtest_name in self.backtests: + del self.backtests[backtest_name] + def adjust_setting(self, user_name: str, setting: str, params: Any): """ Adjusts the specified setting for a user. @@ -638,12 +648,14 @@ class BrighterTrades: # self.candles.set_cache(user_name=user_name) return - def process_incoming_message(self, msg_type: str, msg_data: dict | str) -> dict | None: + def process_incoming_message(self, msg_type: str, msg_data: dict | str, socket_conn) -> dict | None: + """ Processes an incoming message and performs the corresponding actions based on the message type and data. :param msg_type: The type of the incoming message. :param msg_data: The data associated with the incoming message. + :param socket_conn: The WebSocket connection to send updates back to the client. :return: dict|None - A dictionary containing the response message and data, or None if no response is needed or no data is found to ensure the WebSocket channel isn't burdened with unnecessary communication. @@ -706,6 +718,17 @@ class BrighterTrades: r_data = self.connect_or_config_exchange(user_name=user, exchange_name=exchange, api_keys=keys) return standard_reply("Exchange_connection_result", r_data) + # Handle backtest operations + if msg_type == 'submit_backtest': + user_id = self.get_user_info(user_name=msg_data['user_name'], info='User_id') + # Pass socket_conn to the backtest handler + result = self.backtester.handle_backtest_message(user_id, msg_data, socket_conn) + return standard_reply("backtest_submitted", result) + + if msg_type == 'delete_backtest': + self.delete_backtest(msg_data) + return standard_reply("backtest_deleted", {}) + if msg_type == 'reply': # If the message is a reply log the response to the terminal. print(f"\napp.py:Received reply: {msg_data}") diff --git a/src/DataCache_v3.py b/src/DataCache_v3.py index 0379d0e..f12e30e 100644 --- a/src/DataCache_v3.py +++ b/src/DataCache_v3.py @@ -8,7 +8,6 @@ from typing import Any, Tuple, List, Optional import pandas as pd import numpy as np -import json from indicators import Indicator, indicators_registry from shared_utilities import unix_time_millis diff --git a/src/Users.py b/src/Users.py index 7415ace..dd2de38 100644 --- a/src/Users.py +++ b/src/Users.py @@ -562,7 +562,8 @@ class UserIndicatorManagement(UserExchangeManagement): else: raise ValueError(f'{specific_property} is not a specific property of chart_views') - self.modify_user_data(username=user_name, field_name='chart_views', new_data=chart_view) + chart_view_str = json.dumps(chart_view) + self.modify_user_data(username=user_name, field_name='chart_views', new_data=chart_view_str) class Users(UserIndicatorManagement): diff --git a/src/app.py b/src/app.py index b6bf3b8..eb9527a 100644 --- a/src/app.py +++ b/src/app.py @@ -129,8 +129,8 @@ def ws(socket_conn): socket_conn.send(json.dumps({"success": False, "message": "User not logged in"})) return - # Process the incoming message based on the type - resp = brighter_trades.process_incoming_message(msg_type=msg_type, msg_data=msg_data) + # Process the incoming message based on the type, passing socket_conn + resp = brighter_trades.process_incoming_message(msg_type=msg_type, msg_data=msg_data, socket_conn=socket_conn) # Send the response back to the client if resp: diff --git a/src/backtesting.py b/src/backtesting.py index 69d6d29..ae53ebe 100644 --- a/src/backtesting.py +++ b/src/backtesting.py @@ -1,53 +1,312 @@ -from dataclasses import dataclass, field, asdict -from itertools import count +import ast +import json +import re - -@dataclass -class Order: - symbol: str - clientOrderId: hex - transactTime: float - price: float - origQty: float - executedQty: float - cummulativeQuoteQty: float - status: str - timeInForce: str - type: str - side: str - orderId: int = field(default_factory=count().__next__) +import backtrader as bt +import datetime as dt +from DataCache_v3 import DataCache +from Strategies import Strategies +import threading +import numpy as np class Backtester: - def __init__(self): - self.orders = [] + def __init__(self, data_cache: DataCache, strategies: Strategies): + """ Initialize the Backtesting class with a cache for back-tests """ + self.data_cache = data_cache + self.strategies = strategies + # Create a cache for storing back-tests + self.data_cache.create_cache('tests', cache_type='row', size_limit=100, + default_expiration=dt.timedelta(days=1), + eviction_policy='evict') - def create_test_order(self, symbol, side, type, timeInForce, quantity, price=None, newClientOrderId=None): - order = Order(symbol=symbol, clientOrderId=newClientOrderId, transactTime=1507725176595, price=price, - origQty=quantity, executedQty=quantity, cummulativeQuoteQty=(quantity * price), status='FILLED', - timeInForce=timeInForce, type=type, side=side) - self.orders.append(order) - return asdict(order) + def get_default_chart_view(self, user_name): + """Fetch default chart view if no specific source is provided.""" + return self.data_cache.get_datacache_item( + item_name='chart_view', cache_name='users', filter_vals=('user_name', user_name)) - def create_order(self, symbol, side, type, timeInForce, quantity, price=None, newClientOrderId=None): - order = Order(symbol=symbol, clientOrderId=newClientOrderId, transactTime=1507725176595, price=price, - origQty=quantity, executedQty=quantity, cummulativeQuoteQty=(quantity * price), status='FILLED', - timeInForce=timeInForce, type=type, side=side) - self.orders.append(order) - return asdict(order) + def cache_backtest(self, user_name, backtest_name, backtest_data): + """ Cache the backtest data for a user """ + columns = ('user_name', 'strategy_name', 'start_time', 'capital', 'commission', 'results') + values = ( + backtest_data.get('user_name'), + backtest_data.get('strategy'), + backtest_data.get('start_date'), + backtest_data.get('capital', 10000), # Default capital if not provided + backtest_data.get('commission', 0.001), # Default commission + None # No results yet; will be filled in after backtest completion + ) + cache_key = f"backtest:{user_name}:{backtest_name}" + self.data_cache.insert_row_into_cache('tests', columns, values, key=cache_key) - def get_order(self, symbol, orderId): - for order in self.orders: - if order.symbol == symbol: - if order.orderId == orderId: - return asdict(order) - return None + def map_user_strategy(self, user_strategy): + """Maps user strategy details into a Backtrader-compatible strategy class.""" - def get_precision(self, symbol=None): - return 3 + class MappedStrategy(bt.Strategy): + params = ( + ('initial_cash', user_strategy['params'].get('initial_cash', 10000)), + ('commission', user_strategy['params'].get('commission', 0.001)), + ) - def get_min_notional_qty(self, symbol=None): - return 10 + def __init__(self): + # Extract unique sources (exchange, symbol, timeframe) from blocks + self.sources = self.extract_sources(user_strategy) - def get_min_qty(self, symbol=None): - return 0.001 + # Map of source to data feed (used later in next()) + self.source_data_feed_map = {} + + def extract_sources(self, user_strategy): + """Extracts unique sources from the strategy.""" + sources = [] + for block in user_strategy.get('blocks', []): + if block.get('type') in ['last_candle_value', 'trade_action']: + source = self.extract_source_from_block(block) + if source and source not in sources: + sources.append(source) + elif block.get('type') == 'target_market': + target_source = self.extract_target_market(block) + if target_source and target_source not in sources: + sources.append(target_source) + return sources + + def extract_source_from_block(self, block): + """Extract source (exchange, symbol, timeframe) from a strategy block.""" + source = {} + if block.get('type') == 'last_candle_value': + source = block.get('SOURCE', None) + # If SOURCE is missing, use the trade target or default + if not source: + source = self.get_default_chart_view(self.user_name) # Fallback to default + return source + + def extract_target_market(self, block): + """Extracts target market data (timeframe, exchange, symbol) from the trade_action block.""" + target_market = block.get('target_market', {}) + return { + 'timeframe': target_market.get('TF', '5m'), + 'exchange': target_market.get('EXC', 'Binance'), + 'symbol': target_market.get('SYM', 'BTCUSD') + } + + def next(self): + """Execute trading logic using the compiled strategy.""" + try: + exec(self.compiled_logic, {'self': self, 'data_feeds': self.source_data_feed_map}) + except Exception as e: + print(f"Error executing trading logic: {e}") + + return MappedStrategy + + def prepare_data_feed(self, start_date: str, sources: list, user_name: str): + """ + Prepare multiple data feeds based on the start date and list of sources. + """ + try: + # Convert the start date to a datetime object + start_dt = dt.datetime.strptime(start_date, '%Y-%m-%dT%H:%M') + + # Dictionary to map each source to its corresponding data feed + data_feeds = {} + + for source in sources: + # Ensure exchange details contain required keys (fallback if missing) + asset = source.get('asset', 'BTCUSD') + timeframe = source.get('timeframe', '5m') + exchange = source.get('exchange', 'Binance') + + # Fetch OHLC data from DataCache based on the source + ex_details = [asset, timeframe, exchange, user_name] + data = self.data_cache.get_records_since(start_dt, ex_details) + + # Return the data as a Pandas DataFrame compatible with Backtrader + data_feeds[tuple(ex_details)] = data + + return data_feeds + + except Exception as e: + print(f"Error preparing data feed: {e}") + return None + + def run_backtest(self, strategy, data_feed_map, msg_data, user_name, callback, socket_conn): + """ + Runs a backtest using Backtrader on a separate thread and calls the callback with the results when finished. + Also sends progress updates to the client via WebSocket. + """ + + def execute_backtest(): + cerebro = bt.Cerebro() + + # Add the mapped strategy to the backtest + cerebro.addstrategy(strategy) + + # Add all the data feeds to Cerebro + total_bars = 0 # Total number of data points (bars) across all feeds + for source, data_feed in data_feed_map.items(): + bt_feed = bt.feeds.PandasData(dataname=data_feed) + cerebro.adddata(bt_feed) + strategy.source_data_feed_map[source] = bt_feed + total_bars = max(total_bars, len(data_feed)) # Get the total bars from the largest feed + + # Capture initial capital + initial_capital = cerebro.broker.getvalue() + + # Progress tracking variables + current_bar = 0 + last_progress = 0 + + # Custom next function to track progress (if you have a large dataset) + def track_progress(): + nonlocal current_bar, last_progress + current_bar += 1 + progress = (current_bar / total_bars) * 100 + + # Send progress update every 10% increment + if progress >= last_progress + 10: + last_progress += 10 + socket_conn.send(json.dumps({"progress": int(last_progress)})) + + # Attach the custom next method to the strategy + strategy.next = track_progress + + # Run the backtest + print("Running backtest...") + start_time = dt.datetime.now() + cerebro.run() + end_time = dt.datetime.now() + + # Extract performance metrics + final_value = cerebro.broker.getvalue() + run_duration = (end_time - start_time).total_seconds() + + # Send 100% completion + socket_conn.send(json.dumps({"progress": 100})) + + # Prepare the results to pass into the callback + callback({ + "initial_capital": initial_capital, + "final_portfolio_value": final_value, + "run_duration": run_duration + }) + + # Map the user strategy and prepare the data feeds + sources = strategy.extract_sources() + data_feed_map = self.prepare_data_feed(msg_data['start_date'], sources, user_name) + + # Run the backtest in a separate thread + thread = threading.Thread(target=execute_backtest) + thread.start() + + def handle_backtest_message(self, user_id, msg_data, socket_conn): + user_name = msg_data.get('user_name') + backtest_name = f"{msg_data['strategy']}_backtest" + + # Cache the backtest data + self.cache_backtest(user_name, backtest_name, msg_data) + + # Fetch the strategy using user_id and strategy_name + strategy_name = msg_data.get('strategy') + user_strategy = self.strategies.get_strategy_by_name(user_id=user_id, name=strategy_name) + + if not user_strategy: + return {"error": f"Strategy {strategy_name} not found for user {user_name}"} + + # Extract sources from the strategy JSON + sources = self.extract_sources_from_strategy_json(user_strategy.get('strategy_json')) + + if not sources: + return {"error": "No valid sources found in the strategy."} + + # Prepare the data feed map based on extracted sources + data_feed_map = self.prepare_data_feed(msg_data['start_date'], sources, user_name) + + if data_feed_map is None: + return {"error": "Data feed could not be prepared. Please check the data source."} + + # Map the user strategy to a Backtrader strategy class + mapped_strategy = self.map_user_strategy(user_strategy) + + # Define the callback function to handle backtest completion + def backtest_callback(results): + self.store_backtest_results(user_name, backtest_name, results) + self.update_strategy_stats(user_id, strategy_name, results) + + # Run the backtest and pass the callback function, msg_data, and user_name + self.run_backtest(mapped_strategy, data_feed_map, msg_data, user_name, backtest_callback, socket_conn) + + return {"reply": "backtest_started"} + + def extract_sources_from_strategy_json(self, strategy_json): + sources = [] + + # Parse the JSON strategy to extract sources + def traverse_blocks(blocks): + for block in blocks: + if block['type'] == 'source': + source = { + 'timeframe': block['fields'].get('TF'), + 'exchange': block['fields'].get('EXC'), + 'symbol': block['fields'].get('SYM') + } + sources.append(source) + # Recursively traverse inputs and statements + if 'inputs' in block: + traverse_blocks(block['inputs'].values()) + if 'statements' in block: + traverse_blocks(block['statements'].values()) + if 'next' in block: + traverse_blocks([block['next']]) + + traverse_blocks(strategy_json) + return sources + + def update_strategy_stats(self, user_id, strategy_name, results): + """ Update the strategy stats with the backtest results """ + strategy = self.strategies.get_strategy_by_name(user_id=user_id, name=strategy_name) + + if strategy: + initial_capital = results['initial_capital'] + final_value = results['final_portfolio_value'] + returns = np.array(results['returns']) + equity_curve = np.array(results['equity_curve']) + trades = results['trades'] + + total_return = (final_value - initial_capital) / initial_capital * 100 + + risk_free_rate = 0.0 + mean_return = np.mean(returns) + std_return = np.std(returns) + sharpe_ratio = (mean_return - risk_free_rate) / std_return if std_return != 0 else 0 + + running_max = np.maximum.accumulate(equity_curve) + drawdowns = (equity_curve - running_max) / running_max + max_drawdown = np.min(drawdowns) * 100 + + num_trades = len(trades) + wins = sum(1 for trade in trades if trade['profit'] > 0) + losses = num_trades - wins + win_loss_ratio = wins / losses if losses != 0 else wins + + stats = { + 'total_return': total_return, + 'sharpe_ratio': sharpe_ratio, + 'max_drawdown': max_drawdown, + 'number_of_trades': num_trades, + 'win_loss_ratio': win_loss_ratio, + } + + strategy.update_stats(stats) + else: + print(f"Strategy {strategy_name} not found for user {user_id}.") + + def store_backtest_results(self, user_name, backtest_name, results): + """ Store the backtest results in the cache """ + cache_key = f"backtest:{user_name}:{backtest_name}" + + filter_vals = [('tbl_key', cache_key)] + backtest_data = self.data_cache.get_rows_from_cache('tests', filter_vals) + + if not backtest_data.empty: + backtest_data['results'] = results + self.data_cache.insert_row_into_cache('tests', backtest_data.keys(), backtest_data.values(), key=cache_key) + else: + print(f"Backtest {backtest_name} not found in cache.") diff --git a/src/indicators.py b/src/indicators.py index adc2d6b..10af0ae 100644 --- a/src/indicators.py +++ b/src/indicators.py @@ -207,6 +207,7 @@ class MACD(Indicator): self.properties.setdefault('fast_p', 12) self.properties.setdefault('slow_p', 26) self.properties.setdefault('signal_p', 9) + self.properties.setdefault('color_1', generate_random_color()) # Upper band self.properties.setdefault('color_2', generate_random_color()) # Middle band self.properties.setdefault('color_3', generate_random_color()) # Lower band diff --git a/src/static/Strategies.js b/src/static/Strategies.js index baee611..e9579c0 100644 --- a/src/static/Strategies.js +++ b/src/static/Strategies.js @@ -1,39 +1,196 @@ class Strategies { - constructor(target_id) { + constructor() { + // The HTML element that displays the strategies. + this.target_el = null; + // The HTML element that displays the creation form. + this.formElement = null; + // The class responsible for handling server communications. + this.comms = null; + // The class responsible for keeping user data. + this.data = null; // The list of strategies. this.strategies = []; - // The HTML element id that displays the strategies. - this.target_id = target_id; - // The HTML element that displays the strategies. - this.target = null; + // The Blockly workspace. this.workspace = null; - + // Flag to indicate if the instance is initialized. + this._initialized = false; } - // Create the Blockly workspace and define custom blocks - createWorkspace() { + + /** + * Initializes the Strategies instance with necessary dependencies. + * @param {string} target_id - The ID of the HTML element where strategies will be displayed. + * @param {string} formElId - The ID of the HTML element for the strategy creation form. + * @param {Object} data - An object containing user data and communication instances. + */ + initialize(target_id, formElId, data) { + try { + // Get the target element for displaying strategies + this.target_el = document.getElementById(target_id); + if (!this.target_el) { + throw new Error(`Element for displaying strategies "${target_id}" not found.`); + } + + // Get the form element for strategy creation + this.formElement = document.getElementById(formElId); + if (!this.formElement) { + throw new Error(`Strategies form element "${formElId}" not found.`); + } + + if (!data || typeof data !== 'object') { + throw new Error("Invalid data object provided for initialization."); + } + this.data = data; + + if (!this.data.user_name || typeof this.data.user_name !== 'string') { + throw new Error("Invalid user_name provided in data object."); + } + + this.comms = this.data?.comms; + if (!this.comms) { + throw new Error('Communications instance not provided in data.'); + } + + // Register handlers with Comms for specific message types + this.comms.on('strategy_created', this.handleStrategyCreated.bind(this)); + this.comms.on('strategy_updated', this.handleStrategyUpdated.bind(this)); + this.comms.on('strategy_deleted', this.handleStrategyDeleted.bind(this)); + this.comms.on('updates', this.handleUpdates.bind(this)); + + // Fetch saved strategies from the server + this.fetchSavedStrategies(); + this._initialized = true; + } catch (error) { + console.error("Error initializing Strategies instance:", error.message); + } + } + /** + * Handles the creation of a new strategy. + * @param {Object} data - The data for the newly created strategy. + */ + handleStrategyCreated(data) { + console.log("New strategy created:", data); + // Add the new strategy to the list without fetching from the server again + this.strategies.push(data); + // Update the UI + this.updateHtml(); + } + + /** + * Handles updates to the strategy itself (e.g., configuration changes). + * @param {Object} data - The updated strategy data. + */ + handleStrategyUpdated(data) { + console.log("Strategy updated:", data); + const index = this.strategies.findIndex(strategy => strategy.id === data.id); + if (index !== -1) { + this.strategies[index] = data; + } else { + this.strategies.push(data); // Add if not found + } + this.updateHtml(); + } + + /** + * Handles the deletion of a strategy. + * @param {Object} data - The data for the deleted strategy. + */ + handleStrategyDeleted(data) { + try { + console.log("Strategy deleted:", data); + + // Remove the strategy from the local array + this.strategies = this.strategies.filter(strat => strat.name !== data.strategy_name); + + // Update the UI + this.updateHtml(); + } catch (error) { + console.error("Error handling strategy deletion:", error.message); + } + } + + /** + * Handles batch updates for strategies, such as multiple configuration or performance updates. + * @param {Object} data - The data containing batch updates for strategies. + */ + handleUpdates(data) { + const { stg_updts } = data; + if (stg_updts) { + stg_updts.forEach(strategy => this.handleStrategyUpdated(strategy)); + } + } + + /** + * Returns all available strategies. + * @returns {Object[]} - The list of available strategies. + */ + getAvailableStrategies() { + return this.strategies; + } + + /** + * Creates the Blockly workspace with custom blocks and generators. + * Ensures required elements are present in the DOM and initializes the workspace. + * @async + * @throws {Error} If required elements ('blocklyDiv' or 'toolbox') are not found. + */ + async createWorkspace() { + // Ensure 'blocklyDiv' exists in the DOM if (!document.getElementById('blocklyDiv')) { console.error("blocklyDiv is not loaded."); return; } - // Dispose of the existing workspace before creating a new one + // Dispose of the existing workspace if it exists if (this.workspace) { this.workspace.dispose(); } - // Define custom blocks before initializing the workspace - this.defineCustomBlocks(); - this.defineIndicatorBlocks(); + try { + // Load all modules concurrently to reduce loading time + const [customBlocksModule, indicatorBlocksModule, pythonGeneratorsModule, jsonGeneratorsModule] = await Promise.all([ + import('./custom_blocks.js'), + import('./indicator_blocks.js'), + import('./python_generators.js'), + import('./json_generators.js') + ]); - // Initialize Blockly workspace + // Define custom blocks + customBlocksModule.defineCustomBlocks(); + indicatorBlocksModule.defineIndicatorBlocks(); + pythonGeneratorsModule.definePythonGenerators(); + jsonGeneratorsModule.defineJsonGenerators(); + } catch (error) { + console.error("Error loading Blockly modules: ", error); + return; + } + + // Ensure 'toolbox' exists in the DOM + const toolboxElement = document.getElementById('toolbox'); + if (!toolboxElement) { + console.error("toolbox is not loaded."); + return; + } + + // Initialize the Blockly workspace this.workspace = Blockly.inject('blocklyDiv', { - toolbox: document.getElementById('toolbox'), + toolbox: toolboxElement, scrollbars: true, trashcan: true, + grid: { + spacing: 20, + length: 3, + colour: '#ccc', + snap: true + }, + zoom: { + controls: true, + wheel: true, + startScale: 1.0, + maxScale: 3, + minScale: 0.3, + scaleSpeed: 1.2 + } }); - - // Define Python generators after workspace initialization - this.definePythonGenerators(); } // Resize the Blockly workspace @@ -46,93 +203,231 @@ class Strategies { } } - // Generate Python code from the Blockly workspace and return as JSON + /** + * Generates the strategy data including Python code, JSON representation, and workspace XML. + * @returns {string} - A JSON string containing the strategy data. + */ generateStrategyJson() { if (!this.workspace) { console.error("Workspace is not available."); return; } + const nameElement = document.getElementById('name_box'); + if (!nameElement) { + console.error("Name input element (name_box) is not available."); + return; + } + const strategyName = nameElement.value; + + // Initialize code generators Blockly.Python.init(this.workspace); + Blockly.JSON.init(this.workspace); + + // Generate code and data representations const pythonCode = Blockly.Python.workspaceToCode(this.workspace); + const strategyJson = this._generateStrategyJsonFromWorkspace(); const workspaceXml = Blockly.Xml.workspaceToDom(this.workspace); const workspaceXmlText = Blockly.Xml.domToText(workspaceXml); + // Compile and return all information as a JSON string return JSON.stringify({ - name: document.getElementById('name_box').value, + name: strategyName, code: pythonCode, + strategy_json: strategyJson, workspace: workspaceXmlText }); } - // Restore the Blockly workspace from XML - restoreWorkspaceFromXml(workspaceXmlText) { + + /** + * Generates a JSON representation of the strategy from the workspace. + * @private + * @returns {Object[]} - An array of JSON objects representing the top-level blocks. + */ + _generateStrategyJsonFromWorkspace() { + const topBlocks = this.workspace.getTopBlocks(true); + const strategyJson = topBlocks.map(block => this._blockToJson(block)); + return strategyJson; + } + + /** + * Recursively converts a block and its connected blocks into JSON. + * @private + * @param {Blockly.Block} block - The block to convert. + * @returns {Object} - The JSON representation of the block. + */ + _blockToJson(block) { + const json = { + type: block.type, + fields: {}, + inputs: {}, + statements: {} + }; + + // Get field values + block.inputList.forEach(input => { + if (input.fieldRow) { + input.fieldRow.forEach(field => { + if (field.name && field.getValue) { + json.fields[field.name] = field.getValue(); + } + }); + } + + if (input.connection && input.connection.targetBlock()) { + const targetBlock = input.connection.targetBlock(); + if (input.type === Blockly.INPUT_VALUE) { + json.inputs[input.name] = this._blockToJson(targetBlock); + } else if (input.type === Blockly.NEXT_STATEMENT) { + json.statements[input.name] = this._blockToJson(targetBlock); + } + + } + }); + + // Handle next blocks (in statements) + if (block.getNextBlock()) { + json.next = this.blockToJson(block.getNextBlock()); + } + + return json; + } + + /** + * Restores the Blockly workspace from an XML string. + * @param {string} workspaceXmlText - The XML text representing the workspace. + */ + _restoreWorkspaceFromXml(workspaceXmlText) { try { if (!this.workspace) { console.error("Cannot restore workspace: Blockly workspace is not initialized."); return; } + // Parse the XML text into an XML DOM object const workspaceXml = Blockly.utils.xml.textToDom(workspaceXmlText); - this.workspace.clear(); // Clear the current workspace - Blockly.Xml.domToWorkspace(workspaceXml, this.workspace); // Load the new XML into the workspace + // Validate that the XML is not empty and has child nodes + if (!workspaceXml || !workspaceXml.hasChildNodes()) { + console.error('Invalid workspace XML provided.'); + alert('The provided workspace data is invalid and cannot be loaded.'); + return; + } + + // Clear the current workspace + this.workspace.clear(); + + // Load the XML DOM into the workspace + Blockly.Xml.domToWorkspace(workspaceXml, this.workspace); } catch (error) { - console.error('Error restoring workspace from XML:', error); + // Handle specific errors if possible + if (error instanceof SyntaxError) { + console.error('Syntax error in workspace XML:', error.message); + alert('There was a syntax error in the workspace data. Please check the data and try again.'); + } else { + console.error('Unexpected error restoring workspace:', error); + alert('An unexpected error occurred while restoring the workspace.'); + } } } - // Fetch saved strategies + /** + * Fetches the saved strategies from the server. + */ fetchSavedStrategies() { - if (window.UI.data.comms) { - window.UI.data.comms.sendToApp('request', { request: 'strategies', user_name: window.UI.data.user_name }); + if (this.comms) { + try { + // Prepare request data, including user name if available + const requestData = { + request: 'strategies', + user_name: this.data?.user_name + }; + // Send request to application server + this.comms.sendToApp('request', requestData); + } catch (error) { + console.error("Error fetching saved strategies:", error.message); + alert('Unable to connect to the server. Please check your connection or try reinitializing the application.'); + } } else { - console.error('Comms instance not available.'); + throw new Error('Communications instance not available.'); } } - // Set data received from server + /** + * Updates the strategies with the data received from the server and refreshes the UI. + * @param {string|Array} data - The strategies data as a JSON string or an array of strategy objects. + */ set_data(data) { if (typeof data === 'string') { data = JSON.parse(data); } this.strategies = data; - this.update_html(); // Refresh the strategies display + this.updateHtml(); // Refresh the strategies display } - // Hide the "Create New Strategy" form + /** + * Hides the "Create New Strategy" form by adding a 'hidden' class. + */ close_form() { - const formElement = document.getElementById("new_strat_form"); - - if (formElement) { - formElement.style.display = "none"; // Close the form - } else { - console.error('Form element "new_strat_form" not found.'); + if (this.formElement) { + this.formElement.classList.add('hidden'); } } - // Submit or edit strategy + /** + * Submits or edits a strategy based on the provided action. + * @param {string} action - Action type, either 'new' or 'edit'. + */ submitStrategy(action) { - const strategyJson = this.generateStrategyJson(); + const feeBox = document.getElementById('fee_box'); + const nameBox = document.getElementById('name_box'); + const publicCheckbox = document.getElementById('public_checkbox'); - const fee = parseFloat(document.getElementById('fee_box').value) || 0; - if (fee < 0) { - alert("Fee cannot be negative"); + if (!feeBox) { + console.error("fee_box element not found."); + alert("An error occurred: fee input element is missing."); + return; + } + if (!nameBox) { + console.error("name_box element not found."); + alert("An error occurred: name input element is missing."); + return; + } + if (!publicCheckbox) { + console.error("public_checkbox element not found."); + alert("An error occurred: public checkbox element is missing."); return; } - const strategyName = document.getElementById('name_box').value.trim(); + let strategyObject; + try { + strategyObject = JSON.parse(this.generateStrategyJson()); + } catch (error) { + console.error('Failed to parse strategy JSON:', error); + alert('An error occurred while processing the strategy data.'); + return; + } + + const feeValue = feeBox.value.trim(); + const fee = parseFloat(feeValue); + if (isNaN(fee) || fee < 0) { + alert("Please enter a valid, non-negative number for the fee."); + return; + } + + const strategyName = nameBox.value.trim(); if (!strategyName) { alert("Please provide a name for the strategy."); return; } - const is_public = document.getElementById('public_checkbox').checked ? 1 : 0; + const is_public = publicCheckbox.checked ? 1 : 0; - // Prepare the strategy data + // Add user_name, fee, and public fields to the strategy object const strategyData = { - user_name: window.UI.data.user_name, // Include user_name - ...JSON.parse(strategyJson), + ...strategyObject, + user_name: this.data.user_name, fee, public: is_public }; @@ -141,507 +436,146 @@ class Strategies { const messageType = action === 'new' ? 'new_strategy' : 'edit_strategy'; // Format the message and send it using the existing sendToApp function - if (window.UI.data.comms && messageType) { - // Adjust here to pass the messageType and data separately - window.UI.data.comms.sendToApp(messageType, strategyData); + if (this.comms) { + this.comms.sendToApp(messageType, strategyData); this.close_form(); } else { console.error("Comms instance not available or invalid action type."); } } - - // Toggle fee input based on public checkbox + /** + * Toggles the fee input field based on the state of the public checkbox. + * Disables the fee input if the public checkbox is unchecked. + */ toggleFeeBox() { const publicCheckbox = document.getElementById('public_checkbox'); const feeBox = document.getElementById('fee_box'); - feeBox.disabled = !publicCheckbox.checked; - } - // Update strategies UI - update_html() { - let stratsHtml = ''; - for (let strat of this.strategies) { - stratsHtml += ` -
- -
-
${strat.name}
-
-
- ${strat.name} -
Stats: ${JSON.stringify(strat.stats, null, 2)} -
-
`; + if (publicCheckbox && feeBox) { + feeBox.disabled = !publicCheckbox.checked; } - document.getElementById(this.target_id).innerHTML = stratsHtml; } - // Open form for creating or editing a strategy + + /** + * Updates the HTML representation of the strategies. + */ + updateHtml() { + // Logic to update the UI with the current list of strategies + if (this.target_el) { + // Clear existing event listeners + while (this.target_el.firstChild) { + this.target_el.removeChild(this.target_el.firstChild); + } + + // Create and append new elements for all strategies + for (let strat of this.strategies) { + const strategyItem = document.createElement('div'); + strategyItem.className = 'strategy-item'; + + // Delete button + const deleteButton = document.createElement('button'); + deleteButton.className = 'delete-button'; + deleteButton.innerHTML = '✘'; + deleteButton.addEventListener('click', () => this.del(strat.name)); + strategyItem.appendChild(deleteButton); + + // Strategy icon + const strategyIcon = document.createElement('div'); + strategyIcon.className = 'strategy-icon'; + strategyIcon.addEventListener('click', () => this.openForm('edit', strat.name)); + + // Strategy name + const strategyName = document.createElement('div'); + strategyName.className = 'strategy-name'; + strategyName.textContent = strat.name; + strategyIcon.appendChild(strategyName); + strategyItem.appendChild(strategyIcon); + + // Strategy hover details + const strategyHover = document.createElement('div'); + strategyHover.className = 'strategy-hover'; + strategyHover.innerHTML = `${strat.name}
Stats: ${JSON.stringify(strat.stats, null, 2)}`; + strategyItem.appendChild(strategyHover); + + // Append to target element + this.target_el.appendChild(strategyItem); + } + } + } + /** + * Opens the form for creating or editing a strategy. + * @param {string} action - The action to perform ('new' or 'edit'). + * @param {string|null} strategyName - The name of the strategy to edit (only applicable for 'edit' action). + */ openForm(action, strategyName = null) { - const formElement = document.getElementById("new_strat_form"); + console.log(`Opening form for action: ${action}, strategy: ${strategyName}`); + if (this.formElement) { + const headerTitle = this.formElement.querySelector("#draggable_header h1"); + const submitCreateBtn = this.formElement.querySelector("#submit-create"); + const submitEditBtn = this.formElement.querySelector("#submit-edit"); + const nameBox = this.formElement.querySelector('#name_box'); + const publicCheckbox = this.formElement.querySelector('#public_checkbox'); + const feeBox = this.formElement.querySelector('#fee_box'); + + if (!headerTitle || !submitCreateBtn || !submitEditBtn || !nameBox || !publicCheckbox || !feeBox) { + console.error('One or more form elements were not found.'); + return; + } - if (formElement) { if (action === 'new') { - document.querySelector("#draggable_header h1").textContent = "Create New Strategy"; - document.getElementById("submit-create").style.display = "inline-block"; - document.getElementById("submit-edit").style.display = "none"; - document.getElementById('name_box').value = ''; - document.getElementById('public_checkbox').checked = false; - document.getElementById('fee_box').value = 0; + headerTitle.textContent = "Create New Strategy"; + submitCreateBtn.style.display = "inline-block"; + submitEditBtn.style.display = "none"; + nameBox.value = ''; + publicCheckbox.checked = false; + feeBox.value = 0; - // Always create a fresh workspace for new strategy + // Create a fresh workspace this.createWorkspace(); - - // Ensure the workspace is resized after being displayed - setTimeout(() => this.resizeWorkspace(), 0); + requestAnimationFrame(() => this.resizeWorkspace()); } else if (action === 'edit' && strategyName) { - // Ensure workspace is created if not already initialized if (!this.workspace) { this.createWorkspace(); } const strategyData = this.strategies.find(s => s.name === strategyName); if (strategyData) { - document.querySelector("#draggable_header h1").textContent = "Edit Strategy"; - document.getElementById("submit-create").style.display = "none"; - document.getElementById("submit-edit").style.display = "inline-block"; + headerTitle.textContent = "Edit Strategy"; + submitCreateBtn.style.display = "none"; + submitEditBtn.style.display = "inline-block"; - // Populate the form with the strategy data - document.getElementById('name_box').value = strategyData.name; - document.getElementById('public_checkbox').checked = strategyData.public === 1; - document.getElementById('fee_box').value = strategyData.fee || 0; + // Populate the form with strategy data + nameBox.value = strategyData.name; + publicCheckbox.checked = strategyData.public === 1; + feeBox.value = strategyData.fee || 0; - // Restore the Blockly workspace from the saved XML - this.restoreWorkspaceFromXml(strategyData.workspace); + // Restore workspace from saved XML + this._restoreWorkspaceFromXml(strategyData.workspace); + } else { + console.error(`Strategy "${strategyName}" not found.`); } } - formElement.style.display = "grid"; // Display the form + // Display the form + this.formElement.style.display = "grid"; } else { - console.error('Form element "new_strat_form" not found.'); + console.error(`Form element "${this.formElement.id}" not found.`); } } - + /** + * Deletes a strategy by its name. + * @param {string} name - The name of the strategy to be deleted. + */ del(name) { + console.log(`Deleting strategy: ${name}`); const deleteData = { - user_name: window.UI.data.user_name, // Include the user_name - strategy_name: name // Strategy name to be deleted + user_name: this.data.user_name, // Include the user_name + strategy_name: name // Strategy name to be deleted }; - window.UI.data.comms.sendToApp('delete_strategy', deleteData); - - // Remove the strategy from the local array - this.strategies = this.strategies.filter(strat => strat.name !== name); - - // Update the UI - this.update_html(); + // Send delete request to the server + this.comms.sendToApp('delete_strategy', deleteData); } - - // Initialize strategies - initialize() { - this.target = document.getElementById(this.target_id); - if (!this.target) { - console.error('Target element', this.target_id, 'not found.'); - return; - } - this.fetchSavedStrategies(); - } - - // Define Blockly blocks dynamically based on indicators - defineIndicatorBlocks() { - const indicatorOutputs = window.UI.indicators.getIndicatorOutputs(); - const toolboxCategory = document.querySelector('#toolbox category[name="Indicators"]'); - - for (let indicatorName in indicatorOutputs) { - const outputs = indicatorOutputs[indicatorName]; - - // Define the block for this indicator - Blockly.defineBlocksWithJsonArray([{ - "type": indicatorName, - "message0": `${indicatorName} %1`, - "args0": [ - { - "type": "field_dropdown", - "name": "OUTPUT", - "options": outputs.map(output => [output, output]) - } - ], - "output": "Number", - "colour": 230, - "tooltip": `Select the ${indicatorName} output`, - "helpUrl": "" - }]); - - // Define how this block will generate Python code - Blockly.Python[indicatorName] = Blockly.Python.forBlock[indicatorName] = function(block) { - const selectedOutput = block.getFieldValue('OUTPUT'); - const code = `get_${indicatorName.toLowerCase()}_value('${selectedOutput}')`; - return [code, Blockly.Python.ORDER_ATOMIC]; - }; - - // Append dynamically created blocks to the Indicators category in the toolbox - const blockElement = document.createElement('block'); - blockElement.setAttribute('type', indicatorName); - toolboxCategory.appendChild(blockElement); - } - } - - // Define custom Blockly blocks and Python code generation - defineCustomBlocks() { - // Custom block for retrieving last candle values - Blockly.defineBlocksWithJsonArray([ - { - "type": "last_candle_value", - "message0": "Last candle %1 value", - "args0": [ - { - "type": "field_dropdown", - "name": "CANDLE_PART", - "options": [ - ["Open", "open"], - ["High", "high"], - ["Low", "low"], - ["Close", "close"] - ] - } - ], - "output": "Number", - "colour": 230, - "tooltip": "Get the value of the last candle.", - "helpUrl": "" - } - ]); - - // Comparison Block (for Candle Close > Candle Open, etc.) - Blockly.defineBlocksWithJsonArray([ - { - "type": "comparison", - "message0": "%1 %2 %3", - "args0": [ - { - "type": "input_value", - "name": "LEFT" - }, - { - "type": "field_dropdown", - "name": "OPERATOR", - "options": [ - [">", ">"], - ["<", "<"], - ["==", "=="] - ] - }, - { - "type": "input_value", - "name": "RIGHT" - } - ], - "inputsInline": true, - "output": "Boolean", - "colour": 160, - "tooltip": "Compare two values.", - "helpUrl": "" - } - ]); - - Blockly.defineBlocksWithJsonArray([{ - "type": "trade_action", - "message0": "if %1 then %2 with Stop Loss %3 and Take Profit %4 (Options) %5", - "args0": [ - { - "type": "input_value", - "name": "CONDITION", - "check": "Boolean" - }, - { - "type": "field_dropdown", - "name": "TRADE_TYPE", - "options": [ - ["Buy", "buy"], - ["Sell", "sell"] - ] - }, - { - "type": "input_value", - "name": "STOP_LOSS", - "check": "Number" - }, - { - "type": "input_value", - "name": "TAKE_PROFIT", - "check": "Number" - }, - { - "type": "input_statement", - "name": "TRADE_OPTIONS", - "check": "trade_option" // This will accept the chain of trade options - } - ], - "previousStatement": null, - "nextStatement": null, - "colour": 230, - "tooltip": "Executes a trade with optional stop loss, take profit, and trade options", - "helpUrl": "" - }]); - - - // Stop Loss Block - Blockly.defineBlocksWithJsonArray([{ - "type": "stop_loss", - "message0": "Stop Loss %1", - "args0": [ - { - "type": "input_value", - "name": "STOP_LOSS", - "check": "Number" - } - ], - "output": "Number", - "colour": 230, - "tooltip": "Sets a stop loss value", - "helpUrl": "" - }]); - - // Take Profit Block - Blockly.defineBlocksWithJsonArray([{ - "type": "take_profit", - "message0": "Take Profit %1", - "args0": [ - { - "type": "input_value", - "name": "TAKE_PROFIT", - "check": "Number" - } - ], - "output": "Number", - "colour": 230, - "tooltip": "Sets a take profit value", - "helpUrl": "" - }]); - // Logical AND Block - Blockly.defineBlocksWithJsonArray([{ - "type": "logical_and", - "message0": "%1 AND %2", - "args0": [ - { - "type": "input_value", - "name": "LEFT", - "check": "Boolean" - }, - { - "type": "input_value", - "name": "RIGHT", - "check": "Boolean" - } - ], - "inputsInline": true, - "output": "Boolean", - "colour": 210, - "tooltip": "Logical AND of two conditions", - "helpUrl": "" - }]); - // Logical OR Block - Blockly.defineBlocksWithJsonArray([{ - "type": "logical_or", - "message0": "%1 OR %2", - "args0": [ - { - "type": "input_value", - "name": "LEFT", - "check": "Boolean" - }, - { - "type": "input_value", - "name": "RIGHT", - "check": "Boolean" - } - ], - "inputsInline": true, - "output": "Boolean", - "colour": 210, - "tooltip": "Logical OR of two conditions", - "helpUrl": "" - }]); - // "is" Block - Blockly.defineBlocksWithJsonArray([{ - "type": "is_true", - "message0": "%1 is true", - "args0": [ - { - "type": "input_value", - "name": "CONDITION", - "check": "Boolean" - } - ], - "output": "Boolean", - "colour": 160, - "tooltip": "Checks if the condition is true", - "helpUrl": "" - }]); - // Order Type Block with Limit Price - Blockly.defineBlocksWithJsonArray([{ - "type": "order_type", - "message0": "Order Type %1 %2", - "args0": [ - { - "type": "field_dropdown", - "name": "ORDER_TYPE", - "options": [ - ["Market", "market"], - ["Limit", "limit"] - ] - }, - { - "type": "input_value", - "name": "LIMIT_PRICE", // Input for limit price when Limit order is selected - "check": "Number" - } - ], - "previousStatement": "trade_option", - "nextStatement": "trade_option", - "colour": 230, - "tooltip": "Select order type (Market or Limit) with optional limit price", - "helpUrl": "" - }]); - Blockly.defineBlocksWithJsonArray([{ - "type": "value_input", - "message0": "Value %1", - "args0": [ - { - "type": "field_number", - "name": "VALUE", - "value": 0, - "min": 0 - } - ], - "output": "Number", - "colour": 230, - "tooltip": "Enter a numerical value", - "helpUrl": "" - }]); - // Time In Force (TIF) Block - Blockly.defineBlocksWithJsonArray([{ - "type": "time_in_force", - "message0": "Time in Force %1", - "args0": [ - { - "type": "field_dropdown", - "name": "TIF", - "options": [ - ["GTC (Good Till Canceled)", "gtc"], - ["FOK (Fill or Kill)", "fok"], - ["IOC (Immediate or Cancel)", "ioc"] - ] - } - ], - "previousStatement": "trade_option", - "nextStatement": "trade_option", - "colour": 230, - "tooltip": "Select time in force for the order", - "helpUrl": "" - }]); - console.log('Custom blocks defined'); - - } - - // Define Python generators for custom blocks - definePythonGenerators() { - // Last candle value to Python code - Blockly.Python['last_candle_value'] = Blockly.Python.forBlock['last_candle_value'] = function(block) { - var candlePart = block.getFieldValue('CANDLE_PART'); - var code = `market.get_last_candle_value('${candlePart}')`; - return [code, Blockly.Python.ORDER_ATOMIC]; - }; - - // Comparison block to Python code - Blockly.Python['comparison'] = Blockly.Python.forBlock['comparison'] = function(block) { - const left = Blockly.Python.valueToCode(block, 'LEFT', Blockly.Python.ORDER_ATOMIC); - const right = Blockly.Python.valueToCode(block, 'RIGHT', Blockly.Python.ORDER_ATOMIC); - const operator = block.getFieldValue('OPERATOR'); - return [left + ' ' + operator + ' ' + right, Blockly.Python.ORDER_ATOMIC]; - }; - - // Logical OR block to Python code - Blockly.Python['logical_or'] = Blockly.Python.forBlock['logical_or'] = function(block) { - var left = Blockly.Python.valueToCode(block, 'LEFT', Blockly.Python.ORDER_ATOMIC); - var right = Blockly.Python.valueToCode(block, 'RIGHT', Blockly.Python.ORDER_ATOMIC); - var code = `${left} or ${right}`; - return [code, Blockly.Python.ORDER_ATOMIC]; - }; - - // Logical AND block to Python code - Blockly.Python['logical_and'] = Blockly.Python.forBlock['logical_and'] = function(block) { - var left = Blockly.Python.valueToCode(block, 'LEFT', Blockly.Python.ORDER_ATOMIC); - var right = Blockly.Python.valueToCode(block, 'RIGHT', Blockly.Python.ORDER_ATOMIC); - var code = `${left} and ${right}`; - return [code, Blockly.Python.ORDER_ATOMIC]; - }; - - // Stop Loss block to Python code - Blockly.Python['stop_loss'] = Blockly.Python.forBlock['stop_loss'] = function(block) { - var stopLoss = Blockly.Python.valueToCode(block, 'STOP_LOSS', Blockly.Python.ORDER_ATOMIC); - return [stopLoss, Blockly.Python.ORDER_ATOMIC]; - }; - - // Take Profit block to Python code - Blockly.Python['take_profit'] = Blockly.Python.forBlock['take_profit'] = function(block) { - var takeProfit = Blockly.Python.valueToCode(block, 'TAKE_PROFIT', Blockly.Python.ORDER_ATOMIC); - return [takeProfit, Blockly.Python.ORDER_ATOMIC]; - }; - - // Is True block to Python code - Blockly.Python['is_true'] = Blockly.Python.forBlock['is_true'] = function(block) { - var condition = Blockly.Python.valueToCode(block, 'CONDITION', Blockly.Python.ORDER_ATOMIC); - var code = `${condition}`; - return [code, Blockly.Python.ORDER_ATOMIC]; - }; - - // Trade Action block to Python code - Blockly.Python['trade_action'] = Blockly.Python.forBlock['trade_action'] = function(block) { - var condition = Blockly.Python.valueToCode(block, 'CONDITION', Blockly.Python.ORDER_ATOMIC); - var tradeType = block.getFieldValue('TRADE_TYPE'); - var stopLoss = Blockly.Python.valueToCode(block, 'STOP_LOSS', Blockly.Python.ORDER_ATOMIC) || 'None'; - var takeProfit = Blockly.Python.valueToCode(block, 'TAKE_PROFIT', Blockly.Python.ORDER_ATOMIC) || 'None'; - var tradeOptions = Blockly.Python.statementToCode(block, 'TRADE_OPTIONS').trim(); - - var code = `if ${condition}:\n`; - code += ` ${tradeType}_order(stop_loss=${stopLoss}, take_profit=${takeProfit}`; - - // Include trade options if they are set - if (tradeOptions) { - code += `, ${tradeOptions}`; - } - - code += `)\n`; - return code; - }; - - // Order Type block to Python code - Blockly.Python['order_type'] = Blockly.Python.forBlock['order_type'] = function(block) { - var orderType = block.getFieldValue('ORDER_TYPE'); - var limitPrice = Blockly.Python.valueToCode(block, 'LIMIT_PRICE', Blockly.Python.ORDER_ATOMIC) || 'None'; - - // If it's a limit order, include the limit price in the output - if (orderType === 'limit') { - return `order_type='limit', limit_price=${limitPrice}`; - } else { - return `order_type='market'`; - } - }; - - // Value Input block to Python code - Blockly.Python['value_input'] = Blockly.Python.forBlock['value_input'] = function(block) { - var value = block.getFieldValue('VALUE'); - return [value.toString(), Blockly.Python.ORDER_ATOMIC]; // Returning both value and precedence - }; - - // Time in Force block to Python code - Blockly.Python['time_in_force'] = Blockly.Python.forBlock['time_in_force'] = function(block) { - var tif = block.getFieldValue('TIF'); - return `tif='${tif}'`; - }; - - console.log('Python generators defined'); - } } diff --git a/src/static/backtesting.js b/src/static/backtesting.js index f812c3c..254e23b 100644 --- a/src/static/backtesting.js +++ b/src/static/backtesting.js @@ -1,6 +1,190 @@ - class Backtesting { - constructor() { - this.height = height; - } + constructor(ui) { + this.ui = ui; + this.comms = ui.data.comms; + this.tests = []; // Stores the list of saved backtests + this.target_id = 'backtest_display'; // The container to display backtests + + // Register handlers for backtesting messages + this.comms.on('backtest_results', this.handleBacktestResults.bind(this)); + this.comms.on('progress', this.handleProgress.bind(this)); + this.comms.on('backtests_list', this.handleBacktestsList.bind(this)); + this.comms.on('backtest_deleted', this.handleBacktestDeleted.bind(this)); + this.comms.on('updates', this.handleUpdates.bind(this)); + } + + handleBacktestResults(data) { + console.log("Backtest results received:", data.results); + // Logic to stop running animation and display results + this.stopRunningAnimation(data.results); + } + + handleProgress(data) { + console.log("Backtest progress:", data.progress); + // Logic to update progress bar + this.updateProgressBar(data.progress); + } + + handleBacktestsList(data) { + console.log("Backtests list received:", data.tests); + // Logic to update backtesting UI + this.set_data(data.tests); + } + + handleBacktestDeleted(data) { + console.log(`Backtest "${data.name}" was successfully deleted.`); + // Logic to refresh list of backtests + this.fetchSavedTests(); + } + + handleUpdates(data) { + const { trade_updts } = data; + if (trade_updts) { + this.ui.trade.update_received(trade_updts); + } + } + + updateProgressBar(progress) { + const progressBar = document.getElementById('progress_bar'); + if (progressBar) { + progressBar.style.width = `${progress}%`; + progressBar.textContent = `${progress}%`; + } + } + + showRunningAnimation() { + const resultsContainer = document.getElementById('backtest-results'); + const resultsDisplay = document.getElementById('results_display'); + const progressContainer = document.getElementById('backtest-progress-container'); + const progressBar = document.getElementById('progress_bar'); + + resultsContainer.style.display = 'none'; + progressContainer.style.display = 'block'; + progressBar.style.width = '0%'; + progressBar.textContent = '0%'; + resultsDisplay.innerHTML = ''; + } + + displayTestResults(results) { + const resultsContainer = document.getElementById('backtest-results'); + const resultsDisplay = document.getElementById('results_display'); + + resultsContainer.style.display = 'block'; + resultsDisplay.innerHTML = `
${JSON.stringify(results, null, 2)}
`; + } + + stopRunningAnimation(results) { + const progressContainer = document.getElementById('backtest-progress-container'); + progressContainer.style.display = 'none'; + this.displayTestResults(results); + } + + fetchSavedTests() { + this.comms.sendToApp('get_backtests', { user_name: this.ui.data.user_name }); + } + + updateHTML() { + let html = ''; + for (const test of this.tests) { + html += ` +
+ +
${test.name}
+
`; + } + document.getElementById(this.target_id).innerHTML = html; + } + + runTest(testName) { + const testData = { name: testName, user_name: this.ui.data.user_name }; + this.comms.sendToApp('run_backtest', testData); + } + + deleteTest(testName) { + const testData = { name: testName, user_name: this.ui.data.user_name }; + this.comms.sendToApp('delete_backtest', testData); + } + + populateStrategyDropdown() { + const strategyDropdown = document.getElementById('strategy_select'); + strategyDropdown.innerHTML = ''; + const strategies = this.ui.strats.getAvailableStrategies(); + console.log("Available strategies:", strategies); + + strategies.forEach(strategy => { + const option = document.createElement('option'); + option.value = strategy.name; + option.text = strategy.name; + strategyDropdown.appendChild(option); + }); + + if (strategies.length > 0) { + const firstStrategyName = strategies[0].name; + console.log("Setting default strategy to:", firstStrategyName); + strategyDropdown.value = firstStrategyName; + } + } + + openForm(testName = null) { + const formElement = document.getElementById("backtest_form"); + if (!formElement) { + console.error('Form element not found'); + return; + } + + this.populateStrategyDropdown(); + + if (testName) { + const testData = this.tests.find(test => test.name === testName); + if (testData) { + document.querySelector("#backtest_draggable_header h1").textContent = "Edit Backtest"; + document.getElementById('strategy_select').value = testData.strategy; + document.getElementById('start_date').value = testData.start_date; + } + } else { + document.querySelector("#backtest_draggable_header h1").textContent = "Create New Backtest"; + this.clearForm(); + } + + formElement.style.display = "grid"; + } + + closeForm() { + document.getElementById("backtest_form").style.display = "none"; + } + + clearForm() { + document.getElementById('strategy_select').value = ''; + document.getElementById('start_date').value = ''; + } + + submitTest() { + const strategy = document.getElementById('strategy_select').value; + const start_date = document.getElementById('start_date').value; + const capital = parseFloat(document.getElementById('initial_capital').value) || 10000; + const commission = parseFloat(document.getElementById('commission').value) || 0.001; + + if (!strategy) { + alert("Please select a strategy."); + return; + } + + const now = new Date(); + const startDate = new Date(start_date); + + if (startDate > now) { + alert("Start date cannot be in the future."); + return; + } + + const testData = { + strategy, + start_date, + capital, + commission, + user_name: this.ui.data.user_name + }; + + this.comms.sendToApp('submit_backtest', testData); + } } \ No newline at end of file diff --git a/src/static/brighterStyles.css b/src/static/brighterStyles.css index f6decd7..314e00f 100644 --- a/src/static/brighterStyles.css +++ b/src/static/brighterStyles.css @@ -317,20 +317,22 @@ height: 500px; border-style:none; } #indicator_output{ - overflow-y: scroll; - width: 300px; - height:50px; - padding: 3px; - border-style: solid; + color: blueviolet; + position: absolute; + height: fit-content; + width: 150px; + margin-top: 30px; + border-radius: 8px; + background-image: linear-gradient(93deg, #ffffffde, #fffefe29); } #chart_controls{ border-style:none; - width: 775px; + width: 600px; padding: 15px; display: grid; - grid-template-columns:350px 2fr 1fr 1fr; - + grid-template-columns: 4fr 2fr 2fr 2fr; + margin-left: 250; } #indicators{ display: none; diff --git a/src/static/charts.js b/src/static/charts.js index edc4951..bd28663 100644 --- a/src/static/charts.js +++ b/src/static/charts.js @@ -27,12 +27,13 @@ class Charts { // - 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); - console.log('Candle series init:', ph) - }) + // 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); } diff --git a/src/static/communication.js b/src/static/communication.js index 964fc23..c361027 100644 --- a/src/static/communication.js +++ b/src/static/communication.js @@ -2,14 +2,12 @@ class Comms { constructor() { this.connectionOpen = false; this.appCon = null; // WebSocket connection for app communication + this.eventHandlers = {}; // Event handlers for message types // Callback collections that will receive various updates. this.candleUpdateCallbacks = []; this.candleCloseCallbacks = []; this.indicatorUpdateCallbacks = []; - - // Flags - this.connectionOpen = false; } /** @@ -32,6 +30,30 @@ class Comms { } } + /** + * Register an event handler for a specific message type. + * @param {string} messageType - The type of the message to handle. + * @param {function} handler - The handler function to register. + */ + on(messageType, handler) { + if (!this.eventHandlers[messageType]) { + this.eventHandlers[messageType] = []; + } + this.eventHandlers[messageType].push(handler); + } + + /** + * Emit an event to all registered handlers. + * @param {string} messageType - The type of the message. + * @param {Object} data - The data to pass to the handlers. + */ + emit(messageType, data) { + const handlers = this.eventHandlers[messageType]; + if (handlers) { + handlers.forEach(handler => handler(data)); + } + } + /* Callback declarations */ candleUpdate(newCandle) { @@ -40,7 +62,7 @@ class Comms { } } - candleClose(newCandle) { + candleClose(newCandle) { this.sendToApp('candle_data', newCandle); for (const callback of this.candleCloseCallbacks) { @@ -149,7 +171,7 @@ class Comms { } } - /** + /** * 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. @@ -206,7 +228,6 @@ class Comms { } } - setAppCon() { this.appCon = new WebSocket('ws://localhost:5000/ws'); @@ -228,71 +249,8 @@ class Comms { } if (message && message.reply !== undefined) { - // Handle different reply types from the server - if (message.reply === 'updates') { - const { i_updates, s_updates, stg_updts, trade_updts } = message.data; - - // Handle indicator updates - if (i_updates) { - this.indicatorUpdate(i_updates); - window.UI.signals.i_update(i_updates); - } - - // Handle signal updates - if (s_updates) { - window.UI.signals.update_signal_states(s_updates); - window.UI.alerts.publish_alerts('signal_changes', s_updates); - } - - // Handle strategy updates - if (stg_updts) { - window.UI.strats.update_received(stg_updts); - } - - // Handle trade updates - if (trade_updts) { - window.UI.trade.update_received(trade_updts); - } - - } else if (message.reply === 'signals') { - window.UI.signals.set_data(message.data); - - } else if (message.reply === 'strategies') { - window.UI.strats.set_data(message.data); - - } else if (message.reply === 'trades') { - window.UI.trade.set_data(message.data); - - } else if (message.reply === 'signal_created') { - const list_of_one = [message.data]; - window.UI.signals.set_data(list_of_one); - - } else if (message.reply === 'trade_created') { - const list_of_one = [message.data]; - window.UI.trade.set_data(list_of_one); - - } else if (message.reply === 'Exchange_connection_result') { - window.UI.exchanges.postConnection(message.data); - - } else if (message.reply === 'strategy_created') { - // Handle the strategy creation response - if (message.data.success) { - // Success - Notify the user and update the UI - alert(message.data.message); // Display a success message - console.log("New strategy data:", message.data); // Log or handle the new strategy data - - // Optionally, refresh the list of strategies - window.UI.strats.fetchSavedStrategies(); - } else { - // Failure - Notify the user of the error - alert(`Error: ${message.data.message}`); - console.error("Strategy creation error:", message.data.message); - } - - } else { - console.log(message.reply); - console.log(message.data); - } + // Emit the event to registered handlers + this.emit(message.reply, message.data); } } }); @@ -309,8 +267,6 @@ class Comms { }; } - - /** * Sets up a WebSocket connection to the exchange for receiving candlestick data. * @param {string} interval - The interval of the candlestick data. diff --git a/src/static/custom_blocks.js b/src/static/custom_blocks.js new file mode 100644 index 0000000..0af0945 --- /dev/null +++ b/src/static/custom_blocks.js @@ -0,0 +1,427 @@ + // Define custom Blockly blocks and Python code generation + export function defineCustomBlocks() { + // Custom block for retrieving last candle values + Blockly.defineBlocksWithJsonArray([{ + "type": "last_candle_value", + "message0": "Last candle %1 value (Src): %2", + "args0": [ + { + "type": "field_dropdown", + "name": "CANDLE_PART", + "options": [ + ["Open", "open"], + ["High", "high"], + ["Low", "low"], + ["Close", "close"] + ] + }, + { + "type": "input_value", // Accept an optional source block connection + "name": "SOURCE", + "check": "source" // The connected block must be a source block + } + ], + "inputsInline": true, // Place the fields on the same line + "output": "Number", + "colour": 230, + "tooltip": "Get the value of the last candle from the specified or default source.", + "helpUrl": "" + }]); + + // Comparison Block (for Candle Close > Candle Open, etc.) + Blockly.defineBlocksWithJsonArray([ + { + "type": "comparison", + "message0": "%1 %2 %3", + "args0": [ + { + "type": "input_value", + "name": "LEFT" + }, + { + "type": "field_dropdown", + "name": "OPERATOR", + "options": [ + [">", ">"], + ["<", "<"], + ["==", "=="] + ] + }, + { + "type": "input_value", + "name": "RIGHT" + } + ], + "inputsInline": true, + "output": "Boolean", + "colour": 160, + "tooltip": "Compare two values.", + "helpUrl": "" + } + ]); + + Blockly.defineBlocksWithJsonArray([{ + "type": "trade_action", + "message0": "if %1 then %2 units %3 with Stop Loss %4 and Take Profit %5 (Options) %6", + "args0": [ + {"type": "input_value", "name": "CONDITION", "check": "Boolean"}, + {"type": "field_dropdown", "name": "TRADE_TYPE", "options": [["Buy", "buy"], ["Sell", "sell"]]}, + {"type": "input_value", "name": "SIZE", "check": "Number"}, + {"type": "input_value", "name": "STOP_LOSS", "check": "Number"}, + {"type": "input_value", "name": "TAKE_PROFIT", "check": "Number"}, + {"type": "input_statement", "name": "TRADE_OPTIONS", "check": "trade_option"} + ], + "previousStatement": null, + "nextStatement": null, + "colour": 230, + "tooltip": "Executes a trade with size, optional stop loss, take profit, and trade options", + "helpUrl": "" + }]); + + + // Stop Loss Block + Blockly.defineBlocksWithJsonArray([{ + "type": "stop_loss", + "message0": "Stop Loss %1", + "args0": [ + { + "type": "input_value", + "name": "STOP_LOSS", + "check": "Number" + } + ], + "output": "Number", + "colour": 230, + "tooltip": "Sets a stop loss value", + "helpUrl": "" + }]); + + // Take Profit Block + Blockly.defineBlocksWithJsonArray([{ + "type": "take_profit", + "message0": "Take Profit %1", + "args0": [ + { + "type": "input_value", + "name": "TAKE_PROFIT", + "check": "Number" + } + ], + "output": "Number", + "colour": 230, + "tooltip": "Sets a take profit value", + "helpUrl": "" + }]); + // Logical AND Block + Blockly.defineBlocksWithJsonArray([{ + "type": "logical_and", + "message0": "%1 AND %2", + "args0": [ + { + "type": "input_value", + "name": "LEFT", + "check": "Boolean" + }, + { + "type": "input_value", + "name": "RIGHT", + "check": "Boolean" + } + ], + "inputsInline": true, + "output": "Boolean", + "colour": 210, + "tooltip": "Logical AND of two conditions", + "helpUrl": "" + }]); + // Logical OR Block + Blockly.defineBlocksWithJsonArray([{ + "type": "logical_or", + "message0": "%1 OR %2", + "args0": [ + { + "type": "input_value", + "name": "LEFT", + "check": "Boolean" + }, + { + "type": "input_value", + "name": "RIGHT", + "check": "Boolean" + } + ], + "inputsInline": true, + "output": "Boolean", + "colour": 210, + "tooltip": "Logical OR of two conditions", + "helpUrl": "" + }]); + // Block to check if a condition is false + Blockly.defineBlocksWithJsonArray([{ + "type": "is_false", + "message0": "%1 is false", + "args0": [ + { + "type": "input_value", + "name": "CONDITION", + "check": "Boolean" + } + ], + "output": "Boolean", + "colour": 160, + "tooltip": "Checks if the condition is false", + "helpUrl": "" + }]); + // Order Type Block with Limit Price + Blockly.defineBlocksWithJsonArray([{ + "type": "order_type", + "message0": "Order Type %1 %2", + "args0": [ + { + "type": "field_dropdown", + "name": "ORDER_TYPE", + "options": [ + ["Market", "market"], + ["Limit", "limit"] + ] + }, + { + "type": "input_value", + "name": "LIMIT_PRICE", // Input for limit price when Limit order is selected + "check": "Number" + } + ], + "previousStatement": "trade_option", + "nextStatement": "trade_option", + "colour": 230, + "tooltip": "Select order type (Market or Limit) with optional limit price", + "helpUrl": "" + }]); + Blockly.defineBlocksWithJsonArray([{ + "type": "value_input", + "message0": "Value %1", + "args0": [ + { + "type": "field_number", + "name": "VALUE", + "value": 0, + "min": 0 + } + ], + "output": "Number", + "colour": 230, + "tooltip": "Enter a numerical value", + "helpUrl": "" + }]); + // Time In Force (TIF) Block + Blockly.defineBlocksWithJsonArray([{ + "type": "time_in_force", + "message0": "Time in Force %1", + "args0": [ + { + "type": "field_dropdown", + "name": "TIF", + "options": [ + ["GTC (Good Till Canceled)", "gtc"], + ["FOK (Fill or Kill)", "fok"], + ["IOC (Immediate or Cancel)", "ioc"] + ] + } + ], + "previousStatement": "trade_option", + "nextStatement": "trade_option", + "colour": 230, + "tooltip": "Select time in force for the order", + "helpUrl": "" + }]); + + // Dynamically populate the block options using the available data + Blockly.defineBlocksWithJsonArray([{ + "type": "source", + "message0": "src: TF %1 Ex %2 Sym %3", + "args0": [ + { + "type": "field_dropdown", + "name": "TF", + "options": function() { + // Dynamically fetch available timeframes from bt_data.intervals + return bt_data.intervals.map(interval => [interval, interval]); + } + }, + { + "type": "field_dropdown", + "name": "EXC", + "options": function() { + // Dynamically fetch available exchanges from window.UI.exchanges.connected_exchanges + return window.UI.exchanges.connected_exchanges.map(exchange => [exchange, exchange]); + } + }, + { + "type": "field_dropdown", + "name": "SYM", + "options": function() { + // Dynamically fetch available symbols from bt_data.symbols + return bt_data.symbols.map(symbol => [symbol, symbol]); + } + } + ], + "output": "source", // This output allows it to be connected to other blocks expecting a 'source' + "colour": 230, + "tooltip": "Choose the data feed source for the trade or value.", + "helpUrl": "" + }]); + + + Blockly.defineBlocksWithJsonArray([{ + "type": "target_market", + "message0": "Target market: TF %1 Ex %2 Sym %3", + "args0": [ + { + "type": "field_dropdown", + "name": "TF", + "options": function() { + return bt_data.intervals.map(interval => [interval, interval]); + } + }, + { + "type": "field_dropdown", + "name": "EXC", + "options": function() { + return window.UI.exchanges.connected_exchanges.map(exchange => [exchange, exchange]); + } + }, + { + "type": "field_dropdown", + "name": "SYM", + "options": function() { + return bt_data.symbols.map(symbol => [symbol, symbol]); + } + } + ], + "previousStatement": "trade_option", // Allow it to be used as a trade option + "nextStatement": "trade_option", // Chain it with other trade options + "colour": 230, + "tooltip": "Choose the target market for executing trades.", + "helpUrl": "" + }]); + Blockly.defineBlocksWithJsonArray([{ + "type": "strategy_profit_loss", + "message0": "Strategy is %1", + "args0": [ + { + "type": "field_dropdown", + "name": "DIRECTION", + "options": [ + ["up", "up"], + ["down", "down"] + ] + } + ], + "output": "Boolean", + "colour": 230, + "tooltip": "Check if the strategy is up or down.", + "helpUrl": "" + }]); + Blockly.defineBlocksWithJsonArray([{ + "type": "current_balance", + "message0": "Current Balance", + "output": "Number", + "colour": 230, + "tooltip": "Retrieve the current balance of the strategy.", + "helpUrl": "" + }]); + Blockly.defineBlocksWithJsonArray([{ + "type": "starting_balance", + "message0": "Starting Balance", + "output": "Number", + "colour": 230, + "tooltip": "Retrieve the starting balance of the strategy.", + "helpUrl": "" + }]); + Blockly.defineBlocksWithJsonArray([{ + "type": "arithmetic_operator", + "message0": "%1 %2 %3", + "args0": [ + { + "type": "input_value", + "name": "LEFT", + "check": "Number" + }, + { + "type": "field_dropdown", + "name": "OPERATOR", + "options": [ + ["+", "ADD"], + ["-", "SUBTRACT"], + ["*", "MULTIPLY"], + ["/", "DIVIDE"] + ] + }, + { + "type": "input_value", + "name": "RIGHT", + "check": "Number" + } + ], + "inputsInline": true, + "output": "Number", + "colour": 160, + "tooltip": "Perform basic arithmetic operations.", + "helpUrl": "" + }]); + // Block for tracking the number of trades currently in play + Blockly.defineBlocksWithJsonArray([{ + "type": "active_trades", + "message0": "Number of active trades", + "output": "Number", + "colour": 230, + "tooltip": "Get the number of active trades currently open.", + "helpUrl": "" + }]); + // Block for checking if a flag is set + Blockly.defineBlocksWithJsonArray([{ + "type": "flag_is_set", + "message0": "flag %1 is set", + "args0": [ + { + "type": "field_input", + "name": "FLAG_NAME", + "text": "flag_name" + } + ], + "output": "Boolean", + "colour": 160, + "tooltip": "Check if the specified flag is set to True.", + "helpUrl": "" + }]); + // Block for setting a flag based on a condition + Blockly.defineBlocksWithJsonArray([{ + "type": "set_flag", + "message0": "If %1 then set flag %2 to %3", + "args0": [ + { + "type": "input_value", // This will accept a Boolean condition (comparison or logical) + "name": "CONDITION", + "check": "Boolean" + }, + { + "type": "field_input", + "name": "FLAG_NAME", + "text": "flag_name" + }, + { + "type": "field_dropdown", + "name": "FLAG_VALUE", + "options": [["True", "True"], ["False", "False"]] + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": 230, + "tooltip": "Set a flag to True or False if the condition is met.", + "helpUrl": "" + }]); + + + console.log('Custom blocks defined'); + } diff --git a/src/static/data.js b/src/static/data.js index a62215a..f5aa1aa 100644 --- a/src/static/data.js +++ b/src/static/data.js @@ -19,38 +19,49 @@ class Data { /* Comms handles communication with the servers. Register callbacks to handle various incoming messages.*/ this.comms = new Comms(); - this.comms.registerCallback('candle_update', this.candle_update) - this.comms.registerCallback('candle_close', this.candle_close) - this.comms.registerCallback('indicator_update', this.indicator_update) - // Open the connection to our local server. - this.comms.setAppCon(); - /* Open connection for streaming candle data wth the exchange. - Pass it the time period of candles to stream. */ - this.comms.setExchangeCon(this.interval, this.trading_pair); - //Request historical price data from the server. - this.price_history = this.comms.getPriceHistory(this.user_name); - - // Last price from price history. - this.price_history.then((value) => { - if (value && value.length > 0) { - this.last_price = value[value.length - 1].close; - } else { - console.error('Received empty price history data'); - this.last_price = null; - } - }).catch((error) => { - console.error('Error processing price history:', error); - this.last_price = null; - }); - - // Request from the server initialization data for the indicators. - this.indicator_data = this.comms.getIndicatorData(this.user_name); - - // Call back for indicator updates. + // Initialize other properties + this.price_history = null; + this.indicator_data = null; + this.last_price = null; this.i_updates = null; } + /** + * Initializes the Data instance by setting up connections and fetching data. + * Should be called after creating a new instance of Data. + */ + async initialize() { + // Register callbacks + this.comms.registerCallback('candle_update', this.candle_update.bind(this)); + this.comms.registerCallback('candle_close', this.candle_close.bind(this)); + this.comms.registerCallback('indicator_update', this.indicator_update.bind(this)); + + // Open the connection to your local server + this.comms.setAppCon(); + + // Open connection for streaming candle data with the exchange + this.comms.setExchangeCon(this.interval, this.trading_pair); + + // Request historical price data from the server + try { + this.price_history = await this.comms.getPriceHistory(this.user_name); + if (this.price_history && this.price_history.length > 0) { + this.last_price = this.price_history[this.price_history.length - 1].close; + } else { + console.error('Received empty price history data'); + } + } catch (error) { + console.error('Error fetching price history:', error); + } + + // Request indicator data from the server + try { + this.indicator_data = await this.comms.getIndicatorData(this.user_name); + } catch (error) { + console.error('Error fetching indicator data:', error); + } + } candle_update(new_candle){ // This is called everytime a candle update comes from the local server. window.UI.charts.update_main_chart(new_candle); @@ -60,11 +71,17 @@ class Data { registerCallback_i_updates(call_back){ this.i_updates = call_back; } - indicator_update(data){ - // This is called everytime an indicator update come in. - window.UI.data.i_updates(data); + + // This is called everytime an indicator update come in. + indicator_update(data) { + if (typeof this.i_updates === 'function') { + this.i_updates(data); + } else { + console.warn('No indicator update callback registered.'); + } } + candle_close(new_candle){ // This is called everytime a candle closes. //console.log('Candle close:'); diff --git a/src/static/general.js b/src/static/general.js index 55278e2..53f6e13 100644 --- a/src/static/general.js +++ b/src/static/general.js @@ -1,15 +1,16 @@ class User_Interface { constructor() { // Initialize all components needed by the user interface - this.strats = new Strategies('strats_display'); + this.strats = new Strategies(); this.exchanges = new Exchanges(); this.data = new Data(); this.controls = new Controls(); - this.signals = new Signals(this.data.indicators); this.alerts = new Alerts("alert_list"); this.trade = new Trade(); this.users = new Users(); this.indicators = new Indicators(this.data.comms); + this.signals = new Signals(this); + this.backtesting = new Backtesting(this); // Register a callback function for when indicator updates are received from the data object this.data.registerCallback_i_updates(this.indicators.update); @@ -18,10 +19,18 @@ class User_Interface { this.initializeAll(); } - initializeAll() { - window.addEventListener('load', () => { - this.initializeChartsAndIndicators(); - this.initializeResizablePopup("new_strat_form", this.strats.resizeWorkspace.bind(this.strats)); + async initializeAll() { + window.addEventListener('load', async () => { + try { + await this.data.initialize(); // Wait for initialization + this.initializeChartsAndIndicators(); + + // Initialize other UI components here + this.initializeResizablePopup("new_strat_form", this.strats.resizeWorkspace.bind(this.strats), "draggable_header", "resize-strategy"); + this.initializeResizablePopup("backtest_form", null, "backtest_draggable_header", "resize-backtest"); + } catch (error) { + console.error('Initialization failed:', error); + } }); } @@ -46,26 +55,30 @@ class User_Interface { this.controls.init_TP_selector(); this.trade.initialize(); this.exchanges.initialize(); - this.strats.initialize(); + this.strats.initialize('strats_display', 'new_strat_form', this.data); + this.backtesting.fetchSavedTests(); } /** * Make a popup resizable, and optionally pass a resize callback (like for Blockly workspaces). * @param {string} popupId - The ID of the popup to make resizable. * @param {function|null} resizeCallback - Optional callback to run when resizing (for Blockly or other elements). + * @param {string|null} headerId - The ID of the header to use for dragging, or null to drag the entire element. + * @param {string} resizerId - The ID of the resizer handle element. */ - initializeResizablePopup(popupId, resizeCallback = null) { + initializeResizablePopup(popupId, resizeCallback = null, headerId = null, resizerId) { const popupElement = document.getElementById(popupId); - this.dragElement(popupElement); - this.makeResizable(popupElement, resizeCallback); + this.dragElement(popupElement, headerId); + this.makeResizable(popupElement, resizeCallback, resizerId); } /** * Make an element draggable by dragging its header. * @param {HTMLElement} elm - The element to make draggable. + * @param {string|null} headerId - The ID of the header to use for dragging, or null to drag the entire element. */ - dragElement(elm) { - const header = document.getElementById("draggable_header"); + dragElement(elm, headerId = null) { + const header = headerId ? document.getElementById(headerId) : elm; let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; if (header) { @@ -102,9 +115,10 @@ class User_Interface { * Make an element resizable and optionally call a resize callback (like for Blockly workspace). * @param {HTMLElement} elm - The element to make resizable. * @param {function|null} resizeCallback - Optional callback to resize specific content. + * @param {string} resizerId - The ID of the resizer handle element. */ - makeResizable(elm, resizeCallback = null) { - const resizer = document.getElementById("resize-br"); + makeResizable(elm, resizeCallback = null, resizerId) { + const resizer = document.getElementById(resizerId); let originalWidth = 0; let originalHeight = 0; let originalMouseX = 0; diff --git a/src/static/indicator_blocks.js b/src/static/indicator_blocks.js new file mode 100644 index 0000000..4724bae --- /dev/null +++ b/src/static/indicator_blocks.js @@ -0,0 +1,38 @@ + // Define Blockly blocks dynamically based on indicators + export function defineIndicatorBlocks() { + const indicatorOutputs = window.UI.indicators.getIndicatorOutputs(); + const toolboxCategory = document.querySelector('#toolbox category[name="Indicators"]'); + + for (let indicatorName in indicatorOutputs) { + const outputs = indicatorOutputs[indicatorName]; + + // Define the block for this indicator + Blockly.defineBlocksWithJsonArray([{ + "type": indicatorName, + "message0": `${indicatorName} %1`, + "args0": [ + { + "type": "field_dropdown", + "name": "OUTPUT", + "options": outputs.map(output => [output, output]) + } + ], + "output": "Number", + "colour": 230, + "tooltip": `Select the ${indicatorName} output`, + "helpUrl": "" + }]); + + // Define how this block will generate Python code + Blockly.Python[indicatorName] = Blockly.Python.forBlock[indicatorName] = function(block) { + const selectedOutput = block.getFieldValue('OUTPUT'); + const code = `get_${indicatorName.toLowerCase()}_value('${selectedOutput}')`; + return [code, Blockly.Python.ORDER_ATOMIC]; + }; + + // Append dynamically created blocks to the Indicators category in the toolbox + const blockElement = document.createElement('block'); + blockElement.setAttribute('type', indicatorName); + toolboxCategory.appendChild(blockElement); + } + } diff --git a/src/static/indicators.js b/src/static/indicators.js index aab2712..4ce6792 100644 --- a/src/static/indicators.js +++ b/src/static/indicators.js @@ -1,38 +1,55 @@ class Indicator_Output { constructor(name) { - this.legend={}; + this.legend = {}; } - create_legend(name, chart, lineSeries){ + create_legend(name, chart, lineSeries) { // Create legend div and append it to the output element let target_div = document.getElementById('indicator_output'); this.legend[name] = document.createElement('div'); this.legend[name].className = 'legend'; + this.legend[name].style.opacity = 0.1; // Initially mostly transparent + this.legend[name].style.transition = 'opacity 1s ease-out'; // Smooth transition for fade-out target_div.appendChild(this.legend[name]); this.legend[name].style.display = 'block'; this.legend[name].style.left = 3 + 'px'; this.legend[name].style.top = 3 + 'px'; // subscribe set legend text to crosshair moves chart.subscribeCrosshairMove((param) => { - this.set_legend_text(param.seriesPrices.get(lineSeries),name); + this.set_legend_text(param.seriesPrices.get(lineSeries), name); }); } - - set_legend_text(priceValue,name) { + set_legend_text(priceValue, name) { // Callback assigned to fire on crosshair movements. let val = 'n/a'; - if (priceValue !== undefined) { - val = (Math.round(priceValue * 100) / 100).toFixed(2); - } - this.legend[name].innerHTML = name + ' ' + val + ''; + if (priceValue !== undefined) { + val = (Math.round(priceValue * 100) / 100).toFixed(2); + } + + // Update legend text + this.legend[name].innerHTML = `${name} ${val}`; + + // Make legend fully visible + this.legend[name].style.opacity = 1; + this.legend[name].style.display = 'block'; + + // Set a timeout to fade out the legend after 3 seconds + clearTimeout(this.legend[name].fadeTimeout); // Clear any previous timeout to prevent conflicts + this.legend[name].fadeTimeout = setTimeout(() => { + this.legend[name].style.opacity = 0.1; // Gradually fade out + // Set another timeout to hide the element after the fade-out transition + setTimeout(() => { + this.legend[name].style.display = 'none'; + }, 1000); // Wait for the fade-out transition to complete (1s) + }, 1000); } 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.`); + for (const key in this.legend) { + if (key.startsWith(name)) { + this.legend[key].remove(); // Remove the legend from the DOM + delete this.legend[key]; // Remove the reference from the object + } } } } @@ -84,27 +101,71 @@ class Indicator { color: color, lineWidth: lineWidth }); - // Initialise the crosshair legend for the charts. - iOutput.create_legend(this.name, chart, this.lines[name]); + // Initialise the crosshair legend for the charts with a unique name for each line. + iOutput.create_legend(`${this.name}_${name}`, chart, this.lines[name]); } - setLine(name, data, value_name) { - console.log('indicators[68]: setLine takes:(name,data,value_name)'); - console.log(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); - // Update indicator output/crosshair legend. - iOutput.set_legend_text(data.at(-1).value, this.name); + setLine(lineName, data, value_name) { + console.log('indicators[68]: setLine takes:(lineName, data, value_name)'); + console.log(lineName, data, value_name); + + let priceValue; + + // Check if the data is a multi-value object + if (typeof data === 'object' && data !== null && value_name in data) { + // Multi-value indicator: Extract the array for the specific key + const processedData = data[value_name]; + + // Set the data for the line + this.lines[lineName].setData(processedData); + + // Isolate the last value provided and round to 2 decimal places + priceValue = processedData.at(-1).value; + + // Update the display and legend for multi-value indicators + this.updateDisplay(lineName, { [value_name]: priceValue }, 'value'); + } else { + // Single-value indicator: Initialize the data directly + this.lines[lineName].setData(data); + + // Isolate the last value provided and round to 2 decimal places + priceValue = data.at(-1).value; + + // Update the display and legend for single-value indicators + this.updateDisplay(lineName, priceValue, value_name); + } + iOutput.set_legend_text(priceValue, `${this.name}_${lineName}`); } updateDisplay(name, priceValue, value_name) { - // Update the data in the edit and view indicators panel - let element = document.getElementById(this.name + '_' + value_name) - if (element){ - element.value = (Math.round(priceValue * 100) / 100).toFixed(2); + let element = document.getElementById(this.name + '_' + value_name); + if (element) { + if (typeof priceValue === 'object' && priceValue !== null) { + // Handle multiple values by joining them into a single string with labels + let currentValues = element.value ? element.value.split(', ').reduce((acc, pair) => { + let [key, val] = pair.split(': '); + if (!isNaN(parseFloat(val))) { + acc[key] = parseFloat(val); + } + return acc; + }, {}) : {}; + + // Update current values with the new key-value pairs + Object.assign(currentValues, priceValue); + + // Set the updated values back to the element + element.value = Object.entries(currentValues) + .filter(([key, value]) => !isNaN(value)) // Skip NaN values + .map(([key, value]) => `${key}: ${(Math.round(value * 100) / 100).toFixed(2)}`) + .join(', '); // Use comma for formatting + } else { + // Handle simple values as before + element.value = (Math.round(priceValue * 100) / 100).toFixed(2); + } + + // Adjust the element styling dynamically for wrapping and height + element.style.height = 'auto'; // Reset height + element.style.height = (element.scrollHeight) + 'px'; // Adjust height based on content } else { console.warn(`Element with ID ${this.name}_${value_name} not found.`); } @@ -115,12 +176,34 @@ class Indicator { } updateLine(name, data, value_name) { - // Update the line-set data in the chart - this.lines[name].update(data); - // Update indicator output/crosshair legend. - iOutput.set_legend_text(data.value, this.name); - // Update the data in the edit and view indicators panel - this.updateDisplay(name, data.value, value_name); + console.log('indicators[68]: updateLine takes:(name, data, value_name)'); + console.log(name, data, value_name); + + // Check if the data is a multi-value object + if (typeof data === 'object' && data !== null && value_name in data) { + // Multi-value indicator: Extract the array for the specific key + const processedData = data[value_name]; + + // Update the line-set data in the chart + this.lines[name].update(processedData); + + // Isolate the last value provided and round to 2 decimal places + const priceValue = processedData.at(-1).value; + + // Update the display and legend for multi-value indicators + this.updateDisplay(name, { [value_name]: priceValue }, 'value'); + iOutput.set_legend_text(priceValue, `${this.name}_${name}`); + } else { + // Single-value indicator: Initialize the data directly + this.lines[name].update(data); + + // Isolate the last value provided and round to 2 decimal places + const priceValue = data.at(-1).value; + + // Update the display and legend for single-value indicators + this.updateDisplay(name, priceValue, value_name); + iOutput.set_legend_text(priceValue, `${this.name}_${name}`); + } } updateHist(name, data) { @@ -249,19 +332,25 @@ class MACD extends Indicator { 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 => ({ + // Prepare the filtered data for the MACD line + const 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 => ({ + // Set the 'line_m' for the MACD line + this.setLine('line_m', { macd: line_m }, 'macd'); + + // Prepare the filtered data for the signal line + const line_s = filteredData.map(row => ({ time: row.time, value: row.signal - })), 'signal'); + })); - // Set the histogram + // Set the 'line_s' for the signal line + this.setLine('line_s', { signal: line_s }, 'signal'); + + // Set the histogram data this.setHist(this.name, filteredData.map(row => ({ time: row.time, value: row.hist @@ -271,15 +360,35 @@ class MACD extends Indicator { } } + update(data) { - // Update the 'macd' line - this.updateLine('line_m', {time: data[0].time, value: data[0].macd }, 'macd'); + // Filter out rows where macd, signal, or hist are null + const filteredData = data.filter(row => row.macd !== null && row.signal !== null && row.hist !== null); - // Update the 'signal' line - this.updateLine('line_s', { time: data[0].time, value: data[0].signal }, 'signal'); + if (filteredData.length > 0) { + // Update the 'macd' line + const line_m = filteredData.map(row => ({ + time: row.time, + value: row.macd + })); + this.updateLine('line_m', { macd: line_m }, 'macd'); - // Update the 'hist' (histogram) bar - this.updateHist('hist', {time: data[0].time, value: data[0].hist }); + // Update the 'signal' line + const line_s = filteredData.map(row => ({ + time: row.time, + value: row.signal + })); + this.updateLine('line_s', { signal: line_s }, 'signal'); + + // Update the 'hist' (histogram) bar + const hist_data = filteredData.map(row => ({ + time: row.time, + value: row.hist + })); + this.updateHist('hist', hist_data); + } else { + console.error('No valid MACD data found for update.'); + } } } indicatorMap.set("MACD", MACD); @@ -341,30 +450,64 @@ class Bolenger extends Indicator { }; } - init(data) { - // Set the 'line_u' for the upper line - this.setLine('line_u', data.map(row => ({ - time: row.time, - value: row.upper - })), 'value'); + init(data) { + // Filter out rows where upper, middle, or lower are null + const filteredData = data.filter(row => row.upper !== null && row.middle !== null && row.lower !== null); - // Set the 'line_m' for the middle line - this.setLine('line_m', data.map(row => ({ - time: row.time, - value: row.middle - })), 'value2'); + if (filteredData.length > 0) { + // Set the 'line_u' for the upper line + const line_u = filteredData.map(row => ({ + time: row.time, + value: row.upper + })); + this.setLine('line_u', { upper: line_u }, 'upper'); - // Set the 'line_l' for the lower line - this.setLine('line_l', data.map(row => ({ - time: row.time, - value: row.lower - })), 'value3'); -} + // Set the 'line_m' for the middle line + const line_m = filteredData.map(row => ({ + time: row.time, + value: row.middle + })); + this.setLine('line_m', { middle: line_m }, 'middle'); + + // Set the 'line_l' for the lower line + const line_l = filteredData.map(row => ({ + time: row.time, + value: row.lower + })); + this.setLine('line_l', { lower: line_l }, 'lower'); + } else { + console.error('No valid data found for init.'); + } + } update(data) { - this.updateLine('line_u', data[0][0], 'value'); - this.updateLine('line_m', data[1][0], 'value2'); - this.updateLine('line_l', data[2][0], 'value3'); + // Filter out rows where upper, middle, or lower are null + const filteredData = data.filter(row => row.upper !== null && row.middle !== null && row.lower !== null); + + if (filteredData.length > 0) { + // Update the 'upper' line + const line_u = filteredData.map(row => ({ + time: row.time, + value: row.upper + })); + this.updateLine('line_u', { upper: line_u }, 'upper'); + + // Update the 'middle' line + const line_m = filteredData.map(row => ({ + time: row.time, + value: row.middle + })); + this.updateLine('line_m', { middle: line_m }, 'middle'); + + // Update the 'lower' line + const line_l = filteredData.map(row => ({ + time: row.time, + value: row.lower + })); + this.updateLine('line_l', { lower: line_l }, 'lower'); + } else { + console.error('No valid data found for update.'); + } } } indicatorMap.set("BOLBands", Bolenger); @@ -422,8 +565,12 @@ class Indicators { objects, then inserts the data into the charts. */ this.create_indicators(idata.indicators, charts); - // Initialize each indicators with the data. - idata.indicator_data.then( (data) => { this.init_indicators(data); } ); + // Initialize each indicator with the data directly + if (idata.indicator_data) { + this.init_indicators(idata.indicator_data); + } else { + console.error('Indicator data is not available.'); + } } init_indicators(data){ diff --git a/src/static/json_generators.js b/src/static/json_generators.js new file mode 100644 index 0000000..51079ec --- /dev/null +++ b/src/static/json_generators.js @@ -0,0 +1,114 @@ + // Define JSON generators for custom blocks + export function defineJsonGenerators() { + // Initialize JSON generator + if (!Blockly.JSON) { + Blockly.JSON = new Blockly.Generator('JSON'); + } + + + // JSON Generator for 'trade_action' block + Blockly.JSON['trade_action'] = function(block) { + const condition = Blockly.JSON.valueToCode(block, 'CONDITION', Blockly.JSON.ORDER_ATOMIC); + const tradeType = block.getFieldValue('TRADE_TYPE'); + const size = Blockly.JSON.valueToCode(block, 'SIZE', Blockly.JSON.ORDER_ATOMIC) || null; + const stopLoss = Blockly.JSON.valueToCode(block, 'STOP_LOSS', Blockly.JSON.ORDER_ATOMIC) || null; + const takeProfit = Blockly.JSON.valueToCode(block, 'TAKE_PROFIT', Blockly.JSON.ORDER_ATOMIC) || null; + const tradeOptions = Blockly.JSON.statementToCode(block, 'TRADE_OPTIONS').trim(); + + const json = { + type: 'trade_action', + condition: condition, + trade_type: tradeType, + size: size, + stop_loss: stopLoss, + take_profit: takeProfit, + trade_options: tradeOptions ? JSON.parse(tradeOptions) : [] + }; + + return JSON.stringify(json); + }; + // JSON generator for 'order_type' block + Blockly.JSON['order_type'] = function(block) { + const orderType = block.getFieldValue('ORDER_TYPE'); + const limitPrice = Blockly.JSON.valueToCode(block, 'LIMIT_PRICE', Blockly.JSON.ORDER_ATOMIC) || null; + + const json = { + order_type: orderType, + limit_price: limitPrice + }; + return JSON.stringify(json); + }; + + // JSON generator for 'time_in_force' block + Blockly.JSON['time_in_force'] = function(block) { + const tif = block.getFieldValue('TIF'); + const json = { tif: tif }; + return JSON.stringify(json); + }; + + // JSON generator for 'comparison' block + Blockly.JSON['comparison'] = Blockly.JSON.forBlock['comparison'] = function(block) { + const left = Blockly.JSON.valueToCode(block, 'LEFT', Blockly.JSON.ORDER_ATOMIC); + const right = Blockly.JSON.valueToCode(block, 'RIGHT', Blockly.JSON.ORDER_ATOMIC); + const operator = block.getFieldValue('OPERATOR'); + const json = { + type: 'comparison', + operator: operator, + left: left, + right: right + }; + return JSON.stringify(json); + }; + + // JSON generator for 'logical_and' block + Blockly.JSON['logical_and'] = Blockly.JSON.forBlock['logical_and'] = function(block) { + const left = Blockly.JSON.valueToCode(block, 'LEFT', Blockly.JSON.ORDER_ATOMIC); + const right = Blockly.JSON.valueToCode(block, 'RIGHT', Blockly.JSON.ORDER_ATOMIC); + const json = { + type: 'logical_and', + left: left, + right: right + }; + return JSON.stringify(json); + }; + + // JSON generator for 'logical_or' block + Blockly.JSON['logical_or'] = Blockly.JSON.forBlock['logical_or'] = function(block) { + const left = Blockly.JSON.valueToCode(block, 'LEFT', Blockly.JSON.ORDER_ATOMIC); + const right = Blockly.JSON.valueToCode(block, 'RIGHT', Blockly.JSON.ORDER_ATOMIC); + const json = { + type: 'logical_or', + left: left, + right: right + }; + return JSON.stringify(json); + }; + + // JSON generator for 'last_candle_value' block + Blockly.JSON['last_candle_value'] = Blockly.JSON.forBlock['last_candle_value'] = function(block) { + const candlePart = block.getFieldValue('CANDLE_PART'); + const source = Blockly.JSON.valueToCode(block, 'SOURCE', Blockly.JSON.ORDER_ATOMIC) || null; + const json = { + type: 'last_candle_value', + candle_part: candlePart, + source: source + }; + return JSON.stringify(json); + }; + + // JSON generator for 'source' block + Blockly.JSON['source'] = Blockly.JSON.forBlock['source'] = function(block) { + const timeframe = block.getFieldValue('TF'); + const exchange = block.getFieldValue('EXC'); + const symbol = block.getFieldValue('SYM'); + const json = { + type: 'source', + timeframe: timeframe, + exchange: exchange, + symbol: symbol + }; + return JSON.stringify(json); + }; + + console.log('JSON generators defined with forBlock assignments'); + }; diff --git a/src/static/python_generators.js b/src/static/python_generators.js new file mode 100644 index 0000000..3229cd9 --- /dev/null +++ b/src/static/python_generators.js @@ -0,0 +1,178 @@ + // Define Python generators for custom blocks + export function definePythonGenerators() { + // Python Generator for target_market + Blockly.Python['target_market'] = Blockly.Python.forBlock['target_market'] = function(block) { + var timeframe = block.getFieldValue('TF'); + var exchange = block.getFieldValue('EXC'); + var symbol = block.getFieldValue('SYM'); + + var code = `target_market(timeframe='${timeframe}', exchange='${exchange}', symbol='${symbol}')`; + return code; + }; + // Generator for last_candle_value + Blockly.Python['last_candle_value'] = Blockly.Python.forBlock['last_candle_value'] = function(block) { + var candlePart = block.getFieldValue('CANDLE_PART'); + var source = Blockly.Python.valueToCode(block, 'SOURCE', Blockly.Python.ORDER_ATOMIC) || 'None'; // Handle optional source + + var code; + if (source !== 'None') { + // Use the provided source feed if available + code = `get_last_candle_value('${candlePart}', source=${source})`; + } else { + // Fallback to default source + code = `get_last_candle_value('${candlePart}')`; + } + + return [code, Blockly.Python.ORDER_ATOMIC]; + }; + + + // Comparison block to Python code + Blockly.Python['comparison'] = Blockly.Python.forBlock['comparison'] = function(block) { + const left = Blockly.Python.valueToCode(block, 'LEFT', Blockly.Python.ORDER_ATOMIC); + const right = Blockly.Python.valueToCode(block, 'RIGHT', Blockly.Python.ORDER_ATOMIC); + const operator = block.getFieldValue('OPERATOR'); + return [left + ' ' + operator + ' ' + right, Blockly.Python.ORDER_ATOMIC]; + }; + + // Logical OR block to Python code + Blockly.Python['logical_or'] = Blockly.Python.forBlock['logical_or'] = function(block) { + var left = Blockly.Python.valueToCode(block, 'LEFT', Blockly.Python.ORDER_ATOMIC); + var right = Blockly.Python.valueToCode(block, 'RIGHT', Blockly.Python.ORDER_ATOMIC); + var code = `${left} or ${right}`; + return [code, Blockly.Python.ORDER_ATOMIC]; + }; + + // Logical AND block to Python code + Blockly.Python['logical_and'] = Blockly.Python.forBlock['logical_and'] = function(block) { + var left = Blockly.Python.valueToCode(block, 'LEFT', Blockly.Python.ORDER_ATOMIC); + var right = Blockly.Python.valueToCode(block, 'RIGHT', Blockly.Python.ORDER_ATOMIC); + var code = `${left} and ${right}`; + return [code, Blockly.Python.ORDER_ATOMIC]; + }; + + // Stop Loss block to Python code + Blockly.Python['stop_loss'] = Blockly.Python.forBlock['stop_loss'] = function(block) { + var stopLoss = Blockly.Python.valueToCode(block, 'STOP_LOSS', Blockly.Python.ORDER_ATOMIC); + return [stopLoss, Blockly.Python.ORDER_ATOMIC]; + }; + + // Take Profit block to Python code + Blockly.Python['take_profit'] = Blockly.Python.forBlock['take_profit'] = function(block) { + var takeProfit = Blockly.Python.valueToCode(block, 'TAKE_PROFIT', Blockly.Python.ORDER_ATOMIC); + return [takeProfit, Blockly.Python.ORDER_ATOMIC]; + }; + + // Python generator for is_false block + Blockly.Python['is_false'] =Blockly.Python.forBlock['is_false']= function(block) { + var condition = Blockly.Python.valueToCode(block, 'CONDITION', Blockly.Python.ORDER_ATOMIC); + var code = `not ${condition}`; + return [code, Blockly.Python.ORDER_ATOMIC]; + }; + + Blockly.Python['trade_action'] = Blockly.Python.forBlock['trade_action'] = function(block) { + const condition = Blockly.Python.valueToCode(block, 'CONDITION', Blockly.Python.ORDER_ATOMIC); + const tradeType = block.getFieldValue('TRADE_TYPE'); + const size = Blockly.Python.valueToCode(block, 'SIZE', Blockly.Python.ORDER_ATOMIC) || 'None'; + const stopLoss = Blockly.Python.valueToCode(block, 'STOP_LOSS', Blockly.Python.ORDER_ATOMIC) || 'None'; + const takeProfit = Blockly.Python.valueToCode(block, 'TAKE_PROFIT', Blockly.Python.ORDER_ATOMIC) || 'None'; + + // Process trade options + let tradeOptionsCode = Blockly.Python.statementToCode(block, 'TRADE_OPTIONS').trim(); + tradeOptionsCode = tradeOptionsCode.split('\n').filter(line => line.trim() !== ''); + + // Collect all arguments into an array + const argsList = [`size=${size}`, `stop_loss=${stopLoss}`, `take_profit=${takeProfit}`]; + if (tradeOptionsCode.length > 0) { + argsList.push(...tradeOptionsCode); + } + const args = argsList.join(', '); + + const code = `if ${condition}:\n self.${tradeType}(${args})\n`; + return code; + }; + + // Order Type block to Python code + Blockly.Python['order_type'] = Blockly.Python.forBlock['order_type'] = function(block) { + const orderType = block.getFieldValue('ORDER_TYPE'); + const limitPrice = Blockly.Python.valueToCode(block, 'LIMIT_PRICE', Blockly.Python.ORDER_ATOMIC) || 'None'; + + let code = `order_type='${orderType}'`; + if (orderType === 'limit') { + code += `, limit_price=${limitPrice}`; + } + return code; + }; + + // Value Input block to Python code + Blockly.Python['value_input'] = Blockly.Python.forBlock['value_input'] = function(block) { + var value = block.getFieldValue('VALUE'); + return [value.toString(), Blockly.Python.ORDER_ATOMIC]; // Returning both value and precedence + }; + + // Time in Force block to Python code + Blockly.Python['time_in_force'] = Blockly.Python.forBlock['time_in_force'] = function(block) { + const tif = block.getFieldValue('TIF'); + const code = `tif='${tif}'`; + return code; + }; + + Blockly.Python['source'] = Blockly.Python.forBlock['source'] = function(block) { + var timeframe = block.getFieldValue('TF'); + var exchange = block.getFieldValue('EXC'); + var symbol = block.getFieldValue('SYM'); + + // Return the source information as an object or string + var code = `{'timeframe': '${timeframe}', 'exchange': '${exchange}', 'symbol': '${symbol}'}`; + return [code, Blockly.Python.ORDER_ATOMIC]; + }; + // Generator code for strategy_profit_loss block + Blockly.Python['strategy_profit_loss'] = Blockly.Python.forBlock['strategy_profit_loss'] = function(block) { + var direction = block.getFieldValue('DIRECTION'); + var code = `strategy_profit_loss('${direction}')`; + return [code, Blockly.Python.ORDER_ATOMIC]; + }; + // Generator code for current_balance block + Blockly.Python['current_balance'] = Blockly.Python.forBlock['current_balance'] = function() { + var code = 'get_current_balance()'; + return [code, Blockly.Python.ORDER_ATOMIC]; + }; + + // Generator code for starting_balance block + Blockly.Python['starting_balance'] = Blockly.Python.forBlock['starting_balance'] = function() { + var code = 'get_starting_balance()'; + return [code, Blockly.Python.ORDER_ATOMIC]; + }; + // Generator code for arithmetic_operator block + Blockly.Python['arithmetic_operator'] = Blockly.Python.forBlock['arithmetic_operator'] = function(block) { + var operator = block.getFieldValue('OPERATOR'); + var left = Blockly.Python.valueToCode(block, 'LEFT', Blockly.Python.ORDER_ATOMIC); + var right = Blockly.Python.valueToCode(block, 'RIGHT', Blockly.Python.ORDER_ATOMIC); + + var code = `${left} ${operator} ${right}`; + return [code, Blockly.Python.ORDER_ATOMIC]; + }; + + // Python generator for the active_trades block + Blockly.Python['active_trades'] = Blockly.Python.forBlock['active_trades'] = function(block) { + var code = `get_active_trades()`; // You would define this method in your Python backtesting engine + return [code, Blockly.Python.ORDER_ATOMIC]; + }; + // Python generator for the flag_is_set block + Blockly.Python['flag_is_set'] = Blockly.Python.forBlock['flag_is_set'] = function(block) { + var flagName = block.getFieldValue('FLAG_NAME'); + var code = `flag_is_set('${flagName}')`; + return [code, Blockly.Python.ORDER_ATOMIC]; + }; + // Python generator for set_flag block + Blockly.Python['set_flag'] = Blockly.Python.forBlock['set_flag'] = function(block) { + var condition = Blockly.Python.valueToCode(block, 'CONDITION', Blockly.Python.ORDER_ATOMIC); + var flagName = block.getFieldValue('FLAG_NAME'); + var flagValue = block.getFieldValue('FLAG_VALUE') === 'True' ? 'True' : 'False'; + + var code = `if ${condition}:\n set_flag('${flagName}', ${flagValue})\n`; + return code; + }; + + console.log('Python generators defined'); + } diff --git a/src/static/signals.js b/src/static/signals.js index da5d4e3..a156cfc 100644 --- a/src/static/signals.js +++ b/src/static/signals.js @@ -1,159 +1,146 @@ class Signals { - constructor(indicators) { - this.indicators = indicators; - this.signals=[]; + constructor(ui) { + this.ui = ui; + this.comms = ui.data.comms; + this.indicators = ui.indicators; + this.data = ui.data; + this.signals = []; + + // Register handlers with Comms for specific message types + this.comms.on('signal_created', this.handleSignalCreated.bind(this)); + this.comms.on('signal_updated', this.handleSignalUpdated.bind(this)); + this.comms.on('signal_deleted', this.handleSignalDeleted.bind(this)); + this.comms.on('updates', this.handleUpdates.bind(this)); } + + handleSignalCreated(data) { + console.log("New signal created:", data); + // Logic to update signals UI + const list_of_one = [data]; + this.set_data(list_of_one); + } + + handleSignalUpdated(data) { + console.log("Signal updated:", data); + // Logic to update signals UI + this.update_signal_states(data); + } + + handleSignalDeleted(data) { + console.log("Signal deleted:", data); + // Logic to remove signal from UI + this.delete_signal(data.name); + } + + handleUpdates(data) { + const { s_updates } = data; + if (s_updates) { + this.update_signal_states(s_updates); + } + } + // Call to display the 'Create new signal' dialog. open_signal_Form() { document.getElementById("new_sig_form").style.display = "grid"; } // Call to hide the 'Create new signal' dialog. close_signal_Form() { document.getElementById("new_sig_form").style.display = "none"; } - request_signals(){ + request_signals() { // Requests a list of all the signals from the server. - if (window.UI.data.comms) { - window.UI.data.comms.sendToApp('request', { request: 'signals', user_name: window.UI.data.user_name }); + if (this.comms) { + this.comms.sendToApp('request', { request: 'signals', user_name: this.data.user_name }); } else { console.error('Comms instance not available.'); } } - delete_signal(signal_name){ + delete_signal(signal_name) { // Requests that the server remove a specific signal. - window.UI.data.comms.sendToApp('delete_signal', signal_name); + this.comms.sendToApp('delete_signal', { name: signal_name }); // Get the signal element from the UI let child = document.getElementById(signal_name + '_item'); // Ask the parent of the signal element to remove its child(signal) from the document. - child.parentNode.removeChild(child); + if (child && child.parentNode) { + child.parentNode.removeChild(child); + } } - i_update(updates){ - // Check the indicator updates for updates that are use as signal sources. - // Update the signals about these changes. - // Update the html that displays that info. - // Loop through all the signals. - for (let signal in this.signals){ - // Get the name of the 1st source. + i_update(updates) { + for (let signal in this.signals) { let s1 = this.signals[signal].source1; - // Check the updates for a source 1 update. - if (s1 in updates){ - // Get the property of that source. + if (s1 in updates) { let p1 = this.signals[signal].prop1; - // Get the value of that property. let value1 = updates[s1].data[0][p1]; - // Update the signals record of the value. this.signals[signal].value1 = value1.toFixed(2); - } - else{ - // If there is no update move onto the next signal. + } else { console.log('!no update for: s1 maybe the indicator is disabled'); break; } - // If the second source is an indicator and not just a value. - if (this.signals[signal].source2 != 'value'){ - // Get the name of the second source. + if (this.signals[signal].source2 != 'value') { let s2 = this.signals[signal].source2; - // Check is source 2 is in the updates. if (s2 in updates) { - // Get the property of that source. let p2 = this.signals[signal].prop2; - // Get the value of that property. let value2 = updates[s2].data[0][p2]; - // Update the signals record of the value. this.signals[signal].value2 = value2.toFixed(2); - } - else{ - // If there is no update move onto the next signal. + } else { console.log('!no update for: s2 maybe the indicator is disabled'); break; } - // loop to next signal. } - // Update the html element that displays this information. document.getElementById(this.signals[signal].name + '_value1').innerHTML = this.signals[signal].value1; document.getElementById(this.signals[signal].name + '_value2').innerHTML = this.signals[signal].value2; } } - update_signal_states(s_updates){ - for (name in s_updates){ - let id = name + '_state' + + update_signal_states(s_updates) { + for (let name in s_updates) { + let id = name + '_state'; let span = document.getElementById(id); - span.innerHTML = s_updates[name]; + if (span) { + span.innerHTML = s_updates[name]; + } console.log('state change!'); console.log(name); } - } - set_data(signals){ - // Create a list item for every signal and add it to a UL element. + + set_data(signals) { var ul = document.getElementById("signal_list"); - - // loop through a provided list of signals and attributes. - for (let sig in signals){ - - // Create a Json object from each signals. - // During initialization this receives the object in string form. - // when the object is created this function receives an object. - if (typeof(signals[sig]) == 'string'){ - var obj = JSON.parse(signals[sig]); - }else {var obj=signals[sig];} - // Keep a local record of the signals. + for (let sig in signals) { + let obj = typeof(signals[sig]) == 'string' ? JSON.parse(signals[sig]) : signals[sig]; this.signals.push(obj); - // Define the function that is called when deleting an individual signal. - let click_func = "window.UI.signals.delete_signal('" + obj.name + "')"; - // create a delete button for every individual signal. - let delete_btn = ''; + let click_func = `this.delete_signal('${obj.name}')`; + let delete_btn = ``; - // Put all the attributes into html elements. - let signal_name = " " + obj.name + ": "; - let signal_state = "" + obj.state + "
"; - let signal_source1 = "" + obj.source1 + "(" + obj.prop1 + ") "; - let signal_val1 = "" + obj.value1 + ""; - let operator = " " + obj.operator + " "; - let signal_source2 = "" + obj.source2 + "(" + obj.prop2 + ") "; - let signal_val2 = "" + obj.value2 + ""; + let signal_name = ` ${obj.name}: `; + let signal_state = `${obj.state}
`; + let signal_source1 = `${obj.source1}(${obj.prop1}) `; + let signal_val1 = `${obj.value1}`; + let operator = ` ${obj.operator} `; + let signal_source2 = `${obj.source2}(${obj.prop2}) `; + let signal_val2 = `${obj.value2}`; - // Stick all the html together. - let html = delete_btn; - html += signal_name + signal_state; - html += signal_source1 + signal_val1; - html += operator; - html += signal_source2 + signal_val2; + let html = delete_btn + signal_name + signal_state + signal_source1 + signal_val1 + operator + signal_source2 + signal_val2; - // Create the list item. let li = document.createElement("li"); - // Give it an id. li.id = obj.name + '_item'; - // Inject the html. - li.innerHTML= html; - // And add it the the UL we created earlier. + li.innerHTML = html; ul.appendChild(li); } } - fill_prop(target_id, indctr){ - // arg1: Id of of a selection element. - // arg2: Name of an indicator - // Replace the options of an HTML select elements - // with the properties an indicator object. - - // Fetch the objects using name and id received. + fill_prop(target_id, indctr) { var target = document.getElementById(target_id); - var properties = window.UI.data.indicators[indctr]; + var properties = this.indicators[indctr]; - // Remove any previous options in the select tag. removeOptions(target); - // Loop through each property in the object. - // Create an option element for each one. - // Append it to the selection element. - for(let prop in properties) - { - if (prop =='type'|| prop == 'visible' || prop == 'period'){continue;} - if (prop.substring(0,5) == 'color'){continue;} - var opt = document.createElement("option"); - opt.innerHTML = prop; - target.appendChild(opt); + for(let prop in properties) { + if (prop == 'type' || prop == 'visible' || prop == 'period' || prop.substring(0, 5) == 'color') { + continue; } - return; + var opt = document.createElement("option"); + opt.innerHTML = prop; + target.appendChild(opt); + } function removeOptions(selectElement) { var i, L = selectElement.options.length - 1; for(i = L; i >= 0; i--) { @@ -161,140 +148,86 @@ class Signals { } } } - switch_panel(p1,p2){ - // Panel switcher for multi page forms - // arg1 = target from id - // arg2 = next target id - // This function is used in the New Signal dialog in signals - document.getElementById(p1).style.display='none'; - document.getElementById(p2).style.display='grid'; + + switch_panel(p1, p2) { + document.getElementById(p1).style.display = 'none'; + document.getElementById(p2).style.display = 'grid'; } - hideIfTrue(firstValue, scndValue, id){ - // Compare first two args and hides an element if they are equal. - // This function is used in the New Signal dialog in signals - if( firstValue == scndValue){ - document.getElementById(id).style.display='none'; - }else{ - document.getElementById(id).style.display='block' + hideIfTrue(firstValue, scndValue, id) { + if (firstValue == scndValue) { + document.getElementById(id).style.display = 'none'; + } else { + document.getElementById(id).style.display = 'block'; } } - ns_next(n){ - // This function is used in the New Signal dialog in signals - if (n==1){ - // Check input fields. + + ns_next(n) { + if (n == 1) { let sigName = document.getElementById('signal_name').value; let sigSource = document.getElementById('sig_source').value; let sigProp = document.getElementById('sig_prop').value; - if (sigName == '' ) { alert('Please give the signal a name.'); return; } - // Populate sig_display - document.getElementById('sig_display').innerHTML = (sigName + ': {' + sigSource + ':' + sigProp +'}'); - // Popilate Value input - let indctrVal = document.getElementById(sigSource + '_' + sigProp).value; + if (sigName == '') { alert('Please give the signal a name.'); return; } + document.getElementById('sig_display').innerHTML = `${sigName}: {${sigSource}:${sigProp}}`; + let indctrVal = document.getElementById(sigSource + '_' + sigProp).value; document.getElementById('value').value = indctrVal; - - this.switch_panel('panel_1','panel_2'); + this.switch_panel('panel_1', 'panel_2'); } - if (n==2){ + if (n == 2) { + let sigName = document.getElementById('signal_name').value; + let sigSource = document.getElementById('sig_source').value; + let sigProp = document.getElementById('sig_prop').value; + let sig2Source = document.getElementById('sig2_source').value; + let sig2Prop = document.getElementById('sig2_prop').value; + let operator = document.querySelector('input[name="Operator"]:checked').value; + let range = document.getElementById('rangeVal').value; + let sigType = document.getElementById('select_s_type').value; + let value = document.getElementById('value').value; - // Collect all the input fields. - - let sigName = document.getElementById('signal_name').value; // The name of the New Signal. - let sigSource = document.getElementById('sig_source').value; // The source(indicator) of the signal. - let sigProp = document.getElementById('sig_prop').value; // The property to evaluate. - let sig2Source = document.getElementById('sig2_source').value; // The second source if selected. - let sig2Prop = document.getElementById('sig2_prop').value; // The second property to evaluate. - let operator = document.querySelector('input[name="Operator"]:checked').value; // The operator this evaluation will use. - let range = document.getElementById('rangeVal').value; // The value of any range being evaluated. - let sigType = document.getElementById('select_s_type').value; // The type of signal value or indicator comparison. - let value = document.getElementById('value').value; // The input value if it is a value comparison. - - // Create a string to define the signal. - - // Include the first indicator source. - var sig1 = `${sigSource} : ${sigProp}`; - // If it is a comparison signal include the second indicator source. - if (sigType == 'Comparison') { - var sig2 = `${sig2Source} : ${sig2Prop}`; - } - // If it is a value signal include the value. - if (sigType == 'Value') {var sig2 = value;} - - // If the operator is set to range, include the range value in the string. - if (operator == '+/-') { - var operatorStr = `${operator} ${range}`; - } else{ - var operatorStr = operator; - } + let sig1 = `${sigSource} : ${sigProp}`; + let sig2 = sigType == 'Comparison' ? `${sig2Source} : ${sig2Prop}` : value; + let operatorStr = operator == '+/-' ? `${operator} ${range}` : operator; let sigDisplayStr = `(${sigName}) (${sig1}) (${operatorStr}) (${sig2})`; - - // Get the current realtime values of the sources. let sig1_realtime = document.getElementById(sigSource + '_' + sigProp).value; + let sig2_realtime = sigType == 'Comparison' ? document.getElementById(sig2Source + '_' + sig2Prop).value : sig2; - if (sigType == 'Comparison') { - // If its a comparison get the second value from the second source. - var sig2_realtime = document.getElementById(sig2Source + '_' + sig2Prop).value; - }else { - // If not the second realtime value is literally the value. - var sig2_realtime = sig2; - } - // Populate the signal display field with the string. document.getElementById('sig_display2').innerHTML = sigDisplayStr; + document.getElementById('sig_realtime').innerHTML = `(${sigProp} : ${sig1_realtime}) (${operatorStr}) (${sig2_realtime})`; - // Populate the realtime values display. - let realtime_Str = `(${sigProp} : ${sig1_realtime}) (${operatorStr}) (${sig2_realtime})`; - document.getElementById('sig_realtime').innerHTML = realtime_Str; - // Evaluate the signal - var evalStr; - if (operator == '=') {evalStr = (sig1_realtime == sig2_realtime);console.log([sig1_realtime, sig2_realtime, operator,evalStr]);} - if (operator == '>') {evalStr = (sig1_realtime > sig2_realtime);} - if (operator == '<') {evalStr = (sig1_realtime < sig2_realtime);} - if (operator == '+/-') { - evalStr = (Math.abs(sig1_realtime - sig2_realtime) <= range); - } + let evalStr; + if (operator == '=') evalStr = (sig1_realtime == sig2_realtime); + if (operator == '>') evalStr = (sig1_realtime > sig2_realtime); + if (operator == '<') evalStr = (sig1_realtime < sig2_realtime); + if (operator == '+/-') evalStr = (Math.abs(sig1_realtime - sig2_realtime) <= range); - // Populate the signal eval field with the string. document.getElementById('sig_eval').innerHTML = evalStr; - - // Show the panel - this.switch_panel('panel_2','panel_3'); + this.switch_panel('panel_2', 'panel_3'); } } - submitNewSignal(){ + submitNewSignal() { + let name = document.getElementById('signal_name').value; + let source1 = document.getElementById('sig_source').value; + let prop1 = document.getElementById('sig_prop').value; + let source2 = document.getElementById('sig2_source').value; + let prop2 = document.getElementById('sig2_prop').value; + let operator = document.querySelector('input[name="Operator"]:checked').value; + let range = document.getElementById('rangeVal').value; + let sigType = document.getElementById('select_s_type').value; + let value = document.getElementById('value').value; - // Collect all the input fields. - - var name = document.getElementById('signal_name').value; // The name of the New Signal. - var source1 = document.getElementById('sig_source').value; // The source(indicator) of the signal. - var prop1 = document.getElementById('sig_prop').value; // The property to evaluate. - var source2 = document.getElementById('sig2_source').value; // The second source if selected. - var prop2 = document.getElementById('sig2_prop').value; // The second property to evaluate. - var operator = document.querySelector('input[name="Operator"]:checked').value; // The operator this evaluation will use. - var range = document.getElementById('rangeVal').value; // The value of any range being evaluated. - var sigType = document.getElementById('select_s_type').value; // The type of signal value or indicator comparison. - var value = document.getElementById('value').value; // The input value if it is a value comparison. - var state = false; - if (sigType == 'Comparison'){ - var source2 = source2; - var prop2 = prop2; - }else{ - var source2 = 'value'; - var prop2 = value; + if (sigType != 'Comparison') { + source2 = 'value'; + prop2 = value; } - var value1 = null; - var value2 = null; - if (operator == "+/-" ){ - var range = {range : range}; - var data = {name, source1, prop1, operator, source2, prop2, range, state, value1, value2}; - }else{ - var data = {name, source1, prop1, operator, source2, prop2, state, value1, value2}; - } - /* It may be more maintainable to configure the connection inside the different classes - than passing functions, references and callbacks around. */ - window.UI.data.comms.sendToApp( "new_signal", data); - this.close_signal_Form(); + let state = false; + let value1 = null; + let value2 = null; + let data = operator == "+/-" ? {name, source1, prop1, operator, source2, prop2, range, state, value1, value2} : {name, source1, prop1, operator, source2, prop2, state, value1, value2}; + + this.comms.sendToApp("new_signal", data); + this.close_signal_Form(); } -} \ No newline at end of file +} diff --git a/src/static/trade.js b/src/static/trade.js index 9478d40..194b034 100644 --- a/src/static/trade.js +++ b/src/static/trade.js @@ -47,12 +47,15 @@ class Trade { // Store this object pointer for referencing inside callbacks and event handlers. var that = this; // Assign the quote value of the asset to the current price display element. - window.UI.data.price_history.then((ph) => { + if (window.UI.data.price_history && window.UI.data.price_history.length > 0) { + let ph = window.UI.data.price_history; // Assign the last closing price in the price history to the price input element. - that.priceInput_el.value = ph[ph.length-1].close; + that.priceInput_el.value = ph[ph.length - 1].close; // Set the current price display to the same value. that.currentPrice_el.value = that.priceInput_el.value; - }); + } else { + console.error('Price history data is not available or empty.'); + } // Set the trade value to zero. This will update when price and quantity inputs are received. this.tradeValue_el.value = 0; // Toggle current price or input-field for value updates depending on orderType. diff --git a/src/templates/backtest_popup.html b/src/templates/backtest_popup.html new file mode 100644 index 0000000..0caac51 --- /dev/null +++ b/src/templates/backtest_popup.html @@ -0,0 +1,105 @@ + + + + diff --git a/src/templates/backtesting_hud.html b/src/templates/backtesting_hud.html index b3cc64f..dc9fab2 100644 --- a/src/templates/backtesting_hud.html +++ b/src/templates/backtesting_hud.html @@ -1,3 +1,7 @@ -
-

Back Testing

-
+
+ +
+

Back Tests

+
+
+ diff --git a/src/templates/index.html b/src/templates/index.html index 0de3e2f..47a733e 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -29,11 +29,13 @@ + + {% include "backtest_popup.html" %} {% include "new_trade_popup.html" %} {% include "new_strategy_popup.html" %} {% include "new_signal_popup.html" %} diff --git a/src/templates/indicators_hud.html b/src/templates/indicators_hud.html index 1967802..ba33057 100644 --- a/src/templates/indicators_hud.html +++ b/src/templates/indicators_hud.html @@ -42,11 +42,8 @@
- {% if 'value' in indicator_list[indicator] %} - - {% else %} - - - {% endif %} + +
@@ -103,9 +100,11 @@
{% if 'color' in indicator_list[indicator] %} - + {% else %} - - + {% endif %}
diff --git a/src/templates/new_strategy_popup.html b/src/templates/new_strategy_popup.html index e3b0616..9806c26 100644 --- a/src/templates/new_strategy_popup.html +++ b/src/templates/new_strategy_popup.html @@ -41,7 +41,7 @@ -
+
@@ -109,24 +109,36 @@ - - - - + + + + + + + + + + + - + + + + - - - - - - - + + + + + + + + + diff --git a/src/templates/price_chart.html b/src/templates/price_chart.html index 972b38b..58c3ac5 100644 --- a/src/templates/price_chart.html +++ b/src/templates/price_chart.html @@ -1,10 +1,12 @@
+ + + +
- -
@@ -34,8 +36,6 @@ {% endfor %}
- -