diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 83c6a969c..72b5190b9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -19,7 +19,8 @@ from freqtrade.analyze import Analyze from freqtrade.exchange import Exchange from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.persistence import Trade -from freqtrade.rpc.rpc_manager import RPCManager +from freqtrade.rpc import RPCMessageType +from freqtrade.rpc import RPCManager from freqtrade.state import State logger = logging.getLogger(__name__) @@ -91,7 +92,10 @@ class FreqtradeBot(object): # Log state transition state = self.state if state != old_state: - self.rpc.send_msg(f'*Status:* `{state.name.lower()}`') + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'{state.name.lower()}' + }) logger.info('Changing state to: %s', state.name) if state == State.STOPPED: @@ -167,9 +171,10 @@ class FreqtradeBot(object): except OperationalException: tb = traceback.format_exc() hint = 'Issue `/start` if you think it is safe to restart.' - self.rpc.send_msg( - f'*Status:* OperationalException:\n```\n{tb}```{hint}' - ) + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'OperationalException:\n```\n{tb}```{hint}' + }) logger.exception('OperationalException. Stopping trader ...') self.state = State.STOPPED return state_changed @@ -338,7 +343,6 @@ class FreqtradeBot(object): pair_url = self.exchange.get_pair_detail_url(pair) stake_currency = self.config['stake_currency'] fiat_currency = self.config['fiat_display_currency'] - exc_name = self.exchange.name # Calculate amount buy_limit = self.get_target_bid(self.exchange.get_ticker(pair)) @@ -361,12 +365,17 @@ class FreqtradeBot(object): fiat_currency ) - # Create trade entity and return - self.rpc.send_msg( - f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \ -with limit `{buy_limit:.8f} ({stake_amount:.6f} \ -{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`""" - ) + self.rpc.send_msg({ + 'type': RPCMessageType.BUY_NOTIFICATION, + 'exchange': self.exchange.name.capitalize(), + 'pair': pair_s, + 'market_url': pair_url, + 'limit': buy_limit, + 'stake_amount': stake_amount, + 'stake_amount_fiat': stake_amount_fiat, + 'stake_currency': stake_currency, + 'fiat_currency': fiat_currency + }) # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') trade = Trade( @@ -551,7 +560,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ Trade.session.delete(trade) Trade.session.flush() logger.info('Buy order timeout for %s.', trade) - self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled') + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'Unfilled buy order for {pair_s} cancelled due to timeout' + }) return True # if trade is partially complete, edit the stake details for the trade @@ -560,7 +572,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ trade.stake_amount = trade.amount * trade.open_rate trade.open_order_id = None logger.info('Partial buy order timeout for %s.', trade) - self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled') + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'Remaining buy order for {pair_s} cancelled due to timeout' + }) return False # FIX: 20180110, should cancel_order() be cond. or unconditionally called? @@ -578,7 +593,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ trade.close_date = None trade.is_open = True trade.open_order_id = None - self.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled') + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'Unfilled sell order for {pair_s} cancelled due to timeout' + }) logger.info('Sell order timeout for %s.', trade) return True @@ -592,47 +610,47 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ :param limit: limit rate for the sell order :return: None """ - exc = trade.exchange - pair = trade.pair # Execute sell and update trade record order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id'] trade.open_order_id = order_id trade.close_rate_requested = limit - fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2) profit_trade = trade.calc_profit(rate=limit) current_rate = self.exchange.get_ticker(trade.pair)['bid'] - profit = trade.calc_profit_percent(limit) + profit_percent = trade.calc_profit_percent(limit) pair_url = self.exchange.get_pair_detail_url(trade.pair) - gain = "profit" if fmt_exp_profit > 0 else "loss" + gain = "profit" if profit_percent > 0 else "loss" - message = f"*{exc}:* Selling\n" \ - f"*Current Pair:* [{pair}]({pair_url})\n" \ - f"*Limit:* `{limit}`\n" \ - f"*Amount:* `{round(trade.amount, 8)}`\n" \ - f"*Open Rate:* `{trade.open_rate:.8f}`\n" \ - f"*Current Rate:* `{current_rate:.8f}`\n" \ - f"*Profit:* `{round(profit * 100, 2):.2f}%`" \ - "" + msg = { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': trade.exchange.capitalize(), + 'pair': trade.pair, + 'gain': gain, + 'market_url': pair_url, + 'limit': limit, + 'amount': trade.amount, + 'open_rate': trade.open_rate, + 'current_rate': current_rate, + 'profit_amount': profit_trade, + 'profit_percent': profit_percent, + } # For regular case, when the configuration exists if 'stake_currency' in self.config and 'fiat_display_currency' in self.config: - stake = self.config['stake_currency'] - fiat = self.config['fiat_display_currency'] + stake_currency = self.config['stake_currency'] + fiat_currency = self.config['fiat_display_currency'] fiat_converter = CryptoToFiatConverter() profit_fiat = fiat_converter.convert_amount( profit_trade, - stake, - fiat + stake_currency, + fiat_currency, ) - message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \ - f'` / {profit_fiat:.3f} {fiat})`'\ - '' - # Because telegram._forcesell does not have the configuration - # Ignore the FIAT value and does not show the stake_currency as well - else: - gain = "profit" if fmt_exp_profit > 0 else "loss" - message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f})`' + msg.update({ + 'profit_fiat': profit_fiat, + 'stake_currency': stake_currency, + 'fiat_currency': fiat_currency, + }) + # Send the message - self.rpc.send_msg(message) + self.rpc.send_msg(msg) Trade.session.flush() diff --git a/freqtrade/main.py b/freqtrade/main.py index 79080ce37..977212faf 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -13,6 +13,7 @@ from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.freqtradebot import FreqtradeBot from freqtrade.state import State +from freqtrade.rpc import RPCMessageType logger = logging.getLogger('freqtrade') @@ -59,7 +60,10 @@ def main(sysargv: List[str]) -> None: logger.exception('Fatal exception!') finally: if freqtrade: - freqtrade.rpc.send_msg('*Status:* `Process died ...`') + freqtrade.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'process died' + }) freqtrade.cleanup() sys.exit(return_code) @@ -73,8 +77,10 @@ def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot: # Create new instance freqtrade = FreqtradeBot(Configuration(args).get_config()) - freqtrade.rpc.send_msg( - '*Status:* `Config reloaded {freqtrade.state.name.lower()}...`') + freqtrade.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'config reloaded' + }) return freqtrade diff --git a/freqtrade/rpc/__init__.py b/freqtrade/rpc/__init__.py index e69de29bb..31c854f82 100644 --- a/freqtrade/rpc/__init__.py +++ b/freqtrade/rpc/__init__.py @@ -0,0 +1,2 @@ +from .rpc import RPC, RPCMessageType, RPCException # noqa +from .rpc_manager import RPCManager # noqa diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 11658c6fb..621c19fd2 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -3,9 +3,10 @@ This module contains class to define a RPC communications """ import logging from abc import abstractmethod -from datetime import date, datetime, timedelta +from datetime import timedelta, datetime, date from decimal import Decimal -from typing import Any, Dict, List, Tuple +from enum import Enum +from typing import Dict, Any, List import arrow import sqlalchemy as sql @@ -19,6 +20,15 @@ from freqtrade.state import State logger = logging.getLogger(__name__) +class RPCMessageType(Enum): + STATUS_NOTIFICATION = 'status' + BUY_NOTIFICATION = 'buy' + SELL_NOTIFICATION = 'sell' + + def __repr__(self): + return self.value + + class RPCException(Exception): """ Should be raised with a rpc-formatted message in an _rpc_* method @@ -26,7 +36,12 @@ class RPCException(Exception): raise RPCException('*Status:* `no active trade`') """ - pass + def __init__(self, message: str) -> None: + super().__init__(self) + self.message = message + + def __str__(self): + return self.message class RPC(object): @@ -41,20 +56,20 @@ class RPC(object): """ self._freqtrade = freqtrade + @property + def name(self) -> str: + """ Returns the lowercase name of the implementation """ + return self.__class__.__name__.lower() + @abstractmethod def cleanup(self) -> None: """ Cleanup pending module resources """ - @property @abstractmethod - def name(self) -> str: - """ Returns the lowercase name of this module """ - - @abstractmethod - def send_msg(self, msg: str) -> None: + def send_msg(self, msg: Dict[str, str]) -> None: """ Sends a message to all registered rpc modules """ - def _rpc_trade_status(self) -> List[str]: + def _rpc_trade_status(self) -> List[Dict[str, Any]]: """ Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is a remotely exposed function @@ -62,11 +77,11 @@ class RPC(object): # Fetch open trade trades = Trade.query.filter(Trade.is_open.is_(True)).all() if self._freqtrade.state != State.RUNNING: - raise RPCException('*Status:* `trader is not running`') + raise RPCException('trader is not running') elif not trades: - raise RPCException('*Status:* `no active trade`') + raise RPCException('no active trade') else: - result = [] + results = [] for trade in trades: order = None if trade.open_order_id: @@ -76,39 +91,29 @@ class RPC(object): current_profit = trade.calc_profit_percent(current_rate) fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%' if trade.close_profit else None) - market_url = self._freqtrade.exchange.get_pair_detail_url(trade.pair) - trade_date = arrow.get(trade.open_date).humanize() - open_rate = trade.open_rate - close_rate = trade.close_rate - amount = round(trade.amount, 8) - current_profit = round(current_profit * 100, 2) - open_order = '' - if order: - order_type = order['type'] - order_side = order['side'] - order_rem = order['remaining'] - open_order = f'({order_type} {order_side} rem={order_rem:.8f})' - - message = f"*Trade ID:* `{trade.id}`\n" \ - f"*Current Pair:* [{trade.pair}]({market_url})\n" \ - f"*Open Since:* `{trade_date}`\n" \ - f"*Amount:* `{amount}`\n" \ - f"*Open Rate:* `{open_rate:.8f}`\n" \ - f"*Close Rate:* `{close_rate}`\n" \ - f"*Current Rate:* `{current_rate:.8f}`\n" \ - f"*Close Profit:* `{fmt_close_profit}`\n" \ - f"*Current Profit:* `{current_profit:.2f}%`\n" \ - f"*Open Order:* `{open_order}`"\ - - result.append(message) - return result + results.append(dict( + trade_id=trade.id, + pair=trade.pair, + market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair), + date=arrow.get(trade.open_date), + 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['side'], order['remaining'] + ) if order else None, + )) + return results def _rpc_status_table(self) -> DataFrame: trades = Trade.query.filter(Trade.is_open.is_(True)).all() if self._freqtrade.state != State.RUNNING: - raise RPCException('*Status:* `trader is not running`') + raise RPCException('trader is not running') elif not trades: - raise RPCException('*Status:* `no active order`') + raise RPCException('no active order') else: trades_list = [] for trade in trades: @@ -134,7 +139,7 @@ class RPC(object): profit_days: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): - raise RPCException('*Daily [n]:* `must be an integer greater than 0`') + raise RPCException('timescale must be an integer greater than 0') fiat = self._freqtrade.fiat_converter for day in range(0, timescale): @@ -214,7 +219,7 @@ class RPC(object): .order_by(sql.text('profit_sum DESC')).first() if not best_pair: - raise RPCException('*Status:* `no closed trade`') + raise RPCException('no closed trade') bp_pair, bp_rate = best_pair @@ -252,7 +257,7 @@ class RPC(object): 'best_rate': round(bp_rate * 100, 2), } - def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]: + def _rpc_balance(self, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ output = [] total = 0.0 @@ -269,45 +274,47 @@ class RPC(object): rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid'] est_btc: float = rate * balance['total'] total = total + est_btc - output.append( - { - 'currency': coin, - 'available': balance['free'], - 'balance': balance['total'], - 'pending': balance['used'], - 'est_btc': est_btc - } - ) + output.append({ + 'currency': coin, + 'available': balance['free'], + 'balance': balance['total'], + 'pending': balance['used'], + 'est_btc': est_btc, + }) if total == 0.0: - raise RPCException('`All balances are zero.`') + raise RPCException('all balances are zero') fiat = self._freqtrade.fiat_converter symbol = fiat_display_currency value = fiat.convert_amount(total, 'BTC', symbol) - return output, total, symbol, value + return { + 'currencies': output, + 'total': total, + 'symbol': symbol, + 'value': value, + } - def _rpc_start(self) -> str: + def _rpc_start(self) -> Dict[str, str]: """ Handler for start """ if self._freqtrade.state == State.RUNNING: - return '*Status:* `already running`' + return {'status': 'already running'} self._freqtrade.state = State.RUNNING - return '`Starting trader ...`' + return {'status': 'starting trader ...'} - def _rpc_stop(self) -> str: + def _rpc_stop(self) -> Dict[str, str]: """ Handler for stop """ if self._freqtrade.state == State.RUNNING: self._freqtrade.state = State.STOPPED - return '`Stopping trader ...`' + return {'status': 'stopping trader ...'} - return '*Status:* `already stopped`' + return {'status': 'already stopped'} - def _rpc_reload_conf(self) -> str: + def _rpc_reload_conf(self) -> Dict[str, str]: """ Handler for reload_conf. """ self._freqtrade.state = State.RELOAD_CONF - return '*Status:* `Reloading config ...`' + return {'status': 'reloading config ...'} - # FIX: no test for this!!!! def _rpc_forcesell(self, trade_id) -> None: """ Handler for forcesell . @@ -341,7 +348,7 @@ class RPC(object): # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: - raise RPCException('`trader is not running`') + raise RPCException('trader is not running') if trade_id == 'all': # Execute sell for all open orders @@ -358,7 +365,7 @@ class RPC(object): ).first() if not trade: logger.warning('forcesell: Invalid argument received') - raise RPCException('Invalid argument.') + raise RPCException('invalid argument') _exec_forcesell(trade) Trade.session.flush() @@ -369,7 +376,7 @@ class RPC(object): Shows a performance statistic from finished trades """ if self._freqtrade.state != State.RUNNING: - raise RPCException('`trader is not running`') + raise RPCException('trader is not running') pair_rates = Trade.session.query(Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum'), @@ -386,6 +393,6 @@ class RPC(object): def _rpc_count(self) -> List[Trade]: """ Returns the number of trades running """ if self._freqtrade.state != State.RUNNING: - raise RPCException('`trader is not running`') + raise RPCException('trader is not running') return Trade.query.filter(Trade.is_open.is_(True)).all() diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 252bbcdd8..34094ee20 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -2,9 +2,9 @@ This module contains class to manage RPC communications (Telegram, Slack, ...) """ import logging -from typing import List +from typing import List, Dict, Any -from freqtrade.rpc.rpc import RPC +from freqtrade.rpc import RPC logger = logging.getLogger(__name__) @@ -32,11 +32,14 @@ class RPCManager(object): mod.cleanup() del mod - def send_msg(self, msg: str) -> None: + def send_msg(self, msg: Dict[str, Any]) -> None: """ - Send given markdown message to all registered rpc modules - :param msg: message - :return: None + Send given message to all registered rpc modules. + A message consists of one or more key value pairs of strings. + e.g.: + { + 'status': 'stopping bot' + } """ logger.info('Sending rpc message: %s', msg) for mod in self.registered_modules: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 13a2b1913..02b74358e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -4,7 +4,7 @@ This module manage Telegram communication """ import logging -from typing import Any, Callable +from typing import Any, Callable, Dict from tabulate import tabulate from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update @@ -12,7 +12,7 @@ from telegram.error import NetworkError, TelegramError from telegram.ext import CommandHandler, Updater from freqtrade.__init__ import __version__ -from freqtrade.rpc.rpc import RPC, RPCException +from freqtrade.rpc import RPC, RPCException, RPCMessageType logger = logging.getLogger(__name__) @@ -55,10 +55,6 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call class Telegram(RPC): """ This class handles all telegram communication """ - @property - def name(self) -> str: - return "telegram" - def __init__(self, freqtrade) -> None: """ Init the Telegram call, and init the super class RPC @@ -114,9 +110,41 @@ class Telegram(RPC): """ self._updater.stop() - def send_msg(self, msg: str) -> None: + def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ - self._send_msg(msg) + + if msg['type'] == RPCMessageType.BUY_NOTIFICATION: + message = "*{exchange}:* Buying [{pair}]({market_url})\n" \ + "with limit `{limit:.8f}\n" \ + "({stake_amount:.6f} {stake_currency}," \ + "{stake_amount_fiat:.3f} {fiat_currency})`" \ + .format(**msg) + + elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: + msg['amount'] = round(msg['amount'], 8) + msg['profit_percent'] = round(msg['profit_percent'] * 100, 2) + + message = "*{exchange}:* Selling [{pair}]({market_url})\n" \ + "*Limit:* `{limit:.8f}`\n" \ + "*Amount:* `{amount:.8f}`\n" \ + "*Open Rate:* `{open_rate:.8f}`\n" \ + "*Current Rate:* `{current_rate:.8f}`\n" \ + "*Profit:* `{profit_percent:.2f}%`".format(**msg) + + # Check if all sell properties are available. + # This might not be the case if the message origin is triggered by /forcesell + if all(prop in msg for prop in ['gain', 'profit_fiat', + 'fiat_currency', 'stake_currency']): + message += '` ({gain}: {profit_amount:.8f} {stake_currency}`' \ + '` / {profit_fiat:.3f} {fiat_currency})`'.format(**msg) + + elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: + message = '*Status:* `{status}`'.format(**msg) + + else: + raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) + + self._send_msg(message) @authorized_only def _status(self, bot: Bot, update: Update) -> None: @@ -136,8 +164,26 @@ class Telegram(RPC): return try: - for trade_msg in self._rpc_trade_status(): - self._send_msg(trade_msg, bot=bot) + results = self._rpc_trade_status() + # pre format data + for result in results: + result['date'] = result['date'].humanize() + + messages = [ + "*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(**result) + for result in results + ] + for msg in messages: + self._send_msg(msg, bot=bot) except RPCException as e: self._send_msg(str(e), bot=bot) @@ -239,10 +285,9 @@ class Telegram(RPC): def _balance(self, bot: Bot, update: Update) -> None: """ Handler for /balance """ try: - currencys, total, symbol, value = \ - self._rpc_balance(self._config['fiat_display_currency']) + result = self._rpc_balance(self._config['fiat_display_currency']) output = '' - for currency in currencys: + for currency in result['currencies']: output += "*{currency}:*\n" \ "\t`Available: {available: .8f}`\n" \ "\t`Balance: {balance: .8f}`\n" \ @@ -250,8 +295,8 @@ class Telegram(RPC): "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) output += "\n*Estimated Value*:\n" \ - "\t`BTC: {0: .8f}`\n" \ - "\t`{1}: {2: .2f}`\n".format(total, symbol, value) + "\t`BTC: {total: .8f}`\n" \ + "\t`{symbol}: {value: .2f}`\n".format(**result) self._send_msg(output, bot=bot) except RPCException as e: self._send_msg(str(e), bot=bot) @@ -266,7 +311,7 @@ class Telegram(RPC): :return: None """ msg = self._rpc_start() - self._send_msg(msg, bot=bot) + self._send_msg('Status: `{status}`'.format(**msg), bot=bot) @authorized_only def _stop(self, bot: Bot, update: Update) -> None: @@ -278,7 +323,7 @@ class Telegram(RPC): :return: None """ msg = self._rpc_stop() - self._send_msg(msg, bot=bot) + self._send_msg('Status: `{status}`'.format(**msg), bot=bot) @authorized_only def _reload_conf(self, bot: Bot, update: Update) -> None: @@ -290,7 +335,7 @@ class Telegram(RPC): :return: None """ msg = self._rpc_reload_conf() - self._send_msg(msg, bot=bot) + self._send_msg('Status: `{status}`'.format(**msg), bot=bot) @authorized_only def _forcesell(self, bot: Bot, update: Update) -> None: diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 58514d1c0..6e59b4116 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -5,13 +5,13 @@ Unit test file for rpc/rpc.py """ from datetime import datetime -from unittest.mock import MagicMock +from unittest.mock import MagicMock, ANY import pytest from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade -from freqtrade.rpc.rpc import RPC, RPCException +from freqtrade.rpc import RPC, RPCException from freqtrade.state import State from freqtrade.tests.test_freqtradebot import (patch_coinmarketcap, patch_get_signal) @@ -53,24 +53,21 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: rpc._rpc_trade_status() freqtradebot.create_trade() - trades = rpc._rpc_trade_status() - trade = trades[0] + results = rpc._rpc_trade_status() - result_message = [ - '*Trade ID:* `1`\n' - '*Current Pair:* ' - '[ETH/BTC](https://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 trades == result_message - assert trade.find('[ETH/BTC]') >= 0 + assert { + 'trade_id': 1, + 'pair': 'ETH/BTC', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'date': ANY, + 'open_rate': 1.099e-05, + 'close_rate': None, + 'current_rate': 1.098e-05, + 'amount': 90.99181074, + 'close_profit': None, + 'current_profit': -0.59, + 'open_order': '(limit buy rem=0.00000000)' + } == results[0] def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: @@ -92,11 +89,11 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: rpc = RPC(freqtradebot) freqtradebot.state = State.STOPPED - with pytest.raises(RPCException, match=r'.*\*Status:\* `trader is not running``*'): + with pytest.raises(RPCException, match=r'.*trader is not running*'): rpc._rpc_status_table() freqtradebot.state = State.RUNNING - with pytest.raises(RPCException, match=r'.*\*Status:\* `no active order`*'): + with pytest.raises(RPCException, match=r'.*no active order*'): rpc._rpc_status_table() freqtradebot.create_trade() @@ -328,16 +325,17 @@ def test_rpc_balance_handle(default_conf, mocker): freqtradebot = FreqtradeBot(default_conf) rpc = RPC(freqtradebot) - output, total, symbol, value = rpc._rpc_balance(default_conf['fiat_display_currency']) - assert prec_satoshi(total, 12) - assert prec_satoshi(value, 180000) - assert 'USD' in symbol - assert len(output) == 1 - assert 'BTC' in output[0]['currency'] - assert prec_satoshi(output[0]['available'], 10) - assert prec_satoshi(output[0]['balance'], 12) - assert prec_satoshi(output[0]['pending'], 2) - assert prec_satoshi(output[0]['est_btc'], 12) + result = rpc._rpc_balance(default_conf['fiat_display_currency']) + assert prec_satoshi(result['total'], 12) + assert prec_satoshi(result['value'], 180000) + assert 'USD' == result['symbol'] + assert result['currencies'] == [{ + 'currency': 'BTC', + 'available': 10.0, + 'balance': 12.0, + 'pending': 2.0, + 'est_btc': 12.0, + }] def test_rpc_start(mocker, default_conf) -> None: @@ -358,11 +356,11 @@ def test_rpc_start(mocker, default_conf) -> None: freqtradebot.state = State.STOPPED result = rpc._rpc_start() - assert '`Starting trader ...`' in result + assert {'status': 'starting trader ...'} == result assert freqtradebot.state == State.RUNNING result = rpc._rpc_start() - assert '*Status:* `already running`' in result + assert {'status': 'already running'} == result assert freqtradebot.state == State.RUNNING @@ -384,11 +382,12 @@ def test_rpc_stop(mocker, default_conf) -> None: freqtradebot.state = State.RUNNING result = rpc._rpc_stop() - assert '`Stopping trader ...`' in result + assert {'status': 'stopping trader ...'} == result assert freqtradebot.state == State.STOPPED result = rpc._rpc_stop() - assert '*Status:* `already stopped`' in result + + assert {'status': 'already stopped'} == result assert freqtradebot.state == State.STOPPED @@ -421,11 +420,11 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: rpc = RPC(freqtradebot) freqtradebot.state = State.STOPPED - with pytest.raises(RPCException, match=r'.*`trader is not running`*'): + with pytest.raises(RPCException, match=r'.*trader is not running*'): rpc._rpc_forcesell(None) freqtradebot.state = State.RUNNING - with pytest.raises(RPCException, match=r'.*Invalid argument.*'): + with pytest.raises(RPCException, match=r'.*invalid argument*'): rpc._rpc_forcesell(None) rpc._rpc_forcesell('all') @@ -436,10 +435,10 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: rpc._rpc_forcesell('1') freqtradebot.state = State.STOPPED - with pytest.raises(RPCException, match=r'.*`trader is not running`*'): + with pytest.raises(RPCException, match=r'.*trader is not running*'): rpc._rpc_forcesell(None) - with pytest.raises(RPCException, match=r'.*`trader is not running`*'): + with pytest.raises(RPCException, match=r'.*trader is not running*'): rpc._rpc_forcesell('all') freqtradebot.state = State.RUNNING diff --git a/freqtrade/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py index 5aea98d48..1f9b034b9 100644 --- a/freqtrade/tests/rpc/test_rpc_manager.py +++ b/freqtrade/tests/rpc/test_rpc_manager.py @@ -6,8 +6,8 @@ import logging from copy import deepcopy from unittest.mock import MagicMock -from freqtrade.rpc.rpc_manager import RPCManager -from freqtrade.tests.conftest import get_patched_freqtradebot, log_has +from freqtrade.rpc import RPCMessageType, RPCManager +from freqtrade.tests.conftest import log_has, get_patched_freqtradebot def test_rpc_manager_object() -> None: @@ -102,9 +102,12 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, conf) rpc_manager = RPCManager(freqtradebot) - rpc_manager.send_msg('test') + rpc_manager.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'test' + }) - assert log_has('Sending rpc message: test', caplog.record_tuples) + assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples) assert telegram_mock.call_count == 0 @@ -117,7 +120,10 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) - rpc_manager.send_msg('test') + rpc_manager.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'test' + }) - assert log_has('Sending rpc message: test', caplog.record_tuples) + assert log_has("Sending rpc message: {'type': status, 'status': '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 2710328bd..01f248327 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -9,14 +9,17 @@ import re from copy import deepcopy from datetime import datetime from random import randint -from unittest.mock import MagicMock +from unittest.mock import MagicMock, ANY +import arrow +import pytest from telegram import Chat, Message, Update from telegram.error import NetworkError from freqtrade import __version__ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade +from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, @@ -197,6 +200,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) + mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -210,7 +214,19 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), - _rpc_trade_status=MagicMock(return_value=[1, 2, 3]), + _rpc_trade_status=MagicMock(return_value=[{ + 'trade_id': 1, + 'pair': 'ETH/BTC', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'date': arrow.utcnow(), + 'open_rate': 1.099e-05, + 'close_rate': None, + 'current_rate': 1.098e-05, + 'amount': 90.99181074, + 'close_profit': None, + 'current_profit': -0.59, + 'open_order': '(limit buy rem=0.00000000)' + }]), _status_table=status_table, _send_msg=msg_mock ) @@ -224,7 +240,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: freqtradebot.create_trade() telegram._status(bot=MagicMock(), update=update) - assert msg_mock.call_count == 3 + assert msg_mock.call_count == 1 update.message.text = MagicMock() update.message.text.replace = MagicMock(return_value='table 2 3') @@ -598,7 +614,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None: 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 + assert 'all balances are zero' in result def test_start_handle(default_conf, update, mocker) -> None: @@ -664,7 +680,7 @@ def test_stop_handle(default_conf, update, mocker) -> None: telegram._stop(bot=MagicMock(), update=update) assert freqtradebot.state == State.STOPPED assert msg_mock.call_count == 1 - assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] + assert 'stopping trader' in msg_mock.call_args_list[0][0][0] def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: @@ -708,7 +724,7 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None: telegram._reload_conf(bot=MagicMock(), update=update) assert freqtradebot.state == State.RELOAD_CONF assert msg_mock.call_count == 1 - assert 'Reloading config' in msg_mock.call_args_list[0][0][0] + assert 'reloading config' in msg_mock.call_args_list[0][0][0] def test_forcesell_handle(default_conf, update, ticker, fee, @@ -745,12 +761,23 @@ def test_forcesell_handle(default_conf, update, ticker, fee, telegram._forcesell(bot=MagicMock(), update=update) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' 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] + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'profit', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.172e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.172e-05, + 'profit_amount': 6.126e-05, + 'profit_percent': 0.06110514, + 'profit_fiat': 0.9189, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + } == last_msg def test_forcesell_down_handle(default_conf, update, ticker, fee, @@ -791,12 +818,24 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, telegram._forcesell(bot=MagicMock(), update=update) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' 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] + + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'loss', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.044e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.044e-05, + 'profit_amount': -5.492e-05, + 'profit_percent': -0.05478343, + 'profit_fiat': -0.8238000000000001, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + } == last_msg def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None: @@ -829,10 +868,23 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker 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] + msg = rpc_mock.call_args_list[0][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'loss', + 'market_url': ANY, + 'limit': 1.098e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.098e-05, + 'profit_amount': -5.91e-06, + 'profit_percent': -0.00589292, + 'profit_fiat': -0.08865, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + } == msg def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: @@ -866,7 +918,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: 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] + assert 'invalid argument' in msg_mock.call_args_list[0][0][0] # Invalid argument msg_mock.reset_mock() @@ -874,7 +926,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: 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] + assert 'invalid argument' in msg_mock.call_args_list[0][0][0] def test_performance_handle(default_conf, update, ticker, fee, @@ -1026,7 +1078,123 @@ def test_version_handle(default_conf, update, mocker) -> None: assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0] -def test_send_msg(default_conf, mocker) -> None: +def test_send_msg_buy_notification(default_conf, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + telegram.send_msg({ + 'type': RPCMessageType.BUY_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.099e-05, + 'stake_amount': 0.001, + 'stake_amount_fiat': 0.0, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD' + }) + assert msg_mock.call_args[0][0] \ + == '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \ + 'with limit `0.00001099\n' \ + '(0.001000 BTC,0.000 USD)`' + + +def test_send_msg_sell_notification(default_conf, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + telegram.send_msg({ + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Binance', + 'pair': 'KEY/ETH', + 'gain': 'loss', + 'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH', + 'limit': 3.201e-05, + 'amount': 1333.3333333333335, + 'open_rate': 7.5e-05, + 'current_rate': 3.201e-05, + 'profit_amount': -0.05746268, + 'profit_percent': -0.57405275, + 'profit_fiat': -24.81204044792, + 'stake_currency': 'ETH', + 'fiat_currency': 'USD' + }) + assert msg_mock.call_args[0][0] \ + == '*Binance:* Selling [KEY/ETH]' \ + '(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \ + '*Limit:* `0.00003201`\n' \ + '*Amount:* `1333.33333333`\n' \ + '*Open Rate:* `0.00007500`\n' \ + '*Current Rate:* `0.00003201`\n' \ + '*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`' + + msg_mock.reset_mock() + telegram.send_msg({ + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Binance', + 'pair': 'KEY/ETH', + 'gain': 'loss', + 'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH', + 'limit': 3.201e-05, + 'amount': 1333.3333333333335, + 'open_rate': 7.5e-05, + 'current_rate': 3.201e-05, + 'profit_amount': -0.05746268, + 'profit_percent': -0.57405275, + 'stake_currency': 'ETH', + }) + assert msg_mock.call_args[0][0] \ + == '*Binance:* Selling [KEY/ETH]' \ + '(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \ + '*Limit:* `0.00003201`\n' \ + '*Amount:* `1333.33333333`\n' \ + '*Open Rate:* `0.00007500`\n' \ + '*Current Rate:* `0.00003201`\n' \ + '*Profit:* `-57.41%`' + + +def test_send_msg_status_notification(default_conf, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + telegram.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'running' + }) + assert msg_mock.call_args[0][0] == '*Status:* `running`' + + +def test_send_msg_unknown_type(default_conf, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + with pytest.raises(NotImplementedError, match=r'Unknown message type: None'): + telegram.send_msg({ + 'type': None, + }) + + +def test__send_msg(default_conf, mocker) -> None: """ Test send_msg() method """ @@ -1042,7 +1210,7 @@ def test_send_msg(default_conf, mocker) -> None: assert len(bot.method_calls) == 1 -def test_send_msg_network_error(default_conf, mocker, caplog) -> None: +def test__send_msg_network_error(default_conf, mocker, caplog) -> None: """ Test send_msg() method """ diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 17bd6aa7c..450504f57 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -18,9 +18,9 @@ from freqtrade import (DependencyException, OperationalException, TemporaryError, constants) from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade +from freqtrade.rpc import RPCMessageType from freqtrade.state import State -from freqtrade.tests.conftest import (log_has, patch_coinmarketcap, - patch_exchange) +from freqtrade.tests.conftest import log_has, patch_coinmarketcap, patch_exchange # Functions for recurrent object patching @@ -755,7 +755,7 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) -> result = freqtrade._process() assert result is False assert freqtrade.state == State.STOPPED - assert 'OperationalException' in msg_mock.call_args_list[-1][0][0] + assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status'] def test_process_trade_handling( @@ -1375,13 +1375,23 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid']) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] - assert 'Amount' in rpc_mock.call_args_list[-1][0][0] - assert 'Profit' 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] + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'profit', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.172e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.172e-05, + 'profit_amount': 6.126e-05, + 'profit_percent': 0.06110514, + 'profit_fiat': 0.9189, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + } == last_msg def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: @@ -1417,12 +1427,23 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid']) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' 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] + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'loss', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.044e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.044e-05, + 'profit_amount': -5.492e-05, + 'profit_percent': -0.05478343, + 'profit_fiat': -0.8238000000000001, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + } == last_msg def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, @@ -1459,12 +1480,20 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid']) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' 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 'USD' not in rpc_mock.call_args_list[-1][0][0] + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'profit', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.172e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.172e-05, + 'profit_amount': 6.126e-05, + 'profit_percent': 0.06110514, + } == last_msg def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, @@ -1501,10 +1530,20 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid']) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' 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] + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'loss', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.044e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.044e-05, + 'profit_amount': -5.492e-05, + 'profit_percent': -0.05478343, + } == last_msg def test_sell_profit_only_enable_profit(default_conf, limit_buy_order,