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 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..250e6a3e 100644 --- a/autotrader/autotrader.py +++ b/autotrader/autotrader.py @@ -12,12 +12,14 @@ 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 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, @@ -107,7 +109,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 +198,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", @@ -856,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( @@ -1004,29 +1006,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." - ) - sys.exit() - - else: - # Check keys provided - required_keys = ["api_key", "chat_id"] - 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." - ) - 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() - tg_module = importlib.import_module(f"autotrader.comms.tg") - self._notifier = tg_module.Telegram( - api_token=self._global_config_dict["TELEGRAM"]["api_key"], - chat_id=self._global_config_dict["TELEGRAM"]["chat_id"], - ) + 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: 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 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