Merge pull request #192 from gcarq/feature/forcesell-handle-open-orders
/forcesell: handle trades with open orders
This commit is contained in:
		| @@ -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). | ||||
|   | ||||
| @@ -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,13 +218,12 @@ 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): | ||||
| @@ -231,23 +231,20 @@ def _daily(bot: Bot, update: Update) -> None: | ||||
|         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 = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'.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']] | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user