From d75d6b3c6946735498a18528c53091dd5698f51a Mon Sep 17 00:00:00 2001 From: kieran-mackle Date: Tue, 12 Mar 2024 17:09:14 +1000 Subject: [PATCH 1/5] docs(README.md): remove unused badges --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index 7cb34dd0..8f6f619f 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,7 @@ Monthly downloads - - - Build Status - - + Documentation Status From 1586feed143e44a2e9858f0869c30a625d2552a5 Mon Sep 17 00:00:00 2001 From: kieran-mackle Date: Tue, 12 Mar 2024 17:09:56 +1000 Subject: [PATCH 2/5] fix(Notifier): reimplement telegram notifier --- autotrader/autobot.py | 2 +- autotrader/autotrader.py | 22 ++-- autotrader/comms/notifier.py | 2 +- autotrader/comms/tg.py | 235 ++++++++++++----------------------- autotrader/strategy.py | 26 +++- 5 files changed, 121 insertions(+), 166 deletions(-) diff --git a/autotrader/autobot.py b/autotrader/autobot.py index b33f566f..07c3f6d7 100644 --- a/autotrader/autobot.py +++ b/autotrader/autobot.py @@ -176,9 +176,9 @@ def __init__( # Build strategy instantiation arguments strategy_inputs = { "parameters": params, - # "data": self._strat_data, "instrument": self.instrument, "broker": self._broker, + "notifier": self._notifier, } # Instantiate Strategy diff --git a/autotrader/autotrader.py b/autotrader/autotrader.py index 05f8d7f9..fed4644b 100644 --- a/autotrader/autotrader.py +++ b/autotrader/autotrader.py @@ -12,6 +12,7 @@ from ast import literal_eval from threading import Thread from scipy.optimize import brute +from autotrader.comms.tg import Telegram from autotrader.strategy import Strategy from autotrader.autoplot import AutoPlot from autotrader.autobot import AutoTraderBot @@ -107,7 +108,7 @@ def __init__(self) -> None: # Communications self._notify = 0 - self._notification_provider = "" + self._notification_provider: Literal["telegram"] = "" self._notifier = None self._order_summary_fp = None @@ -196,7 +197,7 @@ def configure( feed: Optional[str] = None, home_dir: Optional[str] = None, notify: Optional[int] = 0, - notification_provider: Optional[str] = None, + notification_provider: Optional[Literal["telegram"]] = "telegram", execution_method: Optional[Callable] = None, account_id: Optional[str] = None, environment: Optional[Literal["paper", "live"]] = "paper", @@ -1008,24 +1009,29 @@ def run(self) -> Union[None, Broker]: # Use telegram if "TELEGRAM" not in self._global_config_dict: self.logger.error( - "Please configure your telegram bot in keys.yaml." + "Please configure your telegram bot in keys.yaml. At " + + "a minimum, you must specify the api_key for your bot. You can " + + "also specify your chat_id. If you do not know it, then send your " + + "bot a message before starting AutoTrader again, and it will " + + "be inferred." ) sys.exit() else: # Check keys provided - required_keys = ["api_key", "chat_id"] + required_keys = ["api_key"] for key in required_keys: if key not in self._global_config_dict["TELEGRAM"]: self.logger.error( - f"Please provide {key} under TELEGRAM in keys.yaml." + f"Please define {key} under TELEGRAM in keys.yaml." ) sys.exit() - tg_module = importlib.import_module(f"autotrader.comms.tg") - self._notifier = tg_module.Telegram( + # Instantiate notifier + self._notifier = Telegram( api_token=self._global_config_dict["TELEGRAM"]["api_key"], - chat_id=self._global_config_dict["TELEGRAM"]["chat_id"], + chat_id=self._global_config_dict["TELEGRAM"].get("chat_id"), + logger_kwargs=self._logger_kwargs, ) # Check data feed requirements diff --git a/autotrader/comms/notifier.py b/autotrader/comms/notifier.py index 5117c438..6ef47525 100644 --- a/autotrader/comms/notifier.py +++ b/autotrader/comms/notifier.py @@ -4,7 +4,7 @@ class Notifier(ABC): @abstractmethod - def __init__(self, *args, **kwargs) -> None: + def __init__(self, logger_kwargs: dict = None, *args, **kwargs) -> None: pass @abstractmethod diff --git a/autotrader/comms/tg.py b/autotrader/comms/tg.py index 47c9c783..9b3eb83b 100644 --- a/autotrader/comms/tg.py +++ b/autotrader/comms/tg.py @@ -1,181 +1,106 @@ import os -import telegram +import requests from autotrader.brokers.trading import Order from autotrader.comms.notifier import Notifier -from autotrader.utilities import read_yaml, write_yaml, print_banner -from telegram.ext import Updater, CommandHandler, MessageHandler, Filters +from autotrader.utilities import read_yaml, write_yaml, get_logger class Telegram(Notifier): - def __init__(self, api_token: str = None, chat_id: str = None) -> None: - self.api_token = api_token - self.chat_id = chat_id + """Simple telegram bot to send messages. - if api_token is not None: - self.bot = telegram.Bot(api_token) + To use this, you must first create a Telegram bot via the BotFather. Then, + provide the API token generated here as the api_token. If you do not know + your chat_id, send the bot a message on telegram, and it will be inferred + when this class is instantiated with the api_token. + """ def __repr__(self) -> str: return "AutoTrader-Telegram communication module" - def send_order(self, order: Order, *args, **kwargs) -> None: - side = "long" if order.direction > 0 else "short" - message = ( - f"New {order.instrument} {order.order_type} order created: " - + f"{order.size} units {side}" - ) - - # Create bot and send message - self.bot.send_message(chat_id=self.chat_id, text=message) - - def send_message(self, message: str, *args, **kwargs) -> None: - """A generic method to send a custom message. + def __init__( + self, api_token: str, chat_id: str = None, logger_kwargs: dict = None + ) -> None: + """Instantiate the bot. Parameters ---------- - message : str - The message to be sent. - """ - # Send message - self.bot.send_message(chat_id=self.chat_id, text=message) - - def run_bot(self) -> None: - """A method to initialise your bot and get your chat ID. - This method should be running before you send any messages - to it on Telegram. To start, message the BotFather on - Telegram, and create a new bot. Use the API token provided - to run this method. - - Parameters - ---------- - api_token : str - The API token of your telegram bot. - - Returns - ------- - None. + token : str + The bot API token. + chat_id : str, optional + The default chat_id to send messages to. """ - print_banner() - print(" AutoTrader Telegram Bot") - print("\n Listening for messages...") + # Create logger + logger_kwargs = logger_kwargs if logger_kwargs else {} + self.logger = get_logger(name="telegram_combot", **logger_kwargs) + + # Save attributes + self.token = api_token + if chat_id is None: + # Try get chat ID + self.logger.info( + "No chat ID specified - attempting to load from recent updates." + ) + _, chat_id = self.get_chat_id() + self.chat_id = chat_id + + def get_chat_id(self): + response = requests.get(f"https://api.telegram.org/bot{self.token}/getUpdates") + try: + chat = response.json()["result"][-1]["message"]["chat"] + chat_id = chat["id"] + name = chat["first_name"] + self.logger.info(f"Found chat ID for {name}: {chat_id}.") - # Check for api token - if self.api_token is None: + # Write ID to file for future path = "config/keys.yaml" if os.path.exists(path): - # Look to fetch api token from config file + # Config file exists, proceed config = read_yaml(path) + if "TELEGRAM" in config: - if ( - config["TELEGRAM"] is not None - and "api_key" in config["TELEGRAM"] - ): - self.api_token = config["TELEGRAM"]["api_key"] - - else: - print( - "Please add your Telegram API key to the " - + "keys.yaml file." - ) - return - else: - print( - "Please add a 'TELEGRAM' key to the keys.yaml " - + "file, with a sub-key for the 'api_key'." - ) - return - - updater = Updater(self.api_token, use_context=True) - dp = updater.dispatcher - - dp.add_handler(CommandHandler("start", self._start_command)) - dp.add_handler(CommandHandler("help", self._help_command)) - - dp.add_handler(MessageHandler(Filters.text, self._handle_message)) - - updater.start_polling() - updater.idle() - - @staticmethod - def _start_command( - update, - context, - ): - # Extract user name and chat ID - name = update.message.chat.first_name - chat_id = str(update.message.chat_id) - - # Create response - response = ( - f"Hi {name}, welcome to your very own AutoTrader " - + f"Telegram bot. Your chat ID is {chat_id}. Use this to " - + "set-up trading notifications. Note that this ID has also " - + "been printed to your computer screen for reference." - ) - print("\n Start command activated.") - print(f" Chat ID: {chat_id}") - - # Send response - update.message.reply_text(response) - - @staticmethod - def _help_command( - update, - context, - ): - update.message.reply_text("Help is on the way!") - - # @staticmethod - def _handle_message(self, update, context): - text = str(update.message.text).lower() - - if "id" in text: - chat_id = str(update.message.chat_id) - - if "write" in text: - # Write chat ID to keys.yaml file - path = "config/keys.yaml" - if os.path.exists(path): - # Config file exists, proceed - config = read_yaml(path) - - if "TELEGRAM" in config: + # Telegram in config + self.logger.info("Adding chat_id to configuration file.") + if "chat_id" not in config["TELEGRAM"]: + # Add chat ID config["TELEGRAM"]["chat_id"] = chat_id - else: - config["TELEGRAM"] = { - "api_key": self.api_token, - "chat_id": chat_id, - } - - # Write to file - write_yaml(config, path) - - print("\n Telegram API keys successfully written to file.") - - response = "All done." - else: - response = ( - "I couldn't find your keys.yaml directory. " - + "Make sure you are running the bot from your project " - + "home directory, with config/keys.yaml within it." + # Telegram not in config; insert fresh + self.logger.info( + "Adding telegram configuration details to configuration file." ) + config["TELEGRAM"] = { + "api_key": self.token, + "chat_id": chat_id, + } + + # Write to file + write_yaml(config, path) + + return name, chat_id + + except IndexError: + # No updates to read from + self.logger.error( + "Cannot find chat ID - please make sure you have recently messaged the bot." + ) + return None, None + + def send_message(self, message: str, chat_id: str = None, *args, **kwargs): + if chat_id is None: + chat_id = self.chat_id + self.logger.debug(f"Sending message to {chat_id}: {message}") + url_req = f"https://api.telegram.org/bot{self.token}/sendMessage?chat_id={chat_id}&text={message}" + response = requests.get(url_req) + if response.status_code != 200: + self.logger.error(f"Failed to send message to {chat_id}: {response.reason}") - else: - # Return chat ID - response = f"Your chat ID is {chat_id}." - - if "print" in text or "show" in text: - print(f"\n Chat ID: {chat_id}") - response += " This has also been printed to your computer." - - elif "thank" in text or "ty" in text: - response = "You're welcome." - - else: - response = "I'm not quite ready to respond to that..." - update.message.reply_text(response) + def send_order(self, order: Order, *args, **kwargs) -> None: + side = "long" if order.direction > 0 else "short" + message = ( + f"New {order.instrument} {order.order_type} order created: " + + f"{order.size} units {side}" + ) - @staticmethod - def _error(update, context): - print(f"Update {update} caused error {context.error}") + # Create bot and send message + self.send_message(message) diff --git a/autotrader/strategy.py b/autotrader/strategy.py index ff5cf729..64b2898d 100644 --- a/autotrader/strategy.py +++ b/autotrader/strategy.py @@ -2,6 +2,7 @@ from datetime import datetime from abc import ABC, abstractmethod from autotrader.brokers.broker import Broker +from autotrader.comms.notifier import Notifier from typing import List, Union, TYPE_CHECKING, Optional if TYPE_CHECKING: @@ -11,8 +12,31 @@ class Strategy(ABC): @abstractmethod def __init__( - self, parameters: dict, instrument: str, broker: Broker, *args, **kwargs + self, + parameters: dict, + instrument: str, + broker: Broker, + notifier: Notifier, + *args, + **kwargs ) -> None: + """Instantiate the strategy. This gets called from the AutoTraderBot assigned to + this strategy. + + Parameters + ---------- + parameters : dict + The strategy parameters. + + instrument : str + The instrument to trade. + + broker : Broker + The broker connection. + + notifier : Notifier | None + The notifier object. If notify is not set > 0, then this will be a NoneType object. + """ super().__init__() @abstractmethod From 01cd614792cdd2d600d2d502d89706b803891646 Mon Sep 17 00:00:00 2001 From: kieran-mackle Date: Tue, 12 Mar 2024 17:56:01 +1000 Subject: [PATCH 3/5] fix(AutoTrader): only try instantiate notifier if notify > 0 --- autotrader/autotrader.py | 55 ++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/autotrader/autotrader.py b/autotrader/autotrader.py index fed4644b..1c513f0b 100644 --- a/autotrader/autotrader.py +++ b/autotrader/autotrader.py @@ -1005,34 +1005,35 @@ def run(self) -> Union[None, Broker]: self._global_config_dict = global_config # Create notifier instance - if "telegram" in self._notification_provider.lower(): - # Use telegram - if "TELEGRAM" not in self._global_config_dict: - self.logger.error( - "Please configure your telegram bot in keys.yaml. At " - + "a minimum, you must specify the api_key for your bot. You can " - + "also specify your chat_id. If you do not know it, then send your " - + "bot a message before starting AutoTrader again, and it will " - + "be inferred." - ) - sys.exit() - - else: - # Check keys provided - required_keys = ["api_key"] - for key in required_keys: - if key not in self._global_config_dict["TELEGRAM"]: - self.logger.error( - f"Please define {key} under TELEGRAM in keys.yaml." - ) - sys.exit() + if self._notify > 0: + if "telegram" in self._notification_provider.lower(): + # Use telegram + if "TELEGRAM" not in self._global_config_dict: + self.logger.error( + "Please configure your telegram bot in keys.yaml. At " + + "a minimum, you must specify the api_key for your bot. You can " + + "also specify your chat_id. If you do not know it, then send your " + + "bot a message before starting AutoTrader again, and it will " + + "be inferred." + ) + sys.exit() - # Instantiate notifier - self._notifier = Telegram( - api_token=self._global_config_dict["TELEGRAM"]["api_key"], - chat_id=self._global_config_dict["TELEGRAM"].get("chat_id"), - logger_kwargs=self._logger_kwargs, - ) + else: + # Check keys provided + required_keys = ["api_key"] + for key in required_keys: + if key not in self._global_config_dict["TELEGRAM"]: + self.logger.error( + f"Please define {key} under TELEGRAM in keys.yaml." + ) + sys.exit() + + # Instantiate notifier + self._notifier = Telegram( + api_token=self._global_config_dict["TELEGRAM"]["api_key"], + chat_id=self._global_config_dict["TELEGRAM"].get("chat_id"), + logger_kwargs=self._logger_kwargs, + ) # Check data feed requirements if self._feed is None: From 95ee92cda76d1f7618491b4968405c7cf71f6bb4 Mon Sep 17 00:00:00 2001 From: kieran-mackle Date: Tue, 12 Mar 2024 20:04:03 +1000 Subject: [PATCH 4/5] style(AutoTrader): improve .run return type hint --- autotrader/autotrader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/autotrader/autotrader.py b/autotrader/autotrader.py index 1c513f0b..250e6a3e 100644 --- a/autotrader/autotrader.py +++ b/autotrader/autotrader.py @@ -19,6 +19,7 @@ from autotrader.brokers.broker import Broker from datetime import datetime, timedelta, timezone from typing import Callable, Optional, Literal, Union +from autotrader.brokers.ccxt import Broker as CCXTBroker from autotrader.brokers.virtual import Broker as VirtualBroker from autotrader.utilities import ( read_yaml, @@ -857,7 +858,7 @@ def scan( self._scan_mode = True self._scan_index = scan_index - def run(self) -> Union[None, Broker]: + def run(self) -> Union[None, Broker, VirtualBroker, CCXTBroker]: """Performs essential checks and runs AutoTrader.""" # Create logger self.logger = get_logger( From 6712df91887ef612fa9feca079e85c9ec84ef69b Mon Sep 17 00:00:00 2001 From: kieran-mackle Date: Tue, 12 Mar 2024 20:04:24 +1000 Subject: [PATCH 5/5] fix(ccxt): add exception handling for place_order --- autotrader/brokers/ccxt.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/autotrader/brokers/ccxt.py b/autotrader/brokers/ccxt.py index 6327bb04..a7c7f351 100644 --- a/autotrader/brokers/ccxt.py +++ b/autotrader/brokers/ccxt.py @@ -81,32 +81,37 @@ def place_order(self, order: Order, **kwargs) -> None: """Place an order.""" order() + # Check order meets limits + limits: dict = self.api.markets.get(order.instrument, {}).get("limits", {}) + if limits.get("amount") is not None: + if order.size < limits["amount"]["min"]: + # Order too small + self._logger.warning(f"Order below minimum size: {order}") + return None + # Add order params self._add_params(order) # Submit order to broker if order.order_type == "modify": placed_order = self._modify_order(order) - elif order.order_type in [ - "close", - "reduce", - ]: - raise NotImplementedError( - f"Order type '{order.order_type}' has not " - + "been implemented for the CCXT interface yet." - ) + else: # Regular order side = "buy" if order.direction > 0 else "sell" + # Submit the order - placed_order = self.api.create_order( - symbol=order.instrument, - type=order.order_type, - side=side, - amount=abs(order.size), - price=order.order_limit_price, - params=order.ccxt_params, - ) + try: + placed_order = self.api.create_order( + symbol=order.instrument, + type=order.order_type, + side=side, + amount=abs(order.size), + price=order.order_limit_price, + params=order.ccxt_params, + ) + except Exception as e: + placed_order = e return placed_order