diff --git a/freqtrade/rpc/__init__.py b/freqtrade/rpc/__init__.py index 163e0a8aa..e69de29bb 100644 --- a/freqtrade/rpc/__init__.py +++ b/freqtrade/rpc/__init__.py @@ -1,415 +0,0 @@ -import logging -import re -import arrow -from decimal import Decimal -from datetime import datetime, timedelta -from pandas import DataFrame -import sqlalchemy as sql -# from sqlalchemy import and_, func, text - -from freqtrade.persistence import Trade -from freqtrade.misc import State, get_state, update_state -from freqtrade import exchange -from freqtrade.fiat_convert import CryptoToFiatConverter -from . import telegram - -logger = logging.getLogger(__name__) - -_FIAT_CONVERT = CryptoToFiatConverter() -REGISTERED_MODULES = [] - - -def init(config: dict) -> None: - """ - Initializes all enabled rpc modules - :param config: config to use - :return: None - """ - - if config['telegram'].get('enabled', False): - logger.info('Enabling rpc.telegram ...') - REGISTERED_MODULES.append('telegram') - telegram.init(config) - - -def cleanup() -> None: - """ - Stops all enabled rpc modules - :return: None - """ - if 'telegram' in REGISTERED_MODULES: - logger.debug('Cleaning up rpc.telegram ...') - telegram.cleanup() - - -def send_msg(msg: str) -> None: - """ - Send given markdown message to all registered rpc modules - :param msg: message - :return: None - """ - logger.info(msg) - if 'telegram' in REGISTERED_MODULES: - telegram.send_msg(msg) - - -def shorten_date(_date): - """ - Trim the date so it fits on small screens - """ - new_date = re.sub('seconds?', 'sec', _date) - new_date = re.sub('minutes?', 'min', new_date) - new_date = re.sub('hours?', 'h', new_date) - new_date = re.sub('days?', 'd', new_date) - new_date = re.sub('^an?', '1', new_date) - return new_date - - -# -# Below follows the RPC backend -# it is prefixed with rpc_ -# to raise awareness that it is -# a remotely exposed function - - -def rpc_trade_status(): - # Fetch open trade - trades = Trade.query.filter(Trade.is_open.is_(True)).all() - if get_state() != State.RUNNING: - return (True, '*Status:* `trader is not running`') - elif not trades: - return (True, '*Status:* `no active trade`') - else: - result = [] - for trade in trades: - order = None - if trade.open_order_id: - order = exchange.get_order(trade.open_order_id) - # calculate profit and send message to user - current_rate = exchange.get_ticker(trade.pair, False)['bid'] - current_profit = trade.calc_profit_percent(current_rate) - fmt_close_profit = '{:.2f}%'.format( - round(trade.close_profit * 100, 2) - ) if trade.close_profit else None - message = """ -*Trade ID:* `{trade_id}` -*Current Pair:* [{pair}]({market_url}) -*Open Since:* `{date}` -*Amount:* `{amount}` -*Open Rate:* `{open_rate:.8f}` -*Close Rate:* `{close_rate}` -*Current Rate:* `{current_rate:.8f}` -*Close Profit:* `{close_profit}` -*Current Profit:* `{current_profit:.2f}%` -*Open Order:* `{open_order}` - """.format( - trade_id=trade.id, - pair=trade.pair, - market_url=exchange.get_pair_detail_url(trade.pair), - date=arrow.get(trade.open_date).humanize(), - open_rate=trade.open_rate, - close_rate=trade.close_rate, - current_rate=current_rate, - amount=round(trade.amount, 8), - close_profit=fmt_close_profit, - current_profit=round(current_profit * 100, 2), - open_order='({} rem={:.8f})'.format( - order['type'], order['remaining'] - ) if order else None, - ) - result.append(message) - return (False, result) - - -def rpc_status_table(): - trades = Trade.query.filter(Trade.is_open.is_(True)).all() - if get_state() != State.RUNNING: - return (True, '*Status:* `trader is not running`') - elif not trades: - return (True, '*Status:* `no active order`') - else: - trades_list = [] - for trade in trades: - # calculate profit and send message to user - current_rate = exchange.get_ticker(trade.pair, False)['bid'] - trades_list.append([ - trade.id, - trade.pair, - shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), - '{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate)) - ]) - - columns = ['ID', 'Pair', 'Since', 'Profit'] - df_statuses = DataFrame.from_records(trades_list, columns=columns) - df_statuses = df_statuses.set_index(columns[0]) - # The style used throughout is to return a tuple - # 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(timescale, stake_currency, fiat_display_currency): - today = datetime.utcnow().date() - profit_days = {} - - if not (isinstance(timescale, int) and timescale > 0): - return (True, '*Daily [n]:* `must be an integer greater than 0`') - - fiat = _FIAT_CONVERT - for day in range(0, timescale): - profitday = today - timedelta(days=day) - trades = Trade.query \ - .filter(Trade.is_open.is_(False)) \ - .filter(Trade.close_date >= profitday)\ - .filter(Trade.close_date < (profitday + timedelta(days=1)))\ - .order_by(Trade.close_date)\ - .all() - curdayprofit = sum(trade.calc_profit() for trade in trades) - profit_days[profitday] = { - 'amount': format(curdayprofit, '.8f'), - 'trades': len(trades) - } - - stats = [ - [ - key, - '{value:.8f} {symbol}'.format( - value=float(value['amount']), - symbol=stake_currency - ), - '{value:.3f} {symbol}'.format( - value=fiat.convert_amount( - value['amount'], - stake_currency, - fiat_display_currency - ), - symbol=fiat_display_currency - ), - '{value} trade{s}'.format(value=value['trades'], s='' if value['trades'] < 2 else 's'), - ] - for key, value in profit_days.items() - ] - return (False, stats) - - -def rpc_trade_statistics(stake_currency, fiat_display_currency) -> None: - """ - :return: cumulative profit statistics. - """ - trades = Trade.query.order_by(Trade.id).all() - - profit_all_coin = [] - profit_all_percent = [] - profit_closed_coin = [] - profit_closed_percent = [] - durations = [] - - for trade in trades: - current_rate = None - - if not trade.open_rate: - continue - if trade.close_date: - durations.append((trade.close_date - trade.open_date).total_seconds()) - - if not trade.is_open: - profit_percent = trade.calc_profit_percent() - profit_closed_coin.append(trade.calc_profit()) - profit_closed_percent.append(profit_percent) - else: - # Get current rate - current_rate = exchange.get_ticker(trade.pair, False)['bid'] - profit_percent = trade.calc_profit_percent(rate=current_rate) - - profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))) - profit_all_percent.append(profit_percent) - - best_pair = Trade.session.query(Trade.pair, - sql.func.sum(Trade.close_profit).label('profit_sum')) \ - .filter(Trade.is_open.is_(False)) \ - .group_by(Trade.pair) \ - .order_by(sql.text('profit_sum DESC')) \ - .first() - - if not best_pair: - return (True, '*Status:* `no closed trade`') - - bp_pair, bp_rate = best_pair - - # FIX: we want to keep fiatconverter in a state/environment, - # doing this will utilize its caching functionallity, instead we reinitialize it here - fiat = _FIAT_CONVERT - # Prepare data to display - profit_closed_coin = round(sum(profit_closed_coin), 8) - profit_closed_percent = round(sum(profit_closed_percent) * 100, 2) - profit_closed_fiat = fiat.convert_amount( - profit_closed_coin, - stake_currency, - fiat_display_currency - ) - profit_all_coin = round(sum(profit_all_coin), 8) - profit_all_percent = round(sum(profit_all_percent) * 100, 2) - profit_all_fiat = fiat.convert_amount( - profit_all_coin, - stake_currency, - fiat_display_currency - ) - num = float(len(durations) or 1) - return (False, - {'profit_closed_coin': profit_closed_coin, - 'profit_closed_percent': profit_closed_percent, - 'profit_closed_fiat': profit_closed_fiat, - 'profit_all_coin': profit_all_coin, - 'profit_all_percent': profit_all_percent, - 'profit_all_fiat': profit_all_fiat, - 'trade_count': len(trades), - 'first_trade_date': arrow.get(trades[0].open_date).humanize(), - 'latest_trade_date': arrow.get(trades[-1].open_date).humanize(), - 'avg_duration': str(timedelta(seconds=sum(durations) / - num)).split('.')[0], - 'best_pair': bp_pair, - 'best_rate': round(bp_rate * 100, 2) - }) - - -def rpc_balance(fiat_display_currency): - """ - :return: current account balance per crypto - """ - balances = [ - c for c in exchange.get_balances() - if c['Balance'] or c['Available'] or c['Pending'] - ] - if not balances: - return (True, '`All balances are zero.`') - - output = [] - total = 0.0 - for currency in balances: - coin = currency['Currency'] - if coin == 'BTC': - currency["Rate"] = 1.0 - else: - if coin == 'USDT': - currency["Rate"] = 1.0 / exchange.get_ticker('USDT_BTC', False)['bid'] - else: - currency["Rate"] = exchange.get_ticker('BTC_' + coin, False)['bid'] - currency['BTC'] = currency["Rate"] * currency["Balance"] - total = total + currency['BTC'] - output.append({'currency': currency['Currency'], - 'available': currency['Available'], - 'balance': currency['Balance'], - 'pending': currency['Pending'], - 'est_btc': currency['BTC'] - }) - fiat = _FIAT_CONVERT - symbol = fiat_display_currency - value = fiat.convert_amount(total, 'BTC', symbol) - return (False, (output, total, symbol, value)) - - -def rpc_start(): - """ - Handler for start. - """ - if get_state() == State.RUNNING: - return (True, '*Status:* `already running`') - else: - update_state(State.RUNNING) - - -def rpc_stop(): - """ - Handler for stop. - """ - if get_state() == State.RUNNING: - update_state(State.STOPPED) - return (False, '`Stopping trader ...`') - else: - return (True, '*Status:* `already stopped`') - - -# FIX: no test for this!!!! -def rpc_forcesell(trade_id) -> None: - """ - Handler for forcesell . - Sells the given trade at current price - :return: error or None - """ - def _exec_forcesell(trade: Trade) -> str: - # Check if there is there is an open order - if trade.open_order_id: - order = exchange.get_order(trade.open_order_id) - - # Cancel open LIMIT_BUY orders and close trade - if order and not order['closed'] and order['type'] == 'LIMIT_BUY': - exchange.cancel_order(trade.open_order_id) - trade.close(order.get('rate') or trade.open_rate) - # TODO: sell amount which has been bought already - return - - # Ignore trades with an attached LIMIT_SELL order - if order and not order['closed'] and order['type'] == 'LIMIT_SELL': - return - - # Get current rate and execute sell - current_rate = exchange.get_ticker(trade.pair, False)['bid'] - from freqtrade.main import execute_sell - execute_sell(trade, current_rate) - # ---- EOF def _exec_forcesell ---- - - if get_state() != State.RUNNING: - return (True, '`trader is not running`') - - if trade_id == 'all': - # Execute sell for all open orders - for trade in Trade.query.filter(Trade.is_open.is_(True)).all(): - _exec_forcesell(trade) - return (False, '') - - # Query for trade - trade = Trade.query.filter(sql.and_( - Trade.id == trade_id, - Trade.is_open.is_(True) - )).first() - if not trade: - logger.warning('forcesell: Invalid argument received') - return (True, 'Invalid argument.') - - _exec_forcesell(trade) - return (False, '') - - -def rpc_performance() -> None: - """ - Handler for performance. - Shows a performance statistic from finished trades - """ - if get_state() != State.RUNNING: - return (True, '`trader is not running`') - - pair_rates = Trade.session.query(Trade.pair, - sql.func.sum(Trade.close_profit).label('profit_sum'), - sql.func.count(Trade.pair).label('count')) \ - .filter(Trade.is_open.is_(False)) \ - .group_by(Trade.pair) \ - .order_by(sql.text('profit_sum DESC')) \ - .all() - trades = [] - for (pair, rate, count) in pair_rates: - trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count}) - - return (False, trades) - - -def rpc_count() -> None: - """ - Returns the number of trades running - :return: None - """ - if get_state() != State.RUNNING: - return (True, '`trader is not running`') - - trades = Trade.query.filter(Trade.is_open.is_(True)).all() - return (False, trades) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py new file mode 100644 index 000000000..30acf0bf4 --- /dev/null +++ b/freqtrade/rpc/rpc.py @@ -0,0 +1,373 @@ +""" +This module contains class to define a RPC communications +""" + +import arrow +from decimal import Decimal +from datetime import datetime, timedelta +from pandas import DataFrame +import sqlalchemy as sql +from freqtrade.logger import Logger +from freqtrade.persistence import Trade +from freqtrade.state import State +from freqtrade import exchange +from freqtrade.misc import shorten_date + + +class RPC(object): + """ + RPC class can be used to have extra feature, like bot data, and access to DB data + """ + + def __init__(self, freqtrade) -> None: + """ + Initializes all enabled rpc modules + :param freqtrade: Instance of a freqtrade bot + :return: None + """ + self.freqtrade = freqtrade + self.logger = Logger( + name=__name__, + level=self.freqtrade.config.get('loglevel') + ).get_logger() + + def rpc_trade_status(self) -> (bool, Trade): + """ + Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is + a remotely exposed function + :return: + """ + # Fetch open trade + trades = Trade.query.filter(Trade.is_open.is_(True)).all() + if self.freqtrade.get_state() != State.RUNNING: + return (True, '*Status:* `trader is not running`') + elif not trades: + return (True, '*Status:* `no active trade`') + else: + result = [] + for trade in trades: + order = None + if trade.open_order_id: + order = exchange.get_order(trade.open_order_id) + # calculate profit and send message to user + current_rate = exchange.get_ticker(trade.pair, False)['bid'] + current_profit = trade.calc_profit_percent(current_rate) + fmt_close_profit = '{:.2f}%'.format( + round(trade.close_profit * 100, 2) + ) if trade.close_profit else None + message = "*Trade ID:* `{trade_id}`\n" \ + "*Current Pair:* [{pair}]({market_url})\n" \ + "*Open Since:* `{date}`\n" \ + "*Amount:* `{amount}`\n" \ + "*Open Rate:* `{open_rate:.8f}`\n" \ + "*Close Rate:* `{close_rate}`\n" \ + "*Current Rate:* `{current_rate:.8f}`\n" \ + "*Close Profit:* `{close_profit}`\n" \ + "*Current Profit:* `{current_profit:.2f}%`\n" \ + "*Open Order:* `{open_order}`"\ + .format( + trade_id=trade.id, + pair=trade.pair, + market_url=exchange.get_pair_detail_url(trade.pair), + date=arrow.get(trade.open_date).humanize(), + open_rate=trade.open_rate, + close_rate=trade.close_rate, + current_rate=current_rate, + amount=round(trade.amount, 8), + close_profit=fmt_close_profit, + current_profit=round(current_profit * 100, 2), + open_order='({} rem={:.8f})'.format( + order['type'], order['remaining'] + ) if order else None, + ) + result.append(message) + return (False, result) + + def rpc_status_table(self) -> (bool, DataFrame): + trades = Trade.query.filter(Trade.is_open.is_(True)).all() + if self.freqtrade.get_state() != State.RUNNING: + return (True, '*Status:* `trader is not running`') + elif not trades: + return (True, '*Status:* `no active order`') + else: + trades_list = [] + for trade in trades: + # calculate profit and send message to user + current_rate = exchange.get_ticker(trade.pair, False)['bid'] + trades_list.append([ + trade.id, + trade.pair, + shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), + '{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate)) + ]) + + columns = ['ID', 'Pair', 'Since', 'Profit'] + df_statuses = DataFrame.from_records(trades_list, columns=columns) + df_statuses = df_statuses.set_index(columns[0]) + # The style used throughout is to return a tuple + # 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(self, timescale, stake_currency, fiat_display_currency): + today = datetime.utcnow().date() + profit_days = {} + + if not (isinstance(timescale, int) and timescale > 0): + return (True, '*Daily [n]:* `must be an integer greater than 0`') + + fiat = self.freqtrade.fiat_converter + for day in range(0, timescale): + profitday = today - timedelta(days=day) + trades = Trade.query \ + .filter(Trade.is_open.is_(False)) \ + .filter(Trade.close_date >= profitday)\ + .filter(Trade.close_date < (profitday + timedelta(days=1)))\ + .order_by(Trade.close_date)\ + .all() + curdayprofit = sum(trade.calc_profit() for trade in trades) + profit_days[profitday] = { + 'amount': format(curdayprofit, '.8f'), + 'trades': len(trades) + } + + stats = [ + [ + key, + '{value:.8f} {symbol}'.format( + value=float(value['amount']), + symbol=stake_currency + ), + '{value:.3f} {symbol}'.format( + value=fiat.convert_amount( + value['amount'], + stake_currency, + fiat_display_currency + ), + symbol=fiat_display_currency + ), + '{value} trade{s}'.format(value=value['trades'], s='' if value['trades'] < 2 else 's'), + ] + for key, value in profit_days.items() + ] + return (False, stats) + + def rpc_trade_statistics(self, stake_currency, fiat_display_currency) -> None: + """ + :return: cumulative profit statistics. + """ + trades = Trade.query.order_by(Trade.id).all() + + profit_all_coin = [] + profit_all_percent = [] + profit_closed_coin = [] + profit_closed_percent = [] + durations = [] + + for trade in trades: + current_rate = None + + if not trade.open_rate: + continue + if trade.close_date: + durations.append((trade.close_date - trade.open_date).total_seconds()) + + if not trade.is_open: + profit_percent = trade.calc_profit_percent() + profit_closed_coin.append(trade.calc_profit()) + profit_closed_percent.append(profit_percent) + else: + # Get current rate + current_rate = exchange.get_ticker(trade.pair, False)['bid'] + profit_percent = trade.calc_profit_percent(rate=current_rate) + + profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))) + profit_all_percent.append(profit_percent) + + best_pair = Trade.session.query(Trade.pair, + sql.func.sum(Trade.close_profit).label('profit_sum')) \ + .filter(Trade.is_open.is_(False)) \ + .group_by(Trade.pair) \ + .order_by(sql.text('profit_sum DESC')) \ + .first() + + if not best_pair: + return (True, '*Status:* `no closed trade`') + + bp_pair, bp_rate = best_pair + + # FIX: we want to keep fiatconverter in a state/environment, + # doing this will utilize its caching functionallity, instead we reinitialize it here + fiat = self.freqtrade.fiat_converter + # Prepare data to display + profit_closed_coin = round(sum(profit_closed_coin), 8) + profit_closed_percent = round(sum(profit_closed_percent) * 100, 2) + profit_closed_fiat = fiat.convert_amount( + profit_closed_coin, + stake_currency, + fiat_display_currency + ) + profit_all_coin = round(sum(profit_all_coin), 8) + profit_all_percent = round(sum(profit_all_percent) * 100, 2) + profit_all_fiat = fiat.convert_amount( + profit_all_coin, + stake_currency, + fiat_display_currency + ) + num = float(len(durations) or 1) + return ( + False, + { + 'profit_closed_coin': profit_closed_coin, + 'profit_closed_percent': profit_closed_percent, + 'profit_closed_fiat': profit_closed_fiat, + 'profit_all_coin': profit_all_coin, + 'profit_all_percent': profit_all_percent, + 'profit_all_fiat': profit_all_fiat, + 'trade_count': len(trades), + 'first_trade_date': arrow.get(trades[0].open_date).humanize(), + 'latest_trade_date': arrow.get(trades[-1].open_date).humanize(), + 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], + 'best_pair': bp_pair, + 'best_rate': round(bp_rate * 100, 2) + } + ) + + def rpc_balance(self, fiat_display_currency): + """ + :return: current account balance per crypto + """ + balances = [ + c for c in exchange.get_balances() + if c['Balance'] or c['Available'] or c['Pending'] + ] + if not balances: + return (True, '`All balances are zero.`') + + output = [] + total = 0.0 + for currency in balances: + coin = currency['Currency'] + if coin == 'BTC': + currency["Rate"] = 1.0 + else: + if coin == 'USDT': + currency["Rate"] = 1.0 / exchange.get_ticker('USDT_BTC', False)['bid'] + else: + currency["Rate"] = exchange.get_ticker('BTC_' + coin, False)['bid'] + currency['BTC'] = currency["Rate"] * currency["Balance"] + total = total + currency['BTC'] + output.append({'currency': currency['Currency'], + 'available': currency['Available'], + 'balance': currency['Balance'], + 'pending': currency['Pending'], + 'est_btc': currency['BTC'] + }) + fiat = self.freqtrade.fiat_converter + symbol = fiat_display_currency + value = fiat.convert_amount(total, 'BTC', symbol) + return (False, (output, total, symbol, value)) + + def rpc_start(self) -> (bool, str): + """ + Handler for start. + """ + if self.freqtrade.get_state() == State.RUNNING: + return (True, '*Status:* `already running`') + else: + self.freqtrade.update_state(State.RUNNING) + return (False, '`Starting trader ...`') + + def rpc_stop(self) -> (bool, str): + """ + Handler for stop. + """ + if self.freqtrade.get_state() == State.RUNNING: + self.freqtrade.update_state(State.STOPPED) + return (False, '`Stopping trader ...`') + else: + return (True, '*Status:* `already stopped`') + + # FIX: no test for this!!!! + def rpc_forcesell(self, trade_id) -> None: + """ + Handler for forcesell . + Sells the given trade at current price + :return: error or None + """ + def _exec_forcesell(trade: Trade) -> str: + # Check if there is there is an open order + if trade.open_order_id: + order = exchange.get_order(trade.open_order_id) + + # Cancel open LIMIT_BUY orders and close trade + if order and not order['closed'] and order['type'] == 'LIMIT_BUY': + exchange.cancel_order(trade.open_order_id) + trade.close(order.get('rate') or trade.open_rate) + # TODO: sell amount which has been bought already + return + + # Ignore trades with an attached LIMIT_SELL order + if order and not order['closed'] and order['type'] == 'LIMIT_SELL': + return + + # Get current rate and execute sell + current_rate = exchange.get_ticker(trade.pair, False)['bid'] + self.freqtrade.execute_sell(trade, current_rate) + # ---- EOF def _exec_forcesell ---- + + if self.freqtrade.get_state() != State.RUNNING: + return (True, '`trader is not running`') + + if trade_id == 'all': + # Execute sell for all open orders + for trade in Trade.query.filter(Trade.is_open.is_(True)).all(): + _exec_forcesell(trade) + return (False, '') + + # Query for trade + trade = Trade.query.filter( + sql.and_( + Trade.id == trade_id, + Trade.is_open.is_(True) + ) + ).first() + if not trade: + self.logger.warning('forcesell: Invalid argument received') + return (True, 'Invalid argument.') + + _exec_forcesell(trade) + return (False, '') + + def rpc_performance(self) -> None: + """ + Handler for performance. + Shows a performance statistic from finished trades + """ + if self.freqtrade.get_state() != State.RUNNING: + return (True, '`trader is not running`') + + pair_rates = Trade.session.query(Trade.pair, + sql.func.sum(Trade.close_profit).label('profit_sum'), + sql.func.count(Trade.pair).label('count')) \ + .filter(Trade.is_open.is_(False)) \ + .group_by(Trade.pair) \ + .order_by(sql.text('profit_sum DESC')) \ + .all() + trades = [] + for (pair, rate, count) in pair_rates: + trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count}) + + return (False, trades) + + def rpc_count(self) -> None: + """ + Returns the number of trades running + :return: None + """ + if self.freqtrade.get_state() != State.RUNNING: + return (True, '`trader is not running`') + + trades = Trade.query.filter(Trade.is_open.is_(True)).all() + return (False, trades) diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py new file mode 100644 index 000000000..5987d8c4d --- /dev/null +++ b/freqtrade/rpc/rpc_manager.py @@ -0,0 +1,60 @@ +""" +This module contains class to manage RPC communications (Telegram, Slack, ...) +""" + +from freqtrade.logger import Logger +from freqtrade.rpc.telegram import Telegram + + +class RPCManager(object): + """ + Class to manage RPC objects (Telegram, Slack, ...) + """ + + def __init__(self, freqtrade) -> None: + """ + Initializes all enabled rpc modules + :param config: config to use + :return: None + """ + self.freqtrade = freqtrade + + # Init the logger + self.logger = Logger( + name=__name__, + level=self.freqtrade.config.get('loglevel') + ).get_logger() + + self.registered_modules = [] + self.telegram = None + self._init() + + def _init(self): + """ + Init RPC modules + :return: + """ + if self.freqtrade.config['telegram'].get('enabled', False): + self.logger.info('Enabling rpc.telegram ...') + self.registered_modules.append('telegram') + self.telegram = Telegram(self.freqtrade) + + def cleanup(self) -> None: + """ + Stops all enabled rpc modules + :return: None + """ + if 'telegram' in self.registered_modules: + self.logger.info('Cleaning up rpc.telegram ...') + self.registered_modules.remove('telegram') + self.telegram.cleanup() + + def send_msg(self, msg: str) -> None: + """ + Send given markdown message to all registered rpc modules + :param msg: message + :return: None + """ + self.logger.info(msg) + if 'telegram' in self.registered_modules: + self.telegram.send_msg(msg) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ea170baa1..c0f5f815b 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1,94 +1,14 @@ -import logging -from typing import Any, Callable +""" +This module manage Telegram communication +""" +from typing import Any, Callable +from freqtrade.rpc.rpc import RPC from tabulate import tabulate from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update from telegram.error import NetworkError, TelegramError from telegram.ext import CommandHandler, Updater - -from freqtrade.rpc.__init__ import (rpc_status_table, - rpc_trade_status, - rpc_daily_profit, - rpc_trade_statistics, - rpc_balance, - rpc_start, - rpc_stop, - rpc_forcesell, - rpc_performance, - rpc_count, - ) - -from freqtrade import __version__ - - -# Remove noisy log messages -logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO) -logging.getLogger('telegram').setLevel(logging.INFO) -logger = logging.getLogger(__name__) - -_UPDATER: Updater = None -_CONF = {} - - -def init(config: dict) -> None: - """ - Initializes this module with the given config, - registers all known command handlers - and starts polling for message updates - :param config: config to use - :return: None - """ - global _UPDATER - - _CONF.update(config) - if not is_enabled(): - return - - _UPDATER = Updater(token=config['telegram']['token'], workers=0) - - # Register command handler and start telegram message polling - handles = [ - CommandHandler('status', _status), - CommandHandler('profit', _profit), - CommandHandler('balance', _balance), - CommandHandler('start', _start), - CommandHandler('stop', _stop), - CommandHandler('forcesell', _forcesell), - CommandHandler('performance', _performance), - CommandHandler('daily', _daily), - CommandHandler('count', _count), - CommandHandler('help', _help), - CommandHandler('version', _version), - ] - for handle in handles: - _UPDATER.dispatcher.add_handler(handle) - _UPDATER.start_polling( - clean=True, - bootstrap_retries=-1, - timeout=30, - read_latency=60, - ) - logger.info( - 'rpc.telegram is listening for following commands: %s', - [h.command for h in handles] - ) - - -def cleanup() -> None: - """ - Stops all running telegram threads. - :return: None - """ - if not is_enabled(): - return - _UPDATER.stop() - - -def is_enabled() -> bool: - """ - Returns True if the telegram module is activated, False otherwise - """ - return bool(_CONF['telegram'].get('enabled', False)) +from freqtrade.__init__ import __version__ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]: @@ -97,340 +17,424 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[ :param command_handler: Telegram CommandHandler :return: decorated function """ - def wrapper(*args, **kwargs): + + #def wrapper(self, bot: Bot, update: Update): + def wrapper(self, *args, **kwargs): + update = kwargs.get('update') or args[1] # Reject unauthorized messages - chat_id = int(_CONF['telegram']['chat_id']) + chat_id = int(self._config['telegram']['chat_id']) + if int(update.message.chat_id) != chat_id: - logger.info('Rejected unauthorized message from: %s', update.message.chat_id) + self.logger.info( + 'Rejected unauthorized message from: %s', + update.message.chat_id + ) return wrapper - logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id) + self.logger.info( + 'Executing handler: %s for chat_id: %s', + command_handler.__name__, + chat_id + ) try: - return command_handler(*args, **kwargs) + return command_handler(self, *args, **kwargs) except BaseException: - logger.exception('Exception occurred within Telegram module') + self.logger.exception('Exception occurred within Telegram module') + return wrapper - -@authorized_only -def _status(bot: Bot, update: Update) -> None: +class Telegram(RPC): """ - Handler for /status. - Returns the current TradeThread status - :param bot: telegram bot - :param update: message update - :return: None + Telegram, this class send messages to Telegram """ + def __init__(self, freqtrade) -> None: + """ + Init the Telegram call, and init the super class RPC + :param freqtrade: Instance of a freqtrade bot + :return: None + """ + super().__init__(freqtrade) - # Check if additional parameters are passed - params = update.message.text.replace('/status', '').split(' ') \ - if update.message.text else [] - if 'table' in params: - _status_table(bot, update) - return + self._updater = Updater = None + self._config = freqtrade.config + self._init() - # Fetch open trade - (error, trades) = rpc_trade_status() - if error: - send_msg(trades, bot=bot) - else: - for trademsg in trades: - send_msg(trademsg, bot=bot) + def _init(self) -> None: + """ + Initializes this module with the given config, + registers all known command handlers + 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) -@authorized_only -def _status_table(bot: Bot, update: Update) -> None: - """ - Handler for /status table. - Returns the current TradeThread status in table format - :param bot: telegram bot - :param update: message update - :return: None - """ - # Fetch open trade - (err, df_statuses) = rpc_status_table() - if err: - send_msg(df_statuses, bot=bot) - else: - message = tabulate(df_statuses, headers='keys', tablefmt='simple') - message = "
{}
".format(message) + # Register command handler and start telegram message polling + handles = [ + CommandHandler('status', self._status), + CommandHandler('profit', self._profit), + CommandHandler('balance', self._balance), + CommandHandler('start', self._start), + CommandHandler('stop', self._stop), + CommandHandler('forcesell', self._forcesell), + CommandHandler('performance', self._performance), + CommandHandler('daily', self._daily), + CommandHandler('count', self._count), + CommandHandler('help', self._help), + CommandHandler('version', self._version), + ] + for handle in handles: + self._updater.dispatcher.add_handler(handle) + self._updater.start_polling( + clean=True, + bootstrap_retries=-1, + timeout=30, + read_latency=60, + ) + self.logger.info( + 'rpc.telegram is listening for following commands: %s', + [h.command for h in handles] + ) - send_msg(message, parse_mode=ParseMode.HTML) + def cleanup(self) -> None: + """ + Stops all running telegram threads. + :return: None + """ + if not self.is_enabled(): + return + import pprint + pprint.pprint(self._updater.stop.call_count) + self._updater.stop() -@authorized_only -def _daily(bot: Bot, update: Update) -> None: - """ - Handler for /daily - Returns a daily profit (in BTC) over the last n days. - :param bot: telegram bot - :param update: message update - :return: None - """ - try: - timescale = int(update.message.text.replace('/daily', '').strip()) - except (TypeError, ValueError): - timescale = 7 - (error, stats) = rpc_daily_profit(timescale, - _CONF['stake_currency'], - _CONF['fiat_display_currency']) - if error: - send_msg(stats, bot=bot) - else: - stats = tabulate(stats, - headers=[ - 'Day', - 'Profit {}'.format(_CONF['stake_currency']), - 'Profit {}'.format(_CONF['fiat_display_currency']) - ], - tablefmt='simple') - message = 'Daily Profit over the last {} days:\n
{}
'.format( - timescale, stats) - send_msg(message, bot=bot, parse_mode=ParseMode.HTML) + def is_enabled(self) -> bool: + """ + Returns True if the telegram module is activated, False otherwise + """ + return bool(self._config.get('telegram', {}).get('enabled', False)) + @authorized_only + def _status(self, bot: Bot, update: Update) -> None: + """ + Handler for /status. + Returns the current TradeThread status + :param bot: telegram bot + :param update: message update + :return: None + """ -@authorized_only -def _profit(bot: Bot, update: Update) -> None: - """ - Handler for /profit. - Returns a cumulative profit statistics. - :param bot: telegram bot - :param update: message update - :return: None - """ - (error, stats) = rpc_trade_statistics(_CONF['stake_currency'], - _CONF['fiat_display_currency']) - if error: - send_msg(stats, bot=bot) - return + # Check if additional parameters are passed + params = update.message.text.replace('/status', '').split(' ') \ + if update.message.text else [] + if 'table' in params: + self._status_table(bot, update) + return - # Message to display - markdown_msg = """ -*ROI:* Close trades - ∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)` - ∙ `{profit_closed_fiat:.3f} {fiat}` -*ROI:* All trades - ∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)` - ∙ `{profit_all_fiat:.3f} {fiat}` + # Fetch open trade + (error, trades) = self.rpc_trade_status() + if error: + self.send_msg(trades, bot=bot) + else: + for trademsg in trades: + self.send_msg(trademsg, bot=bot) -*Total Trade Count:* `{trade_count}` -*First Trade opened:* `{first_trade_date}` -*Latest Trade opened:* `{latest_trade_date}` -*Avg. Duration:* `{avg_duration}` -*Best Performing:* `{best_pair}: {best_rate:.2f}%` - """.format( - coin=_CONF['stake_currency'], - fiat=_CONF['fiat_display_currency'], - profit_closed_coin=stats['profit_closed_coin'], - profit_closed_percent=stats['profit_closed_percent'], - profit_closed_fiat=stats['profit_closed_fiat'], - profit_all_coin=stats['profit_all_coin'], - profit_all_percent=stats['profit_all_percent'], - profit_all_fiat=stats['profit_all_fiat'], - trade_count=stats['trade_count'], - first_trade_date=stats['first_trade_date'], - latest_trade_date=stats['latest_trade_date'], - avg_duration=stats['avg_duration'], - best_pair=stats['best_pair'], - best_rate=stats['best_rate'] - ) - send_msg(markdown_msg, bot=bot) + @authorized_only + def _status_table(self, bot: Bot, update: Update) -> None: + """ + Handler for /status table. + Returns the current TradeThread status in table format + :param bot: telegram bot + :param update: message update + :return: None + """ + # Fetch open trade + (err, 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 = "
{}
".format(message) + self.send_msg(message, parse_mode=ParseMode.HTML) -@authorized_only -def _balance(bot: Bot, update: Update) -> None: - """ - Handler for /balance - """ - (error, result) = rpc_balance(_CONF['fiat_display_currency']) - if error: - send_msg('`All balances are zero.`') - return - - (currencys, total, symbol, value) = result - output = '' - for currency in currencys: - output += """*Currency*: {currency} -*Available*: {available} -*Balance*: {balance} -*Pending*: {pending} -*Est. BTC*: {est_btc: .8f} -""".format(**currency) - - output += """*Estimated Value*: -*BTC*: {0: .8f} -*{1}*: {2: .2f} -""".format(total, symbol, value) - send_msg(output) - - -@authorized_only -def _start(bot: Bot, update: Update) -> None: - """ - Handler for /start. - Starts TradeThread - :param bot: telegram bot - :param update: message update - :return: None - """ - (error, msg) = rpc_start() - if error: - send_msg(msg, bot=bot) - - -@authorized_only -def _stop(bot: Bot, update: Update) -> None: - """ - Handler for /stop. - Stops TradeThread - :param bot: telegram bot - :param update: message update - :return: None - """ - (error, msg) = rpc_stop() - send_msg(msg, bot=bot) - - -# FIX: no test for this!!!! -@authorized_only -def _forcesell(bot: Bot, update: Update) -> None: - """ - Handler for /forcesell . - Sells the given trade at current price - :param bot: telegram bot - :param update: message update - :return: None - """ - - trade_id = update.message.text.replace('/forcesell', '').strip() - (error, message) = rpc_forcesell(trade_id) - if error: - send_msg(message, bot=bot) - return - - -@authorized_only -def _performance(bot: Bot, update: Update) -> None: - """ - Handler for /performance. - Shows a performance statistic from finished trades - :param bot: telegram bot - :param update: message update - :return: None - """ - (error, trades) = rpc_performance() - if error: - send_msg(trades, bot=bot) - return - - stats = '\n'.join('{index}.\t{pair}\t{profit:.2f}% ({count})'.format( - index=i + 1, - pair=trade['pair'], - profit=trade['profit'], - count=trade['count'] - ) for i, trade in enumerate(trades)) - message = 'Performance:\n{}'.format(stats) - send_msg(message, parse_mode=ParseMode.HTML) - - -@authorized_only -def _count(bot: Bot, update: Update) -> None: - """ - Handler for /count. - Returns the number of trades running - :param bot: telegram bot - :param update: message update - :return: None - """ - (error, trades) = rpc_count() - if error: - send_msg(trades, bot=bot) - return - - message = tabulate({ - 'current': [len(trades)], - 'max': [_CONF['max_open_trades']] - }, headers=['current', 'max'], tablefmt='simple') - message = "
{}
".format(message) - logger.debug(message) - send_msg(message, parse_mode=ParseMode.HTML) - - -@authorized_only -def _help(bot: Bot, update: Update) -> None: - """ - Handler for /help. - Show commands of the bot - :param bot: telegram bot - :param update: message update - :return: None - """ - message = """ -*/start:* `Starts the trader` -*/stop:* `Stops the trader` -*/status [table]:* `Lists all open trades` - *table :* `will display trades in a table` -*/profit:* `Lists cumulative profit from all finished trades` -*/forcesell |all:* `Instantly sells the given trade or all trades, regardless of profit` -*/performance:* `Show performance of each finished trade grouped by pair` -*/daily :* `Shows profit or loss per day, over the last n days` -*/count:* `Show number of trades running compared to allowed number of trades` -*/balance:* `Show account balance per currency` -*/help:* `This help message` -*/version:* `Show version` - """ - send_msg(message, bot=bot) - - -@authorized_only -def _version(bot: Bot, update: Update) -> None: - """ - Handler for /version. - Show version information - :param bot: telegram bot - :param update: message update - :return: None - """ - send_msg('*Version:* `{}`'.format(__version__), bot=bot) - - -def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: - """ - Send given markdown message - :param msg: message - :param bot: alternative bot - :param parse_mode: telegram parse mode - :return: None - """ - if not is_enabled(): - return - - bot = bot or _UPDATER.bot - - keyboard = [['/daily', '/profit', '/balance'], - ['/status', '/status table', '/performance'], - ['/count', '/start', '/stop', '/help']] - - reply_markup = ReplyKeyboardMarkup(keyboard) - - try: + @authorized_only + def _daily(self, bot: Bot, update: Update) -> None: + """ + Handler for /daily + Returns a daily profit (in BTC) over the last n days. + :param bot: telegram bot + :param update: message update + :return: None + """ try: - bot.send_message( - _CONF['telegram']['chat_id'], msg, - parse_mode=parse_mode, reply_markup=reply_markup + timescale = int(update.message.text.replace('/daily', '').strip()) + except (TypeError, ValueError): + timescale = 7 + (error, stats) = self.rpc_daily_profit( + timescale, + self._config['stake_currency'], + self._config['fiat_display_currency'] + ) + if error: + self.send_msg(stats, bot=bot) + else: + stats = tabulate(stats, + headers=[ + 'Day', + 'Profit {}'.format(self._config['stake_currency']), + 'Profit {}'.format(self._config['fiat_display_currency']) + ], + tablefmt='simple') + message = 'Daily Profit over the last {} days:\n
{}
'\ + .format( + timescale, + stats + ) + self.send_msg(message, bot=bot, parse_mode=ParseMode.HTML) + + @authorized_only + def _profit(self, bot: Bot, update: Update) -> None: + """ + Handler for /profit. + Returns a cumulative profit statistics. + :param bot: telegram bot + :param update: message update + :return: None + """ + (error, stats) = self.rpc_trade_statistics( + self._config['stake_currency'], + self._config['fiat_display_currency'] + ) + if error: + self.send_msg(stats, bot=bot) + return + + # Message to display + markdown_msg = "*ROI:* Close trades\n" \ + "∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \ + "∙ `{profit_closed_fiat:.3f} {fiat}`\n" \ + "*ROI:* All trades\n" \ + "∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \ + "∙ `{profit_all_fiat:.3f} {fiat}`\n" \ + "*Total Trade Count:* `{trade_count}`\n" \ + "*First Trade opened:* `{first_trade_date}`\n" \ + "*Latest Trade opened:* `{latest_trade_date}`\n" \ + "*Avg. Duration:* `{avg_duration}`\n" \ + "*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\ + .format( + coin=self._config['stake_currency'], + fiat=self._config['fiat_display_currency'], + profit_closed_coin=stats['profit_closed_coin'], + profit_closed_percent=stats['profit_closed_percent'], + profit_closed_fiat=stats['profit_closed_fiat'], + profit_all_coin=stats['profit_all_coin'], + profit_all_percent=stats['profit_all_percent'], + profit_all_fiat=stats['profit_all_fiat'], + trade_count=stats['trade_count'], + first_trade_date=stats['first_trade_date'], + latest_trade_date=stats['latest_trade_date'], + avg_duration=stats['avg_duration'], + best_pair=stats['best_pair'], + best_rate=stats['best_rate'] + ) + self.send_msg(markdown_msg, bot=bot) + + @authorized_only + def _balance(self, bot: Bot, update: Update) -> None: + """ + Handler for /balance + """ + (error, result) = self.rpc_balance(self._config['fiat_display_currency']) + if error: + self.send_msg('`All balances are zero.`') + return + + (currencys, total, symbol, value) = result + output = '' + for currency in currencys: + output += """*Currency*: {currency} + *Available*: {available} + *Balance*: {balance} + *Pending*: {pending} + *Est. BTC*: {est_btc: .8f} + """.format(**currency) + + output += """*Estimated Value*: + *BTC*: {0: .8f} + *{1}*: {2: .2f} + """.format(total, symbol, value) + self.send_msg(output) + + @authorized_only + def _start(self, bot: Bot, update: Update) -> None: + """ + Handler for /start. + Starts TradeThread + :param bot: telegram bot + :param update: message update + :return: None + """ + (error, msg) = self.rpc_start() + if error: + self.send_msg(msg, bot=bot) + + @authorized_only + def _stop(self, bot: Bot, update: Update) -> None: + """ + Handler for /stop. + Stops TradeThread + :param bot: telegram bot + :param update: message update + :return: None + """ + (error, msg) = self.rpc_stop() + self.send_msg(msg, bot=bot) + + # FIX: no test for this!!!! + @authorized_only + def _forcesell(self, bot: Bot, update: Update) -> None: + """ + Handler for /forcesell . + Sells the given trade at current price + :param bot: telegram bot + :param update: message update + :return: None + """ + + trade_id = update.message.text.replace('/forcesell', '').strip() + (error, message) = self.rpc_forcesell(trade_id) + if error: + self.send_msg(message, bot=bot) + return + + @authorized_only + def _performance(self, bot: Bot, update: Update) -> None: + """ + Handler for /performance. + Shows a performance statistic from finished trades + :param bot: telegram bot + :param update: message update + :return: None + """ + (error, trades) = self.rpc_performance() + if error: + self.send_msg(trades, bot=bot) + return + + stats = '\n'.join('{index}.\t{pair}\t{profit:.2f}% ({count})'.format( + index=i + 1, + pair=trade['pair'], + profit=trade['profit'], + count=trade['count'] + ) for i, trade in enumerate(trades)) + message = 'Performance:\n{}'.format(stats) + self.send_msg(message, parse_mode=ParseMode.HTML) + + @authorized_only + def _count(self, bot: Bot, update: Update) -> None: + """ + Handler for /count. + Returns the number of trades running + :param bot: telegram bot + :param update: message update + :return: None + """ + (error, trades) = self.rpc_count() + if error: + self.send_msg(trades, bot=bot) + return + + message = tabulate({ + 'current': [len(trades)], + 'max': [self._config['max_open_trades']] + }, headers=['current', 'max'], tablefmt='simple') + message = "
{}
".format(message) + self.logger.debug(message) + self.send_msg(message, parse_mode=ParseMode.HTML) + + @authorized_only + def _help(self, bot: Bot, update: Update) -> None: + """ + Handler for /help. + Show commands of the bot + :param bot: telegram bot + :param update: message update + :return: None + """ + message = "*/start:* `Starts the trader`\n" \ + "*/stop:* `Stops the trader`\n" \ + "*/status [table]:* `Lists all open trades`\n" \ + " *table :* `will display trades in a table`\n" \ + "*/profit:* `Lists cumulative profit from all finished trades`\n" \ + "*/forcesell |all:* `Instantly sells the given trade or all trades, regardless of profit`\n" \ + "*/performance:* `Show performance of each finished trade grouped by pair`\n" \ + "*/daily :* `Shows profit or loss per day, over the last n days`\n" \ + "*/count:* `Show number of trades running compared to allowed number of trades`\n" \ + "*/balance:* `Show account balance per currency`\n" \ + "*/help:* `This help message`\n" \ + "*/version:* `Show version`" + + self.send_msg(message, bot=bot) + + @authorized_only + def _version(self, bot: Bot, update: Update) -> None: + """ + Handler for /version. + Show version information + :param bot: telegram bot + :param update: message update + :return: None + """ + self.send_msg('*Version:* `{}`'.format(__version__), bot=bot) + + def send_msg(self, msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: + """ + Send given markdown message + :param msg: message + :param bot: alternative bot + :param parse_mode: telegram parse mode + :return: None + """ + if not self.is_enabled(): + return + + bot = bot or self._updater.bot + + keyboard = [['/daily', '/profit', '/balance'], + ['/status', '/status table', '/performance'], + ['/count', '/start', '/stop', '/help']] + + reply_markup = ReplyKeyboardMarkup(keyboard) + + try: + try: + bot.send_message( + self._config['telegram']['chat_id'], + text=msg, + parse_mode=parse_mode, + reply_markup=reply_markup + ) + except NetworkError as network_err: + # Sometimes the telegram server resets the current connection, + # if this is the case we send the message again. + self.logger.warning( + 'Got Telegram NetworkError: %s! Trying one more time.', + network_err.message + ) + bot.send_message( + self._config['telegram']['chat_id'], + text=msg, + parse_mode=parse_mode, + reply_markup=reply_markup + ) + except TelegramError as telegram_err: + self.logger.warning( + 'Got TelegramError: %s! Giving up on that message.', + telegram_err.message ) - except NetworkError as network_err: - # Sometimes the telegram server resets the current connection, - # if this is the case we send the message again. - logger.warning( - 'Got Telegram NetworkError: %s! Trying one more time.', - network_err.message - ) - bot.send_message( - _CONF['telegram']['chat_id'], msg, - parse_mode=parse_mode, reply_markup=reply_markup - ) - except TelegramError as telegram_err: - logger.warning('Got TelegramError: %s! Giving up on that message.', telegram_err.message) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 06994ce8e..e9d77b1f5 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -1,204 +1,133 @@ -# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103 +# pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments + +""" +Unit test file for rpc/rpc.py +""" + from datetime import datetime -from copy import deepcopy from unittest.mock import MagicMock + from sqlalchemy import create_engine -from freqtrade.rpc import init, cleanup, send_msg +from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade -import freqtrade.main as main -import freqtrade.misc as misc -import freqtrade.rpc as rpc +from freqtrade.rpc.rpc import RPC +from freqtrade.state import State +from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_pymarketcap -def prec_satoshi(a, b): +# Functions for recurrent object patching +def prec_satoshi(a, b) -> float: """ :return: True if A and B differs less than one satoshi. """ return abs(a - b) < 0.00000001 -def test_init_telegram_enabled(default_conf, mocker): - module_list = [] - mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list) - telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock()) +# Unit tests +def test_rpc_trade_status(default_conf, ticker, mocker) -> None: + """ + Test rpc_trade_status() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker) + mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) - init(default_conf) + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + rpc = RPC(freqtradebot) - assert telegram_mock.call_count == 1 - assert 'telegram' in module_list - - -def test_init_telegram_disabled(default_conf, mocker): - module_list = [] - mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list) - telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock()) - - conf = deepcopy(default_conf) - conf['telegram']['enabled'] = False - init(conf) - - assert telegram_mock.call_count == 0 - assert 'telegram' not in module_list - - -def test_cleanup_telegram_enabled(mocker): - mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram']) - telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock()) - cleanup() - assert telegram_mock.call_count == 1 - - -def test_cleanup_telegram_disabled(mocker): - mocker.patch('freqtrade.rpc.REGISTERED_MODULES', []) - telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock()) - cleanup() - assert telegram_mock.call_count == 0 - - -def test_send_msg_telegram_enabled(mocker): - mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram']) - telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock()) - send_msg('test') - assert telegram_mock.call_count == 1 - - -def test_send_msg_telegram_disabled(mocker): - mocker.patch('freqtrade.rpc.REGISTERED_MODULES', []) - telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock()) - send_msg('test') - assert telegram_mock.call_count == 0 - - -def test_rpc_forcesell(default_conf, update, ticker, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) - mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock()) - cancel_order_mock = MagicMock() - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker, - cancel_order=cancel_order_mock, - get_order=MagicMock(return_value={ - 'closed': True, - 'type': 'LIMIT_BUY', - })) - main.init(default_conf, create_engine('sqlite://')) - - misc.update_state(misc.State.STOPPED) - (error, res) = rpc.rpc_forcesell(None) - assert error - assert res == '`trader is not running`' - misc.update_state(misc.State.RUNNING) - (error, res) = rpc.rpc_forcesell(None) - assert error - assert res == 'Invalid argument.' - - (error, res) = rpc.rpc_forcesell('all') - assert not error - assert res == '' - - main.create_trade(0.001, 5) - (error, res) = rpc.rpc_forcesell('all') - assert not error - assert res == '' - - (error, res) = rpc.rpc_forcesell('1') - assert not error - assert res == '' - - misc.update_state(misc.State.STOPPED) - - (error, res) = rpc.rpc_forcesell(None) - assert error - assert res == '`trader is not running`' - - (error, res) = rpc.rpc_forcesell('all') - assert error - assert res == '`trader is not running`' - - misc.update_state(misc.State.RUNNING) - - assert cancel_order_mock.call_count == 0 - # make an limit-buy open trade - mocker.patch.multiple('freqtrade.exchange', - get_order=MagicMock(return_value={ - 'closed': None, - 'type': 'LIMIT_BUY' - })) - # check that the trade is called, which is done - # by ensuring exchange.cancel_order is called - (error, res) = rpc.rpc_forcesell('1') - assert not error - assert res == '' - assert cancel_order_mock.call_count == 1 - - main.create_trade(0.001, 5) - # make an limit-sell open trade - mocker.patch.multiple('freqtrade.exchange', - get_order=MagicMock(return_value={ - 'closed': None, - 'type': 'LIMIT_SELL' - })) - (error, res) = rpc.rpc_forcesell('2') - assert not error - assert res == '' - # status quo, no exchange calls - assert cancel_order_mock.call_count == 1 - - -def test_rpc_trade_status(default_conf, update, ticker, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) - mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock()) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker) - main.init(default_conf, create_engine('sqlite://')) - - misc.update_state(misc.State.STOPPED) + freqtradebot.update_state(State.STOPPED) (error, result) = rpc.rpc_trade_status() assert error - assert result.find('trader is not running') >= 0 + assert 'trader is not running' in result - misc.update_state(misc.State.RUNNING) + freqtradebot.update_state(State.RUNNING) (error, result) = rpc.rpc_trade_status() assert error - assert result.find('no active trade') >= 0 + assert 'no active trade' in result - main.create_trade(0.001, 5) + freqtradebot.create_trade(0.001, 5) (error, result) = rpc.rpc_trade_status() assert not error trade = result[0] + + result_message = [ + '*Trade ID:* `1`\n' + '*Current Pair:* ' + '[BTC_ETH](https://www.bittrex.com/Market/Index?MarketName=BTC-ETH)\n' + '*Open Since:* `just now`\n' + '*Amount:* `90.99181074`\n' + '*Open Rate:* `0.00001099`\n' + '*Close Rate:* `None`\n' + '*Current Rate:* `0.00001098`\n' + '*Close Profit:* `None`\n' + '*Current Profit:* `-0.59%`\n' + '*Open Order:* `(LIMIT_BUY rem=0.00000000)`' + ] + assert result == result_message assert trade.find('[BTC_ETH]') >= 0 -def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) - mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock()) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker) - mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', - ticker=MagicMock(return_value={'price_usd': 15000.0}), - _cache_symbols=MagicMock(return_value={'BTC': 1})) - mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - main.init(default_conf, create_engine('sqlite://')) +def test_rpc_status_table(default_conf, ticker, mocker) -> None: + """ + Test rpc_status_table() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker) + mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + rpc = RPC(freqtradebot) + + freqtradebot.update_state(State.STOPPED) + (error, result) = rpc.rpc_status_table() + assert error + assert '*Status:* `trader is not running`' in result + + freqtradebot.update_state(State.RUNNING) + (error, result) = rpc.rpc_status_table() + assert error + assert '*Status:* `no active order`' in result + + freqtradebot.create_trade(0.001, 5) + (error, result) = rpc.rpc_status_table() + assert 'just now' in result['Since'].all() + assert 'BTC_ETH' in result['Pair'].all() + assert '-0.59%' in result['Profit'].all() + + +def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker)\ + -> None: + """ + Test rpc_daily_profit() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker, value={'price_usd': 15000.0}) + mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] + rpc = RPC(freqtradebot) + # Create some test data - main.create_trade(0.001, 5) + freqtradebot.create_trade(0.001, 5) trade = Trade.query.first() assert trade @@ -210,8 +139,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s # Try valid data update.message.text = '/daily 2' - (error, days) = rpc.rpc_daily_profit(7, stake_currency, - fiat_display_currency) + (error, days) = rpc.rpc_daily_profit(7, stake_currency, fiat_display_currency) assert not error assert len(days) == 7 for day in days: @@ -225,51 +153,57 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s assert str(days[0][0]) == str(datetime.utcnow().date()) # Try invalid data - (error, days) = rpc.rpc_daily_profit(0, stake_currency, - fiat_display_currency) + (error, days) = rpc.rpc_daily_profit(0, stake_currency, fiat_display_currency) assert error assert days.find('must be an integer greater than 0') >= 0 def test_rpc_trade_statistics( - default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) - mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock()) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker) - mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', - ticker=MagicMock(return_value={'price_usd': 15000.0}), - _cache_symbols=MagicMock(return_value={'BTC': 1})) + default_conf, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker) -> None: + """ + Test rpc_trade_statistics() method + """ + patch_get_signal(mocker, (True, False)) + mocker.patch.multiple( + 'freqtrade.fiat_convert.Pymarketcap', + ticker=MagicMock(return_value={'price_usd': 15000.0}), + _cache_symbols=MagicMock(return_value={'BTC': 1}) + ) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - main.init(default_conf, create_engine('sqlite://')) + mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] - (error, stats) = rpc.rpc_trade_statistics(stake_currency, - fiat_display_currency) + rpc = RPC(freqtradebot) + + (error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency) assert error assert stats.find('no closed trade') >= 0 # Create some test data - main.create_trade(0.001, 5) + freqtradebot.create_trade(0.001, 5) trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) + # Update the ticker with a market going up - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker_sell_up) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker_sell_up + ) trade.update(limit_sell_order) trade.close_date = datetime.utcnow() trade.is_open = False - (error, stats) = rpc.rpc_trade_statistics(stake_currency, - fiat_display_currency) + (error, 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_percent'], 6.2) @@ -287,34 +221,42 @@ def test_rpc_trade_statistics( # Test that rpc_trade_statistics can handle trades that lacks # trade.open_rate (it is set to None) -def test_rpc_trade_statistics_closed( - default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) - mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock()) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker) - mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', - ticker=MagicMock(return_value={'price_usd': 15000.0}), - _cache_symbols=MagicMock(return_value={'BTC': 1})) +def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_up, limit_buy_order, + limit_sell_order): + """ + Test rpc_trade_statistics() method + """ + patch_get_signal(mocker, (True, False)) + mocker.patch.multiple( + 'freqtrade.fiat_convert.Pymarketcap', + ticker=MagicMock(return_value={'price_usd': 15000.0}), + _cache_symbols=MagicMock(return_value={'BTC': 1}) + ) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - main.init(default_conf, create_engine('sqlite://')) + mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] + rpc = RPC(freqtradebot) + # Create some test data - main.create_trade(0.001, 5) + freqtradebot.create_trade(0.001, 5) trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) # Update the ticker with a market going up - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker_sell_up) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker_sell_up + ) trade.update(limit_sell_order) trade.close_date = datetime.utcnow() trade.is_open = False @@ -322,8 +264,7 @@ def test_rpc_trade_statistics_closed( for trade in Trade.query.order_by(Trade.id).all(): trade.open_rate = None - (error, stats) = rpc.rpc_trade_statistics(stake_currency, - fiat_display_currency) + (error, 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_percent'], 0) @@ -339,58 +280,224 @@ def test_rpc_trade_statistics_closed( assert prec_satoshi(stats['best_rate'], 6.2) -def test_rpc_balance_handle(default_conf, update, mocker): - mock_balance = [{ - 'Currency': 'BTC', - 'Balance': 10.0, - 'Available': 12.0, - 'Pending': 0.0, - 'CryptoAddress': 'XXXX', - }, { - 'Currency': 'ETH', - 'Balance': 0.0, - 'Available': 0.0, - 'Pending': 0.0, - 'CryptoAddress': 'XXXX', - }] - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch.multiple('freqtrade.main.exchange', - get_balances=MagicMock(return_value=mock_balance)) - mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', - ticker=MagicMock(return_value={'price_usd': 15000.0}), - _cache_symbols=MagicMock(return_value={'BTC': 1})) +def test_rpc_balance_handle(default_conf, mocker): + """ + Test rpc_balance() method + """ + mock_balance = [ + { + 'Currency': 'BTC', + 'Balance': 10.0, + 'Available': 12.0, + 'Pending': 0.0, + 'CryptoAddress': 'XXXX', + }, + { + 'Currency': 'ETH', + 'Balance': 0.0, + 'Available': 0.0, + 'Pending': 0.0, + 'CryptoAddress': 'XXXX', + } + ] + + patch_get_signal(mocker, (True, False)) + mocker.patch.multiple( + 'freqtrade.fiat_convert.Pymarketcap', + ticker=MagicMock(return_value={'price_usd': 15000.0}), + _cache_symbols=MagicMock(return_value={'BTC': 1}) + ) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) + mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_balances=MagicMock(return_value=mock_balance) + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + rpc = RPC(freqtradebot) (error, res) = rpc.rpc_balance(default_conf['fiat_display_currency']) assert not error (trade, x, y, z) = res assert prec_satoshi(x, 10) assert prec_satoshi(z, 150000) - assert y == 'USD' + assert 'USD' in y assert len(trade) == 1 - assert trade[0]['currency'] == 'BTC' + assert 'BTC' in trade[0]['currency'] assert prec_satoshi(trade[0]['available'], 12) assert prec_satoshi(trade[0]['balance'], 10) assert prec_satoshi(trade[0]['pending'], 0) assert prec_satoshi(trade[0]['est_btc'], 10) -def test_performance_handle( - default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) - msg_mock = MagicMock() - mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker) - main.init(default_conf, create_engine('sqlite://')) +def test_rpc_start(mocker, default_conf) -> None: + """ + Test rpc_start() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker) + mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=MagicMock() + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + rpc = RPC(freqtradebot) + freqtradebot.update_state(State.STOPPED) + + (error, result) = rpc.rpc_start() + assert not error + assert '`Starting trader ...`' in result + assert freqtradebot.get_state() == State.RUNNING + + (error, result) = rpc.rpc_start() + assert error + assert '*Status:* `already running`' in result + assert freqtradebot.get_state() == State.RUNNING + + +def test_rpc_stop(mocker, default_conf) -> None: + """ + Test rpc_stop() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker) + mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=MagicMock() + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + rpc = RPC(freqtradebot) + freqtradebot.update_state(State.RUNNING) + + (error, result) = rpc.rpc_stop() + assert not error + assert '`Stopping trader ...`' in result + assert freqtradebot.get_state() == State.STOPPED + + (error, result) = rpc.rpc_stop() + assert error + assert '*Status:* `already stopped`' in result + assert freqtradebot.get_state() == State.STOPPED + + +def test_rpc_forcesell(default_conf, ticker, mocker) -> None: + """ + Test rpc_forcesell() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker) + mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) + + cancel_order_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker, + cancel_order=cancel_order_mock, + get_order=MagicMock( + return_value={ + 'closed': True, + 'type': 'LIMIT_BUY', + } + ) + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + rpc = RPC(freqtradebot) + + freqtradebot.update_state(State.STOPPED) + (error, res) = rpc.rpc_forcesell(None) + assert error + assert res == '`trader is not running`' + + freqtradebot.update_state(State.RUNNING) + (error, res) = rpc.rpc_forcesell(None) + assert error + assert res == 'Invalid argument.' + + (error, res) = rpc.rpc_forcesell('all') + assert not error + assert res == '' + + freqtradebot.create_trade(0.001, 5) + (error, res) = rpc.rpc_forcesell('all') + assert not error + assert res == '' + + (error, res) = rpc.rpc_forcesell('1') + assert not error + assert res == '' + + freqtradebot.update_state(State.STOPPED) + (error, res) = rpc.rpc_forcesell(None) + assert error + assert res == '`trader is not running`' + + (error, res) = rpc.rpc_forcesell('all') + assert error + assert res == '`trader is not running`' + + freqtradebot.update_state(State.RUNNING) + assert cancel_order_mock.call_count == 0 + # make an limit-buy open trade + mocker.patch( + 'freqtrade.freqtradebot.exchange.get_order', + return_value={ + 'closed': None, + 'type': 'LIMIT_BUY' + } + ) + # check that the trade is called, which is done + # by ensuring exchange.cancel_order is called + (error, res) = rpc.rpc_forcesell('1') + assert not error + assert res == '' + assert cancel_order_mock.call_count == 1 + + freqtradebot.create_trade(0.001, 5) + # make an limit-sell open trade + mocker.patch( + 'freqtrade.freqtradebot.exchange.get_order', + return_value={ + 'closed': None, + 'type': 'LIMIT_SELL' + } + ) + (error, res) = rpc.rpc_forcesell('2') + assert not error + assert res == '' + # status quo, no exchange calls + assert cancel_order_mock.call_count == 1 + + +def test_performance_handle(default_conf, ticker, limit_buy_order, + limit_sell_order, mocker) -> None: + """ + Test rpc_performance() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker) + mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + rpc = RPC(freqtradebot) # Create some test data - main.create_trade(0.001, int(default_conf['ticker_interval'])) + freqtradebot.create_trade(0.001, int(default_conf['ticker_interval'])) trade = Trade.query.first() assert trade @@ -408,3 +515,33 @@ def test_performance_handle( assert res[0]['pair'] == 'BTC_ETH' assert res[0]['count'] == 1 assert prec_satoshi(res[0]['profit'], 6.2) + + +def test_rpc_count(mocker, default_conf, ticker) -> None: + """ + Test rpc_count() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker) + mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_balances=MagicMock(return_value=ticker), + get_ticker=ticker + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + rpc = RPC(freqtradebot) + + (error, trades) = rpc.rpc_count() + nb_trades = len(trades) + assert not error + assert nb_trades == 0 + + # Create some test data + freqtradebot.create_trade(0.001, int(default_conf['ticker_interval'])) + (error, trades) = rpc.rpc_count() + nb_trades = len(trades) + assert not error + assert nb_trades == 1 diff --git a/freqtrade/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py new file mode 100644 index 000000000..e4a7d4cda --- /dev/null +++ b/freqtrade/tests/rpc/test_rpc_manager.py @@ -0,0 +1,139 @@ +""" +Unit test file for rpc/rpc_manager.py +""" + +import logging +from copy import deepcopy +from unittest.mock import MagicMock + +from freqtrade.rpc.rpc_manager import RPCManager +from freqtrade.rpc.telegram import Telegram +import freqtrade.tests.conftest as tt # test tools + + +def test_rpc_manager_object() -> None: + """ + Test the Arguments object has the mandatory methods + :return: None + """ + assert hasattr(RPCManager, '_init') + assert hasattr(RPCManager, 'send_msg') + assert hasattr(RPCManager, 'cleanup') + + +def test__init__(mocker, default_conf) -> None: + """ + Test __init__() method + """ + init_mock = mocker.patch('freqtrade.rpc.rpc_manager.RPCManager._init', MagicMock()) + freqtradebot = tt.get_patched_freqtradebot(mocker, default_conf) + + rpc_manager = RPCManager(freqtradebot) + assert rpc_manager.freqtrade == freqtradebot + 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: + """ + Test _init() method with Telegram disabled + """ + caplog.set_level(logging.DEBUG) + + conf = deepcopy(default_conf) + conf['telegram']['enabled'] = False + + freqtradebot = tt.get_patched_freqtradebot(mocker, conf) + rpc_manager = RPCManager(freqtradebot) + + assert not tt.log_has('Enabling rpc.telegram ...', caplog.record_tuples) + assert rpc_manager.registered_modules == [] + assert rpc_manager.telegram is None + + +def test_init_telegram_enabled(mocker, default_conf, caplog) -> None: + """ + Test _init() method with Telegram enabled + """ + caplog.set_level(logging.DEBUG) + mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + + freqtradebot = tt.get_patched_freqtradebot(mocker, default_conf) + rpc_manager = RPCManager(freqtradebot) + + assert tt.log_has('Enabling rpc.telegram ...', caplog.record_tuples) + len_modules = len(rpc_manager.registered_modules) + assert len_modules == 1 + assert 'telegram' in rpc_manager.registered_modules + assert isinstance(rpc_manager.telegram, Telegram) + + +def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None: + """ + Test cleanup() method with Telegram disabled + """ + caplog.set_level(logging.DEBUG) + telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock()) + + conf = deepcopy(default_conf) + conf['telegram']['enabled'] = False + + freqtradebot = tt.get_patched_freqtradebot(mocker, conf) + rpc_manager = RPCManager(freqtradebot) + rpc_manager.cleanup() + + assert not tt.log_has('Cleaning up rpc.telegram ...', caplog.record_tuples) + assert telegram_mock.call_count == 0 + + +def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None: + """ + Test cleanup() method with Telegram enabled + """ + caplog.set_level(logging.DEBUG) + mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock()) + + freqtradebot = tt.get_patched_freqtradebot(mocker, default_conf) + rpc_manager = RPCManager(freqtradebot) + + # Check we have Telegram as a registered modules + assert 'telegram' in rpc_manager.registered_modules + + rpc_manager.cleanup() + assert tt.log_has('Cleaning up rpc.telegram ...', caplog.record_tuples) + assert 'telegram' not in rpc_manager.registered_modules + assert telegram_mock.call_count == 1 + + +def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: + """ + Test send_msg() method with Telegram disabled + """ + telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + + conf = deepcopy(default_conf) + conf['telegram']['enabled'] = False + + freqtradebot = tt.get_patched_freqtradebot(mocker, conf) + rpc_manager = RPCManager(freqtradebot) + rpc_manager.send_msg('test') + + assert tt.log_has('test', caplog.record_tuples) + assert telegram_mock.call_count == 0 + + +def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: + """ + Test send_msg() method with Telegram disabled + """ + telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + + freqtradebot = tt.get_patched_freqtradebot(mocker, default_conf) + rpc_manager = RPCManager(freqtradebot) + rpc_manager.send_msg('test') + + assert tt.log_has('test', caplog.record_tuples) + assert telegram_mock.call_count == 1 diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 9a1dbcc69..5aaf13742 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -1,148 +1,355 @@ -# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103 -# pragma pylint: disable=unused-argument +# pragma pylint: disable=protected-access, unused-argument, invalid-name +# pragma pylint: disable=too-many-lines, too-many-arguments + +""" +Unit test file for rpc/telegram.py +""" + import re from datetime import datetime from random import randint from unittest.mock import MagicMock +from copy import deepcopy from sqlalchemy import create_engine from telegram import Update, Message, Chat from telegram.error import NetworkError from freqtrade import __version__ -from freqtrade.main import init, create_trade -from freqtrade.misc import update_state, State, get_state +from freqtrade.rpc.telegram import authorized_only +from freqtrade.freqtradebot import FreqtradeBot +from freqtrade.rpc.telegram import Telegram from freqtrade.persistence import Trade -from freqtrade.rpc import telegram -from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \ - _profit, _forcesell, _performance, _daily, _count, _start, _stop, _balance, _version, _help - -import freqtrade.rpc.telegram as tg +from freqtrade.state import State +from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_pymarketcap +from freqtrade.tests.conftest import get_patched_freqtradebot, log_has +import freqtrade.tests.conftest as tt # test tools -def test_is_enabled(default_conf, mocker): - mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf) - default_conf['telegram']['enabled'] = False - assert is_enabled() is False +class DummyCls(Telegram): + """ + Dummy class for testing the Telegram @authorized_only decorator + """ + def __init__(self, freqtrade) -> None: + super().__init__(freqtrade) + self.state = {'called': False} + + @authorized_only + def dummy_handler(self, *args, **kwargs) -> None: + """ + Fake method that only change the state of the object + """ + self.state['called'] = True + + @authorized_only + def dummy_exception(self, *args, **kwargs) -> None: + """ + Fake method that throw an exception + """ + raise Exception('test') -def test_init_disabled(default_conf, mocker): - mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf) - default_conf['telegram']['enabled'] = False - telegram.init(default_conf) +def test__init__(default_conf, mocker) -> None: + """ + Test __init__() method + """ + mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + + telegram = Telegram(get_patched_freqtradebot(mocker, default_conf)) + assert telegram._updater is None + assert telegram._config == default_conf -def test_authorized_only(default_conf, mocker): - mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf) +def test_init(default_conf, mocker, caplog) -> None: + """ + Test _init() method + """ + start_polling = MagicMock() + mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) + Telegram(get_patched_freqtradebot(mocker, default_conf)) + assert start_polling.call_count == 0 + + # number of handles registered + assert start_polling.dispatcher.add_handler.call_count == 11 + assert start_polling.start_polling.call_count == 1 + + message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \ + "['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \ + "['count'], ['help'], ['version']]" + + 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: + """ + Test cleanup() method + """ + updater_mock = MagicMock() + updater_mock.stop = MagicMock() + mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) + + # not enabled + 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() + 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, caplog) -> None: + """ + Test authorized_only() method when we are authorized + """ chat = Chat(0, 0) update = Update(randint(1, 100)) update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) - state = {'called': False} - @authorized_only - def dummy_handler(*args, **kwargs) -> None: - state['called'] = True - - dummy_handler(MagicMock(), update) - assert state['called'] is True + conf = deepcopy(default_conf) + conf['telegram']['enabled'] = False + dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://'))) + dummy.dummy_handler(bot=MagicMock(), update=update) + assert dummy.state['called'] is True + assert tt.log_has( + 'Executing handler: dummy_handler for chat_id: 0', + caplog.record_tuples + ) + assert not tt.log_has( + 'Rejected unauthorized message from: 0', + caplog.record_tuples + ) + assert not tt.log_has( + 'Exception occurred within Telegram module', + caplog.record_tuples + ) -def test_authorized_only_unauthorized(default_conf, mocker): - mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf) - +def test_authorized_only_unauthorized(default_conf, caplog) -> None: + """ + Test authorized_only() method when we are unauthorized + """ chat = Chat(0xdeadbeef, 0) update = Update(randint(1, 100)) update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) - state = {'called': False} - @authorized_only - def dummy_handler(*args, **kwargs) -> None: - state['called'] = True - - dummy_handler(MagicMock(), update) - assert state['called'] is False + conf = deepcopy(default_conf) + conf['telegram']['enabled'] = False + dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://'))) + dummy.dummy_handler(bot=MagicMock(), update=update) + assert dummy.state['called'] is False + assert not tt.log_has( + 'Executing handler: dummy_handler for chat_id: 3735928559', + caplog.record_tuples + ) + assert tt.log_has( + 'Rejected unauthorized message from: 3735928559', + caplog.record_tuples + ) + assert not tt.log_has( + 'Exception occurred within Telegram module', + caplog.record_tuples + ) -def test_authorized_only_exception(default_conf, mocker): - mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf) - +def test_authorized_only_exception(default_conf, caplog) -> None: + """ + Test authorized_only() method when an exception is thrown + """ update = Update(randint(1, 100)) update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0)) - @authorized_only - def dummy_handler(*args, **kwargs) -> None: - raise Exception('test') - - dummy_handler(MagicMock(), update) + conf = deepcopy(default_conf) + conf['telegram']['enabled'] = False + dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://'))) + dummy.dummy_exception(bot=MagicMock(), update=update) + assert dummy.state['called'] is False + assert not tt.log_has( + 'Executing handler: dummy_handler for chat_id: 0', + caplog.record_tuples + ) + assert not tt.log_has( + 'Rejected unauthorized message from: 0', + caplog.record_tuples + ) + assert tt.log_has( + 'Exception occurred within Telegram module', + caplog.record_tuples + ) -def test_status_handle(default_conf, update, ticker, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) +def test_status(default_conf, update, mocker, ticker) -> None: + """ + Test _status() method + """ + update.message.chat.id = 123 + conf = deepcopy(default_conf) + conf['telegram']['enabled'] = False + conf['telegram']['chat_id'] = 123 + + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) msg_mock = MagicMock() - mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker) - init(default_conf, create_engine('sqlite://')) + status_table = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + rpc_trade_status=MagicMock(return_value=(False, [1, 2, 3])), + _status_table=status_table, + send_msg=msg_mock + ) + mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - update_state(State.STOPPED) - _status(bot=MagicMock(), update=update) + freqtradebot = FreqtradeBot(conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + # Create some test data + for _ in range(3): + freqtradebot.create_trade(0.001, 5) + + telegram._status(bot=MagicMock(), update=update) + assert msg_mock.call_count == 3 + + update.message.text = MagicMock() + update.message.text.replace = MagicMock(return_value='table 2 3') + telegram._status(bot=MagicMock(), update=update) + assert status_table.call_count == 1 + + +def test_status_handle(default_conf, update, ticker, mocker) -> None: + """ + Test _status() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) + msg_mock = MagicMock() + status_table = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _status_table=status_table, + send_msg=msg_mock + ) + mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + freqtradebot.update_state(State.STOPPED) + telegram._status(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'trader is not running' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() - update_state(State.RUNNING) - _status(bot=MagicMock(), update=update) + freqtradebot.update_state(State.RUNNING) + telegram._status(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() # Create some test data - create_trade(0.001, int(default_conf['ticker_interval'])) + freqtradebot.create_trade(0.001, int(default_conf['ticker_interval'])) # Trigger status while we have a fulfilled order for the open trade - _status(bot=MagicMock(), update=update) + telegram._status(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert '[BTC_ETH]' in msg_mock.call_args_list[0][0][0] -def test_status_table_handle(default_conf, update, ticker, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) - msg_mock = MagicMock() - mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) +def test_status_table_handle(default_conf, update, ticker, mocker) -> None: + """ + Test _status_table() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker) mocker.patch.multiple( - 'freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker, - buy=MagicMock(return_value='mocked_order_id')) - init(default_conf, create_engine('sqlite://')) - update_state(State.STOPPED) - _status_table(bot=MagicMock(), update=update) + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker, + buy=MagicMock(return_value='mocked_order_id') + ) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + freqtradebot.update_state(State.STOPPED) + telegram._status_table(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'trader is not running' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() - update_state(State.RUNNING) - _status_table(bot=MagicMock(), update=update) + freqtradebot.update_state(State.RUNNING) + telegram._status_table(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'no active order' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() # Create some test data - create_trade(15.0, int(default_conf['ticker_interval'])) + freqtradebot.create_trade(15.0, int(default_conf['ticker_interval'])) - _status_table(bot=MagicMock(), update=update) + telegram._status_table(bot=MagicMock(), update=update) text = re.sub('', '', msg_mock.call_args_list[-1][0][0]) line = text.split("\n") @@ -153,256 +360,35 @@ def test_status_table_handle(default_conf, update, ticker, mocker): assert msg_mock.call_count == 1 -def test_profit_handle( - default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) +def test_daily_handle(default_conf, update, ticker, limit_buy_order, + limit_sell_order, mocker) -> None: + """ + Test _daily() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker, value={'price_usd': 15000.0}) + mocker.patch( + 'freqtrade.fiat_convert.CryptoToFiatConverter._find_price', + return_value=15000.0 + ) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) msg_mock = MagicMock() - mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker) - mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', - ticker=MagicMock(return_value={'price_usd': 15000.0}), - _cache_symbols=MagicMock(return_value={'BTC': 1})) - mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - init(default_conf, create_engine('sqlite://')) + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - _profit(bot=MagicMock(), update=update) - assert msg_mock.call_count == 1 - assert 'no closed trade' in msg_mock.call_args_list[0][0][0] - msg_mock.reset_mock() + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) # Create some test data - create_trade(0.001, int(default_conf['ticker_interval'])) - trade = Trade.query.first() - - # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) - - _profit(bot=MagicMock(), update=update) - assert msg_mock.call_count == 1 - assert 'no closed trade' in msg_mock.call_args_list[-1][0][0] - msg_mock.reset_mock() - - # Update the ticker with a market going up - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker_sell_up) - trade.update(limit_sell_order) - - trade.close_date = datetime.utcnow() - trade.is_open = False - - _profit(bot=MagicMock(), update=update) - assert msg_mock.call_count == 1 - assert '*ROI:* Close trades' in msg_mock.call_args_list[-1][0][0] - assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0] - assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0] - assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] - assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0] - assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0] - - assert '*Best Performing:* `BTC_ETH: 6.20%`' in msg_mock.call_args_list[-1][0][0] - - -def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) - rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=MagicMock()) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker) - mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - init(default_conf, create_engine('sqlite://')) - - # Create some test data - create_trade(0.001, int(default_conf['ticker_interval'])) - - trade = Trade.query.first() - assert trade - - # Increase the price and sell it - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker_sell_up) - - update.message.text = '/forcesell 1' - _forcesell(bot=MagicMock(), update=update) - - assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] - assert 'Amount' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] - assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0] - assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0] - - -def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) - rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=MagicMock()) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker) - mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - init(default_conf, create_engine('sqlite://')) - - # Create some test data - create_trade(0.001, int(default_conf['ticker_interval'])) - - # Decrease the price and sell it - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker_sell_down) - - trade = Trade.query.first() - assert trade - - update.message.text = '/forcesell 1' - _forcesell(bot=MagicMock(), update=update) - - assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] - assert 'Amount' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] - assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] - assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0] - - -def test_forcesell_all_handle(default_conf, update, ticker, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) - rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=MagicMock()) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker) - mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - init(default_conf, create_engine('sqlite://')) - - # Create some test data - for _ in range(4): - create_trade(0.001, int(default_conf['ticker_interval'])) - rpc_mock.reset_mock() - - update.message.text = '/forcesell all' - _forcesell(bot=MagicMock(), update=update) - - assert rpc_mock.call_count == 4 - for args in rpc_mock.call_args_list: - assert '0.00001098' in args[0][0] - assert 'loss: -0.59%, -0.00000591 BTC' in args[0][0] - assert '-0.089 USD' in args[0][0] - - -def test_forcesell_handle_invalid(default_conf, update, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, True)) - msg_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock()) - init(default_conf, create_engine('sqlite://')) - - # Trader is not running - update_state(State.STOPPED) - update.message.text = '/forcesell 1' - _forcesell(bot=MagicMock(), update=update) - assert msg_mock.call_count == 1 - assert 'not running' in msg_mock.call_args_list[0][0][0] - - # No argument - msg_mock.reset_mock() - update_state(State.RUNNING) - update.message.text = '/forcesell' - _forcesell(bot=MagicMock(), update=update) - assert msg_mock.call_count == 1 - assert 'Invalid argument' in msg_mock.call_args_list[0][0][0] - - # Invalid argument - msg_mock.reset_mock() - update_state(State.RUNNING) - update.message.text = '/forcesell 123456' - _forcesell(bot=MagicMock(), update=update) - assert msg_mock.call_count == 1 - assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0] - - -def test_performance_handle( - default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) - msg_mock = MagicMock() - mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker) - init(default_conf, create_engine('sqlite://')) - - # Create some test data - create_trade(0.001, int(default_conf['ticker_interval'])) - trade = Trade.query.first() - assert trade - - # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) - - # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) - - trade.close_date = datetime.utcnow() - trade.is_open = False - _performance(bot=MagicMock(), update=update) - assert msg_mock.call_count == 1 - assert 'Performance' in msg_mock.call_args_list[0][0][0] - assert 'BTC_ETH\t6.20% (1)' in msg_mock.call_args_list[0][0][0] - - -def test_daily_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) - msg_mock = MagicMock() - mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker) - mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', - ticker=MagicMock(return_value={'price_usd': 15000.0}), - _cache_symbols=MagicMock(return_value={'BTC': 1})) - mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - init(default_conf, create_engine('sqlite://')) - - # Create some test data - create_trade(0.001, int(default_conf['ticker_interval'])) + freqtradebot.create_trade(0.001, int(default_conf['ticker_interval'])) trade = Trade.query.first() assert trade @@ -417,7 +403,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, limit_sell_ # Try valid data update.message.text = '/daily 2' - _daily(bot=MagicMock(), update=update) + telegram._daily(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'Daily' in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] @@ -429,8 +415,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, limit_sell_ # Reset msg_mock msg_mock.reset_mock() # Add two other trades - create_trade(0.001, int(default_conf['ticker_interval'])) - create_trade(0.001, int(default_conf['ticker_interval'])) + freqtradebot.create_trade(0.001, int(default_conf['ticker_interval'])) + freqtradebot.create_trade(0.001, int(default_conf['ticker_interval'])) trades = Trade.query.all() for trade in trades: @@ -441,227 +427,178 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, limit_sell_ update.message.text = '/daily 1' - _daily(bot=MagicMock(), update=update) + telegram._daily(bot=MagicMock(), update=update) assert str(' 0.00018651 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 2.798 USD') in msg_mock.call_args_list[0][0][0] assert str(' 3 trades') in msg_mock.call_args_list[0][0][0] -def test_daily_wrong_input(default_conf, update, ticker, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) +def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: + """ + Test _daily() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker, value={'price_usd': 15000.0}) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) msg_mock = MagicMock() - mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker) - mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', - ticker=MagicMock(return_value={'price_usd': 15000.0}), - _cache_symbols=MagicMock(return_value={'BTC': 1})) - mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - init(default_conf, create_engine('sqlite://')) + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) # Try invalid data msg_mock.reset_mock() - update_state(State.RUNNING) + freqtradebot.update_state(State.RUNNING) update.message.text = '/daily -2' - _daily(bot=MagicMock(), update=update) + telegram._daily(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'must be an integer greater than 0' in msg_mock.call_args_list[0][0][0] # Try invalid data msg_mock.reset_mock() - update_state(State.RUNNING) + freqtradebot.update_state(State.RUNNING) update.message.text = '/daily today' - _daily(bot=MagicMock(), update=update) + telegram._daily(bot=MagicMock(), update=update) assert str('Daily Profit over the last 7 days') in msg_mock.call_args_list[0][0][0] -def test_count_handle(default_conf, update, ticker, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, False)) +def test_profit_handle(default_conf, update, ticker, ticker_sell_up, + limit_buy_order, limit_sell_order, mocker) -> None: + """ + Test _profit() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker, value={'price_usd': 15000.0}) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) msg_mock = MagicMock() mocker.patch.multiple( - 'freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock(), - get_ticker=ticker, - buy=MagicMock(return_value='mocked_order_id')) - init(default_conf, create_engine('sqlite://')) - update_state(State.STOPPED) - _count(bot=MagicMock(), update=update) + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + telegram._profit(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 - assert 'not running' in msg_mock.call_args_list[0][0][0] + assert 'no closed trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() - update_state(State.RUNNING) # Create some test data - create_trade(0.001, int(default_conf['ticker_interval'])) + freqtradebot.create_trade(0.001, int(default_conf['ticker_interval'])) + trade = Trade.query.first() + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + + telegram._profit(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert 'no closed trade' in msg_mock.call_args_list[-1][0][0] msg_mock.reset_mock() - _count(bot=MagicMock(), update=update) - msg = '
  current    max\n---------  -----\n        1      {}
'.format( - default_conf['max_open_trades'] - ) - assert msg in msg_mock.call_args_list[0][0][0] + # Update the ticker with a market going up + mocker.patch('freqtrade.freqtradebot.exchange.get_ticker', ticker_sell_up) + trade.update(limit_sell_order) + trade.close_date = datetime.utcnow() + trade.is_open = False -def test_performance_handle_invalid(default_conf, update, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: (True, True)) - msg_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - validate_pairs=MagicMock()) - init(default_conf, create_engine('sqlite://')) - - # Trader is not running - update_state(State.STOPPED) - _performance(bot=MagicMock(), update=update) + telegram._profit(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 - assert 'not running' in msg_mock.call_args_list[0][0][0] + assert '*ROI:* Close trades' in msg_mock.call_args_list[-1][0][0] + assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0] + assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0] + assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] + assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0] + assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0] + + assert '*Best Performing:* `BTC_ETH: 6.20%`' in msg_mock.call_args_list[-1][0][0] -def test_start_handle(default_conf, update, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - msg_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - _CONF=default_conf, - init=MagicMock()) - init(default_conf, create_engine('sqlite://')) - update_state(State.STOPPED) - assert get_state() == State.STOPPED - _start(bot=MagicMock(), update=update) - assert get_state() == State.RUNNING - assert msg_mock.call_count == 0 - - -def test_start_handle_already_running(default_conf, update, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - msg_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - _CONF=default_conf, - init=MagicMock()) - init(default_conf, create_engine('sqlite://')) - update_state(State.RUNNING) - assert get_state() == State.RUNNING - _start(bot=MagicMock(), update=update) - assert get_state() == State.RUNNING - assert msg_mock.call_count == 1 - assert 'already running' in msg_mock.call_args_list[0][0][0] - - -def test_stop_handle(default_conf, update, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - msg_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - _CONF=default_conf, - init=MagicMock()) - init(default_conf, create_engine('sqlite://')) - update_state(State.RUNNING) - assert get_state() == State.RUNNING - _stop(bot=MagicMock(), update=update) - assert get_state() == State.STOPPED - assert msg_mock.call_count == 1 - assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] - - -def test_stop_handle_already_stopped(default_conf, update, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - msg_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - _CONF=default_conf, - init=MagicMock()) - init(default_conf, create_engine('sqlite://')) - update_state(State.STOPPED) - assert get_state() == State.STOPPED - _stop(bot=MagicMock(), update=update) - assert get_state() == State.STOPPED - assert msg_mock.call_count == 1 - assert 'already stopped' in msg_mock.call_args_list[0][0][0] - - -def test_telegram_balance_handle(default_conf, update, mocker): - mock_balance = [{ - 'Currency': 'BTC', - 'Balance': 10.0, - 'Available': 12.0, - 'Pending': 0.0, - 'CryptoAddress': 'XXXX', - }, { - 'Currency': 'ETH', - 'Balance': 0.0, - 'Available': 0.0, - 'Pending': 0.0, - 'CryptoAddress': 'XXXX', - }, { - 'Currency': 'USDT', - 'Balance': 10000.0, - 'Available': 0.0, - 'Pending': 0.0, - 'CryptoAddress': 'XXXX', - }, { - 'Currency': 'LTC', - 'Balance': 10.0, - 'Available': 10.0, - 'Pending': 0.0, - 'CryptoAddress': 'XXXX', - }] +def test_telegram_balance_handle(default_conf, update, mocker) -> None: + """ + Test _balance() method + """ + mock_balance = [ + { + 'Currency': 'BTC', + 'Balance': 10.0, + 'Available': 12.0, + 'Pending': 0.0, + 'CryptoAddress': 'XXXX', + }, + { + 'Currency': 'ETH', + 'Balance': 0.0, + 'Available': 0.0, + 'Pending': 0.0, + 'CryptoAddress': 'XXXX', + }, + { + 'Currency': 'USDT', + 'Balance': 10000.0, + 'Available': 0.0, + 'Pending': 0.0, + 'CryptoAddress': 'XXXX', + }, + { + 'Currency': 'LTC', + 'Balance': 10.0, + 'Available': 10.0, + 'Pending': 0.0, + 'CryptoAddress': 'XXXX', + } + ] def mock_ticker(symbol, refresh): + """ + Mock Bittrex.get_ticker() response + """ if symbol == 'USDT_BTC': return { 'bid': 10000.00, 'ask': 10000.00, 'last': 10000.00, } - else: - return { - 'bid': 0.1, - 'ask': 0.1, - 'last': 0.1, - } - mocker.patch.dict('freqtrade.main._CONF', default_conf) + return { + 'bid': 0.1, + 'ask': 0.1, + 'last': 0.1, + } + + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker, value={'price_usd': 15000.0}) + mocker.patch('freqtrade.freqtradebot.exchange.get_balances', return_value=mock_balance) + mocker.patch('freqtrade.freqtradebot.exchange.get_ticker', side_effect=mock_ticker) + msg_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - get_balances=MagicMock(return_value=mock_balance)) - mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', - ticker=MagicMock(return_value={'price_usd': 15000.0}), - _cache_symbols=MagicMock(return_value={'BTC': 1})) - mocker.patch('freqtrade.main.exchange.get_ticker', side_effect=mock_ticker) + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) - _balance(bot=MagicMock(), update=update) + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + telegram._balance(bot=MagicMock(), update=update) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 assert '*Currency*: BTC' in result @@ -672,135 +609,462 @@ def test_telegram_balance_handle(default_conf, update, mocker): assert '*BTC*: 12.00000000' in result -def test_zero_balance_handle(default_conf, update, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) +def test_zero_balance_handle(default_conf, update, mocker) -> None: + """ + Test _balance() method when the Exchange platform returns nothing + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker, value={'price_usd': 15000.0}) + mocker.patch('freqtrade.freqtradebot.exchange.get_balances', return_value=[]) + msg_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) - mocker.patch.multiple('freqtrade.main.exchange', - get_balances=MagicMock(return_value=[])) - _balance(bot=MagicMock(), update=update) + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + telegram._balance(bot=MagicMock(), update=update) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 assert '`All balances are zero.`' in result -def test_help_handle(default_conf, update, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) +def test_start_handle(default_conf, update, mocker) -> None: + """ + Test _start() method + """ + patch_pymarketcap(mocker) msg_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - _help(bot=MagicMock(), update=update) + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + freqtradebot.update_state(State.STOPPED) + assert freqtradebot.get_state() == State.STOPPED + telegram._start(bot=MagicMock(), update=update) + assert freqtradebot.get_state() == State.RUNNING + assert msg_mock.call_count == 0 + + +def test_start_handle_already_running(default_conf, update, mocker) -> None: + """ + Test _start() method + """ + patch_pymarketcap(mocker) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + freqtradebot.update_state(State.RUNNING) + assert freqtradebot.get_state() == State.RUNNING + telegram._start(bot=MagicMock(), update=update) + assert freqtradebot.get_state() == State.RUNNING + assert msg_mock.call_count == 1 + assert 'already running' in msg_mock.call_args_list[0][0][0] + + +def test_stop_handle(default_conf, update, mocker) -> None: + """ + Test _stop() method + """ + patch_pymarketcap(mocker) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + freqtradebot.update_state(State.RUNNING) + assert freqtradebot.get_state() == State.RUNNING + telegram._stop(bot=MagicMock(), update=update) + assert freqtradebot.get_state() == State.STOPPED + assert msg_mock.call_count == 1 + assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] + + +def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: + """ + Test _stop() method + """ + patch_pymarketcap(mocker) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + freqtradebot.update_state(State.STOPPED) + assert freqtradebot.get_state() == State.STOPPED + telegram._stop(bot=MagicMock(), update=update) + assert freqtradebot.get_state() == State.STOPPED + assert msg_mock.call_count == 1 + assert 'already stopped' in msg_mock.call_args_list[0][0][0] + + +def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker) -> None: + """ + Test _forcesell() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker, value={'price_usd': 15000.0}) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) + rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + # Create some test data + freqtradebot.create_trade(0.001, int(default_conf['ticker_interval'])) + + trade = Trade.query.first() + assert trade + + # Increase the price and sell it + mocker.patch('freqtrade.freqtradebot.exchange.get_ticker', ticker_sell_up) + + update.message.text = '/forcesell 1' + telegram._forcesell(bot=MagicMock(), update=update) + + assert rpc_mock.call_count == 2 + assert 'Selling' in rpc_mock.call_args_list[-1][0][0] + assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] + assert 'Amount' in rpc_mock.call_args_list[-1][0][0] + assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] + assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0] + assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0] + + +def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, mocker) -> None: + """ + Test _forcesell() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker, value={'price_usd': 15000.0}) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) + rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + # Create some test data + freqtradebot.create_trade(0.001, int(default_conf['ticker_interval'])) + + # Decrease the price and sell it + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker_sell_down + ) + + trade = Trade.query.first() + assert trade + + update.message.text = '/forcesell 1' + telegram._forcesell(bot=MagicMock(), update=update) + + assert rpc_mock.call_count == 2 + assert 'Selling' in rpc_mock.call_args_list[-1][0][0] + assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] + assert 'Amount' in rpc_mock.call_args_list[-1][0][0] + assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] + assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] + assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0] + + +def test_forcesell_all_handle(default_conf, update, ticker, mocker) -> None: + """ + Test _forcesell() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker, value={'price_usd': 15000.0}) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) + rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + # Create some test data + for _ in range(4): + freqtradebot.create_trade(0.001, int(default_conf['ticker_interval'])) + rpc_mock.reset_mock() + + update.message.text = '/forcesell all' + telegram._forcesell(bot=MagicMock(), update=update) + + assert rpc_mock.call_count == 4 + for args in rpc_mock.call_args_list: + assert '0.00001098' in args[0][0] + assert 'loss: -0.59%, -0.00000591 BTC' in args[0][0] + assert '-0.089 USD' in args[0][0] + + +def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: + """ + Test _forcesell() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker, value={'price_usd': 15000.0}) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) + + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + # Trader is not running + freqtradebot.update_state(State.STOPPED) + update.message.text = '/forcesell 1' + telegram._forcesell(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert 'not running' in msg_mock.call_args_list[0][0][0] + + # No argument + msg_mock.reset_mock() + freqtradebot.update_state(State.RUNNING) + update.message.text = '/forcesell' + telegram._forcesell(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert 'Invalid argument' in msg_mock.call_args_list[0][0][0] + + # Invalid argument + msg_mock.reset_mock() + freqtradebot.update_state(State.RUNNING) + update.message.text = '/forcesell 123456' + telegram._forcesell(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0] + + +def test_performance_handle(default_conf, update, ticker, limit_buy_order, + limit_sell_order, mocker) -> None: + """ + Test _performance() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker + ) + mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + # Create some test data + freqtradebot.create_trade(0.001, int(default_conf['ticker_interval'])) + trade = Trade.query.first() + assert trade + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + + # Simulate fulfilled LIMIT_SELL order for trade + trade.update(limit_sell_order) + + trade.close_date = datetime.utcnow() + trade.is_open = False + telegram._performance(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert 'Performance' in msg_mock.call_args_list[0][0][0] + assert 'BTC_ETH\t6.20% (1)' in msg_mock.call_args_list[0][0][0] + + +def test_performance_handle_invalid(default_conf, update, mocker) -> None: + """ + Test _performance() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + # Trader is not running + freqtradebot.update_state(State.STOPPED) + telegram._performance(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert 'not running' in msg_mock.call_args_list[0][0][0] + + +def test_count_handle(default_conf, update, ticker, mocker) -> None: + """ + Test _count() method + """ + patch_get_signal(mocker, (True, False)) + patch_pymarketcap(mocker) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + mocker.patch.multiple( + 'freqtrade.freqtradebot.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker, + buy=MagicMock(return_value='mocked_order_id') + ) + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + freqtradebot.update_state(State.STOPPED) + telegram._count(bot=MagicMock(), update=update) + assert msg_mock.call_count == 1 + assert 'not running' in msg_mock.call_args_list[0][0][0] + msg_mock.reset_mock() + freqtradebot.update_state(State.RUNNING) + + # Create some test data + freqtradebot.create_trade(0.001, int(default_conf['ticker_interval'])) + msg_mock.reset_mock() + telegram._count(bot=MagicMock(), update=update) + + msg = '
  current    max\n---------  -----\n        1      {}
'.format( + default_conf['max_open_trades'] + ) + assert msg in msg_mock.call_args_list[0][0][0] + + +def test_help_handle(default_conf, update, mocker) -> None: + """ + Test _help() method + """ + patch_pymarketcap(mocker) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + telegram._help(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0] -def test_version_handle(default_conf, update, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) +def test_version_handle(default_conf, update, mocker) -> None: + """ + Test _version() method + """ + patch_pymarketcap(mocker) msg_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - send_msg=msg_mock) + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + send_msg=msg_mock + ) + freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) - _version(bot=MagicMock(), update=update) + telegram._version(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0] -def test_send_msg(default_conf, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock()) +def test_send_msg(default_conf, mocker) -> None: + """ + Test send_msg() method + """ + patch_pymarketcap(mocker) + mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + conf = deepcopy(default_conf) bot = MagicMock() - send_msg('test', bot) + freqtradebot = FreqtradeBot(conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + telegram._config['telegram']['enabled'] = False + telegram.send_msg('test', bot) assert not bot.method_calls bot.reset_mock() - default_conf['telegram']['enabled'] = True - send_msg('test', bot) + telegram._config['telegram']['enabled'] = True + telegram.send_msg('test', bot) assert len(bot.method_calls) == 1 -def test_send_msg_network_error(default_conf, mocker): - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock()) - default_conf['telegram']['enabled'] = True +def test_send_msg_network_error(default_conf, mocker, caplog) -> None: + """ + Test send_msg() method + """ + patch_pymarketcap(mocker) + mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + conf = deepcopy(default_conf) bot = MagicMock() bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) - send_msg('test', bot) + freqtradebot = FreqtradeBot(conf, create_engine('sqlite://')) + telegram = Telegram(freqtradebot) + + telegram._config['telegram']['enabled'] = True + telegram.send_msg('test', bot) # Bot should've tried to send it twice assert len(bot.method_calls) == 2 - - -def test_init(default_conf, mocker): - start_polling = MagicMock() - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - # mock telegram.ext.Updater - Updater=MagicMock(return_value=start_polling)) - # not enabled - tg.init(default_conf) - assert start_polling.call_count == 0 - # number of handles registered - assert start_polling.dispatcher.add_handler.call_count == 11 - assert start_polling.start_polling.call_count == 1 - - # enabled - default_conf['telegram'] = {} - default_conf['telegram']['enabled'] = True - default_conf['telegram']['token'] = '' - tg.init(default_conf) - - -def test_cleanup(default_conf, mocker): - default_conf['telegram'] = {} - default_conf['telegram']['enabled'] = False - updater_mock = MagicMock() - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - _UPDATER=updater_mock) - # not enabled - tg.cleanup() - assert updater_mock.stop.call_count == 0 - - # enabled - default_conf['telegram']['enabled'] = True - tg.cleanup() - assert updater_mock.stop.call_count == 1 - - -def test_status(default_conf, update, mocker): - update.message.chat.id = 123 - default_conf['telegram'] = {} - default_conf['telegram']['chat_id'] = 123 - mocker.patch('telegram.update', MagicMock()) - mocker.patch.dict('freqtrade.main._CONF', default_conf) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock()) - msg_mock = MagicMock() - status_table = MagicMock() - mocker.patch.multiple('freqtrade.rpc', - send_msg=MagicMock()) - mocker.patch.multiple('freqtrade.rpc.telegram', - _CONF=default_conf, - init=MagicMock(), - rpc_trade_status=MagicMock(return_value=(False, [1, 2, 3])), - _status_table=status_table, - send_msg=msg_mock) - _status(bot=MagicMock(), update=update) - assert msg_mock.call_count == 3 - update.message.text = MagicMock() - update.message.text.replace = MagicMock(return_value='table 2 3') - _status(bot=MagicMock(), update=update) - assert status_table.call_count == 1 + assert tt.log_has( + 'Got TelegramError: Oh snap! Giving up on that message.', + caplog.record_tuples + )