Merge pull request #877 from freqtrade/feature/improve-rpc

Simplify RPCManager and RPC module to implement other clients
This commit is contained in:
Michael Egger 2018-06-13 15:49:49 +02:00 committed by GitHub
commit 2b74982a1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 346 additions and 488 deletions

View File

@ -2,24 +2,34 @@
This module contains class to define a RPC communications This module contains class to define a RPC communications
""" """
import logging import logging
from abc import abstractmethod
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, date
from decimal import Decimal from decimal import Decimal
from typing import Dict, Tuple, Any from typing import Dict, Tuple, Any, List
import arrow import arrow
import sqlalchemy as sql import sqlalchemy as sql
from pandas import DataFrame
from numpy import mean, nan_to_num from numpy import mean, nan_to_num
from pandas import DataFrame
from freqtrade import exchange from freqtrade import exchange
from freqtrade.misc import shorten_date from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.state import State from freqtrade.state import State
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RPCException(Exception):
"""
Should be raised with a rpc-formatted message in an _rpc_* method
if the required state is wrong, i.e.:
raise RPCException('*Status:* `no active trade`')
"""
pass
class RPC(object): class RPC(object):
""" """
RPC class can be used to have extra feature, like bot data, and access to DB data RPC class can be used to have extra feature, like bot data, and access to DB data
@ -30,20 +40,32 @@ class RPC(object):
:param freqtrade: Instance of a freqtrade bot :param freqtrade: Instance of a freqtrade bot
:return: None :return: None
""" """
self.freqtrade = freqtrade self._freqtrade = freqtrade
def rpc_trade_status(self) -> Tuple[bool, Any]: @abstractmethod
def cleanup(self) -> None:
""" Cleanup pending module resources """
@property
@abstractmethod
def name(self) -> str:
""" Returns the lowercase name of this module """
@abstractmethod
def send_msg(self, msg: str) -> None:
""" Sends a message to all registered rpc modules """
def _rpc_trade_status(self) -> List[str]:
""" """
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
a remotely exposed function a remotely exposed function
:return:
""" """
# Fetch open trade # Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self.freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
return True, '*Status:* `trader is not running`' raise RPCException('*Status:* `trader is not running`')
elif not trades: elif not trades:
return True, '*Status:* `no active trade`' raise RPCException('*Status:* `no active trade`')
else: else:
result = [] result = []
for trade in trades: for trade in trades:
@ -82,14 +104,14 @@ class RPC(object):
) if order else None, ) if order else None,
) )
result.append(message) result.append(message)
return False, result return result
def rpc_status_table(self) -> Tuple[bool, Any]: def _rpc_status_table(self) -> DataFrame:
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self.freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
return True, '*Status:* `trader is not running`' raise RPCException('*Status:* `trader is not running`')
elif not trades: elif not trades:
return True, '*Status:* `no active order`' raise RPCException('*Status:* `no active order`')
else: else:
trades_list = [] trades_list = []
for trade in trades: for trade in trades:
@ -105,22 +127,18 @@ class RPC(object):
columns = ['ID', 'Pair', 'Since', 'Profit'] columns = ['ID', 'Pair', 'Since', 'Profit']
df_statuses = DataFrame.from_records(trades_list, columns=columns) df_statuses = DataFrame.from_records(trades_list, columns=columns)
df_statuses = df_statuses.set_index(columns[0]) df_statuses = df_statuses.set_index(columns[0])
# The style used throughout is to return a tuple return df_statuses
# consisting of (error_occured?, result)
# Another approach would be to just return the
# result, or raise error
return False, df_statuses
def rpc_daily_profit( def _rpc_daily_profit(
self, timescale: int, self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]: stake_currency: str, fiat_display_currency: str) -> List[List[Any]]:
today = datetime.utcnow().date() today = datetime.utcnow().date()
profit_days: Dict[date, Dict] = {} profit_days: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0): if not (isinstance(timescale, int) and timescale > 0):
return True, '*Daily [n]:* `must be an integer greater than 0`' raise RPCException('*Daily [n]:* `must be an integer greater than 0`')
fiat = self.freqtrade.fiat_converter fiat = self._freqtrade.fiat_converter
for day in range(0, timescale): for day in range(0, timescale):
profitday = today - timedelta(days=day) profitday = today - timedelta(days=day)
trades = Trade.query \ trades = Trade.query \
@ -135,7 +153,7 @@ class RPC(object):
'trades': len(trades) 'trades': len(trades)
} }
stats = [ return [
[ [
key, key,
'{value:.8f} {symbol}'.format( '{value:.8f} {symbol}'.format(
@ -157,13 +175,10 @@ class RPC(object):
] ]
for key, value in profit_days.items() for key, value in profit_days.items()
] ]
return False, stats
def rpc_trade_statistics( def _rpc_trade_statistics(
self, stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]: self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
""" """ Returns cumulative profit statistics """
:return: cumulative profit statistics.
"""
trades = Trade.query.order_by(Trade.id).all() trades = Trade.query.order_by(Trade.id).all()
profit_all_coin = [] profit_all_coin = []
@ -201,13 +216,13 @@ class RPC(object):
.order_by(sql.text('profit_sum DESC')).first() .order_by(sql.text('profit_sum DESC')).first()
if not best_pair: if not best_pair:
return True, '*Status:* `no closed trade`' raise RPCException('*Status:* `no closed trade`')
bp_pair, bp_rate = best_pair bp_pair, bp_rate = best_pair
# FIX: we want to keep fiatconverter in a state/environment, # FIX: we want to keep fiatconverter in a state/environment,
# doing this will utilize its caching functionallity, instead we reinitialize it here # doing this will utilize its caching functionallity, instead we reinitialize it here
fiat = self.freqtrade.fiat_converter fiat = self._freqtrade.fiat_converter
# Prepare data to display # Prepare data to display
profit_closed_coin = round(sum(profit_closed_coin), 8) profit_closed_coin = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2) profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
@ -224,9 +239,7 @@ class RPC(object):
fiat_display_currency fiat_display_currency
) )
num = float(len(durations) or 1) num = float(len(durations) or 1)
return ( return {
False,
{
'profit_closed_coin': profit_closed_coin, 'profit_closed_coin': profit_closed_coin,
'profit_closed_percent': profit_closed_percent, 'profit_closed_percent': profit_closed_percent,
'profit_closed_fiat': profit_closed_fiat, 'profit_closed_fiat': profit_closed_fiat,
@ -238,21 +251,17 @@ class RPC(object):
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(), 'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
'best_pair': bp_pair, 'best_pair': bp_pair,
'best_rate': round(bp_rate * 100, 2) 'best_rate': round(bp_rate * 100, 2),
} }
)
def rpc_balance(self, fiat_display_currency: str) -> Tuple[bool, Any]: def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]:
""" """ Returns current account balance per crypto """
:return: current account balance per crypto
"""
output = [] output = []
total = 0.0 total = 0.0
for coin, balance in exchange.get_balances().items(): for coin, balance in exchange.get_balances().items():
if not balance['total']: if not balance['total']:
continue continue
rate = None
if coin == 'BTC': if coin == 'BTC':
rate = 1.0 rate = 1.0
else: else:
@ -272,44 +281,39 @@ class RPC(object):
} }
) )
if total == 0.0: if total == 0.0:
return True, '`All balances are zero.`' raise RPCException('`All balances are zero.`')
fiat = self.freqtrade.fiat_converter fiat = self._freqtrade.fiat_converter
symbol = fiat_display_currency symbol = fiat_display_currency
value = fiat.convert_amount(total, 'BTC', symbol) value = fiat.convert_amount(total, 'BTC', symbol)
return False, (output, total, symbol, value) return output, total, symbol, value
def rpc_start(self) -> Tuple[bool, str]: def _rpc_start(self) -> str:
""" """ Handler for start """
Handler for start. if self._freqtrade.state == State.RUNNING:
""" return '*Status:* `already running`'
if self.freqtrade.state == State.RUNNING:
return True, '*Status:* `already running`'
self.freqtrade.state = State.RUNNING self._freqtrade.state = State.RUNNING
return False, '`Starting trader ...`' return '`Starting trader ...`'
def rpc_stop(self) -> Tuple[bool, str]: def _rpc_stop(self) -> str:
""" """ Handler for stop """
Handler for stop. if self._freqtrade.state == State.RUNNING:
""" self._freqtrade.state = State.STOPPED
if self.freqtrade.state == State.RUNNING: return '`Stopping trader ...`'
self.freqtrade.state = State.STOPPED
return False, '`Stopping trader ...`'
return True, '*Status:* `already stopped`' return '*Status:* `already stopped`'
def rpc_reload_conf(self) -> str: def _rpc_reload_conf(self) -> str:
""" Handler for reload_conf. """ """ Handler for reload_conf. """
self.freqtrade.state = State.RELOAD_CONF self._freqtrade.state = State.RELOAD_CONF
return '*Status:* `Reloading config ...`' return '*Status:* `Reloading config ...`'
# FIX: no test for this!!!! # FIX: no test for this!!!!
def rpc_forcesell(self, trade_id) -> Tuple[bool, Any]: def _rpc_forcesell(self, trade_id) -> None:
""" """
Handler for forcesell <id>. Handler for forcesell <id>.
Sells the given trade at current price Sells the given trade at current price
:return: error or None
""" """
def _exec_forcesell(trade: Trade) -> None: def _exec_forcesell(trade: Trade) -> None:
# Check if there is there is an open order # Check if there is there is an open order
@ -335,17 +339,17 @@ class RPC(object):
# Get current rate and execute sell # Get current rate and execute sell
current_rate = exchange.get_ticker(trade.pair, False)['bid'] current_rate = exchange.get_ticker(trade.pair, False)['bid']
self.freqtrade.execute_sell(trade, current_rate) self._freqtrade.execute_sell(trade, current_rate)
# ---- EOF def _exec_forcesell ---- # ---- EOF def _exec_forcesell ----
if self.freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
return True, '`trader is not running`' raise RPCException('`trader is not running`')
if trade_id == 'all': if trade_id == 'all':
# Execute sell for all open orders # Execute sell for all open orders
for trade in Trade.query.filter(Trade.is_open.is_(True)).all(): for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
_exec_forcesell(trade) _exec_forcesell(trade)
return False, '' return
# Query for trade # Query for trade
trade = Trade.query.filter( trade = Trade.query.filter(
@ -356,19 +360,18 @@ class RPC(object):
).first() ).first()
if not trade: if not trade:
logger.warning('forcesell: Invalid argument received') logger.warning('forcesell: Invalid argument received')
return True, 'Invalid argument.' raise RPCException('Invalid argument.')
_exec_forcesell(trade) _exec_forcesell(trade)
Trade.session.flush() Trade.session.flush()
return False, ''
def rpc_performance(self) -> Tuple[bool, Any]: def _rpc_performance(self) -> List[Dict]:
""" """
Handler for performance. Handler for performance.
Shows a performance statistic from finished trades Shows a performance statistic from finished trades
""" """
if self.freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
return True, '`trader is not running`' raise RPCException('`trader is not running`')
pair_rates = Trade.session.query(Trade.pair, pair_rates = Trade.session.query(Trade.pair,
sql.func.sum(Trade.close_profit).label('profit_sum'), sql.func.sum(Trade.close_profit).label('profit_sum'),
@ -377,19 +380,14 @@ class RPC(object):
.group_by(Trade.pair) \ .group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')) \ .order_by(sql.text('profit_sum DESC')) \
.all() .all()
trades = [] return [
for (pair, rate, count) in pair_rates: {'pair': pair, 'profit': round(rate * 100, 2), 'count': count}
trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count}) for pair, rate, count in pair_rates
]
return False, trades def _rpc_count(self) -> List[Trade]:
""" Returns the number of trades running """
if self._freqtrade.state != State.RUNNING:
raise RPCException('`trader is not running`')
def rpc_count(self) -> Tuple[bool, Any]: return Trade.query.filter(Trade.is_open.is_(True)).all()
"""
Returns the number of trades running
:return: None
"""
if self.freqtrade.state != State.RUNNING:
return True, '`trader is not running`'
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
return False, trades

View File

@ -1,11 +1,10 @@
""" """
This module contains class to manage RPC communications (Telegram, Slack, ...) This module contains class to manage RPC communications (Telegram, Slack, ...)
""" """
from typing import Any, List
import logging import logging
from typing import List
from freqtrade.rpc.telegram import Telegram from freqtrade.rpc.rpc import RPC
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -15,36 +14,23 @@ class RPCManager(object):
Class to manage RPC objects (Telegram, Slack, ...) Class to manage RPC objects (Telegram, Slack, ...)
""" """
def __init__(self, freqtrade) -> None: def __init__(self, freqtrade) -> None:
""" """ Initializes all enabled rpc modules """
Initializes all enabled rpc modules self.registered_modules: List[RPC] = []
:param config: config to use
:return: None
"""
self.freqtrade = freqtrade
self.registered_modules: List[str] = [] # Enable telegram
self.telegram: Any = None if freqtrade.config['telegram'].get('enabled', False):
self._init()
def _init(self) -> None:
"""
Init RPC modules
:return:
"""
if self.freqtrade.config['telegram'].get('enabled', False):
logger.info('Enabling rpc.telegram ...') logger.info('Enabling rpc.telegram ...')
self.registered_modules.append('telegram') from freqtrade.rpc.telegram import Telegram
self.telegram = Telegram(self.freqtrade) self.registered_modules.append(Telegram(freqtrade))
def cleanup(self) -> None: def cleanup(self) -> None:
""" """ Stops all enabled rpc modules """
Stops all enabled rpc modules logger.info('Cleaning up rpc modules ...')
:return: None while self.registered_modules:
""" mod = self.registered_modules.pop()
if 'telegram' in self.registered_modules: logger.debug('Cleaning up rpc.%s ...', mod.name)
logger.info('Cleaning up rpc.telegram ...') mod.cleanup()
self.registered_modules.remove('telegram') del mod
self.telegram.cleanup()
def send_msg(self, msg: str) -> None: def send_msg(self, msg: str) -> None:
""" """
@ -52,6 +38,7 @@ class RPCManager(object):
:param msg: message :param msg: message
:return: None :return: None
""" """
logger.info(msg) logger.info('Sending rpc message: %s', msg)
if 'telegram' in self.registered_modules: for mod in self.registered_modules:
self.telegram.send_msg(msg) logger.debug('Forwarding message to rpc.%s', mod.name)
mod.send_msg(msg)

View File

@ -12,11 +12,12 @@ from telegram.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater from telegram.ext import CommandHandler, Updater
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
from freqtrade.rpc.rpc import RPC from freqtrade.rpc.rpc import RPC, RPCException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.debug('Included module rpc.telegram ...')
def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]: def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]:
""" """
@ -25,9 +26,7 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call
:return: decorated function :return: decorated function
""" """
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
""" """ Decorator logic """
Decorator logic
"""
update = kwargs.get('update') or args[1] update = kwargs.get('update') or args[1]
# Reject unauthorized messages # Reject unauthorized messages
@ -54,9 +53,12 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call
class Telegram(RPC): class Telegram(RPC):
""" """ This class handles all telegram communication """
Telegram, this class send messages to Telegram
""" @property
def name(self) -> str:
return "telegram"
def __init__(self, freqtrade) -> None: def __init__(self, freqtrade) -> None:
""" """
Init the Telegram call, and init the super class RPC Init the Telegram call, and init the super class RPC
@ -74,12 +76,7 @@ class Telegram(RPC):
Initializes this module with the given config, Initializes this module with the given config,
registers all known command handlers registers all known command handlers
and starts polling for message updates and starts polling for message updates
:param config: config to use
:return: None
""" """
if not self.is_enabled():
return
self._updater = Updater(token=self._config['telegram']['token'], workers=0) self._updater = Updater(token=self._config['telegram']['token'], workers=0)
# Register command handler and start telegram message polling # Register command handler and start telegram message polling
@ -115,16 +112,11 @@ class Telegram(RPC):
Stops all running telegram threads. Stops all running telegram threads.
:return: None :return: None
""" """
if not self.is_enabled():
return
self._updater.stop() self._updater.stop()
def is_enabled(self) -> bool: def send_msg(self, msg: str) -> None:
""" """ Send a message to telegram channel """
Returns True if the telegram module is activated, False otherwise self._send_msg(msg)
"""
return bool(self._config.get('telegram', {}).get('enabled', False))
@authorized_only @authorized_only
def _status(self, bot: Bot, update: Update) -> None: def _status(self, bot: Bot, update: Update) -> None:
@ -143,13 +135,11 @@ class Telegram(RPC):
self._status_table(bot, update) self._status_table(bot, update)
return return
# Fetch open trade try:
(error, trades) = self.rpc_trade_status() for trade_msg in self._rpc_trade_status():
if error: self._send_msg(trade_msg, bot=bot)
self.send_msg(trades, bot=bot) except RPCException as e:
else: self._send_msg(str(e), bot=bot)
for trademsg in trades:
self.send_msg(trademsg, bot=bot)
@authorized_only @authorized_only
def _status_table(self, bot: Bot, update: Update) -> None: def _status_table(self, bot: Bot, update: Update) -> None:
@ -160,15 +150,12 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
# Fetch open trade try:
(err, df_statuses) = self.rpc_status_table() df_statuses = self._rpc_status_table()
if err:
self.send_msg(df_statuses, bot=bot)
else:
message = tabulate(df_statuses, headers='keys', tablefmt='simple') message = tabulate(df_statuses, headers='keys', tablefmt='simple')
message = "<pre>{}</pre>".format(message) self._send_msg("<pre>{}</pre>".format(message), parse_mode=ParseMode.HTML)
except RPCException as e:
self.send_msg(message, parse_mode=ParseMode.HTML) self._send_msg(str(e), bot=bot)
@authorized_only @authorized_only
def _daily(self, bot: Bot, update: Update) -> None: def _daily(self, bot: Bot, update: Update) -> None:
@ -183,14 +170,12 @@ class Telegram(RPC):
timescale = int(update.message.text.replace('/daily', '').strip()) timescale = int(update.message.text.replace('/daily', '').strip())
except (TypeError, ValueError): except (TypeError, ValueError):
timescale = 7 timescale = 7
(error, stats) = self.rpc_daily_profit( try:
stats = self._rpc_daily_profit(
timescale, timescale,
self._config['stake_currency'], self._config['stake_currency'],
self._config['fiat_display_currency'] self._config['fiat_display_currency']
) )
if error:
self.send_msg(stats, bot=bot)
else:
stats = tabulate(stats, stats = tabulate(stats,
headers=[ headers=[
'Day', 'Day',
@ -199,11 +184,10 @@ class Telegram(RPC):
], ],
tablefmt='simple') tablefmt='simple')
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\ message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\
.format( .format(timescale, stats)
timescale, self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
stats except RPCException as e:
) self._send_msg(str(e), bot=bot)
self.send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
@authorized_only @authorized_only
def _profit(self, bot: Bot, update: Update) -> None: def _profit(self, bot: Bot, update: Update) -> None:
@ -214,13 +198,10 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, stats) = self.rpc_trade_statistics( try:
stats = self._rpc_trade_statistics(
self._config['stake_currency'], self._config['stake_currency'],
self._config['fiat_display_currency'] self._config['fiat_display_currency'])
)
if error:
self.send_msg(stats, bot=bot)
return
# Message to display # Message to display
markdown_msg = "*ROI:* Close trades\n" \ markdown_msg = "*ROI:* Close trades\n" \
@ -250,19 +231,16 @@ class Telegram(RPC):
best_pair=stats['best_pair'], best_pair=stats['best_pair'],
best_rate=stats['best_rate'] best_rate=stats['best_rate']
) )
self.send_msg(markdown_msg, bot=bot) self._send_msg(markdown_msg, bot=bot)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only @authorized_only
def _balance(self, bot: Bot, update: Update) -> None: def _balance(self, bot: Bot, update: Update) -> None:
""" """ Handler for /balance """
Handler for /balance try:
""" currencys, total, symbol, value = \
(error, result) = self.rpc_balance(self._config['fiat_display_currency']) self._rpc_balance(self._config['fiat_display_currency'])
if error:
self.send_msg('`All balances are zero.`')
return
(currencys, total, symbol, value) = result
output = '' output = ''
for currency in currencys: for currency in currencys:
output += "*{currency}:*\n" \ output += "*{currency}:*\n" \
@ -274,7 +252,9 @@ class Telegram(RPC):
output += "\n*Estimated Value*:\n" \ output += "\n*Estimated Value*:\n" \
"\t`BTC: {0: .8f}`\n" \ "\t`BTC: {0: .8f}`\n" \
"\t`{1}: {2: .2f}`\n".format(total, symbol, value) "\t`{1}: {2: .2f}`\n".format(total, symbol, value)
self.send_msg(output) self._send_msg(output, bot=bot)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only @authorized_only
def _start(self, bot: Bot, update: Update) -> None: def _start(self, bot: Bot, update: Update) -> None:
@ -285,9 +265,8 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, msg) = self.rpc_start() msg = self._rpc_start()
if error: self._send_msg(msg, bot=bot)
self.send_msg(msg, bot=bot)
@authorized_only @authorized_only
def _stop(self, bot: Bot, update: Update) -> None: def _stop(self, bot: Bot, update: Update) -> None:
@ -298,8 +277,8 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, msg) = self.rpc_stop() msg = self._rpc_stop()
self.send_msg(msg, bot=bot) self._send_msg(msg, bot=bot)
@authorized_only @authorized_only
def _reload_conf(self, bot: Bot, update: Update) -> None: def _reload_conf(self, bot: Bot, update: Update) -> None:
@ -310,8 +289,8 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
msg = self.rpc_reload_conf() msg = self._rpc_reload_conf()
self.send_msg(msg, bot=bot) self._send_msg(msg, bot=bot)
@authorized_only @authorized_only
def _forcesell(self, bot: Bot, update: Update) -> None: def _forcesell(self, bot: Bot, update: Update) -> None:
@ -324,10 +303,10 @@ class Telegram(RPC):
""" """
trade_id = update.message.text.replace('/forcesell', '').strip() trade_id = update.message.text.replace('/forcesell', '').strip()
(error, message) = self.rpc_forcesell(trade_id) try:
if error: self._rpc_forcesell(trade_id)
self.send_msg(message, bot=bot) except RPCException as e:
return self._send_msg(str(e), bot=bot)
@authorized_only @authorized_only
def _performance(self, bot: Bot, update: Update) -> None: def _performance(self, bot: Bot, update: Update) -> None:
@ -338,11 +317,8 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, trades) = self.rpc_performance() try:
if error: trades = self._rpc_performance()
self.send_msg(trades, bot=bot)
return
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format( stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
index=i + 1, index=i + 1,
pair=trade['pair'], pair=trade['pair'],
@ -350,7 +326,9 @@ class Telegram(RPC):
count=trade['count'] count=trade['count']
) for i, trade in enumerate(trades)) ) for i, trade in enumerate(trades))
message = '<b>Performance:</b>\n{}'.format(stats) message = '<b>Performance:</b>\n{}'.format(stats)
self.send_msg(message, parse_mode=ParseMode.HTML) self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only @authorized_only
def _count(self, bot: Bot, update: Update) -> None: def _count(self, bot: Bot, update: Update) -> None:
@ -361,11 +339,8 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, trades) = self.rpc_count() try:
if error: trades = self._rpc_count()
self.send_msg(trades, bot=bot)
return
message = tabulate({ message = tabulate({
'current': [len(trades)], 'current': [len(trades)],
'max': [self._config['max_open_trades']], 'max': [self._config['max_open_trades']],
@ -373,7 +348,9 @@ class Telegram(RPC):
}, headers=['current', 'max', 'total stake'], tablefmt='simple') }, headers=['current', 'max', 'total stake'], tablefmt='simple')
message = "<pre>{}</pre>".format(message) message = "<pre>{}</pre>".format(message)
logger.debug(message) logger.debug(message)
self.send_msg(message, parse_mode=ParseMode.HTML) self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only @authorized_only
def _help(self, bot: Bot, update: Update) -> None: def _help(self, bot: Bot, update: Update) -> None:
@ -399,7 +376,7 @@ class Telegram(RPC):
"*/help:* `This help message`\n" \ "*/help:* `This help message`\n" \
"*/version:* `Show version`" "*/version:* `Show version`"
self.send_msg(message, bot=bot) self._send_msg(message, bot=bot)
@authorized_only @authorized_only
def _version(self, bot: Bot, update: Update) -> None: def _version(self, bot: Bot, update: Update) -> None:
@ -410,9 +387,9 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
self.send_msg('*Version:* `{}`'.format(__version__), bot=bot) self._send_msg('*Version:* `{}`'.format(__version__), bot=bot)
def send_msg(self, msg: str, bot: Bot = None, def _send_msg(self, msg: str, bot: Bot = None,
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
""" """
Send given markdown message Send given markdown message
@ -421,9 +398,6 @@ class Telegram(RPC):
:param parse_mode: telegram parse mode :param parse_mode: telegram parse mode
:return: None :return: None
""" """
if not self.is_enabled():
return
bot = bot or self._updater.bot bot = bot or self._updater.bot
keyboard = [['/daily', '/profit', '/balance'], keyboard = [['/daily', '/profit', '/balance'],

View File

@ -7,9 +7,11 @@ Unit test file for rpc/rpc.py
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC from freqtrade.rpc.rpc import RPC, RPCException
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
@ -29,7 +31,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -41,19 +43,16 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_trade_status() with pytest.raises(RPCException, match=r'.*trader is not running*'):
assert error rpc._rpc_trade_status()
assert 'trader is not running' in result
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_trade_status() with pytest.raises(RPCException, match=r'.*no active trade*'):
assert error rpc._rpc_trade_status()
assert 'no active trade' in result
freqtradebot.create_trade() freqtradebot.create_trade()
(error, result) = rpc.rpc_trade_status() trades = rpc._rpc_trade_status()
assert not error trade = trades[0]
trade = result[0]
result_message = [ result_message = [
'*Trade ID:* `1`\n' '*Trade ID:* `1`\n'
@ -68,7 +67,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'*Current Profit:* `-0.59%`\n' '*Current Profit:* `-0.59%`\n'
'*Open Order:* `(limit buy rem=0.00000000)`' '*Open Order:* `(limit buy rem=0.00000000)`'
] ]
assert result == result_message assert trades == result_message
assert trade.find('[ETH/BTC]') >= 0 assert trade.find('[ETH/BTC]') >= 0
@ -78,7 +77,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -90,17 +89,15 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_status_table() with pytest.raises(RPCException, match=r'.*\*Status:\* `trader is not running``*'):
assert error rpc._rpc_status_table()
assert '*Status:* `trader is not running`' in result
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_status_table() with pytest.raises(RPCException, match=r'.*\*Status:\* `no active order`*'):
assert error rpc._rpc_status_table()
assert '*Status:* `no active order`' in result
freqtradebot.create_trade() freqtradebot.create_trade()
(error, result) = rpc.rpc_status_table() result = rpc._rpc_status_table()
assert 'just now' in result['Since'].all() assert 'just now' in result['Since'].all()
assert 'ETH/BTC' in result['Pair'].all() assert 'ETH/BTC' in result['Pair'].all()
assert '-0.59%' in result['Profit'].all() assert '-0.59%' in result['Profit'].all()
@ -113,7 +110,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -140,8 +137,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
# Try valid data # Try valid data
update.message.text = '/daily 2' update.message.text = '/daily 2'
(error, days) = rpc.rpc_daily_profit(7, stake_currency, fiat_display_currency) days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
assert not error
assert len(days) == 7 assert len(days) == 7
for day in days: for day in days:
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD'] # [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
@ -154,9 +150,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
assert str(days[0][0]) == str(datetime.utcnow().date()) assert str(days[0][0]) == str(datetime.utcnow().date())
# Try invalid data # Try invalid data
(error, days) = rpc.rpc_daily_profit(0, stake_currency, fiat_display_currency) with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
assert error rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
assert days.find('must be an integer greater than 0') >= 0
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
@ -170,7 +165,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
) )
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -184,9 +179,8 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency) with pytest.raises(RPCException, match=r'.*no closed trade*'):
assert error rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert stats.find('no closed trade') >= 0
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trade()
@ -219,8 +213,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert not error
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05) assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_closed_percent'], 6.2) assert prec_satoshi(stats['profit_closed_percent'], 6.2)
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255) assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
@ -248,7 +241,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
) )
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -281,8 +274,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
for trade in Trade.query.order_by(Trade.id).all(): for trade in Trade.query.order_by(Trade.id).all():
trade.open_rate = None trade.open_rate = None
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert not error
assert prec_satoshi(stats['profit_closed_coin'], 0) assert prec_satoshi(stats['profit_closed_coin'], 0)
assert prec_satoshi(stats['profit_closed_percent'], 0) assert prec_satoshi(stats['profit_closed_percent'], 0)
assert prec_satoshi(stats['profit_closed_fiat'], 0) assert prec_satoshi(stats['profit_closed_fiat'], 0)
@ -320,7 +312,7 @@ def test_rpc_balance_handle(default_conf, mocker):
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
) )
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -330,18 +322,16 @@ def test_rpc_balance_handle(default_conf, mocker):
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency']) output, total, symbol, value = rpc._rpc_balance(default_conf['fiat_display_currency'])
assert not error assert prec_satoshi(total, 12)
(trade, x, y, z) = res assert prec_satoshi(value, 180000)
assert prec_satoshi(x, 12) assert 'USD' in symbol
assert prec_satoshi(z, 180000) assert len(output) == 1
assert 'USD' in y assert 'BTC' in output[0]['currency']
assert len(trade) == 1 assert prec_satoshi(output[0]['available'], 10)
assert 'BTC' in trade[0]['currency'] assert prec_satoshi(output[0]['balance'], 12)
assert prec_satoshi(trade[0]['available'], 10) assert prec_satoshi(output[0]['pending'], 2)
assert prec_satoshi(trade[0]['balance'], 12) assert prec_satoshi(output[0]['est_btc'], 12)
assert prec_satoshi(trade[0]['pending'], 2)
assert prec_satoshi(trade[0]['est_btc'], 12)
def test_rpc_start(mocker, default_conf) -> None: def test_rpc_start(mocker, default_conf) -> None:
@ -350,7 +340,7 @@ def test_rpc_start(mocker, default_conf) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -361,13 +351,11 @@ def test_rpc_start(mocker, default_conf) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_start() result = rpc._rpc_start()
assert not error
assert '`Starting trader ...`' in result assert '`Starting trader ...`' in result
assert freqtradebot.state == State.RUNNING assert freqtradebot.state == State.RUNNING
(error, result) = rpc.rpc_start() result = rpc._rpc_start()
assert error
assert '*Status:* `already running`' in result assert '*Status:* `already running`' in result
assert freqtradebot.state == State.RUNNING assert freqtradebot.state == State.RUNNING
@ -378,7 +366,7 @@ def test_rpc_stop(mocker, default_conf) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -389,13 +377,11 @@ def test_rpc_stop(mocker, default_conf) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_stop() result = rpc._rpc_stop()
assert not error
assert '`Stopping trader ...`' in result assert '`Stopping trader ...`' in result
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
(error, result) = rpc.rpc_stop() result = rpc._rpc_stop()
assert error
assert '*Status:* `already stopped`' in result assert '*Status:* `already stopped`' in result
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
@ -406,7 +392,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@ -428,36 +414,26 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, res) = rpc.rpc_forcesell(None) with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
assert error rpc._rpc_forcesell(None)
assert res == '`trader is not running`'
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
(error, res) = rpc.rpc_forcesell(None) with pytest.raises(RPCException, match=r'.*Invalid argument.*'):
assert error rpc._rpc_forcesell(None)
assert res == 'Invalid argument.'
(error, res) = rpc.rpc_forcesell('all') rpc._rpc_forcesell('all')
assert not error
assert res == ''
freqtradebot.create_trade() freqtradebot.create_trade()
(error, res) = rpc.rpc_forcesell('all') rpc._rpc_forcesell('all')
assert not error
assert res == ''
(error, res) = rpc.rpc_forcesell('1') rpc._rpc_forcesell('1')
assert not error
assert res == ''
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, res) = rpc.rpc_forcesell(None) with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
assert error rpc._rpc_forcesell(None)
assert res == '`trader is not running`'
(error, res) = rpc.rpc_forcesell('all') with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
assert error rpc._rpc_forcesell('all')
assert res == '`trader is not running`'
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
@ -475,9 +451,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
) )
# check that the trade is called, which is done by ensuring exchange.cancel_order is called # check that the trade is called, which is done by ensuring exchange.cancel_order is called
# and trade amount is updated # and trade amount is updated
(error, res) = rpc.rpc_forcesell('1') rpc._rpc_forcesell('1')
assert not error
assert res == ''
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert trade.amount == filled_amount assert trade.amount == filled_amount
@ -495,9 +469,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
} }
) )
# check that the trade is called, which is done by ensuring exchange.cancel_order is called # check that the trade is called, which is done by ensuring exchange.cancel_order is called
(error, res) = rpc.rpc_forcesell('2') rpc._rpc_forcesell('2')
assert not error
assert res == ''
assert cancel_order_mock.call_count == 2 assert cancel_order_mock.call_count == 2
assert trade.amount == amount assert trade.amount == amount
@ -511,9 +483,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
'side': 'sell' 'side': 'sell'
} }
) )
(error, res) = rpc.rpc_forcesell('3') rpc._rpc_forcesell('3')
assert not error
assert res == ''
# status quo, no exchange calls # status quo, no exchange calls
assert cancel_order_mock.call_count == 2 assert cancel_order_mock.call_count == 2
@ -525,7 +495,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -550,8 +520,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
(error, res) = rpc.rpc_performance() res = rpc._rpc_performance()
assert not error
assert len(res) == 1 assert len(res) == 1
assert res[0]['pair'] == 'ETH/BTC' assert res[0]['pair'] == 'ETH/BTC'
assert res[0]['count'] == 1 assert res[0]['count'] == 1
@ -564,7 +533,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -576,14 +545,12 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
(error, trades) = rpc.rpc_count() trades = rpc._rpc_count()
nb_trades = len(trades) nb_trades = len(trades)
assert not error
assert nb_trades == 0 assert nb_trades == 0
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trade()
(error, trades) = rpc.rpc_count() trades = rpc._rpc_count()
nb_trades = len(trades) nb_trades = len(trades)
assert not error
assert nb_trades == 1 assert nb_trades == 1

View File

@ -7,49 +7,35 @@ from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.rpc.rpc_manager import RPCManager from freqtrade.rpc.rpc_manager import RPCManager
from freqtrade.rpc.telegram import Telegram
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
def test_rpc_manager_object() -> None: def test_rpc_manager_object() -> None:
""" """ Test the Arguments object has the mandatory methods """
Test the Arguments object has the mandatory methods
:return: None
"""
assert hasattr(RPCManager, '_init')
assert hasattr(RPCManager, 'send_msg') assert hasattr(RPCManager, 'send_msg')
assert hasattr(RPCManager, 'cleanup') assert hasattr(RPCManager, 'cleanup')
def test__init__(mocker, default_conf) -> None: def test__init__(mocker, default_conf) -> None:
""" """ Test __init__() method """
Test __init__() method conf = deepcopy(default_conf)
""" conf['telegram']['enabled'] = False
init_mock = mocker.patch('freqtrade.rpc.rpc_manager.RPCManager._init', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
assert rpc_manager.freqtrade == freqtradebot
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []
assert rpc_manager.telegram is None
assert init_mock.call_count == 1
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None: def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
""" """ Test _init() method with Telegram disabled """
Test _init() method with Telegram disabled
"""
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False conf['telegram']['enabled'] = False
freqtradebot = get_patched_freqtradebot(mocker, conf) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
rpc_manager = RPCManager(freqtradebot)
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples) assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []
assert rpc_manager.telegram is None
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None: def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
@ -59,14 +45,12 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
rpc_manager = RPCManager(freqtradebot)
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples) assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
len_modules = len(rpc_manager.registered_modules) len_modules = len(rpc_manager.registered_modules)
assert len_modules == 1 assert len_modules == 1
assert 'telegram' in rpc_manager.registered_modules assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
assert isinstance(rpc_manager.telegram, Telegram)
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None: def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
@ -99,11 +83,11 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
# Check we have Telegram as a registered modules # Check we have Telegram as a registered modules
assert 'telegram' in rpc_manager.registered_modules assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
rpc_manager.cleanup() rpc_manager.cleanup()
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples) assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
assert 'telegram' not in rpc_manager.registered_modules assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules]
assert telegram_mock.call_count == 1 assert telegram_mock.call_count == 1
@ -120,7 +104,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test') rpc_manager.send_msg('test')
assert log_has('test', caplog.record_tuples) assert log_has('Sending rpc message: test', caplog.record_tuples)
assert telegram_mock.call_count == 0 assert telegram_mock.call_count == 0
@ -135,5 +119,5 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test') rpc_manager.send_msg('test')
assert log_has('test', caplog.record_tuples) assert log_has('Sending rpc message: test', caplog.record_tuples)
assert telegram_mock.call_count == 1 assert telegram_mock.call_count == 1

View File

@ -32,6 +32,9 @@ class DummyCls(Telegram):
super().__init__(freqtrade) super().__init__(freqtrade)
self.state = {'called': False} self.state = {'called': False}
def _init(self):
pass
@authorized_only @authorized_only
def dummy_handler(self, *args, **kwargs) -> None: def dummy_handler(self, *args, **kwargs) -> None:
""" """
@ -60,9 +63,7 @@ def test__init__(default_conf, mocker) -> None:
def test_init(default_conf, mocker, caplog) -> None: def test_init(default_conf, mocker, caplog) -> None:
""" """ Test _init() method """
Test _init() method
"""
start_polling = MagicMock() start_polling = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
@ -80,21 +81,6 @@ def test_init(default_conf, mocker, caplog) -> None:
assert log_has(message_str, caplog.record_tuples) assert log_has(message_str, caplog.record_tuples)
def test_init_disabled(default_conf, mocker, caplog) -> None:
"""
Test _init() method when Telegram is disabled
"""
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
Telegram(get_patched_freqtradebot(mocker, conf))
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
"['count'], ['help'], ['version']]"
assert not log_has(message_str, caplog.record_tuples)
def test_cleanup(default_conf, mocker) -> None: def test_cleanup(default_conf, mocker) -> None:
""" """
Test cleanup() method Test cleanup() method
@ -103,44 +89,11 @@ def test_cleanup(default_conf, mocker) -> None:
updater_mock.stop = MagicMock() updater_mock.stop = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
# not enabled telegram = Telegram(get_patched_freqtradebot(mocker, default_conf))
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
telegram.cleanup()
assert telegram._updater is None
assert updater_mock.call_count == 0
assert not hasattr(telegram._updater, 'stop')
assert updater_mock.stop.call_count == 0
# enabled
conf['telegram']['enabled'] = True
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
telegram.cleanup() telegram.cleanup()
assert telegram._updater.stop.call_count == 1 assert telegram._updater.stop.call_count == 1
def test_is_enabled(default_conf, mocker) -> None:
"""
Test is_enabled() method
"""
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
telegram = Telegram(get_patched_freqtradebot(mocker, default_conf))
assert telegram.is_enabled()
def test_is_not_enabled(default_conf, mocker) -> None:
"""
Test is_enabled() method
"""
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
assert not telegram.is_enabled()
def test_authorized_only(default_conf, mocker, caplog) -> None: def test_authorized_only(default_conf, mocker, caplog) -> None:
""" """
Test authorized_only() method when we are authorized Test authorized_only() method when we are authorized
@ -256,9 +209,9 @@ def test_status(default_conf, update, mocker, fee, ticker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
rpc_trade_status=MagicMock(return_value=(False, [1, 2, 3])), _rpc_trade_status=MagicMock(return_value=[1, 2, 3]),
_status_table=status_table, _status_table=status_table,
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -296,7 +249,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
_status_table=status_table, _status_table=status_table,
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -341,7 +294,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -397,7 +350,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -465,7 +418,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -506,7 +459,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -604,7 +557,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
@ -634,7 +587,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
@ -656,7 +609,7 @@ def test_start_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -667,7 +620,7 @@ def test_start_handle(default_conf, update, mocker) -> None:
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
telegram._start(bot=MagicMock(), update=update) telegram._start(bot=MagicMock(), update=update)
assert freqtradebot.state == State.RUNNING assert freqtradebot.state == State.RUNNING
assert msg_mock.call_count == 0 assert msg_mock.call_count == 1
def test_start_handle_already_running(default_conf, update, mocker) -> None: def test_start_handle_already_running(default_conf, update, mocker) -> None:
@ -680,7 +633,7 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -705,7 +658,7 @@ def test_stop_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -730,7 +683,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -753,7 +706,7 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -898,7 +851,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
@ -940,7 +893,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
@ -981,7 +934,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
@ -1004,7 +957,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
@ -1047,7 +1000,7 @@ def test_help_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
@ -1067,7 +1020,7 @@ def test_version_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
@ -1089,13 +1042,8 @@ def test_send_msg(default_conf, mocker) -> None:
freqtradebot = FreqtradeBot(conf) freqtradebot = FreqtradeBot(conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = False
telegram.send_msg('test', bot)
assert not bot.method_calls
bot.reset_mock()
telegram._config['telegram']['enabled'] = True telegram._config['telegram']['enabled'] = True
telegram.send_msg('test', bot) telegram._send_msg('test', bot)
assert len(bot.method_calls) == 1 assert len(bot.method_calls) == 1
@ -1113,7 +1061,7 @@ def test_send_msg_network_error(default_conf, mocker, caplog) -> None:
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = True telegram._config['telegram']['enabled'] = True
telegram.send_msg('test', bot) telegram._send_msg('test', bot)
# Bot should've tried to send it twice # Bot should've tried to send it twice
assert len(bot.method_calls) == 2 assert len(bot.method_calls) == 2

View File

@ -57,7 +57,7 @@ def patch_RPCManager(mocker) -> MagicMock:
:param mocker: mocker to patch RPCManager class :param mocker: mocker to patch RPCManager class
:return: RPCManager.send_msg MagicMock to track if this method is called :return: RPCManager.send_msg MagicMock to track if this method is called
""" """
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
rpc_mock = mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) rpc_mock = mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
return rpc_mock return rpc_mock