diff --git a/requirements.txt b/requirements.txt index 30bb2d4..513db52 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ requests==2.30.0 pandas==2.2.3 passlib~=1.7.4 ccxt==4.4.8 - +flask-socketio pytz==2024.2 -backtrader==1.9.78.123 \ No newline at end of file +backtrader==1.9.78.123 +eventlet~=0.37.0 \ No newline at end of file diff --git a/src/BrighterTrades.py b/src/BrighterTrades.py index 53b6e63..dd34f2a 100644 --- a/src/BrighterTrades.py +++ b/src/BrighterTrades.py @@ -14,7 +14,7 @@ from trade import Trades class BrighterTrades: - def __init__(self): + def __init__(self, socketio): # Object that interacts with the persistent data. self.data = DataCache() @@ -46,10 +46,11 @@ class BrighterTrades: self.trades.connect_exchanges(exchanges=self.exchanges) # Object that maintains the strategies data - self.strategies = Strategies(self.data, self.trades) + self.strategies = Strategies(self.data, self.trades, self.indicators) # Object responsible for testing trade and strategies data. - self.backtester = Backtester(data_cache=self.data, strategies=self.strategies) + self.backtester = Backtester(data_cache=self.data, strategies=self.strategies, + indicators=self.indicators, socketio=socketio) 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: @@ -325,29 +326,42 @@ class BrighterTrades: Handles the creation of a new strategy based on the provided data. :param data: A dictionary containing the attributes of the new strategy. - :return: An error message if the required attribute is missing, or the incoming data for chaining on success. + :return: A dictionary indicating success or failure with an appropriate message. """ - # Extract user_name from the data and get user_id - user_name = data.get('user_name') - if not user_name: - return {"success": False, "message": "User not specified"} + # Validate presence of required fields + required_fields = ['user_name', 'name', 'workspace', 'code'] + missing_fields = [field for field in required_fields if field not in data] + if missing_fields: + return {"success": False, "message": f"Missing fields: {', '.join(missing_fields)}"} - # Fetch the user_id using the user_name + # Extract user_name and get user_id + user_name = data.get('user_name') user_id = self.get_user_info(user_name=user_name, info='User_id') if not user_id: return {"success": False, "message": "User ID not found"} + # Validate data types + if not isinstance(data['name'], str) or not data['name'].strip(): + return {"success": False, "message": "Invalid or empty strategy name"} + if not isinstance(data['workspace'], str) or not data['workspace'].strip(): + return {"success": False, "message": "Invalid or empty workspace data"} + if not isinstance(data['code'], list) or not data['code']: + return {"success": False, "message": "Invalid or empty strategy code"} + + # Serialize code to JSON string for storage + import json + code_json = json.dumps(data['code']) + # Prepare the strategy data for insertion strategy_data = { "creator": user_id, - "name": data['name'], - "workspace": data['workspace'], - "code": data['code'], + "name": data['name'].strip(), + "workspace": data['workspace'].strip(), + "code": code_json, "stats": data.get('stats', {}), - "public": data.get('public', 0), # Default to private if not specified - "fee": data.get('fee', None) # Default to None if not specified + "public": int(data.get('public', 0)), + "fee": float(data.get('fee', 0.0)) } - # Save the new strategy (in both cache and database) and return the result. return self.strategies.new_strategy(strategy_data) @@ -651,14 +665,14 @@ class BrighterTrades: return - def process_incoming_message(self, msg_type: str, msg_data: dict | str, socket_conn) -> dict | None: + def process_incoming_message(self, msg_type: str, msg_data: dict, socket_conn_id: str) -> dict | None: """ Processes an incoming message and performs the corresponding actions based on the message type and data. + :param socket_conn_id: The WebSocket connection to send updates back to the client. :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. @@ -728,10 +742,17 @@ class BrighterTrades: # 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) + # Validate required fields + required_fields = ['strategy', 'start_date', 'capital', 'commission', 'user_name'] + if not all(field in msg_data for field in required_fields): + return standard_reply("backtest_error", {"message": "Missing required fields."}) + # Delegate backtest handling to the Backtester + resp = self.backtester.handle_backtest_message( + user_id=self.get_user_info(user_name=msg_data['user_name'], info='User_id'), + msg_data=msg_data, + socket_conn_id=socket_conn_id) + + return standard_reply("backtest_submitted", resp) if msg_type == 'delete_backtest': self.delete_backtest(msg_data) diff --git a/src/Strategies.py b/src/Strategies.py index 161174e..e2ba715 100644 --- a/src/Strategies.py +++ b/src/Strategies.py @@ -7,173 +7,8 @@ from DataCache_v3 import DataCache import datetime as dt -# class Strategy: -# def __init__(self, **args): -# """ -# :param args: An object containing key_value pairs representing strategy attributes. -# Strategy format is defined in strategies.js -# """ -# self.active = None -# self.type = None -# self.trade_amount = None -# self.max_position = None -# self.side = None -# self.trd_in_conds = None -# self.merged_loss = None -# self.gross_loss = None -# self.stop_loss = None -# self.take_profit = None -# self.gross_profit = None -# self.merged_profit = None -# self.name = None -# self.current_value = None -# self.opening_value = None -# self.gross_pl = None -# self.net_pl = None -# self.combined_position = None -# -# # A strategy is defined in Strategies.js it is received from the client, -# # then unpacked and converted into a python object here. -# for name, value in args.items(): -# # Make each keyword-argument a specific_property of the class. -# setattr(self, name, value) -# -# # A container to hold previous state of signals. -# self.last_states = {} -# -# # A list of all the trades made by this strategy. -# self.trades = [] -# -# def get_position(self): -# return self.combined_position -# -# def get_pl(self): -# self.update_pl() -# return self.net_pl -# -# def update_pl(self): -# # sum the pl of all the trades. -# position_sum = 0 -# pl_sum = 0 -# opening_value_sum = 0 -# value_sum = 0 -# for trade in self.trades: -# pl_sum += trade.profit_loss -# position_sum += trade.position_size -# value_sum += trade.value -# opening_value_sum += trade.opening_value -# self.combined_position = position_sum -# self.gross_pl = pl_sum -# self.opening_value = opening_value_sum -# self.current_value = value_sum -# -# def to_json(self): -# return json.dumps(self, default=lambda o: o.__dict__, -# sort_keys=True, indent=4) -# -# def evaluate_strategy(self, signals): -# """ -# :param signals: Signals: A reference to an object that handles current signal states. -# :return action: Action required based on evaluation. format{cmd:str, amount:real, margin:int} -# """ -# -# def condition_satisfied(sig_name, value): -# """ -# Check if a signal has a state of value. -# :param sig_name: str: The name of a signal object to compare states. -# :param value: The state value to compare. -# :return bool: True: == . -# """ -# signal = signals.get_signal_by_name(sig_name) -# # Evaluate for a state change -# if value == 'changed': -# # Store the state if it hasn't been stored yet. -# if sig_name not in self.last_states: -# self.last_states.update({sig_name: signal.state}) -# # Store the new state and return true if the state has changed. -# if self.last_states[sig_name] != signal.state: -# self.last_states.update({sig_name: signal.state}) -# return True -# else: -# # Else return true if the values match. -# return value == json.dumps(signal.state) -# -# def all_conditions_met(conditions): -# # Loops through a lists of signal names and states. -# # Returns True if all combinations are true. -# if len(conditions) < 1: -# print(f"no trade-in conditions supplied: {self.name}") -# return False -# # Evaluate all conditions and return false if any are un-met. -# for trigger_signal in conditions.keys(): -# trigger_value = conditions[trigger_signal] -# # Compare this signal's state with the trigger_value -# print(f'evaluating :({trigger_signal, trigger_value})') -# if not condition_satisfied(trigger_signal, trigger_value): -# print('returning false') -# return False -# print('all conditions met!!!') -# return True -# -# def trade_out_condition_met(condition_type): -# # Retrieve the condition from either the 'stop_loss' or 'take_profit' obj. -# condition = getattr(self, condition_type) -# # Subtypes of conditions are 'conditional' or 'value'. -# if condition.typ == 'conditional': -# signal_name = condition.trig -# signal_value = condition.val -# return condition_satisfied(signal_name, signal_value) -# else: -# if condition_type == 'take_profit': -# if self.merged_profit: -# # If the profit condition is met send command to take profit. -# return self.gross_profit > self.take_profit.val -# else: -# # Loop through each associated trade and test -# for trade in self.trades: -# return trade.profit_loss > self.take_profit.val -# elif condition_type == 'value': -# if self.merged_loss: -# # If the loss condition is met, return a trade-out command. -# return self.gross_loss < self.stop_loss.val -# else: -# # Loop through each associated trade and test -# for trade in self.trades: -# return trade.profit_loss < self.stop_loss.val -# else: -# raise ValueError('trade_out_condition_met: invalid condition_type') -# -# trade_in_cmd = self.side -# if self.side == 'buy': -# trade_out_cmd = 'sell' -# else: -# trade_out_cmd = 'buy' -# if self.type == 'in-out': -# print('evaluating trade in conditions for in / out') -# # If trade-in conditions are met. -# if all_conditions_met(self.trd_in_conds): -# # If the new trade wouldn't exceed max_position. Return a trade-in command. -# proposed_position_size = int(self.combined_position) + int(self.trade_amount) -# if proposed_position_size < int(self.max_position): -# return 'open_position', trade_in_cmd -# -# # If strategy is active test the take-profit or stop-loss conditions. -# if self.active: -# # Conditional take-profit trades-out if a signals equals a set value. -# if trade_out_condition_met('take_profit'): -# return 'take_profit', trade_out_cmd -# -# # Conditional stop-loss trades-outs if a signals value equals a set value. -# if trade_out_condition_met('stop_loss'): -# return 'stop_loss', trade_out_cmd -# -# # No conditions were met. -# print('Strategies were updated and nothing to do.') -# return 'do_nothing', 'nothing' - - class Strategies: - def __init__(self, data: DataCache, trades): + def __init__(self, data: DataCache, trades, indicators): """ Initializes the Strategies class. @@ -182,7 +17,8 @@ class Strategies: """ self.data = data # Database interaction instance self.trades = trades - # self.strat_list = [] # List to hold strategy objects + self.strategy_contexts = {} # Dictionary to keep track of strategy contexts + self.indicators_manager = indicators # Create a cache for strategies with necessary columns self.data.create_cache(name='strategies', @@ -190,7 +26,8 @@ class Strategies: size_limit=100, eviction_policy='deny', default_expiration=dt.timedelta(hours=24), - columns=["id", "creator", "name", "workspace", "code", "stats", "public", "fee"]) + columns=["id", "creator", "name", "workspace", "code", "stats", "public", "fee", + "tbl_key", "strategy_components"]) def new_strategy(self, data: dict) -> dict: """ @@ -202,47 +39,64 @@ class Strategies: try: # Check if a strategy with the same name already exists for this user filter_conditions = [('creator', data.get('creator')), ('name', data['name'])] - existing_strategy = self.data.get_rows_from_datacache(cache_name='strategies', - filter_vals=filter_conditions) + existing_strategy = self.data.get_rows_from_datacache( + cache_name='strategies', + filter_vals=filter_conditions + ) if not existing_strategy.empty: return {"success": False, "message": "A strategy with this name already exists"} - # Serialize complex data fields like workspace and stats - workspace_serialized = json.dumps(data['workspace']) if isinstance(data['workspace'], dict) else data[ - 'workspace'] - stats_serialized = json.dumps(data.get('stats', {})) # Convert stats to a JSON string + # Validate and serialize 'workspace' (XML string) + workspace_data = data['workspace'] + if not isinstance(workspace_data, str) or not workspace_data.strip(): + return {"success": False, "message": "Invalid or empty workspace data"} - # generate a unique identifier + # Serialize 'stats' field + try: + stats_data = data.get('stats', {}) + stats_serialized = json.dumps(stats_data) + except (TypeError, ValueError): + return {"success": False, "message": "Invalid stats data format"} + + # Generate strategy components (code, indicators, data_sources, flags) + strategy_components = self.generate_strategy_code(data['code']) + + # Add the combined strategy components to the data to be stored + data['strategy_components'] = json.dumps(strategy_components) + + # Generate a unique identifier tbl_key = str(uuid.uuid4()) # Insert the strategy into the database and cache self.data.insert_row_into_datacache( cache_name='strategies', - columns=("creator", "name", "workspace", "code", "stats", "public", "fee", 'tbl_key'), + columns=("creator", "name", "workspace", "code", "stats", + "public", "fee", 'tbl_key', 'strategy_components'), values=( data.get('creator'), data['name'], - workspace_serialized, # Serialized workspace + data['workspace'], data['code'], - stats_serialized, # Serialized stats - data.get('public', False), - data.get('fee', 0), - tbl_key + stats_serialized, + bool(data.get('public', 0)), + float(data.get('fee', 0.0)), + tbl_key, + data['strategy_components'] ) ) + # Construct the saved strategy data to return saved_strategy = { "id": tbl_key, # Assuming tbl_key is used as a unique identifier "creator": data.get('creator'), "name": data['name'], - "workspace": data['workspace'], # Original workspace data + "workspace": workspace_data, # Original workspace data "code": data['code'], - "stats": data.get('stats', {}), - "public": data.get('public', False), - "fee": data.get('fee', 0) + "stats": stats_data, + "public": bool(data.get('public', 0)), + "fee": float(data.get('fee', 0.0)) } - # If everything is successful, return a success message - # along with the saved strategy data + # If everything is successful, return a success message along with the saved strategy data return { "success": True, "message": "Strategy created and saved successfully", @@ -251,6 +105,9 @@ class Strategies: except Exception as e: # Catch any exceptions and return a failure message + # Consider logging the exception with traceback for debugging + import traceback + traceback.print_exc() return {"success": False, "message": f"Failed to create strategy: {str(e)}"} def delete_strategy(self, user_id, name: str): @@ -277,22 +134,37 @@ class Strategies: try: tbl_key = data['tbl_key'] # The unique identifier for the strategy - # Serialize complex data fields like workspace and stats - workspace_serialized = json.dumps(data['workspace']) if isinstance(data['workspace'], dict) else data[ - 'workspace'] - stats_serialized = json.dumps(data.get('stats', {})) # Convert stats to a JSON string + # Validate and serialize 'workspace' (XML string) + workspace_data = data['workspace'] + if not isinstance(workspace_data, str) or not workspace_data.strip(): + return {"success": False, "message": "Invalid or empty workspace data"} + + # Serialize 'stats' field + try: + stats_data = data.get('stats', {}) + stats_serialized = json.dumps(stats_data) + except (TypeError, ValueError): + return {"success": False, "message": "Invalid stats data format"} + + # Generate updated strategy components (code, indicators, data_sources, flags) + strategy_components = self.generate_strategy_code(data['code']) + + # Add the combined strategy components to the data to be stored + data['strategy_components'] = json.dumps(strategy_components) # Prepare the columns and values for the update - columns = ("creator", "name", "workspace", "code", "stats", "public", "fee", "tbl_key") + columns = ( + "creator", "name", "workspace", "code", "stats", "public", "fee", "tbl_key", "strategy_components") values = ( data.get('creator'), data['name'], - workspace_serialized, # Serialized workspace + workspace_data, # Use the validated workspace data data['code'], stats_serialized, # Serialized stats - data.get('public', False), - data.get('fee', 0), - tbl_key + bool(data.get('public', 0)), + float(data.get('fee', 0.0)), + tbl_key, + data['strategy_components'] # Serialized strategy components ) # Update the strategy in the database and cache @@ -310,6 +182,8 @@ class Strategies: except Exception as e: # Handle exceptions and return failure message + import traceback + traceback.print_exc() return {"success": False, "message": f"Failed to update strategy: {str(e)}"} def get_all_strategy_names(self, user_id) -> list | None: @@ -376,71 +250,393 @@ class Strategies: if filtered_strategies.empty: return None - # Return the filtered strategy row (or a dictionary if needed) - return filtered_strategies.iloc[0].to_dict() + # Get the strategy row as a dictionary + strategy_row = filtered_strategies.iloc[0].to_dict() - def execute_cmd(self, strategy, action, cmd): - order_type = 'LIMIT' - if action == 'open_position': - # Attempt to create the trade. - status, result = self.trades.new_trade(strategy.symbol, cmd, order_type, strategy.trade_amount) - # If the trade failed. - if status == 'Error': - print(status, result) - return 'failed' - else: - # Set the active flag in strategy. - strategy.active = True - strategy.current_position += strategy.trade_amount - strategy.trades.append(result) - return 'position_opened' + # Deserialize the 'strategy_components' field + try: + strategy_components = json.loads(strategy_row.get('strategy_components', '{}')) + except json.JSONDecodeError: + strategy_components = {} + strategy_row['strategy_components'] = strategy_components - if (action == 'stop_loss') or (action == 'take_profit'): - if action == 'stop_loss': - order_type = 'MARKET' - # Attempt to create the trade. - status, result = self.trades.new_trade(strategy['symbol'], cmd, order_type, strategy['current_position']) - # If the trade failed. - if status == 'Error': - print(status, result) - return 'failed' - else: - # Set the active flag in strategy. - strategy['active'] = False - strategy['current_position'] = 0 - return 'position_closed' + # If 'code' is stored as a JSON string, deserialize it + if isinstance(strategy_row.get('code'), str): + strategy_row['code'] = json.loads(strategy_row['code']) - print(f'Strategies.execute_cmd: Invalid action received: {action}') - return 'failed' + return strategy_row - def update(self, signals): + def generate_strategy_code(self, json_code): """ - Receives a reference to updated signal data. Loops through all - published strategies and evaluates conditions against the data. - This function returns a list of strategies and action commands. + Generates the code for the 'next' method and collects indicators, data sources, and flags. + + :param json_code: JSON representation of the strategy logic. + :return: A dictionary containing 'generated_code', 'indicators', 'data_sources', and 'flags_used'. """ - def process_strategy(strategy): - action, cmd = strategy.evaluate_strategy(signals) - if action != 'do_nothing': - # Execute the command. - return {'action': action, 'result': self.execute_cmd(strategy, action, cmd)} - return {'action': 'none'} + if isinstance(json_code, str): + json_code = json.loads(json_code) - def get_stats(strategy): - position = strategy.get_position() - pl = strategy.get_pl() - stats = {'pos': position, 'pl': pl} - return stats + # Initialize code components + code_lines = [] + indent_level = 1 # For 'next' method code indentation + indent = ' ' * indent_level - # Data object returned to function caller. - return_obj = {} - # Loop through all the published strategies. - # for strategy in self.strat_list: - # actions = process_strategy(strategy) - # stat_updates = get_stats(strategy) - # return_obj[strategy.name] = {'actions': actions, 'stats': stat_updates} - if len(return_obj) == 0: - return False + # Initialize sets to collect indicators, data sources, and flags + self.indicators_used = [] + self.data_sources_used = set() + self.flags_used = set() + + # Generate code based on the JSON structure + code_lines.append(f"def next(self):") + indent_level += 1 # Increase indent level inside the 'next' method + + # Generate code from JSON nodes + code_lines.extend(self.generate_code_from_json(json_code, indent_level)) + + # Join the code lines into a single string + next_method_code = '\n'.join(code_lines) + + # Prepare the combined dictionary + strategy_components = { + 'generated_code': next_method_code, + 'indicators': self.indicators_used, + 'data_sources': list(self.data_sources_used), + 'flags_used': list(self.flags_used) + } + + return strategy_components + + def generate_code_from_json(self, json_nodes, indent_level): + """ + Recursively generates Python code from JSON nodes. + + :param json_nodes: The JSON nodes representing the strategy logic. + :param indent_level: Current indentation level for code formatting. + :return: A list of code lines. + """ + code_lines = [] + indent = ' ' * indent_level + + if isinstance(json_nodes, dict): + json_nodes = [json_nodes] + + for node in json_nodes: + node_type = node.get('type') + if not node_type: + continue # Skip nodes without a type + + if node_type == 'trade_action': + code_lines.extend(self.handle_trade_action(node, indent_level)) + elif node_type in ['set_flag', 'notify_user']: + # Handle actions that generate code lines + if node_type == 'set_flag': + code_lines.extend(self.handle_set_flag(node, indent_level)) + elif node_type == 'notify_user': + code_lines.extend(self.handle_notify_user(node, indent_level)) + elif node_type == 'conditional': + # Handle conditional statements + condition_node = node.get('condition') + actions = node.get('actions', []) + condition_code = self.generate_condition_code(condition_node) + code_lines.append(f"{indent}if {condition_code}:") + # Generate code for actions inside the condition + code_lines.extend(self.generate_code_from_json(actions, indent_level + 1)) + else: + # Handle other node types as needed + pass + + return code_lines + + def generate_condition_code(self, condition_node): + node_type = condition_node.get('type') + if not node_type: + return 'False' # Default to False if node type is missing + + if node_type == 'comparison': + operator = condition_node.get('operator') + left = condition_node.get('left') + right = condition_node.get('right') + left_expr = self.generate_condition_code(left) + right_expr = self.generate_condition_code(right) + operator_map = { + '>': '>', + '<': '<', + '>=': '>=', + '<=': '<=', + '==': '==', + '!=': '!=' + } + return f"({left_expr} {operator_map.get(operator, operator)} {right_expr})" + elif node_type == 'logical_and': + conditions = condition_node.get('conditions', []) + condition_exprs = [self.generate_condition_code(cond) for cond in conditions] + return ' and '.join(condition_exprs) + elif node_type == 'logical_or': + conditions = condition_node.get('conditions', []) + condition_exprs = [self.generate_condition_code(cond) for cond in conditions] + return ' or '.join(condition_exprs) + elif node_type == 'is_false': + condition = condition_node.get('condition') + condition_expr = self.generate_condition_code(condition) + return f"not ({condition_expr})" + elif node_type == 'flag_is_set': + flag_name = condition_node.get('flag_name') + self.flags_used.add(flag_name) + return f"self.flags.get('{flag_name}', False)" + elif node_type == 'strategy_profit_loss': + metric = condition_node.get('metric') + if metric == 'profit': + return 'self.is_in_profit()' + elif metric == 'loss': + return 'self.is_in_loss()' + elif node_type == 'active_trades': + return 'self.get_active_trades()' + elif node_type == 'current_balance': + return 'self.get_current_balance()' + elif node_type == 'starting_balance': + return 'self.get_starting_balance()' + elif node_type == 'value_input': + value = condition_node.get('value', 0) + return str(value) + elif node_type == 'arithmetic_operator': + operator = condition_node.get('operator') + operands = condition_node.get('operands', []) + if len(operands) == 2: + left_expr = self.generate_condition_code(operands[0]) + right_expr = self.generate_condition_code(operands[1]) + operator_map = { + 'ADD': '+', + 'SUBTRACT': '-', + 'MULTIPLY': '*', + 'DIVIDE': '/' + } + return f"({left_expr} {operator_map.get(operator, operator)} {right_expr})" + elif node_type == 'last_candle_value': + candle_part = condition_node.get('candle_part') + source = condition_node.get('source', {}) + data_feed = self.get_data_feed(source) + return f"{data_feed}.{candle_part}[0]" + elif node_type == 'indicator': + indicator_name = condition_node.get('name') + output_field = condition_node.get('output') + # Collect the indicator information + self.indicators_used.append({ + 'name': indicator_name, + 'output': output_field + }) + # Generate code that calls process_indicator + return f"self.process_indicator('{indicator_name}', '{output_field}')" + elif node_type == 'number': + # Handle numeric values + return str(condition_node.get('value', 0)) + elif node_type == 'string': + # Handle string values + return f"'{condition_node.get('value', '')}'" + # Handle other node types as needed else: - return return_obj + return 'False' # Default to False for unhandled types + + def handle_trade_action(self, node, indent_level): + code_lines = [] + indent = ' ' * indent_level + + action = node.get('trade_type') + condition_node = node.get('condition') + size = node.get('size', 1) + stop_loss = node.get('stop_loss') + take_profit = node.get('take_profit') + trade_options = node.get('trade_options', []) + + # Generate code for the condition + if condition_node: + condition_code = self.generate_condition_code(condition_node) + code_lines.append(f"{indent}if {condition_code}:") + action_indent = indent + ' ' + else: + action_indent = indent + + # Prepare order parameters + order_params = [f"size={size}"] + if stop_loss is not None: + order_params.append(f"stop_loss={stop_loss}") + if take_profit is not None: + order_params.append(f"take_profit={take_profit}") + # Handle trade options + for option in trade_options: + if option.get('type') == 'order_type': + order_type = option.get('order_type', 'market') + order_params.append(f"order_type='{order_type}'") + if order_type == 'limit': + limit_price = option.get('limit_price') + if limit_price is not None: + order_params.append(f"price={limit_price}") + elif option.get('type') == 'time_in_force': + tif = option.get('tif') + if tif: + order_params.append(f"tif='{tif}'") + elif option.get('type') == 'target_market': + tf = option.get('timeframe') + exc = option.get('exchange') + sym = option.get('symbol') + order_params.append(f"timeframe='{tf}'") + order_params.append(f"exchange='{exc}'") + order_params.append(f"symbol='{sym}'") + self.data_sources_used.add((exc, sym, tf)) + # Handle other trade options + + params_str = ', '.join(order_params) + code_lines.append(f"{action_indent}self.{action}({params_str})") + + return code_lines + + def handle_set_flag(self, node, indent_level): + code_lines = [] + indent = ' ' * indent_level + + condition_node = node.get('condition') + flag_name = node.get('flag_name') + flag_value = node.get('flag_value', 'True') + + condition_code = self.generate_condition_code(condition_node) + code_lines.append(f"{indent}if {condition_code}:") + code_lines.append(f"{indent} self.flags['{flag_name}'] = {flag_value}") + + return code_lines + + def handle_notify_user(self, node, indent_level): + code_lines = [] + indent = ' ' * indent_level + + message = node.get('message', 'No message provided.') + code_lines.append(f"{indent}self.notify_user('{message}')") + + return code_lines + + def get_data_feed(self, source): + timeframe = source.get('timeframe', 'default') + exchange = source.get('exchange', 'default') + symbol = source.get('symbol', 'default') + source_key = f"{exchange}_{symbol}_{timeframe}" + self.data_sources_used.add((exchange, symbol, timeframe)) + return f"self.datas['{source_key}']" + + def execute_strategy(self, strategy_data): + """ + Executes the given strategy in live trading. + + :param strategy_data: The data for the strategy to execute. + """ + strategy_id = strategy_data.get('tbl_key') + strategy_name = strategy_data.get('name') + user_id = strategy_data['creator'] + + # Get the strategy components + strategy_components = strategy_data.get('strategy_components') + if isinstance(strategy_components, str): + strategy_components = json.loads(strategy_components) + generated_code = strategy_components.get('generated_code') + + # Prepare the execution context + if strategy_id not in self.strategy_contexts: + # Initialize the context for this strategy + context = { + 'flags': {}, + 'starting_balance': self.trades.get_current_balance(user_id), + 'indicators_used': strategy_components.get('indicators', []), + 'strategy_data': strategy_data + } + self.strategy_contexts[strategy_id] = context + else: + context = self.strategy_contexts[strategy_id] + + # Define the local functions and variables needed by the generated code + def process_indicator(indicator_name, output_field): + # Get the latest indicator value using indicators_manager + indicator_def = next((ind for ind in context['indicators_used'] if ind['name'] == indicator_name), None) + if indicator_def is None: + return None + # Assuming indicators_manager.process_indicator returns a DataFrame with the latest values + indicator_df = self.indicators_manager.process_indicator(indicator_def) + if indicator_df is not None and not indicator_df.empty: + return indicator_df.iloc[-1][output_field] + else: + return None + + def buy(size=1, price=None, order_type='market', symbol=None, **kwargs): + # Format the data for CCXT + order_data = { + 'symbol': symbol or strategy_data.get('symbol', 'BTC/USDT'), + 'type': order_type, + 'side': 'buy', + 'amount': size, + 'price': price, + # Include other parameters as needed + } + # Call self.trades.buy with the order data + self.trades.buy(order_data) + + def sell(size=1, price=None, order_type='market', symbol=None, **kwargs): + # Format the data for CCXT + order_data = { + 'symbol': symbol or strategy_data.get('symbol', 'BTC/USDT'), + 'type': order_type, + 'side': 'sell', + 'amount': size, + 'price': price, + # Include other parameters as needed + } + # Call self.trades.sell with the order data + self.trades.sell(order_data) + + # Implement other helper methods as needed + def is_in_profit(): + # Implement logic to determine if the strategy is in profit + return False # Placeholder + + def is_in_loss(): + # Implement logic to determine if the strategy is in loss + return False # Placeholder + + def get_active_trades(): + # Return the number of active trades + return len(self.trades.get_active_trades()) + + def get_current_balance(): + # Return the current balance from self.trades + return self.trades.get_balance() + + def notify_user(message): + # Implement notification logic + print(f"Notification: {message}") + + # Prepare the local namespace for exec + local_vars = { + 'process_indicator': process_indicator, + 'buy': buy, + 'sell': sell, + 'is_in_profit': is_in_profit, + 'is_in_loss': is_in_loss, + 'get_active_trades': get_active_trades, + 'get_current_balance': get_current_balance, + 'notify_user': notify_user, + 'flags': context['flags'], + 'starting_balance': context['starting_balance'], + # Include any other variables or functions needed + } + + # Execute the generated code + try: + exec(generated_code, {}, local_vars) + except Exception as e: + print(f"Error executing strategy {strategy_name}: {e}") + + def update(self): + """ + Loops through and executes all activated strategies. + """ + active_strategies = self.data.get_rows_from_datacache('strategies', [('active', True)]) + if active_strategies.empty: + return # No active strategies to execute + for _, strategy_data in active_strategies.iterrows(): + self.execute_strategy(strategy_data) diff --git a/src/app.py b/src/app.py index eb9527a..f5e0637 100644 --- a/src/app.py +++ b/src/app.py @@ -1,25 +1,34 @@ -import json -import logging +# app.py -from flask import Flask, render_template, request, redirect, jsonify, session, flash -from flask_cors import CORS -from flask_sock import Sock -from email_validator import validate_email, EmailNotValidError +# Monkey patching must occur before other imports +import eventlet +eventlet.monkey_patch() # noqa: E402 -# Handles all updates and requests for locally stored data. -from BrighterTrades import BrighterTrades +# Standard library imports +import logging # noqa: E402 +# import json # noqa: E402 +# import datetime as dt # noqa: E402 + +# Third-party imports +from flask import Flask, render_template, request, redirect, jsonify, session, flash # noqa: E402 +from flask_cors import CORS # noqa: E402 +from flask_socketio import SocketIO, emit, join_room, leave_room, disconnect # noqa: E402 +from email_validator import validate_email, EmailNotValidError # noqa: E402 + +# Local application imports +from BrighterTrades import BrighterTrades # noqa: E402 # Set up logging logging.basicConfig(level=logging.DEBUG) -# Create a BrighterTrades object. This the main application that maintains access to the server, local storage, -# and manages objects that process trade data. -brighter_trades = BrighterTrades() - # Create a Flask object named app that serves the html. app = Flask(__name__) # Create a socket in order to receive requests. -sock = Sock(app) +socketio = SocketIO(app, async_mode='eventlet') + +# Create a BrighterTrades object. This the main application that maintains access to the server, local storage, +# and manages objects that process trade data. +brighter_trades = BrighterTrades(socketio) # Set server configuration globals. CORS_HEADERS = 'Content-Type' @@ -102,49 +111,49 @@ def index(): open_orders=rendered_data['open_orders']) -@sock.route('/ws') -def ws(socket_conn): +@socketio.on('connect') +def handle_connect(): + user_name = request.args.get('user_name') + if user_name and brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'): + # Join a room specific to the user for targeted messaging + room = user_name # You can choose an appropriate room naming strategy + join_room(room) + emit('message', {'reply': 'connected', 'data': 'Connection established'}) + else: + emit('message', {'reply': 'error', 'data': 'User not authenticated'}) + # Disconnect the client if not authenticated + disconnect() + + +@socketio.on('message') +def handle_message(data): """ - Open a WebSocket to handle two-way communication with UI without browser refreshes. + Handle incoming JSON messages with authentication. """ + # Validate input + if 'message_type' not in data or 'data' not in data: + emit('message', {"success": False, "message": "Invalid message format."}) + return - def json_msg_received(msg_obj): - """ - Handle incoming JSON messages with authentication. - """ - # Validate input - if 'message_type' not in msg_obj or 'data' not in msg_obj: - return + msg_type, msg_data = data['message_type'], data['data'] - msg_type, msg_data = msg_obj['message_type'], msg_obj['data'] + # Extract user_name from the incoming message data + user_name = msg_data.get('user_name') + if not user_name: + emit('message', {"success": False, "message": "User not specified"}) + return - # Extract user_name from the incoming message data - user_name = msg_data.get('user_name') - if not user_name: - socket_conn.send(json.dumps({"success": False, "message": "User not specified"})) - return + # Check if the user is logged in + if not brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'): + emit('message', {"success": False, "message": "User not logged in"}) + return - # Check if the user is logged in - if not brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'): - 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, socket_conn_id=request.sid) - # 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: - socket_conn.send(json.dumps(resp)) - - # Main loop to receive messages and handle them - while True: - msg = socket_conn.receive() - if msg: - try: - json_msg = json.loads(msg) - json_msg_received(json_msg) - except json.JSONDecodeError: - print(f'Msg received from client (not JSON): {msg}') + # Send the response back to the client + if resp: + emit('message', resp) @app.route('/settings', methods=['POST']) @@ -338,9 +347,11 @@ def indicator_init(): # Get the indicator data source = {'user_name': username, 'market': chart_view} data = brighter_trades.get_indicator_data(user_name=username, source=source, start_ts=None, num_results=1000) - # indicators={'EMA 5': {'visible': true, 'type': 'EMA', 'color': 'red' },'vol': {'visible': true, 'type': 'Volume'},'New Indicator': {'visible': true, 'type': 'nothing'}} + # indicators={'EMA 5': {'visible': true, 'type': 'EMA', 'color': 'red' }, + # 'vol': {'visible': true, 'type': 'Volume'},'New Indicator': {'visible': true, 'type': 'nothing'}} return jsonify(data), 200 if __name__ == '__main__': - app.run(debug=False, use_reloader=False) + socketio.run(app, host='127.0.0.1', port=5000, debug=False, use_reloader=False) + diff --git a/src/backtesting.py b/src/backtesting.py index ae53ebe..b866190 100644 --- a/src/backtesting.py +++ b/src/backtesting.py @@ -1,30 +1,24 @@ -import ast -import json -import re - import backtrader as bt import datetime as dt from DataCache_v3 import DataCache from Strategies import Strategies -import threading +from indicators import Indicators import numpy as np +import pandas as pd class Backtester: - def __init__(self, data_cache: DataCache, strategies: Strategies): + def __init__(self, data_cache: DataCache, strategies: Strategies, indicators: Indicators, socketio): """ Initialize the Backtesting class with a cache for back-tests """ self.data_cache = data_cache self.strategies = strategies + self.indicators_manager = indicators + self.socketio = socketio # 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 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 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') @@ -39,164 +33,162 @@ class Backtester: cache_key = f"backtest:{user_name}:{backtest_name}" self.data_cache.insert_row_into_cache('tests', columns, values, key=cache_key) - def map_user_strategy(self, user_strategy): + def map_user_strategy(self, user_strategy, precomputed_indicators): """Maps user strategy details into a Backtrader-compatible strategy class.""" + # Extract the generated code and indicators from the strategy components + strategy_components = user_strategy['strategy_components'] + generated_code = strategy_components['generated_code'] + indicators_used = strategy_components['indicators'] + + # Define the strategy class dynamically class MappedStrategy(bt.Strategy): - params = ( - ('initial_cash', user_strategy['params'].get('initial_cash', 10000)), - ('commission', user_strategy['params'].get('commission', 0.001)), - ) - def __init__(self): - # Extract unique sources (exchange, symbol, timeframe) from blocks - self.sources = self.extract_sources(user_strategy) + self.precomputed_indicators = precomputed_indicators + self.indicator_pointers = {} + self.indicator_names = list(precomputed_indicators.keys()) + self.current_step = 0 - # Map of source to data feed (used later in next()) - self.source_data_feed_map = {} + # Initialize pointers for each indicator + for name in self.indicator_names: + self.indicator_pointers[name] = 0 # Start at the first row - 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 + # Initialize any other needed variables + self.flags = {} + self.starting_balance = self.broker.getvalue() - 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 process_indicator(self, indicator_name, output_field): + # Get the DataFrame for the indicator + df = self.precomputed_indicators[indicator_name] - 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') - } + # Get the current index for the indicator + idx = self.indicator_pointers[indicator_name] + + if idx >= len(df): + return None # No more data + + # Get the specific output value + if output_field in df.columns: + value = df.iloc[idx][output_field] + if pd.isna(value): + return None # Handle NaN values + return value + else: + return None # Output field not found def next(self): - """Execute trading logic using the compiled strategy.""" + # Increment pointers + for name in self.indicator_names: + self.indicator_pointers[name] += 1 + + # Increment current step + self.current_step += 1 + + # Generated strategy logic try: - exec(self.compiled_logic, {'self': self, 'data_feeds': self.source_data_feed_map}) + # Execute the generated code + exec(generated_code) except Exception as e: - print(f"Error executing trading logic: {e}") + print(f"Error in strategy execution: {e}") return MappedStrategy - def prepare_data_feed(self, start_date: str, sources: list, user_name: str): + def prepare_data_feed(self, start_date: str, source: dict): """ - Prepare multiple data feeds based on the start date and list of sources. + Prepare the main data feed based on the start date and source. """ 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 = {} + # Ensure exchange details contain required keys (fallback if missing) + timeframe = source.get('timeframe', '1h') + exchange = source.get('exchange', 'Binance') + symbol = source.get('symbol', 'BTCUSDT') - 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 + data = self.data_cache.get_records_since(start_datetime=start_dt, ex_details=[symbol, timeframe, exchange]) - # 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 + return data 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): + def precompute_indicators(self, indicators_definitions, data_feed): """ - 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. + Precompute indicator values and return a dictionary of DataFrames. + """ + precomputed_indicators = {} + total_candles = len(data_feed) + + for indicator_def in indicators_definitions: + indicator_name = indicator_def['name'] + # Compute the indicator values + indicator_df = self.indicators_manager.process_indicator(indicator=indicator_def, + num_results=total_candles) + # Ensure the DataFrame has a consistent index + indicator_df.reset_index(drop=True, inplace=True) + precomputed_indicators[indicator_name] = indicator_df + + return precomputed_indicators + + def run_backtest(self, strategy_class, data_feed, msg_data, user_name, callback, socket_conn_id): + """ + Runs a backtest using Backtrader and uses Flask-SocketIO's background tasks. + Sends progress updates to the client via WebSocket. """ def execute_backtest(): - cerebro = bt.Cerebro() + try: + cerebro = bt.Cerebro() - # Add the mapped strategy to the backtest - cerebro.addstrategy(strategy) + # Add the mapped strategy to the backtest + cerebro.addstrategy(strategy_class) - # 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(): + # Add the main data feed to Cerebro + # noinspection PyArgumentList 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() + # Set initial capital and commission + initial_cash = msg_data.get('capital', 10000) + cerebro.broker.setcash(initial_cash) + commission = msg_data.get('commission', 0.001) + cerebro.broker.setcommission(commission=commission) - # Progress tracking variables - current_bar = 0 - last_progress = 0 + # Run the backtest + print("Running backtest...") + start_time = dt.datetime.now() + cerebro.run() + end_time = dt.datetime.now() - # 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 + # Extract performance metrics + final_value = cerebro.broker.getvalue() + run_duration = (end_time - start_time).total_seconds() - # Send progress update every 10% increment - if progress >= last_progress + 10: - last_progress += 10 - socket_conn.send(json.dumps({"progress": int(last_progress)})) + # Send 100% completion + self.socketio.emit('progress_update', {"progress": 100}, room=socket_conn_id) - # Attach the custom next method to the strategy - strategy.next = track_progress + # Prepare the results to pass into the callback + backtest_results = { + "initial_capital": initial_cash, + "final_portfolio_value": final_value, + "run_duration": run_duration + } - # Run the backtest - print("Running backtest...") - start_time = dt.datetime.now() - cerebro.run() - end_time = dt.datetime.now() + callback(backtest_results) - # Extract performance metrics - final_value = cerebro.broker.getvalue() - run_duration = (end_time - start_time).total_seconds() + except Exception as e: + # Handle exceptions and send error messages to the client + error_message = f"Backtest execution failed: {str(e)}" + self.socketio.emit('backtest_error', {"message": error_message}, room=socket_conn_id) + print(f"[BACKTEST ERROR] {error_message}") - # Send 100% completion - socket_conn.send(json.dumps({"progress": 100})) + # Start the backtest as a background task + self.socketio.start_background_task(execute_backtest) - # 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): + def handle_backtest_message(self, user_id, msg_data, socket_conn_id): user_name = msg_data.get('user_name') backtest_name = f"{msg_data['strategy']}_backtest" @@ -210,55 +202,43 @@ class Backtester: 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')) + # Extract the main data source from the strategy components + strategy_components = user_strategy['strategy_components'] + data_sources = strategy_components['data_sources'] - if not sources: - return {"error": "No valid sources found in the strategy."} + if not data_sources: + return {"error": "No valid data 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) + # For simplicity, use the first data source as the main data feed + main_source = data_sources[0] - if data_feed_map is None: + # Prepare the main data feed + data_feed = self.prepare_data_feed(msg_data['start_date'], main_source) + + if data_feed is None: return {"error": "Data feed could not be prepared. Please check the data source."} + # Precompute indicator values + indicators_definitions = strategy_components['indicators'] + precomputed_indicators = self.precompute_indicators(indicators_definitions, data_feed) + # Map the user strategy to a Backtrader strategy class - mapped_strategy = self.map_user_strategy(user_strategy) + mapped_strategy_class = self.map_user_strategy(user_strategy, precomputed_indicators) # 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) + # Emit the results back to the client + self.socketio.emit('backtest_results', {"test_id": backtest_name, "results": results}, room=socket_conn_id) + print(f"[BACKTEST COMPLETE] Results emitted to user '{user_name}'.") + + # Run the backtest asynchronously + self.run_backtest(mapped_strategy_class, data_feed, msg_data, user_name, backtest_callback, socket_conn_id) 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) diff --git a/src/maintenence/debuging_testing.py b/src/maintenence/debuging_testing.py index 42d956a..bd865c3 100644 --- a/src/maintenence/debuging_testing.py +++ b/src/maintenence/debuging_testing.py @@ -1,4 +1,36 @@ -""" -set_cache_item - create_cache -""" \ No newline at end of file +# test_backtrader_pandasdata.py +import backtrader as bt +import pandas as pd + +# Sample DataFrame +data_feed = pd.DataFrame({ + 'datetime': pd.date_range(start='2021-01-01', periods=5, freq='D'), + 'open': [100, 101, 102, 103, 104], + 'high': [105, 106, 107, 108, 109], + 'low': [95, 96, 97, 98, 99], + 'close': [102, 103, 104, 105, 106], + 'volume': [1000, 1010, 1020, 1030, 1040] +}) + +# Convert 'datetime' to datetime objects and set as index +data_feed['datetime'] = pd.to_datetime(data_feed['datetime']) +data_feed.set_index('datetime', inplace=True) + + +# Define a simple strategy +class TestStrategy(bt.Strategy): + def next(self): + pass + + +cerebro = bt.Cerebro() +cerebro.addstrategy(TestStrategy) + +# Add data feed using Backtrader's PandasData +# noinspection PyArgumentList +bt_feed = bt.feeds.PandasData(dataname=data_feed) +cerebro.adddata(bt_feed) + +# Run backtest +cerebro.run() +print("Backtest completed successfully.") diff --git a/src/static/Strategies.js b/src/static/Strategies.js index b9e020b..f82ea8e 100644 --- a/src/static/Strategies.js +++ b/src/static/Strategies.js @@ -28,7 +28,7 @@ class StratUIManager { * @param {string} action - The action to perform ('new' or 'edit'). * @param {string|null} strategyData - The data of the strategy to edit (only applicable for 'edit' action). */ - displayForm(action, strategyData = null) { + async displayForm(action, strategyData = null) { console.log(`Opening form for action: ${action}, strategy: ${strategyData?.name}`); if (this.formElement) { const headerTitle = this.formElement.querySelector("#draggable_header h1"); @@ -63,11 +63,14 @@ class StratUIManager { // Display the form this.formElement.style.display = "grid"; - // Call the workspace manager to initialize the Blockly workspace after the form becomes visible + // Initialize Blockly workspace after the form becomes visible if (UI.strats && UI.strats.workspaceManager) { - setTimeout(() => { - UI.strats.workspaceManager.initWorkspace(); - }, 100); // Delay slightly to allow the form to render properly + try { + await UI.strats.workspaceManager.initWorkspace(); + console.log("Blockly workspace initialized."); + } catch (error) { + console.error("Failed to initialize Blockly workspace:", error); + } } else { console.error("Workspace manager is not initialized or is unavailable."); } @@ -76,7 +79,6 @@ class StratUIManager { } } - /** * Hides the "Create New Strategy" form by adding a 'hidden' class. */ @@ -264,7 +266,7 @@ class StratWorkspaceManager { * @async * @throws {Error} If required elements ('blocklyDiv' or 'toolbox') are not found. */ - initWorkspace() { + async initWorkspace() { if (!document.getElementById('blocklyDiv')) { console.error("blocklyDiv is not loaded."); return; @@ -275,25 +277,24 @@ class StratWorkspaceManager { } // Initialize custom blocks and Blockly workspace - this._loadModulesAndInitWorkspace(); + await this._loadModulesAndInitWorkspace(); } + async _loadModulesAndInitWorkspace() { if (!this.blocksDefined) { 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') - ]); - - // Define custom blocks - customBlocksModule.defineCustomBlocks(); - indicatorBlocksModule.defineIndicatorBlocks(); - pythonGeneratorsModule.definePythonGenerators(); + // Load and define JSON generators first + const jsonGeneratorsModule = await import('./json_generators.js'); jsonGeneratorsModule.defineJsonGenerators(); + + // Load and define custom blocks + const customBlocksModule = await import('./custom_blocks.js'); + customBlocksModule.defineCustomBlocks(); + + // Load and define indicator blocks + const indicatorBlocksModule = await import('./indicator_blocks.js'); + indicatorBlocksModule.defineIndicatorBlocks(); } catch (error) { console.error("Error loading Blockly modules: ", error); return; @@ -326,6 +327,7 @@ class StratWorkspaceManager { scaleSpeed: 1.2 } }); + console.log('Blockly workspace initialized and modules loaded.'); } /** @@ -358,18 +360,17 @@ class StratWorkspaceManager { 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(); + + // Generate workspace XML for restoration when editing const workspaceXml = Blockly.Xml.workspaceToDom(this.workspace); const workspaceXmlText = Blockly.Xml.domToText(workspaceXml); return JSON.stringify({ name: strategyName, - code: pythonCode, strategy_json: strategyJson, workspace: workspaceXmlText }); @@ -399,6 +400,7 @@ class StratWorkspaceManager { statements: {} }; + // Capture all fields in the block block.inputList.forEach(input => { if (input.fieldRow) { input.fieldRow.forEach(field => { @@ -407,19 +409,32 @@ class StratWorkspaceManager { } }); } + }); + // Capture all connected blocks + block.inputList.forEach(input => { 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 multiple statement connections if applicable + const connectedBlocks = []; + let currentBlock = targetBlock; + while (currentBlock) { + connectedBlocks.push(this._blockToJson(currentBlock)); + currentBlock = currentBlock.getNextBlock(); + } + json.statements[input.name] = connectedBlocks; } } }); + // Handle the next connected block at the same level if (block.getNextBlock()) { - json.next = this._blockToJson(block.getNextBlock()); + const nextBlock = this._blockToJson(block.getNextBlock()); + // Assuming only one 'next' block; adjust if multiple are possible + json.next = nextBlock; } return json; @@ -468,6 +483,9 @@ class Strategies { // Set the delete callback this.uiManager.registerDeleteStrategyCallback(this.deleteStrategy.bind(this)); + + // Bind the submitStrategy method to ensure correct 'this' context + this.submitStrategy = this.submitStrategy.bind(this); } /** @@ -639,42 +657,44 @@ class Strategies { return; } - let strategyObject; + let strategyData; try { - strategyObject = JSON.parse(this.generateStrategyJson()); + // Compile the strategy JSON (conditions and actions) + const compiledStrategy = this.generateStrategyJson(); // Returns JSON string + const parsedStrategy = JSON.parse(compiledStrategy); // Object with 'name', 'strategy_json', 'workspace' + + // Prepare the strategy data to send + strategyData = { + code: parsedStrategy.strategy_json, // The compiled strategy JSON string + workspace: parsedStrategy.workspace, // Serialized workspace XML + name: nameBox.value.trim(), + fee: parseFloat(feeBox.value.trim()), + public: publicCheckbox.checked ? 1 : 0, + user_name: this.data.user_name + // Add 'stats' if necessary + }; + } catch (error) { - console.error('Failed to parse strategy JSON:', error); + console.error('Failed to compile 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) { + // Basic client-side validation + if (isNaN(strategyData.fee) || strategyData.fee < 0) { alert("Please enter a valid, non-negative number for the fee."); return; } - const strategyName = nameBox.value.trim(); - if (!strategyName) { + if (!strategyData.name) { alert("Please provide a name for the strategy."); return; } - const is_public = publicCheckbox.checked ? 1 : 0; - - // Add user_name, fee, and public fields to the strategy object - const strategyData = { - ...strategyObject, - user_name: this.data.user_name, - fee, - public: is_public - }; - - // Determine if this is a new strategy or an edit - const messageType = action === 'new' ? 'new_strategy' : 'edit_strategy'; - + // Send the strategy data to the server if (this.comms) { + // Determine message type based on action + const messageType = action === 'new' ? 'new_strategy' : 'edit_strategy'; this.comms.sendToApp(messageType, strategyData); this.uiManager.hideForm(); } else { diff --git a/src/static/backtesting.js b/src/static/backtesting.js index 254e23b..f9ea634 100644 --- a/src/static/backtesting.js +++ b/src/static/backtesting.js @@ -108,7 +108,7 @@ class Backtesting { populateStrategyDropdown() { const strategyDropdown = document.getElementById('strategy_select'); strategyDropdown.innerHTML = ''; - const strategies = this.ui.strats.getAvailableStrategies(); + const strategies = this.ui.strats.dataManager.getAllStrategies(); console.log("Available strategies:", strategies); strategies.forEach(strategy => { diff --git a/src/static/communication.js b/src/static/communication.js index c361027..0b9d532 100644 --- a/src/static/communication.js +++ b/src/static/communication.js @@ -1,13 +1,88 @@ class Comms { - constructor() { + constructor(userName) { + if (!userName) { + console.error('Comms: Cannot initialize Socket.IO without user_name.'); + return; + } + this.connectionOpen = false; - this.appCon = null; // WebSocket connection for app communication + this.socket = null; // Socket.IO client instance this.eventHandlers = {}; // Event handlers for message types // Callback collections that will receive various updates. this.candleUpdateCallbacks = []; this.candleCloseCallbacks = []; this.indicatorUpdateCallbacks = []; + + // Initialize the message queue + this.messageQueue = []; + + // Save the userName + this.userName = userName; + + // Initialize the socket + this._initializeSocket(); + } + + /** + * Initialize the Socket.IO connection. + */ + _initializeSocket() { + // Initialize Socket.IO client with query parameter + this.socket = io('http://127.0.0.1:5000', { + query: { 'user_name': this.userName }, + transports: ['websocket'], // Optional: Force WebSocket transport + autoConnect: true, + reconnectionAttempts: 5, // Optional: Number of reconnection attempts + reconnectionDelay: 1000 // Optional: Delay between reconnections + }); + + // Handle connection events + this.socket.on('connect', () => { + console.log('Socket.IO: Connected to server'); + this.connectionOpen = true; + + // Flush the message queue + this._flushMessageQueue(); + }); + + this.socket.on('disconnect', (reason) => { + console.log(`Socket.IO: Disconnected from server. Reason: ${reason}`); + this.connectionOpen = false; + }); + + this.socket.on('connect_error', (error) => { + console.error('Socket.IO: Connection error:', error); + }); + + // Handle incoming messages + this.socket.on('message', (data) => { + if (data.reply === 'connected') { + console.log('Socket.IO: Connection established:', data.data); + } else if (data.reply === 'error') { + console.error('Socket.IO: Authentication error:', data.data); + // Optionally, handle authentication errors (e.g., redirect to login) + } else { + // Emit the event to registered handlers + this.emit(data.reply, data.data); + } + }); + } + /** + * Flushes the message queue by sending all queued messages. + */ + _flushMessageQueue() { + while (this.messageQueue.length > 0) { + const { messageType, data } = this.messageQueue.shift(); + this.socket.emit('message', { + message_type: messageType, + data: { + ...data, + user_name: this.userName + } + }); + console.log(`Comms: Sent queued message-> ${JSON.stringify({ messageType, data })}`); + } } /** @@ -114,7 +189,7 @@ class Comms { */ async getIndicatorData(userName) { try { - const response = await fetch('http://localhost:5000/api/indicator_init', { + const response = await fetch('http://127.0.0.1:5000/api/indicator_init', { // Changed to use same host credentials: 'same-origin', mode: 'cors', method: 'POST', @@ -172,8 +247,8 @@ class Comms { } /** - * Sends a request to update an indicator's properties. - * @param {Object} indicatorData - An object containing the updated properties of the indicator. + * Sends a request to create a new indicator. + * @param {Object} indicatorData - An object containing the properties of the new indicator. * @returns {Promise} - The response from the server. */ async submitIndicator(indicatorData) { @@ -188,87 +263,40 @@ class Comms { }); return await response.json(); } catch (error) { - console.error('Error updating indicator:', error); + console.error('Error creating indicator:', error); return { success: false }; } } /** - * Sends a message to the application server via WebSocket. - * Automatically includes the user_name from `window.UI.data.user_name` for authentication. + * Sends a message to the application server via Socket.IO. * @param {string} messageType - The type of the message. * @param {Object} data - The data to be sent with the message. */ sendToApp(messageType, data) { - const user_name = window.UI.data.user_name; // Access user_name from window.UI.data - - if (!user_name) { - console.error('User not logged in. Cannot send message.'); - return; - } - const messageData = { message_type: messageType, data: { - ...data, // Include the existing data - user_name: user_name // Add user_name for authentication + ...data, + user_name: this.userName } }; console.log('Comms: Sending->', JSON.stringify(messageData)); - if (this.connectionOpen) { - this.appCon.send(JSON.stringify(messageData)); + if (this.connectionOpen && this.socket) { + this.socket.emit('message', messageData); } else { - setTimeout(() => { - if (this.appCon) { - this.appCon.send(JSON.stringify(messageData)); - } - }, 1000); + // Not an error; message will be queued + console.warn('Socket.IO connection is not open. Queuing message.'); + // Queue the message to be sent once connected + this.messageQueue.push({ messageType, data }); + console.warn(`Comms: Queued message-> ${JSON.stringify({ messageType, data })} (Connection not open)`); } } - setAppCon() { - this.appCon = new WebSocket('ws://localhost:5000/ws'); - - // On connection open - this.appCon.onopen = () => { - console.log("WebSocket connection established"); - this.appCon.send("Connection OK"); - this.connectionOpen = true; - }; - - // Handle incoming messages - this.appCon.addEventListener('message', (event) => { - if (event.data) { - const message = JSON.parse(event.data); - - if (message && message.request !== undefined) { - console.log('Received a request from the server'); - console.log(message.request); - } - - if (message && message.reply !== undefined) { - // Emit the event to registered handlers - this.emit(message.reply, message.data); - } - } - }); - - // On connection close - this.appCon.onclose = () => { - console.log("WebSocket connection closed"); - this.connectionOpen = false; - }; - - // On WebSocket error - this.appCon.onerror = (error) => { - console.error("WebSocket error:", error); - }; - } - /** - * Sets up a WebSocket connection to the exchange for receiving candlestick data. + * Set up a separate WebSocket connection to the Binance exchange for receiving candlestick data. * @param {string} interval - The interval of the candlestick data. * @param {string} tradingPair - The trading pair to subscribe to. */ @@ -294,5 +322,17 @@ class Comms { this.candleClose(newCandle); } }; + + this.exchangeCon.onopen = () => { + console.log(`Connected to Binance stream for ${tradingPair} at interval ${interval}`); + }; + + this.exchangeCon.onclose = () => { + console.log(`Disconnected from Binance stream for ${tradingPair} at interval ${interval}`); + }; + + this.exchangeCon.onerror = (error) => { + console.error(`WebSocket error on Binance stream for ${tradingPair}:`, error); + }; } -} \ No newline at end of file +} diff --git a/src/static/custom_blocks.js b/src/static/custom_blocks.js index 0af0945..187a1fb 100644 --- a/src/static/custom_blocks.js +++ b/src/static/custom_blocks.js @@ -1,3 +1,4 @@ + // custom_blocks.js // Define custom Blockly blocks and Python code generation export function defineCustomBlocks() { // Custom block for retrieving last candle values @@ -234,6 +235,10 @@ "tooltip": "Select time in force for the order", "helpUrl": "" }]); + // Before defining the block, fetch the required options + const timeframeOptions = bt_data.intervals.map(interval => [interval, interval]); + const exchangeOptions = window.UI.exchanges.connected_exchanges.map(exchange => [exchange, exchange]); + const symbolOptions = bt_data.symbols.map(symbol => [symbol, symbol]); // Dynamically populate the block options using the available data Blockly.defineBlocksWithJsonArray([{ @@ -243,26 +248,17 @@ { "type": "field_dropdown", "name": "TF", - "options": function() { - // Dynamically fetch available timeframes from bt_data.intervals - return bt_data.intervals.map(interval => [interval, interval]); - } + "options": timeframeOptions }, { "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]); - } + "options": exchangeOptions }, { "type": "field_dropdown", "name": "SYM", - "options": function() { - // Dynamically fetch available symbols from bt_data.symbols - return bt_data.symbols.map(symbol => [symbol, symbol]); - } + "options": symbolOptions } ], "output": "source", // This output allows it to be connected to other blocks expecting a 'source' @@ -271,7 +267,10 @@ "helpUrl": "" }]); - + // Similarly, define 'target_market' block + const targetMarketTFOptions = bt_data.intervals.map(interval => [interval, interval]); + const targetMarketEXCOptions = window.UI.exchanges.connected_exchanges.map(exchange => [exchange, exchange]); + const targetMarketSYMOptions = bt_data.symbols.map(symbol => [symbol, symbol]); Blockly.defineBlocksWithJsonArray([{ "type": "target_market", "message0": "Target market: TF %1 Ex %2 Sym %3", @@ -279,23 +278,17 @@ { "type": "field_dropdown", "name": "TF", - "options": function() { - return bt_data.intervals.map(interval => [interval, interval]); - } + "options": targetMarketTFOptions }, { "type": "field_dropdown", "name": "EXC", - "options": function() { - return window.UI.exchanges.connected_exchanges.map(exchange => [exchange, exchange]); - } + "options": targetMarketEXCOptions }, { "type": "field_dropdown", "name": "SYM", - "options": function() { - return bt_data.symbols.map(symbol => [symbol, symbol]); - } + "options": targetMarketSYMOptions } ], "previousStatement": "trade_option", // Allow it to be used as a trade option @@ -310,16 +303,16 @@ "args0": [ { "type": "field_dropdown", - "name": "DIRECTION", + "name": "METRIC", "options": [ - ["up", "up"], - ["down", "down"] + ["in profit", "profit"], + ["in loss", "loss"] ] } ], - "output": "Boolean", + "output": "StrategyMetric", // Custom output type "colour": 230, - "tooltip": "Check if the strategy is up or down.", + "tooltip": "Choose to evaluate the strategy's profit or loss.", "helpUrl": "" }]); Blockly.defineBlocksWithJsonArray([{ @@ -421,7 +414,43 @@ "tooltip": "Set a flag to True or False if the condition is met.", "helpUrl": "" }]); + // Entry Point Block + Blockly.defineBlocksWithJsonArray([{ + "type": "entry_point", + "message0": "Entry Point", + "output": null, + "colour": 120, + "tooltip": "Marks the entry point of the strategy.", + "helpUrl": "" + }]); + // Exit Point Block + Blockly.defineBlocksWithJsonArray([{ + "type": "exit_point", + "message0": "Exit Point", + "output": null, + "colour": 120, + "tooltip": "Marks the exit point of the strategy.", + "helpUrl": "" + }]); - console.log('Custom blocks defined'); + // Notify User Block + Blockly.defineBlocksWithJsonArray([{ + "type": "notify_user", + "message0": "Notify User with Message %1", + "args0": [ + { + "type": "field_input", + "name": "MESSAGE", + "text": "Your message here" + } + ], + "previousStatement": null, + "nextStatement": null, + "colour": 120, + "tooltip": "Sends a notification message to the user.", + "helpUrl": "" + }]); + + console.log('Custom blocks defined'); } diff --git a/src/static/data.js b/src/static/data.js index f5aa1aa..5c19235 100644 --- a/src/static/data.js +++ b/src/static/data.js @@ -16,17 +16,15 @@ class Data { // All the indicators available. this.indicators = bt_data.indicators; - /* Comms handles communication with the servers. Register - callbacks to handle various incoming messages.*/ - this.comms = new Comms(); - // Initialize other properties this.price_history = null; this.indicator_data = null; this.last_price = null; this.i_updates = null; - } + /* Initialize Comms with the user_name */ + this.comms = new Comms(this.user_name); + } /** * Initializes the Data instance by setting up connections and fetching data. * Should be called after creating a new instance of Data. @@ -37,9 +35,6 @@ class Data { 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); diff --git a/src/static/indicator_blocks.js b/src/static/indicator_blocks.js index 4724bae..3fc7672 100644 --- a/src/static/indicator_blocks.js +++ b/src/static/indicator_blocks.js @@ -1,38 +1,52 @@ - // Define Blockly blocks dynamically based on indicators - export function defineIndicatorBlocks() { - const indicatorOutputs = window.UI.indicators.getIndicatorOutputs(); - const toolboxCategory = document.querySelector('#toolbox category[name="Indicators"]'); +// client/indicator_blocks.js - for (let indicatorName in indicatorOutputs) { - const outputs = indicatorOutputs[indicatorName]; +// Define Blockly blocks and their JSON generators dynamically based on indicators +export function defineIndicatorBlocks() { + // Retrieve the indicator outputs configuration + const indicatorOutputs = window.UI.indicators.getIndicatorOutputs(); + const toolboxCategory = document.querySelector('#toolbox category[name="Indicators"]'); - // 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); - } + if (!toolboxCategory) { + console.error('Indicators category not found in the toolbox.'); + return; } + + for (let indicatorName in indicatorOutputs) { + const outputs = indicatorOutputs[indicatorName]; + + // Define the block for this indicator + Blockly.defineBlocksWithJsonArray([{ + "type": indicatorName, + "message0": `${indicatorName} Output %1`, + "args0": [ + { + "type": "field_dropdown", + "name": "OUTPUT", + "options": outputs.map(output => [output, output]) + } + ], + "output": "Number", + "colour": 230, + "tooltip": `Select the ${indicatorName} output`, + "helpUrl": "" + }]); + + // Define the JSON generator for this block + Blockly.JSON[indicatorName] = function(block) { + const selectedOutput = block.getFieldValue('OUTPUT'); + const json = { + type: 'indicator', + name: indicatorName, + output: selectedOutput + }; + return JSON.stringify(json); + }; + + // Append the newly created block to the Indicators category in the toolbox + const blockElement = document.createElement('block'); + blockElement.setAttribute('type', indicatorName); + toolboxCategory.appendChild(blockElement); + } + + console.log('Indicator blocks and their JSON generators have been defined and inserted into the toolbox.'); +} diff --git a/src/static/json_generators.js b/src/static/json_generators.js index 51079ec..cb4f57c 100644 --- a/src/static/json_generators.js +++ b/src/static/json_generators.js @@ -1,114 +1,448 @@ - // Define JSON generators for custom blocks - export function defineJsonGenerators() { - // Initialize JSON generator - if (!Blockly.JSON) { - Blockly.JSON = new Blockly.Generator('JSON'); +// client/json_generators.js + +export function defineJsonGenerators() { + // Initialize the JSON generator if not already initialized + if (!Blockly.JSON) { + Blockly.JSON = new Blockly.Generator('JSON'); + } + + /** + * Helper function to safely parse JSON strings. + * Returns an empty object if parsing fails. + */ + function safeParse(jsonString) { + try { + return JSON.parse(jsonString); + } catch (e) { + console.error("JSON Parsing Error:", e); + return {}; + } + } + + /** + * Trade Action Block JSON Generator + * Captures trade actions including conditions, trade types, sizes, stop loss, take profit, and trade options. + */ + Blockly.JSON['trade_action'] = function(block) { + const condition = Blockly.JSON.valueToCode(block, 'CONDITION', Blockly.JSON.ORDER_ATOMIC) || "False"; + const tradeType = block.getFieldValue('TRADE_TYPE'); // e.g., 'buy' or 'sell' + const size = Blockly.JSON.valueToCode(block, 'SIZE', Blockly.JSON.ORDER_ATOMIC) || 1; + 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 tradeOptionsCode = Blockly.JSON.statementToCode(block, 'TRADE_OPTIONS').trim(); + let tradeOptions = []; + + if (tradeOptionsCode) { + tradeOptions = safeParse(tradeOptionsCode); + // Ensure tradeOptions is an array + if (!Array.isArray(tradeOptions)) { + tradeOptions = [tradeOptions]; + } } - - // 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); + const json = { + type: 'trade_action', + condition: condition, + trade_type: tradeType, + size: parseFloat(size), + stop_loss: stopLoss !== null ? parseFloat(stopLoss) : null, + take_profit: takeProfit !== null ? parseFloat(takeProfit) : null, + trade_options: tradeOptions }; - // 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'); + return JSON.stringify(json); }; + + /** + * Trade Option Block JSON Generator + * Captures trade options like order type, limit price, and time in force. + */ + Blockly.JSON['trade_option'] = function(block) { + const orderType = block.getFieldValue('ORDER_TYPE'); // e.g., 'market', 'limit' + const limitPrice = Blockly.JSON.valueToCode(block, 'LIMIT_PRICE', Blockly.JSON.ORDER_ATOMIC) || null; + const timeInForce = block.getFieldValue('TIF'); // e.g., 'gtc', 'ioc' + + const json = { + order_type: orderType, + limit_price: limitPrice !== null ? parseFloat(limitPrice) : null, + tif: timeInForce + }; + + return JSON.stringify(json); + }; + + /** + * comparison JSON Generator + * Compares two numerical values, where one can be a strategy_profit_loss block. + */ + Blockly.JSON['comparison'] = function(block) { + const operator = block.getFieldValue('OPERATOR'); + + // Generate JSON for left operand + const leftBlock = block.getInputTargetBlock('LEFT'); + let leftValue; + if (leftBlock && leftBlock.type === 'strategy_profit_loss') { + leftValue = JSON.parse(Blockly.JSON['strategy_profit_loss'](leftBlock)); + } else { + leftValue = Blockly.JSON.valueToCode(block, 'LEFT', Blockly.JSON.ORDER_ATOMIC) || 0; + try { + leftValue = JSON.parse(leftValue); + } catch (e) { + leftValue = parseFloat(leftValue); + } + } + + // Generate JSON for right operand + const rightBlock = block.getInputTargetBlock('RIGHT'); + let rightValue; + if (rightBlock && rightBlock.type === 'strategy_profit_loss') { + rightValue = JSON.parse(Blockly.JSON['strategy_profit_loss'](rightBlock)); + } else { + rightValue = Blockly.JSON.valueToCode(block, 'RIGHT', Blockly.JSON.ORDER_ATOMIC) || 0; + try { + rightValue = JSON.parse(rightValue); + } catch (e) { + rightValue = parseFloat(rightValue); + } + } + + const json = { + type: 'comparison', + operator: operator, + left: leftValue, + right: rightValue + }; + + return JSON.stringify(json); + }; + + /** + * Logical AND Block JSON Generator + * Captures logical AND operations between two conditions. + */ + Blockly.JSON['logical_and'] = function(block) { + const condition1 = Blockly.JSON.valueToCode(block, 'CONDITION1', Blockly.JSON.ORDER_ATOMIC) || "False"; + const condition2 = Blockly.JSON.valueToCode(block, 'CONDITION2', Blockly.JSON.ORDER_ATOMIC) || "False"; + + const json = { + type: 'logical_and', + conditions: [condition1, condition2] + }; + + return JSON.stringify(json); + }; + + /** + * Logical OR Block JSON Generator + * Captures logical OR operations between two conditions. + */ + Blockly.JSON['logical_or'] = function(block) { + const condition1 = Blockly.JSON.valueToCode(block, 'CONDITION1', Blockly.JSON.ORDER_ATOMIC) || "False"; + const condition2 = Blockly.JSON.valueToCode(block, 'CONDITION2', Blockly.JSON.ORDER_ATOMIC) || "False"; + + const json = { + type: 'logical_or', + conditions: [condition1, condition2] + }; + + return JSON.stringify(json); + }; + + /** + * Is False Block JSON Generator + * Captures a condition that checks if another condition is false. + */ + Blockly.JSON['is_false'] = function(block) { + const condition = Blockly.JSON.valueToCode(block, 'CONDITION', Blockly.JSON.ORDER_ATOMIC) || "False"; + + const json = { + type: 'is_false', + condition: condition + }; + + return JSON.stringify(json); + }; + + /** + * Arithmetic Operator Block JSON Generator + * Captures arithmetic operations between two numerical values. + */ + Blockly.JSON['arithmetic_operator'] = function(block) { + const operand1 = Blockly.JSON.valueToCode(block, 'OPERAND1', Blockly.JSON.ORDER_ATOMIC) || "0"; + const operand2 = Blockly.JSON.valueToCode(block, 'OPERAND2', Blockly.JSON.ORDER_ATOMIC) || "0"; + const operator = block.getFieldValue('OPERATOR'); // e.g., '+', '-', '*', '/' + + const json = { + type: 'arithmetic_operator', + operator: operator, + operands: [operand1, operand2] + }; + + return JSON.stringify(json); + }; + + /** + * Last Candle Value Block JSON Generator + * Captures the value part of the last candle from a specified data source. + */ + Blockly.JSON['last_candle_value'] = function(block) { + const candlePart = block.getFieldValue('CANDLE_PART'); // e.g., 'open', 'high', 'low', 'close', 'volume' + const sourceCode = Blockly.JSON.valueToCode(block, 'SOURCE', Blockly.JSON.ORDER_ATOMIC) || "{}"; + const source = safeParse(sourceCode); + + const json = { + type: 'last_candle_value', + candle_part: candlePart, + source: source + }; + + return JSON.stringify(json); + }; + + /** + * Source Block JSON Generator + * Captures the data source details like timeframe, exchange, and symbol. + */ + Blockly.JSON['source'] = function(block) { + const timeframe = block.getFieldValue('TF'); // e.g., '5m', '1h' + const exchange = block.getFieldValue('EXC'); // e.g., 'Binance' + const symbol = block.getFieldValue('SYM'); // e.g., 'BTCUSD' + + const json = { + timeframe: timeframe, + exchange: exchange, + symbol: symbol + }; + + return JSON.stringify(json); + }; + + /** + * Target Market Block JSON Generator + * Captures target market parameters for trading. + */ + Blockly.JSON['target_market'] = function(block) { + const timeframe = block.getFieldValue('TIMEFRAME'); // e.g., '5m', '1h' + const exchange = block.getFieldValue('EXCHANGE'); // e.g., 'Binance' + const symbol = block.getFieldValue('SYMBOL'); // e.g., 'BTCUSD' + + const json = { + timeframe: timeframe, + exchange: exchange, + symbol: symbol + }; + + return JSON.stringify(json); + }; + + /** + * strategy_profit_loss JSON Generator + * Outputs the metric (profit or loss) to evaluate. + */ + Blockly.JSON['strategy_profit_loss'] = function(block) { + const metric = block.getFieldValue('METRIC'); // 'profit' or 'loss' + + const json = { + type: 'strategy_profit_loss', + metric: metric + }; + + return JSON.stringify(json); + }; + + /** + * Current Balance Block JSON Generator + * Captures the current balance of the account. + */ + Blockly.JSON['current_balance'] = function(block) { + const json = { + type: 'current_balance' + }; + + return JSON.stringify(json); + }; + + /** + * Starting Balance Block JSON Generator + * Captures the starting balance of the account. + */ + Blockly.JSON['starting_balance'] = function(block) { + const json = { + type: 'starting_balance' + }; + + return JSON.stringify(json); + }; + + /** + * Active Trades Block JSON Generator + * Captures the number of active trades. + */ + Blockly.JSON['active_trades'] = function(block) { + const json = { + type: 'active_trades' + }; + + return JSON.stringify(json); + }; + + /** + * Flag Is Set Block JSON Generator + * Captures whether a specific flag is set. + */ + Blockly.JSON['flag_is_set'] = function(block) { + const flagName = block.getFieldValue('FLAG_NAME'); // e.g., 'flag1' + + const json = { + type: 'flag_is_set', + flag_name: flagName + }; + + return JSON.stringify(json); + }; + + /** + * Set Flag Block JSON Generator + * Captures the action to set a specific flag. + */ + Blockly.JSON['set_flag'] = function(block) { + const flagName = block.getFieldValue('FLAG_NAME'); // e.g., 'flag1' + + const json = { + type: 'set_flag', + flag_name: flagName + }; + + return JSON.stringify(json); + }; + + /** + * Value Input Block JSON Generator + * Captures a numerical input value. + */ + Blockly.JSON['value_input'] = function(block) { + const value = Blockly.JSON.valueToCode(block, 'VALUE', Blockly.JSON.ORDER_ATOMIC) || "0"; + + const json = { + type: 'value_input', + value: parseFloat(value) + }; + + return JSON.stringify(json); + }; + + /** + * Time in Force Block JSON Generator + * Captures the time in force for an order. + */ + Blockly.JSON['time_in_force'] = function(block) { + const tif = block.getFieldValue('TIF'); // e.g., 'gtc', 'ioc' + + const json = { + type: 'time_in_force', + tif: tif + }; + + return JSON.stringify(json); + }; + + /** + * Conditional Execution Block JSON Generator + * Captures conditional statements within the strategy. + */ + Blockly.JSON['conditional_execution'] = function(block) { + const conditionCode = Blockly.JSON.valueToCode(block, 'CONDITION', Blockly.JSON.ORDER_ATOMIC) || "False"; + const actionsCode = Blockly.JSON.statementToCode(block, 'ACTIONS').trim(); + let actions = []; + + if (actionsCode) { + actions = safeParse(actionsCode); + // Ensure actions is an array + if (!Array.isArray(actions)) { + actions = [actions]; + } + } + + const json = { + type: 'conditional_execution', + condition: conditionCode, + actions: actions + }; + + return JSON.stringify(json); + }; + + /** + * Market Order Block JSON Generator + * Captures market order details. + */ + Blockly.JSON['market_order'] = function(block) { + const size = Blockly.JSON.valueToCode(block, 'SIZE', Blockly.JSON.ORDER_ATOMIC) || 0; + const json = { + type: 'market_order', + size: parseFloat(size) + }; + + return JSON.stringify(json); + }; + + /** + * Limit Order Block JSON Generator + * Captures limit order details. + */ + Blockly.JSON['limit_order'] = function(block) { + const size = Blockly.JSON.valueToCode(block, 'SIZE', Blockly.JSON.ORDER_ATOMIC) || 0; + const price = Blockly.JSON.valueToCode(block, 'PRICE', Blockly.JSON.ORDER_ATOMIC) || 0; + const tif = block.getFieldValue('TIF'); // e.g., 'gtc', 'ioc' + + const json = { + type: 'limit_order', + size: parseFloat(size), + price: parseFloat(price), + tif: tif + }; + + return JSON.stringify(json); + }; + + /** + * Entry Point Block JSON Generator + * Captures the entry point of the strategy. + */ + Blockly.JSON['entry_point'] = function(block) { + const json = { + type: 'entry_point' + }; + + return JSON.stringify(json); + }; + + /** + * Exit Point Block JSON Generator + * Captures the exit point of the strategy. + */ + Blockly.JSON['exit_point'] = function(block) { + const json = { + type: 'exit_point' + }; + + return JSON.stringify(json); + }; + + /** + * Notify User Block JSON Generator + * Captures a message to notify the user. + */ + Blockly.JSON['notify_user'] = function(block) { + const message = Blockly.JSON.valueToCode(block, 'MESSAGE', Blockly.JSON.ORDER_ATOMIC) || "No message provided."; + + const json = { + type: 'notify_user', + message: message + }; + + return JSON.stringify(json); + }; + + console.log('All JSON generators have been defined successfully.'); +} diff --git a/src/templates/index.html b/src/templates/index.html index 47a733e..5bd5532 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -11,6 +11,7 @@ +