Skip to content

Commit

Permalink
Add support for telegram in simulation mode. (#150)
Browse files Browse the repository at this point in the history
* add support for telegram with sims

* lints

* lints

* add msg if end

* minor

* minor

* v bump
  • Loading branch information
ZENALC committed Dec 25, 2021
1 parent f4e5974 commit bae91af
Show file tree
Hide file tree
Showing 16 changed files with 355 additions and 268 deletions.
72 changes: 39 additions & 33 deletions algobot/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import time
import webbrowser
from datetime import datetime
from typing import Dict, List, Union
from typing import Dict, List, Optional, Union

from PyQt5 import QtCore, uic
from PyQt5.QtCore import QRunnable, QThreadPool
Expand Down Expand Up @@ -36,7 +36,7 @@
show_and_bring_window_to_front)
from algobot.news_scraper import scrape_news
from algobot.slots import initiate_slots
from algobot.telegram_bot import TelegramBot
from algobot.telegram_bot.bot import TelegramBot
from algobot.threads import backtest_thread, bot_thread, optimizer_thread, worker_thread
from algobot.traders.backtester import Backtester
from algobot.traders.real_trader import RealTrader
Expand Down Expand Up @@ -68,7 +68,7 @@ def __init__(self, parent=None):
self.strategy_manager = StrategyManager(self)
self.statistics = Statistics(self) # Loading statistics
self.thread_pool = QThreadPool(self) # Initiating threading pool
self.threads: Dict[int, QRunnable or None] = {BACKTEST: None, SIMULATION: None, LIVE: None, OPTIMIZER: None}
self.threads: Dict[str, QRunnable or None] = {BACKTEST: None, SIMULATION: None, LIVE: None, OPTIMIZER: None}
self.graphs = (
{'graph': self.simulationGraph, 'plots': [], 'label': self.simulationCoordinates, 'enable': True},
{'graph': self.backtestGraph, 'plots': [], 'label': self.backtestCoordinates, 'enable': True},
Expand All @@ -81,15 +81,20 @@ def __init__(self, parent=None):

self.interface_dictionary = get_interface_dictionary(self)
self.advanced_logging = False

# TODO: Why do we need these? Deduce from trader and threads?
self.running_live = False
self.simulation_running_live = False

self.optimizer: Union[Backtester, None] = None
self.backtester: Union[Backtester, None] = None
self.trader: Union[RealTrader, None] = None
self.simulation_trader: Union[SimulationTrader, None] = None
self.simulation_lower_interval_data: Union[Data, None] = None

self.lower_interval_data: Union[Data, None] = None
self.telegram_bot = None
self.simulation_lower_interval_data: Union[Data, None] = None

self.telegram_bot: Optional[TelegramBot] = None
self.tickers = [] # All available tickers.

if algobot.CURRENT_VERSION == UNKNOWN:
Expand Down Expand Up @@ -128,7 +133,7 @@ def inform_telegram(self, message: str, stop_bot: bool = False):
try:
if self.telegram_bot is None:
api_key = self.configuration.telegramApiKey.text()
self.telegram_bot = TelegramBot(gui=self, token=api_key, bot_thread=None)
self.telegram_bot = TelegramBot(gui=self, token=api_key)

chat_id = self.configuration.telegramChatID.text()
if self.configuration.chat_pass:
Expand Down Expand Up @@ -350,8 +355,7 @@ def initiate_optimizer(self):
worker.signals.restore.connect(lambda: self.set_optimizer_buttons(running=False, clear=False))
worker.signals.error.connect(lambda x: create_popup(self, x))
if self.configuration.enabledOptimizerNotification.isChecked():
worker.signals.finished.connect(lambda: self.inform_telegram('Optimizer has finished running.',
stop_bot=True))
worker.signals.finished.connect(lambda: self.inform_telegram('Optimizer has finished running.'))
worker.signals.activity.connect(lambda data: add_to_table(self.optimizerTableWidget, data=data,
insert_date=False))
self.thread_pool.start(worker)
Expand Down Expand Up @@ -532,7 +536,7 @@ def setup_backtester(self, configuration_dictionary: dict):
self.update_backtest_configuration_gui(configuration_dictionary)
self.add_to_backtest_monitor(f"Started backtest with {symbol} data and {interval.lower()} interval periods.")

def check_strategies(self, caller: int) -> bool:
def check_strategies(self, caller: str) -> bool:
"""
Checks if strategies exist based on the caller provided and prompts an appropriate message.
"""
Expand All @@ -549,7 +553,7 @@ def check_strategies(self, caller: int) -> bool:
return confirm_message_box(message, self)
return True

def validate_ticker(self, caller: int):
def validate_ticker(self, caller: str):
"""
Validate ticker provided before running a bot.
"""
Expand All @@ -563,7 +567,7 @@ def validate_ticker(self, caller: int):
return False
return True

def initiate_bot_thread(self, caller: int):
def initiate_bot_thread(self, caller: str):
"""
Main function that initiates bot thread and handles all data-view logic.
:param caller: Caller that decides whether a live bot or simulation bot is run.
Expand All @@ -574,7 +578,7 @@ def initiate_bot_thread(self, caller: int):
return

self.disable_interface(True, caller)
worker = bot_thread.BotThread(gui=self, caller=caller, logger=self.logger)
worker = self.threads[caller] = bot_thread.BotThread(gui=self, caller=caller, logger=self.logger)
worker.signals.small_error.connect(lambda x: create_popup(self, x))
worker.signals.error.connect(self.end_crash_bot_and_create_popup)
worker.signals.activity.connect(self.add_to_monitor)
Expand All @@ -585,14 +589,14 @@ def initiate_bot_thread(self, caller: int):
worker.signals.restore.connect(lambda: self.disable_interface(disable=False, caller=caller))

# All these below are for Telegram.
worker.signals.force_long.connect(lambda: self.force_long(LIVE))
worker.signals.force_short.connect(lambda: self.force_short(LIVE))
worker.signals.exit_position.connect(lambda: self.exit_position(LIVE))
worker.signals.wait_override.connect(lambda: self.exit_position(LIVE, False))
worker.signals.pause.connect(lambda: self.pause_or_resume_bot(LIVE))
worker.signals.resume.connect(lambda: self.pause_or_resume_bot(LIVE))
worker.signals.force_long.connect(self.force_long)
worker.signals.force_short.connect(self.force_short)
worker.signals.exit_position.connect(self.exit_position)
worker.signals.wait_override.connect(lambda *_args: self.exit_position(caller, False))
worker.signals.resume.connect(self.pause_or_resume_bot)
worker.signals.pause.connect(self.pause_or_resume_bot)
worker.signals.set_custom_stop_loss.connect(self.set_custom_stop_loss)
worker.signals.remove_custom_stop_loss.connect(lambda: self.set_custom_stop_loss(LIVE, False))
worker.signals.remove_custom_stop_loss.connect(lambda *_args: self.set_custom_stop_loss(caller, False))
self.thread_pool.start(worker)

def download_progress_update(self, value: int, message: str, caller):
Expand Down Expand Up @@ -627,10 +631,15 @@ def add_end_bot_status(self, caller):
Adds a status update to let user know that bot has been ended.
:param caller: Caller that'll determine which monitor gets updated.
"""
self.threads[caller] = None
if caller == SIMULATION:
self.add_to_monitor(caller, "Killed simulation bot.")
msg = "Killed simulation bot."
else:
self.add_to_monitor(caller, "Killed bot.")
msg = "Killed bot."

self.add_to_monitor(caller, msg)
if self.telegram_bot is not None:
self.inform_telegram(msg)

def reset_bot_interface(self, caller):
"""
Expand Down Expand Up @@ -677,9 +686,6 @@ def end_bot_gracefully(self, caller, callback=None):

if self.configuration.chat_pass:
self.telegram_bot.send_message(self.configuration.telegramChatID.text(), "Bot has been ended.")
if self.telegram_bot:
self.telegram_bot.stop()
self.telegram_bot = None

while not self.trader.completed_loop:
self.running_live = False
Expand All @@ -703,7 +709,7 @@ def end_bot_gracefully(self, caller, callback=None):
if callback:
callback.emit("Dumped all new data to database.")

def end_crash_bot_and_create_popup(self, caller: int, msg: str):
def end_crash_bot_and_create_popup(self, caller: str, msg: str):
"""
Function that force ends bot in the event that it crashes.
"""
Expand Down Expand Up @@ -789,7 +795,7 @@ def update_interface_info(self, caller, value_dict: dict, grouped_dict: dict):
self.handle_position_buttons(caller=caller)
self.handle_custom_stop_loss_buttons(caller=caller)

def update_interface_text(self, caller: int, value_dict: dict):
def update_interface_text(self, caller: str, value_dict: dict):
"""
Updates interface text based on caller and value dictionary provided.
:param caller: Caller that decides which interface gets updated.
Expand All @@ -805,7 +811,7 @@ def update_interface_text(self, caller: int, value_dict: dict):
main_interface_dictionary['tickerValue'].setText(value_dict['tickerValue'])
main_interface_dictionary['positionValue'].setText(value_dict['currentPositionValue'])

def update_main_interface_and_graphs(self, caller: int, value_dict: dict):
def update_main_interface_and_graphs(self, caller: str, value_dict: dict):
"""
Updates main interface GUI elements based on caller.
:param value_dict: Dictionary with trader values in formatted data types.
Expand Down Expand Up @@ -1097,7 +1103,7 @@ def get_activity_table(self, caller):
else:
raise ValueError("Invalid type of caller specified.")

def add_to_monitor(self, caller: int, message: str):
def add_to_monitor(self, caller: str, message: str):
"""
Adds message to the monitor based on caller.
:param caller: Caller that determines which table gets the message.
Expand Down Expand Up @@ -1261,7 +1267,7 @@ def get_preferred_symbol(self) -> Union[None, str]:
else:
return None

def open_binance(self, caller: int = None):
def open_binance(self, caller: str = None):
"""
Opens Binance hyperlink.
:param caller: If provided, it'll open the link to the caller's symbol's link on Binance. By default, if no
Expand All @@ -1278,7 +1284,7 @@ def open_binance(self, caller: int = None):
symbol = f"USDT_{symbol[4:]}" if index == 0 else f"{symbol[:index]}_USDT"
webbrowser.open(f"https://www.binance.com/en/trade/{symbol}")

def open_trading_view(self, caller: int = None):
def open_trading_view(self, caller: str = None):
"""
Opens TradingView hyperlink.
:param caller: If provided, it'll open the link to the caller's symbol's link on TradingView.
Expand Down Expand Up @@ -1352,7 +1358,7 @@ def import_trades(self, caller):
label.setText("Could not import trade history due to data corruption or no file being selected.")
self.logger.exception(str(e))

def create_popup_and_emit_message(self, caller: int, message: str):
def create_popup_and_emit_message(self, caller: str, message: str):
"""
Creates a popup and emits message simultaneously with caller and messages provided.
:param caller: Caller activity monitor to add message to.
Expand All @@ -1361,7 +1367,7 @@ def create_popup_and_emit_message(self, caller: int, message: str):
self.add_to_monitor(caller, message)
create_popup(self, message)

def get_lower_interval_data(self, caller: int) -> Data:
def get_lower_interval_data(self, caller: str) -> Data:
"""
Returns interface's lower interval data object.
:param caller: Caller that determines which lower interval data object gets returned.
Expand All @@ -1374,7 +1380,7 @@ def get_lower_interval_data(self, caller: int) -> Data:
else:
raise TypeError("Invalid type of caller specified.")

def get_trader(self, caller: int) -> Union[SimulationTrader, Backtester]:
def get_trader(self, caller: str) -> Union[SimulationTrader, Backtester]:
"""
Returns a trader object.
:param caller: Caller that decides which trader object gets returned.
Expand Down
2 changes: 1 addition & 1 deletion algobot/algodict.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


# noinspection DuplicatedCode
def get_interface_dictionary(parent, caller: int = None):
def get_interface_dictionary(parent, caller: str = None):
"""
Returns dictionary of objects from QT. Used for DRY principles.
:param parent: Parent object from which to retrieve objects.
Expand Down
2 changes: 1 addition & 1 deletion algobot/graph_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ def smart_update(graph_dict: Dict[str, Any]):
legend_helper(graph_dict, -1)


def update_main_graphs(gui: Interface, caller: int, value_dict: dict):
def update_main_graphs(gui: Interface, caller: str, value_dict: dict):
"""
Updates graphs and moving averages from statistics based on caller.
:param gui: GUI in which to update main graphs.
Expand Down
4 changes: 2 additions & 2 deletions algobot/interface/config_utils/calendar_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@


def get_calendar_dates(config_obj: Configuration,
caller: int = BACKTEST) -> Tuple[Optional[datetime.date], Optional[datetime.date]]:
caller: str = BACKTEST) -> Tuple[Optional[datetime.date], Optional[datetime.date]]:
"""
Returns start end end dates for backtest. If both are the same, returns None.
:param config_obj: Configuration QDialog object (from configuration.py)
Expand All @@ -30,7 +30,7 @@ def get_calendar_dates(config_obj: Configuration,
return start_date, end_date


def setup_calendar(config_obj: Configuration, caller: int = BACKTEST):
def setup_calendar(config_obj: Configuration, caller: str = BACKTEST):
"""
Parses data if needed and then manipulates GUI elements with data timeframe.
:param config_obj: Configuration QDialog object (from configuration.py)
Expand Down
14 changes: 7 additions & 7 deletions algobot/interface/config_utils/data_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from algobot.interface.configuration import Configuration


def import_data(config_obj: Configuration, caller: int = BACKTEST):
def import_data(config_obj: Configuration, caller: str = BACKTEST):
"""
Imports CSV data and loads it.
:param config_obj: Configuration QDialog object (from configuration.py)
Expand All @@ -40,7 +40,7 @@ def import_data(config_obj: Configuration, caller: int = BACKTEST):
setup_calendar(config_obj=config_obj, caller=caller)


def download_data(config_obj: Configuration, caller: int = BACKTEST):
def download_data(config_obj: Configuration, caller: str = BACKTEST):
"""
Loads data from data object. If the data object is empty, it downloads it.
:param config_obj: Configuration QDialog object (from configuration.py)
Expand All @@ -65,7 +65,7 @@ def download_data(config_obj: Configuration, caller: int = BACKTEST):
config_obj.thread_pool.start(thread)


def set_downloaded_data(config_obj: Configuration, data, caller: int = BACKTEST):
def set_downloaded_data(config_obj: Configuration, data, caller: str = BACKTEST):
"""
If download is successful, the data passed is set to backtest data.
:param config_obj: Configuration QDialog object (from configuration.py)
Expand All @@ -86,7 +86,7 @@ def set_downloaded_data(config_obj: Configuration, data, caller: int = BACKTEST)
setup_calendar(config_obj=config_obj, caller=caller)


def stop_download(config_obj: Configuration, caller: int = BACKTEST):
def stop_download(config_obj: Configuration, caller: str = BACKTEST):
"""
Stops download if download is in progress.
:param config_obj: Configuration QDialog object (from configuration.py)
Expand All @@ -98,7 +98,7 @@ def stop_download(config_obj: Configuration, caller: int = BACKTEST):


def set_download_progress(config_obj: Configuration,
progress: int, message: str, caller: int = BACKTEST, enable_stop: bool = True):
progress: int, message: str, caller: str = BACKTEST, enable_stop: bool = True):
"""
Sets download progress and message with parameters passed.
:param config_obj: Configuration QDialog object (from configuration.py)
Expand All @@ -115,7 +115,7 @@ def set_download_progress(config_obj: Configuration,
config_obj.optimizer_backtest_dict[caller]['downloadLabel'].setText(message)


def handle_download_failure(config_obj: Configuration, e, caller: int = BACKTEST):
def handle_download_failure(config_obj: Configuration, e, caller: str = BACKTEST):
"""
If download fails for backtest data, then GUI gets updated.
:param config_obj: Configuration QDialog object (from configuration.py)
Expand All @@ -126,7 +126,7 @@ def handle_download_failure(config_obj: Configuration, e, caller: int = BACKTEST
config_obj.optimizer_backtest_dict[caller]['infoLabel'].setText(f"Error occurred during download: {e}")


def restore_download_state(config_obj: Configuration, caller: int = BACKTEST):
def restore_download_state(config_obj: Configuration, caller: str = BACKTEST):
"""
Restores GUI to normal state.
:param config_obj: Configuration QDialog object (from configuration.py)
Expand Down
8 changes: 4 additions & 4 deletions algobot/interface/config_utils/strategy_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from algobot.interface.configuration import Configuration


def strategy_enabled(config_obj: Configuration, strategy_name: str, caller: int) -> bool:
def strategy_enabled(config_obj: Configuration, strategy_name: str, caller: str) -> bool:
"""
Returns a boolean whether a strategy is enabled or not.
:param config_obj: Configuration QDialog object (from configuration.py)
Expand All @@ -30,7 +30,7 @@ def strategy_enabled(config_obj: Configuration, strategy_name: str, caller: int)
return config_obj.strategy_dict[tab, strategy_name, 'groupBox'].isChecked()


def get_strategies(config_obj: Configuration, caller: int) -> List[Dict[str, Any]]:
def get_strategies(config_obj: Configuration, caller: str) -> List[Dict[str, Any]]:
"""
Returns strategy information from GUI.
:param config_obj: Configuration QDialog object (from configuration.py)
Expand All @@ -46,7 +46,7 @@ def get_strategies(config_obj: Configuration, caller: int) -> List[Dict[str, Any
return strategies


def get_strategy_values(config_obj: Configuration, strategy_name: str, caller: int, verbose: bool = False) -> List[int]:
def get_strategy_values(config_obj: Configuration, strategy_name: str, caller: str, verbose: bool = False) -> List[int]:
"""
This will return values from the strategy provided.
:param config_obj: Configuration QDialog object (from configuration.py)
Expand All @@ -63,7 +63,7 @@ def get_strategy_values(config_obj: Configuration, strategy_name: str, caller: i
return values


def set_strategy_values(config_obj: Configuration, strategy_name: str, caller: int, values):
def set_strategy_values(config_obj: Configuration, strategy_name: str, caller: str, values):
"""
Set GUI values for a strategy based on values passed.
:param config_obj: Configuration QDialog object (from configuration.py)
Expand Down

0 comments on commit bae91af

Please sign in to comment.