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