diff --git a/config.json.example b/config.json.example index dc2bd6beb..c1686a4ac 100644 --- a/config.json.example +++ b/config.json.example @@ -7,13 +7,17 @@ "ticker_interval" : "5m", "dry_run": false, "trailing_stop": false, - "unfilledtimeout": { - "buy": 10, - "sell": 30 + "unfilled_timeout": { + "buy": { + "after": 10, + "if_buy_signal_still_valid": false + }, + "sell": { + "after": 30 + } }, "bid_strategy": { "ask_last_balance": 0.0, - "timeout_even_if_buy_signal_valid": true, "use_order_book": false, "order_book_top": 1, "check_depth_of_market": { diff --git a/config_binance.json.example b/config_binance.json.example index b5ed62488..7c884feeb 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -7,13 +7,17 @@ "ticker_interval" : "5m", "dry_run": true, "trailing_stop": false, - "unfilledtimeout": { - "buy": 10, - "sell": 30 + "unfilled_timeout": { + "buy": { + "after": 10, + "if_buy_signal_still_valid": false + }, + "sell": { + "after": 30 + } }, "bid_strategy": { "use_order_book": false, - "timeout_even_if_buy_signal_valid": true, "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { diff --git a/config_full.json.example b/config_full.json.example index f191e7b77..79adde9da 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -20,13 +20,17 @@ "0": 0.04 }, "stoploss": -0.10, - "unfilledtimeout": { - "buy": 10, - "sell": 30 + "unfilled_timeout": { + "buy": { + "after": 10, + "if_buy_signal_still_valid": false + }, + "sell": { + "after": 30 + } }, "bid_strategy": { "use_order_book": false, - "timeout_even_if_buy_signal_valid": true, "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { diff --git a/config_kraken.json.example b/config_kraken.json.example index 7f64c3402..4b2520455 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -7,13 +7,17 @@ "ticker_interval" : "5m", "dry_run": true, "trailing_stop": false, - "unfilledtimeout": { - "buy": 10, - "sell": 30 + "unfilled_timeout": { + "buy": { + "after": 10, + "if_buy_signal_still_valid": false + }, + "sell": { + "after": 30 + } }, "bid_strategy": { "use_order_book": false, - "timeout_even_if_buy_signal_valid": true, "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { diff --git a/docs/configuration.md b/docs/configuration.md index 2f13afaa3..b3123cb10 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -58,10 +58,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Float* | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `0.0` (no offset).*
***Datatype:*** *Float* | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
***Datatype:*** *Boolean* -| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Integer* -| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Integer* +| `unfilled_timeout.buy.after` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Integer* +| `unfilled_timeout.buy.if_buy_signal_still_valid` | **Required.** Choose whether you would prefer unfilled buy orders to [timeout if buy signal was still valid](#timeout-even-if-buy-signal-valid). If false, your unfilled buy orders won't get timed out, so use this option wisely.
*Defaults to `true`.*
***Datatype:*** *Boolean* +| `unfilled_timeout.sell.after` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Integer* | `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook). -| `bid_strategy.timeout_even_if_buy_signal_valid` | **Required.** Choose whether you would prefer buy orders to [timeout even if buy signal was still valid](#timeout-even-if-buy-signal-valid). If false, your buy orders will not timeout and will ignore your unfilledtimeout for buy orders, so use this option wisely.
*Defaults to `true`.*
***Datatype:*** *Boolean* | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
***Datatype:*** *Boolean* | `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled).
*Defaults to `1`.*
***Datatype:*** *Positive Integer* | `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market).
*Defaults to `false`.*
***Datatype:*** *Boolean* @@ -128,7 +128,7 @@ Values set in the configuration file always overwrite values set in the strategy * `order_time_in_force` * `stake_currency` * `stake_amount` -* `unfilledtimeout` +* `unfilled_timeout` * `use_sell_signal` (ask_strategy) * `sell_profit_only` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index eae7f4d8d..8c3ccdc6a 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -94,11 +94,22 @@ CONF_SCHEMA = { 'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1}, 'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1}, 'trailing_only_offset_is_reached': {'type': 'boolean'}, - 'unfilledtimeout': { + 'unfilled_timeout': { 'type': 'object', 'properties': { - 'buy': {'type': 'number', 'minimum': 1}, - 'sell': {'type': 'number', 'minimum': 1} + 'buy': { + 'type': 'object', + 'properties': { + 'after': {'type': 'number', 'minimum': 1}, + 'if_buy_signal_still_valid': {'type': 'boolean'}, + }, + }, + 'sell': { + 'type': 'object', + 'properties': { + 'after': {'type': 'number', 'minimum': 1}, + } + } } }, 'bid_strategy': { @@ -110,7 +121,6 @@ CONF_SCHEMA = { 'maximum': 1, 'exclusiveMaximum': False, }, - 'timeout_even_if_buy_signal_valid': {'type': 'boolean'}, 'use_order_book': {'type': 'boolean'}, 'order_book_top': {'type': 'integer', 'maximum': 20, 'minimum': 1}, 'check_depth_of_market': { @@ -286,7 +296,7 @@ SCHEMA_TRADE_REQUIRED = [ 'dry_run', 'dry_run_wallet', 'bid_strategy', - 'unfilledtimeout', + 'unfilled_timeout', 'stoploss', 'minimal_roi', ] diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 20104f469..aaec927f3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -785,7 +785,7 @@ class FreqtradeBot: """ Check if timeout is active, and if the order is still open and timed out """ - timeout = self.config.get('unfilledtimeout', {}).get(side) + timeout = self.config.get('unfilled_timeout', {}).get(side).get('after') ordertime = arrow.get(order['datetime']).datetime if timeout is not None: timeout_threshold = arrow.utcnow().shift(minutes=-timeout).datetime @@ -821,8 +821,20 @@ class FreqtradeBot: if ((order['side'] == 'buy' and order['status'] == 'canceled') or (self._check_timed_out('buy', order))): - self.handle_timedout_limit_buy(trade, order) - self.wallets.update() + # running get_signal on historical data fetched + (buy, sell) = self.strategy.get_signal( + trade.pair, self.strategy.ticker_interval, + self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval)) + + # proceed to cancel buy order by timeout if configuration + # unfilled_timeout.if_buy_signal_still_valid is true (original behaviour) -OR- + # cancel buy order only if buying condition is no longer valid OR if there's + # a sell signal present + config_buy_signal_still_valid = self.config.get('unfilled_timeout', {}) \ + .get('buy').get('if_buy_signal_still_valid') + if (config_buy_signal_still_valid) or (not buy or sell): + self.handle_timedout_limit_buy(trade, order) + self.wallets.update() elif ((order['side'] == 'sell' and order['status'] == 'canceled') or (self._check_timed_out('sell', order))): @@ -846,59 +858,44 @@ class FreqtradeBot: """ reason = "cancelled due to timeout" - # running get_signal on historical data fetched - (buy, sell) = self.strategy.get_signal( - trade.pair, self.strategy.ticker_interval, - self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval)) - - # get config for bid strategy - config_bid_strategy = self.config.get('bid_strategy', {}) - - # proceed to cancel buy order by timeout if timeout_even_if_buy_signal_valid - # is true (original behaviour) -OR- - # cancel buy order only if buying condition is no longer valid OR if there's - # a sell signal present - if config_bid_strategy.get('timeout_even_if_buy_signal_valid', True) or (not buy or sell): - if order['status'] != 'canceled': - corder = self.exchange.cancel_order(trade.open_order_id, trade.pair) - else: - # Order was cancelled already, so we can reuse the existing dict - corder = order - reason = "canceled on exchange" - - if corder.get('remaining', order['remaining']) == order['amount']: - # if trade is not partially completed, just delete the trade - self.handle_buy_order_full_cancel(trade, reason) - return True - - # if trade is partially complete, edit the stake details for the trade - # and close the order - # cancel_order may not contain the full order dict, so we need to fallback - # to the order dict aquired before cancelling. - # we need to fall back to the values from order if corder does not contain these keys. - trade.amount = order['amount'] - corder.get('remaining', order['remaining']) - trade.stake_amount = trade.amount * trade.open_rate - # verify if fees were taken from amount to avoid problems during selling - try: - new_amount = self.get_real_amount(trade, corder if 'fee' in corder else order, - trade.amount) - if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC): - trade.amount = new_amount - # Fee was applied, so set to 0 - trade.fee_open = 0 - trade.recalc_open_trade_price() - except DependencyException as e: - logger.warning("Could not update trade amount: %s", e) - - trade.open_order_id = None - logger.info(f"Partial buy order timeout for {trade.pair}") - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f"Remaining buy order for {trade.pair} cancelled due to timeout" - }) - return False + if order['status'] != 'canceled': + corder = self.exchange.cancel_order(trade.open_order_id, trade.pair) else: - return False + # Order was cancelled already, so we can reuse the existing dict + corder = order + reason = "canceled on exchange" + + if corder.get('remaining', order['remaining']) == order['amount']: + # if trade is not partially completed, just delete the trade + self.handle_buy_order_full_cancel(trade, reason) + return True + + # if trade is partially complete, edit the stake details for the trade + # and close the order + # cancel_order may not contain the full order dict, so we need to fallback + # to the order dict aquired before cancelling. + # we need to fall back to the values from order if corder does not contain these keys. + trade.amount = order['amount'] - corder.get('remaining', order['remaining']) + trade.stake_amount = trade.amount * trade.open_rate + # verify if fees were taken from amount to avoid problems during selling + try: + new_amount = self.get_real_amount(trade, corder if 'fee' in corder else order, + trade.amount) + if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC): + trade.amount = new_amount + # Fee was applied, so set to 0 + trade.fee_open = 0 + trade.recalc_open_trade_price() + except DependencyException as e: + logger.warning("Could not update trade amount: %s", e) + + trade.open_order_id = None + logger.info(f"Partial buy order timeout for {trade.pair}") + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f"Remaining buy order for {trade.pair} cancelled due to timeout" + }) + return False def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool: """ diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 015ba24d9..d1e068a3b 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -66,7 +66,7 @@ class StrategyResolver(IResolver): ("stake_currency", None, False), ("stake_amount", None, False), ("startup_candle_count", None, False), - ("unfilledtimeout", None, False), + ("unfilled_timeout", None, False), ("use_sell_signal", True, True), ("sell_profit_only", False, True), ("ignore_roi_if_buy_signal", False, True), diff --git a/tests/conftest.py b/tests/conftest.py index 068e42d75..349c46499 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -210,13 +210,17 @@ def default_conf(testdatadir): "0": 0.04 }, "stoploss": -0.10, - "unfilledtimeout": { - "buy": 10, - "sell": 30 + "unfilled_timeout": { + "buy": { + "after": 10, + "if_buy_signal_still_valid": False + }, + "sell": { + "after": 30 + } }, "bid_strategy": { "ask_last_balance": 0.0, - "timeout_even_if_buy_signal_valid": True, "use_order_book": False, "order_book_top": 1, "check_depth_of_market": {