nearly all core functionality is flushed out.

This commit is contained in:
Rob 2024-10-13 01:23:26 -03:00
parent 4eda0b6f81
commit 232d479827
15 changed files with 1462 additions and 782 deletions

View File

@ -6,6 +6,7 @@ requests==2.30.0
pandas==2.2.3 pandas==2.2.3
passlib~=1.7.4 passlib~=1.7.4
ccxt==4.4.8 ccxt==4.4.8
flask-socketio
pytz==2024.2 pytz==2024.2
backtrader==1.9.78.123 backtrader==1.9.78.123
eventlet~=0.37.0

View File

@ -14,7 +14,7 @@ from trade import Trades
class BrighterTrades: class BrighterTrades:
def __init__(self): def __init__(self, socketio):
# Object that interacts with the persistent data. # Object that interacts with the persistent data.
self.data = DataCache() self.data = DataCache()
@ -46,10 +46,11 @@ class BrighterTrades:
self.trades.connect_exchanges(exchanges=self.exchanges) self.trades.connect_exchanges(exchanges=self.exchanges)
# Object that maintains the strategies data # 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. # 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) 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: 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. Handles the creation of a new strategy based on the provided data.
:param data: A dictionary containing the attributes of the new strategy. :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 # Validate presence of required fields
user_name = data.get('user_name') required_fields = ['user_name', 'name', 'workspace', 'code']
if not user_name: missing_fields = [field for field in required_fields if field not in data]
return {"success": False, "message": "User not specified"} 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') user_id = self.get_user_info(user_name=user_name, info='User_id')
if not user_id: if not user_id:
return {"success": False, "message": "User ID not found"} 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 # Prepare the strategy data for insertion
strategy_data = { strategy_data = {
"creator": user_id, "creator": user_id,
"name": data['name'], "name": data['name'].strip(),
"workspace": data['workspace'], "workspace": data['workspace'].strip(),
"code": data['code'], "code": code_json,
"stats": data.get('stats', {}), "stats": data.get('stats', {}),
"public": data.get('public', 0), # Default to private if not specified "public": int(data.get('public', 0)),
"fee": data.get('fee', None) # Default to None if not specified "fee": float(data.get('fee', 0.0))
} }
# Save the new strategy (in both cache and database) and return the result. # Save the new strategy (in both cache and database) and return the result.
return self.strategies.new_strategy(strategy_data) return self.strategies.new_strategy(strategy_data)
@ -651,14 +665,14 @@ class BrighterTrades:
return 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. 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_type: The type of the incoming message.
:param msg_data: The data associated with 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 :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 no data is found to ensure the WebSocket channel isn't burdened with unnecessary
communication. communication.
@ -728,10 +742,17 @@ class BrighterTrades:
# Handle backtest operations # Handle backtest operations
if msg_type == 'submit_backtest': if msg_type == 'submit_backtest':
user_id = self.get_user_info(user_name=msg_data['user_name'], info='User_id') # Validate required fields
# Pass socket_conn to the backtest handler required_fields = ['strategy', 'start_date', 'capital', 'commission', 'user_name']
result = self.backtester.handle_backtest_message(user_id, msg_data, socket_conn) if not all(field in msg_data for field in required_fields):
return standard_reply("backtest_submitted", result) 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': if msg_type == 'delete_backtest':
self.delete_backtest(msg_data) self.delete_backtest(msg_data)

View File

@ -7,173 +7,8 @@ from DataCache_v3 import DataCache
import datetime as dt 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:sig_name:state> == <value>.
# """
# 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: class Strategies:
def __init__(self, data: DataCache, trades): def __init__(self, data: DataCache, trades, indicators):
""" """
Initializes the Strategies class. Initializes the Strategies class.
@ -182,7 +17,8 @@ class Strategies:
""" """
self.data = data # Database interaction instance self.data = data # Database interaction instance
self.trades = trades 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 # Create a cache for strategies with necessary columns
self.data.create_cache(name='strategies', self.data.create_cache(name='strategies',
@ -190,7 +26,8 @@ class Strategies:
size_limit=100, size_limit=100,
eviction_policy='deny', eviction_policy='deny',
default_expiration=dt.timedelta(hours=24), 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: def new_strategy(self, data: dict) -> dict:
""" """
@ -202,47 +39,64 @@ class Strategies:
try: try:
# Check if a strategy with the same name already exists for this user # Check if a strategy with the same name already exists for this user
filter_conditions = [('creator', data.get('creator')), ('name', data['name'])] filter_conditions = [('creator', data.get('creator')), ('name', data['name'])]
existing_strategy = self.data.get_rows_from_datacache(cache_name='strategies', existing_strategy = self.data.get_rows_from_datacache(
filter_vals=filter_conditions) cache_name='strategies',
filter_vals=filter_conditions
)
if not existing_strategy.empty: if not existing_strategy.empty:
return {"success": False, "message": "A strategy with this name already exists"} return {"success": False, "message": "A strategy with this name already exists"}
# Serialize complex data fields like workspace and stats # Validate and serialize 'workspace' (XML string)
workspace_serialized = json.dumps(data['workspace']) if isinstance(data['workspace'], dict) else data[ workspace_data = data['workspace']
'workspace'] if not isinstance(workspace_data, str) or not workspace_data.strip():
stats_serialized = json.dumps(data.get('stats', {})) # Convert stats to a JSON string 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()) tbl_key = str(uuid.uuid4())
# Insert the strategy into the database and cache # Insert the strategy into the database and cache
self.data.insert_row_into_datacache( self.data.insert_row_into_datacache(
cache_name='strategies', 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=( values=(
data.get('creator'), data.get('creator'),
data['name'], data['name'],
workspace_serialized, # Serialized workspace data['workspace'],
data['code'], data['code'],
stats_serialized, # Serialized stats stats_serialized,
data.get('public', False), bool(data.get('public', 0)),
data.get('fee', 0), float(data.get('fee', 0.0)),
tbl_key tbl_key,
data['strategy_components']
) )
) )
# Construct the saved strategy data to return # Construct the saved strategy data to return
saved_strategy = { saved_strategy = {
"id": tbl_key, # Assuming tbl_key is used as a unique identifier "id": tbl_key, # Assuming tbl_key is used as a unique identifier
"creator": data.get('creator'), "creator": data.get('creator'),
"name": data['name'], "name": data['name'],
"workspace": data['workspace'], # Original workspace data "workspace": workspace_data, # Original workspace data
"code": data['code'], "code": data['code'],
"stats": data.get('stats', {}), "stats": stats_data,
"public": data.get('public', False), "public": bool(data.get('public', 0)),
"fee": data.get('fee', 0) "fee": float(data.get('fee', 0.0))
} }
# If everything is successful, return a success message # If everything is successful, return a success message along with the saved strategy data
# along with the saved strategy data
return { return {
"success": True, "success": True,
"message": "Strategy created and saved successfully", "message": "Strategy created and saved successfully",
@ -251,6 +105,9 @@ class Strategies:
except Exception as e: except Exception as e:
# Catch any exceptions and return a failure message # 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)}"} return {"success": False, "message": f"Failed to create strategy: {str(e)}"}
def delete_strategy(self, user_id, name: str): def delete_strategy(self, user_id, name: str):
@ -277,22 +134,37 @@ class Strategies:
try: try:
tbl_key = data['tbl_key'] # The unique identifier for the strategy tbl_key = data['tbl_key'] # The unique identifier for the strategy
# Serialize complex data fields like workspace and stats # Validate and serialize 'workspace' (XML string)
workspace_serialized = json.dumps(data['workspace']) if isinstance(data['workspace'], dict) else data[ workspace_data = data['workspace']
'workspace'] if not isinstance(workspace_data, str) or not workspace_data.strip():
stats_serialized = json.dumps(data.get('stats', {})) # Convert stats to a JSON string 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 # 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 = ( values = (
data.get('creator'), data.get('creator'),
data['name'], data['name'],
workspace_serialized, # Serialized workspace workspace_data, # Use the validated workspace data
data['code'], data['code'],
stats_serialized, # Serialized stats stats_serialized, # Serialized stats
data.get('public', False), bool(data.get('public', 0)),
data.get('fee', 0), float(data.get('fee', 0.0)),
tbl_key tbl_key,
data['strategy_components'] # Serialized strategy components
) )
# Update the strategy in the database and cache # Update the strategy in the database and cache
@ -310,6 +182,8 @@ class Strategies:
except Exception as e: except Exception as e:
# Handle exceptions and return failure message # Handle exceptions and return failure message
import traceback
traceback.print_exc()
return {"success": False, "message": f"Failed to update strategy: {str(e)}"} return {"success": False, "message": f"Failed to update strategy: {str(e)}"}
def get_all_strategy_names(self, user_id) -> list | None: def get_all_strategy_names(self, user_id) -> list | None:
@ -376,71 +250,393 @@ class Strategies:
if filtered_strategies.empty: if filtered_strategies.empty:
return None return None
# Return the filtered strategy row (or a dictionary if needed) # Get the strategy row as a dictionary
return filtered_strategies.iloc[0].to_dict() strategy_row = filtered_strategies.iloc[0].to_dict()
def execute_cmd(self, strategy, action, cmd): # Deserialize the 'strategy_components' field
order_type = 'LIMIT' try:
if action == 'open_position': strategy_components = json.loads(strategy_row.get('strategy_components', '{}'))
# Attempt to create the trade. except json.JSONDecodeError:
status, result = self.trades.new_trade(strategy.symbol, cmd, order_type, strategy.trade_amount) strategy_components = {}
# If the trade failed. strategy_row['strategy_components'] = strategy_components
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'
if (action == 'stop_loss') or (action == 'take_profit'): # If 'code' is stored as a JSON string, deserialize it
if action == 'stop_loss': if isinstance(strategy_row.get('code'), str):
order_type = 'MARKET' strategy_row['code'] = json.loads(strategy_row['code'])
# 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'
print(f'Strategies.execute_cmd: Invalid action received: {action}') return strategy_row
return 'failed'
def update(self, signals): def generate_strategy_code(self, json_code):
""" """
Receives a reference to updated signal data. Loops through all Generates the code for the 'next' method and collects indicators, data sources, and flags.
published strategies and evaluates conditions against the data.
This function returns a list of strategies and action commands. :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): if isinstance(json_code, str):
action, cmd = strategy.evaluate_strategy(signals) json_code = json.loads(json_code)
if action != 'do_nothing':
# Execute the command.
return {'action': action, 'result': self.execute_cmd(strategy, action, cmd)}
return {'action': 'none'}
def get_stats(strategy): # Initialize code components
position = strategy.get_position() code_lines = []
pl = strategy.get_pl() indent_level = 1 # For 'next' method code indentation
stats = {'pos': position, 'pl': pl} indent = ' ' * indent_level
return stats
# Data object returned to function caller. # Initialize sets to collect indicators, data sources, and flags
return_obj = {} self.indicators_used = []
# Loop through all the published strategies. self.data_sources_used = set()
# for strategy in self.strat_list: self.flags_used = set()
# actions = process_strategy(strategy)
# stat_updates = get_stats(strategy) # Generate code based on the JSON structure
# return_obj[strategy.name] = {'actions': actions, 'stats': stat_updates} code_lines.append(f"def next(self):")
if len(return_obj) == 0: indent_level += 1 # Increase indent level inside the 'next' method
return False
# 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: 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)

View File

@ -1,25 +1,34 @@
import json # app.py
import logging
from flask import Flask, render_template, request, redirect, jsonify, session, flash # Monkey patching must occur before other imports
from flask_cors import CORS import eventlet
from flask_sock import Sock eventlet.monkey_patch() # noqa: E402
from email_validator import validate_email, EmailNotValidError
# Handles all updates and requests for locally stored data. # Standard library imports
from BrighterTrades import BrighterTrades 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 # Set up logging
logging.basicConfig(level=logging.DEBUG) 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. # Create a Flask object named app that serves the html.
app = Flask(__name__) app = Flask(__name__)
# Create a socket in order to receive requests. # 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. # Set server configuration globals.
CORS_HEADERS = 'Content-Type' CORS_HEADERS = 'Content-Type'
@ -102,49 +111,49 @@ def index():
open_orders=rendered_data['open_orders']) open_orders=rendered_data['open_orders'])
@sock.route('/ws') @socketio.on('connect')
def ws(socket_conn): 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): msg_type, msg_data = data['message_type'], data['data']
"""
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 = 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 # Check if the user is logged in
user_name = msg_data.get('user_name') if not brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'):
if not user_name: emit('message', {"success": False, "message": "User not logged in"})
socket_conn.send(json.dumps({"success": False, "message": "User not specified"})) return
return
# Check if the user is logged in # Process the incoming message based on the type
if not brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'): resp = brighter_trades.process_incoming_message(msg_type=msg_type, msg_data=msg_data, socket_conn_id=request.sid)
socket_conn.send(json.dumps({"success": False, "message": "User not logged in"}))
return
# Process the incoming message based on the type, passing socket_conn # Send the response back to the client
resp = brighter_trades.process_incoming_message(msg_type=msg_type, msg_data=msg_data, socket_conn=socket_conn) if resp:
emit('message', resp)
# 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}')
@app.route('/settings', methods=['POST']) @app.route('/settings', methods=['POST'])
@ -338,9 +347,11 @@ def indicator_init():
# Get the indicator data # Get the indicator data
source = {'user_name': username, 'market': chart_view} source = {'user_name': username, 'market': chart_view}
data = brighter_trades.get_indicator_data(user_name=username, source=source, start_ts=None, num_results=1000) 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 return jsonify(data), 200
if __name__ == '__main__': 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)

View File

@ -1,30 +1,24 @@
import ast
import json
import re
import backtrader as bt import backtrader as bt
import datetime as dt import datetime as dt
from DataCache_v3 import DataCache from DataCache_v3 import DataCache
from Strategies import Strategies from Strategies import Strategies
import threading from indicators import Indicators
import numpy as np import numpy as np
import pandas as pd
class Backtester: 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 """ """ Initialize the Backtesting class with a cache for back-tests """
self.data_cache = data_cache self.data_cache = data_cache
self.strategies = strategies self.strategies = strategies
self.indicators_manager = indicators
self.socketio = socketio
# Create a cache for storing back-tests # Create a cache for storing back-tests
self.data_cache.create_cache('tests', cache_type='row', size_limit=100, self.data_cache.create_cache('tests', cache_type='row', size_limit=100,
default_expiration=dt.timedelta(days=1), default_expiration=dt.timedelta(days=1),
eviction_policy='evict') 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): def cache_backtest(self, user_name, backtest_name, backtest_data):
""" Cache the backtest data for a user """ """ Cache the backtest data for a user """
columns = ('user_name', 'strategy_name', 'start_time', 'capital', 'commission', 'results') columns = ('user_name', 'strategy_name', 'start_time', 'capital', 'commission', 'results')
@ -39,164 +33,162 @@ class Backtester:
cache_key = f"backtest:{user_name}:{backtest_name}" cache_key = f"backtest:{user_name}:{backtest_name}"
self.data_cache.insert_row_into_cache('tests', columns, values, key=cache_key) 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.""" """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): 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): def __init__(self):
# Extract unique sources (exchange, symbol, timeframe) from blocks self.precomputed_indicators = precomputed_indicators
self.sources = self.extract_sources(user_strategy) self.indicator_pointers = {}
self.indicator_names = list(precomputed_indicators.keys())
self.current_step = 0
# Map of source to data feed (used later in next()) # Initialize pointers for each indicator
self.source_data_feed_map = {} for name in self.indicator_names:
self.indicator_pointers[name] = 0 # Start at the first row
def extract_sources(self, user_strategy): # Initialize any other needed variables
"""Extracts unique sources from the strategy.""" self.flags = {}
sources = [] self.starting_balance = self.broker.getvalue()
for block in user_strategy.get('blocks', []):
if block.get('type') in ['last_candle_value', 'trade_action']:
source = self.extract_source_from_block(block)
if source and source not in sources:
sources.append(source)
elif block.get('type') == 'target_market':
target_source = self.extract_target_market(block)
if target_source and target_source not in sources:
sources.append(target_source)
return sources
def extract_source_from_block(self, block): def process_indicator(self, indicator_name, output_field):
"""Extract source (exchange, symbol, timeframe) from a strategy block.""" # Get the DataFrame for the indicator
source = {} df = self.precomputed_indicators[indicator_name]
if block.get('type') == 'last_candle_value':
source = block.get('SOURCE', None)
# If SOURCE is missing, use the trade target or default
if not source:
source = self.get_default_chart_view(self.user_name) # Fallback to default
return source
def extract_target_market(self, block): # Get the current index for the indicator
"""Extracts target market data (timeframe, exchange, symbol) from the trade_action block.""" idx = self.indicator_pointers[indicator_name]
target_market = block.get('target_market', {})
return { if idx >= len(df):
'timeframe': target_market.get('TF', '5m'), return None # No more data
'exchange': target_market.get('EXC', 'Binance'),
'symbol': target_market.get('SYM', 'BTCUSD') # 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): 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: 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: except Exception as e:
print(f"Error executing trading logic: {e}") print(f"Error in strategy execution: {e}")
return MappedStrategy 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: try:
# Convert the start date to a datetime object # Convert the start date to a datetime object
start_dt = dt.datetime.strptime(start_date, '%Y-%m-%dT%H:%M') start_dt = dt.datetime.strptime(start_date, '%Y-%m-%dT%H:%M')
# Dictionary to map each source to its corresponding data feed # Ensure exchange details contain required keys (fallback if missing)
data_feeds = {} timeframe = source.get('timeframe', '1h')
exchange = source.get('exchange', 'Binance')
symbol = source.get('symbol', 'BTCUSDT')
for source in sources: # Fetch OHLC data from DataCache based on the source
# Ensure exchange details contain required keys (fallback if missing) data = self.data_cache.get_records_since(start_datetime=start_dt, ex_details=[symbol, timeframe, exchange])
asset = source.get('asset', 'BTCUSD')
timeframe = source.get('timeframe', '5m')
exchange = source.get('exchange', 'Binance')
# Fetch OHLC data from DataCache based on the source return data
ex_details = [asset, timeframe, exchange, user_name]
data = self.data_cache.get_records_since(start_dt, ex_details)
# Return the data as a Pandas DataFrame compatible with Backtrader
data_feeds[tuple(ex_details)] = data
return data_feeds
except Exception as e: except Exception as e:
print(f"Error preparing data feed: {e}") print(f"Error preparing data feed: {e}")
return None 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. Precompute indicator values and return a dictionary of DataFrames.
Also sends progress updates to the client via WebSocket. """
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(): def execute_backtest():
cerebro = bt.Cerebro() try:
cerebro = bt.Cerebro()
# Add the mapped strategy to the backtest # Add the mapped strategy to the backtest
cerebro.addstrategy(strategy) cerebro.addstrategy(strategy_class)
# Add all the data feeds to Cerebro # Add the main data feed to Cerebro
total_bars = 0 # Total number of data points (bars) across all feeds # noinspection PyArgumentList
for source, data_feed in data_feed_map.items():
bt_feed = bt.feeds.PandasData(dataname=data_feed) bt_feed = bt.feeds.PandasData(dataname=data_feed)
cerebro.adddata(bt_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 # Set initial capital and commission
initial_capital = cerebro.broker.getvalue() 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 # Run the backtest
current_bar = 0 print("Running backtest...")
last_progress = 0 start_time = dt.datetime.now()
cerebro.run()
end_time = dt.datetime.now()
# Custom next function to track progress (if you have a large dataset) # Extract performance metrics
def track_progress(): final_value = cerebro.broker.getvalue()
nonlocal current_bar, last_progress run_duration = (end_time - start_time).total_seconds()
current_bar += 1
progress = (current_bar / total_bars) * 100
# Send progress update every 10% increment # Send 100% completion
if progress >= last_progress + 10: self.socketio.emit('progress_update', {"progress": 100}, room=socket_conn_id)
last_progress += 10
socket_conn.send(json.dumps({"progress": int(last_progress)}))
# Attach the custom next method to the strategy # Prepare the results to pass into the callback
strategy.next = track_progress backtest_results = {
"initial_capital": initial_cash,
"final_portfolio_value": final_value,
"run_duration": run_duration
}
# Run the backtest callback(backtest_results)
print("Running backtest...")
start_time = dt.datetime.now()
cerebro.run()
end_time = dt.datetime.now()
# Extract performance metrics except Exception as e:
final_value = cerebro.broker.getvalue() # Handle exceptions and send error messages to the client
run_duration = (end_time - start_time).total_seconds() 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 # Start the backtest as a background task
socket_conn.send(json.dumps({"progress": 100})) self.socketio.start_background_task(execute_backtest)
# Prepare the results to pass into the callback def handle_backtest_message(self, user_id, msg_data, socket_conn_id):
callback({
"initial_capital": initial_capital,
"final_portfolio_value": final_value,
"run_duration": run_duration
})
# Map the user strategy and prepare the data feeds
sources = strategy.extract_sources()
data_feed_map = self.prepare_data_feed(msg_data['start_date'], sources, user_name)
# Run the backtest in a separate thread
thread = threading.Thread(target=execute_backtest)
thread.start()
def handle_backtest_message(self, user_id, msg_data, socket_conn):
user_name = msg_data.get('user_name') user_name = msg_data.get('user_name')
backtest_name = f"{msg_data['strategy']}_backtest" backtest_name = f"{msg_data['strategy']}_backtest"
@ -210,55 +202,43 @@ class Backtester:
if not user_strategy: if not user_strategy:
return {"error": f"Strategy {strategy_name} not found for user {user_name}"} return {"error": f"Strategy {strategy_name} not found for user {user_name}"}
# Extract sources from the strategy JSON # Extract the main data source from the strategy components
sources = self.extract_sources_from_strategy_json(user_strategy.get('strategy_json')) strategy_components = user_strategy['strategy_components']
data_sources = strategy_components['data_sources']
if not sources: if not data_sources:
return {"error": "No valid sources found in the strategy."} return {"error": "No valid data sources found in the strategy."}
# Prepare the data feed map based on extracted sources # For simplicity, use the first data source as the main data feed
data_feed_map = self.prepare_data_feed(msg_data['start_date'], sources, user_name) 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."} 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 # 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 # Define the callback function to handle backtest completion
def backtest_callback(results): def backtest_callback(results):
self.store_backtest_results(user_name, backtest_name, results) self.store_backtest_results(user_name, backtest_name, results)
self.update_strategy_stats(user_id, strategy_name, results) self.update_strategy_stats(user_id, strategy_name, results)
# Run the backtest and pass the callback function, msg_data, and user_name # Emit the results back to the client
self.run_backtest(mapped_strategy, data_feed_map, msg_data, user_name, backtest_callback, socket_conn) 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"} 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): def update_strategy_stats(self, user_id, strategy_name, results):
""" Update the strategy stats with the backtest results """ """ Update the strategy stats with the backtest results """
strategy = self.strategies.get_strategy_by_name(user_id=user_id, name=strategy_name) strategy = self.strategies.get_strategy_by_name(user_id=user_id, name=strategy_name)

View File

@ -1,4 +1,36 @@
""" # test_backtrader_pandasdata.py
set_cache_item import backtrader as bt
create_cache 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.")

View File

@ -28,7 +28,7 @@ class StratUIManager {
* @param {string} action - The action to perform ('new' or 'edit'). * @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). * @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}`); console.log(`Opening form for action: ${action}, strategy: ${strategyData?.name}`);
if (this.formElement) { if (this.formElement) {
const headerTitle = this.formElement.querySelector("#draggable_header h1"); const headerTitle = this.formElement.querySelector("#draggable_header h1");
@ -63,11 +63,14 @@ class StratUIManager {
// Display the form // Display the form
this.formElement.style.display = "grid"; 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) { if (UI.strats && UI.strats.workspaceManager) {
setTimeout(() => { try {
UI.strats.workspaceManager.initWorkspace(); await UI.strats.workspaceManager.initWorkspace();
}, 100); // Delay slightly to allow the form to render properly console.log("Blockly workspace initialized.");
} catch (error) {
console.error("Failed to initialize Blockly workspace:", error);
}
} else { } else {
console.error("Workspace manager is not initialized or is unavailable."); 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. * Hides the "Create New Strategy" form by adding a 'hidden' class.
*/ */
@ -264,7 +266,7 @@ class StratWorkspaceManager {
* @async * @async
* @throws {Error} If required elements ('blocklyDiv' or 'toolbox') are not found. * @throws {Error} If required elements ('blocklyDiv' or 'toolbox') are not found.
*/ */
initWorkspace() { async initWorkspace() {
if (!document.getElementById('blocklyDiv')) { if (!document.getElementById('blocklyDiv')) {
console.error("blocklyDiv is not loaded."); console.error("blocklyDiv is not loaded.");
return; return;
@ -275,25 +277,24 @@ class StratWorkspaceManager {
} }
// Initialize custom blocks and Blockly workspace // Initialize custom blocks and Blockly workspace
this._loadModulesAndInitWorkspace(); await this._loadModulesAndInitWorkspace();
} }
async _loadModulesAndInitWorkspace() { async _loadModulesAndInitWorkspace() {
if (!this.blocksDefined) { if (!this.blocksDefined) {
try { try {
// Load all modules concurrently to reduce loading time // Load and define JSON generators first
const [customBlocksModule, indicatorBlocksModule, pythonGeneratorsModule, jsonGeneratorsModule] = await Promise.all([ const jsonGeneratorsModule = await import('./json_generators.js');
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();
jsonGeneratorsModule.defineJsonGenerators(); 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) { } catch (error) {
console.error("Error loading Blockly modules: ", error); console.error("Error loading Blockly modules: ", error);
return; return;
@ -326,6 +327,7 @@ class StratWorkspaceManager {
scaleSpeed: 1.2 scaleSpeed: 1.2
} }
}); });
console.log('Blockly workspace initialized and modules loaded.');
} }
/** /**
@ -358,18 +360,17 @@ class StratWorkspaceManager {
const strategyName = nameElement.value; const strategyName = nameElement.value;
// Initialize code generators // Initialize code generators
Blockly.Python.init(this.workspace);
Blockly.JSON.init(this.workspace); Blockly.JSON.init(this.workspace);
// Generate code and data representations // Generate code and data representations
const pythonCode = Blockly.Python.workspaceToCode(this.workspace);
const strategyJson = this._generateStrategyJsonFromWorkspace(); const strategyJson = this._generateStrategyJsonFromWorkspace();
// Generate workspace XML for restoration when editing
const workspaceXml = Blockly.Xml.workspaceToDom(this.workspace); const workspaceXml = Blockly.Xml.workspaceToDom(this.workspace);
const workspaceXmlText = Blockly.Xml.domToText(workspaceXml); const workspaceXmlText = Blockly.Xml.domToText(workspaceXml);
return JSON.stringify({ return JSON.stringify({
name: strategyName, name: strategyName,
code: pythonCode,
strategy_json: strategyJson, strategy_json: strategyJson,
workspace: workspaceXmlText workspace: workspaceXmlText
}); });
@ -399,6 +400,7 @@ class StratWorkspaceManager {
statements: {} statements: {}
}; };
// Capture all fields in the block
block.inputList.forEach(input => { block.inputList.forEach(input => {
if (input.fieldRow) { if (input.fieldRow) {
input.fieldRow.forEach(field => { input.fieldRow.forEach(field => {
@ -407,19 +409,32 @@ class StratWorkspaceManager {
} }
}); });
} }
});
// Capture all connected blocks
block.inputList.forEach(input => {
if (input.connection && input.connection.targetBlock()) { if (input.connection && input.connection.targetBlock()) {
const targetBlock = input.connection.targetBlock(); const targetBlock = input.connection.targetBlock();
if (input.type === Blockly.INPUT_VALUE) { if (input.type === Blockly.INPUT_VALUE) {
json.inputs[input.name] = this._blockToJson(targetBlock); json.inputs[input.name] = this._blockToJson(targetBlock);
} else if (input.type === Blockly.NEXT_STATEMENT) { } 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()) { 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; return json;
@ -468,6 +483,9 @@ class Strategies {
// Set the delete callback // Set the delete callback
this.uiManager.registerDeleteStrategyCallback(this.deleteStrategy.bind(this)); 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; return;
} }
let strategyObject; let strategyData;
try { 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) { } 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.'); alert('An error occurred while processing the strategy data.');
return; return;
} }
const feeValue = feeBox.value.trim(); // Basic client-side validation
const fee = parseFloat(feeValue); if (isNaN(strategyData.fee) || strategyData.fee < 0) {
if (isNaN(fee) || fee < 0) {
alert("Please enter a valid, non-negative number for the fee."); alert("Please enter a valid, non-negative number for the fee.");
return; return;
} }
const strategyName = nameBox.value.trim(); if (!strategyData.name) {
if (!strategyName) {
alert("Please provide a name for the strategy."); alert("Please provide a name for the strategy.");
return; return;
} }
const is_public = publicCheckbox.checked ? 1 : 0; // Send the strategy data to the server
// 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';
if (this.comms) { if (this.comms) {
// Determine message type based on action
const messageType = action === 'new' ? 'new_strategy' : 'edit_strategy';
this.comms.sendToApp(messageType, strategyData); this.comms.sendToApp(messageType, strategyData);
this.uiManager.hideForm(); this.uiManager.hideForm();
} else { } else {

View File

@ -108,7 +108,7 @@ class Backtesting {
populateStrategyDropdown() { populateStrategyDropdown() {
const strategyDropdown = document.getElementById('strategy_select'); const strategyDropdown = document.getElementById('strategy_select');
strategyDropdown.innerHTML = ''; strategyDropdown.innerHTML = '';
const strategies = this.ui.strats.getAvailableStrategies(); const strategies = this.ui.strats.dataManager.getAllStrategies();
console.log("Available strategies:", strategies); console.log("Available strategies:", strategies);
strategies.forEach(strategy => { strategies.forEach(strategy => {

View File

@ -1,13 +1,88 @@
class Comms { class Comms {
constructor() { constructor(userName) {
if (!userName) {
console.error('Comms: Cannot initialize Socket.IO without user_name.');
return;
}
this.connectionOpen = false; 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 this.eventHandlers = {}; // Event handlers for message types
// Callback collections that will receive various updates. // Callback collections that will receive various updates.
this.candleUpdateCallbacks = []; this.candleUpdateCallbacks = [];
this.candleCloseCallbacks = []; this.candleCloseCallbacks = [];
this.indicatorUpdateCallbacks = []; 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) { async getIndicatorData(userName) {
try { 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', credentials: 'same-origin',
mode: 'cors', mode: 'cors',
method: 'POST', method: 'POST',
@ -172,8 +247,8 @@ class Comms {
} }
/** /**
* Sends a request to update an indicator's properties. * Sends a request to create a new indicator.
* @param {Object} indicatorData - An object containing the updated properties of the indicator. * @param {Object} indicatorData - An object containing the properties of the new indicator.
* @returns {Promise<Object>} - The response from the server. * @returns {Promise<Object>} - The response from the server.
*/ */
async submitIndicator(indicatorData) { async submitIndicator(indicatorData) {
@ -188,87 +263,40 @@ class Comms {
}); });
return await response.json(); return await response.json();
} catch (error) { } catch (error) {
console.error('Error updating indicator:', error); console.error('Error creating indicator:', error);
return { success: false }; return { success: false };
} }
} }
/** /**
* Sends a message to the application server via WebSocket. * Sends a message to the application server via Socket.IO.
* Automatically includes the user_name from `window.UI.data.user_name` for authentication.
* @param {string} messageType - The type of the message. * @param {string} messageType - The type of the message.
* @param {Object} data - The data to be sent with the message. * @param {Object} data - The data to be sent with the message.
*/ */
sendToApp(messageType, data) { 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 = { const messageData = {
message_type: messageType, message_type: messageType,
data: { data: {
...data, // Include the existing data ...data,
user_name: user_name // Add user_name for authentication user_name: this.userName
} }
}; };
console.log('Comms: Sending->', JSON.stringify(messageData)); console.log('Comms: Sending->', JSON.stringify(messageData));
if (this.connectionOpen) { if (this.connectionOpen && this.socket) {
this.appCon.send(JSON.stringify(messageData)); this.socket.emit('message', messageData);
} else { } else {
setTimeout(() => { // Not an error; message will be queued
if (this.appCon) { console.warn('Socket.IO connection is not open. Queuing message.');
this.appCon.send(JSON.stringify(messageData)); // Queue the message to be sent once connected
} this.messageQueue.push({ messageType, data });
}, 1000); 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} interval - The interval of the candlestick data.
* @param {string} tradingPair - The trading pair to subscribe to. * @param {string} tradingPair - The trading pair to subscribe to.
*/ */
@ -294,5 +322,17 @@ class Comms {
this.candleClose(newCandle); 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);
};
} }
} }

View File

@ -1,3 +1,4 @@
// custom_blocks.js
// Define custom Blockly blocks and Python code generation // Define custom Blockly blocks and Python code generation
export function defineCustomBlocks() { export function defineCustomBlocks() {
// Custom block for retrieving last candle values // Custom block for retrieving last candle values
@ -234,6 +235,10 @@
"tooltip": "Select time in force for the order", "tooltip": "Select time in force for the order",
"helpUrl": "" "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 // Dynamically populate the block options using the available data
Blockly.defineBlocksWithJsonArray([{ Blockly.defineBlocksWithJsonArray([{
@ -243,26 +248,17 @@
{ {
"type": "field_dropdown", "type": "field_dropdown",
"name": "TF", "name": "TF",
"options": function() { "options": timeframeOptions
// Dynamically fetch available timeframes from bt_data.intervals
return bt_data.intervals.map(interval => [interval, interval]);
}
}, },
{ {
"type": "field_dropdown", "type": "field_dropdown",
"name": "EXC", "name": "EXC",
"options": function() { "options": exchangeOptions
// Dynamically fetch available exchanges from window.UI.exchanges.connected_exchanges
return window.UI.exchanges.connected_exchanges.map(exchange => [exchange, exchange]);
}
}, },
{ {
"type": "field_dropdown", "type": "field_dropdown",
"name": "SYM", "name": "SYM",
"options": function() { "options": symbolOptions
// Dynamically fetch available symbols from bt_data.symbols
return bt_data.symbols.map(symbol => [symbol, symbol]);
}
} }
], ],
"output": "source", // This output allows it to be connected to other blocks expecting a 'source' "output": "source", // This output allows it to be connected to other blocks expecting a 'source'
@ -271,7 +267,10 @@
"helpUrl": "" "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([{ Blockly.defineBlocksWithJsonArray([{
"type": "target_market", "type": "target_market",
"message0": "Target market: TF %1 Ex %2 Sym %3", "message0": "Target market: TF %1 Ex %2 Sym %3",
@ -279,23 +278,17 @@
{ {
"type": "field_dropdown", "type": "field_dropdown",
"name": "TF", "name": "TF",
"options": function() { "options": targetMarketTFOptions
return bt_data.intervals.map(interval => [interval, interval]);
}
}, },
{ {
"type": "field_dropdown", "type": "field_dropdown",
"name": "EXC", "name": "EXC",
"options": function() { "options": targetMarketEXCOptions
return window.UI.exchanges.connected_exchanges.map(exchange => [exchange, exchange]);
}
}, },
{ {
"type": "field_dropdown", "type": "field_dropdown",
"name": "SYM", "name": "SYM",
"options": function() { "options": targetMarketSYMOptions
return bt_data.symbols.map(symbol => [symbol, symbol]);
}
} }
], ],
"previousStatement": "trade_option", // Allow it to be used as a trade option "previousStatement": "trade_option", // Allow it to be used as a trade option
@ -310,16 +303,16 @@
"args0": [ "args0": [
{ {
"type": "field_dropdown", "type": "field_dropdown",
"name": "DIRECTION", "name": "METRIC",
"options": [ "options": [
["up", "up"], ["in profit", "profit"],
["down", "down"] ["in loss", "loss"]
] ]
} }
], ],
"output": "Boolean", "output": "StrategyMetric", // Custom output type
"colour": 230, "colour": 230,
"tooltip": "Check if the strategy is up or down.", "tooltip": "Choose to evaluate the strategy's profit or loss.",
"helpUrl": "" "helpUrl": ""
}]); }]);
Blockly.defineBlocksWithJsonArray([{ Blockly.defineBlocksWithJsonArray([{
@ -421,7 +414,43 @@
"tooltip": "Set a flag to True or False if the condition is met.", "tooltip": "Set a flag to True or False if the condition is met.",
"helpUrl": "" "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');
} }

View File

@ -16,17 +16,15 @@ class Data {
// All the indicators available. // All the indicators available.
this.indicators = bt_data.indicators; 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 // Initialize other properties
this.price_history = null; this.price_history = null;
this.indicator_data = null; this.indicator_data = null;
this.last_price = null; this.last_price = null;
this.i_updates = 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. * Initializes the Data instance by setting up connections and fetching data.
* Should be called after creating a new instance of 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('candle_close', this.candle_close.bind(this));
this.comms.registerCallback('indicator_update', this.indicator_update.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 // Open connection for streaming candle data with the exchange
this.comms.setExchangeCon(this.interval, this.trading_pair); this.comms.setExchangeCon(this.interval, this.trading_pair);

View File

@ -1,38 +1,52 @@
// Define Blockly blocks dynamically based on indicators // client/indicator_blocks.js
export function defineIndicatorBlocks() {
const indicatorOutputs = window.UI.indicators.getIndicatorOutputs();
const toolboxCategory = document.querySelector('#toolbox category[name="Indicators"]');
for (let indicatorName in indicatorOutputs) { // Define Blockly blocks and their JSON generators dynamically based on indicators
const outputs = indicatorOutputs[indicatorName]; 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 if (!toolboxCategory) {
Blockly.defineBlocksWithJsonArray([{ console.error('Indicators category not found in the toolbox.');
"type": indicatorName, return;
"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);
}
} }
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.');
}

View File

@ -1,114 +1,448 @@
// Define JSON generators for custom blocks // client/json_generators.js
export function defineJsonGenerators() {
// Initialize JSON generator export function defineJsonGenerators() {
if (!Blockly.JSON) { // Initialize the JSON generator if not already initialized
Blockly.JSON = new Blockly.Generator('JSON'); 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];
}
} }
const json = {
// JSON Generator for 'trade_action' block type: 'trade_action',
Blockly.JSON['trade_action'] = function(block) { condition: condition,
const condition = Blockly.JSON.valueToCode(block, 'CONDITION', Blockly.JSON.ORDER_ATOMIC); trade_type: tradeType,
const tradeType = block.getFieldValue('TRADE_TYPE'); size: parseFloat(size),
const size = Blockly.JSON.valueToCode(block, 'SIZE', Blockly.JSON.ORDER_ATOMIC) || null; stop_loss: stopLoss !== null ? parseFloat(stopLoss) : null,
const stopLoss = Blockly.JSON.valueToCode(block, 'STOP_LOSS', Blockly.JSON.ORDER_ATOMIC) || null; take_profit: takeProfit !== null ? parseFloat(takeProfit) : null,
const takeProfit = Blockly.JSON.valueToCode(block, 'TAKE_PROFIT', Blockly.JSON.ORDER_ATOMIC) || null; trade_options: tradeOptions
const tradeOptions = Blockly.JSON.statementToCode(block, 'TRADE_OPTIONS').trim();
const json = {
type: 'trade_action',
condition: condition,
trade_type: tradeType,
size: size,
stop_loss: stopLoss,
take_profit: takeProfit,
trade_options: tradeOptions ? JSON.parse(tradeOptions) : []
};
return JSON.stringify(json);
};
// JSON generator for 'order_type' block
Blockly.JSON['order_type'] = function(block) {
const orderType = block.getFieldValue('ORDER_TYPE');
const limitPrice = Blockly.JSON.valueToCode(block, 'LIMIT_PRICE', Blockly.JSON.ORDER_ATOMIC) || null;
const json = {
order_type: orderType,
limit_price: limitPrice
};
return JSON.stringify(json);
}; };
// JSON generator for 'time_in_force' block return JSON.stringify(json);
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');
}; };
/**
* 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.');
}

View File

@ -11,6 +11,7 @@
<script src="{{ url_for('static', filename='blockly/blockly-develop/dist/blockly_compressed.js') }}"></script> <script src="{{ url_for('static', filename='blockly/blockly-develop/dist/blockly_compressed.js') }}"></script>
<script src="{{ url_for('static', filename='blockly/blockly-develop/dist/blocks_compressed.js') }}"></script> <script src="{{ url_for('static', filename='blockly/blockly-develop/dist/blocks_compressed.js') }}"></script>
<script src="{{ url_for('static', filename='blockly/blockly-develop/dist/python_compressed.js') }}"></script> <script src="{{ url_for('static', filename='blockly/blockly-develop/dist/python_compressed.js') }}"></script>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js" crossorigin="anonymous"></script>
<!-- The server passed initiation data to the HTML. This loads it into the DOM. --> <!-- The server passed initiation data to the HTML. This loads it into the DOM. -->
<script type="text/javascript"> <script type="text/javascript">

View File

@ -139,6 +139,12 @@
<block type="take_profit"></block> <block type="take_profit"></block>
<block type="target_market"></block> <block type="target_market"></block>
</category> </category>
<!-- New category for Control Blocks -->
<category name="Control" colour="120">
<block type="entry_point"></block>
<block type="exit_point"></block>
<block type="notify_user"></block>
</category>
</xml> </xml>