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
passlib~=1.7.4
ccxt==4.4.8
flask-socketio
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:
def __init__(self):
def __init__(self, socketio):
# Object that interacts with the persistent data.
self.data = DataCache()
@ -46,10 +46,11 @@ class BrighterTrades:
self.trades.connect_exchanges(exchanges=self.exchanges)
# Object that maintains the strategies data
self.strategies = Strategies(self.data, self.trades)
self.strategies = Strategies(self.data, self.trades, self.indicators)
# Object responsible for testing trade and strategies data.
self.backtester = Backtester(data_cache=self.data, strategies=self.strategies)
self.backtester = Backtester(data_cache=self.data, strategies=self.strategies,
indicators=self.indicators, socketio=socketio)
self.backtests = {} # In-memory storage for backtests (replace with DB access in production)
def create_new_user(self, email: str, username: str, password: str) -> bool:
@ -325,29 +326,42 @@ class BrighterTrades:
Handles the creation of a new strategy based on the provided data.
:param data: A dictionary containing the attributes of the new strategy.
:return: An error message if the required attribute is missing, or the incoming data for chaining on success.
:return: A dictionary indicating success or failure with an appropriate message.
"""
# Extract user_name from the data and get user_id
user_name = data.get('user_name')
if not user_name:
return {"success": False, "message": "User not specified"}
# Validate presence of required fields
required_fields = ['user_name', 'name', 'workspace', 'code']
missing_fields = [field for field in required_fields if field not in data]
if missing_fields:
return {"success": False, "message": f"Missing fields: {', '.join(missing_fields)}"}
# Fetch the user_id using the user_name
# Extract user_name and get user_id
user_name = data.get('user_name')
user_id = self.get_user_info(user_name=user_name, info='User_id')
if not user_id:
return {"success": False, "message": "User ID not found"}
# Validate data types
if not isinstance(data['name'], str) or not data['name'].strip():
return {"success": False, "message": "Invalid or empty strategy name"}
if not isinstance(data['workspace'], str) or not data['workspace'].strip():
return {"success": False, "message": "Invalid or empty workspace data"}
if not isinstance(data['code'], list) or not data['code']:
return {"success": False, "message": "Invalid or empty strategy code"}
# Serialize code to JSON string for storage
import json
code_json = json.dumps(data['code'])
# Prepare the strategy data for insertion
strategy_data = {
"creator": user_id,
"name": data['name'],
"workspace": data['workspace'],
"code": data['code'],
"name": data['name'].strip(),
"workspace": data['workspace'].strip(),
"code": code_json,
"stats": data.get('stats', {}),
"public": data.get('public', 0), # Default to private if not specified
"fee": data.get('fee', None) # Default to None if not specified
"public": int(data.get('public', 0)),
"fee": float(data.get('fee', 0.0))
}
# Save the new strategy (in both cache and database) and return the result.
return self.strategies.new_strategy(strategy_data)
@ -651,14 +665,14 @@ class BrighterTrades:
return
def process_incoming_message(self, msg_type: str, msg_data: dict | str, socket_conn) -> dict | None:
def process_incoming_message(self, msg_type: str, msg_data: dict, socket_conn_id: str) -> dict | None:
"""
Processes an incoming message and performs the corresponding actions based on the message type and data.
:param socket_conn_id: The WebSocket connection to send updates back to the client.
:param msg_type: The type of the incoming message.
:param msg_data: The data associated with the incoming message.
:param socket_conn: The WebSocket connection to send updates back to the client.
:return: dict|None - A dictionary containing the response message and data, or None if no response is needed or
no data is found to ensure the WebSocket channel isn't burdened with unnecessary
communication.
@ -728,10 +742,17 @@ class BrighterTrades:
# Handle backtest operations
if msg_type == 'submit_backtest':
user_id = self.get_user_info(user_name=msg_data['user_name'], info='User_id')
# Pass socket_conn to the backtest handler
result = self.backtester.handle_backtest_message(user_id, msg_data, socket_conn)
return standard_reply("backtest_submitted", result)
# Validate required fields
required_fields = ['strategy', 'start_date', 'capital', 'commission', 'user_name']
if not all(field in msg_data for field in required_fields):
return standard_reply("backtest_error", {"message": "Missing required fields."})
# Delegate backtest handling to the Backtester
resp = self.backtester.handle_backtest_message(
user_id=self.get_user_info(user_name=msg_data['user_name'], info='User_id'),
msg_data=msg_data,
socket_conn_id=socket_conn_id)
return standard_reply("backtest_submitted", resp)
if msg_type == 'delete_backtest':
self.delete_backtest(msg_data)

View File

@ -7,173 +7,8 @@ from DataCache_v3 import DataCache
import datetime as dt
# class Strategy:
# def __init__(self, **args):
# """
# :param args: An object containing key_value pairs representing strategy attributes.
# Strategy format is defined in strategies.js
# """
# self.active = None
# self.type = None
# self.trade_amount = None
# self.max_position = None
# self.side = None
# self.trd_in_conds = None
# self.merged_loss = None
# self.gross_loss = None
# self.stop_loss = None
# self.take_profit = None
# self.gross_profit = None
# self.merged_profit = None
# self.name = None
# self.current_value = None
# self.opening_value = None
# self.gross_pl = None
# self.net_pl = None
# self.combined_position = None
#
# # A strategy is defined in Strategies.js it is received from the client,
# # then unpacked and converted into a python object here.
# for name, value in args.items():
# # Make each keyword-argument a specific_property of the class.
# setattr(self, name, value)
#
# # A container to hold previous state of signals.
# self.last_states = {}
#
# # A list of all the trades made by this strategy.
# self.trades = []
#
# def get_position(self):
# return self.combined_position
#
# def get_pl(self):
# self.update_pl()
# return self.net_pl
#
# def update_pl(self):
# # sum the pl of all the trades.
# position_sum = 0
# pl_sum = 0
# opening_value_sum = 0
# value_sum = 0
# for trade in self.trades:
# pl_sum += trade.profit_loss
# position_sum += trade.position_size
# value_sum += trade.value
# opening_value_sum += trade.opening_value
# self.combined_position = position_sum
# self.gross_pl = pl_sum
# self.opening_value = opening_value_sum
# self.current_value = value_sum
#
# def to_json(self):
# return json.dumps(self, default=lambda o: o.__dict__,
# sort_keys=True, indent=4)
#
# def evaluate_strategy(self, signals):
# """
# :param signals: Signals: A reference to an object that handles current signal states.
# :return action: Action required based on evaluation. format{cmd:str, amount:real, margin:int}
# """
#
# def condition_satisfied(sig_name, value):
# """
# Check if a signal has a state of value.
# :param sig_name: str: The name of a signal object to compare states.
# :param value: The state value to compare.
# :return bool: True: <Signal: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:
def __init__(self, data: DataCache, trades):
def __init__(self, data: DataCache, trades, indicators):
"""
Initializes the Strategies class.
@ -182,7 +17,8 @@ class Strategies:
"""
self.data = data # Database interaction instance
self.trades = trades
# self.strat_list = [] # List to hold strategy objects
self.strategy_contexts = {} # Dictionary to keep track of strategy contexts
self.indicators_manager = indicators
# Create a cache for strategies with necessary columns
self.data.create_cache(name='strategies',
@ -190,7 +26,8 @@ class Strategies:
size_limit=100,
eviction_policy='deny',
default_expiration=dt.timedelta(hours=24),
columns=["id", "creator", "name", "workspace", "code", "stats", "public", "fee"])
columns=["id", "creator", "name", "workspace", "code", "stats", "public", "fee",
"tbl_key", "strategy_components"])
def new_strategy(self, data: dict) -> dict:
"""
@ -202,47 +39,64 @@ class Strategies:
try:
# Check if a strategy with the same name already exists for this user
filter_conditions = [('creator', data.get('creator')), ('name', data['name'])]
existing_strategy = self.data.get_rows_from_datacache(cache_name='strategies',
filter_vals=filter_conditions)
existing_strategy = self.data.get_rows_from_datacache(
cache_name='strategies',
filter_vals=filter_conditions
)
if not existing_strategy.empty:
return {"success": False, "message": "A strategy with this name already exists"}
# Serialize complex data fields like workspace and stats
workspace_serialized = json.dumps(data['workspace']) if isinstance(data['workspace'], dict) else data[
'workspace']
stats_serialized = json.dumps(data.get('stats', {})) # Convert stats to a JSON string
# Validate and serialize 'workspace' (XML string)
workspace_data = data['workspace']
if not isinstance(workspace_data, str) or not workspace_data.strip():
return {"success": False, "message": "Invalid or empty workspace data"}
# generate a unique identifier
# Serialize 'stats' field
try:
stats_data = data.get('stats', {})
stats_serialized = json.dumps(stats_data)
except (TypeError, ValueError):
return {"success": False, "message": "Invalid stats data format"}
# Generate strategy components (code, indicators, data_sources, flags)
strategy_components = self.generate_strategy_code(data['code'])
# Add the combined strategy components to the data to be stored
data['strategy_components'] = json.dumps(strategy_components)
# Generate a unique identifier
tbl_key = str(uuid.uuid4())
# Insert the strategy into the database and cache
self.data.insert_row_into_datacache(
cache_name='strategies',
columns=("creator", "name", "workspace", "code", "stats", "public", "fee", 'tbl_key'),
columns=("creator", "name", "workspace", "code", "stats",
"public", "fee", 'tbl_key', 'strategy_components'),
values=(
data.get('creator'),
data['name'],
workspace_serialized, # Serialized workspace
data['workspace'],
data['code'],
stats_serialized, # Serialized stats
data.get('public', False),
data.get('fee', 0),
tbl_key
stats_serialized,
bool(data.get('public', 0)),
float(data.get('fee', 0.0)),
tbl_key,
data['strategy_components']
)
)
# Construct the saved strategy data to return
saved_strategy = {
"id": tbl_key, # Assuming tbl_key is used as a unique identifier
"creator": data.get('creator'),
"name": data['name'],
"workspace": data['workspace'], # Original workspace data
"workspace": workspace_data, # Original workspace data
"code": data['code'],
"stats": data.get('stats', {}),
"public": data.get('public', False),
"fee": data.get('fee', 0)
"stats": stats_data,
"public": bool(data.get('public', 0)),
"fee": float(data.get('fee', 0.0))
}
# If everything is successful, return a success message
# along with the saved strategy data
# If everything is successful, return a success message along with the saved strategy data
return {
"success": True,
"message": "Strategy created and saved successfully",
@ -251,6 +105,9 @@ class Strategies:
except Exception as e:
# Catch any exceptions and return a failure message
# Consider logging the exception with traceback for debugging
import traceback
traceback.print_exc()
return {"success": False, "message": f"Failed to create strategy: {str(e)}"}
def delete_strategy(self, user_id, name: str):
@ -277,22 +134,37 @@ class Strategies:
try:
tbl_key = data['tbl_key'] # The unique identifier for the strategy
# Serialize complex data fields like workspace and stats
workspace_serialized = json.dumps(data['workspace']) if isinstance(data['workspace'], dict) else data[
'workspace']
stats_serialized = json.dumps(data.get('stats', {})) # Convert stats to a JSON string
# Validate and serialize 'workspace' (XML string)
workspace_data = data['workspace']
if not isinstance(workspace_data, str) or not workspace_data.strip():
return {"success": False, "message": "Invalid or empty workspace data"}
# Serialize 'stats' field
try:
stats_data = data.get('stats', {})
stats_serialized = json.dumps(stats_data)
except (TypeError, ValueError):
return {"success": False, "message": "Invalid stats data format"}
# Generate updated strategy components (code, indicators, data_sources, flags)
strategy_components = self.generate_strategy_code(data['code'])
# Add the combined strategy components to the data to be stored
data['strategy_components'] = json.dumps(strategy_components)
# Prepare the columns and values for the update
columns = ("creator", "name", "workspace", "code", "stats", "public", "fee", "tbl_key")
columns = (
"creator", "name", "workspace", "code", "stats", "public", "fee", "tbl_key", "strategy_components")
values = (
data.get('creator'),
data['name'],
workspace_serialized, # Serialized workspace
workspace_data, # Use the validated workspace data
data['code'],
stats_serialized, # Serialized stats
data.get('public', False),
data.get('fee', 0),
tbl_key
bool(data.get('public', 0)),
float(data.get('fee', 0.0)),
tbl_key,
data['strategy_components'] # Serialized strategy components
)
# Update the strategy in the database and cache
@ -310,6 +182,8 @@ class Strategies:
except Exception as e:
# Handle exceptions and return failure message
import traceback
traceback.print_exc()
return {"success": False, "message": f"Failed to update strategy: {str(e)}"}
def get_all_strategy_names(self, user_id) -> list | None:
@ -376,71 +250,393 @@ class Strategies:
if filtered_strategies.empty:
return None
# Return the filtered strategy row (or a dictionary if needed)
return filtered_strategies.iloc[0].to_dict()
# Get the strategy row as a dictionary
strategy_row = filtered_strategies.iloc[0].to_dict()
def execute_cmd(self, strategy, action, cmd):
order_type = 'LIMIT'
if action == 'open_position':
# Attempt to create the trade.
status, result = self.trades.new_trade(strategy.symbol, cmd, order_type, strategy.trade_amount)
# If the trade failed.
if status == 'Error':
print(status, result)
return 'failed'
else:
# Set the active flag in strategy.
strategy.active = True
strategy.current_position += strategy.trade_amount
strategy.trades.append(result)
return 'position_opened'
# Deserialize the 'strategy_components' field
try:
strategy_components = json.loads(strategy_row.get('strategy_components', '{}'))
except json.JSONDecodeError:
strategy_components = {}
strategy_row['strategy_components'] = strategy_components
if (action == 'stop_loss') or (action == 'take_profit'):
if action == 'stop_loss':
order_type = 'MARKET'
# Attempt to create the trade.
status, result = self.trades.new_trade(strategy['symbol'], cmd, order_type, strategy['current_position'])
# If the trade failed.
if status == 'Error':
print(status, result)
return 'failed'
else:
# Set the active flag in strategy.
strategy['active'] = False
strategy['current_position'] = 0
return 'position_closed'
# If 'code' is stored as a JSON string, deserialize it
if isinstance(strategy_row.get('code'), str):
strategy_row['code'] = json.loads(strategy_row['code'])
print(f'Strategies.execute_cmd: Invalid action received: {action}')
return 'failed'
return strategy_row
def update(self, signals):
def generate_strategy_code(self, json_code):
"""
Receives a reference to updated signal data. Loops through all
published strategies and evaluates conditions against the data.
This function returns a list of strategies and action commands.
Generates the code for the 'next' method and collects indicators, data sources, and flags.
:param json_code: JSON representation of the strategy logic.
:return: A dictionary containing 'generated_code', 'indicators', 'data_sources', and 'flags_used'.
"""
def process_strategy(strategy):
action, cmd = strategy.evaluate_strategy(signals)
if action != 'do_nothing':
# Execute the command.
return {'action': action, 'result': self.execute_cmd(strategy, action, cmd)}
return {'action': 'none'}
if isinstance(json_code, str):
json_code = json.loads(json_code)
def get_stats(strategy):
position = strategy.get_position()
pl = strategy.get_pl()
stats = {'pos': position, 'pl': pl}
return stats
# Initialize code components
code_lines = []
indent_level = 1 # For 'next' method code indentation
indent = ' ' * indent_level
# Data object returned to function caller.
return_obj = {}
# Loop through all the published strategies.
# for strategy in self.strat_list:
# actions = process_strategy(strategy)
# stat_updates = get_stats(strategy)
# return_obj[strategy.name] = {'actions': actions, 'stats': stat_updates}
if len(return_obj) == 0:
return False
# Initialize sets to collect indicators, data sources, and flags
self.indicators_used = []
self.data_sources_used = set()
self.flags_used = set()
# Generate code based on the JSON structure
code_lines.append(f"def next(self):")
indent_level += 1 # Increase indent level inside the 'next' method
# Generate code from JSON nodes
code_lines.extend(self.generate_code_from_json(json_code, indent_level))
# Join the code lines into a single string
next_method_code = '\n'.join(code_lines)
# Prepare the combined dictionary
strategy_components = {
'generated_code': next_method_code,
'indicators': self.indicators_used,
'data_sources': list(self.data_sources_used),
'flags_used': list(self.flags_used)
}
return strategy_components
def generate_code_from_json(self, json_nodes, indent_level):
"""
Recursively generates Python code from JSON nodes.
:param json_nodes: The JSON nodes representing the strategy logic.
:param indent_level: Current indentation level for code formatting.
:return: A list of code lines.
"""
code_lines = []
indent = ' ' * indent_level
if isinstance(json_nodes, dict):
json_nodes = [json_nodes]
for node in json_nodes:
node_type = node.get('type')
if not node_type:
continue # Skip nodes without a type
if node_type == 'trade_action':
code_lines.extend(self.handle_trade_action(node, indent_level))
elif node_type in ['set_flag', 'notify_user']:
# Handle actions that generate code lines
if node_type == 'set_flag':
code_lines.extend(self.handle_set_flag(node, indent_level))
elif node_type == 'notify_user':
code_lines.extend(self.handle_notify_user(node, indent_level))
elif node_type == 'conditional':
# Handle conditional statements
condition_node = node.get('condition')
actions = node.get('actions', [])
condition_code = self.generate_condition_code(condition_node)
code_lines.append(f"{indent}if {condition_code}:")
# Generate code for actions inside the condition
code_lines.extend(self.generate_code_from_json(actions, indent_level + 1))
else:
# Handle other node types as needed
pass
return code_lines
def generate_condition_code(self, condition_node):
node_type = condition_node.get('type')
if not node_type:
return 'False' # Default to False if node type is missing
if node_type == 'comparison':
operator = condition_node.get('operator')
left = condition_node.get('left')
right = condition_node.get('right')
left_expr = self.generate_condition_code(left)
right_expr = self.generate_condition_code(right)
operator_map = {
'>': '>',
'<': '<',
'>=': '>=',
'<=': '<=',
'==': '==',
'!=': '!='
}
return f"({left_expr} {operator_map.get(operator, operator)} {right_expr})"
elif node_type == 'logical_and':
conditions = condition_node.get('conditions', [])
condition_exprs = [self.generate_condition_code(cond) for cond in conditions]
return ' and '.join(condition_exprs)
elif node_type == 'logical_or':
conditions = condition_node.get('conditions', [])
condition_exprs = [self.generate_condition_code(cond) for cond in conditions]
return ' or '.join(condition_exprs)
elif node_type == 'is_false':
condition = condition_node.get('condition')
condition_expr = self.generate_condition_code(condition)
return f"not ({condition_expr})"
elif node_type == 'flag_is_set':
flag_name = condition_node.get('flag_name')
self.flags_used.add(flag_name)
return f"self.flags.get('{flag_name}', False)"
elif node_type == 'strategy_profit_loss':
metric = condition_node.get('metric')
if metric == 'profit':
return 'self.is_in_profit()'
elif metric == 'loss':
return 'self.is_in_loss()'
elif node_type == 'active_trades':
return 'self.get_active_trades()'
elif node_type == 'current_balance':
return 'self.get_current_balance()'
elif node_type == 'starting_balance':
return 'self.get_starting_balance()'
elif node_type == 'value_input':
value = condition_node.get('value', 0)
return str(value)
elif node_type == 'arithmetic_operator':
operator = condition_node.get('operator')
operands = condition_node.get('operands', [])
if len(operands) == 2:
left_expr = self.generate_condition_code(operands[0])
right_expr = self.generate_condition_code(operands[1])
operator_map = {
'ADD': '+',
'SUBTRACT': '-',
'MULTIPLY': '*',
'DIVIDE': '/'
}
return f"({left_expr} {operator_map.get(operator, operator)} {right_expr})"
elif node_type == 'last_candle_value':
candle_part = condition_node.get('candle_part')
source = condition_node.get('source', {})
data_feed = self.get_data_feed(source)
return f"{data_feed}.{candle_part}[0]"
elif node_type == 'indicator':
indicator_name = condition_node.get('name')
output_field = condition_node.get('output')
# Collect the indicator information
self.indicators_used.append({
'name': indicator_name,
'output': output_field
})
# Generate code that calls process_indicator
return f"self.process_indicator('{indicator_name}', '{output_field}')"
elif node_type == 'number':
# Handle numeric values
return str(condition_node.get('value', 0))
elif node_type == 'string':
# Handle string values
return f"'{condition_node.get('value', '')}'"
# Handle other node types as needed
else:
return return_obj
return 'False' # Default to False for unhandled types
def handle_trade_action(self, node, indent_level):
code_lines = []
indent = ' ' * indent_level
action = node.get('trade_type')
condition_node = node.get('condition')
size = node.get('size', 1)
stop_loss = node.get('stop_loss')
take_profit = node.get('take_profit')
trade_options = node.get('trade_options', [])
# Generate code for the condition
if condition_node:
condition_code = self.generate_condition_code(condition_node)
code_lines.append(f"{indent}if {condition_code}:")
action_indent = indent + ' '
else:
action_indent = indent
# Prepare order parameters
order_params = [f"size={size}"]
if stop_loss is not None:
order_params.append(f"stop_loss={stop_loss}")
if take_profit is not None:
order_params.append(f"take_profit={take_profit}")
# Handle trade options
for option in trade_options:
if option.get('type') == 'order_type':
order_type = option.get('order_type', 'market')
order_params.append(f"order_type='{order_type}'")
if order_type == 'limit':
limit_price = option.get('limit_price')
if limit_price is not None:
order_params.append(f"price={limit_price}")
elif option.get('type') == 'time_in_force':
tif = option.get('tif')
if tif:
order_params.append(f"tif='{tif}'")
elif option.get('type') == 'target_market':
tf = option.get('timeframe')
exc = option.get('exchange')
sym = option.get('symbol')
order_params.append(f"timeframe='{tf}'")
order_params.append(f"exchange='{exc}'")
order_params.append(f"symbol='{sym}'")
self.data_sources_used.add((exc, sym, tf))
# Handle other trade options
params_str = ', '.join(order_params)
code_lines.append(f"{action_indent}self.{action}({params_str})")
return code_lines
def handle_set_flag(self, node, indent_level):
code_lines = []
indent = ' ' * indent_level
condition_node = node.get('condition')
flag_name = node.get('flag_name')
flag_value = node.get('flag_value', 'True')
condition_code = self.generate_condition_code(condition_node)
code_lines.append(f"{indent}if {condition_code}:")
code_lines.append(f"{indent} self.flags['{flag_name}'] = {flag_value}")
return code_lines
def handle_notify_user(self, node, indent_level):
code_lines = []
indent = ' ' * indent_level
message = node.get('message', 'No message provided.')
code_lines.append(f"{indent}self.notify_user('{message}')")
return code_lines
def get_data_feed(self, source):
timeframe = source.get('timeframe', 'default')
exchange = source.get('exchange', 'default')
symbol = source.get('symbol', 'default')
source_key = f"{exchange}_{symbol}_{timeframe}"
self.data_sources_used.add((exchange, symbol, timeframe))
return f"self.datas['{source_key}']"
def execute_strategy(self, strategy_data):
"""
Executes the given strategy in live trading.
:param strategy_data: The data for the strategy to execute.
"""
strategy_id = strategy_data.get('tbl_key')
strategy_name = strategy_data.get('name')
user_id = strategy_data['creator']
# Get the strategy components
strategy_components = strategy_data.get('strategy_components')
if isinstance(strategy_components, str):
strategy_components = json.loads(strategy_components)
generated_code = strategy_components.get('generated_code')
# Prepare the execution context
if strategy_id not in self.strategy_contexts:
# Initialize the context for this strategy
context = {
'flags': {},
'starting_balance': self.trades.get_current_balance(user_id),
'indicators_used': strategy_components.get('indicators', []),
'strategy_data': strategy_data
}
self.strategy_contexts[strategy_id] = context
else:
context = self.strategy_contexts[strategy_id]
# Define the local functions and variables needed by the generated code
def process_indicator(indicator_name, output_field):
# Get the latest indicator value using indicators_manager
indicator_def = next((ind for ind in context['indicators_used'] if ind['name'] == indicator_name), None)
if indicator_def is None:
return None
# Assuming indicators_manager.process_indicator returns a DataFrame with the latest values
indicator_df = self.indicators_manager.process_indicator(indicator_def)
if indicator_df is not None and not indicator_df.empty:
return indicator_df.iloc[-1][output_field]
else:
return None
def buy(size=1, price=None, order_type='market', symbol=None, **kwargs):
# Format the data for CCXT
order_data = {
'symbol': symbol or strategy_data.get('symbol', 'BTC/USDT'),
'type': order_type,
'side': 'buy',
'amount': size,
'price': price,
# Include other parameters as needed
}
# Call self.trades.buy with the order data
self.trades.buy(order_data)
def sell(size=1, price=None, order_type='market', symbol=None, **kwargs):
# Format the data for CCXT
order_data = {
'symbol': symbol or strategy_data.get('symbol', 'BTC/USDT'),
'type': order_type,
'side': 'sell',
'amount': size,
'price': price,
# Include other parameters as needed
}
# Call self.trades.sell with the order data
self.trades.sell(order_data)
# Implement other helper methods as needed
def is_in_profit():
# Implement logic to determine if the strategy is in profit
return False # Placeholder
def is_in_loss():
# Implement logic to determine if the strategy is in loss
return False # Placeholder
def get_active_trades():
# Return the number of active trades
return len(self.trades.get_active_trades())
def get_current_balance():
# Return the current balance from self.trades
return self.trades.get_balance()
def notify_user(message):
# Implement notification logic
print(f"Notification: {message}")
# Prepare the local namespace for exec
local_vars = {
'process_indicator': process_indicator,
'buy': buy,
'sell': sell,
'is_in_profit': is_in_profit,
'is_in_loss': is_in_loss,
'get_active_trades': get_active_trades,
'get_current_balance': get_current_balance,
'notify_user': notify_user,
'flags': context['flags'],
'starting_balance': context['starting_balance'],
# Include any other variables or functions needed
}
# Execute the generated code
try:
exec(generated_code, {}, local_vars)
except Exception as e:
print(f"Error executing strategy {strategy_name}: {e}")
def update(self):
"""
Loops through and executes all activated strategies.
"""
active_strategies = self.data.get_rows_from_datacache('strategies', [('active', True)])
if active_strategies.empty:
return # No active strategies to execute
for _, strategy_data in active_strategies.iterrows():
self.execute_strategy(strategy_data)

View File

@ -1,25 +1,34 @@
import json
import logging
# app.py
from flask import Flask, render_template, request, redirect, jsonify, session, flash
from flask_cors import CORS
from flask_sock import Sock
from email_validator import validate_email, EmailNotValidError
# Monkey patching must occur before other imports
import eventlet
eventlet.monkey_patch() # noqa: E402
# Handles all updates and requests for locally stored data.
from BrighterTrades import BrighterTrades
# Standard library imports
import logging # noqa: E402
# import json # noqa: E402
# import datetime as dt # noqa: E402
# Third-party imports
from flask import Flask, render_template, request, redirect, jsonify, session, flash # noqa: E402
from flask_cors import CORS # noqa: E402
from flask_socketio import SocketIO, emit, join_room, leave_room, disconnect # noqa: E402
from email_validator import validate_email, EmailNotValidError # noqa: E402
# Local application imports
from BrighterTrades import BrighterTrades # noqa: E402
# Set up logging
logging.basicConfig(level=logging.DEBUG)
# Create a BrighterTrades object. This the main application that maintains access to the server, local storage,
# and manages objects that process trade data.
brighter_trades = BrighterTrades()
# Create a Flask object named app that serves the html.
app = Flask(__name__)
# Create a socket in order to receive requests.
sock = Sock(app)
socketio = SocketIO(app, async_mode='eventlet')
# Create a BrighterTrades object. This the main application that maintains access to the server, local storage,
# and manages objects that process trade data.
brighter_trades = BrighterTrades(socketio)
# Set server configuration globals.
CORS_HEADERS = 'Content-Type'
@ -102,49 +111,49 @@ def index():
open_orders=rendered_data['open_orders'])
@sock.route('/ws')
def ws(socket_conn):
@socketio.on('connect')
def handle_connect():
user_name = request.args.get('user_name')
if user_name and brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'):
# Join a room specific to the user for targeted messaging
room = user_name # You can choose an appropriate room naming strategy
join_room(room)
emit('message', {'reply': 'connected', 'data': 'Connection established'})
else:
emit('message', {'reply': 'error', 'data': 'User not authenticated'})
# Disconnect the client if not authenticated
disconnect()
@socketio.on('message')
def handle_message(data):
"""
Open a WebSocket to handle two-way communication with UI without browser refreshes.
Handle incoming JSON messages with authentication.
"""
# Validate input
if 'message_type' not in data or 'data' not in data:
emit('message', {"success": False, "message": "Invalid message format."})
return
def json_msg_received(msg_obj):
"""
Handle incoming JSON messages with authentication.
"""
# Validate input
if 'message_type' not in msg_obj or 'data' not in msg_obj:
return
msg_type, msg_data = data['message_type'], data['data']
msg_type, msg_data = msg_obj['message_type'], msg_obj['data']
# Extract user_name from the incoming message data
user_name = msg_data.get('user_name')
if not user_name:
emit('message', {"success": False, "message": "User not specified"})
return
# Extract user_name from the incoming message data
user_name = msg_data.get('user_name')
if not user_name:
socket_conn.send(json.dumps({"success": False, "message": "User not specified"}))
return
# Check if the user is logged in
if not brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'):
emit('message', {"success": False, "message": "User not logged in"})
return
# Check if the user is logged in
if not brighter_trades.get_user_info(user_name=user_name, info='Is logged in?'):
socket_conn.send(json.dumps({"success": False, "message": "User not logged in"}))
return
# Process the incoming message based on the type
resp = brighter_trades.process_incoming_message(msg_type=msg_type, msg_data=msg_data, socket_conn_id=request.sid)
# Process the incoming message based on the type, passing socket_conn
resp = brighter_trades.process_incoming_message(msg_type=msg_type, msg_data=msg_data, socket_conn=socket_conn)
# Send the response back to the client
if resp:
socket_conn.send(json.dumps(resp))
# Main loop to receive messages and handle them
while True:
msg = socket_conn.receive()
if msg:
try:
json_msg = json.loads(msg)
json_msg_received(json_msg)
except json.JSONDecodeError:
print(f'Msg received from client (not JSON): {msg}')
# Send the response back to the client
if resp:
emit('message', resp)
@app.route('/settings', methods=['POST'])
@ -338,9 +347,11 @@ def indicator_init():
# Get the indicator data
source = {'user_name': username, 'market': chart_view}
data = brighter_trades.get_indicator_data(user_name=username, source=source, start_ts=None, num_results=1000)
# indicators={'EMA 5': {'visible': true, 'type': 'EMA', 'color': 'red' },'vol': {'visible': true, 'type': 'Volume'},'New Indicator': {'visible': true, 'type': 'nothing'}}
# indicators={'EMA 5': {'visible': true, 'type': 'EMA', 'color': 'red' },
# 'vol': {'visible': true, 'type': 'Volume'},'New Indicator': {'visible': true, 'type': 'nothing'}}
return jsonify(data), 200
if __name__ == '__main__':
app.run(debug=False, use_reloader=False)
socketio.run(app, host='127.0.0.1', port=5000, debug=False, use_reloader=False)

View File

@ -1,30 +1,24 @@
import ast
import json
import re
import backtrader as bt
import datetime as dt
from DataCache_v3 import DataCache
from Strategies import Strategies
import threading
from indicators import Indicators
import numpy as np
import pandas as pd
class Backtester:
def __init__(self, data_cache: DataCache, strategies: Strategies):
def __init__(self, data_cache: DataCache, strategies: Strategies, indicators: Indicators, socketio):
""" Initialize the Backtesting class with a cache for back-tests """
self.data_cache = data_cache
self.strategies = strategies
self.indicators_manager = indicators
self.socketio = socketio
# Create a cache for storing back-tests
self.data_cache.create_cache('tests', cache_type='row', size_limit=100,
default_expiration=dt.timedelta(days=1),
eviction_policy='evict')
def get_default_chart_view(self, user_name):
"""Fetch default chart view if no specific source is provided."""
return self.data_cache.get_datacache_item(
item_name='chart_view', cache_name='users', filter_vals=('user_name', user_name))
def cache_backtest(self, user_name, backtest_name, backtest_data):
""" Cache the backtest data for a user """
columns = ('user_name', 'strategy_name', 'start_time', 'capital', 'commission', 'results')
@ -39,164 +33,162 @@ class Backtester:
cache_key = f"backtest:{user_name}:{backtest_name}"
self.data_cache.insert_row_into_cache('tests', columns, values, key=cache_key)
def map_user_strategy(self, user_strategy):
def map_user_strategy(self, user_strategy, precomputed_indicators):
"""Maps user strategy details into a Backtrader-compatible strategy class."""
# Extract the generated code and indicators from the strategy components
strategy_components = user_strategy['strategy_components']
generated_code = strategy_components['generated_code']
indicators_used = strategy_components['indicators']
# Define the strategy class dynamically
class MappedStrategy(bt.Strategy):
params = (
('initial_cash', user_strategy['params'].get('initial_cash', 10000)),
('commission', user_strategy['params'].get('commission', 0.001)),
)
def __init__(self):
# Extract unique sources (exchange, symbol, timeframe) from blocks
self.sources = self.extract_sources(user_strategy)
self.precomputed_indicators = precomputed_indicators
self.indicator_pointers = {}
self.indicator_names = list(precomputed_indicators.keys())
self.current_step = 0
# Map of source to data feed (used later in next())
self.source_data_feed_map = {}
# Initialize pointers for each indicator
for name in self.indicator_names:
self.indicator_pointers[name] = 0 # Start at the first row
def extract_sources(self, user_strategy):
"""Extracts unique sources from the strategy."""
sources = []
for block in user_strategy.get('blocks', []):
if block.get('type') in ['last_candle_value', 'trade_action']:
source = self.extract_source_from_block(block)
if source and source not in sources:
sources.append(source)
elif block.get('type') == 'target_market':
target_source = self.extract_target_market(block)
if target_source and target_source not in sources:
sources.append(target_source)
return sources
# Initialize any other needed variables
self.flags = {}
self.starting_balance = self.broker.getvalue()
def extract_source_from_block(self, block):
"""Extract source (exchange, symbol, timeframe) from a strategy block."""
source = {}
if block.get('type') == 'last_candle_value':
source = block.get('SOURCE', None)
# If SOURCE is missing, use the trade target or default
if not source:
source = self.get_default_chart_view(self.user_name) # Fallback to default
return source
def process_indicator(self, indicator_name, output_field):
# Get the DataFrame for the indicator
df = self.precomputed_indicators[indicator_name]
def extract_target_market(self, block):
"""Extracts target market data (timeframe, exchange, symbol) from the trade_action block."""
target_market = block.get('target_market', {})
return {
'timeframe': target_market.get('TF', '5m'),
'exchange': target_market.get('EXC', 'Binance'),
'symbol': target_market.get('SYM', 'BTCUSD')
}
# Get the current index for the indicator
idx = self.indicator_pointers[indicator_name]
if idx >= len(df):
return None # No more data
# Get the specific output value
if output_field in df.columns:
value = df.iloc[idx][output_field]
if pd.isna(value):
return None # Handle NaN values
return value
else:
return None # Output field not found
def next(self):
"""Execute trading logic using the compiled strategy."""
# Increment pointers
for name in self.indicator_names:
self.indicator_pointers[name] += 1
# Increment current step
self.current_step += 1
# Generated strategy logic
try:
exec(self.compiled_logic, {'self': self, 'data_feeds': self.source_data_feed_map})
# Execute the generated code
exec(generated_code)
except Exception as e:
print(f"Error executing trading logic: {e}")
print(f"Error in strategy execution: {e}")
return MappedStrategy
def prepare_data_feed(self, start_date: str, sources: list, user_name: str):
def prepare_data_feed(self, start_date: str, source: dict):
"""
Prepare multiple data feeds based on the start date and list of sources.
Prepare the main data feed based on the start date and source.
"""
try:
# Convert the start date to a datetime object
start_dt = dt.datetime.strptime(start_date, '%Y-%m-%dT%H:%M')
# Dictionary to map each source to its corresponding data feed
data_feeds = {}
# Ensure exchange details contain required keys (fallback if missing)
timeframe = source.get('timeframe', '1h')
exchange = source.get('exchange', 'Binance')
symbol = source.get('symbol', 'BTCUSDT')
for source in sources:
# Ensure exchange details contain required keys (fallback if missing)
asset = source.get('asset', 'BTCUSD')
timeframe = source.get('timeframe', '5m')
exchange = source.get('exchange', 'Binance')
# Fetch OHLC data from DataCache based on the source
data = self.data_cache.get_records_since(start_datetime=start_dt, ex_details=[symbol, timeframe, exchange])
# Fetch OHLC data from DataCache based on the source
ex_details = [asset, timeframe, exchange, user_name]
data = self.data_cache.get_records_since(start_dt, ex_details)
# Return the data as a Pandas DataFrame compatible with Backtrader
data_feeds[tuple(ex_details)] = data
return data_feeds
return data
except Exception as e:
print(f"Error preparing data feed: {e}")
return None
def run_backtest(self, strategy, data_feed_map, msg_data, user_name, callback, socket_conn):
def precompute_indicators(self, indicators_definitions, data_feed):
"""
Runs a backtest using Backtrader on a separate thread and calls the callback with the results when finished.
Also sends progress updates to the client via WebSocket.
Precompute indicator values and return a dictionary of DataFrames.
"""
precomputed_indicators = {}
total_candles = len(data_feed)
for indicator_def in indicators_definitions:
indicator_name = indicator_def['name']
# Compute the indicator values
indicator_df = self.indicators_manager.process_indicator(indicator=indicator_def,
num_results=total_candles)
# Ensure the DataFrame has a consistent index
indicator_df.reset_index(drop=True, inplace=True)
precomputed_indicators[indicator_name] = indicator_df
return precomputed_indicators
def run_backtest(self, strategy_class, data_feed, msg_data, user_name, callback, socket_conn_id):
"""
Runs a backtest using Backtrader and uses Flask-SocketIO's background tasks.
Sends progress updates to the client via WebSocket.
"""
def execute_backtest():
cerebro = bt.Cerebro()
try:
cerebro = bt.Cerebro()
# Add the mapped strategy to the backtest
cerebro.addstrategy(strategy)
# Add the mapped strategy to the backtest
cerebro.addstrategy(strategy_class)
# Add all the data feeds to Cerebro
total_bars = 0 # Total number of data points (bars) across all feeds
for source, data_feed in data_feed_map.items():
# Add the main data feed to Cerebro
# noinspection PyArgumentList
bt_feed = bt.feeds.PandasData(dataname=data_feed)
cerebro.adddata(bt_feed)
strategy.source_data_feed_map[source] = bt_feed
total_bars = max(total_bars, len(data_feed)) # Get the total bars from the largest feed
# Capture initial capital
initial_capital = cerebro.broker.getvalue()
# Set initial capital and commission
initial_cash = msg_data.get('capital', 10000)
cerebro.broker.setcash(initial_cash)
commission = msg_data.get('commission', 0.001)
cerebro.broker.setcommission(commission=commission)
# Progress tracking variables
current_bar = 0
last_progress = 0
# Run the backtest
print("Running backtest...")
start_time = dt.datetime.now()
cerebro.run()
end_time = dt.datetime.now()
# Custom next function to track progress (if you have a large dataset)
def track_progress():
nonlocal current_bar, last_progress
current_bar += 1
progress = (current_bar / total_bars) * 100
# Extract performance metrics
final_value = cerebro.broker.getvalue()
run_duration = (end_time - start_time).total_seconds()
# Send progress update every 10% increment
if progress >= last_progress + 10:
last_progress += 10
socket_conn.send(json.dumps({"progress": int(last_progress)}))
# Send 100% completion
self.socketio.emit('progress_update', {"progress": 100}, room=socket_conn_id)
# Attach the custom next method to the strategy
strategy.next = track_progress
# Prepare the results to pass into the callback
backtest_results = {
"initial_capital": initial_cash,
"final_portfolio_value": final_value,
"run_duration": run_duration
}
# Run the backtest
print("Running backtest...")
start_time = dt.datetime.now()
cerebro.run()
end_time = dt.datetime.now()
callback(backtest_results)
# Extract performance metrics
final_value = cerebro.broker.getvalue()
run_duration = (end_time - start_time).total_seconds()
except Exception as e:
# Handle exceptions and send error messages to the client
error_message = f"Backtest execution failed: {str(e)}"
self.socketio.emit('backtest_error', {"message": error_message}, room=socket_conn_id)
print(f"[BACKTEST ERROR] {error_message}")
# Send 100% completion
socket_conn.send(json.dumps({"progress": 100}))
# Start the backtest as a background task
self.socketio.start_background_task(execute_backtest)
# Prepare the results to pass into the callback
callback({
"initial_capital": initial_capital,
"final_portfolio_value": final_value,
"run_duration": run_duration
})
# Map the user strategy and prepare the data feeds
sources = strategy.extract_sources()
data_feed_map = self.prepare_data_feed(msg_data['start_date'], sources, user_name)
# Run the backtest in a separate thread
thread = threading.Thread(target=execute_backtest)
thread.start()
def handle_backtest_message(self, user_id, msg_data, socket_conn):
def handle_backtest_message(self, user_id, msg_data, socket_conn_id):
user_name = msg_data.get('user_name')
backtest_name = f"{msg_data['strategy']}_backtest"
@ -210,55 +202,43 @@ class Backtester:
if not user_strategy:
return {"error": f"Strategy {strategy_name} not found for user {user_name}"}
# Extract sources from the strategy JSON
sources = self.extract_sources_from_strategy_json(user_strategy.get('strategy_json'))
# Extract the main data source from the strategy components
strategy_components = user_strategy['strategy_components']
data_sources = strategy_components['data_sources']
if not sources:
return {"error": "No valid sources found in the strategy."}
if not data_sources:
return {"error": "No valid data sources found in the strategy."}
# Prepare the data feed map based on extracted sources
data_feed_map = self.prepare_data_feed(msg_data['start_date'], sources, user_name)
# For simplicity, use the first data source as the main data feed
main_source = data_sources[0]
if data_feed_map is None:
# Prepare the main data feed
data_feed = self.prepare_data_feed(msg_data['start_date'], main_source)
if data_feed is None:
return {"error": "Data feed could not be prepared. Please check the data source."}
# Precompute indicator values
indicators_definitions = strategy_components['indicators']
precomputed_indicators = self.precompute_indicators(indicators_definitions, data_feed)
# Map the user strategy to a Backtrader strategy class
mapped_strategy = self.map_user_strategy(user_strategy)
mapped_strategy_class = self.map_user_strategy(user_strategy, precomputed_indicators)
# Define the callback function to handle backtest completion
def backtest_callback(results):
self.store_backtest_results(user_name, backtest_name, results)
self.update_strategy_stats(user_id, strategy_name, results)
# Run the backtest and pass the callback function, msg_data, and user_name
self.run_backtest(mapped_strategy, data_feed_map, msg_data, user_name, backtest_callback, socket_conn)
# Emit the results back to the client
self.socketio.emit('backtest_results', {"test_id": backtest_name, "results": results}, room=socket_conn_id)
print(f"[BACKTEST COMPLETE] Results emitted to user '{user_name}'.")
# Run the backtest asynchronously
self.run_backtest(mapped_strategy_class, data_feed, msg_data, user_name, backtest_callback, socket_conn_id)
return {"reply": "backtest_started"}
def extract_sources_from_strategy_json(self, strategy_json):
sources = []
# Parse the JSON strategy to extract sources
def traverse_blocks(blocks):
for block in blocks:
if block['type'] == 'source':
source = {
'timeframe': block['fields'].get('TF'),
'exchange': block['fields'].get('EXC'),
'symbol': block['fields'].get('SYM')
}
sources.append(source)
# Recursively traverse inputs and statements
if 'inputs' in block:
traverse_blocks(block['inputs'].values())
if 'statements' in block:
traverse_blocks(block['statements'].values())
if 'next' in block:
traverse_blocks([block['next']])
traverse_blocks(strategy_json)
return sources
def update_strategy_stats(self, user_id, strategy_name, results):
""" Update the strategy stats with the backtest results """
strategy = self.strategies.get_strategy_by_name(user_id=user_id, name=strategy_name)

View File

@ -1,4 +1,36 @@
"""
set_cache_item
create_cache
"""
# test_backtrader_pandasdata.py
import backtrader as bt
import pandas as pd
# Sample DataFrame
data_feed = pd.DataFrame({
'datetime': pd.date_range(start='2021-01-01', periods=5, freq='D'),
'open': [100, 101, 102, 103, 104],
'high': [105, 106, 107, 108, 109],
'low': [95, 96, 97, 98, 99],
'close': [102, 103, 104, 105, 106],
'volume': [1000, 1010, 1020, 1030, 1040]
})
# Convert 'datetime' to datetime objects and set as index
data_feed['datetime'] = pd.to_datetime(data_feed['datetime'])
data_feed.set_index('datetime', inplace=True)
# Define a simple strategy
class TestStrategy(bt.Strategy):
def next(self):
pass
cerebro = bt.Cerebro()
cerebro.addstrategy(TestStrategy)
# Add data feed using Backtrader's PandasData
# noinspection PyArgumentList
bt_feed = bt.feeds.PandasData(dataname=data_feed)
cerebro.adddata(bt_feed)
# Run backtest
cerebro.run()
print("Backtest completed successfully.")

View File

@ -28,7 +28,7 @@ class StratUIManager {
* @param {string} action - The action to perform ('new' or 'edit').
* @param {string|null} strategyData - The data of the strategy to edit (only applicable for 'edit' action).
*/
displayForm(action, strategyData = null) {
async displayForm(action, strategyData = null) {
console.log(`Opening form for action: ${action}, strategy: ${strategyData?.name}`);
if (this.formElement) {
const headerTitle = this.formElement.querySelector("#draggable_header h1");
@ -63,11 +63,14 @@ class StratUIManager {
// Display the form
this.formElement.style.display = "grid";
// Call the workspace manager to initialize the Blockly workspace after the form becomes visible
// Initialize Blockly workspace after the form becomes visible
if (UI.strats && UI.strats.workspaceManager) {
setTimeout(() => {
UI.strats.workspaceManager.initWorkspace();
}, 100); // Delay slightly to allow the form to render properly
try {
await UI.strats.workspaceManager.initWorkspace();
console.log("Blockly workspace initialized.");
} catch (error) {
console.error("Failed to initialize Blockly workspace:", error);
}
} else {
console.error("Workspace manager is not initialized or is unavailable.");
}
@ -76,7 +79,6 @@ class StratUIManager {
}
}
/**
* Hides the "Create New Strategy" form by adding a 'hidden' class.
*/
@ -264,7 +266,7 @@ class StratWorkspaceManager {
* @async
* @throws {Error} If required elements ('blocklyDiv' or 'toolbox') are not found.
*/
initWorkspace() {
async initWorkspace() {
if (!document.getElementById('blocklyDiv')) {
console.error("blocklyDiv is not loaded.");
return;
@ -275,25 +277,24 @@ class StratWorkspaceManager {
}
// Initialize custom blocks and Blockly workspace
this._loadModulesAndInitWorkspace();
await this._loadModulesAndInitWorkspace();
}
async _loadModulesAndInitWorkspace() {
if (!this.blocksDefined) {
try {
// Load all modules concurrently to reduce loading time
const [customBlocksModule, indicatorBlocksModule, pythonGeneratorsModule, jsonGeneratorsModule] = await Promise.all([
import('./custom_blocks.js'),
import('./indicator_blocks.js'),
import('./python_generators.js'),
import('./json_generators.js')
]);
// Define custom blocks
customBlocksModule.defineCustomBlocks();
indicatorBlocksModule.defineIndicatorBlocks();
pythonGeneratorsModule.definePythonGenerators();
// Load and define JSON generators first
const jsonGeneratorsModule = await import('./json_generators.js');
jsonGeneratorsModule.defineJsonGenerators();
// Load and define custom blocks
const customBlocksModule = await import('./custom_blocks.js');
customBlocksModule.defineCustomBlocks();
// Load and define indicator blocks
const indicatorBlocksModule = await import('./indicator_blocks.js');
indicatorBlocksModule.defineIndicatorBlocks();
} catch (error) {
console.error("Error loading Blockly modules: ", error);
return;
@ -326,6 +327,7 @@ class StratWorkspaceManager {
scaleSpeed: 1.2
}
});
console.log('Blockly workspace initialized and modules loaded.');
}
/**
@ -358,18 +360,17 @@ class StratWorkspaceManager {
const strategyName = nameElement.value;
// Initialize code generators
Blockly.Python.init(this.workspace);
Blockly.JSON.init(this.workspace);
// Generate code and data representations
const pythonCode = Blockly.Python.workspaceToCode(this.workspace);
const strategyJson = this._generateStrategyJsonFromWorkspace();
// Generate workspace XML for restoration when editing
const workspaceXml = Blockly.Xml.workspaceToDom(this.workspace);
const workspaceXmlText = Blockly.Xml.domToText(workspaceXml);
return JSON.stringify({
name: strategyName,
code: pythonCode,
strategy_json: strategyJson,
workspace: workspaceXmlText
});
@ -399,6 +400,7 @@ class StratWorkspaceManager {
statements: {}
};
// Capture all fields in the block
block.inputList.forEach(input => {
if (input.fieldRow) {
input.fieldRow.forEach(field => {
@ -407,19 +409,32 @@ class StratWorkspaceManager {
}
});
}
});
// Capture all connected blocks
block.inputList.forEach(input => {
if (input.connection && input.connection.targetBlock()) {
const targetBlock = input.connection.targetBlock();
if (input.type === Blockly.INPUT_VALUE) {
json.inputs[input.name] = this._blockToJson(targetBlock);
} else if (input.type === Blockly.NEXT_STATEMENT) {
json.statements[input.name] = this._blockToJson(targetBlock);
// Handle multiple statement connections if applicable
const connectedBlocks = [];
let currentBlock = targetBlock;
while (currentBlock) {
connectedBlocks.push(this._blockToJson(currentBlock));
currentBlock = currentBlock.getNextBlock();
}
json.statements[input.name] = connectedBlocks;
}
}
});
// Handle the next connected block at the same level
if (block.getNextBlock()) {
json.next = this._blockToJson(block.getNextBlock());
const nextBlock = this._blockToJson(block.getNextBlock());
// Assuming only one 'next' block; adjust if multiple are possible
json.next = nextBlock;
}
return json;
@ -468,6 +483,9 @@ class Strategies {
// Set the delete callback
this.uiManager.registerDeleteStrategyCallback(this.deleteStrategy.bind(this));
// Bind the submitStrategy method to ensure correct 'this' context
this.submitStrategy = this.submitStrategy.bind(this);
}
/**
@ -639,42 +657,44 @@ class Strategies {
return;
}
let strategyObject;
let strategyData;
try {
strategyObject = JSON.parse(this.generateStrategyJson());
// Compile the strategy JSON (conditions and actions)
const compiledStrategy = this.generateStrategyJson(); // Returns JSON string
const parsedStrategy = JSON.parse(compiledStrategy); // Object with 'name', 'strategy_json', 'workspace'
// Prepare the strategy data to send
strategyData = {
code: parsedStrategy.strategy_json, // The compiled strategy JSON string
workspace: parsedStrategy.workspace, // Serialized workspace XML
name: nameBox.value.trim(),
fee: parseFloat(feeBox.value.trim()),
public: publicCheckbox.checked ? 1 : 0,
user_name: this.data.user_name
// Add 'stats' if necessary
};
} catch (error) {
console.error('Failed to parse strategy JSON:', error);
console.error('Failed to compile strategy JSON:', error);
alert('An error occurred while processing the strategy data.');
return;
}
const feeValue = feeBox.value.trim();
const fee = parseFloat(feeValue);
if (isNaN(fee) || fee < 0) {
// Basic client-side validation
if (isNaN(strategyData.fee) || strategyData.fee < 0) {
alert("Please enter a valid, non-negative number for the fee.");
return;
}
const strategyName = nameBox.value.trim();
if (!strategyName) {
if (!strategyData.name) {
alert("Please provide a name for the strategy.");
return;
}
const is_public = publicCheckbox.checked ? 1 : 0;
// Add user_name, fee, and public fields to the strategy object
const strategyData = {
...strategyObject,
user_name: this.data.user_name,
fee,
public: is_public
};
// Determine if this is a new strategy or an edit
const messageType = action === 'new' ? 'new_strategy' : 'edit_strategy';
// Send the strategy data to the server
if (this.comms) {
// Determine message type based on action
const messageType = action === 'new' ? 'new_strategy' : 'edit_strategy';
this.comms.sendToApp(messageType, strategyData);
this.uiManager.hideForm();
} else {

View File

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

View File

@ -1,13 +1,88 @@
class Comms {
constructor() {
constructor(userName) {
if (!userName) {
console.error('Comms: Cannot initialize Socket.IO without user_name.');
return;
}
this.connectionOpen = false;
this.appCon = null; // WebSocket connection for app communication
this.socket = null; // Socket.IO client instance
this.eventHandlers = {}; // Event handlers for message types
// Callback collections that will receive various updates.
this.candleUpdateCallbacks = [];
this.candleCloseCallbacks = [];
this.indicatorUpdateCallbacks = [];
// Initialize the message queue
this.messageQueue = [];
// Save the userName
this.userName = userName;
// Initialize the socket
this._initializeSocket();
}
/**
* Initialize the Socket.IO connection.
*/
_initializeSocket() {
// Initialize Socket.IO client with query parameter
this.socket = io('http://127.0.0.1:5000', {
query: { 'user_name': this.userName },
transports: ['websocket'], // Optional: Force WebSocket transport
autoConnect: true,
reconnectionAttempts: 5, // Optional: Number of reconnection attempts
reconnectionDelay: 1000 // Optional: Delay between reconnections
});
// Handle connection events
this.socket.on('connect', () => {
console.log('Socket.IO: Connected to server');
this.connectionOpen = true;
// Flush the message queue
this._flushMessageQueue();
});
this.socket.on('disconnect', (reason) => {
console.log(`Socket.IO: Disconnected from server. Reason: ${reason}`);
this.connectionOpen = false;
});
this.socket.on('connect_error', (error) => {
console.error('Socket.IO: Connection error:', error);
});
// Handle incoming messages
this.socket.on('message', (data) => {
if (data.reply === 'connected') {
console.log('Socket.IO: Connection established:', data.data);
} else if (data.reply === 'error') {
console.error('Socket.IO: Authentication error:', data.data);
// Optionally, handle authentication errors (e.g., redirect to login)
} else {
// Emit the event to registered handlers
this.emit(data.reply, data.data);
}
});
}
/**
* Flushes the message queue by sending all queued messages.
*/
_flushMessageQueue() {
while (this.messageQueue.length > 0) {
const { messageType, data } = this.messageQueue.shift();
this.socket.emit('message', {
message_type: messageType,
data: {
...data,
user_name: this.userName
}
});
console.log(`Comms: Sent queued message-> ${JSON.stringify({ messageType, data })}`);
}
}
/**
@ -114,7 +189,7 @@ class Comms {
*/
async getIndicatorData(userName) {
try {
const response = await fetch('http://localhost:5000/api/indicator_init', {
const response = await fetch('http://127.0.0.1:5000/api/indicator_init', { // Changed to use same host
credentials: 'same-origin',
mode: 'cors',
method: 'POST',
@ -172,8 +247,8 @@ class Comms {
}
/**
* Sends a request to update an indicator's properties.
* @param {Object} indicatorData - An object containing the updated properties of the indicator.
* Sends a request to create a new indicator.
* @param {Object} indicatorData - An object containing the properties of the new indicator.
* @returns {Promise<Object>} - The response from the server.
*/
async submitIndicator(indicatorData) {
@ -188,87 +263,40 @@ class Comms {
});
return await response.json();
} catch (error) {
console.error('Error updating indicator:', error);
console.error('Error creating indicator:', error);
return { success: false };
}
}
/**
* Sends a message to the application server via WebSocket.
* Automatically includes the user_name from `window.UI.data.user_name` for authentication.
* Sends a message to the application server via Socket.IO.
* @param {string} messageType - The type of the message.
* @param {Object} data - The data to be sent with the message.
*/
sendToApp(messageType, data) {
const user_name = window.UI.data.user_name; // Access user_name from window.UI.data
if (!user_name) {
console.error('User not logged in. Cannot send message.');
return;
}
const messageData = {
message_type: messageType,
data: {
...data, // Include the existing data
user_name: user_name // Add user_name for authentication
...data,
user_name: this.userName
}
};
console.log('Comms: Sending->', JSON.stringify(messageData));
if (this.connectionOpen) {
this.appCon.send(JSON.stringify(messageData));
if (this.connectionOpen && this.socket) {
this.socket.emit('message', messageData);
} else {
setTimeout(() => {
if (this.appCon) {
this.appCon.send(JSON.stringify(messageData));
}
}, 1000);
// Not an error; message will be queued
console.warn('Socket.IO connection is not open. Queuing message.');
// Queue the message to be sent once connected
this.messageQueue.push({ messageType, data });
console.warn(`Comms: Queued message-> ${JSON.stringify({ messageType, data })} (Connection not open)`);
}
}
setAppCon() {
this.appCon = new WebSocket('ws://localhost:5000/ws');
// On connection open
this.appCon.onopen = () => {
console.log("WebSocket connection established");
this.appCon.send("Connection OK");
this.connectionOpen = true;
};
// Handle incoming messages
this.appCon.addEventListener('message', (event) => {
if (event.data) {
const message = JSON.parse(event.data);
if (message && message.request !== undefined) {
console.log('Received a request from the server');
console.log(message.request);
}
if (message && message.reply !== undefined) {
// Emit the event to registered handlers
this.emit(message.reply, message.data);
}
}
});
// On connection close
this.appCon.onclose = () => {
console.log("WebSocket connection closed");
this.connectionOpen = false;
};
// On WebSocket error
this.appCon.onerror = (error) => {
console.error("WebSocket error:", error);
};
}
/**
* Sets up a WebSocket connection to the exchange for receiving candlestick data.
* Set up a separate WebSocket connection to the Binance exchange for receiving candlestick data.
* @param {string} interval - The interval of the candlestick data.
* @param {string} tradingPair - The trading pair to subscribe to.
*/
@ -294,5 +322,17 @@ class Comms {
this.candleClose(newCandle);
}
};
this.exchangeCon.onopen = () => {
console.log(`Connected to Binance stream for ${tradingPair} at interval ${interval}`);
};
this.exchangeCon.onclose = () => {
console.log(`Disconnected from Binance stream for ${tradingPair} at interval ${interval}`);
};
this.exchangeCon.onerror = (error) => {
console.error(`WebSocket error on Binance stream for ${tradingPair}:`, error);
};
}
}
}

View File

@ -1,3 +1,4 @@
// custom_blocks.js
// Define custom Blockly blocks and Python code generation
export function defineCustomBlocks() {
// Custom block for retrieving last candle values
@ -234,6 +235,10 @@
"tooltip": "Select time in force for the order",
"helpUrl": ""
}]);
// Before defining the block, fetch the required options
const timeframeOptions = bt_data.intervals.map(interval => [interval, interval]);
const exchangeOptions = window.UI.exchanges.connected_exchanges.map(exchange => [exchange, exchange]);
const symbolOptions = bt_data.symbols.map(symbol => [symbol, symbol]);
// Dynamically populate the block options using the available data
Blockly.defineBlocksWithJsonArray([{
@ -243,26 +248,17 @@
{
"type": "field_dropdown",
"name": "TF",
"options": function() {
// Dynamically fetch available timeframes from bt_data.intervals
return bt_data.intervals.map(interval => [interval, interval]);
}
"options": timeframeOptions
},
{
"type": "field_dropdown",
"name": "EXC",
"options": function() {
// Dynamically fetch available exchanges from window.UI.exchanges.connected_exchanges
return window.UI.exchanges.connected_exchanges.map(exchange => [exchange, exchange]);
}
"options": exchangeOptions
},
{
"type": "field_dropdown",
"name": "SYM",
"options": function() {
// Dynamically fetch available symbols from bt_data.symbols
return bt_data.symbols.map(symbol => [symbol, symbol]);
}
"options": symbolOptions
}
],
"output": "source", // This output allows it to be connected to other blocks expecting a 'source'
@ -271,7 +267,10 @@
"helpUrl": ""
}]);
// Similarly, define 'target_market' block
const targetMarketTFOptions = bt_data.intervals.map(interval => [interval, interval]);
const targetMarketEXCOptions = window.UI.exchanges.connected_exchanges.map(exchange => [exchange, exchange]);
const targetMarketSYMOptions = bt_data.symbols.map(symbol => [symbol, symbol]);
Blockly.defineBlocksWithJsonArray([{
"type": "target_market",
"message0": "Target market: TF %1 Ex %2 Sym %3",
@ -279,23 +278,17 @@
{
"type": "field_dropdown",
"name": "TF",
"options": function() {
return bt_data.intervals.map(interval => [interval, interval]);
}
"options": targetMarketTFOptions
},
{
"type": "field_dropdown",
"name": "EXC",
"options": function() {
return window.UI.exchanges.connected_exchanges.map(exchange => [exchange, exchange]);
}
"options": targetMarketEXCOptions
},
{
"type": "field_dropdown",
"name": "SYM",
"options": function() {
return bt_data.symbols.map(symbol => [symbol, symbol]);
}
"options": targetMarketSYMOptions
}
],
"previousStatement": "trade_option", // Allow it to be used as a trade option
@ -310,16 +303,16 @@
"args0": [
{
"type": "field_dropdown",
"name": "DIRECTION",
"name": "METRIC",
"options": [
["up", "up"],
["down", "down"]
["in profit", "profit"],
["in loss", "loss"]
]
}
],
"output": "Boolean",
"output": "StrategyMetric", // Custom output type
"colour": 230,
"tooltip": "Check if the strategy is up or down.",
"tooltip": "Choose to evaluate the strategy's profit or loss.",
"helpUrl": ""
}]);
Blockly.defineBlocksWithJsonArray([{
@ -421,7 +414,43 @@
"tooltip": "Set a flag to True or False if the condition is met.",
"helpUrl": ""
}]);
// Entry Point Block
Blockly.defineBlocksWithJsonArray([{
"type": "entry_point",
"message0": "Entry Point",
"output": null,
"colour": 120,
"tooltip": "Marks the entry point of the strategy.",
"helpUrl": ""
}]);
// Exit Point Block
Blockly.defineBlocksWithJsonArray([{
"type": "exit_point",
"message0": "Exit Point",
"output": null,
"colour": 120,
"tooltip": "Marks the exit point of the strategy.",
"helpUrl": ""
}]);
console.log('Custom blocks defined');
// Notify User Block
Blockly.defineBlocksWithJsonArray([{
"type": "notify_user",
"message0": "Notify User with Message %1",
"args0": [
{
"type": "field_input",
"name": "MESSAGE",
"text": "Your message here"
}
],
"previousStatement": null,
"nextStatement": null,
"colour": 120,
"tooltip": "Sends a notification message to the user.",
"helpUrl": ""
}]);
console.log('Custom blocks defined');
}

View File

@ -16,17 +16,15 @@ class Data {
// All the indicators available.
this.indicators = bt_data.indicators;
/* Comms handles communication with the servers. Register
callbacks to handle various incoming messages.*/
this.comms = new Comms();
// Initialize other properties
this.price_history = null;
this.indicator_data = null;
this.last_price = null;
this.i_updates = null;
}
/* Initialize Comms with the user_name */
this.comms = new Comms(this.user_name);
}
/**
* Initializes the Data instance by setting up connections and fetching data.
* Should be called after creating a new instance of Data.
@ -37,9 +35,6 @@ class Data {
this.comms.registerCallback('candle_close', this.candle_close.bind(this));
this.comms.registerCallback('indicator_update', this.indicator_update.bind(this));
// Open the connection to your local server
this.comms.setAppCon();
// Open connection for streaming candle data with the exchange
this.comms.setExchangeCon(this.interval, this.trading_pair);

View File

@ -1,38 +1,52 @@
// Define Blockly blocks dynamically based on indicators
export function defineIndicatorBlocks() {
const indicatorOutputs = window.UI.indicators.getIndicatorOutputs();
const toolboxCategory = document.querySelector('#toolbox category[name="Indicators"]');
// client/indicator_blocks.js
for (let indicatorName in indicatorOutputs) {
const outputs = indicatorOutputs[indicatorName];
// Define Blockly blocks and their JSON generators dynamically based on indicators
export function defineIndicatorBlocks() {
// Retrieve the indicator outputs configuration
const indicatorOutputs = window.UI.indicators.getIndicatorOutputs();
const toolboxCategory = document.querySelector('#toolbox category[name="Indicators"]');
// Define the block for this indicator
Blockly.defineBlocksWithJsonArray([{
"type": indicatorName,
"message0": `${indicatorName} %1`,
"args0": [
{
"type": "field_dropdown",
"name": "OUTPUT",
"options": outputs.map(output => [output, output])
}
],
"output": "Number",
"colour": 230,
"tooltip": `Select the ${indicatorName} output`,
"helpUrl": ""
}]);
// Define how this block will generate Python code
Blockly.Python[indicatorName] = Blockly.Python.forBlock[indicatorName] = function(block) {
const selectedOutput = block.getFieldValue('OUTPUT');
const code = `get_${indicatorName.toLowerCase()}_value('${selectedOutput}')`;
return [code, Blockly.Python.ORDER_ATOMIC];
};
// Append dynamically created blocks to the Indicators category in the toolbox
const blockElement = document.createElement('block');
blockElement.setAttribute('type', indicatorName);
toolboxCategory.appendChild(blockElement);
}
if (!toolboxCategory) {
console.error('Indicators category not found in the toolbox.');
return;
}
for (let indicatorName in indicatorOutputs) {
const outputs = indicatorOutputs[indicatorName];
// Define the block for this indicator
Blockly.defineBlocksWithJsonArray([{
"type": indicatorName,
"message0": `${indicatorName} Output %1`,
"args0": [
{
"type": "field_dropdown",
"name": "OUTPUT",
"options": outputs.map(output => [output, output])
}
],
"output": "Number",
"colour": 230,
"tooltip": `Select the ${indicatorName} output`,
"helpUrl": ""
}]);
// Define the JSON generator for this block
Blockly.JSON[indicatorName] = function(block) {
const selectedOutput = block.getFieldValue('OUTPUT');
const json = {
type: 'indicator',
name: indicatorName,
output: selectedOutput
};
return JSON.stringify(json);
};
// Append the newly created block to the Indicators category in the toolbox
const blockElement = document.createElement('block');
blockElement.setAttribute('type', indicatorName);
toolboxCategory.appendChild(blockElement);
}
console.log('Indicator blocks and their JSON generators have been defined and inserted into the toolbox.');
}

View File

@ -1,114 +1,448 @@
// Define JSON generators for custom blocks
export function defineJsonGenerators() {
// Initialize JSON generator
if (!Blockly.JSON) {
Blockly.JSON = new Blockly.Generator('JSON');
// client/json_generators.js
export function defineJsonGenerators() {
// Initialize the JSON generator if not already initialized
if (!Blockly.JSON) {
Blockly.JSON = new Blockly.Generator('JSON');
}
/**
* Helper function to safely parse JSON strings.
* Returns an empty object if parsing fails.
*/
function safeParse(jsonString) {
try {
return JSON.parse(jsonString);
} catch (e) {
console.error("JSON Parsing Error:", e);
return {};
}
}
/**
* Trade Action Block JSON Generator
* Captures trade actions including conditions, trade types, sizes, stop loss, take profit, and trade options.
*/
Blockly.JSON['trade_action'] = function(block) {
const condition = Blockly.JSON.valueToCode(block, 'CONDITION', Blockly.JSON.ORDER_ATOMIC) || "False";
const tradeType = block.getFieldValue('TRADE_TYPE'); // e.g., 'buy' or 'sell'
const size = Blockly.JSON.valueToCode(block, 'SIZE', Blockly.JSON.ORDER_ATOMIC) || 1;
const stopLoss = Blockly.JSON.valueToCode(block, 'STOP_LOSS', Blockly.JSON.ORDER_ATOMIC) || null;
const takeProfit = Blockly.JSON.valueToCode(block, 'TAKE_PROFIT', Blockly.JSON.ORDER_ATOMIC) || null;
const tradeOptionsCode = Blockly.JSON.statementToCode(block, 'TRADE_OPTIONS').trim();
let tradeOptions = [];
if (tradeOptionsCode) {
tradeOptions = safeParse(tradeOptionsCode);
// Ensure tradeOptions is an array
if (!Array.isArray(tradeOptions)) {
tradeOptions = [tradeOptions];
}
}
// JSON Generator for 'trade_action' block
Blockly.JSON['trade_action'] = function(block) {
const condition = Blockly.JSON.valueToCode(block, 'CONDITION', Blockly.JSON.ORDER_ATOMIC);
const tradeType = block.getFieldValue('TRADE_TYPE');
const size = Blockly.JSON.valueToCode(block, 'SIZE', Blockly.JSON.ORDER_ATOMIC) || null;
const stopLoss = Blockly.JSON.valueToCode(block, 'STOP_LOSS', Blockly.JSON.ORDER_ATOMIC) || null;
const takeProfit = Blockly.JSON.valueToCode(block, 'TAKE_PROFIT', Blockly.JSON.ORDER_ATOMIC) || null;
const tradeOptions = Blockly.JSON.statementToCode(block, 'TRADE_OPTIONS').trim();
const json = {
type: 'trade_action',
condition: condition,
trade_type: tradeType,
size: size,
stop_loss: stopLoss,
take_profit: takeProfit,
trade_options: tradeOptions ? JSON.parse(tradeOptions) : []
};
return JSON.stringify(json);
};
// JSON generator for 'order_type' block
Blockly.JSON['order_type'] = function(block) {
const orderType = block.getFieldValue('ORDER_TYPE');
const limitPrice = Blockly.JSON.valueToCode(block, 'LIMIT_PRICE', Blockly.JSON.ORDER_ATOMIC) || null;
const json = {
order_type: orderType,
limit_price: limitPrice
};
return JSON.stringify(json);
const json = {
type: 'trade_action',
condition: condition,
trade_type: tradeType,
size: parseFloat(size),
stop_loss: stopLoss !== null ? parseFloat(stopLoss) : null,
take_profit: takeProfit !== null ? parseFloat(takeProfit) : null,
trade_options: tradeOptions
};
// JSON generator for 'time_in_force' block
Blockly.JSON['time_in_force'] = function(block) {
const tif = block.getFieldValue('TIF');
const json = { tif: tif };
return JSON.stringify(json);
};
// JSON generator for 'comparison' block
Blockly.JSON['comparison'] = Blockly.JSON.forBlock['comparison'] = function(block) {
const left = Blockly.JSON.valueToCode(block, 'LEFT', Blockly.JSON.ORDER_ATOMIC);
const right = Blockly.JSON.valueToCode(block, 'RIGHT', Blockly.JSON.ORDER_ATOMIC);
const operator = block.getFieldValue('OPERATOR');
const json = {
type: 'comparison',
operator: operator,
left: left,
right: right
};
return JSON.stringify(json);
};
// JSON generator for 'logical_and' block
Blockly.JSON['logical_and'] = Blockly.JSON.forBlock['logical_and'] = function(block) {
const left = Blockly.JSON.valueToCode(block, 'LEFT', Blockly.JSON.ORDER_ATOMIC);
const right = Blockly.JSON.valueToCode(block, 'RIGHT', Blockly.JSON.ORDER_ATOMIC);
const json = {
type: 'logical_and',
left: left,
right: right
};
return JSON.stringify(json);
};
// JSON generator for 'logical_or' block
Blockly.JSON['logical_or'] = Blockly.JSON.forBlock['logical_or'] = function(block) {
const left = Blockly.JSON.valueToCode(block, 'LEFT', Blockly.JSON.ORDER_ATOMIC);
const right = Blockly.JSON.valueToCode(block, 'RIGHT', Blockly.JSON.ORDER_ATOMIC);
const json = {
type: 'logical_or',
left: left,
right: right
};
return JSON.stringify(json);
};
// JSON generator for 'last_candle_value' block
Blockly.JSON['last_candle_value'] = Blockly.JSON.forBlock['last_candle_value'] = function(block) {
const candlePart = block.getFieldValue('CANDLE_PART');
const source = Blockly.JSON.valueToCode(block, 'SOURCE', Blockly.JSON.ORDER_ATOMIC) || null;
const json = {
type: 'last_candle_value',
candle_part: candlePart,
source: source
};
return JSON.stringify(json);
};
// JSON generator for 'source' block
Blockly.JSON['source'] = Blockly.JSON.forBlock['source'] = function(block) {
const timeframe = block.getFieldValue('TF');
const exchange = block.getFieldValue('EXC');
const symbol = block.getFieldValue('SYM');
const json = {
type: 'source',
timeframe: timeframe,
exchange: exchange,
symbol: symbol
};
return JSON.stringify(json);
};
console.log('JSON generators defined with forBlock assignments');
return JSON.stringify(json);
};
/**
* Trade Option Block JSON Generator
* Captures trade options like order type, limit price, and time in force.
*/
Blockly.JSON['trade_option'] = function(block) {
const orderType = block.getFieldValue('ORDER_TYPE'); // e.g., 'market', 'limit'
const limitPrice = Blockly.JSON.valueToCode(block, 'LIMIT_PRICE', Blockly.JSON.ORDER_ATOMIC) || null;
const timeInForce = block.getFieldValue('TIF'); // e.g., 'gtc', 'ioc'
const json = {
order_type: orderType,
limit_price: limitPrice !== null ? parseFloat(limitPrice) : null,
tif: timeInForce
};
return JSON.stringify(json);
};
/**
* comparison JSON Generator
* Compares two numerical values, where one can be a strategy_profit_loss block.
*/
Blockly.JSON['comparison'] = function(block) {
const operator = block.getFieldValue('OPERATOR');
// Generate JSON for left operand
const leftBlock = block.getInputTargetBlock('LEFT');
let leftValue;
if (leftBlock && leftBlock.type === 'strategy_profit_loss') {
leftValue = JSON.parse(Blockly.JSON['strategy_profit_loss'](leftBlock));
} else {
leftValue = Blockly.JSON.valueToCode(block, 'LEFT', Blockly.JSON.ORDER_ATOMIC) || 0;
try {
leftValue = JSON.parse(leftValue);
} catch (e) {
leftValue = parseFloat(leftValue);
}
}
// Generate JSON for right operand
const rightBlock = block.getInputTargetBlock('RIGHT');
let rightValue;
if (rightBlock && rightBlock.type === 'strategy_profit_loss') {
rightValue = JSON.parse(Blockly.JSON['strategy_profit_loss'](rightBlock));
} else {
rightValue = Blockly.JSON.valueToCode(block, 'RIGHT', Blockly.JSON.ORDER_ATOMIC) || 0;
try {
rightValue = JSON.parse(rightValue);
} catch (e) {
rightValue = parseFloat(rightValue);
}
}
const json = {
type: 'comparison',
operator: operator,
left: leftValue,
right: rightValue
};
return JSON.stringify(json);
};
/**
* Logical AND Block JSON Generator
* Captures logical AND operations between two conditions.
*/
Blockly.JSON['logical_and'] = function(block) {
const condition1 = Blockly.JSON.valueToCode(block, 'CONDITION1', Blockly.JSON.ORDER_ATOMIC) || "False";
const condition2 = Blockly.JSON.valueToCode(block, 'CONDITION2', Blockly.JSON.ORDER_ATOMIC) || "False";
const json = {
type: 'logical_and',
conditions: [condition1, condition2]
};
return JSON.stringify(json);
};
/**
* Logical OR Block JSON Generator
* Captures logical OR operations between two conditions.
*/
Blockly.JSON['logical_or'] = function(block) {
const condition1 = Blockly.JSON.valueToCode(block, 'CONDITION1', Blockly.JSON.ORDER_ATOMIC) || "False";
const condition2 = Blockly.JSON.valueToCode(block, 'CONDITION2', Blockly.JSON.ORDER_ATOMIC) || "False";
const json = {
type: 'logical_or',
conditions: [condition1, condition2]
};
return JSON.stringify(json);
};
/**
* Is False Block JSON Generator
* Captures a condition that checks if another condition is false.
*/
Blockly.JSON['is_false'] = function(block) {
const condition = Blockly.JSON.valueToCode(block, 'CONDITION', Blockly.JSON.ORDER_ATOMIC) || "False";
const json = {
type: 'is_false',
condition: condition
};
return JSON.stringify(json);
};
/**
* Arithmetic Operator Block JSON Generator
* Captures arithmetic operations between two numerical values.
*/
Blockly.JSON['arithmetic_operator'] = function(block) {
const operand1 = Blockly.JSON.valueToCode(block, 'OPERAND1', Blockly.JSON.ORDER_ATOMIC) || "0";
const operand2 = Blockly.JSON.valueToCode(block, 'OPERAND2', Blockly.JSON.ORDER_ATOMIC) || "0";
const operator = block.getFieldValue('OPERATOR'); // e.g., '+', '-', '*', '/'
const json = {
type: 'arithmetic_operator',
operator: operator,
operands: [operand1, operand2]
};
return JSON.stringify(json);
};
/**
* Last Candle Value Block JSON Generator
* Captures the value part of the last candle from a specified data source.
*/
Blockly.JSON['last_candle_value'] = function(block) {
const candlePart = block.getFieldValue('CANDLE_PART'); // e.g., 'open', 'high', 'low', 'close', 'volume'
const sourceCode = Blockly.JSON.valueToCode(block, 'SOURCE', Blockly.JSON.ORDER_ATOMIC) || "{}";
const source = safeParse(sourceCode);
const json = {
type: 'last_candle_value',
candle_part: candlePart,
source: source
};
return JSON.stringify(json);
};
/**
* Source Block JSON Generator
* Captures the data source details like timeframe, exchange, and symbol.
*/
Blockly.JSON['source'] = function(block) {
const timeframe = block.getFieldValue('TF'); // e.g., '5m', '1h'
const exchange = block.getFieldValue('EXC'); // e.g., 'Binance'
const symbol = block.getFieldValue('SYM'); // e.g., 'BTCUSD'
const json = {
timeframe: timeframe,
exchange: exchange,
symbol: symbol
};
return JSON.stringify(json);
};
/**
* Target Market Block JSON Generator
* Captures target market parameters for trading.
*/
Blockly.JSON['target_market'] = function(block) {
const timeframe = block.getFieldValue('TIMEFRAME'); // e.g., '5m', '1h'
const exchange = block.getFieldValue('EXCHANGE'); // e.g., 'Binance'
const symbol = block.getFieldValue('SYMBOL'); // e.g., 'BTCUSD'
const json = {
timeframe: timeframe,
exchange: exchange,
symbol: symbol
};
return JSON.stringify(json);
};
/**
* strategy_profit_loss JSON Generator
* Outputs the metric (profit or loss) to evaluate.
*/
Blockly.JSON['strategy_profit_loss'] = function(block) {
const metric = block.getFieldValue('METRIC'); // 'profit' or 'loss'
const json = {
type: 'strategy_profit_loss',
metric: metric
};
return JSON.stringify(json);
};
/**
* Current Balance Block JSON Generator
* Captures the current balance of the account.
*/
Blockly.JSON['current_balance'] = function(block) {
const json = {
type: 'current_balance'
};
return JSON.stringify(json);
};
/**
* Starting Balance Block JSON Generator
* Captures the starting balance of the account.
*/
Blockly.JSON['starting_balance'] = function(block) {
const json = {
type: 'starting_balance'
};
return JSON.stringify(json);
};
/**
* Active Trades Block JSON Generator
* Captures the number of active trades.
*/
Blockly.JSON['active_trades'] = function(block) {
const json = {
type: 'active_trades'
};
return JSON.stringify(json);
};
/**
* Flag Is Set Block JSON Generator
* Captures whether a specific flag is set.
*/
Blockly.JSON['flag_is_set'] = function(block) {
const flagName = block.getFieldValue('FLAG_NAME'); // e.g., 'flag1'
const json = {
type: 'flag_is_set',
flag_name: flagName
};
return JSON.stringify(json);
};
/**
* Set Flag Block JSON Generator
* Captures the action to set a specific flag.
*/
Blockly.JSON['set_flag'] = function(block) {
const flagName = block.getFieldValue('FLAG_NAME'); // e.g., 'flag1'
const json = {
type: 'set_flag',
flag_name: flagName
};
return JSON.stringify(json);
};
/**
* Value Input Block JSON Generator
* Captures a numerical input value.
*/
Blockly.JSON['value_input'] = function(block) {
const value = Blockly.JSON.valueToCode(block, 'VALUE', Blockly.JSON.ORDER_ATOMIC) || "0";
const json = {
type: 'value_input',
value: parseFloat(value)
};
return JSON.stringify(json);
};
/**
* Time in Force Block JSON Generator
* Captures the time in force for an order.
*/
Blockly.JSON['time_in_force'] = function(block) {
const tif = block.getFieldValue('TIF'); // e.g., 'gtc', 'ioc'
const json = {
type: 'time_in_force',
tif: tif
};
return JSON.stringify(json);
};
/**
* Conditional Execution Block JSON Generator
* Captures conditional statements within the strategy.
*/
Blockly.JSON['conditional_execution'] = function(block) {
const conditionCode = Blockly.JSON.valueToCode(block, 'CONDITION', Blockly.JSON.ORDER_ATOMIC) || "False";
const actionsCode = Blockly.JSON.statementToCode(block, 'ACTIONS').trim();
let actions = [];
if (actionsCode) {
actions = safeParse(actionsCode);
// Ensure actions is an array
if (!Array.isArray(actions)) {
actions = [actions];
}
}
const json = {
type: 'conditional_execution',
condition: conditionCode,
actions: actions
};
return JSON.stringify(json);
};
/**
* Market Order Block JSON Generator
* Captures market order details.
*/
Blockly.JSON['market_order'] = function(block) {
const size = Blockly.JSON.valueToCode(block, 'SIZE', Blockly.JSON.ORDER_ATOMIC) || 0;
const json = {
type: 'market_order',
size: parseFloat(size)
};
return JSON.stringify(json);
};
/**
* Limit Order Block JSON Generator
* Captures limit order details.
*/
Blockly.JSON['limit_order'] = function(block) {
const size = Blockly.JSON.valueToCode(block, 'SIZE', Blockly.JSON.ORDER_ATOMIC) || 0;
const price = Blockly.JSON.valueToCode(block, 'PRICE', Blockly.JSON.ORDER_ATOMIC) || 0;
const tif = block.getFieldValue('TIF'); // e.g., 'gtc', 'ioc'
const json = {
type: 'limit_order',
size: parseFloat(size),
price: parseFloat(price),
tif: tif
};
return JSON.stringify(json);
};
/**
* Entry Point Block JSON Generator
* Captures the entry point of the strategy.
*/
Blockly.JSON['entry_point'] = function(block) {
const json = {
type: 'entry_point'
};
return JSON.stringify(json);
};
/**
* Exit Point Block JSON Generator
* Captures the exit point of the strategy.
*/
Blockly.JSON['exit_point'] = function(block) {
const json = {
type: 'exit_point'
};
return JSON.stringify(json);
};
/**
* Notify User Block JSON Generator
* Captures a message to notify the user.
*/
Blockly.JSON['notify_user'] = function(block) {
const message = Blockly.JSON.valueToCode(block, 'MESSAGE', Blockly.JSON.ORDER_ATOMIC) || "No message provided.";
const json = {
type: 'notify_user',
message: message
};
return JSON.stringify(json);
};
console.log('All JSON generators have been defined successfully.');
}

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/blocks_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. -->
<script type="text/javascript">

View File

@ -139,6 +139,12 @@
<block type="take_profit"></block>
<block type="target_market"></block>
</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>