Cancel all open orders after receiving /stop or ctrl+c

This commit is contained in:
jpribyl
2020-04-24 16:16:52 -06:00
parent 0f50449196
commit bd51cd332b
16 changed files with 124 additions and 52 deletions

View File

@@ -13,7 +13,7 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat
ARGS_STRATEGY = ["strategy", "strategy_path"]
ARGS_TRADE = ["db_url", "sd_notify", "dry_run"]
ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "cancel_open_orders_on_exit"]
ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange",
"max_open_trades", "stake_amount", "fee"]

View File

@@ -109,6 +109,11 @@ AVAILABLE_CLI_OPTIONS = {
help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).',
action='store_true',
),
"cancel_open_orders_on_exit": Arg(
'--cancel-open-orders-on-exit',
help='Close unfilled open orders when the bot stops / exits',
action='store_true',
),
# Optimize common
"ticker_interval": Arg(
'-i', '--ticker-interval',

View File

@@ -134,6 +134,11 @@ class Configuration:
if config['runmode'] not in TRADING_MODES:
return
if self.args.get('cancel_open_orders_on_exit', False):
config.update({
'cancel_open_orders_on_exit': self.args.get('cancel_open_orders_on_exit')
})
if config.get('dry_run', False):
logger.info('Dry run is enabled')
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:

View File

@@ -85,6 +85,7 @@ CONF_SCHEMA = {
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
'dry_run': {'type': 'boolean'},
'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET},
'cancel_open_orders_on_exit': {'type': 'boolean', 'default': False},
'process_only_new_candles': {'type': 'boolean'},
'minimal_roi': {
'type': 'object',
@@ -318,3 +319,10 @@ SCHEMA_MINIMAL_REQUIRED = [
'dataformat_ohlcv',
'dataformat_trades',
]
CANCEL_REASON = {
"TIMEOUT": "cancelled due to timeout",
"PARTIALLY_FILLED": "partially filled - keeping order open",
"ALL_CANCELLED": "cancelled (all unfilled orders cancelled)",
"CANCELLED_ON_EXCHANGE": "cancelled on exchange",
}

View File

@@ -113,6 +113,9 @@ class FreqtradeBot:
"""
logger.info('Cleaning up modules ...')
if self.config['cancel_open_orders_on_exit']:
self.cancel_all_open_orders()
self.rpc.cleanup()
persistence.cleanup()
@@ -162,6 +165,13 @@ class FreqtradeBot:
Trade.session.flush()
def process_stopped(self) -> None:
"""
Close all trades that were left open
"""
if self.config['cancel_open_orders_on_exit']:
self.cancel_all_open_orders()
def _refresh_whitelist(self, trades: List[Trade] = []) -> List[str]:
"""
Refresh whitelist from pairlist or edge and extend it with trades.
@@ -875,11 +885,7 @@ class FreqtradeBot:
default_retval=False)(pair=trade.pair,
trade=trade,
order=order))):
self.handle_timedout_limit_buy(trade, order)
self.wallets.update()
order_type = self.strategy.order_types['buy']
self._notify_buy_cancel(trade, order_type)
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT'])
elif (order['side'] == 'sell' and (
trade_state_update
@@ -888,24 +894,42 @@ class FreqtradeBot:
default_retval=False)(pair=trade.pair,
trade=trade,
order=order))):
reason = self.handle_timedout_limit_sell(trade, order)
self.wallets.update()
order_type = self.strategy.order_types['sell']
self._notify_sell_cancel(trade, order_type, reason)
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT'])
def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool:
def cancel_all_open_orders(self) -> None:
"""
Buy timeout - cancel order
Cancel all orders that are currently open
:return: None
"""
for trade in Trade.get_open_order_trades():
try:
order = self.exchange.get_order(trade.open_order_id, trade.pair)
except (DependencyException, InvalidOrderException):
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue
if order['side'] == 'buy':
self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
elif order['side'] == 'sell':
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool:
"""
Buy cancel - cancel order
:return: True if order was fully cancelled
"""
was_trade_fully_canceled = False
if order['status'] != 'canceled':
reason = "cancelled due to timeout"
reason = constants.CANCEL_REASON['TIMEOUT']
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
trade.amount)
else:
# Order was cancelled already, so we can reuse the existing dict
corder = order
reason = "cancelled on exchange"
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info('Buy order %s for %s.', reason, trade)
@@ -914,40 +938,42 @@ class FreqtradeBot:
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
Trade.session.flush()
return True
was_trade_fully_canceled = True
else:
# 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'] - safe_value_fallback(corder, order,
'remaining', 'remaining')
trade.stake_amount = trade.amount * trade.open_rate
self.update_trade_state(trade, corder, trade.amount)
# 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'] - safe_value_fallback(corder, order,
'remaining', 'remaining')
trade.stake_amount = trade.amount * trade.open_rate
self.update_trade_state(trade, corder, trade.amount)
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 {trade.pair} cancelled due to timeout'
})
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 {trade.pair} cancelled due to timeout'
})
return False
self.wallets.update()
self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'])
return was_trade_fully_canceled
def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> str:
def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str:
"""
Sell timeout - cancel order and update trade
Sell cancel - cancel order and update trade
:return: Reason for cancel
"""
# if trade is not partially completed, just cancel the trade
if order['remaining'] == order['amount'] or order.get('filled') == 0.0:
if not self.exchange.check_order_canceled_empty(order):
reason = "cancelled due to timeout"
# if trade is not partially completed, just delete the trade
self.exchange.cancel_order(trade.open_order_id, trade.pair)
logger.info('Sell order %s for %s.', reason, trade)
else:
reason = "cancelled on exchange"
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info('Sell order %s for %s.', reason, trade)
trade.close_rate = None
@@ -957,11 +983,17 @@ class FreqtradeBot:
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
else:
# TODO: figure out how to handle partially complete sell orders
reason = constants.CANCEL_REASON['PARTIALLY_FILLED']
return reason
# TODO: figure out how to handle partially complete sell orders
return 'partially filled - keeping order open'
self.wallets.update()
self._notify_sell_cancel(
trade,
order_type=self.strategy.order_types['sell'],
reason=reason
)
return reason
def _safe_sell_amount(self, pair: str, amount: float) -> float:
"""

View File

@@ -6,6 +6,7 @@
"fiat_display_currency": "{{ fiat_display_currency }}",
"ticker_interval": "{{ ticker_interval }}",
"dry_run": {{ dry_run | lower }},
"cancel_open_orders_on_exit": false,
"unfilledtimeout": {
"buy": 10,
"sell": 30

View File

@@ -131,8 +131,7 @@ class Worker:
return result
def _process_stopped(self) -> None:
# Maybe do here something in the future...
pass
self.freqtrade.process_stopped()
def _process_running(self) -> None:
try: