diff --git a/config.json.example b/config.json.example index 21e0814ce..e8473e919 100644 --- a/config.json.example +++ b/config.json.example @@ -6,7 +6,10 @@ "ticker_interval" : "5m", "dry_run": false, "trailing_stop": false, - "unfilledtimeout": 600, + "unfilledtimeout": { + "buy": 10, + "sell": 30 + }, "bid_strategy": { "ask_last_balance": 0.0 }, diff --git a/docs/configuration.md b/docs/configuration.md index 984f2529b..dd16ef6b5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -27,7 +27,8 @@ The table below will list all configuration parameters. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. | `trailing_stoploss` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). | `trailing_stoploss_positve` | 0 | No | Changes stop-loss once profit has been reached. -| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled. +| `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. +| `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. | `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below. | `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). | `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d80eea6f4..ec7765455 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -63,7 +63,13 @@ CONF_SCHEMA = { 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, 'trailing_stop': {'type': 'boolean'}, 'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1}, - 'unfilledtimeout': {'type': 'integer', 'minimum': 0}, + 'unfilledtimeout': { + 'type': 'object', + 'properties': { + 'buy': {'type': 'number', 'minimum': 3}, + 'sell': {'type': 'number', 'minimum': 10} + } + }, 'bid_strategy': { 'type': 'object', 'properties': { diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 72b5190b9..83c6a969c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -19,8 +19,7 @@ from freqtrade.analyze import Analyze from freqtrade.exchange import Exchange from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.persistence import Trade -from freqtrade.rpc import RPCMessageType -from freqtrade.rpc import RPCManager +from freqtrade.rpc.rpc_manager import RPCManager from freqtrade.state import State logger = logging.getLogger(__name__) @@ -92,10 +91,7 @@ class FreqtradeBot(object): # Log state transition state = self.state if state != old_state: - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'{state.name.lower()}' - }) + self.rpc.send_msg(f'*Status:* `{state.name.lower()}`') logger.info('Changing state to: %s', state.name) if state == State.STOPPED: @@ -171,10 +167,9 @@ class FreqtradeBot(object): except OperationalException: tb = traceback.format_exc() hint = 'Issue `/start` if you think it is safe to restart.' - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'OperationalException:\n```\n{tb}```{hint}' - }) + self.rpc.send_msg( + f'*Status:* OperationalException:\n```\n{tb}```{hint}' + ) logger.exception('OperationalException. Stopping trader ...') self.state = State.STOPPED return state_changed @@ -343,6 +338,7 @@ 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)) @@ -365,17 +361,12 @@ class FreqtradeBot(object): 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 - }) + # 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})`""" + ) # 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( @@ -560,10 +551,7 @@ class FreqtradeBot(object): Trade.session.delete(trade) Trade.session.flush() logger.info('Buy order timeout for %s.', trade) - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'Unfilled buy order for {pair_s} cancelled due to timeout' - }) + self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled') return True # if trade is partially complete, edit the stake details for the trade @@ -572,10 +560,7 @@ class FreqtradeBot(object): 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({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'Remaining buy order for {pair_s} cancelled due to timeout' - }) + self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled') return False # FIX: 20180110, should cancel_order() be cond. or unconditionally called? @@ -593,10 +578,7 @@ class FreqtradeBot(object): trade.close_date = None trade.is_open = True trade.open_order_id = None - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'Unfilled sell order for {pair_s} cancelled due to timeout' - }) + self.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled') logger.info('Sell order timeout for %s.', trade) return True @@ -610,47 +592,47 @@ class FreqtradeBot(object): :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_percent = trade.calc_profit_percent(limit) + profit = trade.calc_profit_percent(limit) pair_url = self.exchange.get_pair_detail_url(trade.pair) - gain = "profit" if profit_percent > 0 else "loss" + gain = "profit" if fmt_exp_profit > 0 else "loss" - 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, - } + 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}%`" \ + "" # For regular case, when the configuration exists if 'stake_currency' in self.config and 'fiat_display_currency' in self.config: - stake_currency = self.config['stake_currency'] - fiat_currency = self.config['fiat_display_currency'] + stake = self.config['stake_currency'] + fiat = self.config['fiat_display_currency'] fiat_converter = CryptoToFiatConverter() profit_fiat = fiat_converter.convert_amount( profit_trade, - stake_currency, - fiat_currency, + stake, + fiat ) - msg.update({ - 'profit_fiat': profit_fiat, - 'stake_currency': stake_currency, - 'fiat_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})`' # Send the message - self.rpc.send_msg(msg) + self.rpc.send_msg(message) Trade.session.flush() diff --git a/freqtrade/main.py b/freqtrade/main.py index 977212faf..79080ce37 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -13,7 +13,6 @@ 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') @@ -60,10 +59,7 @@ def main(sysargv: List[str]) -> None: logger.exception('Fatal exception!') finally: if freqtrade: - freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': 'process died' - }) + freqtrade.rpc.send_msg('*Status:* `Process died ...`') freqtrade.cleanup() sys.exit(return_code) @@ -77,10 +73,8 @@ def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot: # Create new instance freqtrade = FreqtradeBot(Configuration(args).get_config()) - freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': 'config reloaded' - }) + freqtrade.rpc.send_msg( + '*Status:* `Config reloaded {freqtrade.state.name.lower()}...`') return freqtrade diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9411e983b..11658c6fb 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -3,10 +3,9 @@ This module contains class to define a RPC communications """ import logging from abc import abstractmethod -from datetime import timedelta, datetime, date +from datetime import date, datetime, timedelta from decimal import Decimal -from enum import Enum -from typing import Dict, Any, List +from typing import Any, Dict, List, Tuple import arrow import sqlalchemy as sql @@ -20,15 +19,6 @@ 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 @@ -36,12 +26,7 @@ class RPCException(Exception): raise RPCException('*Status:* `no active trade`') """ - def __init__(self, message: str) -> None: - super().__init__(self) - self.message = message - - def __str__(self): - return self.message + pass class RPC(object): @@ -56,20 +41,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 send_msg(self, msg: Dict[str, str]) -> None: + def name(self) -> str: + """ Returns the lowercase name of this module """ + + @abstractmethod + def send_msg(self, msg: str) -> None: """ Sends a message to all registered rpc modules """ - def _rpc_trade_status(self) -> List[Dict[str, Any]]: + def _rpc_trade_status(self) -> List[str]: """ Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is a remotely exposed function @@ -77,11 +62,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('trader is not running') + raise RPCException('*Status:* `trader is not running`') elif not trades: - raise RPCException('no active trade') + raise RPCException('*Status:* `no active trade`') else: - results = [] + result = [] for trade in trades: order = None if trade.open_order_id: @@ -91,29 +76,39 @@ 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) - 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 + 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 def _rpc_status_table(self) -> DataFrame: trades = Trade.query.filter(Trade.is_open.is_(True)).all() if self._freqtrade.state != State.RUNNING: - raise RPCException('trader is not running') + raise RPCException('*Status:* `trader is not running`') elif not trades: - raise RPCException('no active order') + raise RPCException('*Status:* `no active order`') else: trades_list = [] for trade in trades: @@ -139,7 +134,7 @@ class RPC(object): profit_days: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): - raise RPCException('timescale must be an integer greater than 0') + raise RPCException('*Daily [n]:* `must be an integer greater than 0`') fiat = self._freqtrade.fiat_converter for day in range(0, timescale): @@ -219,7 +214,7 @@ class RPC(object): .order_by(sql.text('profit_sum DESC')).first() if not best_pair: - raise RPCException('no closed trade') + raise RPCException('*Status:* `no closed trade`') bp_pair, bp_rate = best_pair @@ -227,26 +222,26 @@ class RPC(object): # doing this will utilize its caching functionallity, instead we reinitialize it here fiat = self._freqtrade.fiat_converter # Prepare data to display - profit_closed_coin_sum = round(sum(profit_closed_coin), 8) + profit_closed_coin = round(sum(profit_closed_coin), 8) profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2) profit_closed_fiat = fiat.convert_amount( - profit_closed_coin_sum, + profit_closed_coin, stake_currency, fiat_display_currency ) - profit_all_coin_sum = round(sum(profit_all_coin), 8) + profit_all_coin = round(sum(profit_all_coin), 8) profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2) profit_all_fiat = fiat.convert_amount( - profit_all_coin_sum, + profit_all_coin, stake_currency, fiat_display_currency ) num = float(len(durations) or 1) return { - 'profit_closed_coin': profit_closed_coin_sum, + 'profit_closed_coin': profit_closed_coin, 'profit_closed_percent': profit_closed_percent, 'profit_closed_fiat': profit_closed_fiat, - 'profit_all_coin': profit_all_coin_sum, + 'profit_all_coin': profit_all_coin, 'profit_all_percent': profit_all_percent, 'profit_all_fiat': profit_all_fiat, 'trade_count': len(trades), @@ -257,7 +252,7 @@ class RPC(object): 'best_rate': round(bp_rate * 100, 2), } - def _rpc_balance(self, fiat_display_currency: str) -> Dict: + def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]: """ Returns current account balance per crypto """ output = [] total = 0.0 @@ -274,47 +269,45 @@ 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 { - 'currencies': output, - 'total': total, - 'symbol': symbol, - 'value': value, - } + return output, total, symbol, value - def _rpc_start(self) -> Dict[str, str]: + def _rpc_start(self) -> str: """ Handler for start """ if self._freqtrade.state == State.RUNNING: - return {'status': 'already running'} + return '*Status:* `already running`' self._freqtrade.state = State.RUNNING - return {'status': 'starting trader ...'} + return '`Starting trader ...`' - def _rpc_stop(self) -> Dict[str, str]: + def _rpc_stop(self) -> str: """ Handler for stop """ if self._freqtrade.state == State.RUNNING: self._freqtrade.state = State.STOPPED - return {'status': 'stopping trader ...'} + return '`Stopping trader ...`' - return {'status': 'already stopped'} + return '*Status:* `already stopped`' - def _rpc_reload_conf(self) -> Dict[str, str]: + def _rpc_reload_conf(self) -> 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 . @@ -348,7 +341,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 @@ -365,7 +358,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() @@ -376,7 +369,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'), @@ -393,6 +386,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/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py index 1f9b034b9..5aea98d48 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 import RPCMessageType, RPCManager -from freqtrade.tests.conftest import log_has, get_patched_freqtradebot +from freqtrade.rpc.rpc_manager import RPCManager +from freqtrade.tests.conftest import get_patched_freqtradebot, log_has def test_rpc_manager_object() -> None: @@ -102,12 +102,9 @@ 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({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': 'test' - }) + rpc_manager.send_msg('test') - assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples) + assert log_has('Sending rpc message: test', caplog.record_tuples) assert telegram_mock.call_count == 0 @@ -120,10 +117,7 @@ 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({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': 'test' - }) + rpc_manager.send_msg('test') - assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples) + assert log_has('Sending rpc message: 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 01f248327..2710328bd 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -9,17 +9,14 @@ import re from copy import deepcopy from datetime import datetime from random import randint -from unittest.mock import MagicMock, ANY +from unittest.mock import MagicMock -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, @@ -200,7 +197,6 @@ 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(), @@ -214,19 +210,7 @@ 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=[{ - '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)' - }]), + _rpc_trade_status=MagicMock(return_value=[1, 2, 3]), _status_table=status_table, _send_msg=msg_mock ) @@ -240,7 +224,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 == 1 + assert msg_mock.call_count == 3 update.message.text = MagicMock() update.message.text.replace = MagicMock(return_value='table 2 3') @@ -614,7 +598,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: @@ -680,7 +664,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: @@ -724,7 +708,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, @@ -761,23 +745,12 @@ def test_forcesell_handle(default_conf, update, ticker, fee, telegram._forcesell(bot=MagicMock(), update=update) assert rpc_mock.call_count == 2 - 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 + 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] def test_forcesell_down_handle(default_conf, update, ticker, fee, @@ -818,24 +791,12 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, telegram._forcesell(bot=MagicMock(), update=update) assert rpc_mock.call_count == 2 - - 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 + 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] def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None: @@ -868,23 +829,10 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker telegram._forcesell(bot=MagicMock(), update=update) assert rpc_mock.call_count == 4 - 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 + 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: @@ -918,7 +866,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() @@ -926,7 +874,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, @@ -1078,123 +1026,7 @@ 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_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: +def test_send_msg(default_conf, mocker) -> None: """ Test send_msg() method """ @@ -1210,7 +1042,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 450504f57..baa4c48c9 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -20,7 +20,8 @@ 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 diff --git a/requirements.txt b/requirements.txt index c7ab27462..3fb91888c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -ccxt==1.16.16 -SQLAlchemy==1.2.10 +ccxt==1.15.42 +SQLAlchemy==1.2.9 python-telegram-bot==10.1.0 arrow==0.12.1 cachetools==2.1.0