Merge pull request #972 from freqtrade/feature/rewrite-rpc

Rewrite RPC module
This commit is contained in:
Matthias 2018-07-12 19:38:01 +02:00 committed by GitHub
commit c17e8d6abb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 526 additions and 233 deletions

View File

@ -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()

View File

@ -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

View File

@ -0,0 +1,2 @@
from .rpc import RPC, RPCMessageType, RPCException # noqa
from .rpc_manager import RPCManager # noqa

View File

@ -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 <id>.
@ -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()

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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
"""

View File

@ -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,