• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

freqtrade / freqtrade / 4131167254

pending completion
4131167254

push

github-actions

GitHub
Merge pull request #7983 from stash86/bt-metrics

16866 of 17748 relevant lines covered (95.03%)

0.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.86
/freqtrade/freqtradebot.py
1
"""
2
Freqtrade is the main module of this bot. It contains the class Freqtrade()
3
"""
4
import copy
1✔
5
import logging
1✔
6
import traceback
1✔
7
from datetime import datetime, time, timedelta, timezone
1✔
8
from math import isclose
1✔
9
from threading import Lock
1✔
10
from typing import Any, Dict, List, Optional, Tuple
1✔
11

12
from schedule import Scheduler
1✔
13

14
from freqtrade import constants
1✔
15
from freqtrade.configuration import validate_config_consistency
1✔
16
from freqtrade.constants import BuySell, Config, LongShort
1✔
17
from freqtrade.data.converter import order_book_to_dataframe
1✔
18
from freqtrade.data.dataprovider import DataProvider
1✔
19
from freqtrade.edge import Edge
1✔
20
from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode, SignalDirection,
1✔
21
                             State, TradingMode)
22
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
1✔
23
                                  InvalidOrderException, PricingError)
24
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
1✔
25
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
1✔
26
from freqtrade.mixins import LoggingMixin
1✔
27
from freqtrade.persistence import Order, PairLocks, Trade, init_db
1✔
28
from freqtrade.plugins.pairlistmanager import PairListManager
1✔
29
from freqtrade.plugins.protectionmanager import ProtectionManager
1✔
30
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
1✔
31
from freqtrade.rpc import RPCManager
1✔
32
from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
1✔
33
from freqtrade.strategy.interface import IStrategy
1✔
34
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
1✔
35
from freqtrade.util import FtPrecise
1✔
36
from freqtrade.wallets import Wallets
1✔
37

38

39
logger = logging.getLogger(__name__)
1✔
40

41

42
class FreqtradeBot(LoggingMixin):
1✔
43
    """
44
    Freqtrade is the main class of the bot.
45
    This is from here the bot start its logic.
46
    """
47

48
    def __init__(self, config: Config) -> None:
1✔
49
        """
50
        Init all variables and objects the bot needs to work
51
        :param config: configuration dict, you can use Configuration.get_config()
52
        to get the config dict.
53
        """
54
        self.active_pair_whitelist: List[str] = []
1✔
55

56
        # Init bot state
57
        self.state = State.STOPPED
1✔
58

59
        # Init objects
60
        self.config = config
1✔
61

62
        self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
1✔
63

64
        # Check config consistency here since strategies can set certain options
65
        validate_config_consistency(config)
1✔
66

67
        self.exchange = ExchangeResolver.load_exchange(
1✔
68
            self.config['exchange']['name'], self.config, load_leverage_tiers=True)
69

70
        init_db(self.config['db_url'])
1✔
71

72
        self.wallets = Wallets(self.config, self.exchange)
1✔
73

74
        PairLocks.timeframe = self.config['timeframe']
1✔
75

76
        self.pairlists = PairListManager(self.exchange, self.config)
1✔
77

78
        # RPC runs in separate threads, can start handling external commands just after
79
        # initialization, even before Freqtradebot has a chance to start its throttling,
80
        # so anything in the Freqtradebot instance should be ready (initialized), including
81
        # the initial state of the bot.
82
        # Keep this at the end of this initialization method.
83
        self.rpc: RPCManager = RPCManager(self)
1✔
84

85
        self.dataprovider = DataProvider(self.config, self.exchange, rpc=self.rpc)
1✔
86
        self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider)
1✔
87

88
        self.dataprovider.add_pairlisthandler(self.pairlists)
1✔
89

90
        # Attach Dataprovider to strategy instance
91
        self.strategy.dp = self.dataprovider
1✔
92
        # Attach Wallets to strategy instance
93
        self.strategy.wallets = self.wallets
1✔
94

95
        # Initializing Edge only if enabled
96
        self.edge = Edge(self.config, self.exchange, self.strategy) if \
1✔
97
            self.config.get('edge', {}).get('enabled', False) else None
98

99
        # Init ExternalMessageConsumer if enabled
100
        self.emc = ExternalMessageConsumer(self.config, self.dataprovider) if \
1✔
101
            self.config.get('external_message_consumer', {}).get('enabled', False) else None
102

103
        self.active_pair_whitelist = self._refresh_active_whitelist()
1✔
104

105
        # Set initial bot state from config
106
        initial_state = self.config.get('initial_state')
1✔
107
        self.state = State[initial_state.upper()] if initial_state else State.STOPPED
1✔
108

109
        # Protect exit-logic from forcesell and vice versa
110
        self._exit_lock = Lock()
1✔
111
        LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
1✔
112

113
        self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
1✔
114

115
        self._schedule = Scheduler()
1✔
116

117
        if self.trading_mode == TradingMode.FUTURES:
1✔
118

119
            def update():
1✔
120
                self.update_funding_fees()
1✔
121
                self.wallets.update()
1✔
122

123
            # TODO: This would be more efficient if scheduled in utc time, and performed at each
124
            # TODO: funding interval, specified by funding_fee_times on the exchange classes
125
            for time_slot in range(0, 24):
1✔
126
                for minutes in [0, 15, 30, 45]:
1✔
127
                    t = str(time(time_slot, minutes, 2))
1✔
128
                    self._schedule.every().day.at(t).do(update)
1✔
129
        self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc)
1✔
130

131
        self.strategy.ft_bot_start()
1✔
132
        # Initialize protections AFTER bot start - otherwise parameters are not loaded.
133
        self.protections = ProtectionManager(self.config, self.strategy.protections)
1✔
134

135
    def notify_status(self, msg: str) -> None:
1✔
136
        """
137
        Public method for users of this class (worker, etc.) to send notifications
138
        via RPC about changes in the bot status.
139
        """
140
        self.rpc.send_msg({
1✔
141
            'type': RPCMessageType.STATUS,
142
            'status': msg
143
        })
144

145
    def cleanup(self) -> None:
1✔
146
        """
147
        Cleanup pending resources on an already stopped bot
148
        :return: None
149
        """
150
        logger.info('Cleaning up modules ...')
1✔
151
        try:
1✔
152
            # Wrap db activities in shutdown to avoid problems if database is gone,
153
            # and raises further exceptions.
154
            if self.config['cancel_open_orders_on_exit']:
1✔
155
                self.cancel_all_open_orders()
1✔
156

157
            self.check_for_open_trades()
1✔
158
        except Exception as e:
1✔
159
            logger.warning(f'Exception during cleanup: {e.__class__.__name__} {e}')
1✔
160

161
        finally:
162
            self.strategy.ft_bot_cleanup()
1✔
163

164
        self.rpc.cleanup()
1✔
165
        if self.emc:
1✔
166
            self.emc.shutdown()
1✔
167
        self.exchange.close()
1✔
168
        try:
1✔
169
            Trade.commit()
1✔
170
        except Exception:
1✔
171
            # Exeptions here will be happening if the db disappeared.
172
            # At which point we can no longer commit anyway.
173
            pass
1✔
174

175
    def startup(self) -> None:
1✔
176
        """
177
        Called on startup and after reloading the bot - triggers notifications and
178
        performs startup tasks
179
        """
180
        self.rpc.startup_messages(self.config, self.pairlists, self.protections)
1✔
181
        # Update older trades with precision and precision mode
182
        self.startup_backpopulate_precision()
1✔
183
        if not self.edge:
1✔
184
            # Adjust stoploss if it was changed
185
            Trade.stoploss_reinitialization(self.strategy.stoploss)
1✔
186

187
        # Only update open orders on startup
188
        # This will update the database after the initial migration
189
        self.startup_update_open_orders()
1✔
190

191
    def process(self) -> None:
1✔
192
        """
193
        Queries the persistence layer for open trades and handles them,
194
        otherwise a new trade is created.
195
        :return: True if one or more trades has been created or closed, False otherwise
196
        """
197

198
        # Check whether markets have to be reloaded and reload them when it's needed
199
        self.exchange.reload_markets()
1✔
200

201
        self.update_trades_without_assigned_fees()
1✔
202

203
        # Query trades from persistence layer
204
        trades: List[Trade] = Trade.get_open_trades()
1✔
205

206
        self.active_pair_whitelist = self._refresh_active_whitelist(trades)
1✔
207

208
        # Refreshing candles
209
        self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
1✔
210
                                  self.strategy.gather_informative_pairs())
211

212
        strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
1✔
213

214
        self.strategy.analyze(self.active_pair_whitelist)
1✔
215

216
        with self._exit_lock:
1✔
217
            # Check for exchange cancelations, timeouts and user requested replace
218
            self.manage_open_orders()
1✔
219

220
        # Protect from collisions with force_exit.
221
        # Without this, freqtrade my try to recreate stoploss_on_exchange orders
222
        # while exiting is in process, since telegram messages arrive in an different thread.
223
        with self._exit_lock:
1✔
224
            trades = Trade.get_open_trades()
1✔
225
            # First process current opened trades (positions)
226
            self.exit_positions(trades)
1✔
227

228
        # Check if we need to adjust our current positions before attempting to buy new trades.
229
        if self.strategy.position_adjustment_enable:
1✔
230
            with self._exit_lock:
1✔
231
                self.process_open_trade_positions()
1✔
232

233
        # Then looking for buy opportunities
234
        if self.get_free_open_trades():
1✔
235
            self.enter_positions()
1✔
236
        if self.trading_mode == TradingMode.FUTURES:
1✔
237
            self._schedule.run_pending()
1✔
238
        Trade.commit()
1✔
239
        self.rpc.process_msg_queue(self.dataprovider._msg_queue)
1✔
240
        self.last_process = datetime.now(timezone.utc)
1✔
241

242
    def process_stopped(self) -> None:
1✔
243
        """
244
        Close all orders that were left open
245
        """
246
        if self.config['cancel_open_orders_on_exit']:
1✔
247
            self.cancel_all_open_orders()
1✔
248

249
    def check_for_open_trades(self):
1✔
250
        """
251
        Notify the user when the bot is stopped (not reloaded)
252
        and there are still open trades active.
253
        """
254
        open_trades = Trade.get_open_trades()
1✔
255

256
        if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG:
1✔
257
            msg = {
1✔
258
                'type': RPCMessageType.WARNING,
259
                'status':
260
                    f"{len(open_trades)} open trades active.\n\n"
261
                    f"Handle these trades manually on {self.exchange.name}, "
262
                    f"or '/start' the bot again and use '/stopentry' "
263
                    f"to handle open trades gracefully. \n"
264
                    f"{'Note: Trades are simulated (dry run).' if self.config['dry_run'] else ''}",
265
            }
266
            self.rpc.send_msg(msg)
1✔
267

268
    def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]:
1✔
269
        """
270
        Refresh active whitelist from pairlist or edge and extend it with
271
        pairs that have open trades.
272
        """
273
        # Refresh whitelist
274
        _prev_whitelist = self.pairlists.whitelist
1✔
275
        self.pairlists.refresh_pairlist()
1✔
276
        _whitelist = self.pairlists.whitelist
1✔
277

278
        # Calculating Edge positioning
279
        if self.edge:
1✔
280
            self.edge.calculate(_whitelist)
1✔
281
            _whitelist = self.edge.adjust(_whitelist)
1✔
282

283
        if trades:
1✔
284
            # Extend active-pair whitelist with pairs of open trades
285
            # It ensures that candle (OHLCV) data are downloaded for open trades as well
286
            _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist])
1✔
287

288
        # Called last to include the included pairs
289
        if _prev_whitelist != _whitelist:
1✔
290
            self.rpc.send_msg({'type': RPCMessageType.WHITELIST, 'data': _whitelist})
1✔
291

292
        return _whitelist
1✔
293

294
    def get_free_open_trades(self) -> int:
1✔
295
        """
296
        Return the number of free open trades slots or 0 if
297
        max number of open trades reached
298
        """
299
        open_trades = Trade.get_open_trade_count()
1✔
300
        return max(0, self.config['max_open_trades'] - open_trades)
1✔
301

302
    def update_funding_fees(self):
1✔
303
        if self.trading_mode == TradingMode.FUTURES:
1✔
304
            trades = Trade.get_open_trades()
1✔
305
            try:
1✔
306
                for trade in trades:
1✔
307
                    funding_fees = self.exchange.get_funding_fees(
1✔
308
                        pair=trade.pair,
309
                        amount=trade.amount,
310
                        is_short=trade.is_short,
311
                        open_date=trade.date_last_filled_utc
312
                    )
313
                    trade.funding_fees = funding_fees
1✔
314
            except ExchangeError:
×
315
                logger.warning("Could not update funding fees for open trades.")
×
316

317
    def startup_backpopulate_precision(self):
1✔
318

319
        trades = Trade.get_trades([Trade.contract_size.is_(None)])
1✔
320
        for trade in trades:
1✔
321
            if trade.exchange != self.exchange.id:
1✔
322
                continue
1✔
323
            trade.precision_mode = self.exchange.precisionMode
1✔
324
            trade.amount_precision = self.exchange.get_precision_amount(trade.pair)
1✔
325
            trade.price_precision = self.exchange.get_precision_price(trade.pair)
1✔
326
            trade.contract_size = self.exchange.get_contract_size(trade.pair)
1✔
327
        Trade.commit()
1✔
328

329
    def startup_update_open_orders(self):
1✔
330
        """
331
        Updates open orders based on order list kept in the database.
332
        Mainly updates the state of orders - but may also close trades
333
        """
334
        if self.config['dry_run'] or self.config['exchange'].get('skip_open_order_update', False):
1✔
335
            # Updating open orders in dry-run does not make sense and will fail.
336
            return
1✔
337

338
        orders = Order.get_open_orders()
1✔
339
        logger.info(f"Updating {len(orders)} open orders.")
1✔
340
        for order in orders:
1✔
341
            try:
1✔
342
                fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
1✔
343
                                                                 order.ft_order_side == 'stoploss')
344

345
                self.update_trade_state(order.trade, order.order_id, fo,
1✔
346
                                        stoploss_order=(order.ft_order_side == 'stoploss'))
347

348
            except InvalidOrderException as e:
1✔
349
                logger.warning(f"Error updating Order {order.order_id} due to {e}.")
1✔
350
                if order.order_date_utc - timedelta(days=5) < datetime.now(timezone.utc):
1✔
351
                    logger.warning(
1✔
352
                        "Order is older than 5 days. Assuming order was fully cancelled.")
353
                    fo = order.to_ccxt_object()
1✔
354
                    fo['status'] = 'canceled'
1✔
355
                    self.handle_timedout_order(fo, order.trade)
1✔
356

357
            except ExchangeError as e:
1✔
358

359
                logger.warning(f"Error updating Order {order.order_id} due to {e}")
1✔
360

361
        if self.trading_mode == TradingMode.FUTURES:
1✔
362
            self._schedule.run_pending()
×
363

364
    def update_trades_without_assigned_fees(self) -> None:
1✔
365
        """
366
        Update closed trades without close fees assigned.
367
        Only acts when Orders are in the database, otherwise the last order-id is unknown.
368
        """
369
        if self.config['dry_run']:
1✔
370
            # Updating open orders in dry-run does not make sense and will fail.
371
            return
1✔
372

373
        trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees()
1✔
374
        for trade in trades:
1✔
375
            if not trade.is_open and not trade.fee_updated(trade.exit_side):
1✔
376
                # Get sell fee
377
                order = trade.select_order(trade.exit_side, False)
1✔
378
                if not order:
1✔
379
                    order = trade.select_order('stoploss', False)
×
380
                if order:
1✔
381
                    logger.info(
1✔
382
                        f"Updating {trade.exit_side}-fee on trade {trade}"
383
                        f"for order {order.order_id}."
384
                    )
385
                    self.update_trade_state(trade, order.order_id,
1✔
386
                                            stoploss_order=order.ft_order_side == 'stoploss',
387
                                            send_msg=False)
388

389
        trades = Trade.get_open_trades_without_assigned_fees()
1✔
390
        for trade in trades:
1✔
391
            with self._exit_lock:
1✔
392
                if trade.is_open and not trade.fee_updated(trade.entry_side):
1✔
393
                    order = trade.select_order(trade.entry_side, False)
1✔
394
                    open_order = trade.select_order(trade.entry_side, True)
1✔
395
                    if order and open_order is None:
1✔
396
                        logger.info(
1✔
397
                            f"Updating {trade.entry_side}-fee on trade {trade}"
398
                            f"for order {order.order_id}."
399
                        )
400
                        self.update_trade_state(trade, order.order_id, send_msg=False)
1✔
401

402
    def handle_insufficient_funds(self, trade: Trade):
1✔
403
        """
404
        Try refinding a lost trade.
405
        Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy).
406
        Tries to walk the stored orders and sell them off eventually.
407
        """
408
        logger.info(f"Trying to refind lost order for {trade}")
1✔
409
        for order in trade.orders:
1✔
410
            logger.info(f"Trying to refind {order}")
1✔
411
            fo = None
1✔
412
            if not order.ft_is_open:
1✔
413
                logger.debug(f"Order {order} is no longer open.")
1✔
414
                continue
1✔
415
            try:
1✔
416
                fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
1✔
417
                                                                 order.ft_order_side == 'stoploss')
418
                if order.ft_order_side == 'stoploss':
1✔
419
                    if fo and fo['status'] == 'open':
1✔
420
                        # Assume this as the open stoploss order
421
                        trade.stoploss_order_id = order.order_id
1✔
422
                elif order.ft_order_side == trade.exit_side:
1✔
423
                    if fo and fo['status'] == 'open':
1✔
424
                        # Assume this as the open order
425
                        trade.open_order_id = order.order_id
1✔
426
                elif order.ft_order_side == trade.entry_side:
1✔
427
                    if fo and fo['status'] == 'open':
1✔
428
                        trade.open_order_id = order.order_id
1✔
429
                if fo:
1✔
430
                    logger.info(f"Found {order} for trade {trade}.")
1✔
431
                    self.update_trade_state(trade, order.order_id, fo,
1✔
432
                                            stoploss_order=order.ft_order_side == 'stoploss')
433

434
            except ExchangeError:
1✔
435
                logger.warning(f"Error updating {order.order_id}.")
1✔
436

437
#
438
# BUY / enter positions / open trades logic and methods
439
#
440

441
    def enter_positions(self) -> int:
1✔
442
        """
443
        Tries to execute entry orders for new trades (positions)
444
        """
445
        trades_created = 0
1✔
446

447
        whitelist = copy.deepcopy(self.active_pair_whitelist)
1✔
448
        if not whitelist:
1✔
449
            self.log_once("Active pair whitelist is empty.", logger.info)
1✔
450
            return trades_created
1✔
451
        # Remove pairs for currently opened trades from the whitelist
452
        for trade in Trade.get_open_trades():
1✔
453
            if trade.pair in whitelist:
1✔
454
                whitelist.remove(trade.pair)
1✔
455
                logger.debug('Ignoring %s in pair whitelist', trade.pair)
1✔
456

457
        if not whitelist:
1✔
458
            self.log_once("No currency pair in active pair whitelist, "
1✔
459
                          "but checking to exit open trades.", logger.info)
460
            return trades_created
1✔
461
        if PairLocks.is_global_lock(side='*'):
1✔
462
            # This only checks for total locks (both sides).
463
            # per-side locks will be evaluated by `is_pair_locked` within create_trade,
464
            # once the direction for the trade is clear.
465
            lock = PairLocks.get_pair_longest_lock('*')
1✔
466
            if lock:
1✔
467
                self.log_once(f"Global pairlock active until "
1✔
468
                              f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}. "
469
                              f"Not creating new trades, reason: {lock.reason}.", logger.info)
470
            else:
471
                self.log_once("Global pairlock active. Not creating new trades.", logger.info)
×
472
            return trades_created
1✔
473
        # Create entity and execute trade for each pair from whitelist
474
        for pair in whitelist:
1✔
475
            try:
1✔
476
                trades_created += self.create_trade(pair)
1✔
477
            except DependencyException as exception:
1✔
478
                logger.warning('Unable to create trade for %s: %s', pair, exception)
1✔
479

480
        if not trades_created:
1✔
481
            logger.debug("Found no enter signals for whitelisted currencies. Trying again...")
1✔
482

483
        return trades_created
1✔
484

485
    def create_trade(self, pair: str) -> bool:
1✔
486
        """
487
        Check the implemented trading strategy for buy signals.
488

489
        If the pair triggers the buy signal a new trade record gets created
490
        and the buy-order opening the trade gets issued towards the exchange.
491

492
        :return: True if a trade has been created.
493
        """
494
        logger.debug(f"create_trade for pair {pair}")
1✔
495

496
        analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe)
1✔
497
        nowtime = analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None
1✔
498

499
        # get_free_open_trades is checked before create_trade is called
500
        # but it is still used here to prevent opening too many trades within one iteration
501
        if not self.get_free_open_trades():
1✔
502
            logger.debug(f"Can't open a new trade for {pair}: max number of trades is reached.")
1✔
503
            return False
1✔
504

505
        # running get_signal on historical data fetched
506
        (signal, enter_tag) = self.strategy.get_entry_signal(
1✔
507
            pair,
508
            self.strategy.timeframe,
509
            analyzed_df
510
        )
511

512
        if signal:
1✔
513
            if self.strategy.is_pair_locked(pair, candle_date=nowtime, side=signal):
1✔
514
                lock = PairLocks.get_pair_longest_lock(pair, nowtime, signal)
1✔
515
                if lock:
1✔
516
                    self.log_once(f"Pair {pair} {lock.side} is locked until "
1✔
517
                                  f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)} "
518
                                  f"due to {lock.reason}.",
519
                                  logger.info)
520
                else:
521
                    self.log_once(f"Pair {pair} is currently locked.", logger.info)
×
522
                return False
1✔
523
            stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
1✔
524

525
            bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {})
1✔
526
            if ((bid_check_dom.get('enabled', False)) and
1✔
527
                    (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
528
                if self._check_depth_of_market(pair, bid_check_dom, side=signal):
1✔
529
                    return self.execute_entry(
1✔
530
                        pair,
531
                        stake_amount,
532
                        enter_tag=enter_tag,
533
                        is_short=(signal == SignalDirection.SHORT)
534
                    )
535
                else:
536
                    return False
1✔
537

538
            return self.execute_entry(
1✔
539
                pair,
540
                stake_amount,
541
                enter_tag=enter_tag,
542
                is_short=(signal == SignalDirection.SHORT)
543
            )
544
        else:
545
            return False
1✔
546

547
#
548
# BUY / increase positions / DCA logic and methods
549
#
550
    def process_open_trade_positions(self):
1✔
551
        """
552
        Tries to execute additional buy or sell orders for open trades (positions)
553
        """
554
        # Walk through each pair and check if it needs changes
555
        for trade in Trade.get_open_trades():
1✔
556
            # If there is any open orders, wait for them to finish.
557
            if trade.open_order_id is None:
1✔
558
                try:
1✔
559
                    self.check_and_call_adjust_trade_position(trade)
1✔
560
                except DependencyException as exception:
1✔
561
                    logger.warning(
1✔
562
                        f"Unable to adjust position of trade for {trade.pair}: {exception}")
563

564
    def check_and_call_adjust_trade_position(self, trade: Trade):
1✔
565
        """
566
        Check the implemented trading strategy for adjustment command.
567
        If the strategy triggers the adjustment, a new order gets issued.
568
        Once that completes, the existing trade is modified to match new data.
569
        """
570
        current_entry_rate, current_exit_rate = self.exchange.get_rates(
1✔
571
            trade.pair, True, trade.is_short)
572

573
        current_entry_profit = trade.calc_profit_ratio(current_entry_rate)
1✔
574
        current_exit_profit = trade.calc_profit_ratio(current_exit_rate)
1✔
575

576
        min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
1✔
577
                                                                  current_entry_rate,
578
                                                                  self.strategy.stoploss)
579
        min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
1✔
580
                                                                 current_exit_rate,
581
                                                                 self.strategy.stoploss)
582
        max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate)
1✔
583
        stake_available = self.wallets.get_available_stake_amount()
1✔
584
        logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
1✔
585
        stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
1✔
586
                                             default_retval=None)(
587
            trade=trade,
588
            current_time=datetime.now(timezone.utc), current_rate=current_entry_rate,
589
            current_profit=current_entry_profit, min_stake=min_entry_stake,
590
            max_stake=min(max_entry_stake, stake_available),
591
            current_entry_rate=current_entry_rate, current_exit_rate=current_exit_rate,
592
            current_entry_profit=current_entry_profit, current_exit_profit=current_exit_profit
593
        )
594

595
        if stake_amount is not None and stake_amount > 0.0:
1✔
596
            # We should increase our position
597
            if self.strategy.max_entry_position_adjustment > -1:
1✔
598
                count_of_entries = trade.nr_of_successful_entries
1✔
599
                if count_of_entries > self.strategy.max_entry_position_adjustment:
1✔
600
                    logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
1✔
601
                    return
1✔
602
                else:
603
                    logger.debug("Max adjustment entries is set to unlimited.")
×
604
            self.execute_entry(trade.pair, stake_amount, price=current_entry_rate,
1✔
605
                               trade=trade, is_short=trade.is_short)
606

607
        if stake_amount is not None and stake_amount < 0.0:
1✔
608
            # We should decrease our position
609
            amount = self.exchange.amount_to_contract_precision(
1✔
610
                trade.pair,
611
                abs(float(FtPrecise(stake_amount * trade.leverage) / FtPrecise(current_exit_rate))))
612
            if amount > trade.amount:
1✔
613
                # This is currently ineffective as remaining would become < min tradable
614
                # Fixing this would require checking for 0.0 there -
615
                # if we decide that this callback is allowed to "fully exit"
616
                logger.info(
1✔
617
                    f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}")
618
                amount = trade.amount
1✔
619

620
            if amount == 0.0:
1✔
621
                logger.info("Amount to exit is 0.0 due to exchange limits - not exiting.")
1✔
622
                return
1✔
623

624
            remaining = (trade.amount - amount) * current_exit_rate
1✔
625
            if remaining < min_exit_stake:
1✔
626
                logger.info(f"Remaining amount of {remaining} would be smaller "
1✔
627
                            f"than the minimum of {min_exit_stake}.")
628
                return
1✔
629

630
            self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple(
1✔
631
                exit_type=ExitType.PARTIAL_EXIT), sub_trade_amt=amount)
632

633
    def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool:
1✔
634
        """
635
        Checks depth of market before executing a buy
636
        """
637
        conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0)
1✔
638
        logger.info(f"Checking depth of market for {pair} ...")
1✔
639
        order_book = self.exchange.fetch_l2_order_book(pair, 1000)
1✔
640
        order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
1✔
641
        order_book_bids = order_book_data_frame['b_size'].sum()
1✔
642
        order_book_asks = order_book_data_frame['a_size'].sum()
1✔
643

644
        entry_side = order_book_bids if side == SignalDirection.LONG else order_book_asks
1✔
645
        exit_side = order_book_asks if side == SignalDirection.LONG else order_book_bids
1✔
646
        bids_ask_delta = entry_side / exit_side
1✔
647

648
        bids = f"Bids: {order_book_bids}"
1✔
649
        asks = f"Asks: {order_book_asks}"
1✔
650
        delta = f"Delta: {bids_ask_delta}"
1✔
651

652
        logger.info(
1✔
653
            f"{bids}, {asks}, {delta}, Direction: {side.value}"
654
            f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, "
655
            f"Immediate Bid Quantity: {order_book['bids'][0][1]}, "
656
            f"Immediate Ask Quantity: {order_book['asks'][0][1]}."
657
        )
658
        if bids_ask_delta >= conf_bids_to_ask_delta:
1✔
659
            logger.info(f"Bids to asks delta for {pair} DOES satisfy condition.")
1✔
660
            return True
1✔
661
        else:
662
            logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
1✔
663
            return False
1✔
664

665
    def execute_entry(
1✔
666
        self,
667
        pair: str,
668
        stake_amount: float,
669
        price: Optional[float] = None,
670
        *,
671
        is_short: bool = False,
672
        ordertype: Optional[str] = None,
673
        enter_tag: Optional[str] = None,
674
        trade: Optional[Trade] = None,
675
        order_adjust: bool = False,
676
        leverage_: Optional[float] = None,
677
    ) -> bool:
678
        """
679
        Executes a limit buy for the given pair
680
        :param pair: pair for which we want to create a LIMIT_BUY
681
        :param stake_amount: amount of stake-currency for the pair
682
        :return: True if a buy order is created, false if it fails.
683
        """
684
        time_in_force = self.strategy.order_time_in_force['entry']
1✔
685

686
        side: BuySell = 'sell' if is_short else 'buy'
1✔
687
        name = 'Short' if is_short else 'Long'
1✔
688
        trade_side: LongShort = 'short' if is_short else 'long'
1✔
689
        pos_adjust = trade is not None
1✔
690

691
        enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake(
1✔
692
            pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_)
693

694
        if not stake_amount:
1✔
695
            return False
1✔
696

697
        msg = (f"Position adjust: about to create a new order for {pair} with stake: "
1✔
698
               f"{stake_amount} for {trade}" if pos_adjust
699
               else
700
               f"{name} signal found: about create a new trade for {pair} with stake_amount: "
701
               f"{stake_amount} ...")
702
        logger.info(msg)
1✔
703
        amount = (stake_amount / enter_limit_requested) * leverage
1✔
704
        order_type = ordertype or self.strategy.order_types['entry']
1✔
705

706
        if not pos_adjust and not strategy_safe_wrapper(
1✔
707
                self.strategy.confirm_trade_entry, default_retval=True)(
708
                pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
709
                time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
710
                entry_tag=enter_tag, side=trade_side):
711
            logger.info(f"User denied entry for {pair}.")
1✔
712
            return False
1✔
713
        order = self.exchange.create_order(
1✔
714
            pair=pair,
715
            ordertype=order_type,
716
            side=side,
717
            amount=amount,
718
            rate=enter_limit_requested,
719
            reduceOnly=False,
720
            time_in_force=time_in_force,
721
            leverage=leverage
722
        )
723
        order_obj = Order.parse_from_ccxt_object(order, pair, side)
1✔
724
        order_id = order['id']
1✔
725
        order_status = order.get('status')
1✔
726
        logger.info(f"Order #{order_id} was created for {pair} and status is {order_status}.")
1✔
727

728
        # we assume the order is executed at the price requested
729
        enter_limit_filled_price = enter_limit_requested
1✔
730
        amount_requested = amount
1✔
731

732
        if order_status == 'expired' or order_status == 'rejected':
1✔
733

734
            # return false if the order is not filled
735
            if float(order['filled']) == 0:
1✔
736
                logger.warning(f'{name} {time_in_force} order with time in force {order_type} '
1✔
737
                               f'for {pair} is {order_status} by {self.exchange.name}.'
738
                               ' zero amount is fulfilled.')
739
                return False
1✔
740
            else:
741
                # the order is partially fulfilled
742
                # in case of IOC orders we can check immediately
743
                # if the order is fulfilled fully or partially
744
                logger.warning('%s %s order with time in force %s for %s is %s by %s.'
1✔
745
                               ' %s amount fulfilled out of %s (%s remaining which is canceled).',
746
                               name, time_in_force, order_type, pair, order_status,
747
                               self.exchange.name, order['filled'], order['amount'],
748
                               order['remaining']
749
                               )
750
                amount = safe_value_fallback(order, 'filled', 'amount')
1✔
751
                enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
1✔
752

753
        # in case of FOK the order may be filled immediately and fully
754
        elif order_status == 'closed':
1✔
755
            amount = safe_value_fallback(order, 'filled', 'amount')
1✔
756
            enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
1✔
757

758
        # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
759
        fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
1✔
760
        base_currency = self.exchange.get_pair_base_currency(pair)
1✔
761
        open_date = datetime.now(timezone.utc)
1✔
762

763
        # This is a new trade
764
        if trade is None:
1✔
765
            funding_fees = 0.0
1✔
766
            try:
1✔
767
                funding_fees = self.exchange.get_funding_fees(
1✔
768
                    pair=pair, amount=amount, is_short=is_short, open_date=open_date)
769
            except ExchangeError:
1✔
770
                logger.warning("Could not find funding fee.")
1✔
771

772
            trade = Trade(
1✔
773
                pair=pair,
774
                base_currency=base_currency,
775
                stake_currency=self.config['stake_currency'],
776
                stake_amount=stake_amount,
777
                amount=amount,
778
                is_open=True,
779
                amount_requested=amount_requested,
780
                fee_open=fee,
781
                fee_close=fee,
782
                open_rate=enter_limit_filled_price,
783
                open_rate_requested=enter_limit_requested,
784
                open_date=open_date,
785
                exchange=self.exchange.id,
786
                open_order_id=order_id,
787
                strategy=self.strategy.get_strategy_name(),
788
                enter_tag=enter_tag,
789
                timeframe=timeframe_to_minutes(self.config['timeframe']),
790
                leverage=leverage,
791
                is_short=is_short,
792
                trading_mode=self.trading_mode,
793
                funding_fees=funding_fees,
794
                amount_precision=self.exchange.get_precision_amount(pair),
795
                price_precision=self.exchange.get_precision_price(pair),
796
                precision_mode=self.exchange.precisionMode,
797
                contract_size=self.exchange.get_contract_size(pair),
798
            )
799
        else:
800
            # This is additional buy, we reset fee_open_currency so timeout checking can work
801
            trade.is_open = True
1✔
802
            trade.fee_open_currency = None
1✔
803
            trade.open_rate_requested = enter_limit_requested
1✔
804
            trade.open_order_id = order_id
1✔
805

806
        trade.orders.append(order_obj)
1✔
807
        trade.recalc_trade_from_orders()
1✔
808
        Trade.query.session.add(trade)
1✔
809
        Trade.commit()
1✔
810

811
        # Updating wallets
812
        self.wallets.update()
1✔
813

814
        self._notify_enter(trade, order_obj, order_type, sub_trade=pos_adjust)
1✔
815

816
        if pos_adjust:
1✔
817
            if order_status == 'closed':
1✔
818
                logger.info(f"DCA order closed, trade should be up to date: {trade}")
1✔
819
                trade = self.cancel_stoploss_on_exchange(trade)
1✔
820
            else:
821
                logger.info(f"DCA order {order_status}, will wait for resolution: {trade}")
1✔
822

823
        # Update fees if order is non-opened
824
        if order_status in constants.NON_OPEN_EXCHANGE_STATES:
1✔
825
            self.update_trade_state(trade, order_id, order)
1✔
826

827
        return True
1✔
828

829
    def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade:
1✔
830
        # First cancelling stoploss on exchange ...
831
        if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
1✔
832
            try:
1✔
833
                logger.info(f"Canceling stoploss on exchange for {trade}")
1✔
834
                co = self.exchange.cancel_stoploss_order_with_result(
1✔
835
                    trade.stoploss_order_id, trade.pair, trade.amount)
836
                trade.update_order(co)
1✔
837
                # Reset stoploss order id.
838
                trade.stoploss_order_id = None
1✔
839
            except InvalidOrderException:
1✔
840
                logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
1✔
841
        return trade
1✔
842

843
    def get_valid_enter_price_and_stake(
1✔
844
        self, pair: str, price: Optional[float], stake_amount: float,
845
        trade_side: LongShort,
846
        entry_tag: Optional[str],
847
        trade: Optional[Trade],
848
        order_adjust: bool,
849
        leverage_: Optional[float],
850
    ) -> Tuple[float, float, float]:
851

852
        if price:
1✔
853
            enter_limit_requested = price
1✔
854
        else:
855
            # Calculate price
856
            enter_limit_requested = self.exchange.get_rate(
1✔
857
                pair, side='entry', is_short=(trade_side == 'short'), refresh=True)
858
        if not order_adjust:
1✔
859
            # Don't call custom_entry_price in order-adjust scenario
860
            custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
1✔
861
                                                       default_retval=enter_limit_requested)(
862
                pair=pair, current_time=datetime.now(timezone.utc),
863
                proposed_rate=enter_limit_requested, entry_tag=entry_tag,
864
                side=trade_side,
865
            )
866

867
            enter_limit_requested = self.get_valid_price(custom_entry_price, enter_limit_requested)
1✔
868

869
        if not enter_limit_requested:
1✔
870
            raise PricingError('Could not determine entry price.')
1✔
871

872
        if self.trading_mode != TradingMode.SPOT and trade is None:
1✔
873
            max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
1✔
874
            if leverage_:
1✔
875
                leverage = leverage_
1✔
876
            else:
877
                leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
1✔
878
                    pair=pair,
879
                    current_time=datetime.now(timezone.utc),
880
                    current_rate=enter_limit_requested,
881
                    proposed_leverage=1.0,
882
                    max_leverage=max_leverage,
883
                    side=trade_side, entry_tag=entry_tag,
884
                )
885
            # Cap leverage between 1.0 and max_leverage.
886
            leverage = min(max(leverage, 1.0), max_leverage)
1✔
887
        else:
888
            # Changing leverage currently not possible
889
            leverage = trade.leverage if trade else 1.0
1✔
890

891
        # Min-stake-amount should actually include Leverage - this way our "minimal"
892
        # stake- amount might be higher than necessary.
893
        # We do however also need min-stake to determine leverage, therefore this is ignored as
894
        # edge-case for now.
895
        min_stake_amount = self.exchange.get_min_pair_stake_amount(
1✔
896
            pair, enter_limit_requested, self.strategy.stoploss, leverage)
897
        max_stake_amount = self.exchange.get_max_pair_stake_amount(
1✔
898
            pair, enter_limit_requested, leverage)
899

900
        if not self.edge and trade is None:
1✔
901
            stake_available = self.wallets.get_available_stake_amount()
1✔
902
            stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
1✔
903
                                                 default_retval=stake_amount)(
904
                pair=pair, current_time=datetime.now(timezone.utc),
905
                current_rate=enter_limit_requested, proposed_stake=stake_amount,
906
                min_stake=min_stake_amount, max_stake=min(max_stake_amount, stake_available),
907
                leverage=leverage, entry_tag=entry_tag, side=trade_side
908
            )
909

910
        stake_amount = self.wallets.validate_stake_amount(
1✔
911
            pair=pair,
912
            stake_amount=stake_amount,
913
            min_stake_amount=min_stake_amount,
914
            max_stake_amount=max_stake_amount,
915
            trade_amount=trade.stake_amount if trade else None,
916
        )
917

918
        return enter_limit_requested, stake_amount, leverage
1✔
919

920
    def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str] = None,
1✔
921
                      fill: bool = False, sub_trade: bool = False) -> None:
922
        """
923
        Sends rpc notification when a entry order occurred.
924
        """
925
        msg_type = RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY
1✔
926
        open_rate = order.safe_price
1✔
927

928
        if open_rate is None:
1✔
929
            open_rate = trade.open_rate
1✔
930

931
        current_rate = trade.open_rate_requested
1✔
932
        if self.dataprovider.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
1✔
933
            current_rate = self.exchange.get_rate(
1✔
934
                trade.pair, side='entry', is_short=trade.is_short, refresh=False)
935

936
        msg = {
1✔
937
            'trade_id': trade.id,
938
            'type': msg_type,
939
            'buy_tag': trade.enter_tag,
940
            'enter_tag': trade.enter_tag,
941
            'exchange': trade.exchange.capitalize(),
942
            'pair': trade.pair,
943
            'leverage': trade.leverage if trade.leverage else None,
944
            'direction': 'Short' if trade.is_short else 'Long',
945
            'limit': open_rate,  # Deprecated (?)
946
            'open_rate': open_rate,
947
            'order_type': order_type,
948
            'stake_amount': trade.stake_amount,
949
            'stake_currency': self.config['stake_currency'],
950
            'fiat_currency': self.config.get('fiat_display_currency', None),
951
            'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount),
952
            'open_date': trade.open_date or datetime.utcnow(),
953
            'current_rate': current_rate,
954
            'sub_trade': sub_trade,
955
        }
956

957
        # Send the message
958
        self.rpc.send_msg(msg)
1✔
959

960
    def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str,
1✔
961
                             sub_trade: bool = False) -> None:
962
        """
963
        Sends rpc notification when a entry order cancel occurred.
964
        """
965
        current_rate = self.exchange.get_rate(
1✔
966
            trade.pair, side='entry', is_short=trade.is_short, refresh=False)
967

968
        msg = {
1✔
969
            'trade_id': trade.id,
970
            'type': RPCMessageType.ENTRY_CANCEL,
971
            'buy_tag': trade.enter_tag,
972
            'enter_tag': trade.enter_tag,
973
            'exchange': trade.exchange.capitalize(),
974
            'pair': trade.pair,
975
            'leverage': trade.leverage,
976
            'direction': 'Short' if trade.is_short else 'Long',
977
            'limit': trade.open_rate,
978
            'order_type': order_type,
979
            'stake_amount': trade.stake_amount,
980
            'stake_currency': self.config['stake_currency'],
981
            'fiat_currency': self.config.get('fiat_display_currency', None),
982
            'amount': trade.amount,
983
            'open_date': trade.open_date,
984
            'current_rate': current_rate,
985
            'reason': reason,
986
            'sub_trade': sub_trade,
987
        }
988

989
        # Send the message
990
        self.rpc.send_msg(msg)
1✔
991

992
#
993
# SELL / exit positions / close trades logic and methods
994
#
995

996
    def exit_positions(self, trades: List[Trade]) -> int:
1✔
997
        """
998
        Tries to execute exit orders for open trades (positions)
999
        """
1000
        trades_closed = 0
1✔
1001
        for trade in trades:
1✔
1002
            try:
1✔
1003

1004
                if (self.strategy.order_types.get('stoploss_on_exchange') and
1✔
1005
                        self.handle_stoploss_on_exchange(trade)):
1006
                    trades_closed += 1
1✔
1007
                    Trade.commit()
1✔
1008
                    continue
1✔
1009
                # Check if we can sell our current pair
1010
                if trade.open_order_id is None and trade.is_open and self.handle_trade(trade):
1✔
1011
                    trades_closed += 1
1✔
1012

1013
            except DependencyException as exception:
1✔
1014
                logger.warning(f'Unable to exit trade {trade.pair}: {exception}')
1✔
1015

1016
        # Updating wallets if any trade occurred
1017
        if trades_closed:
1✔
1018
            self.wallets.update()
1✔
1019

1020
        return trades_closed
1✔
1021

1022
    def handle_trade(self, trade: Trade) -> bool:
1✔
1023
        """
1024
        Exits the current pair if the threshold is reached and updates the trade record.
1025
        :return: True if trade has been sold/exited_short, False otherwise
1026
        """
1027
        if not trade.is_open:
1✔
1028
            raise DependencyException(f'Attempt to handle closed trade: {trade}')
1✔
1029

1030
        logger.debug('Handling %s ...', trade)
1✔
1031

1032
        (enter, exit_) = (False, False)
1✔
1033
        exit_tag = None
1✔
1034
        exit_signal_type = "exit_short" if trade.is_short else "exit_long"
1✔
1035

1036
        if (self.config.get('use_exit_signal', True) or
1✔
1037
                self.config.get('ignore_roi_if_entry_signal', False)):
1038
            analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
1✔
1039
                                                                      self.strategy.timeframe)
1040

1041
            (enter, exit_, exit_tag) = self.strategy.get_exit_signal(
1✔
1042
                trade.pair,
1043
                self.strategy.timeframe,
1044
                analyzed_df,
1045
                is_short=trade.is_short
1046
            )
1047

1048
        logger.debug('checking exit')
1✔
1049
        exit_rate = self.exchange.get_rate(
1✔
1050
            trade.pair, side='exit', is_short=trade.is_short, refresh=True)
1051
        if self._check_and_execute_exit(trade, exit_rate, enter, exit_, exit_tag):
1✔
1052
            return True
1✔
1053

1054
        logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
1✔
1055
        return False
1✔
1056

1057
    def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
1✔
1058
                                enter: bool, exit_: bool, exit_tag: Optional[str]) -> bool:
1059
        """
1060
        Check and execute trade exit
1061
        """
1062
        exits: List[ExitCheckTuple] = self.strategy.should_exit(
1✔
1063
            trade,
1064
            exit_rate,
1065
            datetime.now(timezone.utc),
1066
            enter=enter,
1067
            exit_=exit_,
1068
            force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
1069
        )
1070
        for should_exit in exits:
1✔
1071
            if should_exit.exit_flag:
1✔
1072
                exit_tag1 = exit_tag if should_exit.exit_type == ExitType.EXIT_SIGNAL else None
1✔
1073
                logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
1✔
1074
                            f'{f" Tag: {exit_tag1}" if exit_tag1 is not None else ""}')
1075
                exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag1)
1✔
1076
                if exited:
1✔
1077
                    return True
1✔
1078
        return False
1✔
1079

1080
    def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
1✔
1081
        """
1082
        Abstracts creating stoploss orders from the logic.
1083
        Handles errors and updates the trade database object.
1084
        Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
1085
        :return: True if the order succeeded, and False in case of problems.
1086
        """
1087
        try:
1✔
1088
            stoploss_order = self.exchange.stoploss(
1✔
1089
                pair=trade.pair,
1090
                amount=trade.amount,
1091
                stop_price=stop_price,
1092
                order_types=self.strategy.order_types,
1093
                side=trade.exit_side,
1094
                leverage=trade.leverage
1095
            )
1096

1097
            order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss')
1✔
1098
            trade.orders.append(order_obj)
1✔
1099
            trade.stoploss_order_id = str(stoploss_order['id'])
1✔
1100
            trade.stoploss_last_update = datetime.now(timezone.utc)
1✔
1101
            return True
1✔
1102
        except InsufficientFundsError as e:
1✔
1103
            logger.warning(f"Unable to place stoploss order {e}.")
1✔
1104
            # Try to figure out what went wrong
1105
            self.handle_insufficient_funds(trade)
1✔
1106

1107
        except InvalidOrderException as e:
1✔
1108
            trade.stoploss_order_id = None
1✔
1109
            logger.error(f'Unable to place a stoploss order on exchange. {e}')
1✔
1110
            logger.warning('Exiting the trade forcefully')
1✔
1111
            self.execute_trade_exit(trade, stop_price, exit_check=ExitCheckTuple(
1✔
1112
                exit_type=ExitType.EMERGENCY_EXIT))
1113

1114
        except ExchangeError:
1✔
1115
            trade.stoploss_order_id = None
1✔
1116
            logger.exception('Unable to place a stoploss order on exchange.')
1✔
1117
        return False
1✔
1118

1119
    def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
1✔
1120
        """
1121
        Check if trade is fulfilled in which case the stoploss
1122
        on exchange should be added immediately if stoploss on exchange
1123
        is enabled.
1124
        # TODO: liquidation price always on exchange, even without stoploss_on_exchange
1125
        # Therefore fetching account liquidations for open pairs may make sense.
1126
        """
1127

1128
        logger.debug('Handling stoploss on exchange %s ...', trade)
1✔
1129

1130
        stoploss_order = None
1✔
1131

1132
        try:
1✔
1133
            # First we check if there is already a stoploss on exchange
1134
            stoploss_order = self.exchange.fetch_stoploss_order(
1✔
1135
                trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None
1136
        except InvalidOrderException as exception:
1✔
1137
            logger.warning('Unable to fetch stoploss order: %s', exception)
1✔
1138

1139
        if stoploss_order:
1✔
1140
            trade.update_order(stoploss_order)
1✔
1141

1142
        # We check if stoploss order is fulfilled
1143
        if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
1✔
1144
            trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
1✔
1145
            self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
1✔
1146
                                    stoploss_order=True)
1147
            self._notify_exit(trade, "stoploss", True)
1✔
1148
            self.handle_protections(trade.pair, trade.trade_direction)
1✔
1149
            return True
1✔
1150

1151
        if trade.open_order_id or not trade.is_open:
1✔
1152
            # Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case
1153
            # as the Amount on the exchange is tied up in another trade.
1154
            # The trade can be closed already (sell-order fill confirmation came in this iteration)
1155
            return False
1✔
1156

1157
        # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
1158
        if not stoploss_order:
1✔
1159
            stoploss = (
1✔
1160
                self.edge.stoploss(pair=trade.pair)
1161
                if self.edge else
1162
                trade.stop_loss_pct / trade.leverage
1163
            )
1164
            if trade.is_short:
1✔
1165
                stop_price = trade.open_rate * (1 - stoploss)
1✔
1166
            else:
1167
                stop_price = trade.open_rate * (1 + stoploss)
1✔
1168

1169
            if self.create_stoploss_order(trade=trade, stop_price=stop_price):
1✔
1170
                # The above will return False if the placement failed and the trade was force-sold.
1171
                # in which case the trade will be closed - which we must check below.
1172
                return False
1✔
1173

1174
        # If stoploss order is canceled for some reason we add it again
1175
        if (trade.is_open
1✔
1176
                and stoploss_order
1177
                and stoploss_order['status'] in ('canceled', 'cancelled')):
1178
            if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation):
1✔
1179
                return False
1✔
1180
            else:
1181
                logger.warning('Stoploss order was cancelled, but unable to recreate one.')
1✔
1182

1183
        # Finally we check if stoploss on exchange should be moved up because of trailing.
1184
        # Triggered Orders are now real orders - so don't replace stoploss anymore
1185
        if (
1✔
1186
            trade.is_open and stoploss_order
1187
            and stoploss_order.get('status_stop') != 'triggered'
1188
            and (self.config.get('trailing_stop', False)
1189
                 or self.config.get('use_custom_stoploss', False))
1190
        ):
1191
            # if trailing stoploss is enabled we check if stoploss value has changed
1192
            # in which case we cancel stoploss order and put another one with new
1193
            # value immediately
1194
            self.handle_trailing_stoploss_on_exchange(trade, stoploss_order)
1✔
1195

1196
        return False
1✔
1197

1198
    def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: Dict) -> None:
1✔
1199
        """
1200
        Check to see if stoploss on exchange should be updated
1201
        in case of trailing stoploss on exchange
1202
        :param trade: Corresponding Trade
1203
        :param order: Current on exchange stoploss order
1204
        :return: None
1205
        """
1206
        stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stoploss_or_liquidation)
1✔
1207

1208
        if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
1✔
1209
            # we check if the update is necessary
1210
            update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
1✔
1211
            upd_req = datetime.now(timezone.utc) - timedelta(seconds=update_beat)
1✔
1212
            if trade.stoploss_last_update_utc and upd_req >= trade.stoploss_last_update_utc:
1✔
1213
                # cancelling the current stoploss on exchange first
1214
                logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} "
1✔
1215
                            f"(orderid:{order['id']}) in order to add another one ...")
1216
                try:
1✔
1217
                    co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair,
1✔
1218
                                                                         trade.amount)
1219
                    trade.update_order(co)
1✔
1220
                except InvalidOrderException:
1✔
1221
                    logger.exception(f"Could not cancel stoploss order {order['id']} "
1✔
1222
                                     f"for pair {trade.pair}")
1223

1224
                # Create new stoploss order
1225
                if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
1✔
1226
                    logger.warning(f"Could not create trailing stoploss order "
1✔
1227
                                   f"for pair {trade.pair}.")
1228

1229
    def manage_open_orders(self) -> None:
1✔
1230
        """
1231
        Management of open orders on exchange. Unfilled orders might be cancelled if timeout
1232
        was met or replaced if there's a new candle and user has requested it.
1233
        Timeout setting takes priority over limit order adjustment request.
1234
        :return: None
1235
        """
1236
        for trade in Trade.get_open_order_trades():
1✔
1237
            try:
1✔
1238
                if not trade.open_order_id:
1✔
1239
                    continue
×
1240
                order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
1✔
1241
            except (ExchangeError):
1✔
1242
                logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
1✔
1243
                continue
1✔
1244

1245
            fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
1✔
1246
            not_closed = order['status'] == 'open' or fully_cancelled
1✔
1247
            order_obj = trade.select_order_by_order_id(trade.open_order_id)
1✔
1248

1249
            if not_closed:
1✔
1250
                if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
1✔
1251
                   trade, order_obj, datetime.now(timezone.utc))):
1252
                    self.handle_timedout_order(order, trade)
1✔
1253
                else:
1254
                    self.replace_order(order, order_obj, trade)
1✔
1255

1256
    def handle_timedout_order(self, order: Dict, trade: Trade) -> None:
1✔
1257
        """
1258
        Check if current analyzed order timed out and cancel if necessary.
1259
        :param order: Order dict grabbed with exchange.fetch_order()
1260
        :param trade: Trade object.
1261
        :return: None
1262
        """
1263
        if order['side'] == trade.entry_side:
1✔
1264
            self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
1✔
1265
        else:
1266
            canceled = self.handle_cancel_exit(
1✔
1267
                trade, order, constants.CANCEL_REASON['TIMEOUT'])
1268
            canceled_count = trade.get_exit_order_count()
1✔
1269
            max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
1✔
1270
            if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
1✔
1271
                logger.warning(f'Emergency exiting trade {trade}, as the exit order '
1✔
1272
                               f'timed out {max_timeouts} times.')
1273
                try:
1✔
1274
                    self.execute_trade_exit(
1✔
1275
                        trade, order['price'],
1276
                        exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT))
1277
                except DependencyException as exception:
1✔
1278
                    logger.warning(
1✔
1279
                        f'Unable to emergency sell trade {trade.pair}: {exception}')
1280

1281
    def replace_order(self, order: Dict, order_obj: Optional[Order], trade: Trade) -> None:
1✔
1282
        """
1283
        Check if current analyzed entry order should be replaced or simply cancelled.
1284
        To simply cancel the existing order(no replacement) adjust_entry_price() should return None
1285
        To maintain existing order adjust_entry_price() should return order_obj.price
1286
        To replace existing order adjust_entry_price() should return desired price for limit order
1287
        :param order: Order dict grabbed with exchange.fetch_order()
1288
        :param order_obj: Order object.
1289
        :param trade: Trade object.
1290
        :return: None
1291
        """
1292
        analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
1✔
1293
                                                                  self.strategy.timeframe)
1294
        latest_candle_open_date = analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None
1✔
1295
        latest_candle_close_date = timeframe_to_next_date(self.strategy.timeframe,
1✔
1296
                                                          latest_candle_open_date)
1297
        # Check if new candle
1298
        if order_obj and latest_candle_close_date > order_obj.order_date_utc:
1✔
1299
            # New candle
1300
            proposed_rate = self.exchange.get_rate(
1✔
1301
                trade.pair, side='entry', is_short=trade.is_short, refresh=True)
1302
            adjusted_entry_price = strategy_safe_wrapper(self.strategy.adjust_entry_price,
1✔
1303
                                                         default_retval=order_obj.price)(
1304
                trade=trade, order=order_obj, pair=trade.pair,
1305
                current_time=datetime.now(timezone.utc), proposed_rate=proposed_rate,
1306
                current_order_rate=order_obj.price, entry_tag=trade.enter_tag,
1307
                side=trade.entry_side)
1308

1309
            replacing = True
1✔
1310
            cancel_reason = constants.CANCEL_REASON['REPLACE']
1✔
1311
            if not adjusted_entry_price:
1✔
1312
                replacing = False
1✔
1313
                cancel_reason = constants.CANCEL_REASON['USER_CANCEL']
1✔
1314
            if order_obj.price != adjusted_entry_price:
1✔
1315
                # cancel existing order if new price is supplied or None
1316
                self.handle_cancel_enter(trade, order, cancel_reason,
1✔
1317
                                         replacing=replacing)
1318
                if adjusted_entry_price:
1✔
1319
                    # place new order only if new price is supplied
1320
                    self.execute_entry(
1✔
1321
                        pair=trade.pair,
1322
                        stake_amount=(order_obj.remaining * order_obj.price / trade.leverage),
1323
                        price=adjusted_entry_price,
1324
                        trade=trade,
1325
                        is_short=trade.is_short,
1326
                        order_adjust=True,
1327
                    )
1328

1329
    def cancel_all_open_orders(self) -> None:
1✔
1330
        """
1331
        Cancel all orders that are currently open
1332
        :return: None
1333
        """
1334

1335
        for trade in Trade.get_open_order_trades():
1✔
1336
            try:
1✔
1337
                order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
1✔
1338
            except (ExchangeError):
1✔
1339
                logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
1✔
1340
                continue
1✔
1341

1342
            if order['side'] == trade.entry_side:
1✔
1343
                self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
1✔
1344

1345
            elif order['side'] == trade.exit_side:
1✔
1346
                self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
1✔
1347
        Trade.commit()
1✔
1348

1349
    def handle_cancel_enter(
1✔
1350
            self, trade: Trade, order: Dict, reason: str,
1351
            replacing: Optional[bool] = False
1352
    ) -> bool:
1353
        """
1354
        entry cancel - cancel order
1355
        :param replacing: Replacing order - prevent trade deletion.
1356
        :return: True if trade was fully cancelled
1357
        """
1358
        was_trade_fully_canceled = False
1✔
1359
        side = trade.entry_side.capitalize()
1✔
1360

1361
        # Cancelled orders may have the status of 'canceled' or 'closed'
1362
        if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
1✔
1363
            filled_val: float = order.get('filled', 0.0) or 0.0
1✔
1364
            filled_stake = filled_val * trade.open_rate
1✔
1365
            minstake = self.exchange.get_min_pair_stake_amount(
1✔
1366
                trade.pair, trade.open_rate, self.strategy.stoploss)
1367

1368
            if filled_val > 0 and minstake and filled_stake < minstake:
1✔
1369
                logger.warning(
1✔
1370
                    f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
1371
                    f"as the filled amount of {filled_val} would result in an unexitable trade.")
1372
                return False
1✔
1373
            corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
1✔
1374
                                                            trade.amount)
1375
            # Avoid race condition where the order could not be cancelled coz its already filled.
1376
            # Simply bailing here is the only safe way - as this order will then be
1377
            # handled in the next iteration.
1378
            if corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES:
1✔
1379
                logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.")
1✔
1380
                return False
1✔
1381
        else:
1382
            # Order was cancelled already, so we can reuse the existing dict
1383
            corder = order
1✔
1384
            reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
1✔
1385

1386
        logger.info('%s order %s for %s.', side, reason, trade)
1✔
1387

1388
        # Using filled to determine the filled amount
1389
        filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
1✔
1390
        if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
1✔
1391
            # if trade is not partially completed and it's the only order, just delete the trade
1392
            open_order_count = len([order for order in trade.orders if order.status == 'open'])
1✔
1393
            if open_order_count <= 1 and trade.nr_of_successful_entries == 0 and not replacing:
1✔
1394
                logger.info(f'{side} order fully cancelled. Removing {trade} from database.')
1✔
1395
                trade.delete()
1✔
1396
                was_trade_fully_canceled = True
1✔
1397
                reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
1✔
1398
            else:
1399
                self.update_trade_state(trade, trade.open_order_id, corder)
1✔
1400
                trade.open_order_id = None
1✔
1401
                logger.info(f'{side} Order timeout for {trade}.')
1✔
1402
        else:
1403
            # update_trade_state (and subsequently recalc_trade_from_orders) will handle updates
1404
            # to the trade object
1405
            self.update_trade_state(trade, trade.open_order_id, corder)
1✔
1406
            trade.open_order_id = None
1✔
1407

1408
            logger.info(f'Partial {trade.entry_side} order timeout for {trade}.')
1✔
1409
            reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
1✔
1410

1411
        self.wallets.update()
1✔
1412
        self._notify_enter_cancel(trade, order_type=self.strategy.order_types['entry'],
1✔
1413
                                  reason=reason)
1414
        return was_trade_fully_canceled
1✔
1415

1416
    def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool:
1✔
1417
        """
1418
        exit order cancel - cancel order and update trade
1419
        :return: True if exit order was cancelled, false otherwise
1420
        """
1421
        cancelled = False
1✔
1422
        # Cancelled orders may have the status of 'canceled' or 'closed'
1423
        if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
1✔
1424
            filled_val: float = order.get('filled', 0.0) or 0.0
1✔
1425
            filled_rem_stake = trade.stake_amount - filled_val * trade.open_rate
1✔
1426
            minstake = self.exchange.get_min_pair_stake_amount(
1✔
1427
                trade.pair, trade.open_rate, self.strategy.stoploss)
1428
            # Double-check remaining amount
1429
            if filled_val > 0:
1✔
1430
                reason = constants.CANCEL_REASON['PARTIALLY_FILLED']
1✔
1431
                if minstake and filled_rem_stake < minstake:
1✔
1432
                    logger.warning(
1✔
1433
                        f"Order {trade.open_order_id} for {trade.pair} not cancelled, as "
1434
                        f"the filled amount of {filled_val} would result in an unexitable trade.")
1435
                    reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
1✔
1436

1437
                    self._notify_exit_cancel(
1✔
1438
                        trade,
1439
                        order_type=self.strategy.order_types['exit'],
1440
                        reason=reason, order_id=order['id'],
1441
                        sub_trade=trade.amount != order['amount']
1442
                    )
1443
                    return False
1✔
1444

1445
            try:
1✔
1446
                co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
1✔
1447
                                                            trade.amount)
1448
            except InvalidOrderException:
1✔
1449
                logger.exception(
1✔
1450
                    f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
1451
                return False
1✔
1452
            trade.close_rate = None
1✔
1453
            trade.close_rate_requested = None
1✔
1454
            trade.close_profit = None
1✔
1455
            trade.close_profit_abs = None
1✔
1456
            # Set exit_reason for fill message
1457
            exit_reason_prev = trade.exit_reason
1✔
1458
            trade.exit_reason = trade.exit_reason + f", {reason}" if trade.exit_reason else reason
1✔
1459
            self.update_trade_state(trade, trade.open_order_id, co)
1✔
1460
            # Order might be filled above in odd timing issues.
1461
            if co.get('status') in ('canceled', 'cancelled'):
1✔
1462
                trade.exit_reason = None
1✔
1463
                trade.open_order_id = None
1✔
1464
            else:
1465
                trade.exit_reason = exit_reason_prev
1✔
1466

1467
            logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
1✔
1468
            cancelled = True
1✔
1469
        else:
1470
            reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
1✔
1471
            logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
1✔
1472
            self.update_trade_state(trade, trade.open_order_id, order)
1✔
1473
            trade.open_order_id = None
1✔
1474

1475
        self._notify_exit_cancel(
1✔
1476
            trade,
1477
            order_type=self.strategy.order_types['exit'],
1478
            reason=reason, order_id=order['id'], sub_trade=trade.amount != order['amount']
1479
        )
1480
        return cancelled
1✔
1481

1482
    def _safe_exit_amount(self, trade: Trade, pair: str, amount: float) -> float:
1✔
1483
        """
1484
        Get sellable amount.
1485
        Should be trade.amount - but will fall back to the available amount if necessary.
1486
        This should cover cases where get_real_amount() was not able to update the amount
1487
        for whatever reason.
1488
        :param trade: Trade we're working with
1489
        :param pair: Pair we're trying to sell
1490
        :param amount: amount we expect to be available
1491
        :return: amount to sell
1492
        :raise: DependencyException: if available balance is not within 2% of the available amount.
1493
        """
1494
        # Update wallets to ensure amounts tied up in a stoploss is now free!
1495
        self.wallets.update()
1✔
1496
        if self.trading_mode == TradingMode.FUTURES:
1✔
1497
            return amount
1✔
1498

1499
        trade_base_currency = self.exchange.get_pair_base_currency(pair)
1✔
1500
        wallet_amount = self.wallets.get_free(trade_base_currency)
1✔
1501
        logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}")
1✔
1502
        if wallet_amount >= amount:
1✔
1503
            # A safe exit amount isn't needed for futures, you can just exit/close the position
1504
            return amount
1✔
1505
        elif wallet_amount > amount * 0.98:
1✔
1506
            logger.info(f"{pair} - Falling back to wallet-amount {wallet_amount} -> {amount}.")
1✔
1507
            trade.amount = wallet_amount
1✔
1508
            return wallet_amount
1✔
1509
        else:
1510
            raise DependencyException(
1✔
1511
                f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}")
1512

1513
    def execute_trade_exit(
1✔
1514
            self,
1515
            trade: Trade,
1516
            limit: float,
1517
            exit_check: ExitCheckTuple,
1518
            *,
1519
            exit_tag: Optional[str] = None,
1520
            ordertype: Optional[str] = None,
1521
            sub_trade_amt: float = None,
1522
    ) -> bool:
1523
        """
1524
        Executes a trade exit for the given trade and limit
1525
        :param trade: Trade instance
1526
        :param limit: limit rate for the sell order
1527
        :param exit_check: CheckTuple with signal and reason
1528
        :return: True if it succeeds False
1529
        """
1530
        try:
1✔
1531
            trade.funding_fees = self.exchange.get_funding_fees(
1✔
1532
                pair=trade.pair,
1533
                amount=trade.amount,
1534
                is_short=trade.is_short,
1535
                open_date=trade.date_last_filled_utc,
1536
            )
1537
        except ExchangeError:
1✔
1538
            logger.warning("Could not update funding fee.")
1✔
1539

1540
        exit_type = 'exit'
1✔
1541
        exit_reason = exit_tag or exit_check.exit_reason
1✔
1542
        if exit_check.exit_type in (
1✔
1543
                ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION):
1544
            exit_type = 'stoploss'
1✔
1545

1546
        # set custom_exit_price if available
1547
        proposed_limit_rate = limit
1✔
1548
        current_profit = trade.calc_profit_ratio(limit)
1✔
1549
        custom_exit_price = strategy_safe_wrapper(self.strategy.custom_exit_price,
1✔
1550
                                                  default_retval=proposed_limit_rate)(
1551
            pair=trade.pair, trade=trade,
1552
            current_time=datetime.now(timezone.utc),
1553
            proposed_rate=proposed_limit_rate, current_profit=current_profit,
1554
            exit_tag=exit_reason)
1555

1556
        limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
1✔
1557

1558
        # First cancelling stoploss on exchange ...
1559
        trade = self.cancel_stoploss_on_exchange(trade)
1✔
1560

1561
        order_type = ordertype or self.strategy.order_types[exit_type]
1✔
1562
        if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
1✔
1563
            # Emergency sells (default to market!)
1564
            order_type = self.strategy.order_types.get("emergency_exit", "market")
1✔
1565

1566
        amount = self._safe_exit_amount(trade, trade.pair, sub_trade_amt or trade.amount)
1✔
1567
        time_in_force = self.strategy.order_time_in_force['exit']
1✔
1568

1569
        if (exit_check.exit_type != ExitType.LIQUIDATION
1✔
1570
                and not sub_trade_amt
1571
                and not strategy_safe_wrapper(
1572
                    self.strategy.confirm_trade_exit, default_retval=True)(
1573
                    pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
1574
                    time_in_force=time_in_force, exit_reason=exit_reason,
1575
                    sell_reason=exit_reason,  # sellreason -> compatibility
1576
                    current_time=datetime.now(timezone.utc))):
1577
            logger.info(f"User denied exit for {trade.pair}.")
1✔
1578
            return False
1✔
1579

1580
        try:
1✔
1581
            # Execute sell and update trade record
1582
            order = self.exchange.create_order(
1✔
1583
                pair=trade.pair,
1584
                ordertype=order_type,
1585
                side=trade.exit_side,
1586
                amount=amount,
1587
                rate=limit,
1588
                leverage=trade.leverage,
1589
                reduceOnly=self.trading_mode == TradingMode.FUTURES,
1590
                time_in_force=time_in_force
1591
            )
1592
        except InsufficientFundsError as e:
1✔
1593
            logger.warning(f"Unable to place order {e}.")
1✔
1594
            # Try to figure out what went wrong
1595
            self.handle_insufficient_funds(trade)
1✔
1596
            return False
1✔
1597

1598
        order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side)
1✔
1599
        trade.orders.append(order_obj)
1✔
1600

1601
        trade.open_order_id = order['id']
1✔
1602
        trade.exit_order_status = ''
1✔
1603
        trade.close_rate_requested = limit
1✔
1604
        trade.exit_reason = exit_reason
1✔
1605

1606
        self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
1✔
1607
        # In case of market sell orders the order can be closed immediately
1608
        if order.get('status', 'unknown') in ('closed', 'expired'):
1✔
1609
            self.update_trade_state(trade, trade.open_order_id, order)
1✔
1610
        Trade.commit()
1✔
1611

1612
        return True
1✔
1613

1614
    def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False,
1✔
1615
                     sub_trade: bool = False, order: Order = None) -> None:
1616
        """
1617
        Sends rpc notification when a sell occurred.
1618
        """
1619
        # Use cached rates here - it was updated seconds ago.
1620
        current_rate = self.exchange.get_rate(
1✔
1621
            trade.pair, side='exit', is_short=trade.is_short, refresh=False) if not fill else None
1622

1623
        # second condition is for mypy only; order will always be passed during sub trade
1624
        if sub_trade and order is not None:
1✔
1625
            amount = order.safe_filled if fill else order.amount
1✔
1626
            order_rate: float = order.safe_price
1✔
1627

1628
            profit = trade.calc_profit(rate=order_rate, amount=amount, open_rate=trade.open_rate)
1✔
1629
            profit_ratio = trade.calc_profit_ratio(order_rate, amount, trade.open_rate)
1✔
1630
        else:
1631
            order_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
1✔
1632
            profit = trade.calc_profit(rate=order_rate) + (0.0 if fill else trade.realized_profit)
1✔
1633
            profit_ratio = trade.calc_profit_ratio(order_rate)
1✔
1634
            amount = trade.amount
1✔
1635
        gain = "profit" if profit_ratio > 0 else "loss"
1✔
1636

1637
        msg = {
1✔
1638
            'type': (RPCMessageType.EXIT_FILL if fill
1639
                     else RPCMessageType.EXIT),
1640
            'trade_id': trade.id,
1641
            'exchange': trade.exchange.capitalize(),
1642
            'pair': trade.pair,
1643
            'leverage': trade.leverage,
1644
            'direction': 'Short' if trade.is_short else 'Long',
1645
            'gain': gain,
1646
            'limit': order_rate,  # Deprecated
1647
            'order_rate': order_rate,
1648
            'order_type': order_type,
1649
            'amount': amount,
1650
            'open_rate': trade.open_rate,
1651
            'close_rate': order_rate,
1652
            'current_rate': current_rate,
1653
            'profit_amount': profit,
1654
            'profit_ratio': profit_ratio,
1655
            'buy_tag': trade.enter_tag,
1656
            'enter_tag': trade.enter_tag,
1657
            'sell_reason': trade.exit_reason,  # Deprecated
1658
            'exit_reason': trade.exit_reason,
1659
            'open_date': trade.open_date,
1660
            'close_date': trade.close_date or datetime.utcnow(),
1661
            'stake_amount': trade.stake_amount,
1662
            'stake_currency': self.config['stake_currency'],
1663
            'fiat_currency': self.config.get('fiat_display_currency'),
1664
            'sub_trade': sub_trade,
1665
            'cumulative_profit': trade.realized_profit,
1666
        }
1667

1668
        # Send the message
1669
        self.rpc.send_msg(msg)
1✔
1670

1671
    def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str,
1✔
1672
                            order_id: str, sub_trade: bool = False) -> None:
1673
        """
1674
        Sends rpc notification when a sell cancel occurred.
1675
        """
1676
        if trade.exit_order_status == reason:
1✔
1677
            return
1✔
1678
        else:
1679
            trade.exit_order_status = reason
1✔
1680

1681
        order = trade.select_order_by_order_id(order_id)
1✔
1682
        if not order:
1✔
1683
            raise DependencyException(
×
1684
                f"Order_obj not found for {order_id}. This should not have happened.")
1685

1686
        profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
1✔
1687
        profit_trade = trade.calc_profit(rate=profit_rate)
1✔
1688
        current_rate = self.exchange.get_rate(
1✔
1689
            trade.pair, side='exit', is_short=trade.is_short, refresh=False)
1690
        profit_ratio = trade.calc_profit_ratio(profit_rate)
1✔
1691
        gain = "profit" if profit_ratio > 0 else "loss"
1✔
1692

1693
        msg = {
1✔
1694
            'type': RPCMessageType.EXIT_CANCEL,
1695
            'trade_id': trade.id,
1696
            'exchange': trade.exchange.capitalize(),
1697
            'pair': trade.pair,
1698
            'leverage': trade.leverage,
1699
            'direction': 'Short' if trade.is_short else 'Long',
1700
            'gain': gain,
1701
            'limit': profit_rate or 0,
1702
            'order_type': order_type,
1703
            'amount': order.safe_amount_after_fee,
1704
            'open_rate': trade.open_rate,
1705
            'current_rate': current_rate,
1706
            'profit_amount': profit_trade,
1707
            'profit_ratio': profit_ratio,
1708
            'buy_tag': trade.enter_tag,
1709
            'enter_tag': trade.enter_tag,
1710
            'sell_reason': trade.exit_reason,  # Deprecated
1711
            'exit_reason': trade.exit_reason,
1712
            'open_date': trade.open_date,
1713
            'close_date': trade.close_date or datetime.now(timezone.utc),
1714
            'stake_currency': self.config['stake_currency'],
1715
            'fiat_currency': self.config.get('fiat_display_currency', None),
1716
            'reason': reason,
1717
            'sub_trade': sub_trade,
1718
            'stake_amount': trade.stake_amount,
1719
        }
1720

1721
        # Send the message
1722
        self.rpc.send_msg(msg)
1✔
1723

1724
#
1725
# Common update trade state methods
1726
#
1727

1728
    def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None,
1✔
1729
                           stoploss_order: bool = False, send_msg: bool = True) -> bool:
1730
        """
1731
        Checks trades with open orders and updates the amount if necessary
1732
        Handles closing both buy and sell orders.
1733
        :param trade: Trade object of the trade we're analyzing
1734
        :param order_id: Order-id of the order we're analyzing
1735
        :param action_order: Already acquired order object
1736
        :param send_msg: Send notification - should always be True except in "recovery" methods
1737
        :return: True if order has been cancelled without being filled partially, False otherwise
1738
        """
1739
        if not order_id:
1✔
1740
            logger.warning(f'Orderid for trade {trade} is empty.')
1✔
1741
            return False
1✔
1742

1743
        # Update trade with order values
1744
        logger.info(f'Found open order for {trade}')
1✔
1745
        try:
1✔
1746
            order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id,
1✔
1747
                                                                                trade.pair,
1748
                                                                                stoploss_order)
1749
        except InvalidOrderException as exception:
1✔
1750
            logger.warning('Unable to fetch order %s: %s', order_id, exception)
1✔
1751
            return False
1✔
1752

1753
        trade.update_order(order)
1✔
1754

1755
        if self.exchange.check_order_canceled_empty(order):
1✔
1756
            # Trade has been cancelled on exchange
1757
            # Handling of this will happen in check_handle_timedout.
1758
            return True
1✔
1759

1760
        order_obj = trade.select_order_by_order_id(order_id)
1✔
1761
        if not order_obj:
1✔
1762
            raise DependencyException(
×
1763
                f"Order_obj not found for {order_id}. This should not have happened.")
1764

1765
        self.handle_order_fee(trade, order_obj, order)
1✔
1766

1767
        trade.update_trade(order_obj)
1✔
1768

1769
        if order.get('status') in constants.NON_OPEN_EXCHANGE_STATES:
1✔
1770
            # If a entry order was closed, force update on stoploss on exchange
1771
            if order.get('side') == trade.entry_side:
1✔
1772
                trade = self.cancel_stoploss_on_exchange(trade)
1✔
1773
                if not self.edge:
1✔
1774
                    # TODO: should shorting/leverage be supported by Edge,
1775
                    # then this will need to be fixed.
1776
                    trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
1✔
1777
            if order.get('side') == trade.entry_side or trade.amount > 0:
1✔
1778
                # Must also run for partial exits
1779
                # TODO: Margin will need to use interest_rate as well.
1780
                # interest_rate = self.exchange.get_interest_rate()
1781
                trade.set_liquidation_price(self.exchange.get_liquidation_price(
1✔
1782
                    pair=trade.pair,
1783
                    open_rate=trade.open_rate,
1784
                    is_short=trade.is_short,
1785
                    amount=trade.amount,
1786
                    stake_amount=trade.stake_amount,
1787
                    wallet_balance=trade.stake_amount,
1788
                ))
1789

1790
            # Updating wallets when order is closed
1791
            self.wallets.update()
1✔
1792
        Trade.commit()
1✔
1793

1794
        self.order_close_notify(trade, order_obj, stoploss_order, send_msg)
1✔
1795

1796
        return False
1✔
1797

1798
    def order_close_notify(
1✔
1799
            self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool):
1800
        """send "fill" notifications"""
1801

1802
        sub_trade = not isclose(order.safe_amount_after_fee,
1✔
1803
                                trade.amount, abs_tol=constants.MATH_CLOSE_PREC)
1804
        if order.ft_order_side == trade.exit_side:
1✔
1805
            # Exit notification
1806
            if send_msg and not stoploss_order and not trade.open_order_id:
1✔
1807
                self._notify_exit(trade, '', fill=True, sub_trade=sub_trade, order=order)
1✔
1808
            if not trade.is_open:
1✔
1809
                self.handle_protections(trade.pair, trade.trade_direction)
1✔
1810
        elif send_msg and not trade.open_order_id and not stoploss_order:
1✔
1811
            # Enter fill
1812
            self._notify_enter(trade, order, fill=True, sub_trade=sub_trade)
1✔
1813

1814
    def handle_protections(self, pair: str, side: LongShort) -> None:
1✔
1815
        # Lock pair for one candle to prevent immediate rebuys
1816
        self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason='Auto lock')
1✔
1817
        prot_trig = self.protections.stop_per_pair(pair, side=side)
1✔
1818
        if prot_trig:
1✔
1819
            msg = {'type': RPCMessageType.PROTECTION_TRIGGER, }
1✔
1820
            msg.update(prot_trig.to_json())
1✔
1821
            self.rpc.send_msg(msg)
1✔
1822

1823
        prot_trig_glb = self.protections.global_stop(side=side)
1✔
1824
        if prot_trig_glb:
1✔
1825
            msg = {'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL, }
1✔
1826
            msg.update(prot_trig_glb.to_json())
1✔
1827
            self.rpc.send_msg(msg)
1✔
1828

1829
    def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
1✔
1830
                              amount: float, fee_abs: float, order_obj: Order) -> Optional[float]:
1831
        """
1832
        Applies the fee to amount (either from Order or from Trades).
1833
        Can eat into dust if more than the required asset is available.
1834
        Can't happen in Futures mode - where Fees are always in settlement currency,
1835
        never in base currency.
1836
        """
1837
        self.wallets.update()
1✔
1838
        amount_ = trade.amount
1✔
1839
        if order_obj.ft_order_side == trade.exit_side or order_obj.ft_order_side == 'stoploss':
1✔
1840
            # check against remaining amount!
1841
            amount_ = trade.amount - amount
1✔
1842

1843
        if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount_:
1✔
1844
            # Eat into dust if we own more than base currency
1845
            logger.info(f"Fee amount for {trade} was in base currency - "
1✔
1846
                        f"Eating Fee {fee_abs} into dust.")
1847
        elif fee_abs != 0:
1✔
1848
            logger.info(f"Applying fee on amount for {trade}, fee={fee_abs}.")
1✔
1849
            return fee_abs
1✔
1850
        return None
1✔
1851

1852
    def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None:
1✔
1853
        # Try update amount (binance-fix)
1854
        try:
1✔
1855
            fee_abs = self.get_real_amount(trade, order, order_obj)
1✔
1856
            if fee_abs is not None:
1✔
1857
                order_obj.ft_fee_base = fee_abs
1✔
1858
        except DependencyException as exception:
1✔
1859
            logger.warning("Could not update trade amount: %s", exception)
1✔
1860

1861
    def get_real_amount(self, trade: Trade, order: Dict, order_obj: Order) -> Optional[float]:
1✔
1862
        """
1863
        Detect and update trade fee.
1864
        Calls trade.update_fee() upon correct detection.
1865
        Returns modified amount if the fee was taken from the destination currency.
1866
        Necessary for exchanges which charge fees in base currency (e.g. binance)
1867
        :return: Absolute fee to apply for this order or None
1868
        """
1869
        # Init variables
1870
        order_amount = safe_value_fallback(order, 'filled', 'amount')
1✔
1871
        # Only run for closed orders
1872
        if trade.fee_updated(order.get('side', '')) or order['status'] == 'open':
1✔
1873
            return None
1✔
1874

1875
        trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
1✔
1876
        # use fee from order-dict if possible
1877
        if self.exchange.order_has_fee(order):
1✔
1878
            fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(
1✔
1879
                order['fee'], order['symbol'], order['cost'], order_obj.safe_filled)
1880
            logger.info(f"Fee for Trade {trade} [{order_obj.ft_order_side}]: "
1✔
1881
                        f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
1882
            if fee_rate is None or fee_rate < 0.02:
1✔
1883
                # Reject all fees that report as > 2%.
1884
                # These are most likely caused by a parsing bug in ccxt
1885
                # due to multiple trades (https://github.com/ccxt/ccxt/issues/8025)
1886
                trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
1✔
1887
                if trade_base_currency == fee_currency:
1✔
1888
                    # Apply fee to amount
1889
                    return self.apply_fee_conditional(trade, trade_base_currency,
1✔
1890
                                                      amount=order_amount, fee_abs=fee_cost,
1891
                                                      order_obj=order_obj)
1892
                return None
1✔
1893
        return self.fee_detection_from_trades(
1✔
1894
            trade, order, order_obj, order_amount, order.get('trades', []))
1895

1896
    def fee_detection_from_trades(self, trade: Trade, order: Dict, order_obj: Order,
1✔
1897
                                  order_amount: float, trades: List) -> Optional[float]:
1898
        """
1899
        fee-detection fallback to Trades.
1900
        Either uses provided trades list or the result of fetch_my_trades to get correct fee.
1901
        """
1902
        if not trades:
1✔
1903
            trades = self.exchange.get_trades_for_order(
1✔
1904
                self.exchange.get_order_id_conditional(order), trade.pair, order_obj.order_date)
1905

1906
        if len(trades) == 0:
1✔
1907
            logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
1✔
1908
            return None
1✔
1909
        fee_currency = None
1✔
1910
        amount = 0
1✔
1911
        fee_abs = 0.0
1✔
1912
        fee_cost = 0.0
1✔
1913
        trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
1✔
1914
        fee_rate_array: List[float] = []
1✔
1915
        for exectrade in trades:
1✔
1916
            amount += exectrade['amount']
1✔
1917
            if self.exchange.order_has_fee(exectrade):
1✔
1918
                # Prefer singular fee
1919
                fees = [exectrade['fee']]
1✔
1920
            else:
1921
                fees = exectrade.get('fees', [])
1✔
1922
            for fee in fees:
1✔
1923

1924
                fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(
1✔
1925
                    fee, exectrade['symbol'], exectrade['cost'], exectrade['amount']
1926
                )
1927
                fee_cost += fee_cost_
1✔
1928
                if fee_rate_ is not None:
1✔
1929
                    fee_rate_array.append(fee_rate_)
1✔
1930
                # only applies if fee is in quote currency!
1931
                if trade_base_currency == fee_currency:
1✔
1932
                    fee_abs += fee_cost_
1✔
1933
        # Ensure at least one trade was found:
1934
        if fee_currency:
1✔
1935
            # fee_rate should use mean
1936
            fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None
1✔
1937
            if fee_rate is not None and fee_rate < 0.02:
1✔
1938
                # Only update if fee-rate is < 2%
1939
                trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
1✔
1940
            else:
1941
                logger.warning(
1✔
1942
                    f"Not updating {order.get('side', '')}-fee - rate: {fee_rate}, {fee_currency}.")
1943

1944
        if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
1✔
1945
            # * Leverage could be a cause for this warning
1946
            logger.warning(f"Amount {amount} does not match amount {trade.amount}")
1✔
1947
            raise DependencyException("Half bought? Amounts don't match")
1✔
1948

1949
        if fee_abs != 0:
1✔
1950
            return self.apply_fee_conditional(
1✔
1951
                trade, trade_base_currency, amount=amount, fee_abs=fee_abs, order_obj=order_obj)
1952
        return None
1✔
1953

1954
    def get_valid_price(self, custom_price: float, proposed_price: float) -> float:
1✔
1955
        """
1956
        Return the valid price.
1957
        Check if the custom price is of the good type if not return proposed_price
1958
        :return: valid price for the order
1959
        """
1960
        if custom_price:
1✔
1961
            try:
1✔
1962
                valid_custom_price = float(custom_price)
1✔
1963
            except ValueError:
1✔
1964
                valid_custom_price = proposed_price
1✔
1965
        else:
1966
            valid_custom_price = proposed_price
1✔
1967

1968
        cust_p_max_dist_r = self.config.get('custom_price_max_distance_ratio', 0.02)
1✔
1969
        min_custom_price_allowed = proposed_price - (proposed_price * cust_p_max_dist_r)
1✔
1970
        max_custom_price_allowed = proposed_price + (proposed_price * cust_p_max_dist_r)
1✔
1971

1972
        # Bracket between min_custom_price_allowed and max_custom_price_allowed
1973
        return max(
1✔
1974
            min(valid_custom_price, max_custom_price_allowed),
1975
            min_custom_price_allowed)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc