brighter-trading/src/BrighterTrades.py

610 lines
26 KiB
Python

from typing import Any
from Users import Users
from DataCache_v3 import DataCache
from Strategies import Strategies
from backtesting import Backtester
from candles import Candles
from Configuration import Configuration
from ExchangeInterface import ExchangeInterface
from indicators import Indicators
from Signals import Signals
from trade import Trades
class BrighterTrades:
def __init__(self):
# Object that interacts and maintains exchange_interface and account data
self.exchanges = ExchangeInterface()
# Object that interacts with the persistent data.
self.data = DataCache(self.exchanges)
# Configuration for the app
self.config = Configuration()
# The object that manages users in the system.
self.users = Users(data_cache=self.data)
# Object that maintains signals.
self.signals = Signals(self.config)
# Object that maintains candlestick and price data.
self.candles = Candles(users=self.users, exchanges=self.exchanges, data_source=self.data,
config=self.config)
# Object that interacts with and maintains data from available indicators
self.indicators = Indicators(self.candles, self.users)
# Object that maintains the trades data
self.trades = Trades(self.users)
# The Trades object needs to connect to an exchange_interface.
self.trades.connect_exchanges(exchanges=self.exchanges)
# Object that maintains the strategies data
self.strategies = Strategies(self.data, self.trades)
# Object responsible for testing trade and strategies data.
self.backtester = Backtester()
def create_new_user(self, email: str, username: str, password: str) -> bool:
"""
Creates a new user and logs the user in.
:param email: User's email address.
:param username: User's user_name.
:param password: User's password.
:return: bool - True on successful creation and log in.
"""
if not email or not username or not password:
raise ValueError("Missing required arguments for 'create_new_user'")
try:
self.users.create_new_user(email=email, username=username, password=password)
login_successful = self.users.log_in_user(username=username, password=password)
return login_successful
except Exception as e:
# Handle specific exceptions or log the error
raise ValueError("Error creating a new user: " + str(e))
def log_user_in_out(self, user_name: str, cmd: str, password: str = None):
"""
Logs the user in or out based on the provided command.
:param user_name: The user_name.
:param cmd: The command indicating the action to perform ('logout' or 'login').
:param password: The password for logging in. Required if cmd is 'login'.
:return: True if the action was successful, False otherwise.
"""
if cmd not in ['login', 'logout']:
raise ValueError("Invalid command. Expected 'login' or 'logout'.")
try:
if cmd == 'logout':
return self.users.log_out_user(username=user_name)
elif cmd == 'login':
if password is None:
raise ValueError("Password is required for login.")
return self.users.log_in_user(username=user_name, password=password)
except Exception as e:
# Handle specific exceptions or log the error
raise ValueError("Error during user login/logout: " + str(e))
def get_user_info(self, user_name: str, info: str) -> Any | None:
"""
Returns specified user info.
:param user_name: The user_name.
:param info: The information being requested.
:return: The requested info or None.
:raises ValueError: If the provided info is invalid.
"""
if info == 'Chart View':
try:
return self.users.get_chart_view(user_name=user_name)
except Exception as e:
# Handle specific exceptions or log the error
raise ValueError("Error retrieving chart view information: " + str(e))
elif info == 'Is logged in?':
try:
return self.users.is_logged_in(user_name=user_name)
except Exception as e:
# Handle specific exceptions or log the error
raise ValueError("Error checking logged in status: " + str(e))
elif info == 'User_id':
try:
return self.users.get_id(user_name=user_name)
except Exception as e:
# Handle specific exceptions or log the error
raise ValueError("Error fetching id: " + str(e))
else:
raise ValueError("Invalid information requested: " + info)
def get_market_info(self, info: str, **kwargs) -> Any:
"""
Request market information from the application.
:param info: str - The information requested.
:param kwargs: arguments required depending on the info requested.
:return: The info requested.
"""
if info == 'Candle History':
chart_view = kwargs.get('chart_view', {})
num_records = kwargs.get('num_records', 10)
symbol = chart_view.get('market')
timeframe = chart_view.get('timeframe')
exchange_name = chart_view.get('exchange')
user_name = kwargs.get('user_name')
if symbol and timeframe and exchange_name and user_name:
return self.candles.get_candle_history(num_records=num_records,
symbol=symbol,
interval=timeframe,
exchange_name=exchange_name,
user_name=user_name)
else:
missing_args = [arg for arg in ['symbol', 'timeframe', 'exchange', 'user_name'] if arg not in kwargs]
raise ValueError(f"Missing required arguments for 'Candle History': {', '.join(missing_args)}")
elif info == 'Something Else':
# Add code or action for 'Something Else'
pass
else:
raise ValueError(f"Unknown or missing argument for get_market_info(): {info}")
return None
def get_indicator_data(self, user_name: str, source: dict, start_ts: float = None, num_results: int = None) -> dict:
"""
Fetches indicator data for a specific user.
:param user_name: The name of the user making the request.
:param source: A dictionary containing values specific to the type of indicator.
:param start_ts: The optional timestamp to start fetching the data from.
:param num_results: The optional number of results requested.
:return: dict - A dictionary of timestamp indexed indicator data.
:raises: ValueError if user_name or source is invalid.
"""
if not user_name:
raise ValueError("Invalid user_name provided.")
if not source:
raise ValueError("Invalid source provided.")
# Additional validation checks for start_ts and num_results if needed
return self.indicators.get_indicator_data(user_name=user_name, source=source, start_ts=start_ts,
num_results=num_results)
def connect_user_to_exchange(self, user_name: str, default_exchange: str, default_keys: dict = None) -> bool:
"""
Connects an exchange if it is not already connected.
:param user_name: str - The user executing the action.
:param default_exchange: - The name of the default exchange to connect.
:param default_keys: default API keys.
:return: bool - True on success.
"""
active_exchanges = self.users.get_exchanges(user_name, category='active_exchanges')
success = False
for exchange in active_exchanges:
keys = self.users.get_api_keys(user_name, exchange)
result = self.connect_or_config_exchange(user_name=user_name,
exchange_name=exchange,
api_keys=keys)
if (result['status'] == 'success') or (result['status'] == 'already_connected'):
success = True
if not success:
# If no active exchange was successfully connected, connect to the default exchange
result = self.connect_or_config_exchange(user_name=user_name,
exchange_name=default_exchange,
api_keys=default_keys)
if result['status'] == 'success':
success = True
return success
def get_js_init_data(self, user_name: str) -> dict:
"""
Returns a JSON object of initialization data.
This is passed into the frontend HTML template for the javascript to access in the rendered HTML.
:param user_name: str - The name of the user making the query.
"""
chart_view = self.users.get_chart_view(user_name=user_name)
indicator_types = self.indicators.get_available_indicator_types()
available_indicators = self.indicators.get_indicator_list(user_name)
if not chart_view:
chart_view = {'timeframe': '', 'exchange_name': '', 'market': ''}
if not indicator_types:
indicator_types = []
if not available_indicators:
available_indicators = []
js_data = {
'i_types': indicator_types,
'indicators': available_indicators,
'timeframe': chart_view.get('timeframe'),
'exchange_name': chart_view.get('exchange_name'),
'trading_pair': chart_view.get('market'),
'user_name': user_name,
'public_exchanges': self.exchanges.get_public_exchanges()
}
return js_data
def get_rendered_data(self, user_name: str) -> dict:
"""
Returns data required to render the HTML template of the application's frontend.
:param user_name: The name of the user executing the request.
:return: A dictionary containing the requested data.
"""
chart_view = self.users.get_chart_view(user_name=user_name)
exchange = self.exchanges.get_exchange(ename=chart_view.get('exchange'), uname=user_name)
# noinspection PyDictCreation
r_data = {}
r_data['title'] = self.config.get_setting('application_title')
r_data['chart_interval'] = chart_view.get('timeframe', '')
r_data['selected_exchange'] = chart_view.get('exchange', '')
r_data['intervals'] = exchange.intervals if exchange else []
r_data['symbols'] = exchange.get_symbols() if exchange else {}
r_data['available_exchanges'] = self.exchanges.get_available_exchanges() or []
r_data['connected_exchanges'] = self.exchanges.get_connected_exchanges(user_name) or []
r_data['configured_exchanges'] = self.users.get_exchanges(
user_name, category='configured_exchanges') or []
r_data['my_balances'] = self.exchanges.get_all_balances(user_name) or {}
r_data['indicator_types'] = self.indicators.get_available_indicator_types() or []
r_data['indicator_list'] = self.indicators.get_indicator_list(user_name) or []
r_data['enabled_indicators'] = self.indicators.get_indicator_list(user_name, only_enabled=True) or []
r_data['ma_vals'] = self.indicators.MV_AVERAGE_ENUM
r_data['active_trades'] = self.exchanges.get_all_activated(user_name, fetch_type='trades') or {}
r_data['open_orders'] = self.exchanges.get_all_activated(user_name, fetch_type='orders') or {}
return r_data
def received_cdata(self, cdata: dict) -> dict | None:
"""
Processes the received candle data and updates the indicators, signals, trades, and strategies.
:param cdata: Dictionary containing the most recent price data.
:return: Dictionary of updates to be passed onto the UI or None if received duplicate data.
"""
# Return if this candle is the same as the last candle received, except when it's the first candle received.
if not self.candles.cached_last_candle:
self.candles.cached_last_candle = cdata
elif cdata['time'] == self.candles.cached_last_candle['time']:
return None
self.candles.set_new_candle(cdata)
# i_updates = self.indicators.update_indicators()
state_changes = self.signals.process_all_signals(self.indicators)
trade_updates = self.trades.update(float(cdata['close']))
stg_updates = self.strategies.update(self.signals)
updates = {}
# if i_updates:
# updates['i_updates'] = i_updates
if state_changes:
updates['s_updates'] = state_changes
if trade_updates:
updates['trade_updts'] = trade_updates
if stg_updates:
updates['stg_updts'] = stg_updates
return updates
def received_new_signal(self, data: dict) -> str | dict:
"""
Handles the creation of a new signal based on the provided data.
:param data: A dictionary containing the attributes of the new signal.
:return: An error message if the required attribute is missing, or the incoming data for chaining on success.
"""
if 'name' not in data:
return "The new signal must have a 'name' attribute."
self.signals.new_signal(data)
self.config.set_setting('signals_list', self.signals.get_signals('dict'))
return data
def received_new_strategy(self, data: dict) -> str | dict:
"""
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.
"""
if 'name' not in data:
return "The new strategy must have a 'name' attribute."
self.strategies.new_strategy(data)
self.config.set_setting('strategies', self.strategies.get_strategies('dict'))
return data
def delete_strategy(self, strategy_name: str) -> None:
"""
Deletes the specified strategy from the strategies instance and the configuration file.
:param strategy_name: The name of the strategy to delete.
:return: None
:raises ValueError: If the strategy does not exist or there are issues
with removing it from the configuration file.
"""
# if not self.strategies.has_strategy(strategy_name):
# raise ValueError(f"The strategy '{strategy_name}' does not exist.")
self.strategies.delete_strategy(strategy_name)
try:
self.config.remove('strategies', strategy_name)
except Exception as e:
raise ValueError(f"Failed to remove the strategy '{strategy_name}' from the configuration file: {str(e)}")
def delete_signal(self, signal_name: str) -> None:
"""
Deletes a signal from the signals instance and removes it from the configuration file.
:param signal_name: The name of the signal to delete.
:return: None
"""
# Delete the signal from the signals instance.
self.signals.delete_signal(signal_name)
# Delete the signal from the configuration file.
self.config.remove('signals', signal_name)
def get_signals_json(self) -> str:
"""
Retrieve all the signals from the signals instance and return them as a JSON object.
:return: str - A JSON object containing all the signals.
"""
return self.signals.get_signals('json')
def get_strategies_json(self) -> str:
"""
Retrieve all the strategies from the strategies instance and return them as a JSON object.
:return: str - A JSON object containing all the strategies.
"""
return self.strategies.get_strategies('json')
def connect_or_config_exchange(self, user_name: str, exchange_name: str, api_keys: dict = None) -> dict:
"""
Connects to an exchange if not already connected, or configures the exchange connection for a single user.
:param user_name: str - The name of the user.
:param exchange_name: str - The name of the exchange.
:param api_keys: dict - The API keys for the exchange.
:return: dict - A dictionary containing the result of the operation.
"""
result = {
'exchange': exchange_name,
'status': '',
'message': ''
}
try:
if self.exchanges.exchange_data.query("user == @user_name and name == @exchange_name").empty:
# Exchange is not connected, try to connect
success = self.exchanges.connect_exchange(exchange_name=exchange_name, user_name=user_name,
api_keys=api_keys)
if success:
self.users.active_exchange(exchange=exchange_name, user_name=user_name, cmd='set')
if api_keys:
self.users.update_api_keys(api_keys=api_keys, exchange=exchange_name, user_name=user_name)
result['status'] = 'success'
result['message'] = f'Successfully connected to {exchange_name}.'
else:
result['status'] = 'failure'
result['message'] = f'Failed to connect to {exchange_name}.'
else:
# Exchange is already connected, update API keys if provided
if api_keys:
self.users.update_api_keys(api_keys=api_keys, exchange=exchange_name, user_name=user_name)
result['status'] = 'already_connected'
result['message'] = f'{exchange_name}: API keys updated.'
except Exception as e:
result['status'] = 'error'
result['message'] = f"Failed to connect to {exchange_name} for user '{user_name}': {str(e)}"
return result
def close_trade(self, trade_id):
"""
Closes a trade identified by the given trade ID.
:param trade_id: The ID of the trade to be closed.
"""
if self.trades.is_valid_trade_id(trade_id):
self.trades.close_trade(trade_id)
self.config.remove('trades', trade_id)
print(f"Trade {trade_id} has been closed.")
else:
print(f"Invalid trade ID: {trade_id}. Unable to close the trade.")
def received_new_trade(self, data: dict) -> dict | None:
"""
Called when a new trade has been defined and created in the UI.
:param data: A dictionary containing the attributes of the trade.
:return: The details of the trade as a dictionary, or None on failure.
"""
def vld(attr):
"""
Casts numeric strings to float before returning the attribute.
Returns None if the attribute is absent in the data.
"""
if attr in data and data[attr] != '':
try:
return float(data[attr])
except ValueError:
return data[attr]
else:
return None
# Forward the request to trades.
status, result = self.trades.new_trade(target=vld('exchange_name'), symbol=vld('symbol'), price=vld('price'),
side=vld('side'), order_type=vld('orderType'),
qty=vld('quantity'))
if status == 'Error':
print(f'Error placing the trade: {result}')
return None
print(f'Trade order received: exchange_name={vld("exchange_name")}, '
f'symbol={vld("symbol")}, '
f'side={vld("side")}, '
f'type={vld("orderType")}, '
f'quantity={vld("quantity")}, '
f'price={vld("price")}')
# Update config's list of trades and save to file.
self.config.update_data('trades', self.trades.get_trades('dict'))
trade_obj = self.trades.get_trade_by_id(result)
if trade_obj:
# Return the trade object that was created in a form that can be converted to json.
return trade_obj.__dict__
else:
return None
def get_trades(self):
""" Return a JSON object of all the trades in the trades instance."""
return self.trades.get_trades('dict')
def adjust_setting(self, user_name: str, setting: str, params: Any):
"""
Adjusts the specified setting for a user.
:param user_name: The name of the user.
:param setting: The setting to adjust.
:param params: The parameters for the setting adjustment.
:raises ValueError: If the provided setting is not supported.
"""
print(f"[SETTINGS()] MODIFYING({user_name, setting})")
if setting == 'interval':
interval_state = params['timeframe']
self.users.set_chart_view(values=interval_state, specific_property='timeframe', user_name=user_name)
elif setting == 'trading_pair':
trading_pair = params['symbol']
self.users.set_chart_view(values=trading_pair, specific_property='market', user_name=user_name)
elif setting == 'exchange':
exchange_name = params['exchange_name']
# Get the list of available symbols (markets) for the specified exchange and user.
markets = self.exchanges.get_exchange(ename=exchange_name, uname=user_name).get_symbols()
# Check if the markets list is empty
if not markets:
# If no markets are available, exit without changing the chart view.
print(f"No available markets found for exchange '{exchange_name}'. Chart view remains unchanged.")
return
# Get the currently viewed market for the user.
current_symbol = self.users.get_chart_view(user_name=user_name, prop='market')
# Determine the market to display based on availability.
if current_symbol not in markets:
# If the current market is not available, default to the first available market.
market = markets[0]
else:
# Otherwise, continue displaying the current market.
market = current_symbol
# Update the user's chart view to reflect the new exchange and default market.
self.users.set_chart_view(values=exchange_name, specific_property='exchange_name',
user_name=user_name, default_market=market)
elif setting == 'toggle_indicator':
indicators_to_toggle = params.getlist('indicator')
user_id = self.get_user_info(user_name=user_name, info='User_id')
self.indicators.toggle_indicators(user_id=user_id, indicator_names=indicators_to_toggle)
elif setting == 'edit_indicator':
self.indicators.edit_indicator(user_name=user_name, params=params)
elif setting == 'new_indicator':
self.indicators.new_indicator(user_name=user_name, params=params)
else:
print(f'ERROR SETTING VALUE')
print(f'The string received by the server was: /n{params}')
# Now that the state is changed reload price history.
self.candles.set_cache(user_name=user_name)
return
def process_incoming_message(self, msg_type: str, msg_data: dict | str) -> dict | None:
"""
Processes an incoming message and performs the corresponding actions based on the message type and data.
:param msg_type: The type of the incoming message.
:param msg_data: The data associated with the incoming message.
:return: dict|None - A dictionary containing the response message and data, or None if no response is needed.
"""
def standard_reply(reply_msg: str, reply_data: Any) -> dict:
""" Formats a standard reply message. """
return {"reply": reply_msg, "data": reply_data}
if msg_type == 'candle_data':
if r_data := self.received_cdata(msg_data):
return standard_reply("updates", r_data)
if msg_type == 'request':
if msg_data == 'signals':
if signals := self.get_signals_json():
return standard_reply("signals", signals)
elif msg_data == 'strategies':
if strategies := self.get_strategies_json():
return standard_reply("strategies", strategies)
elif msg_data == 'trades':
if trades := self.get_trades():
return standard_reply("trades", trades)
else:
print('Warning: Unhandled request!')
print(msg_data)
# Processing commands
if msg_type == 'delete_signal':
self.delete_signal(msg_data)
if msg_type == 'delete_strategy':
self.delete_strategy(msg_data)
if msg_type == 'close_trade':
self.close_trade(msg_data)
if msg_type == 'new_signal':
if r_data := self.received_new_signal(msg_data):
return standard_reply("signal_created", r_data)
if msg_type == 'new_strategy':
if r_data := self.received_new_strategy(msg_data):
return standard_reply("strategy_created", r_data)
if msg_type == 'new_trade':
if r_data := self.received_new_trade(msg_data):
return standard_reply("trade_created", r_data)
if msg_type == 'config_exchange':
user, exchange, keys = msg_data['user'], msg_data['exch'], msg_data['keys']
r_data = self.connect_or_config_exchange(user_name=user, exchange_name=exchange, api_keys=keys)
return standard_reply("Exchange_connection_result", r_data)
if msg_type == 'reply':
# If the message is a reply log the response to the terminal.
print(f"\napp.py:Received reply: {msg_data}")