use more granular msg dict for buy/sell notifications

This commit is contained in:
gcarq 2018-07-03 20:26:48 +02:00
parent 4cb1aa1d97
commit 0920fb6120
10 changed files with 244 additions and 101 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__)
@ -92,6 +93,7 @@ class FreqtradeBot(object):
state = self.state
if state != old_state:
self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'{state.name.lower()}'
})
logger.info('Changing state to: %s', state.name)
@ -170,6 +172,7 @@ class FreqtradeBot(object):
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}'
})
logger.exception('OperationalException. Stopping trader ...')
@ -340,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))
@ -363,12 +365,16 @@ class FreqtradeBot(object):
fiat_currency
)
# Create trade entity and return
self.rpc.send_msg({
'status':
f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \
with limit `{buy_limit:.8f} ({stake_amount:.6f} \
{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`"""
'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')
@ -555,6 +561,7 @@ class FreqtradeBot(object):
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'
})
return True
@ -566,6 +573,7 @@ class FreqtradeBot(object):
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'
})
return False
@ -586,6 +594,7 @@ class FreqtradeBot(object):
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'
})
logger.info('Sell order timeout for %s.', trade)
@ -601,47 +610,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 = 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({'status': 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')
@ -60,6 +61,7 @@ def main(sysargv: List[str]) -> None:
finally:
if freqtrade:
freqtrade.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'process died'
})
freqtrade.cleanup()
@ -76,6 +78,7 @@ 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'
})
return freqtrade

View File

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

View File

@ -5,6 +5,7 @@ import logging
from abc import abstractmethod
from datetime import timedelta, datetime, date
from decimal import Decimal
from enum import Enum
from typing import Dict, Any, List
import arrow
@ -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
@ -33,11 +43,6 @@ class RPCException(Exception):
def __str__(self):
return self.message
def __json__(self):
return {
'msg': self.message
}
class RPC(object):
"""

View File

@ -2,9 +2,9 @@
This module contains class to manage RPC communications (Telegram, Slack, ...)
"""
import logging
from typing import List, Dict
from typing import List, Dict, Any
from freqtrade.rpc.rpc import RPC
from freqtrade.rpc import RPC
logger = logging.getLogger(__name__)
@ -32,7 +32,7 @@ class RPCManager(object):
mod.cleanup()
del mod
def send_msg(self, msg: Dict[str, str]) -> None:
def send_msg(self, msg: Dict[str, Any]) -> None:
"""
Send given message to all registered rpc modules.
A message consists of one or more key value pairs of strings.

View File

@ -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__)
@ -110,9 +110,42 @@ class Telegram(RPC):
"""
self._updater.stop()
def send_msg(self, msg: Dict[str, str]) -> None:
def send_msg(self, msg: Dict[str, Any]) -> None:
""" Send a message to telegram channel """
self._send_msg('*Status:* `{status}`'.format(**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)
self._send_msg(message)
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:

View File

@ -11,7 +11,7 @@ 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)

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({'status': 'test'})
rpc_manager.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'test'
})
assert log_has("Sending rpc message: {'status': '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({'status': 'test'})
rpc_manager.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'test'
})
assert log_has("Sending rpc message: {'status': '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,7 +9,7 @@ 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
from telegram import Chat, Message, Update
from telegram.error import NetworkError
@ -17,6 +17,7 @@ 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,
@ -757,13 +758,23 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
telegram._forcesell(bot=MagicMock(), update=update)
assert rpc_mock.call_count == 2
last_call = rpc_mock.call_args_list[-1][0][0]['status']
assert 'Selling' in last_call
assert '[ETH/BTC]' in last_call
assert 'Amount' in last_call
assert '0.00001172' in last_call
assert 'profit: 6.11%, 0.00006126' in last_call
assert '0.919 USD' in last_call
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,
@ -803,14 +814,25 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
update.message.text = '/forcesell 1'
telegram._forcesell(bot=MagicMock(), update=update)
last_call = rpc_mock.call_args_list[-1][0][0]['status']
assert rpc_mock.call_count == 2
assert 'Selling' in last_call
assert '[ETH/BTC]' in last_call
assert 'Amount' in last_call
assert '0.00001044' in last_call
assert 'loss: -5.48%, -0.00005492' in last_call
assert '-0.824 USD' in last_call
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:
@ -843,10 +865,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]['status']
assert 'loss: -0.59%, -0.00000591 BTC' in args[0][0]['status']
assert '-0.089 USD' in args[0][0]['status']
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:
@ -1040,7 +1075,22 @@ 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() -> None:
# TODO: implement me
pass
def test_send_msg_sell_notification() -> None:
# TODO: implement me
pass
def test_send_msg_status_notification() -> None:
# TODO: implement me
pass
def test__send_msg(default_conf, mocker) -> None:
"""
Test send_msg() method
"""
@ -1056,7 +1106,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
@ -1375,14 +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
last_call = rpc_mock.call_args_list[-1][0][0]['status']
assert 'Selling' in last_call
assert '[ETH/BTC]' in last_call
assert 'Amount' in last_call
assert 'Profit' in last_call
assert '0.00001172' in last_call
assert 'profit: 6.11%, 0.00006126' in last_call
assert '0.919 USD' in last_call
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:
@ -1418,13 +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
last_call = rpc_mock.call_args_list[-1][0][0]['status']
assert 'Selling' in last_call
assert '[ETH/BTC]' in last_call
assert 'Amount' in last_call
assert '0.00001044' in last_call
assert 'loss: -5.48%, -0.00005492' in last_call
assert '-0.824 USD' in last_call
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,
@ -1461,13 +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
last_call = rpc_mock.call_args_list[-1][0][0]['status']
assert 'Selling' in last_call
assert '[ETH/BTC]' in last_call
assert 'Amount' in last_call
assert '0.00001172' in last_call
assert '(profit: 6.11%, 0.00006126)' in last_call
assert 'USD' not in last_call
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,
@ -1504,11 +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
last_call = rpc_mock.call_args_list[-1][0][0]['status']
assert 'Selling' in last_call
assert '[ETH/BTC]' in last_call
assert '0.00001044' in last_call
assert 'loss: -5.48%, -0.00005492' in last_call
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,