diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 2198fd917..ba0ad1785 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -87,7 +87,8 @@ class Trade(_DECL_BASE): :param order: order retrieved by exchange.get_order() :return: None """ - if not order['closed']: + # Ignore open and cancelled orders + if not order['closed'] or order['rate'] is None: return logger.info('Updating trade (id=%d) ...', self.id) @@ -96,22 +97,28 @@ class Trade(_DECL_BASE): self.open_rate = order['rate'] self.amount = order['amount'] logger.info('LIMIT_BUY has been fulfilled for %s.', self) + self.open_order_id = None elif order['type'] == 'LIMIT_SELL': - # Set close rate and set actual profit - self.close_rate = order['rate'] - self.close_profit = self.calc_profit() - self.close_date = datetime.utcnow() - self.is_open = False - logger.info( - 'Marking %s as closed as the trade is fulfilled and found no open orders for it.', - self - ) + self.close(order['rate']) else: raise ValueError('Unknown order type: {}'.format(order['type'])) - - self.open_order_id = None Trade.session.flush() + def close(self, rate: float) -> None: + """ + Sets close_rate to the given rate, calculates total profit + and marks trade as closed + """ + self.close_rate = rate + self.close_profit = self.calc_profit() + self.close_date = datetime.utcnow() + self.is_open = False + self.open_order_id = None + logger.info( + 'Marking %s as closed as the trade is fulfilled and found no open orders for it.', + self + ) + def calc_profit(self, rate: Optional[float] = None) -> float: """ Calculates the profit in percentage (including fee). diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4b0465ddb..dc5dbf734 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1,10 +1,10 @@ import logging import re from datetime import timedelta, date +from decimal import Decimal from typing import Callable, Any import arrow -from decimal import Decimal from pandas import DataFrame from sqlalchemy import and_, func, text, between from tabulate import tabulate @@ -208,6 +208,7 @@ def _status_table(bot: Bot, update: Update) -> None: send_msg(message, parse_mode=ParseMode.HTML) + @authorized_only def _daily(bot: Bot, update: Update) -> None: """ @@ -217,37 +218,33 @@ def _daily(bot: Bot, update: Update) -> None: :param update: message update :return: None """ - trades = Trade.query.order_by(Trade.close_date).all() today = date.today().toordinal() profit_days = {} - + try: timescale = int(update.message.text.replace('/daily', '').strip()) - except: + except (TypeError, ValueError): timescale = 5 - + if not (isinstance(timescale, int) and timescale > 0): send_msg('*Daily [n]:* `must be an integer greater than 0`', bot=bot) return for day in range(0, timescale): - #need to query between day+1 and day-1 - nextdate = date.fromordinal(today-day+1) - prevdate = date.fromordinal(today-day-1) - trades = Trade.query.filter(between(Trade.close_date, prevdate, nextdate)).all() - curdayprofit = 0 - for trade in trades: - curdayprofit += trade.close_profit * trade.stake_amount - profit_days[date.fromordinal(today-day)] = format(curdayprofit, '.8f') + # need to query between day+1 and day-1 + nextdate = date.fromordinal(today-day+1) + prevdate = date.fromordinal(today-day-1) + trades = Trade.query.filter(between(Trade.close_date, prevdate, nextdate)).all() + curdayprofit = sum(trade.close_profit * trade.stake_amount for trade in trades) + profit_days[date.fromordinal(today-day)] = format(curdayprofit, '.8f') - stats = [] - for key, value in profit_days.items(): - stats.append([key, str(value) + ' BTC']) + stats = [[key, str(value) + ' BTC'] for key, value in profit_days.items()] stats = tabulate(stats, headers=['Day', 'Profit'], tablefmt='simple') message = 'Daily Profit over the last {} days:\n
{}'.format(timescale, stats) send_msg(message, bot=bot, parse_mode=ParseMode.HTML) - + + @authorized_only def _profit(bot: Bot, update: Update) -> None: """ @@ -391,10 +388,7 @@ def _forcesell(bot: Bot, update: Update) -> None: if trade_id == 'all': # Execute sell for all open orders for trade in Trade.query.filter(Trade.is_open.is_(True)).all(): - # Get current rate - current_rate = exchange.get_ticker(trade.pair)['bid'] - from freqtrade.main import execute_sell - execute_sell(trade, current_rate) + _exec_forcesell(trade) return # Query for trade @@ -406,10 +400,8 @@ def _forcesell(bot: Bot, update: Update) -> None: send_msg('Invalid argument. See `/help` to view usage') logger.warning('/forcesell: Invalid argument received') return - # Get current rate - current_rate = exchange.get_ticker(trade.pair)['bid'] - from freqtrade.main import execute_sell - execute_sell(trade, current_rate) + + _exec_forcesell(trade) @authorized_only @@ -504,11 +496,11 @@ def _version(bot: Bot, update: Update) -> None: send_msg('*Version:* `{}`'.format(__version__), bot=bot) -def shorten_date(date): +def shorten_date(_date): """ Trim the date so it fits on small screens """ - new_date = re.sub('seconds?', 'sec', date) + new_date = re.sub('seconds?', 'sec', _date) new_date = re.sub('minutes?', 'min', new_date) new_date = re.sub('hours?', 'h', new_date) new_date = re.sub('days?', 'd', new_date) @@ -516,6 +508,28 @@ def shorten_date(date): return new_date +def _exec_forcesell(trade: Trade) -> None: + # Check if there is there is an open order + if trade.open_order_id: + order = exchange.get_order(trade.open_order_id) + + # Cancel open LIMIT_BUY orders and close trade + if order and not order['closed'] and order['type'] == 'LIMIT_BUY': + exchange.cancel_order(trade.open_order_id) + trade.close(order.get('rate') or trade.open_rate) + # TODO: sell amount which has been bought already + return + + # Ignore trades with an attached LIMIT_SELL order + if order and not order['closed'] and order['type'] == 'LIMIT_SELL': + return + + # Get current rate and execute sell + current_rate = exchange.get_ticker(trade.pair)['bid'] + from freqtrade.main import execute_sell + execute_sell(trade, current_rate) + + def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: """ Send given markdown message @@ -529,7 +543,7 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO bot = bot or _UPDATER.bot - keyboard = [['/daily', '/profit', '/balance' ], + keyboard = [['/daily', '/profit', '/balance'], ['/status', '/status table', '/performance'], ['/count', '/start', '/stop', '/help']] diff --git a/freqtrade/tests/test_rpc_telegram.py b/freqtrade/tests/test_rpc_telegram.py index 58a9fcaa9..901450f14 100644 --- a/freqtrade/tests/test_rpc_telegram.py +++ b/freqtrade/tests/test_rpc_telegram.py @@ -14,7 +14,8 @@ from freqtrade.misc import update_state, State, get_state from freqtrade.persistence import Trade from freqtrade.rpc import telegram from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \ - _profit, _forcesell, _performance, _daily, _count, _start, _stop, _balance, _version, _help + _profit, _forcesell, _performance, _daily, _count, _start, _stop, _balance, _version, _help, \ + _exec_forcesell def test_is_enabled(default_conf, mocker): @@ -220,6 +221,30 @@ def test_forcesell_handle(default_conf, update, ticker, mocker): assert '0.07256061 (profit: ~-0.64%)' in rpc_mock.call_args_list[-1][0][0] +def test_exec_forcesell_open_orders(default_conf, ticker, mocker): + mocker.patch.dict('freqtrade.main._CONF', default_conf) + cancel_order_mock = MagicMock() + mocker.patch.multiple('freqtrade.main.exchange', + get_ticker=ticker, + get_order=MagicMock(return_value={ + 'closed': None, + 'type': 'LIMIT_BUY', + }), + cancel_order=cancel_order_mock) + trade = Trade( + pair='BTC_ETH', + open_rate=1, + exchange='BITTREX', + open_order_id='123456789', + amount=1, + fee=0.0, + ) + _exec_forcesell(trade) + + assert cancel_order_mock.call_count == 1 + assert trade.is_open is False + + def test_forcesell_all_handle(default_conf, update, ticker, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)