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}
-
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 += `
+ `;
+ }
+ 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