Skip to content

Commit bae91af

Browse files
authored
Add support for telegram in simulation mode. (#150)
* add support for telegram with sims * lints * lints * add msg if end * minor * minor * v bump
1 parent f4e5974 commit bae91af

16 files changed

+355
-268
lines changed

algobot/__main__.py

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import time
88
import webbrowser
99
from datetime import datetime
10-
from typing import Dict, List, Union
10+
from typing import Dict, List, Optional, Union
1111

1212
from PyQt5 import QtCore, uic
1313
from PyQt5.QtCore import QRunnable, QThreadPool
@@ -36,7 +36,7 @@
3636
show_and_bring_window_to_front)
3737
from algobot.news_scraper import scrape_news
3838
from algobot.slots import initiate_slots
39-
from algobot.telegram_bot import TelegramBot
39+
from algobot.telegram_bot.bot import TelegramBot
4040
from algobot.threads import backtest_thread, bot_thread, optimizer_thread, worker_thread
4141
from algobot.traders.backtester import Backtester
4242
from algobot.traders.real_trader import RealTrader
@@ -68,7 +68,7 @@ def __init__(self, parent=None):
6868
self.strategy_manager = StrategyManager(self)
6969
self.statistics = Statistics(self) # Loading statistics
7070
self.thread_pool = QThreadPool(self) # Initiating threading pool
71-
self.threads: Dict[int, QRunnable or None] = {BACKTEST: None, SIMULATION: None, LIVE: None, OPTIMIZER: None}
71+
self.threads: Dict[str, QRunnable or None] = {BACKTEST: None, SIMULATION: None, LIVE: None, OPTIMIZER: None}
7272
self.graphs = (
7373
{'graph': self.simulationGraph, 'plots': [], 'label': self.simulationCoordinates, 'enable': True},
7474
{'graph': self.backtestGraph, 'plots': [], 'label': self.backtestCoordinates, 'enable': True},
@@ -81,15 +81,20 @@ def __init__(self, parent=None):
8181

8282
self.interface_dictionary = get_interface_dictionary(self)
8383
self.advanced_logging = False
84+
85+
# TODO: Why do we need these? Deduce from trader and threads?
8486
self.running_live = False
8587
self.simulation_running_live = False
88+
8689
self.optimizer: Union[Backtester, None] = None
8790
self.backtester: Union[Backtester, None] = None
8891
self.trader: Union[RealTrader, None] = None
8992
self.simulation_trader: Union[SimulationTrader, None] = None
90-
self.simulation_lower_interval_data: Union[Data, None] = None
93+
9194
self.lower_interval_data: Union[Data, None] = None
92-
self.telegram_bot = None
95+
self.simulation_lower_interval_data: Union[Data, None] = None
96+
97+
self.telegram_bot: Optional[TelegramBot] = None
9398
self.tickers = [] # All available tickers.
9499

95100
if algobot.CURRENT_VERSION == UNKNOWN:
@@ -128,7 +133,7 @@ def inform_telegram(self, message: str, stop_bot: bool = False):
128133
try:
129134
if self.telegram_bot is None:
130135
api_key = self.configuration.telegramApiKey.text()
131-
self.telegram_bot = TelegramBot(gui=self, token=api_key, bot_thread=None)
136+
self.telegram_bot = TelegramBot(gui=self, token=api_key)
132137

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

535-
def check_strategies(self, caller: int) -> bool:
539+
def check_strategies(self, caller: str) -> bool:
536540
"""
537541
Checks if strategies exist based on the caller provided and prompts an appropriate message.
538542
"""
@@ -549,7 +553,7 @@ def check_strategies(self, caller: int) -> bool:
549553
return confirm_message_box(message, self)
550554
return True
551555

552-
def validate_ticker(self, caller: int):
556+
def validate_ticker(self, caller: str):
553557
"""
554558
Validate ticker provided before running a bot.
555559
"""
@@ -563,7 +567,7 @@ def validate_ticker(self, caller: int):
563567
return False
564568
return True
565569

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

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

587591
# All these below are for Telegram.
588-
worker.signals.force_long.connect(lambda: self.force_long(LIVE))
589-
worker.signals.force_short.connect(lambda: self.force_short(LIVE))
590-
worker.signals.exit_position.connect(lambda: self.exit_position(LIVE))
591-
worker.signals.wait_override.connect(lambda: self.exit_position(LIVE, False))
592-
worker.signals.pause.connect(lambda: self.pause_or_resume_bot(LIVE))
593-
worker.signals.resume.connect(lambda: self.pause_or_resume_bot(LIVE))
592+
worker.signals.force_long.connect(self.force_long)
593+
worker.signals.force_short.connect(self.force_short)
594+
worker.signals.exit_position.connect(self.exit_position)
595+
worker.signals.wait_override.connect(lambda *_args: self.exit_position(caller, False))
596+
worker.signals.resume.connect(self.pause_or_resume_bot)
597+
worker.signals.pause.connect(self.pause_or_resume_bot)
594598
worker.signals.set_custom_stop_loss.connect(self.set_custom_stop_loss)
595-
worker.signals.remove_custom_stop_loss.connect(lambda: self.set_custom_stop_loss(LIVE, False))
599+
worker.signals.remove_custom_stop_loss.connect(lambda *_args: self.set_custom_stop_loss(caller, False))
596600
self.thread_pool.start(worker)
597601

598602
def download_progress_update(self, value: int, message: str, caller):
@@ -627,10 +631,15 @@ def add_end_bot_status(self, caller):
627631
Adds a status update to let user know that bot has been ended.
628632
:param caller: Caller that'll determine which monitor gets updated.
629633
"""
634+
self.threads[caller] = None
630635
if caller == SIMULATION:
631-
self.add_to_monitor(caller, "Killed simulation bot.")
636+
msg = "Killed simulation bot."
632637
else:
633-
self.add_to_monitor(caller, "Killed bot.")
638+
msg = "Killed bot."
639+
640+
self.add_to_monitor(caller, msg)
641+
if self.telegram_bot is not None:
642+
self.inform_telegram(msg)
634643

635644
def reset_bot_interface(self, caller):
636645
"""
@@ -677,9 +686,6 @@ def end_bot_gracefully(self, caller, callback=None):
677686

678687
if self.configuration.chat_pass:
679688
self.telegram_bot.send_message(self.configuration.telegramChatID.text(), "Bot has been ended.")
680-
if self.telegram_bot:
681-
self.telegram_bot.stop()
682-
self.telegram_bot = None
683689

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

706-
def end_crash_bot_and_create_popup(self, caller: int, msg: str):
712+
def end_crash_bot_and_create_popup(self, caller: str, msg: str):
707713
"""
708714
Function that force ends bot in the event that it crashes.
709715
"""
@@ -789,7 +795,7 @@ def update_interface_info(self, caller, value_dict: dict, grouped_dict: dict):
789795
self.handle_position_buttons(caller=caller)
790796
self.handle_custom_stop_loss_buttons(caller=caller)
791797

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

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

1100-
def add_to_monitor(self, caller: int, message: str):
1106+
def add_to_monitor(self, caller: str, message: str):
11011107
"""
11021108
Adds message to the monitor based on caller.
11031109
:param caller: Caller that determines which table gets the message.
@@ -1261,7 +1267,7 @@ def get_preferred_symbol(self) -> Union[None, str]:
12611267
else:
12621268
return None
12631269

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

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

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

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

1377-
def get_trader(self, caller: int) -> Union[SimulationTrader, Backtester]:
1383+
def get_trader(self, caller: str) -> Union[SimulationTrader, Backtester]:
13781384
"""
13791385
Returns a trader object.
13801386
:param caller: Caller that decides which trader object gets returned.

algobot/algodict.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
# noinspection DuplicatedCode
9-
def get_interface_dictionary(parent, caller: int = None):
9+
def get_interface_dictionary(parent, caller: str = None):
1010
"""
1111
Returns dictionary of objects from QT. Used for DRY principles.
1212
:param parent: Parent object from which to retrieve objects.

algobot/graph_helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ def smart_update(graph_dict: Dict[str, Any]):
368368
legend_helper(graph_dict, -1)
369369

370370

371-
def update_main_graphs(gui: Interface, caller: int, value_dict: dict):
371+
def update_main_graphs(gui: Interface, caller: str, value_dict: dict):
372372
"""
373373
Updates graphs and moving averages from statistics based on caller.
374374
:param gui: GUI in which to update main graphs.

algobot/interface/config_utils/calendar_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717

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

3232

33-
def setup_calendar(config_obj: Configuration, caller: int = BACKTEST):
33+
def setup_calendar(config_obj: Configuration, caller: str = BACKTEST):
3434
"""
3535
Parses data if needed and then manipulates GUI elements with data timeframe.
3636
:param config_obj: Configuration QDialog object (from configuration.py)

algobot/interface/config_utils/data_utils.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from algobot.interface.configuration import Configuration
1717

1818

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

4242

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

6767

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

8888

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

9999

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

117117

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

128128

129-
def restore_download_state(config_obj: Configuration, caller: int = BACKTEST):
129+
def restore_download_state(config_obj: Configuration, caller: str = BACKTEST):
130130
"""
131131
Restores GUI to normal state.
132132
:param config_obj: Configuration QDialog object (from configuration.py)

algobot/interface/config_utils/strategy_utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from algobot.interface.configuration import Configuration
1515

1616

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

3232

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

4848

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

6565

66-
def set_strategy_values(config_obj: Configuration, strategy_name: str, caller: int, values):
66+
def set_strategy_values(config_obj: Configuration, strategy_name: str, caller: str, values):
6767
"""
6868
Set GUI values for a strategy based on values passed.
6969
:param config_obj: Configuration QDialog object (from configuration.py)

0 commit comments

Comments
 (0)