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

freqtrade / freqtrade / 6181253459

08 Sep 2023 06:04AM UTC coverage: 94.614% (+0.06%) from 94.556%
6181253459

push

github-actions

web-flow
Merge pull request #9159 from stash86/fix-adjust

remove old codes when we only can do partial entries

2 of 2 new or added lines in 1 file covered. (100.0%)

19114 of 20202 relevant lines covered (94.61%)

0.95 hits per line

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

96.82
/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 typing import Any, Dict, List, Optional, Tuple
1✔
11

12
from schedule import Scheduler
1✔
13

14
from freqtrade import constants
1✔
15
from freqtrade.configuration import validate_config_consistency
1✔
16
from freqtrade.constants import BuySell, Config, ExchangeConfig, LongShort
1✔
17
from freqtrade.data.converter import order_book_to_dataframe
1✔
18
from freqtrade.data.dataprovider import DataProvider
1✔
19
from freqtrade.edge import Edge
1✔
20
from freqtrade.enums import (ExitCheckTuple, ExitType, RPCMessageType, RunMode, SignalDirection,
1✔
21
                             State, TradingMode)
22
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
1✔
23
                                  InvalidOrderException, PricingError)
24
from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, timeframe_to_minutes, timeframe_to_next_date,
1✔
25
                                timeframe_to_seconds)
26
from freqtrade.exchange.common import remove_exchange_credentials
1✔
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 (RPCBuyMsg, RPCCancelMsg, RPCProtectionMsg, RPCSellCancelMsg,
1✔
37
                                     RPCSellMsg)
38
from freqtrade.strategy.interface import IStrategy
1✔
39
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
1✔
40
from freqtrade.util import FtPrecise
1✔
41
from freqtrade.util.binance_mig 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 credentail 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.pairlists = PairListManager(self.exchange, self.config)
1✔
86

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

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

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

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

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

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

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

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

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

122
        self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT)
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
            # TODO: This would be more efficient if scheduled in utc time, and performed at each
133
            # TODO: funding interval, specified by funding_fee_times on the exchange classes
134
            for time_slot in range(0, 24):
1✔
135
                for minutes in [0, 15, 30, 45]:
1✔
136
                    t = str(time(time_slot, minutes, 2))
1✔
137
                    self._schedule.every().day.at(t).do(update)
1✔
138
        self.last_process: Optional[datetime] = None
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 notify_status(self, msg: str, msg_type=RPCMessageType.STATUS) -> None:
1✔
145
        """
146
        Public method for users of this class (worker, etc.) to send notifications
147
        via RPC about changes in the bot status.
148
        """
149
        self.rpc.send_msg({
1✔
150
            'type': msg_type,
151
            'status': msg
152
        })
153

154
    def cleanup(self) -> None:
1✔
155
        """
156
        Cleanup pending resources on an already stopped bot
157
        :return: None
158
        """
159
        logger.info('Cleaning up modules ...')
1✔
160
        try:
1✔
161
            # Wrap db activities in shutdown to avoid problems if database is gone,
162
            # and raises further exceptions.
163
            if self.config['cancel_open_orders_on_exit']:
1✔
164
                self.cancel_all_open_orders()
1✔
165

166
            self.check_for_open_trades()
1✔
167
        except Exception as e:
1✔
168
            logger.warning(f'Exception during cleanup: {e.__class__.__name__} {e}')
1✔
169

170
        finally:
171
            self.strategy.ft_bot_cleanup()
1✔
172

173
        self.rpc.cleanup()
1✔
174
        if self.emc:
1✔
175
            self.emc.shutdown()
1✔
176
        self.exchange.close()
1✔
177
        try:
1✔
178
            Trade.commit()
1✔
179
        except Exception:
1✔
180
            # Exeptions here will be happening if the db disappeared.
181
            # At which point we can no longer commit anyway.
182
            pass
1✔
183

184
    def startup(self) -> None:
1✔
185
        """
186
        Called on startup and after reloading the bot - triggers notifications and
187
        performs startup tasks
188
        """
189
        migrate_binance_futures_names(self.config)
1✔
190
        set_startup_time()
1✔
191

192
        self.rpc.startup_messages(self.config, self.pairlists, self.protections)
1✔
193
        # Update older trades with precision and precision mode
194
        self.startup_backpopulate_precision()
1✔
195
        if not self.edge:
1✔
196
            # Adjust stoploss if it was changed
197
            Trade.stoploss_reinitialization(self.strategy.stoploss)
1✔
198

199
        # Only update open orders on startup
200
        # This will update the database after the initial migration
201
        self.startup_update_open_orders()
1✔
202

203
    def process(self) -> None:
1✔
204
        """
205
        Queries the persistence layer for open trades and handles them,
206
        otherwise a new trade is created.
207
        :return: True if one or more trades has been created or closed, False otherwise
208
        """
209

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

213
        self.update_trades_without_assigned_fees()
1✔
214

215
        # Query trades from persistence layer
216
        trades: List[Trade] = Trade.get_open_trades()
1✔
217

218
        self.active_pair_whitelist = self._refresh_active_whitelist(trades)
1✔
219

220
        # Refreshing candles
221
        self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
1✔
222
                                  self.strategy.gather_informative_pairs())
223

224
        strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)(
1✔
225
            current_time=datetime.now(timezone.utc))
226

227
        self.strategy.analyze(self.active_pair_whitelist)
1✔
228

229
        with self._exit_lock:
1✔
230
            # Check for exchange cancelations, timeouts and user requested replace
231
            self.manage_open_orders()
1✔
232

233
        # Protect from collisions with force_exit.
234
        # Without this, freqtrade may try to recreate stoploss_on_exchange orders
235
        # while exiting is in process, since telegram messages arrive in an different thread.
236
        with self._exit_lock:
1✔
237
            trades = Trade.get_open_trades()
1✔
238
            # First process current opened trades (positions)
239
            self.exit_positions(trades)
1✔
240

241
        # Check if we need to adjust our current positions before attempting to buy new trades.
242
        if self.strategy.position_adjustment_enable:
1✔
243
            with self._exit_lock:
1✔
244
                self.process_open_trade_positions()
1✔
245

246
        # Then looking for buy opportunities
247
        if self.get_free_open_trades():
1✔
248
            self.enter_positions()
1✔
249
        if self.trading_mode == TradingMode.FUTURES:
1✔
250
            self._schedule.run_pending()
1✔
251
        Trade.commit()
1✔
252
        self.rpc.process_msg_queue(self.dataprovider._msg_queue)
1✔
253
        self.last_process = datetime.now(timezone.utc)
1✔
254

255
    def process_stopped(self) -> None:
1✔
256
        """
257
        Close all orders that were left open
258
        """
259
        if self.config['cancel_open_orders_on_exit']:
1✔
260
            self.cancel_all_open_orders()
1✔
261

262
    def check_for_open_trades(self):
1✔
263
        """
264
        Notify the user when the bot is stopped (not reloaded)
265
        and there are still open trades active.
266
        """
267
        open_trades = Trade.get_open_trades()
1✔
268

269
        if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG:
1✔
270
            msg = {
1✔
271
                'type': RPCMessageType.WARNING,
272
                'status':
273
                    f"{len(open_trades)} open trades active.\n\n"
274
                    f"Handle these trades manually on {self.exchange.name}, "
275
                    f"or '/start' the bot again and use '/stopentry' "
276
                    f"to handle open trades gracefully. \n"
277
                    f"{'Note: Trades are simulated (dry run).' if self.config['dry_run'] else ''}",
278
            }
279
            self.rpc.send_msg(msg)
1✔
280

281
    def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]:
1✔
282
        """
283
        Refresh active whitelist from pairlist or edge and extend it with
284
        pairs that have open trades.
285
        """
286
        # Refresh whitelist
287
        _prev_whitelist = self.pairlists.whitelist
1✔
288
        self.pairlists.refresh_pairlist()
1✔
289
        _whitelist = self.pairlists.whitelist
1✔
290

291
        # Calculating Edge positioning
292
        if self.edge:
1✔
293
            self.edge.calculate(_whitelist)
1✔
294
            _whitelist = self.edge.adjust(_whitelist)
1✔
295

296
        if trades:
1✔
297
            # Extend active-pair whitelist with pairs of open trades
298
            # It ensures that candle (OHLCV) data are downloaded for open trades as well
299
            _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist])
1✔
300

301
        # Called last to include the included pairs
302
        if _prev_whitelist != _whitelist:
1✔
303
            self.rpc.send_msg({'type': RPCMessageType.WHITELIST, 'data': _whitelist})
1✔
304

305
        return _whitelist
1✔
306

307
    def get_free_open_trades(self) -> int:
1✔
308
        """
309
        Return the number of free open trades slots or 0 if
310
        max number of open trades reached
311
        """
312
        open_trades = Trade.get_open_trade_count()
1✔
313
        return max(0, self.config['max_open_trades'] - open_trades)
1✔
314

315
    def update_funding_fees(self):
1✔
316
        if self.trading_mode == TradingMode.FUTURES:
1✔
317
            trades = Trade.get_open_trades()
1✔
318
            try:
1✔
319
                for trade in trades:
1✔
320
                    funding_fees = self.exchange.get_funding_fees(
1✔
321
                        pair=trade.pair,
322
                        amount=trade.amount,
323
                        is_short=trade.is_short,
324
                        open_date=trade.date_last_filled_utc
325
                    )
326
                    trade.funding_fees = funding_fees
1✔
327
            except ExchangeError:
×
328
                logger.warning("Could not update funding fees for open trades.")
×
329

330
    def startup_backpopulate_precision(self):
1✔
331

332
        trades = Trade.get_trades([Trade.contract_size.is_(None)])
1✔
333
        for trade in trades:
1✔
334
            if trade.exchange != self.exchange.id:
1✔
335
                continue
1✔
336
            trade.precision_mode = self.exchange.precisionMode
1✔
337
            trade.amount_precision = self.exchange.get_precision_amount(trade.pair)
1✔
338
            trade.price_precision = self.exchange.get_precision_price(trade.pair)
1✔
339
            trade.contract_size = self.exchange.get_contract_size(trade.pair)
1✔
340
        Trade.commit()
1✔
341

342
    def startup_update_open_orders(self):
1✔
343
        """
344
        Updates open orders based on order list kept in the database.
345
        Mainly updates the state of orders - but may also close trades
346
        """
347
        if self.config['dry_run'] or self.config['exchange'].get('skip_open_order_update', False):
1✔
348
            # Updating open orders in dry-run does not make sense and will fail.
349
            return
1✔
350

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

369
            except InvalidOrderException as e:
1✔
370
                logger.warning(f"Error updating Order {order.order_id} due to {e}.")
1✔
371
                if order.order_date_utc - timedelta(days=5) < datetime.now(timezone.utc):
1✔
372
                    logger.warning(
1✔
373
                        "Order is older than 5 days. Assuming order was fully cancelled.")
374
                    fo = order.to_ccxt_object()
1✔
375
                    fo['status'] = 'canceled'
1✔
376
                    self.handle_cancel_order(fo, order.trade, constants.CANCEL_REASON['TIMEOUT'])
1✔
377

378
            except ExchangeError as e:
1✔
379

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

382
        if self.trading_mode == TradingMode.FUTURES:
1✔
383
            self._schedule.run_pending()
×
384

385
    def update_trades_without_assigned_fees(self) -> None:
1✔
386
        """
387
        Update closed trades without close fees assigned.
388
        Only acts when Orders are in the database, otherwise the last order-id is unknown.
389
        """
390
        if self.config['dry_run']:
1✔
391
            # Updating open orders in dry-run does not make sense and will fail.
392
            return
1✔
393

394
        trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees()
1✔
395
        for trade in trades:
1✔
396
            if not trade.is_open and not trade.fee_updated(trade.exit_side):
1✔
397
                # Get sell fee
398
                order = trade.select_order(trade.exit_side, False, only_filled=True)
1✔
399
                if not order:
1✔
400
                    order = trade.select_order('stoploss', False)
×
401
                if order:
1✔
402
                    logger.info(
1✔
403
                        f"Updating {trade.exit_side}-fee on trade {trade}"
404
                        f"for order {order.order_id}."
405
                    )
406
                    self.update_trade_state(trade, order.order_id,
1✔
407
                                            stoploss_order=order.ft_order_side == 'stoploss',
408
                                            send_msg=False)
409

410
        trades = Trade.get_open_trades_without_assigned_fees()
1✔
411
        for trade in trades:
1✔
412
            with self._exit_lock:
1✔
413
                if trade.is_open and not trade.fee_updated(trade.entry_side):
1✔
414
                    order = trade.select_order(trade.entry_side, False, only_filled=True)
1✔
415
                    open_order = trade.select_order(trade.entry_side, True)
1✔
416
                    if order and open_order is None:
1✔
417
                        logger.info(
1✔
418
                            f"Updating {trade.entry_side}-fee on trade {trade}"
419
                            f"for order {order.order_id}."
420
                        )
421
                        self.update_trade_state(trade, order.order_id, send_msg=False)
1✔
422

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

455
            except ExchangeError:
1✔
456
                logger.warning(f"Error updating {order.order_id}.")
1✔
457

458
    def handle_onexchange_order(self, trade: Trade):
1✔
459
        """
460
        Try refinding a order that is not in the database.
461
        Only used balance disappeared, which would make exiting impossible.
462
        """
463
        try:
1✔
464
            orders = self.exchange.fetch_orders(trade.pair, trade.open_date_utc)
1✔
465
            for order in orders:
1✔
466
                trade_order = [o for o in trade.orders if o.order_id == order['id']]
1✔
467
                if trade_order:
1✔
468
                    continue
1✔
469
                logger.info(f"Found previously unknown order {order['id']} for {trade.pair}.")
1✔
470

471
                order_obj = Order.parse_from_ccxt_object(order, trade.pair, order['side'])
1✔
472
                order_obj.order_filled_date = datetime.fromtimestamp(
1✔
473
                    safe_value_fallback(order, 'lastTradeTimestamp', 'timestamp') // 1000,
474
                    tz=timezone.utc)
475
                trade.orders.append(order_obj)
1✔
476
                # TODO: how do we handle open_order_id ...
477
                Trade.commit()
1✔
478
                prev_exit_reason = trade.exit_reason
1✔
479
                trade.exit_reason = ExitType.SOLD_ON_EXCHANGE.value
1✔
480
                self.update_trade_state(trade, order['id'], order)
1✔
481

482
                logger.info(f"handled order {order['id']}")
1✔
483
                if not trade.is_open:
1✔
484
                    # Trade was just closed
485
                    trade.close_date = order_obj.order_filled_date
1✔
486
                    Trade.commit()
1✔
487
                    break
1✔
488
                else:
489
                    trade.exit_reason = prev_exit_reason
×
490
                    Trade.commit()
×
491

492
        except ExchangeError:
×
493
            logger.warning("Error finding onexchange order")
×
494
#
495
# BUY / enter positions / open trades logic and methods
496
#
497

498
    def enter_positions(self) -> int:
1✔
499
        """
500
        Tries to execute entry orders for new trades (positions)
501
        """
502
        trades_created = 0
1✔
503

504
        whitelist = deepcopy(self.active_pair_whitelist)
1✔
505
        if not whitelist:
1✔
506
            self.log_once("Active pair whitelist is empty.", logger.info)
1✔
507
            return trades_created
1✔
508
        # Remove pairs for currently opened trades from the whitelist
509
        for trade in Trade.get_open_trades():
1✔
510
            if trade.pair in whitelist:
1✔
511
                whitelist.remove(trade.pair)
1✔
512
                logger.debug('Ignoring %s in pair whitelist', trade.pair)
1✔
513

514
        if not whitelist:
1✔
515
            self.log_once("No currency pair in active pair whitelist, "
1✔
516
                          "but checking to exit open trades.", logger.info)
517
            return trades_created
1✔
518
        if PairLocks.is_global_lock(side='*'):
1✔
519
            # This only checks for total locks (both sides).
520
            # per-side locks will be evaluated by `is_pair_locked` within create_trade,
521
            # once the direction for the trade is clear.
522
            lock = PairLocks.get_pair_longest_lock('*')
1✔
523
            if lock:
1✔
524
                self.log_once(f"Global pairlock active until "
1✔
525
                              f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}. "
526
                              f"Not creating new trades, reason: {lock.reason}.", logger.info)
527
            else:
528
                self.log_once("Global pairlock active. Not creating new trades.", logger.info)
×
529
            return trades_created
1✔
530
        # Create entity and execute trade for each pair from whitelist
531
        for pair in whitelist:
1✔
532
            try:
1✔
533
                with self._exit_lock:
1✔
534
                    trades_created += self.create_trade(pair)
1✔
535
            except DependencyException as exception:
1✔
536
                logger.warning('Unable to create trade for %s: %s', pair, exception)
1✔
537

538
        if not trades_created:
1✔
539
            logger.debug("Found no enter signals for whitelisted currencies. Trying again...")
1✔
540

541
        return trades_created
1✔
542

543
    def create_trade(self, pair: str) -> bool:
1✔
544
        """
545
        Check the implemented trading strategy for buy signals.
546

547
        If the pair triggers the buy signal a new trade record gets created
548
        and the buy-order opening the trade gets issued towards the exchange.
549

550
        :return: True if a trade has been created.
551
        """
552
        logger.debug(f"create_trade for pair {pair}")
1✔
553

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

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

563
        # running get_signal on historical data fetched
564
        (signal, enter_tag) = self.strategy.get_entry_signal(
1✔
565
            pair,
566
            self.strategy.timeframe,
567
            analyzed_df
568
        )
569

570
        if signal:
1✔
571
            if self.strategy.is_pair_locked(pair, candle_date=nowtime, side=signal):
1✔
572
                lock = PairLocks.get_pair_longest_lock(pair, nowtime, signal)
1✔
573
                if lock:
1✔
574
                    self.log_once(f"Pair {pair} {lock.side} is locked until "
1✔
575
                                  f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)} "
576
                                  f"due to {lock.reason}.",
577
                                  logger.info)
578
                else:
579
                    self.log_once(f"Pair {pair} is currently locked.", logger.info)
×
580
                return False
1✔
581
            stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge)
1✔
582

583
            bid_check_dom = self.config.get('entry_pricing', {}).get('check_depth_of_market', {})
1✔
584
            if ((bid_check_dom.get('enabled', False)) and
1✔
585
                    (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
586
                if self._check_depth_of_market(pair, bid_check_dom, side=signal):
1✔
587
                    return self.execute_entry(
1✔
588
                        pair,
589
                        stake_amount,
590
                        enter_tag=enter_tag,
591
                        is_short=(signal == SignalDirection.SHORT)
592
                    )
593
                else:
594
                    return False
1✔
595

596
            return self.execute_entry(
1✔
597
                pair,
598
                stake_amount,
599
                enter_tag=enter_tag,
600
                is_short=(signal == SignalDirection.SHORT)
601
            )
602
        else:
603
            return False
1✔
604

605
#
606
# BUY / increase positions / DCA logic and methods
607
#
608
    def process_open_trade_positions(self):
1✔
609
        """
610
        Tries to execute additional buy or sell orders for open trades (positions)
611
        """
612
        # Walk through each pair and check if it needs changes
613
        for trade in Trade.get_open_trades():
1✔
614
            # If there is any open orders, wait for them to finish.
615
            if trade.open_order_id is None:
1✔
616
                # Do a wallets update (will be ratelimited to once per hour)
617
                self.wallets.update(False)
1✔
618
                try:
1✔
619
                    self.check_and_call_adjust_trade_position(trade)
1✔
620
                except DependencyException as exception:
1✔
621
                    logger.warning(
1✔
622
                        f"Unable to adjust position of trade for {trade.pair}: {exception}")
623

624
    def check_and_call_adjust_trade_position(self, trade: Trade):
1✔
625
        """
626
        Check the implemented trading strategy for adjustment command.
627
        If the strategy triggers the adjustment, a new order gets issued.
628
        Once that completes, the existing trade is modified to match new data.
629
        """
630
        current_entry_rate, current_exit_rate = self.exchange.get_rates(
1✔
631
            trade.pair, True, trade.is_short)
632

633
        current_entry_profit = trade.calc_profit_ratio(current_entry_rate)
1✔
634
        current_exit_profit = trade.calc_profit_ratio(current_exit_rate)
1✔
635

636
        min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
1✔
637
                                                                  current_entry_rate,
638
                                                                  0.0)
639
        min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
1✔
640
                                                                 current_exit_rate,
641
                                                                 self.strategy.stoploss)
642
        max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate)
1✔
643
        stake_available = self.wallets.get_available_stake_amount()
1✔
644
        logger.debug(f"Calling adjust_trade_position for pair {trade.pair}")
1✔
645
        stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
1✔
646
                                             default_retval=None, supress_error=True)(
647
            trade=trade,
648
            current_time=datetime.now(timezone.utc), current_rate=current_entry_rate,
649
            current_profit=current_entry_profit, min_stake=min_entry_stake,
650
            max_stake=min(max_entry_stake, stake_available),
651
            current_entry_rate=current_entry_rate, current_exit_rate=current_exit_rate,
652
            current_entry_profit=current_entry_profit, current_exit_profit=current_exit_profit
653
        )
654

655
        if stake_amount is not None and stake_amount > 0.0:
1✔
656
            # We should increase our position
657
            if self.strategy.max_entry_position_adjustment > -1:
1✔
658
                count_of_entries = trade.nr_of_successful_entries
1✔
659
                if count_of_entries > self.strategy.max_entry_position_adjustment:
1✔
660
                    logger.debug(f"Max adjustment entries for {trade.pair} has been reached.")
1✔
661
                    return
1✔
662
                else:
663
                    logger.debug("Max adjustment entries is set to unlimited.")
×
664
            self.execute_entry(trade.pair, stake_amount, price=current_entry_rate,
1✔
665
                               trade=trade, is_short=trade.is_short)
666

667
        if stake_amount is not None and stake_amount < 0.0:
1✔
668
            # We should decrease our position
669
            amount = self.exchange.amount_to_contract_precision(
1✔
670
                trade.pair,
671
                abs(float(FtPrecise(stake_amount * trade.leverage) / FtPrecise(current_exit_rate))))
672
            if amount > trade.amount:
1✔
673
                # This is currently ineffective as remaining would become < min tradable
674
                # Fixing this would require checking for 0.0 there -
675
                # if we decide that this callback is allowed to "fully exit"
676
                logger.info(
1✔
677
                    f"Adjusting amount to trade.amount as it is higher. {amount} > {trade.amount}")
678
                amount = trade.amount
1✔
679

680
            if amount == 0.0:
1✔
681
                logger.info("Amount to exit is 0.0 due to exchange limits - not exiting.")
1✔
682
                return
1✔
683

684
            remaining = (trade.amount - amount) * current_exit_rate
1✔
685
            if min_exit_stake and remaining < min_exit_stake:
1✔
686
                logger.info(f"Remaining amount of {remaining} would be smaller "
1✔
687
                            f"than the minimum of {min_exit_stake}.")
688
                return
1✔
689

690
            self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple(
1✔
691
                exit_type=ExitType.PARTIAL_EXIT), sub_trade_amt=amount)
692

693
    def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool:
1✔
694
        """
695
        Checks depth of market before executing a buy
696
        """
697
        conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0)
1✔
698
        logger.info(f"Checking depth of market for {pair} ...")
1✔
699
        order_book = self.exchange.fetch_l2_order_book(pair, 1000)
1✔
700
        order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
1✔
701
        order_book_bids = order_book_data_frame['b_size'].sum()
1✔
702
        order_book_asks = order_book_data_frame['a_size'].sum()
1✔
703

704
        entry_side = order_book_bids if side == SignalDirection.LONG else order_book_asks
1✔
705
        exit_side = order_book_asks if side == SignalDirection.LONG else order_book_bids
1✔
706
        bids_ask_delta = entry_side / exit_side
1✔
707

708
        bids = f"Bids: {order_book_bids}"
1✔
709
        asks = f"Asks: {order_book_asks}"
1✔
710
        delta = f"Delta: {bids_ask_delta}"
1✔
711

712
        logger.info(
1✔
713
            f"{bids}, {asks}, {delta}, Direction: {side.value}"
714
            f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, "
715
            f"Immediate Bid Quantity: {order_book['bids'][0][1]}, "
716
            f"Immediate Ask Quantity: {order_book['asks'][0][1]}."
717
        )
718
        if bids_ask_delta >= conf_bids_to_ask_delta:
1✔
719
            logger.info(f"Bids to asks delta for {pair} DOES satisfy condition.")
1✔
720
            return True
1✔
721
        else:
722
            logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
1✔
723
            return False
1✔
724

725
    def execute_entry(
1✔
726
        self,
727
        pair: str,
728
        stake_amount: float,
729
        price: Optional[float] = None,
730
        *,
731
        is_short: bool = False,
732
        ordertype: Optional[str] = None,
733
        enter_tag: Optional[str] = None,
734
        trade: Optional[Trade] = None,
735
        order_adjust: bool = False,
736
        leverage_: Optional[float] = None,
737
    ) -> bool:
738
        """
739
        Executes a limit buy for the given pair
740
        :param pair: pair for which we want to create a LIMIT_BUY
741
        :param stake_amount: amount of stake-currency for the pair
742
        :return: True if a buy order is created, false if it fails.
743
        """
744
        time_in_force = self.strategy.order_time_in_force['entry']
1✔
745

746
        side: BuySell = 'sell' if is_short else 'buy'
1✔
747
        name = 'Short' if is_short else 'Long'
1✔
748
        trade_side: LongShort = 'short' if is_short else 'long'
1✔
749
        pos_adjust = trade is not None
1✔
750

751
        enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake(
1✔
752
            pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_,
753
            pos_adjust)
754

755
        if not stake_amount:
1✔
756
            return False
1✔
757

758
        msg = (f"Position adjust: about to create a new order for {pair} with stake: "
1✔
759
               f"{stake_amount} for {trade}" if pos_adjust
760
               else
761
               f"{name} signal found: about create a new trade for {pair} with stake_amount: "
762
               f"{stake_amount} ...")
763
        logger.info(msg)
1✔
764
        amount = (stake_amount / enter_limit_requested) * leverage
1✔
765
        order_type = ordertype or self.strategy.order_types['entry']
1✔
766

767
        if not pos_adjust and not strategy_safe_wrapper(
1✔
768
                self.strategy.confirm_trade_entry, default_retval=True)(
769
                pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
770
                time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
771
                entry_tag=enter_tag, side=trade_side):
772
            logger.info(f"User denied entry for {pair}.")
1✔
773
            return False
1✔
774
        order = self.exchange.create_order(
1✔
775
            pair=pair,
776
            ordertype=order_type,
777
            side=side,
778
            amount=amount,
779
            rate=enter_limit_requested,
780
            reduceOnly=False,
781
            time_in_force=time_in_force,
782
            leverage=leverage
783
        )
784
        order_obj = Order.parse_from_ccxt_object(order, pair, side, amount, enter_limit_requested)
1✔
785
        order_id = order['id']
1✔
786
        order_status = order.get('status')
1✔
787
        logger.info(f"Order {order_id} was created for {pair} and status is {order_status}.")
1✔
788

789
        # we assume the order is executed at the price requested
790
        enter_limit_filled_price = enter_limit_requested
1✔
791
        amount_requested = amount
1✔
792

793
        if order_status == 'expired' or order_status == 'rejected':
1✔
794

795
            # return false if the order is not filled
796
            if float(order['filled']) == 0:
1✔
797
                logger.warning(f'{name} {time_in_force} order with time in force {order_type} '
1✔
798
                               f'for {pair} is {order_status} by {self.exchange.name}.'
799
                               ' zero amount is fulfilled.')
800
                return False
1✔
801
            else:
802
                # the order is partially fulfilled
803
                # in case of IOC orders we can check immediately
804
                # if the order is fulfilled fully or partially
805
                logger.warning('%s %s order with time in force %s for %s is %s by %s.'
1✔
806
                               ' %s amount fulfilled out of %s (%s remaining which is canceled).',
807
                               name, time_in_force, order_type, pair, order_status,
808
                               self.exchange.name, order['filled'], order['amount'],
809
                               order['remaining']
810
                               )
811
                amount = safe_value_fallback(order, 'filled', 'amount', amount)
1✔
812
                enter_limit_filled_price = safe_value_fallback(
1✔
813
                    order, 'average', 'price', enter_limit_filled_price)
814

815
        # in case of FOK the order may be filled immediately and fully
816
        elif order_status == 'closed':
1✔
817
            amount = safe_value_fallback(order, 'filled', 'amount', amount)
1✔
818
            enter_limit_filled_price = safe_value_fallback(
1✔
819
                order, 'average', 'price', enter_limit_requested)
820

821
        # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
822
        fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
1✔
823
        base_currency = self.exchange.get_pair_base_currency(pair)
1✔
824
        open_date = datetime.now(timezone.utc)
1✔
825

826
        # This is a new trade
827
        if trade is None:
1✔
828
            funding_fees = 0.0
1✔
829
            try:
1✔
830
                funding_fees = self.exchange.get_funding_fees(
1✔
831
                    pair=pair, amount=amount, is_short=is_short, open_date=open_date)
832
            except ExchangeError:
1✔
833
                logger.warning("Could not find funding fee.")
1✔
834

835
            trade = Trade(
1✔
836
                pair=pair,
837
                base_currency=base_currency,
838
                stake_currency=self.config['stake_currency'],
839
                stake_amount=stake_amount,
840
                amount=amount,
841
                is_open=True,
842
                amount_requested=amount_requested,
843
                fee_open=fee,
844
                fee_close=fee,
845
                open_rate=enter_limit_filled_price,
846
                open_rate_requested=enter_limit_requested,
847
                open_date=open_date,
848
                exchange=self.exchange.id,
849
                open_order_id=order_id,
850
                strategy=self.strategy.get_strategy_name(),
851
                enter_tag=enter_tag,
852
                timeframe=timeframe_to_minutes(self.config['timeframe']),
853
                leverage=leverage,
854
                is_short=is_short,
855
                trading_mode=self.trading_mode,
856
                funding_fees=funding_fees,
857
                amount_precision=self.exchange.get_precision_amount(pair),
858
                price_precision=self.exchange.get_precision_price(pair),
859
                precision_mode=self.exchange.precisionMode,
860
                contract_size=self.exchange.get_contract_size(pair),
861
            )
862
            stoploss = self.strategy.stoploss if not self.edge else self.edge.get_stoploss(pair)
1✔
863
            trade.adjust_stop_loss(trade.open_rate, stoploss, initial=True)
1✔
864

865
        else:
866
            # This is additional buy, we reset fee_open_currency so timeout checking can work
867
            trade.is_open = True
1✔
868
            trade.fee_open_currency = None
1✔
869
            trade.open_rate_requested = enter_limit_requested
1✔
870
            trade.open_order_id = order_id
1✔
871

872
        trade.orders.append(order_obj)
1✔
873
        trade.recalc_trade_from_orders()
1✔
874
        Trade.session.add(trade)
1✔
875
        Trade.commit()
1✔
876

877
        # Updating wallets
878
        self.wallets.update()
1✔
879

880
        self._notify_enter(trade, order_obj, order_type, sub_trade=pos_adjust)
1✔
881

882
        if pos_adjust:
1✔
883
            if order_status == 'closed':
1✔
884
                logger.info(f"DCA order closed, trade should be up to date: {trade}")
1✔
885
                trade = self.cancel_stoploss_on_exchange(trade)
1✔
886
            else:
887
                logger.info(f"DCA order {order_status}, will wait for resolution: {trade}")
1✔
888

889
        # Update fees if order is non-opened
890
        if order_status in constants.NON_OPEN_EXCHANGE_STATES:
1✔
891
            self.update_trade_state(trade, order_id, order)
1✔
892

893
        return True
1✔
894

895
    def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade:
1✔
896
        # First cancelling stoploss on exchange ...
897
        if trade.stoploss_order_id:
1✔
898
            try:
1✔
899
                logger.info(f"Canceling stoploss on exchange for {trade}")
1✔
900
                co = self.exchange.cancel_stoploss_order_with_result(
1✔
901
                    trade.stoploss_order_id, trade.pair, trade.amount)
902
                self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True)
1✔
903

904
                # Reset stoploss order id.
905
                trade.stoploss_order_id = None
1✔
906
            except InvalidOrderException:
1✔
907
                logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id} "
1✔
908
                                 f"for pair {trade.pair}")
909
        return trade
1✔
910

911
    def get_valid_enter_price_and_stake(
1✔
912
        self, pair: str, price: Optional[float], stake_amount: float,
913
        trade_side: LongShort,
914
        entry_tag: Optional[str],
915
        trade: Optional[Trade],
916
        order_adjust: bool,
917
        leverage_: Optional[float],
918
        pos_adjust: bool,
919
    ) -> Tuple[float, float, float]:
920
        """
921
        Validate and eventually adjust (within limits) limit, amount and leverage
922
        :return: Tuple with (price, amount, leverage)
923
        """
924

925
        if price:
1✔
926
            enter_limit_requested = price
1✔
927
        else:
928
            # Calculate price
929
            enter_limit_requested = self.exchange.get_rate(
1✔
930
                pair, side='entry', is_short=(trade_side == 'short'), refresh=True)
931
        if not order_adjust:
1✔
932
            # Don't call custom_entry_price in order-adjust scenario
933
            custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
1✔
934
                                                       default_retval=enter_limit_requested)(
935
                pair=pair, current_time=datetime.now(timezone.utc),
936
                proposed_rate=enter_limit_requested, entry_tag=entry_tag,
937
                side=trade_side,
938
            )
939

940
            enter_limit_requested = self.get_valid_price(custom_entry_price, enter_limit_requested)
1✔
941

942
        if not enter_limit_requested:
1✔
943
            raise PricingError('Could not determine entry price.')
1✔
944

945
        if self.trading_mode != TradingMode.SPOT and trade is None:
1✔
946
            max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
1✔
947
            if leverage_:
1✔
948
                leverage = leverage_
1✔
949
            else:
950
                leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
1✔
951
                    pair=pair,
952
                    current_time=datetime.now(timezone.utc),
953
                    current_rate=enter_limit_requested,
954
                    proposed_leverage=1.0,
955
                    max_leverage=max_leverage,
956
                    side=trade_side, entry_tag=entry_tag,
957
                )
958
            # Cap leverage between 1.0 and max_leverage.
959
            leverage = min(max(leverage, 1.0), max_leverage)
1✔
960
        else:
961
            # Changing leverage currently not possible
962
            leverage = trade.leverage if trade else 1.0
1✔
963

964
        # Min-stake-amount should actually include Leverage - this way our "minimal"
965
        # stake- amount might be higher than necessary.
966
        # We do however also need min-stake to determine leverage, therefore this is ignored as
967
        # edge-case for now.
968
        min_stake_amount = self.exchange.get_min_pair_stake_amount(
1✔
969
            pair, enter_limit_requested,
970
            self.strategy.stoploss if not pos_adjust else 0.0,
971
            leverage)
972
        max_stake_amount = self.exchange.get_max_pair_stake_amount(
1✔
973
            pair, enter_limit_requested, leverage)
974

975
        if not self.edge and trade is None:
1✔
976
            stake_available = self.wallets.get_available_stake_amount()
1✔
977
            stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
1✔
978
                                                 default_retval=stake_amount)(
979
                pair=pair, current_time=datetime.now(timezone.utc),
980
                current_rate=enter_limit_requested, proposed_stake=stake_amount,
981
                min_stake=min_stake_amount, max_stake=min(max_stake_amount, stake_available),
982
                leverage=leverage, entry_tag=entry_tag, side=trade_side
983
            )
984

985
        stake_amount = self.wallets.validate_stake_amount(
1✔
986
            pair=pair,
987
            stake_amount=stake_amount,
988
            min_stake_amount=min_stake_amount,
989
            max_stake_amount=max_stake_amount,
990
            trade_amount=trade.stake_amount if trade else None,
991
        )
992

993
        return enter_limit_requested, stake_amount, leverage
1✔
994

995
    def _notify_enter(self, trade: Trade, order: Order, order_type: str,
1✔
996
                      fill: bool = False, sub_trade: bool = False) -> None:
997
        """
998
        Sends rpc notification when a entry order occurred.
999
        """
1000
        open_rate = order.safe_price
1✔
1001

1002
        if open_rate is None:
1✔
1003
            open_rate = trade.open_rate
×
1004

1005
        current_rate = trade.open_rate_requested
1✔
1006
        if self.dataprovider.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
1✔
1007
            current_rate = self.exchange.get_rate(
1✔
1008
                trade.pair, side='entry', is_short=trade.is_short, refresh=False)
1009

1010
        msg: RPCBuyMsg = {
1✔
1011
            'trade_id': trade.id,
1012
            'type': RPCMessageType.ENTRY_FILL if fill else RPCMessageType.ENTRY,
1013
            'buy_tag': trade.enter_tag,
1014
            'enter_tag': trade.enter_tag,
1015
            'exchange': trade.exchange.capitalize(),
1016
            'pair': trade.pair,
1017
            'leverage': trade.leverage if trade.leverage else None,
1018
            'direction': 'Short' if trade.is_short else 'Long',
1019
            'limit': open_rate,  # Deprecated (?)
1020
            'open_rate': open_rate,
1021
            'order_type': order_type,
1022
            'stake_amount': trade.stake_amount,
1023
            'stake_currency': self.config['stake_currency'],
1024
            'base_currency': self.exchange.get_pair_base_currency(trade.pair),
1025
            'fiat_currency': self.config.get('fiat_display_currency', None),
1026
            'amount': order.safe_amount_after_fee if fill else (order.amount or trade.amount),
1027
            'open_date': trade.open_date_utc or datetime.now(timezone.utc),
1028
            'current_rate': current_rate,
1029
            'sub_trade': sub_trade,
1030
        }
1031

1032
        # Send the message
1033
        self.rpc.send_msg(msg)
1✔
1034

1035
    def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str,
1✔
1036
                             sub_trade: bool = False) -> None:
1037
        """
1038
        Sends rpc notification when a entry order cancel occurred.
1039
        """
1040
        current_rate = self.exchange.get_rate(
1✔
1041
            trade.pair, side='entry', is_short=trade.is_short, refresh=False)
1042

1043
        msg: RPCCancelMsg = {
1✔
1044
            'trade_id': trade.id,
1045
            'type': RPCMessageType.ENTRY_CANCEL,
1046
            'buy_tag': trade.enter_tag,
1047
            'enter_tag': trade.enter_tag,
1048
            'exchange': trade.exchange.capitalize(),
1049
            'pair': trade.pair,
1050
            'leverage': trade.leverage,
1051
            'direction': 'Short' if trade.is_short else 'Long',
1052
            'limit': trade.open_rate,
1053
            'order_type': order_type,
1054
            'stake_amount': trade.stake_amount,
1055
            'open_rate': trade.open_rate,
1056
            'stake_currency': self.config['stake_currency'],
1057
            'base_currency': self.exchange.get_pair_base_currency(trade.pair),
1058
            'fiat_currency': self.config.get('fiat_display_currency', None),
1059
            'amount': trade.amount,
1060
            'open_date': trade.open_date,
1061
            'current_rate': current_rate,
1062
            'reason': reason,
1063
            'sub_trade': sub_trade,
1064
        }
1065

1066
        # Send the message
1067
        self.rpc.send_msg(msg)
1✔
1068

1069
#
1070
# SELL / exit positions / close trades logic and methods
1071
#
1072

1073
    def exit_positions(self, trades: List[Trade]) -> int:
1✔
1074
        """
1075
        Tries to execute exit orders for open trades (positions)
1076
        """
1077
        trades_closed = 0
1✔
1078
        for trade in trades:
1✔
1079

1080
            if trade.open_order_id is None and not self.wallets.check_exit_amount(trade):
1✔
1081
                logger.warning(
×
1082
                    f'Not enough {trade.safe_base_currency} in wallet to exit {trade}. '
1083
                    'Trying to recover.')
1084
                self.handle_onexchange_order(trade)
×
1085

1086
            try:
1✔
1087
                try:
1✔
1088
                    if (self.strategy.order_types.get('stoploss_on_exchange') and
1✔
1089
                            self.handle_stoploss_on_exchange(trade)):
1090
                        trades_closed += 1
1✔
1091
                        Trade.commit()
1✔
1092
                        continue
1✔
1093

1094
                except InvalidOrderException as exception:
×
1095
                    logger.warning(
×
1096
                        f'Unable to handle stoploss on exchange for {trade.pair}: {exception}')
1097
                # Check if we can sell our current pair
1098
                if trade.open_order_id is None and trade.is_open and self.handle_trade(trade):
1✔
1099
                    trades_closed += 1
1✔
1100

1101
            except DependencyException as exception:
1✔
1102
                logger.warning(f'Unable to exit trade {trade.pair}: {exception}')
1✔
1103

1104
        # Updating wallets if any trade occurred
1105
        if trades_closed:
1✔
1106
            self.wallets.update()
1✔
1107

1108
        return trades_closed
1✔
1109

1110
    def handle_trade(self, trade: Trade) -> bool:
1✔
1111
        """
1112
        Exits the current pair if the threshold is reached and updates the trade record.
1113
        :return: True if trade has been sold/exited_short, False otherwise
1114
        """
1115
        if not trade.is_open:
1✔
1116
            raise DependencyException(f'Attempt to handle closed trade: {trade}')
1✔
1117

1118
        logger.debug('Handling %s ...', trade)
1✔
1119

1120
        (enter, exit_) = (False, False)
1✔
1121
        exit_tag = None
1✔
1122
        exit_signal_type = "exit_short" if trade.is_short else "exit_long"
1✔
1123

1124
        if (self.config.get('use_exit_signal', True) or
1✔
1125
                self.config.get('ignore_roi_if_entry_signal', False)):
1126
            analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
1✔
1127
                                                                      self.strategy.timeframe)
1128

1129
            (enter, exit_, exit_tag) = self.strategy.get_exit_signal(
1✔
1130
                trade.pair,
1131
                self.strategy.timeframe,
1132
                analyzed_df,
1133
                is_short=trade.is_short
1134
            )
1135

1136
        logger.debug('checking exit')
1✔
1137
        exit_rate = self.exchange.get_rate(
1✔
1138
            trade.pair, side='exit', is_short=trade.is_short, refresh=True)
1139
        if self._check_and_execute_exit(trade, exit_rate, enter, exit_, exit_tag):
1✔
1140
            return True
1✔
1141

1142
        logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
1✔
1143
        return False
1✔
1144

1145
    def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
1✔
1146
                                enter: bool, exit_: bool, exit_tag: Optional[str]) -> bool:
1147
        """
1148
        Check and execute trade exit
1149
        """
1150
        exits: List[ExitCheckTuple] = self.strategy.should_exit(
1✔
1151
            trade,
1152
            exit_rate,
1153
            datetime.now(timezone.utc),
1154
            enter=enter,
1155
            exit_=exit_,
1156
            force_stoploss=self.edge.get_stoploss(trade.pair) if self.edge else 0
1157
        )
1158
        for should_exit in exits:
1✔
1159
            if should_exit.exit_flag:
1✔
1160
                exit_tag1 = exit_tag if should_exit.exit_type == ExitType.EXIT_SIGNAL else None
1✔
1161
                logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.exit_type}'
1✔
1162
                            f'{f" Tag: {exit_tag1}" if exit_tag1 is not None else ""}')
1163
                exited = self.execute_trade_exit(trade, exit_rate, should_exit, exit_tag=exit_tag1)
1✔
1164
                if exited:
1✔
1165
                    return True
1✔
1166
        return False
1✔
1167

1168
    def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
1✔
1169
        """
1170
        Abstracts creating stoploss orders from the logic.
1171
        Handles errors and updates the trade database object.
1172
        Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
1173
        :return: True if the order succeeded, and False in case of problems.
1174
        """
1175
        try:
1✔
1176
            stoploss_order = self.exchange.create_stoploss(
1✔
1177
                pair=trade.pair,
1178
                amount=trade.amount,
1179
                stop_price=stop_price,
1180
                order_types=self.strategy.order_types,
1181
                side=trade.exit_side,
1182
                leverage=trade.leverage
1183
            )
1184

1185
            order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss',
1✔
1186
                                                     trade.amount, stop_price)
1187
            trade.orders.append(order_obj)
1✔
1188
            trade.stoploss_order_id = str(stoploss_order['id'])
1✔
1189
            trade.stoploss_last_update = datetime.now(timezone.utc)
1✔
1190
            return True
1✔
1191
        except InsufficientFundsError as e:
1✔
1192
            logger.warning(f"Unable to place stoploss order {e}.")
1✔
1193
            # Try to figure out what went wrong
1194
            self.handle_insufficient_funds(trade)
1✔
1195

1196
        except InvalidOrderException as e:
1✔
1197
            trade.stoploss_order_id = None
1✔
1198
            logger.error(f'Unable to place a stoploss order on exchange. {e}')
1✔
1199
            logger.warning('Exiting the trade forcefully')
1✔
1200
            self.emergency_exit(trade, stop_price)
1✔
1201

1202
        except ExchangeError:
1✔
1203
            trade.stoploss_order_id = None
1✔
1204
            logger.exception('Unable to place a stoploss order on exchange.')
1✔
1205
        return False
1✔
1206

1207
    def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
1✔
1208
        """
1209
        Check if trade is fulfilled in which case the stoploss
1210
        on exchange should be added immediately if stoploss on exchange
1211
        is enabled.
1212
        # TODO: liquidation price always on exchange, even without stoploss_on_exchange
1213
        # Therefore fetching account liquidations for open pairs may make sense.
1214
        """
1215

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

1218
        stoploss_order = None
1✔
1219

1220
        try:
1✔
1221
            # First we check if there is already a stoploss on exchange
1222
            stoploss_order = self.exchange.fetch_stoploss_order(
1✔
1223
                trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None
1224
        except InvalidOrderException as exception:
1✔
1225
            logger.warning('Unable to fetch stoploss order: %s', exception)
1✔
1226

1227
        if stoploss_order:
1✔
1228
            self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
1✔
1229
                                    stoploss_order=True)
1230

1231
        # We check if stoploss order is fulfilled
1232
        if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
1✔
1233
            trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
1✔
1234
            self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
1✔
1235
                                    stoploss_order=True)
1236
            self._notify_exit(trade, "stoploss", True)
1✔
1237
            self.handle_protections(trade.pair, trade.trade_direction)
1✔
1238
            return True
1✔
1239

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

1246
        # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
1247
        if not stoploss_order:
1✔
1248
            stop_price = trade.stoploss_or_liquidation
1✔
1249
            if self.edge:
1✔
1250
                stoploss = self.edge.get_stoploss(pair=trade.pair)
×
1251
                stop_price = (
×
1252
                    trade.open_rate * (1 - stoploss) if trade.is_short
1253
                    else trade.open_rate * (1 + stoploss)
1254
                )
1255

1256
            if self.create_stoploss_order(trade=trade, stop_price=stop_price):
1✔
1257
                # The above will return False if the placement failed and the trade was force-sold.
1258
                # in which case the trade will be closed - which we must check below.
1259
                return False
1✔
1260

1261
        # If stoploss order is canceled for some reason we add it again
1262
        if (trade.is_open
1✔
1263
                and stoploss_order
1264
                and stoploss_order['status'] in ('canceled', 'cancelled')):
1265
            if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation):
1✔
1266
                return False
1✔
1267
            else:
1268
                logger.warning('Stoploss order was cancelled, but unable to recreate one.')
1✔
1269

1270
        # Finally we check if stoploss on exchange should be moved up because of trailing.
1271
        # Triggered Orders are now real orders - so don't replace stoploss anymore
1272
        if (
1✔
1273
            trade.is_open and stoploss_order
1274
            and stoploss_order.get('status_stop') != 'triggered'
1275
            and (self.config.get('trailing_stop', False)
1276
                 or self.config.get('use_custom_stoploss', False))
1277
        ):
1278
            # if trailing stoploss is enabled we check if stoploss value has changed
1279
            # in which case we cancel stoploss order and put another one with new
1280
            # value immediately
1281
            self.handle_trailing_stoploss_on_exchange(trade, stoploss_order)
1✔
1282

1283
        return False
1✔
1284

1285
    def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: Dict) -> None:
1✔
1286
        """
1287
        Check to see if stoploss on exchange should be updated
1288
        in case of trailing stoploss on exchange
1289
        :param trade: Corresponding Trade
1290
        :param order: Current on exchange stoploss order
1291
        :return: None
1292
        """
1293
        stoploss_norm = self.exchange.price_to_precision(
1✔
1294
            trade.pair, trade.stoploss_or_liquidation,
1295
            rounding_mode=ROUND_DOWN if trade.is_short else ROUND_UP)
1296

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

1306
                self.cancel_stoploss_on_exchange(trade)
1✔
1307
                if not trade.is_open:
1✔
1308
                    logger.warning(
×
1309
                        f"Trade {trade} is closed, not creating trailing stoploss order.")
1310
                    return
×
1311

1312
                # Create new stoploss order
1313
                if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
1✔
1314
                    logger.warning(f"Could not create trailing stoploss order "
1✔
1315
                                   f"for pair {trade.pair}.")
1316

1317
    def manage_open_orders(self) -> None:
1✔
1318
        """
1319
        Management of open orders on exchange. Unfilled orders might be cancelled if timeout
1320
        was met or replaced if there's a new candle and user has requested it.
1321
        Timeout setting takes priority over limit order adjustment request.
1322
        :return: None
1323
        """
1324
        for trade in Trade.get_open_order_trades():
1✔
1325
            try:
1✔
1326
                if not trade.open_order_id:
1✔
1327
                    continue
×
1328
                order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
1✔
1329
            except (ExchangeError):
1✔
1330
                logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
1✔
1331
                continue
1✔
1332

1333
            fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
1✔
1334
            not_closed = order['status'] == 'open' or fully_cancelled
1✔
1335
            order_obj = trade.select_order_by_order_id(trade.open_order_id)
1✔
1336

1337
            if not_closed:
1✔
1338
                if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
1✔
1339
                   trade, order_obj, datetime.now(timezone.utc))):
1340
                    self.handle_cancel_order(order, trade, constants.CANCEL_REASON['TIMEOUT'])
1✔
1341
                else:
1342
                    self.replace_order(order, order_obj, trade)
1✔
1343

1344
    def handle_cancel_order(self, order: Dict, trade: Trade, reason: str) -> None:
1✔
1345
        """
1346
        Check if current analyzed order timed out and cancel if necessary.
1347
        :param order: Order dict grabbed with exchange.fetch_order()
1348
        :param trade: Trade object.
1349
        :return: None
1350
        """
1351
        if order['side'] == trade.entry_side:
1✔
1352
            self.handle_cancel_enter(trade, order, reason)
1✔
1353
        else:
1354
            canceled = self.handle_cancel_exit(trade, order, reason)
1✔
1355
            canceled_count = trade.get_exit_order_count()
1✔
1356
            max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
1✔
1357
            if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
1✔
1358
                logger.warning(f'Emergency exiting trade {trade}, as the exit order '
1✔
1359
                               f'timed out {max_timeouts} times.')
1360
                self.emergency_exit(trade, order['price'])
1✔
1361

1362
    def emergency_exit(self, trade: Trade, price: float) -> None:
1✔
1363
        try:
1✔
1364
            self.execute_trade_exit(
1✔
1365
                trade, price,
1366
                exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT))
1367
        except DependencyException as exception:
1✔
1368
            logger.warning(
1✔
1369
                f'Unable to emergency exit trade {trade.pair}: {exception}')
1370

1371
    def replace_order(self, order: Dict, order_obj: Optional[Order], trade: Trade) -> None:
1✔
1372
        """
1373
        Check if current analyzed entry order should be replaced or simply cancelled.
1374
        To simply cancel the existing order(no replacement) adjust_entry_price() should return None
1375
        To maintain existing order adjust_entry_price() should return order_obj.price
1376
        To replace existing order adjust_entry_price() should return desired price for limit order
1377
        :param order: Order dict grabbed with exchange.fetch_order()
1378
        :param order_obj: Order object.
1379
        :param trade: Trade object.
1380
        :return: None
1381
        """
1382
        analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
1✔
1383
                                                                  self.strategy.timeframe)
1384
        latest_candle_open_date = analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None
1✔
1385
        latest_candle_close_date = timeframe_to_next_date(self.strategy.timeframe,
1✔
1386
                                                          latest_candle_open_date)
1387
        # Check if new candle
1388
        if (
1✔
1389
            order_obj and order_obj.side == trade.entry_side
1390
            and latest_candle_close_date > order_obj.order_date_utc
1391
        ):
1392
            # New candle
1393
            proposed_rate = self.exchange.get_rate(
1✔
1394
                trade.pair, side='entry', is_short=trade.is_short, refresh=True)
1395
            adjusted_entry_price = strategy_safe_wrapper(self.strategy.adjust_entry_price,
1✔
1396
                                                         default_retval=order_obj.price)(
1397
                trade=trade, order=order_obj, pair=trade.pair,
1398
                current_time=datetime.now(timezone.utc), proposed_rate=proposed_rate,
1399
                current_order_rate=order_obj.safe_price, entry_tag=trade.enter_tag,
1400
                side=trade.entry_side)
1401

1402
            replacing = True
1✔
1403
            cancel_reason = constants.CANCEL_REASON['REPLACE']
1✔
1404
            if not adjusted_entry_price:
1✔
1405
                replacing = False
1✔
1406
                cancel_reason = constants.CANCEL_REASON['USER_CANCEL']
1✔
1407
            if order_obj.price != adjusted_entry_price:
1✔
1408
                # cancel existing order if new price is supplied or None
1409
                self.handle_cancel_enter(trade, order, cancel_reason,
1✔
1410
                                         replacing=replacing)
1411
                if adjusted_entry_price:
1✔
1412
                    # place new order only if new price is supplied
1413
                    if not self.execute_entry(
1✔
1414
                        pair=trade.pair,
1415
                        stake_amount=(
1416
                            order_obj.safe_remaining * order_obj.safe_price / trade.leverage),
1417
                        price=adjusted_entry_price,
1418
                        trade=trade,
1419
                        is_short=trade.is_short,
1420
                        order_adjust=True,
1421
                    ):
1422
                        logger.warning(f"Could not replace order for {trade}.")
1✔
1423
                        if trade.nr_of_successful_entries == 0:
1✔
1424
                            # this is the first entry and we didn't get filled yet, delete trade
1425
                            logger.warning(f"Removing {trade} from database.")
1✔
1426
                            self._notify_enter_cancel(
1✔
1427
                                trade, order_type=self.strategy.order_types['entry'],
1428
                                reason=constants.CANCEL_REASON['REPLACE_FAILED'])
1429
                            trade.delete()
1✔
1430

1431
    def cancel_all_open_orders(self) -> None:
1✔
1432
        """
1433
        Cancel all orders that are currently open
1434
        :return: None
1435
        """
1436

1437
        for trade in Trade.get_open_order_trades():
1✔
1438
            if not trade.open_order_id:
1✔
1439
                continue
×
1440
            try:
1✔
1441
                order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
1✔
1442
            except (ExchangeError):
1✔
1443
                logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
1✔
1444
                continue
1✔
1445

1446
            if order['side'] == trade.entry_side:
1✔
1447
                self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
1✔
1448

1449
            elif order['side'] == trade.exit_side:
1✔
1450
                self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
1✔
1451
        Trade.commit()
1✔
1452

1453
    def handle_cancel_enter(
1✔
1454
            self, trade: Trade, order: Dict, reason: str,
1455
            replacing: Optional[bool] = False
1456
    ) -> bool:
1457
        """
1458
        entry cancel - cancel order
1459
        :param replacing: Replacing order - prevent trade deletion.
1460
        :return: True if trade was fully cancelled
1461
        """
1462
        was_trade_fully_canceled = False
1✔
1463
        side = trade.entry_side.capitalize()
1✔
1464
        if not trade.open_order_id:
1✔
1465
            logger.warning(f"No open order for {trade}.")
×
1466
            return False
×
1467

1468
        # Cancelled orders may have the status of 'canceled' or 'closed'
1469
        if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
1✔
1470
            filled_val: float = order.get('filled', 0.0) or 0.0
1✔
1471
            filled_stake = filled_val * trade.open_rate
1✔
1472
            minstake = self.exchange.get_min_pair_stake_amount(
1✔
1473
                trade.pair, trade.open_rate, self.strategy.stoploss)
1474

1475
            if filled_val > 0 and minstake and filled_stake < minstake:
1✔
1476
                logger.warning(
1✔
1477
                    f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
1478
                    f"as the filled amount of {filled_val} would result in an unexitable trade.")
1479
                return False
1✔
1480
            corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
1✔
1481
                                                            trade.amount)
1482
            # Avoid race condition where the order could not be cancelled coz its already filled.
1483
            # Simply bailing here is the only safe way - as this order will then be
1484
            # handled in the next iteration.
1485
            if corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES:
1✔
1486
                logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.")
1✔
1487
                return False
1✔
1488
        else:
1489
            # Order was cancelled already, so we can reuse the existing dict
1490
            corder = order
1✔
1491
            reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
1✔
1492

1493
        logger.info(f'{side} order {reason} for {trade}.')
1✔
1494

1495
        # Using filled to determine the filled amount
1496
        filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
1✔
1497
        if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
1✔
1498
            # if trade is not partially completed and it's the only order, just delete the trade
1499
            open_order_count = len([order for order in trade.orders if order.status == 'open'])
1✔
1500
            if open_order_count <= 1 and trade.nr_of_successful_entries == 0 and not replacing:
1✔
1501
                logger.info(f'{side} order fully cancelled. Removing {trade} from database.')
1✔
1502
                trade.delete()
1✔
1503
                was_trade_fully_canceled = True
1✔
1504
                reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}"
1✔
1505
            else:
1506
                self.update_trade_state(trade, trade.open_order_id, corder)
1✔
1507
                trade.open_order_id = None
1✔
1508
                logger.info(f'{side} Order timeout for {trade}.')
1✔
1509
        else:
1510
            # update_trade_state (and subsequently recalc_trade_from_orders) will handle updates
1511
            # to the trade object
1512
            self.update_trade_state(trade, trade.open_order_id, corder)
1✔
1513
            trade.open_order_id = None
1✔
1514

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

1518
        self.wallets.update()
1✔
1519
        self._notify_enter_cancel(trade, order_type=self.strategy.order_types['entry'],
1✔
1520
                                  reason=reason)
1521
        return was_trade_fully_canceled
1✔
1522

1523
    def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool:
1✔
1524
        """
1525
        exit order cancel - cancel order and update trade
1526
        :return: True if exit order was cancelled, false otherwise
1527
        """
1528
        cancelled = False
1✔
1529
        # Cancelled orders may have the status of 'canceled' or 'closed'
1530
        if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES:
1✔
1531
            filled_amt: float = order.get('filled', 0.0) or 0.0
1✔
1532
            # Filled val is in quote currency (after leverage)
1533
            filled_rem_stake = trade.stake_amount - (filled_amt * trade.open_rate / trade.leverage)
1✔
1534
            minstake = self.exchange.get_min_pair_stake_amount(
1✔
1535
                trade.pair, trade.open_rate, self.strategy.stoploss)
1536
            # Double-check remaining amount
1537
            if filled_amt > 0:
1✔
1538
                reason = constants.CANCEL_REASON['PARTIALLY_FILLED']
1✔
1539
                if minstake and filled_rem_stake < minstake:
1✔
1540
                    logger.warning(
1✔
1541
                        f"Order {trade.open_order_id} for {trade.pair} not cancelled, as "
1542
                        f"the filled amount of {filled_amt} would result in an unexitable trade.")
1543
                    reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
1✔
1544

1545
                    self._notify_exit_cancel(
1✔
1546
                        trade,
1547
                        order_type=self.strategy.order_types['exit'],
1548
                        reason=reason, order_id=order['id'],
1549
                        sub_trade=trade.amount != order['amount']
1550
                    )
1551
                    return False
1✔
1552

1553
            try:
1✔
1554
                order = self.exchange.cancel_order_with_result(
1✔
1555
                    order['id'], trade.pair, trade.amount)
1556
            except InvalidOrderException:
1✔
1557
                logger.exception(
1✔
1558
                    f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
1559
                return False
1✔
1560

1561
            # Set exit_reason for fill message
1562
            exit_reason_prev = trade.exit_reason
1✔
1563
            trade.exit_reason = trade.exit_reason + f", {reason}" if trade.exit_reason else reason
1✔
1564
            # Order might be filled above in odd timing issues.
1565
            if order.get('status') in ('canceled', 'cancelled'):
1✔
1566
                trade.exit_reason = None
1✔
1567
                trade.open_order_id = None
1✔
1568
            else:
1569
                trade.exit_reason = exit_reason_prev
1✔
1570
            cancelled = True
1✔
1571
        else:
1572
            reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
1✔
1573
            trade.exit_reason = None
1✔
1574
            trade.open_order_id = None
1✔
1575

1576
        self.update_trade_state(trade, order['id'], order)
1✔
1577

1578
        logger.info(f'{trade.exit_side.capitalize()} order {reason} for {trade}.')
1✔
1579
        trade.close_rate = None
1✔
1580
        trade.close_rate_requested = None
1✔
1581

1582
        self._notify_exit_cancel(
1✔
1583
            trade,
1584
            order_type=self.strategy.order_types['exit'],
1585
            reason=reason, order_id=order['id'], sub_trade=trade.amount != order['amount']
1586
        )
1587
        return cancelled
1✔
1588

1589
    def _safe_exit_amount(self, trade: Trade, pair: str, amount: float) -> float:
1✔
1590
        """
1591
        Get sellable amount.
1592
        Should be trade.amount - but will fall back to the available amount if necessary.
1593
        This should cover cases where get_real_amount() was not able to update the amount
1594
        for whatever reason.
1595
        :param trade: Trade we're working with
1596
        :param pair: Pair we're trying to sell
1597
        :param amount: amount we expect to be available
1598
        :return: amount to sell
1599
        :raise: DependencyException: if available balance is not within 2% of the available amount.
1600
        """
1601
        # Update wallets to ensure amounts tied up in a stoploss is now free!
1602
        self.wallets.update()
1✔
1603
        if self.trading_mode == TradingMode.FUTURES:
1✔
1604
            # A safe exit amount isn't needed for futures, you can just exit/close the position
1605
            return amount
1✔
1606

1607
        trade_base_currency = self.exchange.get_pair_base_currency(pair)
1✔
1608
        wallet_amount = self.wallets.get_free(trade_base_currency)
1✔
1609
        logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}")
1✔
1610
        if wallet_amount >= amount:
1✔
1611
            return amount
1✔
1612
        elif wallet_amount > amount * 0.98:
1✔
1613
            logger.info(f"{pair} - Falling back to wallet-amount {wallet_amount} -> {amount}.")
1✔
1614
            trade.amount = wallet_amount
1✔
1615
            return wallet_amount
1✔
1616
        else:
1617
            raise DependencyException(
1✔
1618
                f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}")
1619

1620
    def execute_trade_exit(
1✔
1621
            self,
1622
            trade: Trade,
1623
            limit: float,
1624
            exit_check: ExitCheckTuple,
1625
            *,
1626
            exit_tag: Optional[str] = None,
1627
            ordertype: Optional[str] = None,
1628
            sub_trade_amt: Optional[float] = None,
1629
    ) -> bool:
1630
        """
1631
        Executes a trade exit for the given trade and limit
1632
        :param trade: Trade instance
1633
        :param limit: limit rate for the sell order
1634
        :param exit_check: CheckTuple with signal and reason
1635
        :return: True if it succeeds False
1636
        """
1637
        try:
1✔
1638
            trade.funding_fees = self.exchange.get_funding_fees(
1✔
1639
                pair=trade.pair,
1640
                amount=trade.amount,
1641
                is_short=trade.is_short,
1642
                open_date=trade.date_last_filled_utc,
1643
            )
1644
        except ExchangeError:
1✔
1645
            logger.warning("Could not update funding fee.")
1✔
1646

1647
        exit_type = 'exit'
1✔
1648
        exit_reason = exit_tag or exit_check.exit_reason
1✔
1649
        if exit_check.exit_type in (
1✔
1650
                ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION):
1651
            exit_type = 'stoploss'
1✔
1652

1653
        # set custom_exit_price if available
1654
        proposed_limit_rate = limit
1✔
1655
        current_profit = trade.calc_profit_ratio(limit)
1✔
1656
        custom_exit_price = strategy_safe_wrapper(self.strategy.custom_exit_price,
1✔
1657
                                                  default_retval=proposed_limit_rate)(
1658
            pair=trade.pair, trade=trade,
1659
            current_time=datetime.now(timezone.utc),
1660
            proposed_rate=proposed_limit_rate, current_profit=current_profit,
1661
            exit_tag=exit_reason)
1662

1663
        limit = self.get_valid_price(custom_exit_price, proposed_limit_rate)
1✔
1664

1665
        # First cancelling stoploss on exchange ...
1666
        trade = self.cancel_stoploss_on_exchange(trade)
1✔
1667

1668
        order_type = ordertype or self.strategy.order_types[exit_type]
1✔
1669
        if exit_check.exit_type == ExitType.EMERGENCY_EXIT:
1✔
1670
            # Emergency sells (default to market!)
1671
            order_type = self.strategy.order_types.get("emergency_exit", "market")
1✔
1672

1673
        amount = self._safe_exit_amount(trade, trade.pair, sub_trade_amt or trade.amount)
1✔
1674
        time_in_force = self.strategy.order_time_in_force['exit']
1✔
1675

1676
        if (exit_check.exit_type != ExitType.LIQUIDATION
1✔
1677
                and not sub_trade_amt
1678
                and not strategy_safe_wrapper(
1679
                    self.strategy.confirm_trade_exit, default_retval=True)(
1680
                    pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
1681
                    time_in_force=time_in_force, exit_reason=exit_reason,
1682
                    sell_reason=exit_reason,  # sellreason -> compatibility
1683
                    current_time=datetime.now(timezone.utc))):
1684
            logger.info(f"User denied exit for {trade.pair}.")
1✔
1685
            return False
1✔
1686

1687
        try:
1✔
1688
            # Execute sell and update trade record
1689
            order = self.exchange.create_order(
1✔
1690
                pair=trade.pair,
1691
                ordertype=order_type,
1692
                side=trade.exit_side,
1693
                amount=amount,
1694
                rate=limit,
1695
                leverage=trade.leverage,
1696
                reduceOnly=self.trading_mode == TradingMode.FUTURES,
1697
                time_in_force=time_in_force
1698
            )
1699
        except InsufficientFundsError as e:
1✔
1700
            logger.warning(f"Unable to place order {e}.")
1✔
1701
            # Try to figure out what went wrong
1702
            self.handle_insufficient_funds(trade)
1✔
1703
            return False
1✔
1704

1705
        order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side, amount, limit)
1✔
1706
        trade.orders.append(order_obj)
1✔
1707

1708
        trade.open_order_id = order['id']
1✔
1709
        trade.exit_order_status = ''
1✔
1710
        trade.close_rate_requested = limit
1✔
1711
        trade.exit_reason = exit_reason
1✔
1712

1713
        self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj)
1✔
1714
        # In case of market sell orders the order can be closed immediately
1715
        if order.get('status', 'unknown') in ('closed', 'expired'):
1✔
1716
            self.update_trade_state(trade, trade.open_order_id, order)
1✔
1717
        Trade.commit()
1✔
1718

1719
        return True
1✔
1720

1721
    def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False,
1✔
1722
                     sub_trade: bool = False, order: Optional[Order] = None) -> None:
1723
        """
1724
        Sends rpc notification when a sell occurred.
1725
        """
1726
        # Use cached rates here - it was updated seconds ago.
1727
        current_rate = self.exchange.get_rate(
1✔
1728
            trade.pair, side='exit', is_short=trade.is_short, refresh=False) if not fill else None
1729

1730
        # second condition is for mypy only; order will always be passed during sub trade
1731
        if sub_trade and order is not None:
1✔
1732
            amount = order.safe_filled if fill else order.safe_amount
1✔
1733
            order_rate: float = order.safe_price
1✔
1734

1735
            profit = trade.calculate_profit(order_rate, amount, trade.open_rate)
1✔
1736
        else:
1737
            order_rate = trade.safe_close_rate
1✔
1738
            profit = trade.calculate_profit(rate=order_rate)
1✔
1739
            amount = trade.amount
1✔
1740
        gain = "profit" if profit.profit_ratio > 0 else "loss"
1✔
1741

1742
        msg: RPCSellMsg = {
1✔
1743
            'type': (RPCMessageType.EXIT_FILL if fill
1744
                     else RPCMessageType.EXIT),
1745
            'trade_id': trade.id,
1746
            'exchange': trade.exchange.capitalize(),
1747
            'pair': trade.pair,
1748
            'leverage': trade.leverage,
1749
            'direction': 'Short' if trade.is_short else 'Long',
1750
            'gain': gain,
1751
            'limit': order_rate,  # Deprecated
1752
            'order_rate': order_rate,
1753
            'order_type': order_type,
1754
            'amount': amount,
1755
            'open_rate': trade.open_rate,
1756
            'close_rate': order_rate,
1757
            'current_rate': current_rate,
1758
            'profit_amount': profit.profit_abs if fill else profit.total_profit,
1759
            'profit_ratio': profit.profit_ratio,
1760
            'buy_tag': trade.enter_tag,
1761
            'enter_tag': trade.enter_tag,
1762
            'sell_reason': trade.exit_reason,  # Deprecated
1763
            'exit_reason': trade.exit_reason,
1764
            'open_date': trade.open_date_utc,
1765
            'close_date': trade.close_date_utc or datetime.now(timezone.utc),
1766
            'stake_amount': trade.stake_amount,
1767
            'stake_currency': self.config['stake_currency'],
1768
            'base_currency': self.exchange.get_pair_base_currency(trade.pair),
1769
            'fiat_currency': self.config.get('fiat_display_currency'),
1770
            'sub_trade': sub_trade,
1771
            'cumulative_profit': trade.realized_profit,
1772
        }
1773

1774
        # Send the message
1775
        self.rpc.send_msg(msg)
1✔
1776

1777
    def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str,
1✔
1778
                            order_id: str, sub_trade: bool = False) -> None:
1779
        """
1780
        Sends rpc notification when a sell cancel occurred.
1781
        """
1782
        if trade.exit_order_status == reason:
1✔
1783
            return
1✔
1784
        else:
1785
            trade.exit_order_status = reason
1✔
1786

1787
        order_or_none = trade.select_order_by_order_id(order_id)
1✔
1788
        order = self.order_obj_or_raise(order_id, order_or_none)
1✔
1789

1790
        profit_rate: float = trade.safe_close_rate
1✔
1791
        profit = trade.calculate_profit(rate=profit_rate)
1✔
1792
        current_rate = self.exchange.get_rate(
1✔
1793
            trade.pair, side='exit', is_short=trade.is_short, refresh=False)
1794
        gain = "profit" if profit.profit_ratio > 0 else "loss"
1✔
1795

1796
        msg: RPCSellCancelMsg = {
1✔
1797
            'type': RPCMessageType.EXIT_CANCEL,
1798
            'trade_id': trade.id,
1799
            'exchange': trade.exchange.capitalize(),
1800
            'pair': trade.pair,
1801
            'leverage': trade.leverage,
1802
            'direction': 'Short' if trade.is_short else 'Long',
1803
            'gain': gain,
1804
            'limit': profit_rate or 0,
1805
            'order_type': order_type,
1806
            'amount': order.safe_amount_after_fee,
1807
            'open_rate': trade.open_rate,
1808
            'current_rate': current_rate,
1809
            'profit_amount': profit.profit_abs,
1810
            'profit_ratio': profit.profit_ratio,
1811
            'buy_tag': trade.enter_tag,
1812
            'enter_tag': trade.enter_tag,
1813
            'sell_reason': trade.exit_reason,  # Deprecated
1814
            'exit_reason': trade.exit_reason,
1815
            'open_date': trade.open_date,
1816
            'close_date': trade.close_date or datetime.now(timezone.utc),
1817
            'stake_currency': self.config['stake_currency'],
1818
            'base_currency': self.exchange.get_pair_base_currency(trade.pair),
1819
            'fiat_currency': self.config.get('fiat_display_currency', None),
1820
            'reason': reason,
1821
            'sub_trade': sub_trade,
1822
            'stake_amount': trade.stake_amount,
1823
        }
1824

1825
        # Send the message
1826
        self.rpc.send_msg(msg)
1✔
1827

1828
    def order_obj_or_raise(self, order_id: str, order_obj: Optional[Order]) -> Order:
1✔
1829
        if not order_obj:
1✔
1830
            raise DependencyException(
×
1831
                f"Order_obj not found for {order_id}. This should not have happened.")
1832
        return order_obj
1✔
1833

1834
#
1835
# Common update trade state methods
1836
#
1837

1838
    def update_trade_state(
1✔
1839
            self, trade: Trade, order_id: Optional[str],
1840
            action_order: Optional[Dict[str, Any]] = None,
1841
            stoploss_order: bool = False, send_msg: bool = True) -> bool:
1842
        """
1843
        Checks trades with open orders and updates the amount if necessary
1844
        Handles closing both buy and sell orders.
1845
        :param trade: Trade object of the trade we're analyzing
1846
        :param order_id: Order-id of the order we're analyzing
1847
        :param action_order: Already acquired order object
1848
        :param send_msg: Send notification - should always be True except in "recovery" methods
1849
        :return: True if order has been cancelled without being filled partially, False otherwise
1850
        """
1851
        if not order_id:
1✔
1852
            logger.warning(f'Orderid for trade {trade} is empty.')
1✔
1853
            return False
1✔
1854

1855
        # Update trade with order values
1856
        if not stoploss_order:
1✔
1857
            logger.info(f'Found open order for {trade}')
1✔
1858
        try:
1✔
1859
            order = action_order or self.exchange.fetch_order_or_stoploss_order(
1✔
1860
                order_id, trade.pair, stoploss_order)
1861
        except InvalidOrderException as exception:
1✔
1862
            logger.warning('Unable to fetch order %s: %s', order_id, exception)
1✔
1863
            return False
1✔
1864

1865
        trade.update_order(order)
1✔
1866

1867
        if self.exchange.check_order_canceled_empty(order):
1✔
1868
            # Trade has been cancelled on exchange
1869
            # Handling of this will happen in check_handle_timedout.
1870
            return True
1✔
1871

1872
        order_obj_or_none = trade.select_order_by_order_id(order_id)
1✔
1873
        order_obj = self.order_obj_or_raise(order_id, order_obj_or_none)
1✔
1874

1875
        self.handle_order_fee(trade, order_obj, order)
1✔
1876

1877
        trade.update_trade(order_obj)
1✔
1878

1879
        trade = self._update_trade_after_fill(trade, order_obj)
1✔
1880
        Trade.commit()
1✔
1881

1882
        self.order_close_notify(trade, order_obj, stoploss_order, send_msg)
1✔
1883

1884
        return False
1✔
1885

1886
    def _update_trade_after_fill(self, trade: Trade, order: Order) -> Trade:
1✔
1887
        if order.status in constants.NON_OPEN_EXCHANGE_STATES:
1✔
1888
            # If a entry order was closed, force update on stoploss on exchange
1889
            if order.ft_order_side == trade.entry_side:
1✔
1890
                trade = self.cancel_stoploss_on_exchange(trade)
1✔
1891
                if not self.edge:
1✔
1892
                    # TODO: should shorting/leverage be supported by Edge,
1893
                    # then this will need to be fixed.
1894
                    trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
1✔
1895
            if order.ft_order_side == trade.entry_side or (trade.amount > 0 and trade.is_open):
1✔
1896
                # Must also run for partial exits
1897
                # TODO: Margin will need to use interest_rate as well.
1898
                # interest_rate = self.exchange.get_interest_rate()
1899
                try:
1✔
1900
                    trade.set_liquidation_price(self.exchange.get_liquidation_price(
1✔
1901
                        pair=trade.pair,
1902
                        open_rate=trade.open_rate,
1903
                        is_short=trade.is_short,
1904
                        amount=trade.amount,
1905
                        stake_amount=trade.stake_amount,
1906
                        leverage=trade.leverage,
1907
                        wallet_balance=trade.stake_amount,
1908
                    ))
1909
                except DependencyException:
1✔
1910
                    logger.warning('Unable to calculate liquidation price')
1✔
1911
                if self.strategy.use_custom_stoploss:
1✔
1912
                    current_rate = self.exchange.get_rate(
×
1913
                        trade.pair, side='exit', is_short=trade.is_short, refresh=True)
1914
                    profit = trade.calc_profit_ratio(current_rate)
×
1915
                    self.strategy.ft_stoploss_adjust(current_rate, trade,
×
1916
                                                     datetime.now(timezone.utc), profit, 0,
1917
                                                     after_fill=True)
1918
            # Updating wallets when order is closed
1919
            self.wallets.update()
1✔
1920
        return trade
1✔
1921

1922
    def order_close_notify(
1✔
1923
            self, trade: Trade, order: Order, stoploss_order: bool, send_msg: bool):
1924
        """send "fill" notifications"""
1925

1926
        sub_trade = not isclose(order.safe_amount_after_fee,
1✔
1927
                                trade.amount, abs_tol=constants.MATH_CLOSE_PREC)
1928
        if order.ft_order_side == trade.exit_side:
1✔
1929
            # Exit notification
1930
            if send_msg and not stoploss_order and not trade.open_order_id:
1✔
1931
                self._notify_exit(trade, '', fill=True, sub_trade=sub_trade, order=order)
1✔
1932
            if not trade.is_open:
1✔
1933
                self.handle_protections(trade.pair, trade.trade_direction)
1✔
1934
        elif send_msg and not trade.open_order_id and not stoploss_order:
1✔
1935
            # Enter fill
1936
            self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade)
1✔
1937

1938
    def handle_protections(self, pair: str, side: LongShort) -> None:
1✔
1939
        # Lock pair for one candle to prevent immediate rebuys
1940
        self.strategy.lock_pair(pair, datetime.now(timezone.utc), reason='Auto lock')
1✔
1941
        prot_trig = self.protections.stop_per_pair(pair, side=side)
1✔
1942
        if prot_trig:
1✔
1943
            msg: RPCProtectionMsg = {
1✔
1944
                'type': RPCMessageType.PROTECTION_TRIGGER,
1945
                'base_currency': self.exchange.get_pair_base_currency(prot_trig.pair),
1946
                **prot_trig.to_json()  # type: ignore
1947
            }
1948
            self.rpc.send_msg(msg)
1✔
1949

1950
        prot_trig_glb = self.protections.global_stop(side=side)
1✔
1951
        if prot_trig_glb:
1✔
1952
            msg = {
1✔
1953
                'type': RPCMessageType.PROTECTION_TRIGGER_GLOBAL,
1954
                'base_currency': self.exchange.get_pair_base_currency(prot_trig_glb.pair),
1955
                **prot_trig_glb.to_json()  # type: ignore
1956
            }
1957
            self.rpc.send_msg(msg)
1✔
1958

1959
    def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,
1✔
1960
                              amount: float, fee_abs: float, order_obj: Order) -> Optional[float]:
1961
        """
1962
        Applies the fee to amount (either from Order or from Trades).
1963
        Can eat into dust if more than the required asset is available.
1964
        In case of trade adjustment orders, trade.amount will not have been adjusted yet.
1965
        Can't happen in Futures mode - where Fees are always in settlement currency,
1966
        never in base currency.
1967
        """
1968
        self.wallets.update()
1✔
1969
        amount_ = trade.amount
1✔
1970
        if order_obj.ft_order_side == trade.exit_side or order_obj.ft_order_side == 'stoploss':
1✔
1971
            # check against remaining amount!
1972
            amount_ = trade.amount - amount
1✔
1973

1974
        if trade.nr_of_successful_entries >= 1 and order_obj.ft_order_side == trade.entry_side:
1✔
1975
            # In case of rebuy's, trade.amount doesn't contain the amount of the last entry.
1976
            amount_ = trade.amount + amount
1✔
1977

1978
        if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount_:
1✔
1979
            # Eat into dust if we own more than base currency
1980
            logger.info(f"Fee amount for {trade} was in base currency - "
1✔
1981
                        f"Eating Fee {fee_abs} into dust.")
1982
        elif fee_abs != 0:
1✔
1983
            logger.info(f"Applying fee on amount for {trade}, fee={fee_abs}.")
1✔
1984
            return fee_abs
1✔
1985
        return None
1✔
1986

1987
    def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None:
1✔
1988
        # Try update amount (binance-fix)
1989
        try:
1✔
1990
            fee_abs = self.get_real_amount(trade, order, order_obj)
1✔
1991
            if fee_abs is not None:
1✔
1992
                order_obj.ft_fee_base = fee_abs
1✔
1993
        except DependencyException as exception:
1✔
1994
            logger.warning("Could not update trade amount: %s", exception)
1✔
1995

1996
    def get_real_amount(self, trade: Trade, order: Dict, order_obj: Order) -> Optional[float]:
1✔
1997
        """
1998
        Detect and update trade fee.
1999
        Calls trade.update_fee() upon correct detection.
2000
        Returns modified amount if the fee was taken from the destination currency.
2001
        Necessary for exchanges which charge fees in base currency (e.g. binance)
2002
        :return: Absolute fee to apply for this order or None
2003
        """
2004
        # Init variables
2005
        order_amount = safe_value_fallback(order, 'filled', 'amount')
1✔
2006
        # Only run for closed orders
2007
        if (
1✔
2008
            trade.fee_updated(order.get('side', ''))
2009
            or order['status'] == 'open'
2010
            or order_obj.ft_fee_base
2011
        ):
2012
            return None
1✔
2013

2014
        trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
1✔
2015
        # use fee from order-dict if possible
2016
        if self.exchange.order_has_fee(order):
1✔
2017
            fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(
1✔
2018
                order['fee'], order['symbol'], order['cost'], order_obj.safe_filled)
2019
            logger.info(f"Fee for Trade {trade} [{order_obj.ft_order_side}]: "
1✔
2020
                        f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}")
2021
            if fee_rate is None or fee_rate < 0.02:
1✔
2022
                # Reject all fees that report as > 2%.
2023
                # These are most likely caused by a parsing bug in ccxt
2024
                # due to multiple trades (https://github.com/ccxt/ccxt/issues/8025)
2025
                trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
1✔
2026
                if trade_base_currency == fee_currency:
1✔
2027
                    # Apply fee to amount
2028
                    return self.apply_fee_conditional(trade, trade_base_currency,
1✔
2029
                                                      amount=order_amount, fee_abs=fee_cost,
2030
                                                      order_obj=order_obj)
2031
                return None
1✔
2032
        return self.fee_detection_from_trades(
1✔
2033
            trade, order, order_obj, order_amount, order.get('trades', []))
2034

2035
    def fee_detection_from_trades(self, trade: Trade, order: Dict, order_obj: Order,
1✔
2036
                                  order_amount: float, trades: List) -> Optional[float]:
2037
        """
2038
        fee-detection fallback to Trades.
2039
        Either uses provided trades list or the result of fetch_my_trades to get correct fee.
2040
        """
2041
        if not trades:
1✔
2042
            trades = self.exchange.get_trades_for_order(
1✔
2043
                self.exchange.get_order_id_conditional(order), trade.pair, order_obj.order_date)
2044

2045
        if len(trades) == 0:
1✔
2046
            logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
1✔
2047
            return None
1✔
2048
        fee_currency = None
1✔
2049
        amount = 0
1✔
2050
        fee_abs = 0.0
1✔
2051
        fee_cost = 0.0
1✔
2052
        trade_base_currency = self.exchange.get_pair_base_currency(trade.pair)
1✔
2053
        fee_rate_array: List[float] = []
1✔
2054
        for exectrade in trades:
1✔
2055
            amount += exectrade['amount']
1✔
2056
            if self.exchange.order_has_fee(exectrade):
1✔
2057
                # Prefer singular fee
2058
                fees = [exectrade['fee']]
1✔
2059
            else:
2060
                fees = exectrade.get('fees', [])
1✔
2061
            for fee in fees:
1✔
2062

2063
                fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(
1✔
2064
                    fee, exectrade['symbol'], exectrade['cost'], exectrade['amount']
2065
                )
2066
                fee_cost += fee_cost_
1✔
2067
                if fee_rate_ is not None:
1✔
2068
                    fee_rate_array.append(fee_rate_)
1✔
2069
                # only applies if fee is in quote currency!
2070
                if trade_base_currency == fee_currency:
1✔
2071
                    fee_abs += fee_cost_
1✔
2072
        # Ensure at least one trade was found:
2073
        if fee_currency:
1✔
2074
            # fee_rate should use mean
2075
            fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None
1✔
2076
            if fee_rate is not None and fee_rate < 0.02:
1✔
2077
                # Only update if fee-rate is < 2%
2078
                trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', ''))
1✔
2079
            else:
2080
                logger.warning(
1✔
2081
                    f"Not updating {order.get('side', '')}-fee - rate: {fee_rate}, {fee_currency}.")
2082

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

2088
        if fee_abs != 0:
1✔
2089
            return self.apply_fee_conditional(
1✔
2090
                trade, trade_base_currency, amount=amount, fee_abs=fee_abs, order_obj=order_obj)
2091
        return None
1✔
2092

2093
    def get_valid_price(self, custom_price: float, proposed_price: float) -> float:
1✔
2094
        """
2095
        Return the valid price.
2096
        Check if the custom price is of the good type if not return proposed_price
2097
        :return: valid price for the order
2098
        """
2099
        if custom_price:
1✔
2100
            try:
1✔
2101
                valid_custom_price = float(custom_price)
1✔
2102
            except ValueError:
1✔
2103
                valid_custom_price = proposed_price
1✔
2104
        else:
2105
            valid_custom_price = proposed_price
1✔
2106

2107
        cust_p_max_dist_r = self.config.get('custom_price_max_distance_ratio', 0.02)
1✔
2108
        min_custom_price_allowed = proposed_price - (proposed_price * cust_p_max_dist_r)
1✔
2109
        max_custom_price_allowed = proposed_price + (proposed_price * cust_p_max_dist_r)
1✔
2110

2111
        # Bracket between min_custom_price_allowed and max_custom_price_allowed
2112
        return max(
1✔
2113
            min(valid_custom_price, max_custom_price_allowed),
2114
            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