From a68df4569f20222fb502a3ddeea024fb74cedaf4 Mon Sep 17 00:00:00 2001 From: mraniki <8766259+mraniki@users.noreply.github.com> Date: Wed, 3 Jul 2024 12:44:38 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=A5=20breaking=20with=20handler=20temp?= =?UTF-8?q?late=20capability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- findmyorder/default_settings.toml | 5 +- findmyorder/handler/__init__.py | 9 ++ findmyorder/handler/basic.py | 61 ++++++++ findmyorder/handler/handler.py | 124 ++++++++++++++++ findmyorder/handler/simple.py | 61 -------- findmyorder/handler/standard.py | 91 ++++++++++++ findmyorder/main.py | 233 +++++++++++++++--------------- 7 files changed, 402 insertions(+), 182 deletions(-) create mode 100644 findmyorder/handler/__init__.py create mode 100644 findmyorder/handler/basic.py create mode 100644 findmyorder/handler/handler.py delete mode 100644 findmyorder/handler/simple.py create mode 100644 findmyorder/handler/standard.py diff --git a/findmyorder/default_settings.toml b/findmyorder/default_settings.toml index 716a9db..98a90d2 100644 --- a/findmyorder/default_settings.toml +++ b/findmyorder/default_settings.toml @@ -19,6 +19,9 @@ VALUE = "Production - Default" # Module Enable/Disable findmyorder_enabled = true +[default.findmyorder.template] +enabled = true +parser = "standard" # Keyword to be use to identify an order action_identifier = "BUY SELL LONG SHORT" # Keyword identifier for stoploss @@ -33,14 +36,12 @@ order_type_identifier = "spot future margin" leverage_type_identifier = "cross isolated" # Keyword identifier for comments comment_identifier = "comment=" - # Stoploss default value is none is provided stop_loss = 1000 # Take-Profit default value is none is provided take_profit = 1000 # Quantity default value is none is provided quantity = 1 - # Settings to enable or disable # instrument mapping instrument_mapping = true diff --git a/findmyorder/handler/__init__.py b/findmyorder/handler/__init__.py new file mode 100644 index 0000000..65bc9b1 --- /dev/null +++ b/findmyorder/handler/__init__.py @@ -0,0 +1,9 @@ +""" + + +""" + +# from .basic import BasicHandler +from .standard import StandardHandler + +__all__ = ["StandardHandler"] diff --git a/findmyorder/handler/basic.py b/findmyorder/handler/basic.py new file mode 100644 index 0000000..cee8279 --- /dev/null +++ b/findmyorder/handler/basic.py @@ -0,0 +1,61 @@ +""" +Basic Parser + +""" + +from loguru import logger +from pyparsing import ( + Optional, + Word, + alphas, + nums, + one_of, + pyparsing_common, +) + +from .handler import ParserClient + + +class BasicHandler(ParserClient): + + def __init__(self, **kwargs): + """ + Initialize the Handler object + + """ + + super().__init__(**kwargs) + + async def identify_order( + self, + my_string: str, + ) -> dict: + """ + Identify an order and return a dictionary + with the order parameters + + Args: + my_string (str): Message + + Returns: + dict with the order parameters: + action, instrument + + """ + try: + action = ( + one_of(self.action_identifier, caseless=True) + .set_results_name("action") + .set_parse_action(pyparsing_common.upcase_tokens) + ) + instrument = Word(alphas + nums).set_results_name("instrument") + + order_grammar = action("action") + Optional(instrument, default=None) + + order = order_grammar.parse_string(instring=my_string, parse_all=False) + logger.debug("Order parsed {}", order) + return order.asDict() + + except Exception as error: + logger.error(error) + return error diff --git a/findmyorder/handler/handler.py b/findmyorder/handler/handler.py new file mode 100644 index 0000000..71230d1 --- /dev/null +++ b/findmyorder/handler/handler.py @@ -0,0 +1,124 @@ +from datetime import datetime, timezone + +from loguru import logger + + +class ParserClient: + """ + Parser Handler Base Class + + Args: + **kwargs: + + Methods: + search(self) + identify_order(self) + get_order(self) + replace_instrument(self) + + """ + + def __init__(self, **kwargs): + """ + Initialize the chat client. + """ + + self.name = kwargs.get("name", None) + self.enabled = kwargs.get("enabled", None) + + self.action_identifier = kwargs.get("action_identifier", None) + self.stop_loss_identifier = kwargs.get("stop_loss_identifier", None) + self.take_profit_identifier = kwargs.get("take_profit_identifier", None) + self.quantity_identifier = kwargs.get("quantity_identifier", None) + self.order_type_identifier = kwargs.get("order_type_identifier", None) + self.leverage_type_identifier = kwargs.get("leverage_type_identifier", None) + self.comment_identifier = kwargs.get("comment_identifier", None) + self.stop_loss = kwargs.get("stop_loss", None) + self.take_profit = kwargs.get("take_profit", None) + self.quantity = kwargs.get("quantity", None) + self.instrument_mapping = kwargs.get("instrument_mapping", None) + self.mapping = kwargs.get("mapping", None) + self.ignore_instrument = kwargs.get("ignore_instrument", None) + + async def identify_order( + self, + my_string: str, + ) -> dict: + """ + Identify an order and return a dictionary + with the order parameters to be implemented in + the child class + + """ + + async def search(self, message: str) -> bool: + """ + Search an order. + + Args: + message (str): Message + + Returns: + bool + + """ + if message: + order_identifier = message.split()[0].lower() + if order_identifier in ( + action.lower() for action in self.action_identifiers + ): + logger.debug("Order identifier found in {}", order_identifier) + return True + + return False + + async def replace_instrument(self, order): + """ + Replace instrument by an alternative instrument, if the + instrument is not in the mapping, it will be ignored. + + Args: + order (dict): + + Returns: + dict + """ + instrument = order["instrument"] + for item in self.mapping: + if item["id"] == instrument: + order["instrument"] = item["alt"] + break + logger.debug("Instrument symbol changed", order) + return order + + async def get_order( + self, + msg: str, + ): + """ + Get an order from a message. The message can be + an order or an order identifier + + Args: + msg (str): Message + + Returns: + dict + + """ + if not await self.search(msg): + logger.debug("No order identified") + return None + order = await self.identify_order(msg) + if isinstance(order, dict): + order["timestamp"] = datetime.now(timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%SZ" + ) + if self.instrument_mapping: + logger.debug("mapping") + await self.replace_instrument(order) + if order["instrument"] in self.ignore_instrument: + logger.debug("Ignoring instrument {}", order["instrument"]) + return + logger.debug("Order identified {}", order) + return order diff --git a/findmyorder/handler/simple.py b/findmyorder/handler/simple.py deleted file mode 100644 index a52d5a5..0000000 --- a/findmyorder/handler/simple.py +++ /dev/null @@ -1,61 +0,0 @@ - # async def identify_order( - # self, - # my_string: str, - # ) -> dict: - # """ - # Identify an order and return a dictionary - # with the order parameters - - # Args: - # my_string (str): Message - - # Returns: - # dict - - # """ - # try: - # action = ( - # one_of(self.action_identifier, caseless=True) - # .set_results_name("action") - # .set_parse_action(pyparsing_common.upcase_tokens) - # ) - # instrument = Word(alphas + nums).set_results_name("instrument") - # stop_loss = Combine( - # Suppress(self.stop_loss_identifier) + Word(nums) - # ).set_results_name("stop_loss") - # take_profit = Combine( - # Suppress(self.take_profit_identifier) + Word(nums) - # ).set_results_name("take_profit") - # quantity = Combine( - # Suppress(self.quantity_identifier) - # + Word(nums) - # + Optional(Suppress("%")) - # ).set_results_name("quantity") - # order_type = one_of( - # self.order_type_identifier, caseless=True - # ).set_results_name("order_type") - # leverage_type = one_of( - # self.leverage_type_identifier, caseless=True - # ).set_results_name("leverage_type") - # comment = Combine( - # Suppress(self.comment_identifier) + Word(alphas) - # ).set_results_name("comment") - - # order_grammar = ( - # action("action") - # + Optional(instrument, default=None) - # + Optional(stop_loss, default=self.stop_loss) - # + Optional(take_profit, default=self.take_profit) - # + Optional(quantity, default=self.quantity) - # + Optional(order_type, default=None) - # + Optional(leverage_type, default=None) - # + Optional(comment, default=None) - # ) - - # order = order_grammar.parse_string(instring=my_string, parse_all=False) - # logger.debug("Order parsed {}", order) - # return order.asDict() - - # except Exception as error: - # logger.error(error) - # return error \ No newline at end of file diff --git a/findmyorder/handler/standard.py b/findmyorder/handler/standard.py new file mode 100644 index 0000000..95f244c --- /dev/null +++ b/findmyorder/handler/standard.py @@ -0,0 +1,91 @@ +""" +Standard Parser + +""" + +from loguru import logger +from pyparsing import ( + Combine, + Optional, + Suppress, + Word, + alphas, + nums, + one_of, + pyparsing_common, +) + +from .handler import ParserClient + + +class StandardHandler(ParserClient): + + def __init__(self, **kwargs): + """ + Initialize the Handler object + + """ + + super().__init__(**kwargs) + + async def identify_order( + self, + my_string: str, + ) -> dict: + """ + Identify an order and return a dictionary + with the order parameters + + Args: + my_string (str): Message + + Returns: + dict + + """ + try: + action = ( + one_of(self.action_identifier, caseless=True) + .set_results_name("action") + .set_parse_action(pyparsing_common.upcase_tokens) + ) + instrument = Word(alphas + nums).set_results_name("instrument") + stop_loss = Combine( + Suppress(self.stop_loss_identifier) + Word(nums) + ).set_results_name("stop_loss") + take_profit = Combine( + Suppress(self.take_profit_identifier) + Word(nums) + ).set_results_name("take_profit") + quantity = Combine( + Suppress(self.quantity_identifier) + + Word(nums) + + Optional(Suppress("%")) + ).set_results_name("quantity") + order_type = one_of( + self.order_type_identifier, caseless=True + ).set_results_name("order_type") + leverage_type = one_of( + self.leverage_type_identifier, caseless=True + ).set_results_name("leverage_type") + comment = Combine( + Suppress(self.comment_identifier) + Word(alphas) + ).set_results_name("comment") + + order_grammar = ( + action("action") + + Optional(instrument, default=None) + + Optional(stop_loss, default=self.stop_loss) + + Optional(take_profit, default=self.take_profit) + + Optional(quantity, default=self.quantity) + + Optional(order_type, default=None) + + Optional(leverage_type, default=None) + + Optional(comment, default=None) + ) + + order = order_grammar.parse_string(instring=my_string, parse_all=False) + logger.debug("Order parsed {}", order) + return order.asDict() + + except Exception as error: + logger.error(error) + return error diff --git a/findmyorder/main.py b/findmyorder/main.py index de36450..681cc35 100644 --- a/findmyorder/main.py +++ b/findmyorder/main.py @@ -3,19 +3,9 @@ """ -from datetime import datetime, timezone +import importlib from loguru import logger -from pyparsing import ( - Combine, - Optional, - Suppress, - Word, - alphas, - nums, - one_of, - pyparsing_common, -) from findmyorder import __version__ @@ -56,42 +46,94 @@ def __init__( self.enabled = settings.findmyorder_enabled if not self.enabled: - return - self.handler = settings.handler or None - self.action_identifier = settings.action_identifier - self.stop_loss_identifier = settings.stop_loss_identifier - self.take_profit_identifier = settings.take_profit_identifier - self.quantity_identifier = settings.quantity_identifier - self.order_type_identifier = settings.order_type_identifier - self.leverage_type_identifier = settings.leverage_type_identifier - self.comment_identifier = settings.comment_identifier - self.stop_loss = settings.stop_loss - self.take_profit = settings.take_profit - self.quantity = settings.quantity - self.instrument_mapping = settings.instrument_mapping - self.mapping = settings.mapping - self.ignore_instrument = settings.ignore_instrument + logger.info("FindMyOrder is disabled. No Parser will be created.") + self.clients = [] + # Create a client for each client in settings.myllm + for name, client_config in settings.findmyorder.items(): + # Skip template and empty string client names + if name in ["", "template"] or not client_config.get("enabled"): + continue + try: + # Create the client + logger.debug("Creating client {}", name) + client = self._create_client(**client_config, name=name) + # If the client has a valid client attribute, append it to the list + if client and getattr(client, "client", None): + self.clients.append(client) + except Exception as e: + # Log the error if the client fails to be created + logger.error(f"Failed to create client {name}: {e}") + + # Log the number of clients that were created + logger.info(f"Loaded {len(self.clients)} clients") + if not self.clients: + logger.warning( + "No clients were created. Check your settings or disable the module." + ) - async def search(self, message: str) -> bool: + def _create_client(self, **kwargs): """ - Search an order. + Create a client based on the given protocol. - Args: - message (str): Message + This function takes in a dictionary of keyword arguments, `kwargs`, + containing the necessary information to create a client. The required + key in `kwargs` is "parser_library", which specifies the parser to use + to identify the order. The value of "parser_library" must match one of the + libraries supported by findmyorder. + + This function retrieves the class used to create the client based on the + value of "parser_library" from the mapping of parser names to client classes + stored in `self.client_classes`. If the value of "parser_library" does not + match any of the libraries supported, the function logs an error message + and returns None. + + If the class used to create the client is found, the function creates a + new instance of the class using the keyword arguments in `kwargs` and + returns it. + + The function returns a client object based on the specified protocol + or None if the library is not supported. + + Parameters: + **kwargs (dict): A dictionary of keyword arguments containing the + necessary information for creating the client. The required key is + "parser_library". Returns: - bool + A client object based on the specified protocol or None if the + library is not supported. + + """ + library = kwargs.get("parser_library", "standard") + client_class = self.client_classes.get(f"{library.capitalize()}Handler") + + if client_class is None: + logger.error(f"library {library} not supported") + return None + + return client_class(**kwargs) + def get_all_client_classes(self): """ - if message: - order_identifier = message.split()[0].lower() - if order_identifier in ( - action.lower() for action in self.action_identifiers - ): - logger.debug("Order identifier found in {}", order_identifier) - return True + Retrieves all client classes from the `findmyorder.handler` module. - return False + This function imports the `findmyorder.handler` module and retrieves + all the classes defined in it. + + The function returns a dictionary where the keys are the + names of the classes and the values are the corresponding + class objects. + + Returns: + dict: A dictionary containing all the client classes + from the `findmyorder.handler` module. + """ + provider_module = importlib.import_module("findmyorder.handler") + return { + name: cls + for name, cls in provider_module.__dict__.items() + if isinstance(cls, type) + } async def get_info(self): """ @@ -101,7 +143,23 @@ async def get_info(self): str """ - return f"{__class__.__name__} {__version__}\n" + version_info = f"ℹī¸ {type(self).__name__} {__version__}\n" + client_info = "".join(f"🔎 {client.name}\n" for client in self.clients) + return version_info + client_info.strip() + + async def search(self, message: str) -> bool: + """ + Search an order. + + Args: + message (str): Message + + Returns: + bool + + """ + for client in self.clients: + await client.search() async def identify_order( self, @@ -118,52 +176,22 @@ async def identify_order( dict """ - try: - action = ( - one_of(self.action_identifier, caseless=True) - .set_results_name("action") - .set_parse_action(pyparsing_common.upcase_tokens) - ) - instrument = Word(alphas + nums).set_results_name("instrument") - stop_loss = Combine( - Suppress(self.stop_loss_identifier) + Word(nums) - ).set_results_name("stop_loss") - take_profit = Combine( - Suppress(self.take_profit_identifier) + Word(nums) - ).set_results_name("take_profit") - quantity = Combine( - Suppress(self.quantity_identifier) - + Word(nums) - + Optional(Suppress("%")) - ).set_results_name("quantity") - order_type = one_of( - self.order_type_identifier, caseless=True - ).set_results_name("order_type") - leverage_type = one_of( - self.leverage_type_identifier, caseless=True - ).set_results_name("leverage_type") - comment = Combine( - Suppress(self.comment_identifier) + Word(alphas) - ).set_results_name("comment") - - order_grammar = ( - action("action") - + Optional(instrument, default=None) - + Optional(stop_loss, default=self.stop_loss) - + Optional(take_profit, default=self.take_profit) - + Optional(quantity, default=self.quantity) - + Optional(order_type, default=None) - + Optional(leverage_type, default=None) - + Optional(comment, default=None) - ) + for client in self.clients: + await client.identify_order() + + async def replace_instrument(self, order): + """ + Replace instrument by an alternative instrument, if the + instrument is not in the mapping, it will be ignored. - order = order_grammar.parse_string(instring=my_string, parse_all=False) - logger.debug("Order parsed {}", order) - return order.asDict() + Args: + order (dict): - except Exception as error: - logger.error(error) - return error + Returns: + dict + """ + for client in self.clients: + await client.replace_instrument() async def get_order( self, @@ -180,38 +208,5 @@ async def get_order( dict """ - if not await self.search(msg): - logger.debug("No order identified") - return None - order = await self.identify_order(msg) - if isinstance(order, dict): - order["timestamp"] = datetime.now(timezone.utc).strftime( - "%Y-%m-%dT%H:%M:%SZ" - ) - if self.instrument_mapping: - logger.debug("mapping") - await self.replace_instrument(order) - if order["instrument"] in self.ignore_instrument: - logger.debug("Ignoring instrument {}", order["instrument"]) - return - logger.debug("Order identified {}", order) - return order - - async def replace_instrument(self, order): - """ - Replace instrument by an alternative instrument, if the - instrument is not in the mapping, it will be ignored. - - Args: - order (dict): - - Returns: - dict - """ - instrument = order["instrument"] - for item in self.mapping: - if item["id"] == instrument: - order["instrument"] = item["alt"] - break - logger.debug("Instrument symbol changed", order) - return order + for client in self.clients: + await client.get_order()