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

freqtrade / freqtrade / 9394559170

26 Apr 2024 06:36AM UTC coverage: 94.656% (-0.02%) from 94.674%
9394559170

push

github

xmatthias
Loader should be passed as kwarg for clarity

20280 of 21425 relevant lines covered (94.66%)

0.95 hits per line

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

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

13
from schedule import Scheduler
1✔
14

15
from freqtrade import constants
1✔
16
from freqtrade.configuration import validate_config_consistency
1✔
17
from freqtrade.constants import BuySell, Config, EntryExecuteMode, ExchangeConfig, LongShort
1✔
18
from freqtrade.data.converter import order_book_to_dataframe
1✔
19
from freqtrade.data.dataprovider import DataProvider
1✔
20
from freqtrade.edge import Edge
1✔
21
from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, SignalDirection, State,
1✔
22
                             TradingMode)
23
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
1✔
24
                                  InvalidOrderException, PricingError)
25
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, remove_exchange_credentials,
1✔
26
                                timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds)
27
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
1✔
28
from freqtrade.mixins import LoggingMixin
1✔
29
from freqtrade.persistence import Order, PairLocks, Trade, init_db
1✔
30
from freqtrade.persistence.key_value_store import set_startup_time
1✔
31
from freqtrade.plugins.pairlistmanager import PairListManager
1✔
32
from freqtrade.plugins.protectionmanager import ProtectionManager
1✔
33
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
1✔
34
from freqtrade.rpc import RPCManager
1✔
35
from freqtrade.rpc.external_message_consumer import ExternalMessageConsumer
1✔
36
from freqtrade.rpc.rpc_types import (ProfitLossStr, RPCCancelMsg, RPCEntryMsg, RPCExitCancelMsg,
1✔
37
                                     RPCExitMsg, RPCProtectionMsg)
38
from freqtrade.strategy.interface import IStrategy
1✔
39
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
1✔
40
from freqtrade.util import MeasureTime
1✔
41
from freqtrade.util.migrations import migrate_binance_futures_names
1✔
42
from freqtrade.wallets import Wallets
1✔
43

44

45
logger = logging.getLogger(__name__)
1✔
46

47

48
class FreqtradeBot(LoggingMixin):
1✔
49
    """
50
    Freqtrade is the main class of the bot.
51
    This is from here the bot start its logic.
52
    """
53

54
    def __init__(self, config: Config) -> None:
1✔
55
        """
56
        Init all variables and objects the bot needs to work
57
        :param config: configuration dict, you can use Configuration.get_config()
58
        to get the config dict.
59
        """
60
        self.active_pair_whitelist: List[str] = []
1✔
61

62
        # Init bot state
63
        self.state = State.STOPPED
1✔
64

65
        # Init objects
66
        self.config = config
1✔
67
        exchange_config: ExchangeConfig = deepcopy(config['exchange'])
1✔
68
        # Remove credentials from original exchange config to avoid accidental credential exposure
69
        remove_exchange_credentials(config['exchange'], True)
1✔
70

71
        self.strategy: IStrategy = StrategyResolver.load_strategy(self.config)
1✔
72

73
        # Check config consistency here since strategies can set certain options
74
        validate_config_consistency(config)
1✔
75

76
        self.exchange = ExchangeResolver.load_exchange(
1✔
77
            self.config, exchange_config=exchange_config, load_leverage_tiers=True)
78

79
        init_db(self.config['db_url'])
1✔
80

81
        self.wallets = Wallets(self.config, self.exchange)
1✔
82

83
        PairLocks.timeframe = self.config['timeframe']
1✔
84

85
        self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
1✔
86
        self.last_process: Optional[datetime] = None
1✔
87

88
        # RPC runs in separate threads, can start handling external commands just after
89
        # initialization, even before Freqtradebot has a chance to start its throttling,
90
        # so anything in the Freqtradebot instance should be ready (initialized), including
91
        # the initial state of the bot.
92
        # Keep this at the end of this initialization method.
93
        self.rpc: RPCManager = RPCManager(self)
1✔
94

95
        self.dataprovider = DataProvider(self.config, self.exchange, rpc=self.rpc)
1✔
96
        self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider)
1✔
97

98
        self.dataprovider.add_pairlisthandler(self.pairlists)
1✔
99

100
        # Attach Dataprovider to strategy instance
101
        self.strategy.dp = self.dataprovider
1✔
102
        # Attach Wallets to strategy instance
103
        self.strategy.wallets = self.wallets
1✔
104

105
        # Initializing Edge only if enabled
106
        self.edge = Edge(self.config, self.exchange, self.strategy) if \
1✔
107
            self.config.get('edge', {}).get('enabled', False) else None
108

109
        # Init ExternalMessageConsumer if enabled
110
        self.emc = ExternalMessageConsumer(self.config, self.dataprovider) if \
1✔
111
            self.config.get('external_message_consumer', {}).get('enabled', False) else None
112

113
        self.active_pair_whitelist = self._refresh_active_whitelist()
1✔
114

115
        # Set initial bot state from config
116
        initial_state = self.config.get('initial_state')
1✔
117
        self.state = State[initial_state.upper()] if initial_state else State.STOPPED
1✔
118

119
        # Protect exit-logic from forcesell and vice versa
120
        self._exit_lock = Lock()
1✔
121
        timeframe_secs = timeframe_to_seconds(self.strategy.timeframe)
1✔
122
        LoggingMixin.__init__(self, logger, timeframe_secs)
1✔
123

124
        self._schedule = Scheduler()
1✔
125

126
        if self.trading_mode == TradingMode.FUTURES:
1✔
127

128
            def update():
1✔
129
                self.update_funding_fees()
1✔
130
                self.wallets.update()
1✔
131

132
            # This would be more efficient if scheduled in utc time, and performed at each
133
            # funding interval, specified by funding_fee_times on the exchange classes
134
            # However, this reduces the precision - and might therefore lead to problems.
135
            for time_slot in range(0, 24):
1✔
136
                for minutes in [1, 31]:
1✔
137
                    t = str(time(time_slot, minutes, 2))
1✔
138
                    self._schedule.every().day.at(t).do(update)
1✔
139

140
        self.strategy.ft_bot_start()
1✔
141
        # Initialize protections AFTER bot start - otherwise parameters are not loaded.
142
        self.protections = ProtectionManager(self.config, self.strategy.protections)
1✔
143

144
        def log_took_too_long(duration: float, time_limit: float):
1✔
145
            logger.warning(
×
146
                f"Strategy analysis took {duration:.2f}, which is 25% of the timeframe. "
147
                "This can lead to delayed orders and missed signals."
148
                "Consider either reducing the amount of work your strategy performs "
149
                "or reduce the amount of pairs in the Pairlist."
150
            )
151

152
        self._measure_execution = MeasureTime(log_took_too_long, timeframe_secs * 0.25)
1✔
153

154
    def notify_status(self, msg: str, msg_type=RPCMessageType.STATUS) -> None:
1✔
155
        """
156
        Public method for users of this class (worker, etc.) to send notifications
157
        via RPC about changes in the bot status.
158
        """
159
        self.rpc.send_msg({
1✔
160
            'type': msg_type,
161
            'status': msg
162
        })
163

164
    def cleanup(self) -> None:
1✔
165
        """
166
        Cleanup pending resources on an already stopped bot
167
        :return: None
168
        """
169
        logger.info('Cleaning up modules ...')
1✔
170
        try:
1✔
171
            # Wrap db activities in shutdown to avoid problems if database is gone,
172
            # and raises further exceptions.
173
            if self.config['cancel_open_orders_on_exit']:
1✔
174
                self.cancel_all_open_orders()
1✔
175

176
            self.check_for_open_trades()
1✔
177
        except Exception as e:
1✔
178
            logger.warning(f'Exception during cleanup: {e.__class__.__name__} {e}')
1✔
179

180
        finally:
181
            self.strategy.ft_bot_cleanup()
1✔
182

183
        self.rpc.cleanup()
1✔
184
        if self.emc:
1✔
185
            self.emc.shutdown()
1✔
186
        self.exchange.close()
1✔
187
        try:
1✔
188
            Trade.commit()
1✔
189
        except Exception:
1✔
190
            # Exceptions here will be happening if the db disappeared.
191
            # At which point we can no longer commit anyway.
192
            pass
1✔
193

194
    def startup(self) -> None:
1✔
195
        """
196
        Called on startup and after reloading the bot - triggers notifications and
197
        performs startup tasks
198
        """
199
        migrate_binance_futures_names(self.config)
1✔
200
        set_startup_time()
1✔
201

202
        self.rpc.startup_messages(self.config, self.pairlists, self.protections)
1✔
203
        # Update older trades with precision and precision mode
204
        self.startup_backpopulate_precision()
1✔
205
        if not self.edge:
1✔
206
            # Adjust stoploss if it was changed
207
            Trade.stoploss_reinitialization(self.strategy.stoploss)
1✔
208

209
        # Only update open orders on startup
210
        # This will update the database after the initial migration
211
        self.startup_update_open_orders()
1✔
212
        self.update_funding_fees()
1✔
213

214
    def process(self) -> None:
1✔
215
        """
216
        Queries the persistence layer for open trades and handles them,
217
        otherwise a new trade is created.
218
        :return: True if one or more trades has been created or closed, False otherwise
219
        """
220

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

224
        self.update_trades_without_assigned_fees()
1✔
225

226
        # Query trades from persistence layer
227
        trades: List[Trade] = Trade.get_open_trades()
1✔
228

229
        self.active_pair_whitelist = self._refresh_active_whitelist(trades)
1✔
230

231
        # Refreshing candles
232
        self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
1✔
233
                                  self.strategy.gather_informative_pairs())
234

235
        strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
1✔
236
            current_time=datetime.now(timezone.utc))
237

238
        with self._measure_execution:
1✔
239
            self.strategy.analyze(self.active_pair_whitelist)
1✔
240

241
        with self._exit_lock:
1✔
242
            # Check for exchange cancellations, timeouts and user requested replace
243
            self.manage_open_orders()
1✔
244

245
        # Protect from collisions with force_exit.
246
        # Without this, freqtrade may try to recreate stoploss_on_exchange orders
247
        # while exiting is in process, since telegram messages arrive in an different thread.
248
        with self._exit_lock:
1✔
249
            trades = Trade.get_open_trades()
1✔
250
            # First process current opened trades (positions)
251
            self.exit_positions(trades)
1✔
252

253
        # Check if we need to adjust our current positions before attempting to buy new trades.
254
        if self.strategy.position_adjustment_enable:
1✔
255
            with self._exit_lock:
1✔
256
                self.process_open_trade_positions()
1✔
257

258
        # Then looking for buy opportunities
259
        if self.get_free_open_trades():
1✔
260
            self.enter_positions()
1✔
261
        if self.trading_mode == TradingMode.FUTURES:
1✔
262
            self._schedule.run_pending()
1✔
263
        Trade.commit()
1✔
264
        self.rpc.process_msg_queue(self.dataprovider._msg_queue)
1✔
265
        self.last_process = datetime.now(timezone.utc)
1✔
266

267
    def process_stopped(self) -> None:
1✔
268
        """
269
        Close all orders that were left open
270
        """
271
        if self.config['cancel_open_orders_on_exit']:
1✔
272
            self.cancel_all_open_orders()
1✔
273

274
    def check_for_open_trades(self):
1✔
275
        """
276
        Notify the user when the bot is stopped (not reloaded)
277
        and there are still open trades active.
278
        """
279
        open_trades = Trade.get_open_trades()
1✔
280

281
        if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG:
1✔
282
            msg = {
1✔
283
                'type': RPCMessageType.WARNING,
284
                'status':
285
                    f"{len(open_trades)} open trades active.\n\n"
286
                    f"Handle these trades manually on {self.exchange.name}, "
287
                    f"or '/start' the bot again and use '/stopentry' "
288
                    f"to handle open trades gracefully. \n"
289
                    f"{'Note: Trades are simulated (dry run).' if self.config['dry_run'] else ''}",
290
            }
291
            self.rpc.send_msg(msg)
1✔
292

293
    def _refresh_active_whitelist(self, trades: Optional[List[Trade]] = None) -> List[str]:
1✔
294
        """
295
        Refresh active whitelist from pairlist or edge and extend it with
296
        pairs that have open trades.
297
        """
298
        # Refresh whitelist
299
        _prev_whitelist = self.pairlists.whitelist
1✔
300
        self.pairlists.refresh_pairlist()
1✔
301
        _whitelist = self.pairlists.whitelist
1✔
302

303
        # Calculating Edge positioning
304
        if self.edge:
1✔
305
            self.edge.calculate(_whitelist)
1✔
306
            _whitelist = self.edge.adjust(_whitelist)
1✔
307

308
        if trades:
1✔
309
            # Extend active-pair whitelist with pairs of open trades
310
            # It ensures that candle (OHLCV) data are downloaded for open trades as well
311
            _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist])
1✔
312

313
        # Called last to include the included pairs
314
        if _prev_whitelist != _whitelist:
1✔
315
            self.rpc.send_msg({'type': RPCMessageType.WHITELIST, 'data': _whitelist})
1✔
316

317
        return _whitelist
1✔
318

319
    def get_free_open_trades(self) -> int:
1✔
320
        """
321
        Return the number of free open trades slots or 0 if
322
        max number of open trades reached
323
        """
324
        open_trades = Trade.get_open_trade_count()
1✔
325
        return max(0, self.config['max_open_trades'] - open_trades)
1✔
326

327
    def update_funding_fees(self) -> None:
1✔
328
        if self.trading_mode == TradingMode.FUTURES:
1✔
329
            trades: List[Trade] = Trade.get_open_trades()
1✔
330
            for trade in trades:
1✔
331
                trade.set_funding_fees(
1✔
332
                    self.exchange.get_funding_fees(
333
                        pair=trade.pair,
334
                        amount=trade.amount,
335
                        is_short=trade.is_short,
336
                        open_date=trade.date_last_filled_utc)
337
                )
338

339
    def startup_backpopulate_precision(self) -> None:
1✔
340

341
        trades = Trade.get_trades([Trade.contract_size.is_(None)])
1✔
342
        for trade in trades:
1✔
343
            if trade.exchange != self.exchange.id:
1✔
344
                continue
1✔
345
            trade.precision_mode = self.exchange.precisionMode
1✔
346
            trade.amount_precision = self.exchange.get_precision_amount(trade.pair)
1✔
347
            trade.price_precision = self.exchange.get_precision_price(trade.pair)
1✔
348
            trade.contract_size = self.exchange.get_contract_size(trade.pair)
1✔
349
        Trade.commit()
1✔
350

351
    def startup_update_open_orders(self):
1✔
352
        """
353
        Updates open orders based on order list kept in the database.
354
        Mainly updates the state of orders - but may also close trades
355
        """
356
        if self.config['dry_run'] or self.config['exchange'].get('skip_open_order_update', False):
1✔
357
            # Updating open orders in dry-run does not make sense and will fail.
358
            return
1✔
359

360
        orders = Order.get_open_orders()
1✔
361
        logger.info(f"Updating {len(orders)} open orders.")
1✔
362
        for order in orders:
1✔
363
            try:
1✔
364
                fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
1✔
365
                                                                 order.ft_order_side == 'stoploss')
366
                if not order.trade:
1✔
367
                    # This should not happen, but it does if trades were deleted manually.
368
                    # This can only incur on sqlite, which doesn't enforce foreign constraints.
369
                    logger.warning(
×
370
                        f"Order {order.order_id} has no trade attached. "
371
                        "This may suggest a database corruption. "
372
                        f"The expected trade ID is {order.ft_trade_id}. Ignoring this order."
373
                    )
374
                    continue
×
375
                self.update_trade_state(order.trade, order.order_id, fo,
1✔
376
                                        stoploss_order=(order.ft_order_side == 'stoploss'))
377

378
            except InvalidOrderException as e:
1✔
379
                logger.warning(f"Error updating Order {order.order_id} due to {e}.")
1✔
380
                if order.order_date_utc - timedelta(days=5) < datetime.now(timezone.utc):
1✔
381
                    logger.warning(
1✔
382
                        "Order is older than 5 days. Assuming order was fully cancelled.")
383
                    fo = order.to_ccxt_object()
1✔
384
                    fo['status'] = 'canceled'
1✔
385
                    self.handle_cancel_order(
1✔
386
                        fo, order, order.trade, constants.CANCEL_REASON['TIMEOUT']
387
                    )
388

389
            except ExchangeError as e:
1✔
390

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

393
    def update_trades_without_assigned_fees(self) -> None:
1✔
394
        """
395
        Update closed trades without close fees assigned.
396
        Only acts when Orders are in the database, otherwise the last order-id is unknown.
397
        """
398
        if self.config['dry_run']:
1✔
399
            # Updating open orders in dry-run does not make sense and will fail.
400
            return
1✔
401

402
        trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees()
1✔
403
        for trade in trades:
1✔
404
            if not trade.is_open and not trade.fee_updated(trade.exit_side):
1✔
405
                # Get sell fee
406
                order = trade.select_order(trade.exit_side, False, only_filled=True)
1✔
407
                if not order:
1✔
408
                    order = trade.select_order('stoploss', False)
×
409
                if order:
1✔
410
                    logger.info(
1✔
411
                        f"Updating {trade.exit_side}-fee on trade {trade}"
412
                        f"for order {order.order_id}."
413
                    )
414
                    self.update_trade_state(trade, order.order_id,
1✔
415
                                            stoploss_order=order.ft_order_side == 'stoploss',
416
                                            send_msg=False)
417

418
        trades = Trade.get_open_trades_without_assigned_fees()
1✔
419
        for trade in trades:
1✔
420
            with self._exit_lock:
1✔
421
                if trade.is_open and not trade.fee_updated(trade.entry_side):
1✔
422
                    order = trade.select_order(trade.entry_side, False, only_filled=True)
1✔
423
                    open_order = trade.select_order(trade.entry_side, True)
1✔
424
                    if order and open_order is None:
1✔
425
                        logger.info(
1✔
426
                            f"Updating {trade.entry_side}-fee on trade {trade}"
427
                            f"for order {order.order_id}."
428
                        )
429
                        self.update_trade_state(trade, order.order_id, send_msg=False)
1✔
430

431
    def handle_insufficient_funds(self, trade: Trade):
1✔
432
        """
433
        Try refinding a lost trade.
434
        Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy).
435
        Tries to walk the stored orders and updates the trade state if necessary.
436
        """
437
        logger.info(f"Trying to refind lost order for {trade}")
1✔
438
        for order in trade.orders:
1✔
439
            logger.info(f"Trying to refind {order}")
1✔
440
            fo = None
1✔
441
            if not order.ft_is_open:
1✔
442
                logger.debug(f"Order {order} is no longer open.")
1✔
443
                continue
1✔
444
            try:
1✔
445
                fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair,
1✔
446
                                                                 order.ft_order_side == 'stoploss')
447
                if fo:
1✔
448
                    logger.info(f"Found {order} for trade {trade}.")
1✔
449
                    self.update_trade_state(trade, order.order_id, fo,
1✔
450
                                            stoploss_order=order.ft_order_side == 'stoploss')
451

452
            except ExchangeError:
1✔
453
                logger.warning(f"Error updating {order.order_id}.")
1✔
454

455
    def handle_onexchange_order(self, trade: Trade):
1✔
456
        """
457
        Try refinding a order that is not in the database.
458
        Only used balance disappeared, which would make exiting impossible.
459
        """
460
        try:
1✔
461
            orders = self.exchange.fetch_orders(
1✔
462
                trade.pair, trade.open_date_utc - timedelta(seconds=10))
463
            prev_exit_reason = trade.exit_reason
1✔
464
            prev_trade_state = trade.is_open
1✔
465
            prev_trade_amount = trade.amount
1✔
466
            for order in orders:
1✔
467
                trade_order = [o for o in trade.orders if o.order_id == order['id']]
1✔
468

469
                if trade_order:
1✔
470
                    # We knew this order, but didn't have it updated properly
471
                    order_obj = trade_order[0]
1✔
472
                else:
473
                    logger.info(f"Found previously unknown order {order['id']} for {trade.pair}.")
1✔
474

475
                    order_obj = Order.parse_from_ccxt_object(order, trade.pair, order['side'])
1✔
476
                    order_obj.order_filled_date = datetime.fromtimestamp(
1✔
477
                        safe_value_fallback(order, 'lastTradeTimestamp', 'timestamp') // 1000,
478
                        tz=timezone.utc)
479
                    trade.orders.append(order_obj)
1✔
480
                    Trade.commit()
1✔
481
                    trade.exit_reason = ExitType.SOLD_ON_EXCHANGE.value
1✔
482

483
                self.update_trade_state(trade, order['id'], order, send_msg=False)
1✔
484

485
                logger.info(f"handled order {order['id']}")
1✔
486

487
            # Refresh trade from database
488
            Trade.session.refresh(trade)
1✔
489
            if not trade.is_open:
1✔
490
                # Trade was just closed
491
                trade.close_date = trade.date_last_filled_utc
1✔
492
                self.order_close_notify(trade, order_obj,
1✔
493
                                        order_obj.ft_order_side == 'stoploss',
494
                                        send_msg=prev_trade_state != trade.is_open)
495
            else:
496
                trade.exit_reason = prev_exit_reason
1✔
497
                total = self.wallets.get_total(trade.base_currency) if trade.base_currency else 0
1✔
498
                if total < trade.amount:
1✔
499
                    if total > trade.amount * 0.98:
1✔
500
                        logger.warning(
1✔
501
                            f"{trade} has a total of {trade.amount} {trade.base_currency}, "
502
                            f"but the Wallet shows a total of {total} {trade.base_currency}. "
503
                            f"Adjusting trade amount to {total}."
504
                            "This may however lead to further issues."
505
                        )
506
                        trade.amount = total
1✔
507
                    else:
508
                        logger.warning(
1✔
509
                            f"{trade} has a total of {trade.amount} {trade.base_currency}, "
510
                            f"but the Wallet shows a total of {total} {trade.base_currency}. "
511
                            "Refusing to adjust as the difference is too large."
512
                            "This may however lead to further issues."
513
                        )
514
                if prev_trade_amount != trade.amount:
1✔
515
                    # Cancel stoploss on exchange if the amount changed
516
                    trade = self.cancel_stoploss_on_exchange(trade)
1✔
517
            Trade.commit()
1✔
518

519
        except ExchangeError:
×
520
            logger.warning("Error finding onexchange order.")
×
521
        except Exception:
×
522
            # catching https://github.com/freqtrade/freqtrade/issues/9025
523
            logger.warning("Error finding onexchange order", exc_info=True)
×
524
#
525
# BUY / enter positions / open trades logic and methods
526
#
527

528
    def enter_positions(self) -> int:
1✔
529
        """
530
        Tries to execute entry orders for new trades (positions)
531
        """
532
        trades_created = 0
1✔
533

534
        whitelist = deepcopy(self.active_pair_whitelist)
1✔
535
        if not whitelist:
1✔
536
            self.log_once("Active pair whitelist is empty.", logger.info)
1✔
537
            return trades_created
1✔
538
        # Remove pairs for currently opened trades from the whitelist
539
        for trade in Trade.get_open_trades():
1✔
540
            if trade.pair in whitelist:
1✔
541
                whitelist.remove(trade.pair)
1✔
542
                logger.debug('Ignoring %s in pair whitelist', trade.pair)
1✔
543

544
        if not whitelist:
1✔
545
            self.log_once("No currency pair in active pair whitelist, "
1✔
546
                          "but checking to exit open trades.", logger.info)
547
            return trades_created
1✔
548
        if PairLocks.is_global_lock(side='*'):
1✔
549
            # This only checks for total locks (both sides).
550
            # per-side locks will be evaluated by `is_pair_locked` within create_trade,
551
            # once the direction for the trade is clear.
552
            lock = PairLocks.get_pair_longest_lock('*')
1✔
553
            if lock:
1✔
554
                self.log_once(f"Global pairlock active until "
1✔
555
                              f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}. "
556
                              f"Not creating new trades, reason: {lock.reason}.", logger.info)
557
            else:
558
                self.log_once("Global pairlock active. Not creating new trades.", logger.info)
×
559
            return trades_created
1✔
560
        # Create entity and execute trade for each pair from whitelist
561
        for pair in whitelist:
1✔
562
            try:
1✔
563
                with self._exit_lock:
1✔
564
                    trades_created += self.create_trade(pair)
1✔
565
            except DependencyException as exception:
1✔
566
                logger.warning('Unable to create trade for %s: %s', pair, exception)
1✔
567

568
        if not trades_created:
1✔
569
            logger.debug("Found no enter signals for whitelisted currencies. Trying again...")
1✔
570

571
        return trades_created
1✔
572

573
    def create_trade(self, pair: str) -> bool:
1✔
574
        """
575
        Check the implemented trading strategy for buy signals.
576

577
        If the pair triggers the buy signal a new trade record gets created
578
        and the buy-order opening the trade gets issued towards the exchange.
579

580
        :return: True if a trade has been created.
581
        """
582
        logger.debug(f"create_trade for pair {pair}")
1✔
583

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

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

593
        # running get_signal on historical data fetched
594
        (signal, enter_tag) = self.strategy.get_entry_signal(
1✔
595
            pair,
596
            self.strategy.timeframe,
597
            analyzed_df
598
        )
599

600
        if signal:
1✔
601
            if self.strategy.is_pair_locked(pair, candle_date=nowtime, side=signal):
1✔
602
                lock = PairLocks.get_pair_longest_lock(pair, nowtime, signal)
1✔
603
                if lock:
1✔
604
                    self.log_once(f"Pair {pair} {lock.side} is locked until "
1✔
605
                                  f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)} "
606
                                  f"due to {lock.reason}.",
607
                                  logger.info)
608
                else:
609
                    self.log_once(f"Pair {pair} is currently locked.", logger.info)
×
610
                return False
1✔
611
            stake_amount = self.wallets.get_trade_stake_amount(
1✔
612
                pair, self.config['max_open_trades'], self.edge)
613

614
            bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {})
1✔
615
            if ((bid_check_dom.get('enabled', False)) and
1✔
616
                    (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
617
                if self._check_depth_of_market(pair, bid_check_dom, side=signal):
1✔
618
                    return self.execute_entry(
1✔
619
                        pair,
620
                        stake_amount,
621
                        enter_tag=enter_tag,
622
                        is_short=(signal == SignalDirection.SHORT)
623
                    )
624
                else:
625
                    return False
1✔
626

627
            return self.execute_entry(
1✔
628
                pair,
629
                stake_amount,
630
                enter_tag=enter_tag,
631
                is_short=(signal == SignalDirection.SHORT)
632
            )
633
        else:
634
            return False
1✔
635

636
#
637
# BUY / increase positions / DCA logic and methods
638
#
639
    def process_open_trade_positions(self):
1✔
640
        """
641
        Tries to execute additional buy or sell orders for open trades (positions)
642
        """
643
        # Walk through each pair and check if it needs changes
644
        for trade in Trade.get_open_trades():
1✔
645
            # If there is any open orders, wait for them to finish.
646
            # TODO Remove to allow mul open orders
647
            if not trade.has_open_orders:
1✔
648
                # Do a wallets update (will be ratelimited to once per hour)
649
                self.wallets.update(False)
1✔
650
                try:
1✔
651
                    self.check_and_call_adjust_trade_position(trade)
1✔
652
                except DependencyException as exception:
1✔
653
                    logger.warning(
1✔
654
                        f"Unable to adjust position of trade for {trade.pair}: {exception}")
655

656
    def check_and_call_adjust_trade_position(self, trade: Trade):
1✔
657
        """
658
        Check the implemented trading strategy for adjustment command.
659
        If the strategy triggers the adjustment, a new order gets issued.
660
        Once that completes, the existing trade is modified to match new data.
661
        """
662
        current_entry_rate, current_exit_rate = self.exchange.get_rates(
1✔
663
            trade.pair, True, trade.is_short)
664

665
        current_entry_profit = trade.calc_profit_ratio(current_entry_rate)
1✔
666
        current_exit_profit = trade.calc_profit_ratio(current_exit_rate)
1✔
667

668
        min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
1✔
669
                                                                  current_entry_rate,
670
                                                                  0.0)
671
        min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
1✔
672
                                                                 current_exit_rate,
673
                                                                 self.strategy.stoploss)
674
        max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate)
1✔
675
        stake_available = self.wallets.get_available_stake_amount()
1✔
676
        logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
1✔
677
        stake_amount, order_tag = self.strategy._adjust_trade_position_internal(
1✔
678
            trade=trade,
679
            current_time=datetime.now(timezone.utc), current_rate=current_entry_rate,
680
            current_profit=current_entry_profit, min_stake=min_entry_stake,
681
            max_stake=min(max_entry_stake, stake_available),
682
            current_entry_rate=current_entry_rate, current_exit_rate=current_exit_rate,
683
            current_entry_profit=current_entry_profit, current_exit_profit=current_exit_profit
684
        )
685

686
        if stake_amount is not None and stake_amount > 0.0:
1✔
687
            # We should increase our position
688
            if self.strategy.max_entry_position_adjustment > -1:
1✔
689
                count_of_entries = trade.nr_of_successful_entries
1✔
690
                if count_of_entries > self.strategy.max_entry_position_adjustment:
1✔
691
                    logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
1✔
692
                    return
1✔
693
                else:
694
                    logger.debug("Max adjustment entries is set to unlimited.")
×
695
            self.execute_entry(trade.pair, stake_amount, price=current_entry_rate,
1✔
696
                               trade=trade, is_short=trade.is_short, mode='pos_adjust',
697
                               enter_tag=order_tag)
698

699
        if stake_amount is not None and stake_amount < 0.0:
1✔
700
            # We should decrease our position
701
            amount = self.exchange.amount_to_contract_precision(
1✔
702
                trade.pair,
703
                abs(float(stake_amount * trade.amount / trade.stake_amount)))
704

705
            if amount == 0.0:
1✔
706
                logger.info("Amount to exit is 0.0 due to exchange limits - not exiting.")
1✔
707
                return
1✔
708

709
            remaining = (trade.amount - amount) * current_exit_rate
1✔
710
            if min_exit_stake and remaining != 0 and remaining < min_exit_stake:
1✔
711
                logger.info(f"Remaining amount of {remaining} would be smaller "
1✔
712
                            f"than the minimum of {min_exit_stake}.")
713
                return
1✔
714

715
            self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple(
1✔
716
                exit_type=ExitType.PARTIAL_EXIT), sub_trade_amt=amount, exit_tag=order_tag)
717

718
    def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool:
1✔
719
        """
720
        Checks depth of market before executing a buy
721
        """
722
        conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0)
1✔
723
        logger.info(f"Checking depth of market for {pair} ...")
1✔
724
        order_book = self.exchange.fetch_l2_order_book(pair, 1000)
1✔
725
        order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
1✔
726
        order_book_bids = order_book_data_frame['b_size'].sum()
1✔
727
        order_book_asks = order_book_data_frame['a_size'].sum()
1✔
728

729
        entry_side = order_book_bids if side == SignalDirection.LONG else order_book_asks
1✔
730
        exit_side = order_book_asks if side == SignalDirection.LONG else order_book_bids
1✔
731
        bids_ask_delta = entry_side / exit_side
1✔
732

733
        bids = f"Bids: {order_book_bids}"
1✔
734
        asks = f"Asks: {order_book_asks}"
1✔
735
        delta = f"Delta: {bids_ask_delta}"
1✔
736

737
        logger.info(
1✔
738
            f"{bids}, {asks}, {delta}, Direction: {side.value} "
739
            f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, "
740
            f"Immediate Bid Quantity: {order_book['bids'][0][1]}, "
741
            f"Immediate Ask Quantity: {order_book['asks'][0][1]}."
742
        )
743
        if bids_ask_delta >= conf_bids_to_ask_delta:
1✔
744
            logger.info(f"Bids to asks delta for {pair} DOES satisfy condition.")
1✔
745
            return True
1✔
746
        else:
747
            logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
1✔
748
            return False
1✔
749

750
    def execute_entry(
1✔
751
        self,
752
        pair: str,
753
        stake_amount: float,
754
        price: Optional[float] = None,
755
        *,
756
        is_short: bool = False,
757
        ordertype: Optional[str] = None,
758
        enter_tag: Optional[str] = None,
759
        trade: Optional[Trade] = None,
760
        mode: EntryExecuteMode = 'initial',
761
        leverage_: Optional[float] = None,
762
    ) -> bool:
763
        """
764
        Executes a limit buy for the given pair
765
        :param pair: pair for which we want to create a LIMIT_BUY
766
        :param stake_amount: amount of stake-currency for the pair
767
        :return: True if a buy order is created, false if it fails.
768
        :raise: DependencyException or it's subclasses like ExchangeError.
769
        """
770
        time_in_force = self.strategy.order_time_in_force['entry']
1✔
771

772
        side: BuySell = 'sell' if is_short else 'buy'
1✔
773
        name = 'Short' if is_short else 'Long'
1✔
774
        trade_side: LongShort = 'short' if is_short else 'long'
1✔
775
        pos_adjust = trade is not None
1✔
776

777
        enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake(
1✔
778
            pair, price, stake_amount, trade_side, enter_tag, trade, mode, leverage_)
779

780
        if not stake_amount:
1✔
781
            return False
1✔
782

783
        msg = (f"Position adjust: about to create a new order for {pair} with stake_amount: "
1✔
784
               f"{stake_amount} for {trade}" if mode == 'pos_adjust'
785
               else
786
               (f"Replacing {side} order: about create a new order for {pair} with stake_amount: "
787
                f"{stake_amount} ..."
788
                if mode == 'replace' else
789
                f"{name} signal found: about create a new trade for {pair} with stake_amount: "
790
                f"{stake_amount} ..."
791
                ))
792
        logger.info(msg)
1✔
793
        amount = (stake_amount / enter_limit_requested) * leverage
1✔
794
        order_type = ordertype or self.strategy.order_types['entry']
1✔
795

796
        if mode == 'initial' and not strategy_safe_wrapper(
1✔
797
                self.strategy.confirm_trade_entry, default_retval=True)(
798
                pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
799
                time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
800
                entry_tag=enter_tag, side=trade_side):
801
            logger.info(f"User denied entry for {pair}.")
1✔
802
            return False
1✔
803
        order = self.exchange.create_order(
1✔
804
            pair=pair,
805
            ordertype=order_type,
806
            side=side,
807
            amount=amount,
808
            rate=enter_limit_requested,
809
            reduceOnly=False,
810
            time_in_force=time_in_force,
811
            leverage=leverage
812
        )
813
        order_obj = Order.parse_from_ccxt_object(order, pair, side, amount, enter_limit_requested)
1✔
814
        order_obj.ft_order_tag = enter_tag
1✔
815
        order_id = order['id']
1✔
816
        order_status = order.get('status')
1✔
817
        logger.info(f"Order {order_id} was created for {pair} and status is {order_status}.")
1✔
818

819
        # we assume the order is executed at the price requested
820
        enter_limit_filled_price = enter_limit_requested
1✔
821
        amount_requested = amount
1✔
822

823
        if order_status == 'expired' or order_status == 'rejected':
1✔
824

825
            # return false if the order is not filled
826
            if float(order['filled']) == 0:
1✔
827
                logger.warning(f'{name} {time_in_force} order with time in force {order_type} '
1✔
828
                               f'for {pair} is {order_status} by {self.exchange.name}.'
829
                               ' zero amount is fulfilled.')
830
                return False
1✔
831
            else:
832
                # the order is partially fulfilled
833
                # in case of IOC orders we can check immediately
834
                # if the order is fulfilled fully or partially
835
                logger.warning('%s %s order with time in force %s for %s is %s by %s.'
1✔
836
                               ' %s amount fulfilled out of %s (%s remaining which is canceled).',
837
                               name, time_in_force, order_type, pair, order_status,
838
                               self.exchange.name, order['filled'], order['amount'],
839
                               order['remaining']
840
                               )
841
                amount = safe_value_fallback(order, 'filled', 'amount', amount)
1✔
842
                enter_limit_filled_price = safe_value_fallback(
1✔
843
                    order, 'average', 'price', enter_limit_filled_price)
844

845
        # in case of FOK the order may be filled immediately and fully
846
        elif order_status == 'closed':
1✔
847
            amount = safe_value_fallback(order, 'filled', 'amount', amount)
1✔
848
            enter_limit_filled_price = safe_value_fallback(
1✔
849
                order, 'average', 'price', enter_limit_requested)
850

851
        # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
852
        fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
1✔
853
        base_currency = self.exchange.get_pair_base_currency(pair)
1✔
854
        open_date = datetime.now(timezone.utc)
1✔
855

856
        funding_fees = self.exchange.get_funding_fees(
1✔
857
            pair=pair,
858
            amount=amount + trade.amount if trade else amount,
859
            is_short=is_short,
860
            open_date=trade.date_last_filled_utc if trade else open_date
861
        )
862

863
        # This is a new trade
864
        if trade is None:
1✔
865

866
            trade = Trade(
1✔
867
                pair=pair,
868
                base_currency=base_currency,
869
                stake_currency=self.config['stake_currency'],
870
                stake_amount=stake_amount,
871
                amount=amount,
872
                is_open=True,
873
                amount_requested=amount_requested,
874
                fee_open=fee,
875
                fee_close=fee,
876
                open_rate=enter_limit_filled_price,
877
                open_rate_requested=enter_limit_requested,
878
                open_date=open_date,
879
                exchange=self.exchange.id,
880
                strategy=self.strategy.get_strategy_name(),
881
                enter_tag=enter_tag,
882
                timeframe=timeframe_to_minutes(self.config['timeframe']),
883
                leverage=leverage,
884
                is_short=is_short,
885
                trading_mode=self.trading_mode,
886
                funding_fees=funding_fees,
887
                amount_precision=self.exchange.get_precision_amount(pair),
888
                price_precision=self.exchange.get_precision_price(pair),
889
                precision_mode=self.exchange.precisionMode,
890
                contract_size=self.exchange.get_contract_size(pair),
891
            )
892
            stoploss = self.strategy.stoploss if not self.edge else self.edge.get_stoploss(pair)
1✔
893
            trade.adjust_stop_loss(trade.open_rate, stoploss, initial=True)
1✔
894

895
        else:
896
            # This is additional buy, we reset fee_open_currency so timeout checking can work
897
            trade.is_open = True
1✔
898
            trade.fee_open_currency = None
1✔
899
            trade.open_rate_requested = enter_limit_requested
1✔
900
            trade.set_funding_fees(funding_fees)
1✔
901

902
        trade.orders.append(order_obj)
1✔
903
        trade.recalc_trade_from_orders()
1✔
904
        Trade.session.add(trade)
1✔
905
        Trade.commit()
1✔
906

907
        # Updating wallets
908
        self.wallets.update()
1✔
909

910
        self._notify_enter(trade, order_obj, order_type, sub_trade=pos_adjust)
1✔
911

912
        if pos_adjust:
1✔
913
            if order_status == 'closed':
1✔
914
                logger.info(f"DCA order closed, trade should be up to date: {trade}")
1✔
915
                trade = self.cancel_stoploss_on_exchange(trade)
1✔
916
            else:
917
                logger.info(f"DCA order {order_status}, will wait for resolution: {trade}")
1✔
918

919
        # Update fees if order is non-opened
920
        if order_status in constants.NON_OPEN_EXCHANGE_STATES:
1✔
921
            self.update_trade_state(trade, order_id, order)
1✔
922

923
        return True
1✔
924

925
    def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade:
1✔
926
        # First cancelling stoploss on exchange ...
927
        for oslo in trade.open_sl_orders:
1✔
928
            try:
1✔
929
                logger.info(f"Cancelling stoploss on exchange for {trade} "
1✔
930
                            f"order: {oslo.order_id}")
931
                co = self.exchange.cancel_stoploss_order_with_result(
1✔
932
                    oslo.order_id, trade.pair, trade.amount)
933
                self.update_trade_state(trade, oslo.order_id, co, stoploss_order=True)
1✔
934
            except InvalidOrderException:
1✔
935
                logger.exception(f"Could not cancel stoploss order {oslo.order_id} "
1✔
936
                                 f"for pair {trade.pair}")
937
        return trade
1✔
938

939
    def get_valid_enter_price_and_stake(
1✔
940
        self, pair: str, price: Optional[float], stake_amount: float,
941
        trade_side: LongShort,
942
        entry_tag: Optional[str],
943
        trade: Optional[Trade],
944
        mode: EntryExecuteMode,
945
        leverage_: Optional[float],
946
    ) -> Tuple[float, float, float]:
947
        """
948
        Validate and eventually adjust (within limits) limit, amount and leverage
949
        :return: Tuple with (price, amount, leverage)
950
        """
951

952
        if price:
1✔
953
            enter_limit_requested = price
1✔
954
        else:
955
            # Calculate price
956
            enter_limit_requested = self.exchange.get_rate(
1✔
957
                pair, side='entry', is_short=(trade_side == 'short'), refresh=True)
958
        if mode != 'replace':
1✔
959
            # Don't call custom_entry_price in order-adjust scenario
960
            custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
1✔
961
                                                       default_retval=enter_limit_requested)(
962
                pair=pair, trade=trade,
963
                current_time=datetime.now(timezone.utc),
964
                proposed_rate=enter_limit_requested, entry_tag=entry_tag,
965
                side=trade_side,
966
            )
967

968
            enter_limit_requested = self.get_valid_price(custom_entry_price, enter_limit_requested)
1✔
969

970
        if not enter_limit_requested:
1✔
971
            raise PricingError('Could not determine entry price.')
1✔
972

973
        if self.trading_mode != TradingMode.SPOT and trade is None:
1✔
974
            max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
1✔
975
            if leverage_:
1✔
976
                leverage = leverage_
1✔
977
            else:
978
                leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
1✔
979
                    pair=pair,
980
                    current_time=datetime.now(timezone.utc),
981
                    current_rate=enter_limit_requested,
982
                    proposed_leverage=1.0,
983
                    max_leverage=max_leverage,
984
                    side=trade_side, entry_tag=entry_tag,
985
                )
986
            # Cap leverage between 1.0 and max_leverage.
987
            leverage = min(max(leverage, 1.0), max_leverage)
1✔
988
        else:
989
            # Changing leverage currently not possible
990
            leverage = trade.leverage if trade else 1.0
1✔
991

992
        # Min-stake-amount should actually include Leverage - this way our "minimal"
993
        # stake- amount might be higher than necessary.
994
        # We do however also need min-stake to determine leverage, therefore this is ignored as
995
        # edge-case for now.
996
        min_stake_amount = self.exchange.get_min_pair_stake_amount(
1✔
997
            pair, enter_limit_requested,
998
            self.strategy.stoploss if not mode == 'pos_adjust' else 0.0,
999
            leverage)
1000
        max_stake_amount = self.exchange.get_max_pair_stake_amount(
1✔
1001
            pair, enter_limit_requested, leverage)
1002

1003
        if not self.edge and trade is None:
1✔
1004
            stake_available = self.wallets.get_available_stake_amount()
1✔
1005
            stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
1✔
1006
                                                 default_retval=stake_amount)(
1007
                pair=pair, current_time=datetime.now(timezone.utc),
1008
                current_rate=enter_limit_requested, proposed_stake=stake_amount,
1009
                min_stake=min_stake_amount, max_stake=min(max_stake_amount, stake_available),
1010
                leverage=leverage, entry_tag=entry_tag, side=trade_side
1011
            )
1012

1013
        stake_amount = self.wallets.validate_stake_amount(
1✔
1014
            pair=pair,
1015
            stake_amount=stake_amount,
1016
            min_stake_amount=min_stake_amount,
1017
            max_stake_amount=max_stake_amount,
1018
            trade_amount=trade.stake_amount if trade else None,
1019
        )
1020

1021
        return enter_limit_requested, stake_amount, leverage
1✔
1022

1023
    def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str],
1✔
1024
                      fill: bool = False, sub_trade: bool = False) -> None:
1025
        """
1026
        Sends rpc notification when a entry order occurred.
1027
        """
1028
        open_rate = order.safe_price
1✔
1029

1030
        if open_rate is None:
1✔
1031
            open_rate = trade.open_rate
×
1032

1033
        current_rate = self.exchange.get_rate(
1✔
1034
            trade.pair, side='entry', is_short=trade.is_short, refresh=False)
1035

1036
        msg: RPCEntryMsg = {
1✔
1037
            'trade_id': trade.id,
1038
            'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY,
1039
            'buy_tag': trade.enter_tag,
1040
            'enter_tag': trade.enter_tag,
1041
            'exchange': trade.exchange.capitalize(),
1042
            'pair': trade.pair,
1043
            'leverage': trade.leverage if trade.leverage else None,
1044
            'direction': 'Short' if trade.is_short else 'Long',
1045
            'limit': open_rate,  # Deprecated (?)
1046
            'open_rate': open_rate,
1047
            'order_type': order_type or 'unknown',
1048
            'stake_amount': trade.stake_amount,
1049
            'stake_currency': self.config['stake_currency'],
1050
            'base_currency': self.exchange.get_pair_base_currency(trade.pair),
1051
            'quote_currency': self.exchange.get_pair_quote_currency(trade.pair),
1052
            'fiat_currency': self.config.get('fiat_display_currency', None),
1053
            'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount),
1054
            'open_date': trade.open_date_utc or datetime.now(timezone.utc),
1055
            'current_rate': current_rate,
1056
            'sub_trade': sub_trade,
1057
        }
1058

1059
        # Send the message
1060
        self.rpc.send_msg(msg)
1✔
1061

1062
    def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str,
1✔
1063
                             sub_trade: bool = False) -> None:
1064
        """
1065
        Sends rpc notification when a entry order cancel occurred.
1066
        """
1067
        current_rate = self.exchange.get_rate(
1✔
1068
            trade.pair, side='entry', is_short=trade.is_short, refresh=False)
1069

1070
        msg: RPCCancelMsg = {
1✔
1071
            'trade_id': trade.id,
1072
            'type': RPCMessageType.ENTRY_CANCEL,
1073
            'buy_tag': trade.enter_tag,
1074
            'enter_tag': trade.enter_tag,
1075
            'exchange': trade.exchange.capitalize(),
1076
            'pair': trade.pair,
1077
            'leverage': trade.leverage,
1078
            'direction': 'Short' if trade.is_short else 'Long',
1079
            'limit': trade.open_rate,
1080
            'order_type': order_type,
1081
            'stake_amount': trade.stake_amount,
1082
            'open_rate': trade.open_rate,
1083
            'stake_currency': self.config['stake_currency'],
1084
            'base_currency': self.exchange.get_pair_base_currency(trade.pair),
1085
            'quote_currency': self.exchange.get_pair_quote_currency(trade.pair),
1086
            'fiat_currency': self.config.get('fiat_display_currency', None),
1087
            'amount': trade.amount,
1088
            'open_date': trade.open_date,
1089
            'current_rate': current_rate,
1090
            'reason': reason,
1091
            'sub_trade': sub_trade,
1092
        }
1093

1094
        # Send the message
1095
        self.rpc.send_msg(msg)
1✔
1096

1097
#
1098
# SELL / exit positions / close trades logic and methods
1099
#
1100

1101
    def exit_positions(self, trades: List[Trade]) -> int:
1✔
1102
        """
1103
        Tries to execute exit orders for open trades (positions)
1104
        """
1105
        trades_closed = 0
1✔
1106
        for trade in trades:
1✔
1107

1108
            if (
1✔
1109
                not trade.has_open_orders
1110
                and not trade.has_open_sl_orders
1111
                and not self.wallets.check_exit_amount(trade)
1112
            ):
1113
                logger.warning(
1✔
1114
                    f'Not enough {trade.safe_base_currency} in wallet to exit {trade}. '
1115
                    'Trying to recover.')
1116
                self.handle_onexchange_order(trade)
1✔
1117

1118
            try:
1✔
1119
                try:
1✔
1120
                    if (self.strategy.order_types.get('stoploss_on_exchange') and
1✔
1121
                            self.handle_stoploss_on_exchange(trade)):
1122
                        trades_closed += 1
1✔
1123
                        Trade.commit()
1✔
1124
                        continue
1✔
1125

1126
                except InvalidOrderException as exception:
×
1127
                    logger.warning(
×
1128
                        f'Unable to handle stoploss on exchange for {trade.pair}: {exception}')
1129
                # Check if we can sell our current pair
1130
                if not trade.has_open_orders and trade.is_open and self.handle_trade(trade):
1✔
1131
                    trades_closed += 1
1✔
1132

1133
            except DependencyException as exception:
1✔
1134
                logger.warning(f'Unable to exit trade {trade.pair}: {exception}')
1✔
1135

1136
        # Updating wallets if any trade occurred
1137
        if trades_closed:
1✔
1138
            self.wallets.update()
1✔
1139

1140
        return trades_closed
1✔
1141

1142
    def handle_trade(self, trade: Trade) -> bool:
1✔
1143
        """
1144
        Exits the current pair if the threshold is reached and updates the trade record.
1145
        :return: True if trade has been sold/exited_short, False otherwise
1146
        """
1147
        if not trade.is_open:
1✔
1148
            raise DependencyException(f'Attempt to handle closed trade: {trade}')
1✔
1149

1150
        logger.debug('Handling %s ...', trade)
1✔
1151

1152
        (enter, exit_) = (False, False)
1✔
1153
        exit_tag = None
1✔
1154
        exit_signal_type = "exit_short" if trade.is_short else "exit_long"
1✔
1155

1156
        if (self.config.get('use_exit_signal', True) or
1✔
1157
                self.config.get('ignore_roi_if_entry_signal', False)):
1158
            analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
1✔
1159
                                                                      self.strategy.timeframe)
1160

1161
            (enter, exit_, exit_tag) = self.strategy.get_exit_signal(
1✔
1162
                trade.pair,
1163
                self.strategy.timeframe,
1164
                analyzed_df,
1165
                is_short=trade.is_short
1166
            )
1167

1168
        logger.debug('checking exit')
1✔
1169
        exit_rate = self.exchange.get_rate(
1✔
1170
            trade.pair, side='exit', is_short=trade.is_short, refresh=True)
1171
        if self._check_and_execute_exit(trade, exit_rate, enter, exit_, exit_tag):
1✔
1172
            return True
1✔
1173

1174
        logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
1✔
1175
        return False
1✔
1176

1177
    def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
1✔
1178
                                enter: bool, exit_: bool, exit_tag: Optional[str]) -> bool:
1179
        """
1180
        Check and execute trade exit
1181
        """
1182
        exits: List[ExitCheckTuple] = self.strategy.should_exit(
1✔
1183
            trade,
1184
            exit_rate,
1185
            datetime.now(timezone.utc),
1186
            enter=enter,
1187
            exit_=exit_,
1188
            force_stoploss=self.edge.get_stoploss(trade.pair) if self.edge else 0
1189
        )
1190
        for should_exit in exits:
1✔
1191
            if should_exit.exit_flag:
1✔
1192
                exit_tag1 = exit_tag if should_exit.exit_type == ExitType.EXIT_SIGNAL else None
1✔
1193
                logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
1✔
1194
                            f'{f" Tag: {exit_tag1}" if exit_tag1 is not None else ""}')
1195
                exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag1)
1✔
1196
                if exited:
1✔
1197
                    return True
1✔
1198
        return False
1✔
1199

1200
    def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
1✔
1201
        """
1202
        Abstracts creating stoploss orders from the logic.
1203
        Handles errors and updates the trade database object.
1204
        Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
1205
        :return: True if the order succeeded, and False in case of problems.
1206
        """
1207
        try:
1✔
1208
            stoploss_order = self.exchange.create_stoploss(
1✔
1209
                pair=trade.pair,
1210
                amount=trade.amount,
1211
                stop_price=stop_price,
1212
                order_types=self.strategy.order_types,
1213
                side=trade.exit_side,
1214
                leverage=trade.leverage
1215
            )
1216

1217
            order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss',
1✔
1218
                                                     trade.amount, stop_price)
1219
            trade.orders.append(order_obj)
1✔
1220
            return True
1✔
1221
        except InsufficientFundsError as e:
1✔
1222
            logger.warning(f"Unable to place stoploss order {e}.")
1✔
1223
            # Try to figure out what went wrong
1224
            self.handle_insufficient_funds(trade)
1✔
1225

1226
        except InvalidOrderException as e:
1✔
1227
            logger.error(f'Unable to place a stoploss order on exchange. {e}')
1✔
1228
            logger.warning('Exiting the trade forcefully')
1✔
1229
            self.emergency_exit(trade, stop_price)
1✔
1230

1231
        except ExchangeError:
1✔
1232
            logger.exception('Unable to place a stoploss order on exchange.')
1✔
1233
        return False
1✔
1234

1235
    def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
1✔
1236
        """
1237
        Check if trade is fulfilled in which case the stoploss
1238
        on exchange should be added immediately if stoploss on exchange
1239
        is enabled.
1240
        # TODO: liquidation price always on exchange, even without stoploss_on_exchange
1241
        # Therefore fetching account liquidations for open pairs may make sense.
1242
        """
1243

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

1246
        stoploss_orders = []
1✔
1247
        for slo in trade.open_sl_orders:
1✔
1248
            stoploss_order = None
1✔
1249
            try:
1✔
1250
                # First we check if there is already a stoploss on exchange
1251
                stoploss_order = self.exchange.fetch_stoploss_order(
1✔
1252
                    slo.order_id, trade.pair) if slo.order_id else None
1253
            except InvalidOrderException as exception:
×
1254
                logger.warning('Unable to fetch stoploss order: %s', exception)
×
1255

1256
            if stoploss_order:
1✔
1257
                stoploss_orders.append(stoploss_order)
1✔
1258
                self.update_trade_state(trade, slo.order_id, stoploss_order,
1✔
1259
                                        stoploss_order=True)
1260

1261
            # We check if stoploss order is fulfilled
1262
            if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
1✔
1263
                trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
1✔
1264
                self._notify_exit(trade, "stoploss", True)
1✔
1265
                self.handle_protections(trade.pair, trade.trade_direction)
1✔
1266
                return True
1✔
1267

1268
        if trade.has_open_orders or not trade.is_open:
1✔
1269
            # Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case
1270
            # as the Amount on the exchange is tied up in another trade.
1271
            # The trade can be closed already (sell-order fill confirmation came in this iteration)
1272
            return False
1✔
1273

1274
        # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
1275
        if len(stoploss_orders) == 0:
1✔
1276
            stop_price = trade.stoploss_or_liquidation
1✔
1277
            if self.edge:
1✔
1278
                stoploss = self.edge.get_stoploss(pair=trade.pair)
×
1279
                stop_price = (
×
1280
                    trade.open_rate * (1 - stoploss) if trade.is_short
1281
                    else trade.open_rate * (1 + stoploss)
1282
                )
1283

1284
            if self.create_stoploss_order(trade=trade, stop_price=stop_price):
1✔
1285
                # The above will return False if the placement failed and the trade was force-sold.
1286
                # in which case the trade will be closed - which we must check below.
1287
                return False
1✔
1288

1289
        self.manage_trade_stoploss_orders(trade, stoploss_orders)
1✔
1290

1291
        return False
1✔
1292

1293
    def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: Dict) -> None:
1✔
1294
        """
1295
        Check to see if stoploss on exchange should be updated
1296
        in case of trailing stoploss on exchange
1297
        :param trade: Corresponding Trade
1298
        :param order: Current on exchange stoploss order
1299
        :return: None
1300
        """
1301
        stoploss_norm = self.exchange.price_to_precision(
1✔
1302
            trade.pair, trade.stoploss_or_liquidation,
1303
            rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP)
1304

1305
        if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
1✔
1306
            # we check if the update is necessary
1307
            update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
1✔
1308
            upd_req = datetime.now(timezone.utc) - timedelta(seconds=update_beat)
1✔
1309
            if trade.stoploss_last_update_utc and upd_req >= trade.stoploss_last_update_utc:
1✔
1310
                # cancelling the current stoploss on exchange first
1311
                logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} "
1✔
1312
                            f"(orderid:{order['id']}) in order to add another one ...")
1313

1314
                self.cancel_stoploss_on_exchange(trade)
1✔
1315
                if not trade.is_open:
1✔
1316
                    logger.warning(
×
1317
                        f"Trade {trade} is closed, not creating trailing stoploss order.")
1318
                    return
×
1319

1320
                # Create new stoploss order
1321
                if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
1✔
1322
                    logger.warning(f"Could not create trailing stoploss order "
1✔
1323
                                   f"for pair {trade.pair}.")
1324

1325
    def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: List[Dict]):
1✔
1326
        """
1327
        Perform required actions according to existing stoploss orders of trade
1328
        :param trade: Corresponding Trade
1329
        :param stoploss_orders: Current on exchange stoploss orders
1330
        :return: None
1331
        """
1332
        # If all stoploss ordered are canceled for some reason we add it again
1333
        canceled_sl_orders = [o for o in stoploss_orders
1✔
1334
                              if o['status'] in ('canceled', 'cancelled')]
1335
        if (
1✔
1336
            trade.is_open and
1337
            len(stoploss_orders) > 0 and
1338
            len(stoploss_orders) == len(canceled_sl_orders)
1339
        ):
1340
            if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation):
1✔
1341
                return False
1✔
1342
            else:
1343
                logger.warning('All Stoploss orders are cancelled, but unable to recreate one.')
1✔
1344

1345
        active_sl_orders = [o for o in stoploss_orders if o not in canceled_sl_orders]
1✔
1346
        if len(active_sl_orders) > 0:
1✔
1347
            last_active_sl_order = active_sl_orders[-1]
1✔
1348
            # Finally we check if stoploss on exchange should be moved up because of trailing.
1349
            # Triggered Orders are now real orders - so don't replace stoploss anymore
1350
            if (trade.is_open and
1✔
1351
                    last_active_sl_order.get('status_stop') != 'triggered' and
1352
                    (self.config.get('trailing_stop', False) or
1353
                     self.config.get('use_custom_stoploss', False))):
1354
                # if trailing stoploss is enabled we check if stoploss value has changed
1355
                # in which case we cancel stoploss order and put another one with new
1356
                # value immediately
1357
                self.handle_trailing_stoploss_on_exchange(trade, last_active_sl_order)
1✔
1358

1359
        return
1✔
1360

1361
    def manage_open_orders(self) -> None:
1✔
1362
        """
1363
        Management of open orders on exchange. Unfilled orders might be cancelled if timeout
1364
        was met or replaced if there's a new candle and user has requested it.
1365
        Timeout setting takes priority over limit order adjustment request.
1366
        :return: None
1367
        """
1368
        for trade in Trade.get_open_trades():
1✔
1369
            open_order: Order
1370
            for open_order in trade.open_orders:
1✔
1371
                try:
1✔
1372
                    order = self.exchange.fetch_order(open_order.order_id, trade.pair)
1✔
1373

1374
                except (ExchangeError):
1✔
1375
                    logger.info(
1✔
1376
                        'Cannot query order for %s due to %s', trade, traceback.format_exc()
1377
                    )
1378
                    continue
1✔
1379

1380
                fully_cancelled = self.update_trade_state(trade, open_order.order_id, order)
1✔
1381
                not_closed = order['status'] == 'open' or fully_cancelled
1✔
1382

1383
                if not_closed:
1✔
1384
                    if (
1✔
1385
                        fully_cancelled or (
1386
                            open_order and self.strategy.ft_check_timed_out(
1387
                                trade, open_order, datetime.now(timezone.utc)
1388
                            )
1389
                        )
1390
                    ):
1391
                        self.handle_cancel_order(
1✔
1392
                            order, open_order, trade, constants.CANCEL_REASON['TIMEOUT']
1393
                        )
1394
                    else:
1395
                        self.replace_order(order, open_order, trade)
1✔
1396

1397
    def handle_cancel_order(self, order: Dict, order_obj: Order, trade: Trade, reason: str) -> None:
1✔
1398
        """
1399
        Check if current analyzed order timed out and cancel if necessary.
1400
        :param order: Order dict grabbed with exchange.fetch_order()
1401
        :param order_obj: Order object from the database.
1402
        :param trade: Trade object.
1403
        :return: None
1404
        """
1405
        if order['side'] == trade.entry_side:
1✔
1406
            self.handle_cancel_enter(trade, order, order_obj, reason)
1✔
1407
        else:
1408
            canceled = self.handle_cancel_exit(trade, order, order_obj, reason)
1✔
1409
            canceled_count = trade.get_canceled_exit_order_count()
1✔
1410
            max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
1✔
1411
            if (canceled and max_timeouts > 0 and canceled_count >= max_timeouts):
1✔
1412
                logger.warning(f"Emergency exiting trade {trade}, as the exit order "
1✔
1413
                               f"timed out {max_timeouts} times. force selling {order['amount']}.")
1414
                self.emergency_exit(trade, order['price'], order['amount'])
1✔
1415

1416
    def emergency_exit(
1✔
1417
            self, trade: Trade, price: float, sub_trade_amt: Optional[float] = None) -> None:
1418
        try:
1✔
1419
            self.execute_trade_exit(
1✔
1420
                trade, price,
1421
                exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT),
1422
                sub_trade_amt=sub_trade_amt
1423
                )
1424
        except DependencyException as exception:
1✔
1425
            logger.warning(
1✔
1426
                f'Unable to emergency exit trade {trade.pair}: {exception}')
1427

1428
    def replace_order_failed(self, trade: Trade, msg: str) -> None:
1✔
1429
        """
1430
        Order replacement fail handling.
1431
        Deletes the trade if necessary.
1432
        :param trade: Trade object.
1433
        :param msg: Error message.
1434
        """
1435
        logger.warning(msg)
1✔
1436
        if trade.nr_of_successful_entries == 0:
1✔
1437
            # this is the first entry and we didn't get filled yet, delete trade
1438
            logger.warning(f"Removing {trade} from database.")
1✔
1439
            self._notify_enter_cancel(
1✔
1440
                trade, order_type=self.strategy.order_types['entry'],
1441
                reason=constants.CANCEL_REASON['REPLACE_FAILED'])
1442
            trade.delete()
1✔
1443

1444
    def replace_order(self, order: Dict, order_obj: Optional[Order], trade: Trade) -> None:
1✔
1445
        """
1446
        Check if current analyzed entry order should be replaced or simply cancelled.
1447
        To simply cancel the existing order(no replacement) adjust_entry_price() should return None
1448
        To maintain existing order adjust_entry_price() should return order_obj.price
1449
        To replace existing order adjust_entry_price() should return desired price for limit order
1450
        :param order: Order dict grabbed with exchange.fetch_order()
1451
        :param order_obj: Order object.
1452
        :param trade: Trade object.
1453
        :return: None
1454
        """
1455
        analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
1✔
1456
                                                                  self.strategy.timeframe)
1457
        latest_candle_open_date = analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None
1✔
1458
        latest_candle_close_date = timeframe_to_next_date(self.strategy.timeframe,
1✔
1459
                                                          latest_candle_open_date)
1460
        # Check if new candle
1461
        if (
1✔
1462
            order_obj and order_obj.side == trade.entry_side
1463
            and latest_candle_close_date > order_obj.order_date_utc
1464
        ):
1465
            # New candle
1466
            proposed_rate = self.exchange.get_rate(
1✔
1467
                trade.pair, side='entry', is_short=trade.is_short, refresh=True)
1468
            adjusted_entry_price = strategy_safe_wrapper(
1✔
1469
                self.strategy.adjust_entry_price, default_retval=order_obj.safe_placement_price)(
1470
                trade=trade, order=order_obj, pair=trade.pair,
1471
                current_time=datetime.now(timezone.utc), proposed_rate=proposed_rate,
1472
                current_order_rate=order_obj.safe_placement_price, entry_tag=trade.enter_tag,
1473
                side=trade.trade_direction)
1474

1475
            replacing = True
1✔
1476
            cancel_reason = constants.CANCEL_REASON['REPLACE']
1✔
1477
            if not adjusted_entry_price:
1✔
1478
                replacing = False
1✔
1479
                cancel_reason = constants.CANCEL_REASON['USER_CANCEL']
1✔
1480
            if order_obj.safe_placement_price != adjusted_entry_price:
1✔
1481
                # cancel existing order if new price is supplied or None
1482
                res = self.handle_cancel_enter(trade, order, order_obj, cancel_reason,
1✔
1483
                                               replacing=replacing)
1484
                if not res:
1✔
1485
                    self.replace_order_failed(
1✔
1486
                        trade, f"Could not cancel order for {trade}, therefore not replacing.")
1487
                    return
1✔
1488
                if adjusted_entry_price:
1✔
1489
                    # place new order only if new price is supplied
1490
                    try:
1✔
1491
                        if not self.execute_entry(
1✔
1492
                            pair=trade.pair,
1493
                            stake_amount=(
1494
                                order_obj.safe_remaining * order_obj.safe_price / trade.leverage),
1495
                            price=adjusted_entry_price,
1496
                            trade=trade,
1497
                            is_short=trade.is_short,
1498
                            mode='replace',
1499
                        ):
1500
                            self.replace_order_failed(
1✔
1501
                                trade, f"Could not replace order for {trade}.")
1502
                    except DependencyException as exception:
1✔
1503
                        logger.warning(
1✔
1504
                            f'Unable to replace order for {trade.pair}: {exception}')
1505
                        self.replace_order_failed(trade, f"Could not replace order for {trade}.")
1✔
1506

1507
    def cancel_all_open_orders(self) -> None:
1✔
1508
        """
1509
        Cancel all orders that are currently open
1510
        :return: None
1511
        """
1512

1513
        for trade in Trade.get_open_trades():
1✔
1514
            for open_order in trade.open_orders:
1✔
1515
                try:
1✔
1516
                    order = self.exchange.fetch_order(open_order.order_id, trade.pair)
1✔
1517
                except (ExchangeError):
1✔
1518
                    logger.info("Can't query order for %s due to %s", trade, traceback.format_exc())
1✔
1519
                    continue
1✔
1520

1521
                if order['side'] == trade.entry_side:
1✔
1522
                    self.handle_cancel_enter(
1✔
1523
                        trade, order, open_order, constants.CANCEL_REASON['ALL_CANCELLED']
1524
                    )
1525

1526
                elif order['side'] == trade.exit_side:
1✔
1527
                    self.handle_cancel_exit(
1✔
1528
                        trade, order, open_order, constants.CANCEL_REASON['ALL_CANCELLED']
1529
                    )
1530
        Trade.commit()
1✔
1531

1532
    def handle_cancel_enter(
1✔
1533
            self, trade: Trade, order: Dict, order_obj: Order,
1534
            reason: str, replacing: Optional[bool] = False
1535
    ) -> bool:
1536
        """
1537
        entry cancel - cancel order
1538
        :param order_obj: Order object from the database.
1539
        :param replacing: Replacing order - prevent trade deletion.
1540
        :return: True if trade was fully cancelled
1541
        """
1542
        was_trade_fully_canceled = False
1✔
1543
        order_id = order_obj.order_id
1✔
1544
        side = trade.entry_side.capitalize()
1✔
1545

1546
        if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
1✔
1547
            filled_val: float = order.get('filled', 0.0) or 0.0
1✔
1548
            filled_stake = filled_val * trade.open_rate
1✔
1549
            minstake = self.exchange.get_min_pair_stake_amount(
1✔
1550
                trade.pair, trade.open_rate, self.strategy.stoploss)
1551

1552
            if filled_val > 0 and minstake and filled_stake < minstake:
1✔
1553
                logger.warning(
1✔
1554
                    f"Order {order_id} for {trade.pair} not cancelled, "
1555
                    f"as the filled amount of {filled_val} would result in an unexitable trade.")
1556
                return False
1✔
1557
            corder = self.exchange.cancel_order_with_result(order_id, trade.pair, trade.amount)
1✔
1558
            order_obj.ft_cancel_reason = reason
1✔
1559
            # if replacing, retry fetching the order 3 times if the status is not what we need
1560
            if replacing:
1✔
1561
                retry_count = 0
1✔
1562
                while (
1✔
1563
                    corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES
1564
                    and retry_count < 3
1565
                ):
1566
                    sleep(0.5)
1✔
1567
                    corder = self.exchange.fetch_order(order_id, trade.pair)
1✔
1568
                    retry_count += 1
1✔
1569

1570
            # Avoid race condition where the order could not be cancelled coz its already filled.
1571
            # Simply bailing here is the only safe way - as this order will then be
1572
            # handled in the next iteration.
1573
            if corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES:
1✔
1574
                logger.warning(f"Order {order_id} for {trade.pair} not cancelled.")
1✔
1575
                return False
1✔
1576
        else:
1577
            # Order was cancelled already, so we can reuse the existing dict
1578
            corder = order
1✔
1579
            if order_obj.ft_cancel_reason is None:
1✔
1580
                order_obj.ft_cancel_reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
1✔
1581

1582
        logger.info(f'{side} order {order_obj.ft_cancel_reason} for {trade}.')
1✔
1583

1584
        # Using filled to determine the filled amount
1585
        filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
1✔
1586
        if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
1✔
1587
            was_trade_fully_canceled = True
1✔
1588
            # if trade is not partially completed and it's the only order, just delete the trade
1589
            open_order_count = len([
1✔
1590
                order for order in trade.orders if order.ft_is_open and order.order_id != order_id
1591
                ])
1592
            if open_order_count < 1 and trade.nr_of_successful_entries == 0 and not replacing:
1✔
1593
                logger.info(f'{side} order fully cancelled. Removing {trade} from database.')
1✔
1594
                trade.delete()
1✔
1595
                order_obj.ft_cancel_reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
1✔
1596
            else:
1597
                self.update_trade_state(trade, order_id, corder)
1✔
1598
                logger.info(f'{side} Order timeout for {trade}.')
1✔
1599
        else:
1600
            # update_trade_state (and subsequently recalc_trade_from_orders) will handle updates
1601
            # to the trade object
1602
            self.update_trade_state(trade, order_id, corder)
1✔
1603

1604
            logger.info(f'Partial {trade.entry_side} order timeout for {trade}.')
1✔
1605
            order_obj.ft_cancel_reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
1✔
1606

1607
        self.wallets.update()
1✔
1608
        self._notify_enter_cancel(trade, order_type=self.strategy.order_types['entry'],
1✔
1609
                                  reason=order_obj.ft_cancel_reason)
1610
        return was_trade_fully_canceled
1✔
1611

1612
    def handle_cancel_exit(
1✔
1613
        self, trade: Trade, order: Dict, order_obj: Order, reason: str
1614
    ) -> bool:
1615
        """
1616
        exit order cancel - cancel order and update trade
1617
        :return: True if exit order was cancelled, false otherwise
1618
        """
1619
        order_id = order_obj.order_id
1✔
1620
        cancelled = False
1✔
1621
        # Cancelled orders may have the status of 'canceled' or 'closed'
1622
        if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
1✔
1623
            filled_amt: float = order.get('filled', 0.0) or 0.0
1✔
1624
            # Filled val is in quote currency (after leverage)
1625
            filled_rem_stake = trade.stake_amount - (filled_amt * trade.open_rate / trade.leverage)
1✔
1626
            minstake = self.exchange.get_min_pair_stake_amount(
1✔
1627
                trade.pair, trade.open_rate, self.strategy.stoploss)
1628
            # Double-check remaining amount
1629
            if filled_amt > 0:
1✔
1630
                reason = constants.CANCEL_REASON['PARTIALLY_FILLED']
1✔
1631
                if minstake and filled_rem_stake < minstake:
1✔
1632
                    logger.warning(
1✔
1633
                        f"Order {order_id} for {trade.pair} not cancelled, as "
1634
                        f"the filled amount of {filled_amt} would result in an unexitable trade.")
1635
                    reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
1✔
1636

1637
                    self._notify_exit_cancel(
1✔
1638
                        trade,
1639
                        order_type=self.strategy.order_types['exit'],
1640
                        reason=reason, order_id=order['id'],
1641
                        sub_trade=trade.amount != order['amount']
1642
                    )
1643
                    return False
1✔
1644
            order_obj.ft_cancel_reason = reason
1✔
1645
            try:
1✔
1646
                order = self.exchange.cancel_order_with_result(
1✔
1647
                    order['id'], trade.pair, trade.amount)
1648
            except InvalidOrderException:
1✔
1649
                logger.exception(
1✔
1650
                    f"Could not cancel {trade.exit_side} order {order_id}")
1651
                return False
1✔
1652

1653
            # Set exit_reason for fill message
1654
            exit_reason_prev = trade.exit_reason
1✔
1655
            trade.exit_reason = trade.exit_reason + f", {reason}" if trade.exit_reason else reason
1✔
1656
            # Order might be filled above in odd timing issues.
1657
            if order.get('status') in ('canceled', 'cancelled'):
1✔
1658
                trade.exit_reason = None
1✔
1659
            else:
1660
                trade.exit_reason = exit_reason_prev
1✔
1661
            cancelled = True
1✔
1662
        else:
1663
            if order_obj.ft_cancel_reason is None:
1✔
1664
                order_obj.ft_cancel_reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
1✔
1665
            trade.exit_reason = None
1✔
1666

1667
        self.update_trade_state(trade, order['id'], order)
1✔
1668

1669
        logger.info(
1✔
1670
            f'{trade.exit_side.capitalize()} order {order_obj.ft_cancel_reason} for {trade}.')
1671
        trade.close_rate = None
1✔
1672
        trade.close_rate_requested = None
1✔
1673

1674
        self._notify_exit_cancel(
1✔
1675
            trade,
1676
            order_type=self.strategy.order_types['exit'],
1677
            reason=order_obj.ft_cancel_reason, order_id=order['id'],
1678
            sub_trade=trade.amount != order['amount']
1679
        )
1680
        return cancelled
1✔
1681

1682
    def _safe_exit_amount(self, trade: Trade, pair: str, amount: float) -> float:
1✔
1683
        """
1684
        Get sellable amount.
1685
        Should be trade.amount - but will fall back to the available amount if necessary.
1686
        This should cover cases where get_real_amount() was not able to update the amount
1687
        for whatever reason.
1688
        :param trade: Trade we're working with
1689
        :param pair: Pair we're trying to sell
1690
        :param amount: amount we expect to be available
1691
        :return: amount to sell
1692
        :raise: DependencyException: if available balance is not within 2% of the available amount.
1693
        """
1694
        # Update wallets to ensure amounts tied up in a stoploss is now free!
1695
        self.wallets.update()
1✔
1696
        if self.trading_mode == TradingMode.FUTURES:
1✔
1697
            # A safe exit amount isn't needed for futures, you can just exit/close the position
1698
            return amount
1✔
1699

1700
        trade_base_currency = self.exchange.get_pair_base_currency(pair)
1✔
1701
        wallet_amount = self.wallets.get_free(trade_base_currency)
1✔
1702
        logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}")
1✔
1703
        if wallet_amount >= amount:
1✔
1704
            return amount
1✔
1705
        elif wallet_amount > amount * 0.98:
1✔
1706
            logger.info(f"{pair} - Falling back to wallet-amount {wallet_amount} -> {amount}.")
1✔
1707
            trade.amount = wallet_amount
1✔
1708
            return wallet_amount
1✔
1709
        else:
1710
            raise DependencyException(
1✔
1711
                f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}")
1712

1713
    def execute_trade_exit(
1✔
1714
            self,
1715
            trade: Trade,
1716
            limit: float,
1717
            exit_check: ExitCheckTuple,
1718
            *,
1719
            exit_tag: Optional[str] = None,
1720
            ordertype: Optional[str] = None,
1721
            sub_trade_amt: Optional[float] = None,
1722
    ) -> bool:
1723
        """
1724
        Executes a trade exit for the given trade and limit
1725
        :param trade: Trade instance
1726
        :param limit: limit rate for the sell order
1727
        :param exit_check: CheckTuple with signal and reason
1728
        :return: True if it succeeds False
1729
        """
1730
        trade.set_funding_fees(
1✔
1731
            self.exchange.get_funding_fees(
1732
                pair=trade.pair,
1733
                amount=trade.amount,
1734
                is_short=trade.is_short,
1735
                open_date=trade.date_last_filled_utc)
1736
        )
1737

1738
        exit_type = 'exit'
1✔
1739
        exit_reason = exit_tag or exit_check.exit_reason
1✔
1740
        if exit_check.exit_type in (
1✔
1741
                ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION):
1742
            exit_type = 'stoploss'
1✔
1743

1744
        # set custom_exit_price if available
1745
        proposed_limit_rate = limit
1✔
1746
        current_profit = trade.calc_profit_ratio(limit)
1✔
1747
        custom_exit_price = strategy_safe_wrapper(self.strategy.custom_exit_price,
1✔
1748
                                                  default_retval=proposed_limit_rate)(
1749
            pair=trade.pair, trade=trade,
1750
            current_time=datetime.now(timezone.utc),
1751
            proposed_rate=proposed_limit_rate, current_profit=current_profit,
1752
            exit_tag=exit_reason)
1753

1754
        limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
1✔
1755

1756
        # First cancelling stoploss on exchange ...
1757
        trade = self.cancel_stoploss_on_exchange(trade)
1✔
1758

1759
        order_type = ordertype or self.strategy.order_types[exit_type]
1✔
1760
        if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
1✔
1761
            # Emergency sells (default to market!)
1762
            order_type = self.strategy.order_types.get("emergency_exit", "market")
1✔
1763

1764
        amount = self._safe_exit_amount(trade, trade.pair, sub_trade_amt or trade.amount)
1✔
1765
        time_in_force = self.strategy.order_time_in_force['exit']
1✔
1766

1767
        if (exit_check.exit_type != ExitType.LIQUIDATION
1✔
1768
                and not sub_trade_amt
1769
                and not strategy_safe_wrapper(
1770
                    self.strategy.confirm_trade_exit, default_retval=True)(
1771
                    pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
1772
                    time_in_force=time_in_force, exit_reason=exit_reason,
1773
                    sell_reason=exit_reason,  # sellreason -> compatibility
1774
                    current_time=datetime.now(timezone.utc))):
1775
            logger.info(f"User denied exit for {trade.pair}.")
1✔
1776
            return False
1✔
1777

1778
        try:
1✔
1779
            # Execute sell and update trade record
1780
            order = self.exchange.create_order(
1✔
1781
                pair=trade.pair,
1782
                ordertype=order_type,
1783
                side=trade.exit_side,
1784
                amount=amount,
1785
                rate=limit,
1786
                leverage=trade.leverage,
1787
                reduceOnly=self.trading_mode == TradingMode.FUTURES,
1788
                time_in_force=time_in_force
1789
            )
1790
        except InsufficientFundsError as e:
1✔
1791
            logger.warning(f"Unable to place order {e}.")
1✔
1792
            # Try to figure out what went wrong
1793
            self.handle_insufficient_funds(trade)
1✔
1794
            return False
1✔
1795

1796
        order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side, amount, limit)
1✔
1797
        order_obj.ft_order_tag = exit_reason
1✔
1798
        trade.orders.append(order_obj)
1✔
1799

1800
        trade.exit_order_status = ''
1✔
1801
        trade.close_rate_requested = limit
1✔
1802
        trade.exit_reason = exit_reason
1✔
1803

1804
        self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
1✔
1805
        # In case of market sell orders the order can be closed immediately
1806
        if order.get('status', 'unknown') in ('closed', 'expired'):
1✔
1807
            self.update_trade_state(trade, order_obj.order_id, order)
1✔
1808
        Trade.commit()
1✔
1809

1810
        return True
1✔
1811

1812
    def _notify_exit(self, trade: Trade, order_type: Optional[str], fill: bool = False,
1✔
1813
                     sub_trade: bool = False, order: Optional[Order] = None) -> None:
1814
        """
1815
        Sends rpc notification when a sell occurred.
1816
        """
1817
        # Use cached rates here - it was updated seconds ago.
1818
        current_rate = self.exchange.get_rate(
1✔
1819
            trade.pair, side='exit', is_short=trade.is_short, refresh=False) if not fill else None
1820

1821
        # second condition is for mypy only; order will always be passed during sub trade
1822
        if sub_trade and order is not None:
1✔
1823
            amount = order.safe_filled if fill else order.safe_amount
1✔
1824
            order_rate: float = order.safe_price
1✔
1825

1826
            profit = trade.calculate_profit(order_rate, amount, trade.open_rate)
1✔
1827
        else:
1828
            order_rate = trade.safe_close_rate
1✔
1829
            profit = trade.calculate_profit(rate=order_rate)
1✔
1830
            amount = trade.amount
1✔
1831
        gain: ProfitLossStr = "profit" if profit.profit_ratio > 0 else "loss"
1✔
1832

1833
        msg: RPCExitMsg = {
1✔
1834
            'type': (RPCMessageType.EXIT_FILL if fill
1835
                     else RPCMessageType.EXIT),
1836
            'trade_id': trade.id,
1837
            'exchange': trade.exchange.capitalize(),
1838
            'pair': trade.pair,
1839
            'leverage': trade.leverage,
1840
            'direction': 'Short' if trade.is_short else 'Long',
1841
            'gain': gain,
1842
            'limit': order_rate,  # Deprecated
1843
            'order_rate': order_rate,
1844
            'order_type': order_type or 'unknown',
1845
            'amount': amount,
1846
            'open_rate': trade.open_rate,
1847
            'close_rate': order_rate,
1848
            'current_rate': current_rate,
1849
            'profit_amount': profit.profit_abs,
1850
            'profit_ratio': profit.profit_ratio,
1851
            'buy_tag': trade.enter_tag,
1852
            'enter_tag': trade.enter_tag,
1853
            'exit_reason': trade.exit_reason,
1854
            'open_date': trade.open_date_utc,
1855
            'close_date': trade.close_date_utc or datetime.now(timezone.utc),
1856
            'stake_amount': trade.stake_amount,
1857
            'stake_currency': self.config['stake_currency'],
1858
            'base_currency': self.exchange.get_pair_base_currency(trade.pair),
1859
            'quote_currency': self.exchange.get_pair_quote_currency(trade.pair),
1860
            'fiat_currency': self.config.get('fiat_display_currency'),
1861
            'sub_trade': sub_trade,
1862
            'cumulative_profit': trade.realized_profit,
1863
            'final_profit_ratio': trade.close_profit if not trade.is_open else None,
1864
            'is_final_exit': trade.is_open is False,
1865
        }
1866

1867
        # Send the message
1868
        self.rpc.send_msg(msg)
1✔
1869

1870
    def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str,
1✔
1871
                            order_id: str, sub_trade: bool = False) -> None:
1872
        """
1873
        Sends rpc notification when a sell cancel occurred.
1874
        """
1875
        if trade.exit_order_status == reason:
1✔
1876
            return
1✔
1877
        else:
1878
            trade.exit_order_status = reason
1✔
1879

1880
        order_or_none = trade.select_order_by_order_id(order_id)
1✔
1881
        order = self.order_obj_or_raise(order_id, order_or_none)
1✔
1882

1883
        profit_rate: float = trade.safe_close_rate
1✔
1884
        profit = trade.calculate_profit(rate=profit_rate)
1✔
1885
        current_rate = self.exchange.get_rate(
1✔
1886
            trade.pair, side='exit', is_short=trade.is_short, refresh=False)
1887
        gain: ProfitLossStr = "profit" if profit.profit_ratio > 0 else "loss"
1✔
1888

1889
        msg: RPCExitCancelMsg = {
1✔
1890
            'type': RPCMessageType.EXIT_CANCEL,
1891
            'trade_id': trade.id,
1892
            'exchange': trade.exchange.capitalize(),
1893
            'pair': trade.pair,
1894
            'leverage': trade.leverage,
1895
            'direction': 'Short' if trade.is_short else 'Long',
1896
            'gain': gain,
1897
            'limit': profit_rate or 0,
1898
            'order_type': order_type,
1899
            'amount': order.safe_amount_after_fee,
1900
            'open_rate': trade.open_rate,
1901
            'current_rate': current_rate,
1902
            'profit_amount': profit.profit_abs,
1903
            'profit_ratio': profit.profit_ratio,
1904
            'buy_tag': trade.enter_tag,
1905
            'enter_tag': trade.enter_tag,
1906
            'exit_reason': trade.exit_reason,
1907
            'open_date': trade.open_date,
1908
            'close_date': trade.close_date or datetime.now(timezone.utc),
1909
            'stake_currency': self.config['stake_currency'],
1910
            'base_currency': self.exchange.get_pair_base_currency(trade.pair),
1911
            'quote_currency': self.exchange.get_pair_quote_currency(trade.pair),
1912
            'fiat_currency': self.config.get('fiat_display_currency', None),
1913
            'reason': reason,
1914
            'sub_trade': sub_trade,
1915
            'stake_amount': trade.stake_amount,
1916
        }
1917

1918
        # Send the message
1919
        self.rpc.send_msg(msg)
1✔
1920

1921
    def order_obj_or_raise(self, order_id: str, order_obj: Optional[Order]) -> Order:
1✔
1922
        if not order_obj:
1✔
1923
            raise DependencyException(
×
1924
                f"Order_obj not found for {order_id}. This should not have happened.")
1925
        return order_obj
1✔
1926

1927
#
1928
# Common update trade state methods
1929
#
1930

1931
    def update_trade_state(
1✔
1932
            self, trade: Trade, order_id: Optional[str],
1933
            action_order: Optional[Dict[str, Any]] = None, *,
1934
            stoploss_order: bool = False, send_msg: bool = True) -> bool:
1935
        """
1936
        Checks trades with open orders and updates the amount if necessary
1937
        Handles closing both buy and sell orders.
1938
        :param trade: Trade object of the trade we're analyzing
1939
        :param order_id: Order-id of the order we're analyzing
1940
        :param action_order: Already acquired order object
1941
        :param send_msg: Send notification - should always be True except in "recovery" methods
1942
        :return: True if order has been cancelled without being filled partially, False otherwise
1943
        """
1944
        if not order_id:
1✔
1945
            logger.warning(f'Orderid for trade {trade} is empty.')
1✔
1946
            return False
1✔
1947

1948
        # Update trade with order values
1949
        if not stoploss_order:
1✔
1950
            logger.info(f'Found open order for {trade}')
1✔
1951
        try:
1✔
1952
            order = action_order or self.exchange.fetch_order_or_stoploss_order(
1✔
1953
                order_id, trade.pair, stoploss_order)
1954
        except InvalidOrderException as exception:
1✔
1955
            logger.warning('Unable to fetch order %s: %s', order_id, exception)
1✔
1956
            return False
1✔
1957

1958
        trade.update_order(order)
1✔
1959

1960
        if self.exchange.check_order_canceled_empty(order):
1✔
1961
            # Trade has been cancelled on exchange
1962
            # Handling of this will happen in handle_cancel_order.
1963
            return True
1✔
1964

1965
        order_obj_or_none = trade.select_order_by_order_id(order_id)
1✔
1966
        order_obj = self.order_obj_or_raise(order_id, order_obj_or_none)
1✔
1967

1968
        self.handle_order_fee(trade, order_obj, order)
1✔
1969

1970
        trade.update_trade(order_obj, not send_msg)
1✔
1971

1972
        trade = self._update_trade_after_fill(trade, order_obj, send_msg)
1✔
1973
        Trade.commit()
1✔
1974

1975
        self.order_close_notify(trade, order_obj, stoploss_order, send_msg)
1✔
1976

1977
        return False
1✔
1978

1979
    def _update_trade_after_fill(self, trade: Trade, order: Order, send_msg: bool) -> Trade:
1✔
1980
        if order.status in constants.NON_OPEN_EXCHANGE_STATES:
1✔
1981
            strategy_safe_wrapper(
1✔
1982
                self.strategy.order_filled, default_retval=None)(
1983
                pair=trade.pair, trade=trade, order=order, current_time=datetime.now(timezone.utc))
1984
            # If a entry order was closed, force update on stoploss on exchange
1985
            if order.ft_order_side == trade.entry_side:
1✔
1986
                if send_msg:
1✔
1987
                    # Don't cancel stoploss in recovery modes immediately
1988
                    trade = self.cancel_stoploss_on_exchange(trade)
1✔
1989
                if not self.edge:
1✔
1990
                    # TODO: should shorting/leverage be supported by Edge,
1991
                    # then this will need to be fixed.
1992
                    trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
1✔
1993
            if order.ft_order_side == trade.entry_side or (trade.amount > 0 and trade.is_open):
1✔
1994
                # Must also run for partial exits
1995
                # TODO: Margin will need to use interest_rate as well.
1996
                # interest_rate = self.exchange.get_interest_rate()
1997
                try:
1✔
1998
                    trade.set_liquidation_price(self.exchange.get_liquidation_price(
1✔
1999
                        pair=trade.pair,
2000
                        open_rate=trade.open_rate,
2001
                        is_short=trade.is_short,
2002
                        amount=trade.amount,
2003
                        stake_amount=trade.stake_amount,
2004
                        leverage=trade.leverage,
2005
                        wallet_balance=trade.stake_amount,
2006
                    ))
2007
                except DependencyException:
1✔
2008
                    logger.warning('Unable to calculate liquidation price')
1✔
2009
                if self.strategy.use_custom_stoploss:
1✔
2010
                    current_rate = self.exchange.get_rate(
1✔
2011
                        trade.pair, side='exit', is_short=trade.is_short, refresh=True)
2012
                    profit = trade.calc_profit_ratio(current_rate)
1✔
2013
                    self.strategy.ft_stoploss_adjust(current_rate, trade,
1✔
2014
                                                     datetime.now(timezone.utc), profit, 0,
2015
                                                     after_fill=True)
2016
            # Updating wallets when order is closed
2017
            self.wallets.update()
1✔
2018
        return trade
1✔
2019

2020
    def order_close_notify(
1✔
2021
            self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool):
2022
        """send "fill" notifications"""
2023

2024
        if order.ft_order_side == trade.exit_side:
1✔
2025
            # Exit notification
2026
            if send_msg and not stoploss_order and order.order_id not in trade.open_orders_ids:
1✔
2027
                self._notify_exit(trade, order.order_type, fill=True,
1✔
2028
                                  sub_trade=trade.is_open, order=order)
2029
            if not trade.is_open:
1✔
2030
                self.handle_protections(trade.pair, trade.trade_direction)
1✔
2031
        elif send_msg and order.order_id not in trade.open_orders_ids and not stoploss_order:
1✔
2032
            sub_trade = not isclose(order.safe_amount_after_fee,
1✔
2033
                                    trade.amount, abs_tol=constants.MATH_CLOSE_PREC)
2034
            # Enter fill
2035
            self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade)
1✔
2036

2037
    def handle_protections(self, pair: str, side: LongShort) -> None:
1✔
2038
        # Lock pair for one candle to prevent immediate rebuys
2039
        self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason='Auto lock')
1✔
2040
        prot_trig = self.protections.stop_per_pair(pair, side=side)
1✔
2041
        if prot_trig:
1✔
2042
            msg: RPCProtectionMsg = {
1✔
2043
                'type': RPCMessageType.PROTECTION_TRIGGER,
2044
                'base_currency': self.exchange.get_pair_base_currency(prot_trig.pair),
2045
                **prot_trig.to_json()  # type: ignore
2046
            }
2047
            self.rpc.send_msg(msg)
1✔
2048

2049
        prot_trig_glb = self.protections.global_stop(side=side)
1✔
2050
        if prot_trig_glb:
1✔
2051
            msg = {
1✔
2052
                'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
2053
                'base_currency': self.exchange.get_pair_base_currency(prot_trig_glb.pair),
2054
                **prot_trig_glb.to_json()  # type: ignore
2055
            }
2056
            self.rpc.send_msg(msg)
1✔
2057

2058
    def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
1✔
2059
                              amount: float, fee_abs: float, order_obj: Order) -> Optional[float]:
2060
        """
2061
        Applies the fee to amount (either from Order or from Trades).
2062
        Can eat into dust if more than the required asset is available.
2063
        In case of trade adjustment orders, trade.amount will not have been adjusted yet.
2064
        Can't happen in Futures mode - where Fees are always in settlement currency,
2065
        never in base currency.
2066
        """
2067
        self.wallets.update()
1✔
2068
        amount_ = trade.amount
1✔
2069
        if order_obj.ft_order_side == trade.exit_side or order_obj.ft_order_side == 'stoploss':
1✔
2070
            # check against remaining amount!
2071
            amount_ = trade.amount - amount
1✔
2072

2073
        if trade.nr_of_successful_entries >= 1 and order_obj.ft_order_side == trade.entry_side:
1✔
2074
            # In case of rebuy's, trade.amount doesn't contain the amount of the last entry.
2075
            amount_ = trade.amount + amount
1✔
2076

2077
        if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount_:
1✔
2078
            # Eat into dust if we own more than base currency
2079
            logger.info(f"Fee amount for {trade} was in base currency - "
1✔
2080
                        f"Eating Fee {fee_abs} into dust.")
2081
        elif fee_abs != 0:
1✔
2082
            logger.info(f"Applying fee on amount for {trade}, fee={fee_abs}.")
1✔
2083
            return fee_abs
1✔
2084
        return None
1✔
2085

2086
    def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None:
1✔
2087
        # Try update amount (binance-fix)
2088
        try:
1✔
2089
            fee_abs = self.get_real_amount(trade, order, order_obj)
1✔
2090
            if fee_abs is not None:
1✔
2091
                order_obj.ft_fee_base = fee_abs
1✔
2092
        except DependencyException as exception:
1✔
2093
            logger.warning("Could not update trade amount: %s", exception)
1✔
2094

2095
    def get_real_amount(self, trade: Trade, order: Dict, order_obj: Order) -> Optional[float]:
1✔
2096
        """
2097
        Detect and update trade fee.
2098
        Calls trade.update_fee() upon correct detection.
2099
        Returns modified amount if the fee was taken from the destination currency.
2100
        Necessary for exchanges which charge fees in base currency (e.g. binance)
2101
        :return: Absolute fee to apply for this order or None
2102
        """
2103
        # Init variables
2104
        order_amount = safe_value_fallback(order, 'filled', 'amount')
1✔
2105
        # Only run for closed orders
2106
        if (
1✔
2107
            trade.fee_updated(order.get('side', ''))
2108
            or order['status'] == 'open'
2109
            or order_obj.ft_fee_base
2110
        ):
2111
            return None
1✔
2112

2113
        trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
1✔
2114
        # use fee from order-dict if possible
2115
        if self.exchange.order_has_fee(order):
1✔
2116
            fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(
1✔
2117
                order['fee'], order['symbol'], order['cost'], order_obj.safe_filled)
2118
            logger.info(f"Fee for Trade {trade} [{order_obj.ft_order_side}]: "
1✔
2119
                        f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
2120
            if fee_rate is None or fee_rate < 0.02:
1✔
2121
                # Reject all fees that report as > 2%.
2122
                # These are most likely caused by a parsing bug in ccxt
2123
                # due to multiple trades (https://github.com/ccxt/ccxt/issues/8025)
2124
                trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
1✔
2125
                if trade_base_currency == fee_currency:
1✔
2126
                    # Apply fee to amount
2127
                    return self.apply_fee_conditional(trade, trade_base_currency,
1✔
2128
                                                      amount=order_amount, fee_abs=fee_cost,
2129
                                                      order_obj=order_obj)
2130
                return None
1✔
2131
        return self.fee_detection_from_trades(
1✔
2132
            trade, order, order_obj, order_amount, order.get('trades', []))
2133

2134
    def fee_detection_from_trades(self, trade: Trade, order: Dict, order_obj: Order,
1✔
2135
                                  order_amount: float, trades: List) -> Optional[float]:
2136
        """
2137
        fee-detection fallback to Trades.
2138
        Either uses provided trades list or the result of fetch_my_trades to get correct fee.
2139
        """
2140
        if not trades:
1✔
2141
            trades = self.exchange.get_trades_for_order(
1✔
2142
                self.exchange.get_order_id_conditional(order), trade.pair, order_obj.order_date)
2143

2144
        if len(trades) == 0:
1✔
2145
            logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
1✔
2146
            return None
1✔
2147
        fee_currency = None
1✔
2148
        amount = 0
1✔
2149
        fee_abs = 0.0
1✔
2150
        fee_cost = 0.0
1✔
2151
        trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
1✔
2152
        fee_rate_array: List[float] = []
1✔
2153
        for exectrade in trades:
1✔
2154
            amount += exectrade['amount']
1✔
2155
            if self.exchange.order_has_fee(exectrade):
1✔
2156
                # Prefer singular fee
2157
                fees = [exectrade['fee']]
1✔
2158
            else:
2159
                fees = exectrade.get('fees', [])
1✔
2160
            for fee in fees:
1✔
2161

2162
                fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(
1✔
2163
                    fee, exectrade['symbol'], exectrade['cost'], exectrade['amount']
2164
                )
2165
                fee_cost += fee_cost_
1✔
2166
                if fee_rate_ is not None:
1✔
2167
                    fee_rate_array.append(fee_rate_)
1✔
2168
                # only applies if fee is in quote currency!
2169
                if trade_base_currency == fee_currency:
1✔
2170
                    fee_abs += fee_cost_
1✔
2171
        # Ensure at least one trade was found:
2172
        if fee_currency:
1✔
2173
            # fee_rate should use mean
2174
            fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None
1✔
2175
            if fee_rate is not None and fee_rate < 0.02:
1✔
2176
                # Only update if fee-rate is < 2%
2177
                trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
1✔
2178
            else:
2179
                logger.warning(
1✔
2180
                    f"Not updating {order.get('side', '')}-fee - rate: {fee_rate}, {fee_currency}.")
2181

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

2187
        if fee_abs != 0:
1✔
2188
            return self.apply_fee_conditional(
1✔
2189
                trade, trade_base_currency, amount=amount, fee_abs=fee_abs, order_obj=order_obj)
2190
        return None
1✔
2191

2192
    def get_valid_price(self, custom_price: float, proposed_price: float) -> float:
1✔
2193
        """
2194
        Return the valid price.
2195
        Check if the custom price is of the good type if not return proposed_price
2196
        :return: valid price for the order
2197
        """
2198
        if custom_price:
1✔
2199
            try:
1✔
2200
                valid_custom_price = float(custom_price)
1✔
2201
            except ValueError:
1✔
2202
                valid_custom_price = proposed_price
1✔
2203
        else:
2204
            valid_custom_price = proposed_price
1✔
2205

2206
        cust_p_max_dist_r = self.config.get('custom_price_max_distance_ratio', 0.02)
1✔
2207
        min_custom_price_allowed = proposed_price - (proposed_price * cust_p_max_dist_r)
1✔
2208
        max_custom_price_allowed = proposed_price + (proposed_price * cust_p_max_dist_r)
1✔
2209

2210
        # Bracket between min_custom_price_allowed and max_custom_price_allowed
2211
        return max(
1✔
2212
            min(valid_custom_price, max_custom_price_allowed),
2213
            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