diff --git a/freqtrade/rpc/__init__.py b/freqtrade/rpc/__init__.py index 24c9b754e..500024ef8 100644 --- a/freqtrade/rpc/__init__.py +++ b/freqtrade/rpc/__init__.py @@ -1,8 +1,10 @@ import logging import re import arrow +from decimal import Decimal from datetime import datetime, timedelta from pandas import DataFrame +import sqlalchemy as sql from freqtrade.persistence import Trade from freqtrade.misc import State, get_state @@ -182,3 +184,114 @@ def rpc_daily_profit(timescale, stake_currency, fiat_display_currency): for key, value in profit_days.items() ] return (False, stats) + + +def rpc_trade_statistics(stake_currency, fiat_display_currency) -> None: + """ + :return: cumulative profit statistics. + """ + trades = Trade.query.order_by(Trade.id).all() + + profit_all_coin = [] + profit_all_percent = [] + profit_closed_coin = [] + profit_closed_percent = [] + durations = [] + + for trade in trades: + current_rate = None + + if not trade.open_rate: + continue + if trade.close_date: + durations.append((trade.close_date - trade.open_date).total_seconds()) + + if not trade.is_open: + profit_percent = trade.calc_profit_percent() + profit_closed_coin.append(trade.calc_profit()) + profit_closed_percent.append(profit_percent) + else: + # Get current rate + current_rate = exchange.get_ticker(trade.pair, False)['bid'] + profit_percent = trade.calc_profit_percent(rate=current_rate) + + profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))) + profit_all_percent.append(profit_percent) + + best_pair = Trade.session.query(Trade.pair, + sql.func.sum(Trade.close_profit).label('profit_sum')) \ + .filter(Trade.is_open.is_(False)) \ + .group_by(Trade.pair) \ + .order_by(sql.text('profit_sum DESC')) \ + .first() + + if not best_pair: + return (True, '*Status:* `no closed trade`') + + bp_pair, bp_rate = best_pair + + # FIX: we want to keep fiatconverter in a state/environment, + # doing this will utilize its caching functionallity, instead we reinitialize it here + fiat = CryptoToFiatConverter() + # Prepare data to display + profit_closed_coin = round(sum(profit_closed_coin), 8) + profit_closed_percent = round(sum(profit_closed_percent) * 100, 2) + profit_closed_fiat = fiat.convert_amount( + profit_closed_coin, + stake_currency, + fiat_display_currency + ) + profit_all_coin = round(sum(profit_all_coin), 8) + profit_all_percent = round(sum(profit_all_percent) * 100, 2) + profit_all_fiat = fiat.convert_amount( + profit_all_coin, + stake_currency, + fiat_display_currency + ) + return (False, + {'profit_closed_coin': profit_closed_coin, + 'profit_closed_percent': profit_closed_percent, + 'profit_closed_fiat': profit_closed_fiat, + 'profit_all_coin': profit_all_coin, + 'profit_all_percent': profit_all_percent, + 'profit_all_fiat': profit_all_fiat, + 'trade_count': len(trades), + 'first_trade_date': arrow.get(trades[0].open_date).humanize(), + 'latest_trade_date': arrow.get(trades[-1].open_date).humanize(), + 'avg_duration': str(timedelta(seconds=sum(durations) / + float(len(durations)))).split('.')[0], + 'best_pair': bp_pair, + 'best_rate': round(bp_rate * 100, 2) + }) + + # Message to display + markdown_msg = """ +*ROI:* Close trades + ∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)` + ∙ `{profit_closed_fiat:.3f} {fiat}` +*ROI:* All trades + ∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)` + ∙ `{profit_all_fiat:.3f} {fiat}` + +*Total Trade Count:* `{trade_count}` +*First Trade opened:* `{first_trade_date}` +*Latest Trade opened:* `{latest_trade_date}` +*Avg. Duration:* `{avg_duration}` +*Best Performing:* `{best_pair}: {best_rate:.2f}%` + """.format( + coin=stake_currency, + fiat=fiat_display_currency, + profit_closed_coin=profit_closed_coin, + profit_closed_percent=profit_closed_percent, + profit_closed_fiat=profit_closed_fiat, + profit_all_coin=profit_all_coin, + profit_all_percent=profit_all_percent, + profit_all_fiat=profit_all_fiat, + trade_count=len(trades), + first_trade_date=arrow.get(trades[0].open_date).humanize(), + latest_trade_date=arrow.get(trades[-1].open_date).humanize(), + avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0], + best_pair=bp_pair, + best_rate=round(bp_rate * 100, 2), + ) + return markdown_msg diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 82bbec7be..61280aea2 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1,16 +1,18 @@ import logging -from datetime import timedelta -from decimal import Decimal from typing import Any, Callable -import arrow from sqlalchemy import and_, func, text from tabulate import tabulate from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update from telegram.error import NetworkError, TelegramError from telegram.ext import CommandHandler, Updater -from freqtrade.rpc.__init__ import rpc_status_table, rpc_trade_status, rpc_daily_profit +from freqtrade.rpc.__init__ import (rpc_status_table, + rpc_trade_status, + rpc_daily_profit, + rpc_trade_statistics + ) + from freqtrade import __version__, exchange from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.misc import State, get_state, update_state @@ -197,63 +199,12 @@ def _profit(bot: Bot, update: Update) -> None: :param update: message update :return: None """ - trades = Trade.query.order_by(Trade.id).all() - - profit_all_coin = [] - profit_all_percent = [] - profit_closed_coin = [] - profit_closed_percent = [] - durations = [] - - for trade in trades: - current_rate = None - - if not trade.open_rate: - continue - if trade.close_date: - durations.append((trade.close_date - trade.open_date).total_seconds()) - - if not trade.is_open: - profit_percent = trade.calc_profit_percent() - profit_closed_coin.append(trade.calc_profit()) - profit_closed_percent.append(profit_percent) - else: - # Get current rate - current_rate = exchange.get_ticker(trade.pair, False)['bid'] - profit_percent = trade.calc_profit_percent(rate=current_rate) - - profit_all_coin.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))) - profit_all_percent.append(profit_percent) - - best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \ - .filter(Trade.is_open.is_(False)) \ - .group_by(Trade.pair) \ - .order_by(text('profit_sum DESC')) \ - .first() - - if not best_pair: - send_msg('*Status:* `no closed trade`', bot=bot) + (error, stats) = rpc_trade_statistics(_CONF['stake_currency'], + _CONF['fiat_display_currency']) + if error: + send_msg(stats, bot=bot) return - bp_pair, bp_rate = best_pair - - # Prepare data to display - profit_closed_coin = round(sum(profit_closed_coin), 8) - profit_closed_percent = round(sum(profit_closed_percent) * 100, 2) - profit_closed_fiat = _FIAT_CONVERT.convert_amount( - profit_closed_coin, - _CONF['stake_currency'], - _CONF['fiat_display_currency'] - ) - profit_all_coin = round(sum(profit_all_coin), 8) - profit_all_percent = round(sum(profit_all_percent) * 100, 2) - profit_all_fiat = _FIAT_CONVERT.convert_amount( - profit_all_coin, - _CONF['stake_currency'], - _CONF['fiat_display_currency'] - ) - - # Message to display markdown_msg = """ *ROI:* Close trades ∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)` @@ -270,18 +221,18 @@ def _profit(bot: Bot, update: Update) -> None: """.format( coin=_CONF['stake_currency'], fiat=_CONF['fiat_display_currency'], - profit_closed_coin=profit_closed_coin, - profit_closed_percent=profit_closed_percent, - profit_closed_fiat=profit_closed_fiat, - profit_all_coin=profit_all_coin, - profit_all_percent=profit_all_percent, - profit_all_fiat=profit_all_fiat, - trade_count=len(trades), - first_trade_date=arrow.get(trades[0].open_date).humanize(), - latest_trade_date=arrow.get(trades[-1].open_date).humanize(), - avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0], - best_pair=bp_pair, - best_rate=round(bp_rate * 100, 2), + profit_closed_coin=stats['profit_closed_coin'], + profit_closed_percent=stats['profit_closed_percent'], + profit_closed_fiat=stats['profit_closed_fiat'], + profit_all_coin=stats['profit_all_coin'], + profit_all_percent=stats['profit_all_percent'], + profit_all_fiat=stats['profit_all_fiat'], + trade_count=stats['trade_count'], + first_trade_date=stats['first_trade_date'], + latest_trade_date=stats['latest_trade_date'], + avg_duration=stats['avg_duration'], + best_pair=stats['best_pair'], + best_rate=stats['best_rate'] ) send_msg(markdown_msg, bot=bot) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 199a3a99c..d4424d120 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -11,6 +11,13 @@ import freqtrade.misc as misc import freqtrade.rpc as rpc +def prec_satoshi(a, b): + """ + :return: True if A and B differs less than one satoshi. + """ + return abs(a - b) < 0.00000001 + + def test_init_telegram_enabled(default_conf, mocker): module_list = [] mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list) @@ -142,3 +149,58 @@ def test_rpc_daily_profit(default_conf, update, ticker, limit_buy_order, limit_s fiat_display_currency) assert error assert days.find('must be an integer greater than 0') >= 0 + + +def test_rpc_trade_statistics( + default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker): + mocker.patch.dict('freqtrade.main._CONF', default_conf) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + msg_mock = MagicMock() + mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) + mocker.patch.multiple('freqtrade.rpc.telegram', + _CONF=default_conf, + init=MagicMock()) + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker) + mocker.patch.multiple('freqtrade.fiat_convert.Pymarketcap', + ticker=MagicMock(return_value={'price_usd': 15000.0}), + _cache_symbols=MagicMock(return_value={'BTC': 1})) + mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) + main.init(default_conf, create_engine('sqlite://')) + stake_currency = default_conf['stake_currency'] + fiat_display_currency = default_conf['fiat_display_currency'] + + (error, stats) = rpc.rpc_trade_statistics(stake_currency, + fiat_display_currency) + assert error + assert stats.find('no closed trade') >= 0 + + # Create some test data + main.create_trade(0.001) + trade = Trade.query.first() + # Simulate fulfilled LIMIT_BUY order for trade + trade.update(limit_buy_order) + # Update the ticker with a market going up + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker_sell_up) + trade.update(limit_sell_order) + trade.close_date = datetime.utcnow() + trade.is_open = False + + (error, stats) = rpc.rpc_trade_statistics(stake_currency, + fiat_display_currency) + assert not error + assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05) + assert prec_satoshi(stats['profit_closed_percent'], 6.2) + assert prec_satoshi(stats['profit_closed_fiat'], 0.93255) + assert prec_satoshi(stats['profit_all_coin'], 6.217e-05) + assert prec_satoshi(stats['profit_all_percent'], 6.2) + assert prec_satoshi(stats['profit_all_fiat'], 0.93255) + assert stats['trade_count'] == 1 + assert stats['first_trade_date'] == 'just now' + assert stats['latest_trade_date'] == 'just now' + assert stats['avg_duration'] == '0:00:00' + assert stats['best_pair'] == 'BTC_ETH' + assert prec_satoshi(stats['best_rate'], 6.2)