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

freqtrade / freqtrade / 9394559170

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

push

github

xmatthias
Loader should be passed as kwarg for clarity

20280 of 21425 relevant lines covered (94.66%)

0.95 hits per line

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

97.08
/freqtrade/exchange/exchange.py
1
# pragma pylint: disable=W0603
2
"""
1✔
3
Cryptocurrency Exchanges support
4
"""
5
import asyncio
1✔
6
import inspect
1✔
7
import logging
1✔
8
import signal
1✔
9
from copy import deepcopy
1✔
10
from datetime import datetime, timedelta, timezone
1✔
11
from math import floor, isnan
1✔
12
from threading import Lock
1✔
13
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
1✔
14

15
import ccxt
1✔
16
import ccxt.async_support as ccxt_async
1✔
17
from cachetools import TTLCache
1✔
18
from ccxt import TICK_SIZE
1✔
19
from dateutil import parser
1✔
20
from pandas import DataFrame, concat
1✔
21

22
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk,
1✔
23
                                 BuySell, Config, EntryExit, ExchangeConfig,
24
                                 ListPairsWithTimeframes, MakerTaker, OBLiteral, PairWithTimeframe)
25
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
1✔
26
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, RunMode, TradingMode
1✔
27
from freqtrade.exceptions import (ConfigurationError, DDosProtection, ExchangeError,
1✔
28
                                  InsufficientFundsError, InvalidOrderException,
29
                                  OperationalException, PricingError, RetryableOrderError,
30
                                  TemporaryError)
31
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_exchange_credentials,
1✔
32
                                       retrier, retrier_async)
33
from freqtrade.exchange.exchange_utils import (ROUND, ROUND_DOWN, ROUND_UP, CcxtModuleType,
1✔
34
                                               amount_to_contract_precision, amount_to_contracts,
35
                                               amount_to_precision, contracts_to_amount,
36
                                               date_minus_candles, is_exchange_known_ccxt,
37
                                               market_is_active, price_to_precision)
38
from freqtrade.exchange.exchange_utils_timeframe import (timeframe_to_minutes, timeframe_to_msecs,
1✔
39
                                                         timeframe_to_next_date,
40
                                                         timeframe_to_prev_date,
41
                                                         timeframe_to_seconds)
42
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
1✔
43
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
1✔
44
                            safe_value_fallback2)
45
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
1✔
46
from freqtrade.util import dt_from_ts, dt_now
1✔
47
from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts
1✔
48
from freqtrade.util.periodic_cache import PeriodicCache
1✔
49

50

51
logger = logging.getLogger(__name__)
1✔
52

53

54
class Exchange:
1✔
55

56
    # Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
57
    _params: Dict = {}
1✔
58

59
    # Additional parameters - added to the ccxt object
60
    _ccxt_params: Dict = {}
1✔
61

62
    # Dict to specify which options each exchange implements
63
    # This defines defaults, which can be selectively overridden by subclasses using _ft_has
64
    # or by specifying them in the configuration.
65
    _ft_has_default: Dict = {
1✔
66
        "stoploss_on_exchange": False,
67
        "stop_price_param": "stopLossPrice",  # Used for stoploss_on_exchange request
68
        "stop_price_prop": "stopLossPrice",  # Used for stoploss_on_exchange response parsing
69
        "order_time_in_force": ["GTC"],
70
        "ohlcv_params": {},
71
        "ohlcv_candle_limit": 500,
72
        "ohlcv_has_history": True,  # Some exchanges (Kraken) don't provide history via ohlcv
73
        "ohlcv_partial_candle": True,
74
        "ohlcv_require_since": False,
75
        # Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
76
        "ohlcv_volume_currency": "base",  # "base" or "quote"
77
        "tickers_have_quoteVolume": True,
78
        "tickers_have_bid_ask": True,  # bid / ask empty for fetch_tickers
79
        "tickers_have_price": True,
80
        "trades_pagination": "time",  # Possible are "time" or "id"
81
        "trades_pagination_arg": "since",
82
        "l2_limit_range": None,
83
        "l2_limit_range_required": True,  # Allow Empty L2 limit (kucoin)
84
        "mark_ohlcv_price": "mark",
85
        "mark_ohlcv_timeframe": "8h",
86
        "funding_fee_timeframe": "8h",
87
        "ccxt_futures_name": "swap",
88
        "needs_trading_fees": False,  # use fetch_trading_fees to cache fees
89
        "order_props_in_contracts": ['amount', 'filled', 'remaining'],
90
        # Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
91
        "marketOrderRequiresPrice": False,
92
        "exchange_has_overrides": {},  # Dictionary overriding ccxt's "has".
93
        # Expected to be in the format {"fetchOHLCV": True} or {"fetchOHLCV": False}
94
    }
95
    _ft_has: Dict = {}
1✔
96
    _ft_has_futures: Dict = {}
1✔
97

98
    _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
1✔
99
        # TradingMode.SPOT always supported and not required in this list
100
    ]
101

102
    def __init__(self, config: Config, *, exchange_config: Optional[ExchangeConfig] = None,
1✔
103
                 validate: bool = True, load_leverage_tiers: bool = False) -> None:
104
        """
105
        Initializes this module with the given config,
106
        it does basic validation whether the specified exchange and pairs are valid.
107
        :return: None
108
        """
109
        self._api: ccxt.Exchange
1✔
110
        self._api_async: ccxt_async.Exchange = None
1✔
111
        self._markets: Dict = {}
1✔
112
        self._trading_fees: Dict[str, Any] = {}
1✔
113
        self._leverage_tiers: Dict[str, List[Dict]] = {}
1✔
114
        # Lock event loop. This is necessary to avoid race-conditions when using force* commands
115
        # Due to funding fee fetching.
116
        self._loop_lock = Lock()
1✔
117
        self.loop = self._init_async_loop()
1✔
118
        self._config: Config = {}
1✔
119

120
        self._config.update(config)
1✔
121

122
        # Holds last candle refreshed time of each pair
123
        self._pairs_last_refresh_time: Dict[PairWithTimeframe, int] = {}
1✔
124
        # Timestamp of last markets refresh
125
        self._last_markets_refresh: int = 0
1✔
126

127
        # Cache for 10 minutes ...
128
        self._cache_lock = Lock()
1✔
129
        self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10)
1✔
130
        # Cache values for 300 to avoid frequent polling of the exchange for prices
131
        # Caching only applies to RPC methods, so prices for open trades are still
132
        # refreshed once every iteration.
133
        # Shouldn't be too high either, as it'll freeze UI updates in case of open orders.
134
        self._exit_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=300)
1✔
135
        self._entry_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=300)
1✔
136

137
        # Holds candles
138
        self._klines: Dict[PairWithTimeframe, DataFrame] = {}
1✔
139
        self._expiring_candle_cache: Dict[Tuple[str, int], PeriodicCache] = {}
1✔
140

141
        # Holds all open sell orders for dry_run
142
        self._dry_run_open_orders: Dict[str, Any] = {}
1✔
143

144
        if config['dry_run']:
1✔
145
            logger.info('Instance is running with dry_run enabled')
1✔
146
        logger.info(f"Using CCXT {ccxt.__version__}")
1✔
147
        exchange_conf: Dict[str, Any] = exchange_config if exchange_config else config['exchange']
1✔
148
        remove_exchange_credentials(exchange_conf, config.get('dry_run', False))
1✔
149
        self.log_responses = exchange_conf.get('log_responses', False)
1✔
150

151
        # Leverage properties
152
        self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
1✔
153
        self.margin_mode: MarginMode = (
1✔
154
            MarginMode(config.get('margin_mode'))
155
            if config.get('margin_mode')
156
            else MarginMode.NONE
157
        )
158
        self.liquidation_buffer = config.get('liquidation_buffer', 0.05)
1✔
159

160
        # Deep merge ft_has with default ft_has options
161
        self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
1✔
162
        if self.trading_mode == TradingMode.FUTURES:
1✔
163
            self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
1✔
164
        if exchange_conf.get('_ft_has_params'):
1✔
165
            self._ft_has = deep_merge_dicts(exchange_conf.get('_ft_has_params'),
1✔
166
                                            self._ft_has)
167
            logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
1✔
168

169
        # Assign this directly for easy access
170
        self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle']
1✔
171

172
        self._trades_pagination = self._ft_has['trades_pagination']
1✔
173
        self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
1✔
174

175
        # Initialize ccxt objects
176
        ccxt_config = self._ccxt_config
1✔
177
        ccxt_config = deep_merge_dicts(exchange_conf.get('ccxt_config', {}), ccxt_config)
1✔
178
        ccxt_config = deep_merge_dicts(exchange_conf.get('ccxt_sync_config', {}), ccxt_config)
1✔
179

180
        self._api = self._init_ccxt(exchange_conf, ccxt_kwargs=ccxt_config)
1✔
181

182
        ccxt_async_config = self._ccxt_config
1✔
183
        ccxt_async_config = deep_merge_dicts(exchange_conf.get('ccxt_config', {}),
1✔
184
                                             ccxt_async_config)
185
        ccxt_async_config = deep_merge_dicts(exchange_conf.get('ccxt_async_config', {}),
1✔
186
                                             ccxt_async_config)
187
        self._api_async = self._init_ccxt(
1✔
188
            exchange_conf, ccxt_async, ccxt_kwargs=ccxt_async_config)
189

190
        logger.info(f'Using Exchange "{self.name}"')
1✔
191
        self.required_candle_call_count = 1
1✔
192
        if validate:
1✔
193
            # Initial markets load
194
            self._load_markets()
1✔
195
            self.validate_config(config)
1✔
196
            self._startup_candle_count: int = config.get('startup_candle_count', 0)
1✔
197
            self.required_candle_call_count = self.validate_required_startup_candles(
1✔
198
                self._startup_candle_count, config.get('timeframe', ''))
199

200
        # Converts the interval provided in minutes in config to seconds
201
        self.markets_refresh_interval: int = exchange_conf.get(
1✔
202
            "markets_refresh_interval", 60) * 60 * 1000
203

204
        if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
1✔
205
            self.fill_leverage_tiers()
1✔
206
        self.additional_exchange_init()
1✔
207

208
    def __del__(self):
1✔
209
        """
210
        Destructor - clean up async stuff
211
        """
212
        self.close()
1✔
213

214
    def close(self):
1✔
215
        logger.debug("Exchange object destroyed, closing async loop")
1✔
216
        if (self._api_async and inspect.iscoroutinefunction(self._api_async.close)
1✔
217
                and self._api_async.session):
218
            logger.debug("Closing async ccxt session.")
×
219
            self.loop.run_until_complete(self._api_async.close())
×
220
        if self.loop and not self.loop.is_closed():
1✔
221
            self.loop.close()
1✔
222

223
    def _init_async_loop(self) -> asyncio.AbstractEventLoop:
1✔
224
        loop = asyncio.new_event_loop()
1✔
225
        asyncio.set_event_loop(loop)
1✔
226
        return loop
1✔
227

228
    def validate_config(self, config):
1✔
229
        # Check if timeframe is available
230
        self.validate_timeframes(config.get('timeframe'))
1✔
231

232
        # Check if all pairs are available
233
        self.validate_stakecurrency(config['stake_currency'])
1✔
234
        if not config['exchange'].get('skip_pair_validation'):
1✔
235
            self.validate_pairs(config['exchange']['pair_whitelist'])
1✔
236
        self.validate_ordertypes(config.get('order_types', {}))
1✔
237
        self.validate_order_time_in_force(config.get('order_time_in_force', {}))
1✔
238
        self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
1✔
239
        self.validate_pricing(config['exit_pricing'])
1✔
240
        self.validate_pricing(config['entry_pricing'])
1✔
241

242
    def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt, *,
1✔
243
                   ccxt_kwargs: Dict) -> ccxt.Exchange:
244
        """
245
        Initialize ccxt with given config and return valid
246
        ccxt instance.
247
        """
248
        # Find matching class for the given exchange name
249
        name = exchange_config['name']
1✔
250

251
        if not is_exchange_known_ccxt(name, ccxt_module):
1✔
252
            raise OperationalException(f'Exchange {name} is not supported by ccxt')
1✔
253

254
        ex_config = {
1✔
255
            'apiKey': exchange_config.get('key'),
256
            'secret': exchange_config.get('secret'),
257
            'password': exchange_config.get('password'),
258
            'uid': exchange_config.get('uid', ''),
259
        }
260
        if ccxt_kwargs:
1✔
261
            logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
1✔
262
        if self._ccxt_params:
1✔
263
            # Inject static options after the above output to not confuse users.
264
            ccxt_kwargs = deep_merge_dicts(self._ccxt_params, ccxt_kwargs)
1✔
265
        if ccxt_kwargs:
1✔
266
            ex_config.update(ccxt_kwargs)
1✔
267
        try:
1✔
268

269
            api = getattr(ccxt_module, name.lower())(ex_config)
1✔
270
        except (KeyError, AttributeError) as e:
1✔
271
            raise OperationalException(f'Exchange {name} is not supported') from e
1✔
272
        except ccxt.BaseError as e:
1✔
273
            raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
1✔
274

275
        return api
1✔
276

277
    @property
1✔
278
    def _ccxt_config(self) -> Dict:
1✔
279
        # Parameters to add directly to ccxt sync/async initialization.
280
        if self.trading_mode == TradingMode.MARGIN:
1✔
281
            return {
1✔
282
                "options": {
283
                    "defaultType": "margin"
284
                }
285
            }
286
        elif self.trading_mode == TradingMode.FUTURES:
1✔
287
            return {
1✔
288
                "options": {
289
                    "defaultType": self._ft_has["ccxt_futures_name"]
290
                }
291
            }
292
        else:
293
            return {}
1✔
294

295
    @property
1✔
296
    def name(self) -> str:
1✔
297
        """exchange Name (from ccxt)"""
298
        return self._api.name
1✔
299

300
    @property
1✔
301
    def id(self) -> str:
1✔
302
        """exchange ccxt id"""
303
        return self._api.id
×
304

305
    @property
1✔
306
    def timeframes(self) -> List[str]:
1✔
307
        return list((self._api.timeframes or {}).keys())
1✔
308

309
    @property
1✔
310
    def markets(self) -> Dict[str, Any]:
1✔
311
        """exchange ccxt markets"""
312
        if not self._markets:
1✔
313
            logger.info("Markets were not loaded. Loading them now..")
1✔
314
            self._load_markets()
1✔
315
        return self._markets
1✔
316

317
    @property
1✔
318
    def precisionMode(self) -> int:
1✔
319
        """exchange ccxt precisionMode"""
320
        return self._api.precisionMode
1✔
321

322
    def additional_exchange_init(self) -> None:
1✔
323
        """
324
        Additional exchange initialization logic.
325
        .api will be available at this point.
326
        Must be overridden in child methods if required.
327
        """
328
        pass
1✔
329

330
    def _log_exchange_response(self, endpoint: str, response, *, add_info=None) -> None:
1✔
331
        """ Log exchange responses """
332
        if self.log_responses:
1✔
333
            add_info_str = "" if add_info is None else f" {add_info}: "
1✔
334
            logger.info(f"API {endpoint}: {add_info_str}{response}")
1✔
335

336
    def ohlcv_candle_limit(
1✔
337
            self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
338
        """
339
        Exchange ohlcv candle limit
340
        Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
341
        per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
342
        TODO: this is most likely no longer needed since only bittrex needed this.
343
        :param timeframe: Timeframe to check
344
        :param candle_type: Candle-type
345
        :param since_ms: Starting timestamp
346
        :return: Candle limit as integer
347
        """
348
        return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get(
1✔
349
            timeframe, self._ft_has.get('ohlcv_candle_limit')))
350

351
    def get_markets(
1✔
352
            self,
353
            base_currencies: Optional[List[str]] = None,
354
            quote_currencies: Optional[List[str]] = None,
355
            spot_only: bool = False, margin_only: bool = False, futures_only: bool = False,
356
            tradable_only: bool = True,
357
            active_only: bool = False) -> Dict[str, Any]:
358
        """
359
        Return exchange ccxt markets, filtered out by base currency and quote currency
360
        if this was requested in parameters.
361
        """
362
        markets = self.markets
1✔
363
        if not markets:
1✔
364
            raise OperationalException("Markets were not loaded.")
1✔
365

366
        if base_currencies:
1✔
367
            markets = {k: v for k, v in markets.items() if v['base'] in base_currencies}
1✔
368
        if quote_currencies:
1✔
369
            markets = {k: v for k, v in markets.items() if v['quote'] in quote_currencies}
1✔
370
        if tradable_only:
1✔
371
            markets = {k: v for k, v in markets.items() if self.market_is_tradable(v)}
1✔
372
        if spot_only:
1✔
373
            markets = {k: v for k, v in markets.items() if self.market_is_spot(v)}
1✔
374
        if margin_only:
1✔
375
            markets = {k: v for k, v in markets.items() if self.market_is_margin(v)}
×
376
        if futures_only:
1✔
377
            markets = {k: v for k, v in markets.items() if self.market_is_future(v)}
1✔
378
        if active_only:
1✔
379
            markets = {k: v for k, v in markets.items() if market_is_active(v)}
1✔
380
        return markets
1✔
381

382
    def get_quote_currencies(self) -> List[str]:
1✔
383
        """
384
        Return a list of supported quote currencies
385
        """
386
        markets = self.markets
1✔
387
        return sorted(set([x['quote'] for _, x in markets.items()]))
1✔
388

389
    def get_pair_quote_currency(self, pair: str) -> str:
1✔
390
        """ Return a pair's quote currency (base/quote:settlement) """
391
        return self.markets.get(pair, {}).get('quote', '')
1✔
392

393
    def get_pair_base_currency(self, pair: str) -> str:
1✔
394
        """ Return a pair's base currency (base/quote:settlement) """
395
        return self.markets.get(pair, {}).get('base', '')
1✔
396

397
    def market_is_future(self, market: Dict[str, Any]) -> bool:
1✔
398
        return (
1✔
399
            market.get(self._ft_has["ccxt_futures_name"], False) is True and
400
            market.get('linear', False) is True
401
        )
402

403
    def market_is_spot(self, market: Dict[str, Any]) -> bool:
1✔
404
        return market.get('spot', False) is True
1✔
405

406
    def market_is_margin(self, market: Dict[str, Any]) -> bool:
1✔
407
        return market.get('margin', False) is True
1✔
408

409
    def market_is_tradable(self, market: Dict[str, Any]) -> bool:
1✔
410
        """
411
        Check if the market symbol is tradable by Freqtrade.
412
        Ensures that Configured mode aligns to
413
        """
414
        return (
1✔
415
            market.get('quote', None) is not None
416
            and market.get('base', None) is not None
417
            and (self.precisionMode != TICK_SIZE
418
                 # Too low precision will falsify calculations
419
                 or market.get('precision', {}).get('price') > 1e-11)
420
            and ((self.trading_mode == TradingMode.SPOT and self.market_is_spot(market))
421
                 or (self.trading_mode == TradingMode.MARGIN and self.market_is_margin(market))
422
                 or (self.trading_mode == TradingMode.FUTURES and self.market_is_future(market)))
423
        )
424

425
    def klines(self, pair_interval: PairWithTimeframe, copy: bool = True) -> DataFrame:
1✔
426
        if pair_interval in self._klines:
1✔
427
            return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
1✔
428
        else:
429
            return DataFrame()
1✔
430

431
    def get_contract_size(self, pair: str) -> Optional[float]:
1✔
432
        if self.trading_mode == TradingMode.FUTURES:
1✔
433
            market = self.markets.get(pair, {})
1✔
434
            contract_size: float = 1.0
1✔
435
            if not market:
1✔
436
                return None
1✔
437
            if market.get('contractSize') is not None:
1✔
438
                # ccxt has contractSize in markets as string
439
                contract_size = float(market['contractSize'])
1✔
440
            return contract_size
1✔
441
        else:
442
            return 1
1✔
443

444
    def _trades_contracts_to_amount(self, trades: List) -> List:
1✔
445
        if len(trades) > 0 and 'symbol' in trades[0]:
1✔
446
            contract_size = self.get_contract_size(trades[0]['symbol'])
1✔
447
            if contract_size != 1:
1✔
448
                for trade in trades:
1✔
449
                    trade['amount'] = trade['amount'] * contract_size
1✔
450
        return trades
1✔
451

452
    def _order_contracts_to_amount(self, order: Dict) -> Dict:
1✔
453
        if 'symbol' in order and order['symbol'] is not None:
1✔
454
            contract_size = self.get_contract_size(order['symbol'])
1✔
455
            if contract_size != 1:
1✔
456
                for prop in self._ft_has.get('order_props_in_contracts', []):
1✔
457
                    if prop in order and order[prop] is not None:
1✔
458
                        order[prop] = order[prop] * contract_size
1✔
459
        return order
1✔
460

461
    def _amount_to_contracts(self, pair: str, amount: float) -> float:
1✔
462

463
        contract_size = self.get_contract_size(pair)
1✔
464
        return amount_to_contracts(amount, contract_size)
1✔
465

466
    def _contracts_to_amount(self, pair: str, num_contracts: float) -> float:
1✔
467

468
        contract_size = self.get_contract_size(pair)
1✔
469
        return contracts_to_amount(num_contracts, contract_size)
1✔
470

471
    def amount_to_contract_precision(self, pair: str, amount: float) -> float:
1✔
472
        """
473
        Helper wrapper around amount_to_contract_precision
474
        """
475
        contract_size = self.get_contract_size(pair)
1✔
476

477
        return amount_to_contract_precision(amount, self.get_precision_amount(pair),
1✔
478
                                            self.precisionMode, contract_size)
479

480
    def _load_async_markets(self, reload: bool = False) -> None:
1✔
481
        try:
1✔
482
            if self._api_async:
1✔
483
                self.loop.run_until_complete(
1✔
484
                    self._api_async.load_markets(reload=reload, params={}))
485

486
        except (asyncio.TimeoutError, ccxt.BaseError) as e:
1✔
487
            logger.warning('Could not load async markets. Reason: %s', e)
1✔
488
            return
1✔
489

490
    def _load_markets(self) -> None:
1✔
491
        """ Initialize markets both sync and async """
492
        try:
1✔
493
            self._markets = self._api.load_markets(params={})
1✔
494
            self._load_async_markets()
1✔
495
            self._last_markets_refresh = dt_ts()
1✔
496
            if self._ft_has['needs_trading_fees']:
1✔
497
                self._trading_fees = self.fetch_trading_fees()
1✔
498

499
        except ccxt.BaseError:
1✔
500
            logger.exception('Unable to initialize markets.')
1✔
501

502
    def reload_markets(self, force: bool = False) -> None:
1✔
503
        """Reload markets both sync and async if refresh interval has passed """
504
        # Check whether markets have to be reloaded
505
        if (
1✔
506
            not force
507
            and self._last_markets_refresh > 0
508
            and (self._last_markets_refresh + self.markets_refresh_interval > dt_ts())
509
        ):
510
            return None
1✔
511
        logger.debug("Performing scheduled market reload..")
1✔
512
        try:
1✔
513
            self._markets = self._api.load_markets(reload=True, params={})
1✔
514
            # Also reload async markets to avoid issues with newly listed pairs
515
            self._load_async_markets(reload=True)
1✔
516
            self._last_markets_refresh = dt_ts()
1✔
517
            self.fill_leverage_tiers()
1✔
518
        except ccxt.BaseError:
1✔
519
            logger.exception("Could not reload markets.")
1✔
520

521
    def validate_stakecurrency(self, stake_currency: str) -> None:
1✔
522
        """
523
        Checks stake-currency against available currencies on the exchange.
524
        Only runs on startup. If markets have not been loaded, there's been a problem with
525
        the connection to the exchange.
526
        :param stake_currency: Stake-currency to validate
527
        :raise: OperationalException if stake-currency is not available.
528
        """
529
        if not self._markets:
1✔
530
            raise OperationalException(
1✔
531
                'Could not load markets, therefore cannot start. '
532
                'Please investigate the above error for more details.'
533
            )
534
        quote_currencies = self.get_quote_currencies()
1✔
535
        if stake_currency not in quote_currencies:
1✔
536
            raise ConfigurationError(
1✔
537
                f"{stake_currency} is not available as stake on {self.name}. "
538
                f"Available currencies are: {', '.join(quote_currencies)}")
539

540
    def validate_pairs(self, pairs: List[str]) -> None:
1✔
541
        """
542
        Checks if all given pairs are tradable on the current exchange.
543
        :param pairs: list of pairs
544
        :raise: OperationalException if one pair is not available
545
        :return: None
546
        """
547

548
        if not self.markets:
1✔
549
            logger.warning('Unable to validate pairs (assuming they are correct).')
1✔
550
            return
1✔
551
        extended_pairs = expand_pairlist(pairs, list(self.markets), keep_invalid=True)
1✔
552
        invalid_pairs = []
1✔
553
        for pair in extended_pairs:
1✔
554
            # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
555
            if self.markets and pair not in self.markets:
1✔
556
                raise OperationalException(
1✔
557
                    f'Pair {pair} is not available on {self.name} {self.trading_mode.value}. '
558
                    f'Please remove {pair} from your whitelist.')
559

560
                # From ccxt Documentation:
561
                # markets.info: An associative array of non-common market properties,
562
                # including fees, rates, limits and other general market information.
563
                # The internal info array is different for each particular market,
564
                # its contents depend on the exchange.
565
                # It can also be a string or similar ... so we need to verify that first.
566
            elif (isinstance(self.markets[pair].get('info'), dict)
1✔
567
                  and self.markets[pair].get('info', {}).get('prohibitedIn', False)):
568
                # Warn users about restricted pairs in whitelist.
569
                # We cannot determine reliably if Users are affected.
570
                logger.warning(f"Pair {pair} is restricted for some users on this exchange."
1✔
571
                               f"Please check if you are impacted by this restriction "
572
                               f"on the exchange and eventually remove {pair} from your whitelist.")
573
            if (self._config['stake_currency'] and
1✔
574
                    self.get_pair_quote_currency(pair) != self._config['stake_currency']):
575
                invalid_pairs.append(pair)
1✔
576
        if invalid_pairs:
1✔
577
            raise OperationalException(
1✔
578
                f"Stake-currency '{self._config['stake_currency']}' not compatible with "
579
                f"pair-whitelist. Please remove the following pairs: {invalid_pairs}")
580

581
    def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str:
1✔
582
        """
583
        Get valid pair combination of curr_1 and curr_2 by trying both combinations.
584
        """
585
        for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
1✔
586
            if pair in self.markets and self.markets[pair].get('active'):
1✔
587
                return pair
1✔
588
        raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
1✔
589

590
    def validate_timeframes(self, timeframe: Optional[str]) -> None:
1✔
591
        """
592
        Check if timeframe from config is a supported timeframe on the exchange
593
        """
594
        if not hasattr(self._api, "timeframes") or self._api.timeframes is None:
1✔
595
            # If timeframes attribute is missing (or is None), the exchange probably
596
            # has no fetchOHLCV method.
597
            # Therefore we also show that.
598
            raise OperationalException(
1✔
599
                f"The ccxt library does not provide the list of timeframes "
600
                f"for the exchange {self.name} and this exchange "
601
                f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}")
602

603
        if timeframe and (timeframe not in self.timeframes):
1✔
604
            raise ConfigurationError(
1✔
605
                f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}")
606

607
        if (
1✔
608
            timeframe
609
            and self._config['runmode'] != RunMode.UTIL_EXCHANGE
610
            and timeframe_to_minutes(timeframe) < 1
611
        ):
612
            raise ConfigurationError("Timeframes < 1m are currently not supported by Freqtrade.")
1✔
613

614
    def validate_ordertypes(self, order_types: Dict) -> None:
1✔
615
        """
616
        Checks if order-types configured in strategy/config are supported
617
        """
618
        if any(v == 'market' for k, v in order_types.items()):
1✔
619
            if not self.exchange_has('createMarketOrder'):
1✔
620
                raise ConfigurationError(
1✔
621
                    f'Exchange {self.name} does not support market orders.')
622
        self.validate_stop_ordertypes(order_types)
1✔
623

624
    def validate_stop_ordertypes(self, order_types: Dict) -> None:
1✔
625
        """
626
        Validate stoploss order types
627
        """
628
        if (order_types.get("stoploss_on_exchange")
1✔
629
                and not self._ft_has.get("stoploss_on_exchange", False)):
630
            raise ConfigurationError(
1✔
631
                f'On exchange stoploss is not supported for {self.name}.'
632
            )
633
        if self.trading_mode == TradingMode.FUTURES:
1✔
634
            price_mapping = self._ft_has.get('stop_price_type_value_mapping', {}).keys()
1✔
635
            if (
1✔
636
                order_types.get("stoploss_on_exchange", False) is True
637
                and 'stoploss_price_type' in order_types
638
                and order_types['stoploss_price_type'] not in price_mapping
639
            ):
640
                raise ConfigurationError(
1✔
641
                    f'On exchange stoploss price type is not supported for {self.name}.'
642
                )
643

644
    def validate_pricing(self, pricing: Dict) -> None:
1✔
645
        if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'):
1✔
646
            raise ConfigurationError(f'Orderbook not available for {self.name}.')
1✔
647
        if (not pricing.get('use_order_book', False) and (
1✔
648
                not self.exchange_has('fetchTicker')
649
                or not self._ft_has['tickers_have_price'])):
650
            raise ConfigurationError(f'Ticker pricing not available for {self.name}.')
1✔
651

652
    def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
1✔
653
        """
654
        Checks if order time in force configured in strategy/config are supported
655
        """
656
        if any(v.upper() not in self._ft_has["order_time_in_force"]
1✔
657
               for k, v in order_time_in_force.items()):
658
            raise ConfigurationError(
1✔
659
                f'Time in force policies are not supported for {self.name} yet.')
660

661
    def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
1✔
662
        """
663
        Checks if required startup_candles is more than ohlcv_candle_limit().
664
        Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
665
        """
666

667
        candle_limit = self.ohlcv_candle_limit(
1✔
668
            timeframe, self._config['candle_type_def'],
669
            dt_ts(date_minus_candles(timeframe, startup_candles))
670
            if timeframe else None)
671
        # Require one more candle - to account for the still open candle.
672
        candle_count = startup_candles + 1
1✔
673
        # Allow 5 calls to the exchange per pair
674
        required_candle_call_count = int(
1✔
675
            (candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1))
676
        if self._ft_has['ohlcv_has_history']:
1✔
677

678
            if required_candle_call_count > 5:
1✔
679
                # Only allow 5 calls per pair to somewhat limit the impact
680
                raise ConfigurationError(
1✔
681
                    f"This strategy requires {startup_candles} candles to start, "
682
                    "which is more than 5x "
683
                    f"the amount of candles {self.name} provides for {timeframe}.")
684
        elif required_candle_call_count > 1:
1✔
685
            raise ConfigurationError(
1✔
686
                f"This strategy requires {startup_candles} candles to start, which is more than "
687
                f"the amount of candles {self.name} provides for {timeframe}.")
688
        if required_candle_call_count > 1:
1✔
689
            logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. "
1✔
690
                           f"This can result in slower operations for the bot. Please check "
691
                           f"if you really need {startup_candles} candles for your strategy")
692
        return required_candle_call_count
1✔
693

694
    def validate_trading_mode_and_margin_mode(
1✔
695
        self,
696
        trading_mode: TradingMode,
697
        margin_mode: Optional[MarginMode]  # Only None when trading_mode = TradingMode.SPOT
698
    ):
699
        """
700
        Checks if freqtrade can perform trades using the configured
701
        trading mode(Margin, Futures) and MarginMode(Cross, Isolated)
702
        Throws OperationalException:
703
            If the trading_mode/margin_mode type are not supported by freqtrade on this exchange
704
        """
705
        if trading_mode != TradingMode.SPOT and (
1✔
706
            (trading_mode, margin_mode) not in self._supported_trading_mode_margin_pairs
707
        ):
708
            mm_value = margin_mode and margin_mode.value
1✔
709
            raise OperationalException(
1✔
710
                f"Freqtrade does not support {mm_value} {trading_mode.value} on {self.name}"
711
            )
712

713
    def get_option(self, param: str, default: Optional[Any] = None) -> Any:
1✔
714
        """
715
        Get parameter value from _ft_has
716
        """
717
        return self._ft_has.get(param, default)
1✔
718

719
    def exchange_has(self, endpoint: str) -> bool:
1✔
720
        """
721
        Checks if exchange implements a specific API endpoint.
722
        Wrapper around ccxt 'has' attribute
723
        :param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
724
        :return: bool
725
        """
726
        if endpoint in self._ft_has.get('exchange_has_overrides', {}):
1✔
727
            return self._ft_has['exchange_has_overrides'][endpoint]
1✔
728
        return endpoint in self._api.has and self._api.has[endpoint]
1✔
729

730
    def get_precision_amount(self, pair: str) -> Optional[float]:
1✔
731
        """
732
        Returns the amount precision of the exchange.
733
        :param pair: Pair to get precision for
734
        :return: precision for amount or None. Must be used in combination with precisionMode
735
        """
736
        return self.markets.get(pair, {}).get('precision', {}).get('amount', None)
1✔
737

738
    def get_precision_price(self, pair: str) -> Optional[float]:
1✔
739
        """
740
        Returns the price precision of the exchange.
741
        :param pair: Pair to get precision for
742
        :return: precision for price or None. Must be used in combination with precisionMode
743
        """
744
        return self.markets.get(pair, {}).get('precision', {}).get('price', None)
1✔
745

746
    def amount_to_precision(self, pair: str, amount: float) -> float:
1✔
747
        """
748
        Returns the amount to buy or sell to a precision the Exchange accepts
749

750
        """
751
        return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
1✔
752

753
    def price_to_precision(self, pair: str, price: float, *, rounding_mode: int = ROUND) -> float:
1✔
754
        """
755
        Returns the price rounded to the precision the Exchange accepts.
756
        The default price_rounding_mode in conf is ROUND.
757
        For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
758
        """
759
        return price_to_precision(price, self.get_precision_price(pair),
1✔
760
                                  self.precisionMode, rounding_mode=rounding_mode)
761

762
    def price_get_one_pip(self, pair: str, price: float) -> float:
1✔
763
        """
764
        Gets the "1 pip" value for this pair.
765
        Used in PriceFilter to calculate the 1pip movements.
766
        """
767
        precision = self.markets[pair]['precision']['price']
1✔
768
        if self.precisionMode == TICK_SIZE:
1✔
769
            return precision
1✔
770
        else:
771
            return 1 / pow(10, precision)
1✔
772

773
    def get_min_pair_stake_amount(
1✔
774
        self,
775
        pair: str,
776
        price: float,
777
        stoploss: float,
778
        leverage: Optional[float] = 1.0
779
    ) -> Optional[float]:
780
        return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage)
1✔
781

782
    def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
1✔
783
        max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max', leverage)
1✔
784
        if max_stake_amount is None:
1✔
785
            # * Should never be executed
786
            raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
×
787
                                       'never set max_stake_amount to None')
788
        return max_stake_amount
1✔
789

790
    def _get_stake_amount_limit(
1✔
791
        self,
792
        pair: str,
793
        price: float,
794
        stoploss: float,
795
        limit: Literal['min', 'max'],
796
        leverage: Optional[float] = 1.0
797
    ) -> Optional[float]:
798

799
        isMin = limit == 'min'
1✔
800

801
        try:
1✔
802
            market = self.markets[pair]
1✔
803
        except KeyError:
1✔
804
            raise ValueError(f"Can't get market information for symbol {pair}")
1✔
805

806
        if isMin:
1✔
807
            # reserve some percent defined in config (5% default) + stoploss
808
            margin_reserve: float = 1.0 + self._config.get('amount_reserve_percent',
1✔
809
                                                           DEFAULT_AMOUNT_RESERVE_PERCENT)
810
            stoploss_reserve = (
1✔
811
                margin_reserve / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
812
            )
813
            # it should not be more than 50%
814
            stoploss_reserve = max(min(stoploss_reserve, 1.5), 1)
1✔
815
        else:
816
            margin_reserve = 1.0
1✔
817
            stoploss_reserve = 1.0
1✔
818

819
        stake_limits = []
1✔
820
        limits = market['limits']
1✔
821
        if (limits['cost'][limit] is not None):
1✔
822
            stake_limits.append(
1✔
823
                self._contracts_to_amount(pair, limits['cost'][limit]) * stoploss_reserve
824
            )
825

826
        if (limits['amount'][limit] is not None):
1✔
827
            stake_limits.append(
1✔
828
                self._contracts_to_amount(pair, limits['amount'][limit]) * price * margin_reserve
829
            )
830

831
        if not stake_limits:
1✔
832
            return None if isMin else float('inf')
1✔
833

834
        # The value returned should satisfy both limits: for amount (base currency) and
835
        # for cost (quote, stake currency), so max() is used here.
836
        # See also #2575 at github.
837
        return self._get_stake_amount_considering_leverage(
1✔
838
            max(stake_limits) if isMin else min(stake_limits),
839
            leverage or 1.0
840
        )
841

842
    def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
1✔
843
        """
844
        Takes the minimum stake amount for a pair with no leverage and returns the minimum
845
        stake amount when leverage is considered
846
        :param stake_amount: The stake amount for a pair before leverage is considered
847
        :param leverage: The amount of leverage being used on the current trade
848
        """
849
        return stake_amount / leverage
1✔
850

851
    # Dry-run methods
852

853
    def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
1✔
854
                             rate: float, leverage: float, params: Optional[Dict] = None,
855
                             stop_loss: bool = False) -> Dict[str, Any]:
856
        now = dt_now()
1✔
857
        order_id = f'dry_run_{side}_{pair}_{now.timestamp()}'
1✔
858
        # Rounding here must respect to contract sizes
859
        _amount = self._contracts_to_amount(
1✔
860
            pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)))
861
        dry_order: Dict[str, Any] = {
1✔
862
            'id': order_id,
863
            'symbol': pair,
864
            'price': rate,
865
            'average': rate,
866
            'amount': _amount,
867
            'cost': _amount * rate,
868
            'type': ordertype,
869
            'side': side,
870
            'filled': 0,
871
            'remaining': _amount,
872
            'datetime': now.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
873
            'timestamp': dt_ts(now),
874
            'status': "open",
875
            'fee': None,
876
            'info': {},
877
            'leverage': leverage
878
        }
879
        if stop_loss:
1✔
880
            dry_order["info"] = {"stopPrice": dry_order["price"]}
1✔
881
            dry_order[self._ft_has['stop_price_prop']] = dry_order["price"]
1✔
882
            # Workaround to avoid filling stoploss orders immediately
883
            dry_order["ft_order_type"] = "stoploss"
1✔
884
        orderbook: Optional[OrderBook] = None
1✔
885
        if self.exchange_has('fetchL2OrderBook'):
1✔
886
            orderbook = self.fetch_l2_order_book(pair, 20)
1✔
887
        if ordertype == "limit" and orderbook:
1✔
888
            # Allow a 1% price difference
889
            allowed_diff = 0.01
1✔
890
            if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
1✔
891
                logger.info(
1✔
892
                    f"Converted order {pair} to market order due to price {rate} crossing spread "
893
                    f"by more than {allowed_diff:.2%}.")
894
                dry_order["type"] = "market"
1✔
895

896
        if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
1✔
897
            # Update market order pricing
898
            average = self.get_dry_market_fill_price(pair, side, amount, rate, orderbook)
1✔
899
            dry_order.update({
1✔
900
                'average': average,
901
                'filled': _amount,
902
                'remaining': 0.0,
903
                'status': "closed",
904
                'cost': (dry_order['amount'] * average)
905
            })
906
            # market orders will always incurr taker fees
907
            dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
1✔
908

909
        dry_order = self.check_dry_limit_order_filled(
1✔
910
            dry_order, immediate=True, orderbook=orderbook)
911

912
        self._dry_run_open_orders[dry_order["id"]] = dry_order
1✔
913
        # Copy order and close it - so the returned order is open unless it's a market order
914
        return dry_order
1✔
915

916
    def add_dry_order_fee(
1✔
917
        self,
918
        pair: str,
919
        dry_order: Dict[str, Any],
920
        taker_or_maker: MakerTaker,
921
    ) -> Dict[str, Any]:
922
        fee = self.get_fee(pair, taker_or_maker=taker_or_maker)
1✔
923
        dry_order.update({
1✔
924
            'fee': {
925
                'currency': self.get_pair_quote_currency(pair),
926
                'cost': dry_order['cost'] * fee,
927
                'rate': fee
928
            }
929
        })
930
        return dry_order
1✔
931

932
    def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float,
1✔
933
                                  orderbook: Optional[OrderBook]) -> float:
934
        """
935
        Get the market order fill price based on orderbook interpolation
936
        """
937
        if self.exchange_has('fetchL2OrderBook'):
1✔
938
            if not orderbook:
1✔
939
                orderbook = self.fetch_l2_order_book(pair, 20)
×
940
            ob_type: OBLiteral = 'asks' if side == 'buy' else 'bids'
1✔
941
            slippage = 0.05
1✔
942
            max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
1✔
943

944
            remaining_amount = amount
1✔
945
            filled_value = 0.0
1✔
946
            book_entry_price = 0.0
1✔
947
            for book_entry in orderbook[ob_type]:
1✔
948
                book_entry_price = book_entry[0]
1✔
949
                book_entry_coin_volume = book_entry[1]
1✔
950
                if remaining_amount > 0:
1✔
951
                    if remaining_amount < book_entry_coin_volume:
1✔
952
                        # Orderbook at this slot bigger than remaining amount
953
                        filled_value += remaining_amount * book_entry_price
1✔
954
                        break
1✔
955
                    else:
956
                        filled_value += book_entry_coin_volume * book_entry_price
1✔
957
                    remaining_amount -= book_entry_coin_volume
1✔
958
                else:
959
                    break
×
960
            else:
961
                # If remaining_amount wasn't consumed completely (break was not called)
962
                filled_value += remaining_amount * book_entry_price
1✔
963
            forecast_avg_filled_price = max(filled_value, 0) / amount
1✔
964
            # Limit max. slippage to specified value
965
            if side == 'buy':
1✔
966
                forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
1✔
967

968
            else:
969
                forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
1✔
970

971
            return self.price_to_precision(pair, forecast_avg_filled_price)
1✔
972

973
        return rate
1✔
974

975
    def _dry_is_price_crossed(self, pair: str, side: str, limit: float,
1✔
976
                              orderbook: Optional[OrderBook] = None, offset: float = 0.0) -> bool:
977
        if not self.exchange_has('fetchL2OrderBook'):
1✔
978
            return True
1✔
979
        if not orderbook:
1✔
980
            orderbook = self.fetch_l2_order_book(pair, 1)
1✔
981
        try:
1✔
982
            if side == 'buy':
1✔
983
                price = orderbook['asks'][0][0]
1✔
984
                if limit * (1 - offset) >= price:
1✔
985
                    return True
1✔
986
            else:
987
                price = orderbook['bids'][0][0]
1✔
988
                if limit * (1 + offset) <= price:
1✔
989
                    return True
1✔
990
        except IndexError:
1✔
991
            # Ignore empty orderbooks when filling - can be filled with the next iteration.
992
            pass
1✔
993
        return False
1✔
994

995
    def check_dry_limit_order_filled(
1✔
996
            self, order: Dict[str, Any], immediate: bool = False,
997
            orderbook: Optional[OrderBook] = None) -> Dict[str, Any]:
998
        """
999
        Check dry-run limit order fill and update fee (if it filled).
1000
        """
1001
        if (order['status'] != "closed"
1✔
1002
                and order['type'] in ["limit"]
1003
                and not order.get('ft_order_type')):
1004
            pair = order['symbol']
1✔
1005
            if self._dry_is_price_crossed(pair, order['side'], order['price'], orderbook):
1✔
1006
                order.update({
1✔
1007
                    'status': 'closed',
1008
                    'filled': order['amount'],
1009
                    'remaining': 0,
1010
                })
1011

1012
                self.add_dry_order_fee(
1✔
1013
                    pair,
1014
                    order,
1015
                    'taker' if immediate else 'maker',
1016
                )
1017

1018
        return order
1✔
1019

1020
    def fetch_dry_run_order(self, order_id) -> Dict[str, Any]:
1✔
1021
        """
1022
        Return dry-run order
1023
        Only call if running in dry-run mode.
1024
        """
1025
        try:
1✔
1026
            order = self._dry_run_open_orders[order_id]
1✔
1027
            order = self.check_dry_limit_order_filled(order)
1✔
1028
            return order
1✔
1029
        except KeyError as e:
1✔
1030
            from freqtrade.persistence import Order
1✔
1031
            order = Order.order_by_id(order_id)
1✔
1032
            if order:
1✔
1033
                ccxt_order = order.to_ccxt_object(self._ft_has['stop_price_prop'])
1✔
1034
                self._dry_run_open_orders[order_id] = ccxt_order
1✔
1035
                return ccxt_order
1✔
1036
            # Gracefully handle errors with dry-run orders.
1037
            raise InvalidOrderException(
1✔
1038
                f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
1039

1040
    # Order handling
1041

1042
    def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
1✔
1043
        if self.trading_mode != TradingMode.SPOT:
1✔
1044
            self.set_margin_mode(pair, self.margin_mode, accept_fail)
1✔
1045
            self._set_leverage(leverage, pair, accept_fail)
1✔
1046

1047
    def _get_params(
1✔
1048
        self,
1049
        side: BuySell,
1050
        ordertype: str,
1051
        leverage: float,
1052
        reduceOnly: bool,
1053
        time_in_force: str = 'GTC',
1054
    ) -> Dict:
1055
        params = self._params.copy()
1✔
1056
        if time_in_force != 'GTC' and ordertype != 'market':
1✔
1057
            params.update({'timeInForce': time_in_force.upper()})
1✔
1058
        if reduceOnly:
1✔
1059
            params.update({'reduceOnly': True})
1✔
1060
        return params
1✔
1061

1062
    def _order_needs_price(self, ordertype: str) -> bool:
1✔
1063
        return (
1✔
1064
            ordertype != 'market'
1065
            or self._api.options.get("createMarketBuyOrderRequiresPrice", False)
1066
            or self._ft_has.get('marketOrderRequiresPrice', False)
1067
        )
1068

1069
    def create_order(
1✔
1070
        self,
1071
        *,
1072
        pair: str,
1073
        ordertype: str,
1074
        side: BuySell,
1075
        amount: float,
1076
        rate: float,
1077
        leverage: float,
1078
        reduceOnly: bool = False,
1079
        time_in_force: str = 'GTC',
1080
    ) -> Dict:
1081
        if self._config['dry_run']:
1✔
1082
            dry_order = self.create_dry_run_order(
1✔
1083
                pair, ordertype, side, amount, self.price_to_precision(pair, rate), leverage)
1084
            return dry_order
1✔
1085

1086
        params = self._get_params(side, ordertype, leverage, reduceOnly, time_in_force)
1✔
1087

1088
        try:
1✔
1089
            # Set the precision for amount and price(rate) as accepted by the exchange
1090
            amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
1✔
1091
            needs_price = self._order_needs_price(ordertype)
1✔
1092
            rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
1✔
1093

1094
            if not reduceOnly:
1✔
1095
                self._lev_prep(pair, leverage, side)
1✔
1096

1097
            order = self._api.create_order(
1✔
1098
                pair,
1099
                ordertype,
1100
                side,
1101
                amount,
1102
                rate_for_order,
1103
                params,
1104
            )
1105
            if order.get('status') is None:
1✔
1106
                # Map empty status to open.
1107
                order['status'] = 'open'
1✔
1108

1109
            if order.get('type') is None:
1✔
1110
                order['type'] = ordertype
1✔
1111

1112
            self._log_exchange_response('create_order', order)
1✔
1113
            order = self._order_contracts_to_amount(order)
1✔
1114
            return order
1✔
1115

1116
        except ccxt.InsufficientFunds as e:
1✔
1117
            raise InsufficientFundsError(
1✔
1118
                f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
1119
                f'Tried to {side} amount {amount} at rate {rate}.'
1120
                f'Message: {e}') from e
1121
        except ccxt.InvalidOrder as e:
1✔
1122
            raise InvalidOrderException(
1✔
1123
                f'Could not create {ordertype} {side} order on market {pair}. '
1124
                f'Tried to {side} amount {amount} at rate {rate}. '
1125
                f'Message: {e}') from e
1126
        except ccxt.DDoSProtection as e:
1✔
1127
            raise DDosProtection(e) from e
×
1128
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1129
            raise TemporaryError(
1✔
1130
                f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
1131
        except ccxt.BaseError as e:
1✔
1132
            raise OperationalException(e) from e
1✔
1133

1134
    def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
1✔
1135
        """
1136
        Verify stop_loss against stoploss-order value (limit or price)
1137
        Returns True if adjustment is necessary.
1138
        """
1139
        if not self._ft_has.get('stoploss_on_exchange'):
1✔
1140
            raise OperationalException(f"stoploss is not implemented for {self.name}.")
1✔
1141
        price_param = self._ft_has['stop_price_prop']
1✔
1142
        return (
1✔
1143
            order.get(price_param, None) is None
1144
            or ((side == "sell" and stop_loss > float(order[price_param])) or
1145
                (side == "buy" and stop_loss < float(order[price_param])))
1146
        )
1147

1148
    def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
1✔
1149

1150
        available_order_Types: Dict[str, str] = self._ft_has["stoploss_order_types"]
1✔
1151

1152
        if user_order_type in available_order_Types.keys():
1✔
1153
            ordertype = available_order_Types[user_order_type]
1✔
1154
        else:
1155
            # Otherwise pick only one available
1156
            ordertype = list(available_order_Types.values())[0]
1✔
1157
            user_order_type = list(available_order_Types.keys())[0]
1✔
1158
        return ordertype, user_order_type
1✔
1159

1160
    def _get_stop_limit_rate(self, stop_price: float, order_types: Dict, side: str) -> float:
1✔
1161
        # Limit price threshold: As limit price should always be below stop-price
1162
        limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
1✔
1163
        if side == "sell":
1✔
1164
            limit_rate = stop_price * limit_price_pct
1✔
1165
        else:
1166
            limit_rate = stop_price * (2 - limit_price_pct)
1✔
1167

1168
        bad_stop_price = ((stop_price < limit_rate) if side ==
1✔
1169
                          "sell" else (stop_price > limit_rate))
1170
        # Ensure rate is less than stop price
1171
        if bad_stop_price:
1✔
1172
            # This can for example happen if the stop / liquidation price is set to 0
1173
            # Which is possible if a market-order closes right away.
1174
            # The InvalidOrderException will bubble up to exit_positions, where it will be
1175
            # handled gracefully.
1176
            raise InvalidOrderException(
1✔
1177
                "In stoploss limit order, stop price should be more than limit price. "
1178
                f"Stop price: {stop_price}, Limit price: {limit_rate}, "
1179
                f"Limit Price pct: {limit_price_pct}"
1180
                )
1181
        return limit_rate
1✔
1182

1183
    def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
1✔
1184
        params = self._params.copy()
1✔
1185
        # Verify if stopPrice works for your exchange, else configure stop_price_param
1186
        params.update({self._ft_has['stop_price_param']: stop_price})
1✔
1187
        return params
1✔
1188

1189
    @retrier(retries=0)
1✔
1190
    def create_stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict,
1✔
1191
                        side: BuySell, leverage: float) -> Dict:
1192
        """
1193
        creates a stoploss order.
1194
        requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
1195
            to the corresponding exchange type.
1196

1197
        The precise ordertype is determined by the order_types dict or exchange default.
1198

1199
        The exception below should never raise, since we disallow
1200
        starting the bot in validate_ordertypes()
1201

1202
        This may work with a limited number of other exchanges, but correct working
1203
            needs to be tested individually.
1204
        WARNING: setting `stoploss_on_exchange` to True will NOT auto-enable stoploss on exchange.
1205
            `stoploss_adjust` must still be implemented for this to work.
1206
        """
1207
        if not self._ft_has['stoploss_on_exchange']:
1✔
1208
            raise OperationalException(f"stoploss is not implemented for {self.name}.")
1✔
1209

1210
        user_order_type = order_types.get('stoploss', 'market')
1✔
1211
        ordertype, user_order_type = self._get_stop_order_type(user_order_type)
1✔
1212
        round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
1✔
1213
        stop_price_norm = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
1✔
1214
        limit_rate = None
1✔
1215
        if user_order_type == 'limit':
1✔
1216
            limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
1✔
1217
            limit_rate = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
1✔
1218

1219
        if self._config['dry_run']:
1✔
1220
            dry_order = self.create_dry_run_order(
1✔
1221
                pair,
1222
                ordertype,
1223
                side,
1224
                amount,
1225
                stop_price_norm,
1226
                stop_loss=True,
1227
                leverage=leverage,
1228
            )
1229
            return dry_order
1✔
1230

1231
        try:
1✔
1232
            params = self._get_stop_params(side=side, ordertype=ordertype,
1✔
1233
                                           stop_price=stop_price_norm)
1234
            if self.trading_mode == TradingMode.FUTURES:
1✔
1235
                params['reduceOnly'] = True
1✔
1236
                if 'stoploss_price_type' in order_types and 'stop_price_type_field' in self._ft_has:
1✔
1237
                    price_type = self._ft_has['stop_price_type_value_mapping'][
1✔
1238
                        order_types.get('stoploss_price_type', PriceType.LAST)]
1239
                    params[self._ft_has['stop_price_type_field']] = price_type
1✔
1240

1241
            amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
1✔
1242

1243
            self._lev_prep(pair, leverage, side, accept_fail=True)
1✔
1244
            order = self._api.create_order(symbol=pair, type=ordertype, side=side,
1✔
1245
                                           amount=amount, price=limit_rate, params=params)
1246
            self._log_exchange_response('create_stoploss_order', order)
1✔
1247
            order = self._order_contracts_to_amount(order)
1✔
1248
            logger.info(f"stoploss {user_order_type} order added for {pair}. "
1✔
1249
                        f"stop price: {stop_price}. limit: {limit_rate}")
1250
            return order
1✔
1251
        except ccxt.InsufficientFunds as e:
1✔
1252
            raise InsufficientFundsError(
1✔
1253
                f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
1254
                f'Tried to {side} amount {amount} at rate {limit_rate} with '
1255
                f'stop-price {stop_price_norm}. Message: {e}') from e
1256
        except (ccxt.InvalidOrder, ccxt.BadRequest, ccxt.OperationRejected) as e:
1✔
1257
            # Errors:
1258
            # `Order would trigger immediately.`
1259
            raise InvalidOrderException(
1✔
1260
                f'Could not create {ordertype} {side} order on market {pair}. '
1261
                f'Tried to {side} amount {amount} at rate {limit_rate} with '
1262
                f'stop-price {stop_price_norm}. Message: {e}') from e
1263
        except ccxt.DDoSProtection as e:
1✔
1264
            raise DDosProtection(e) from e
1✔
1265
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1266
            raise TemporaryError(
1✔
1267
                f"Could not place stoploss order due to {e.__class__.__name__}. "
1268
                f"Message: {e}") from e
1269
        except ccxt.BaseError as e:
1✔
1270
            raise OperationalException(e) from e
1✔
1271

1272
    def fetch_order_emulated(self, order_id: str, pair: str, params: Dict) -> Dict:
1✔
1273
        """
1274
        Emulated fetch_order if the exchange doesn't support fetch_order, but requires separate
1275
        calls for open and closed orders.
1276
        """
1277
        try:
1✔
1278
            order = self._api.fetch_open_order(order_id, pair, params=params)
1✔
1279
            self._log_exchange_response('fetch_open_order', order)
1✔
1280
            order = self._order_contracts_to_amount(order)
1✔
1281
            return order
1✔
1282
        except ccxt.OrderNotFound:
1✔
1283
            try:
1✔
1284
                order = self._api.fetch_closed_order(order_id, pair, params=params)
1✔
1285
                self._log_exchange_response('fetch_closed_order', order)
1✔
1286
                order = self._order_contracts_to_amount(order)
1✔
1287
                return order
1✔
1288
            except ccxt.OrderNotFound as e:
×
1289
                raise RetryableOrderError(
×
1290
                    f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
1291
        except ccxt.InvalidOrder as e:
1✔
1292
            raise InvalidOrderException(
1✔
1293
                f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e
1294
        except ccxt.DDoSProtection as e:
1✔
1295
            raise DDosProtection(e) from e
1✔
1296
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1297
            raise TemporaryError(
1✔
1298
                f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
1299
        except ccxt.BaseError as e:
1✔
1300
            raise OperationalException(e) from e
1✔
1301

1302
    @retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
1✔
1303
    def fetch_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
1✔
1304
        if self._config['dry_run']:
1✔
1305
            return self.fetch_dry_run_order(order_id)
1✔
1306
        if params is None:
1✔
1307
            params = {}
1✔
1308
        try:
1✔
1309
            if not self.exchange_has('fetchOrder'):
1✔
1310
                return self.fetch_order_emulated(order_id, pair, params)
1✔
1311
            order = self._api.fetch_order(order_id, pair, params=params)
1✔
1312
            self._log_exchange_response('fetch_order', order)
1✔
1313
            order = self._order_contracts_to_amount(order)
1✔
1314
            return order
1✔
1315
        except ccxt.OrderNotFound as e:
1✔
1316
            raise RetryableOrderError(
1✔
1317
                f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
1318
        except ccxt.InvalidOrder as e:
1✔
1319
            raise InvalidOrderException(
1✔
1320
                f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e
1321
        except ccxt.DDoSProtection as e:
1✔
1322
            raise DDosProtection(e) from e
1✔
1323
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1324
            raise TemporaryError(
1✔
1325
                f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
1326
        except ccxt.BaseError as e:
1✔
1327
            raise OperationalException(e) from e
1✔
1328

1329
    def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
1✔
1330
        return self.fetch_order(order_id, pair, params)
1✔
1331

1332
    def fetch_order_or_stoploss_order(self, order_id: str, pair: str,
1✔
1333
                                      stoploss_order: bool = False) -> Dict:
1334
        """
1335
        Simple wrapper calling either fetch_order or fetch_stoploss_order depending on
1336
        the stoploss_order parameter
1337
        :param order_id: OrderId to fetch order
1338
        :param pair: Pair corresponding to order_id
1339
        :param stoploss_order: If true, uses fetch_stoploss_order, otherwise fetch_order.
1340
        """
1341
        if stoploss_order:
1✔
1342
            return self.fetch_stoploss_order(order_id, pair)
1✔
1343
        return self.fetch_order(order_id, pair)
1✔
1344

1345
    def check_order_canceled_empty(self, order: Dict) -> bool:
1✔
1346
        """
1347
        Verify if an order has been cancelled without being partially filled
1348
        :param order: Order dict as returned from fetch_order()
1349
        :return: True if order has been cancelled without being filled, False otherwise.
1350
        """
1351
        return (order.get('status') in NON_OPEN_EXCHANGE_STATES
1✔
1352
                and order.get('filled') == 0.0)
1353

1354
    @retrier
1✔
1355
    def cancel_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
1✔
1356
        if self._config['dry_run']:
1✔
1357
            try:
1✔
1358
                order = self.fetch_dry_run_order(order_id)
1✔
1359

1360
                order.update({'status': 'canceled', 'filled': 0.0, 'remaining': order['amount']})
1✔
1361
                return order
1✔
1362
            except InvalidOrderException:
1✔
1363
                return {}
1✔
1364

1365
        if params is None:
1✔
1366
            params = {}
1✔
1367
        try:
1✔
1368
            order = self._api.cancel_order(order_id, pair, params=params)
1✔
1369
            self._log_exchange_response('cancel_order', order)
1✔
1370
            order = self._order_contracts_to_amount(order)
1✔
1371
            return order
1✔
1372
        except ccxt.InvalidOrder as e:
1✔
1373
            raise InvalidOrderException(
1✔
1374
                f'Could not cancel order. Message: {e}') from e
1375
        except ccxt.DDoSProtection as e:
1✔
1376
            raise DDosProtection(e) from e
1✔
1377
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1378
            raise TemporaryError(
1✔
1379
                f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
1380
        except ccxt.BaseError as e:
1✔
1381
            raise OperationalException(e) from e
1✔
1382

1383
    def cancel_stoploss_order(
1✔
1384
            self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
1385
        return self.cancel_order(order_id, pair, params)
1✔
1386

1387
    def is_cancel_order_result_suitable(self, corder) -> bool:
1✔
1388
        if not isinstance(corder, dict):
1✔
1389
            return False
1✔
1390

1391
        required = ('fee', 'status', 'amount')
1✔
1392
        return all(corder.get(k, None) is not None for k in required)
1✔
1393

1394
    def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
1✔
1395
        """
1396
        Cancel order returning a result.
1397
        Creates a fake result if cancel order returns a non-usable result
1398
        and fetch_order does not work (certain exchanges don't return cancelled orders)
1399
        :param order_id: Orderid to cancel
1400
        :param pair: Pair corresponding to order_id
1401
        :param amount: Amount to use for fake response
1402
        :return: Result from either cancel_order if usable, or fetch_order
1403
        """
1404
        try:
1✔
1405
            corder = self.cancel_order(order_id, pair)
1✔
1406
            if self.is_cancel_order_result_suitable(corder):
1✔
1407
                return corder
1✔
1408
        except InvalidOrderException:
1✔
1409
            logger.warning(f"Could not cancel order {order_id} for {pair}.")
1✔
1410
        try:
1✔
1411
            order = self.fetch_order(order_id, pair)
1✔
1412
        except InvalidOrderException:
1✔
1413
            logger.warning(f"Could not fetch cancelled order {order_id}.")
1✔
1414
            order = {
1✔
1415
                'id': order_id,
1416
                'status': 'canceled',
1417
                'amount': amount,
1418
                'filled': 0.0,
1419
                'fee': {},
1420
                'info': {}
1421
            }
1422

1423
        return order
1✔
1424

1425
    def cancel_stoploss_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
1✔
1426
        """
1427
        Cancel stoploss order returning a result.
1428
        Creates a fake result if cancel order returns a non-usable result
1429
        and fetch_order does not work (certain exchanges don't return cancelled orders)
1430
        :param order_id: stoploss-order-id to cancel
1431
        :param pair: Pair corresponding to order_id
1432
        :param amount: Amount to use for fake response
1433
        :return: Result from either cancel_order if usable, or fetch_order
1434
        """
1435
        corder = self.cancel_stoploss_order(order_id, pair)
1✔
1436
        if self.is_cancel_order_result_suitable(corder):
1✔
1437
            return corder
1✔
1438
        try:
1✔
1439
            order = self.fetch_stoploss_order(order_id, pair)
1✔
1440
        except InvalidOrderException:
1✔
1441
            logger.warning(f"Could not fetch cancelled stoploss order {order_id}.")
1✔
1442
            order = {'id': order_id, 'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
1✔
1443

1444
        return order
1✔
1445

1446
    @retrier
1✔
1447
    def get_balances(self) -> dict:
1✔
1448

1449
        try:
1✔
1450
            balances = self._api.fetch_balance()
1✔
1451
            # Remove additional info from ccxt results
1452
            balances.pop("info", None)
1✔
1453
            balances.pop("free", None)
1✔
1454
            balances.pop("total", None)
1✔
1455
            balances.pop("used", None)
1✔
1456

1457
            return balances
1✔
1458
        except ccxt.DDoSProtection as e:
1✔
1459
            raise DDosProtection(e) from e
1✔
1460
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1461
            raise TemporaryError(
1✔
1462
                f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
1463
        except ccxt.BaseError as e:
1✔
1464
            raise OperationalException(e) from e
1✔
1465

1466
    @retrier
1✔
1467
    def fetch_positions(self, pair: Optional[str] = None) -> List[Dict]:
1✔
1468
        """
1469
        Fetch positions from the exchange.
1470
        If no pair is given, all positions are returned.
1471
        :param pair: Pair for the query
1472
        """
1473
        if self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES:
1✔
1474
            return []
1✔
1475
        try:
1✔
1476
            symbols = []
1✔
1477
            if pair:
1✔
1478
                symbols.append(pair)
1✔
1479
            positions: List[Dict] = self._api.fetch_positions(symbols)
1✔
1480
            self._log_exchange_response('fetch_positions', positions)
1✔
1481
            return positions
1✔
1482
        except ccxt.DDoSProtection as e:
1✔
1483
            raise DDosProtection(e) from e
1✔
1484
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1485
            raise TemporaryError(
1✔
1486
                f'Could not get positions due to {e.__class__.__name__}. Message: {e}') from e
1487
        except ccxt.BaseError as e:
1✔
1488
            raise OperationalException(e) from e
1✔
1489

1490
    def _fetch_orders_emulate(self, pair: str, since_ms: int) -> List[Dict]:
1✔
1491
        orders = []
1✔
1492
        if self.exchange_has('fetchClosedOrders'):
1✔
1493
            orders = self._api.fetch_closed_orders(pair, since=since_ms)
1✔
1494
            if self.exchange_has('fetchOpenOrders'):
1✔
1495
                orders_open = self._api.fetch_open_orders(pair, since=since_ms)
1✔
1496
                orders.extend(orders_open)
1✔
1497
        return orders
1✔
1498

1499
    @retrier(retries=0)
1✔
1500
    def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None) -> List[Dict]:
1✔
1501
        """
1502
        Fetch all orders for a pair "since"
1503
        :param pair: Pair for the query
1504
        :param since: Starting time for the query
1505
        """
1506
        if self._config['dry_run']:
1✔
1507
            return []
1✔
1508

1509
        try:
1✔
1510
            since_ms = int((since.timestamp() - 10) * 1000)
1✔
1511

1512
            if self.exchange_has('fetchOrders'):
1✔
1513
                if not params:
1✔
1514
                    params = {}
1✔
1515
                try:
1✔
1516
                    orders: List[Dict] = self._api.fetch_orders(pair, since=since_ms, params=params)
1✔
1517
                except ccxt.NotSupported:
1✔
1518
                    # Some exchanges don't support fetchOrders
1519
                    # attempt to fetch open and closed orders separately
1520
                    orders = self._fetch_orders_emulate(pair, since_ms)
1✔
1521
            else:
1522
                orders = self._fetch_orders_emulate(pair, since_ms)
1✔
1523
            self._log_exchange_response('fetch_orders', orders)
1✔
1524
            orders = [self._order_contracts_to_amount(o) for o in orders]
1✔
1525
            return orders
1✔
1526
        except ccxt.DDoSProtection as e:
1✔
1527
            raise DDosProtection(e) from e
1✔
1528
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1529
            raise TemporaryError(
1✔
1530
                f'Could not fetch positions due to {e.__class__.__name__}. Message: {e}') from e
1531
        except ccxt.BaseError as e:
1✔
1532
            raise OperationalException(e) from e
1✔
1533

1534
    @retrier
1✔
1535
    def fetch_trading_fees(self) -> Dict[str, Any]:
1✔
1536
        """
1537
        Fetch user account trading fees
1538
        Can be cached, should not update often.
1539
        """
1540
        if (self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES
1✔
1541
                or not self.exchange_has('fetchTradingFees')):
1542
            return {}
1✔
1543
        try:
1✔
1544
            trading_fees: Dict[str, Any] = self._api.fetch_trading_fees()
1✔
1545
            self._log_exchange_response('fetch_trading_fees', trading_fees)
1✔
1546
            return trading_fees
1✔
1547
        except ccxt.DDoSProtection as e:
1✔
1548
            raise DDosProtection(e) from e
1✔
1549
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1550
            raise TemporaryError(
1✔
1551
                f'Could not fetch trading fees due to {e.__class__.__name__}. Message: {e}') from e
1552
        except ccxt.BaseError as e:
1✔
1553
            raise OperationalException(e) from e
1✔
1554

1555
    @retrier
1✔
1556
    def fetch_bids_asks(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
1✔
1557
        """
1558
        :param symbols: List of symbols to fetch
1559
        :param cached: Allow cached result
1560
        :return: fetch_bids_asks result
1561
        """
1562
        if not self.exchange_has('fetchBidsAsks'):
1✔
1563
            return {}
1✔
1564
        if cached:
1✔
1565
            with self._cache_lock:
1✔
1566
                tickers = self._fetch_tickers_cache.get('fetch_bids_asks')
1✔
1567
            if tickers:
1✔
1568
                return tickers
1✔
1569
        try:
1✔
1570
            tickers = self._api.fetch_bids_asks(symbols)
1✔
1571
            with self._cache_lock:
1✔
1572
                self._fetch_tickers_cache['fetch_bids_asks'] = tickers
1✔
1573
            return tickers
1✔
1574
        except ccxt.NotSupported as e:
1✔
1575
            raise OperationalException(
1✔
1576
                f'Exchange {self._api.name} does not support fetching bids/asks in batch. '
1577
                f'Message: {e}') from e
1578
        except ccxt.DDoSProtection as e:
1✔
1579
            raise DDosProtection(e) from e
1✔
1580
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1581
            raise TemporaryError(
1✔
1582
                f'Could not load bids/asks due to {e.__class__.__name__}. Message: {e}') from e
1583
        except ccxt.BaseError as e:
1✔
1584
            raise OperationalException(e) from e
1✔
1585

1586
    @retrier
1✔
1587
    def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
1✔
1588
        """
1589
        :param cached: Allow cached result
1590
        :return: fetch_tickers result
1591
        """
1592
        tickers: Tickers
1593
        if not self.exchange_has('fetchTickers'):
1✔
1594
            return {}
1✔
1595
        if cached:
1✔
1596
            with self._cache_lock:
1✔
1597
                tickers = self._fetch_tickers_cache.get('fetch_tickers')  # type: ignore
1✔
1598
            if tickers:
1✔
1599
                return tickers
1✔
1600
        try:
1✔
1601
            tickers = self._api.fetch_tickers(symbols)
1✔
1602
            with self._cache_lock:
1✔
1603
                self._fetch_tickers_cache['fetch_tickers'] = tickers
1✔
1604
            return tickers
1✔
1605
        except ccxt.NotSupported as e:
1✔
1606
            raise OperationalException(
1✔
1607
                f'Exchange {self._api.name} does not support fetching tickers in batch. '
1608
                f'Message: {e}') from e
1609
        except ccxt.BadSymbol as e:
1✔
1610
            logger.warning(f"Could not load tickers due to {e.__class__.__name__}. Message: {e} ."
1✔
1611
                           "Reloading markets.")
1612
            self.reload_markets(True)
1✔
1613
            # Re-raise exception to repeat the call.
1614
            raise TemporaryError from e
1✔
1615
        except ccxt.DDoSProtection as e:
1✔
1616
            raise DDosProtection(e) from e
1✔
1617
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1618
            raise TemporaryError(
1✔
1619
                f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e
1620
        except ccxt.BaseError as e:
1✔
1621
            raise OperationalException(e) from e
1✔
1622

1623
    # Pricing info
1624

1625
    @retrier
1✔
1626
    def fetch_ticker(self, pair: str) -> Ticker:
1✔
1627
        try:
1✔
1628
            if (pair not in self.markets or
1✔
1629
                    self.markets[pair].get('active', False) is False):
1630
                raise ExchangeError(f"Pair {pair} not available")
1✔
1631
            data: Ticker = self._api.fetch_ticker(pair)
1✔
1632
            return data
1✔
1633
        except ccxt.DDoSProtection as e:
1✔
1634
            raise DDosProtection(e) from e
1✔
1635
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1636
            raise TemporaryError(
1✔
1637
                f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e
1638
        except ccxt.BaseError as e:
1✔
1639
            raise OperationalException(e) from e
1✔
1640

1641
    @staticmethod
1✔
1642
    def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]],
1✔
1643
                               range_required: bool = True):
1644
        """
1645
        Get next greater value in the list.
1646
        Used by fetch_l2_order_book if the api only supports a limited range
1647
        """
1648
        if not limit_range:
1✔
1649
            return limit
1✔
1650

1651
        result = min([x for x in limit_range if limit <= x] + [max(limit_range)])
1✔
1652
        if not range_required and limit > result:
1✔
1653
            # Range is not required - we can use None as parameter.
1654
            return None
1✔
1655
        return result
1✔
1656

1657
    @retrier
1✔
1658
    def fetch_l2_order_book(self, pair: str, limit: int = 100) -> OrderBook:
1✔
1659
        """
1660
        Get L2 order book from exchange.
1661
        Can be limited to a certain amount (if supported).
1662
        Returns a dict in the format
1663
        {'asks': [price, volume], 'bids': [price, volume]}
1664
        """
1665
        limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'],
1✔
1666
                                             self._ft_has['l2_limit_range_required'])
1667
        try:
1✔
1668

1669
            return self._api.fetch_l2_order_book(pair, limit1)
1✔
1670
        except ccxt.NotSupported as e:
1✔
1671
            raise OperationalException(
1✔
1672
                f'Exchange {self._api.name} does not support fetching order book.'
1673
                f'Message: {e}') from e
1674
        except ccxt.DDoSProtection as e:
1✔
1675
            raise DDosProtection(e) from e
×
1676
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1677
            raise TemporaryError(
1✔
1678
                f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e
1679
        except ccxt.BaseError as e:
1✔
1680
            raise OperationalException(e) from e
1✔
1681

1682
    def _get_price_side(self, side: str, is_short: bool, conf_strategy: Dict) -> BidAsk:
1✔
1683
        price_side = conf_strategy['price_side']
1✔
1684

1685
        if price_side in ('same', 'other'):
1✔
1686
            price_map = {
1✔
1687
                ('entry', 'long', 'same'): 'bid',
1688
                ('entry', 'long', 'other'): 'ask',
1689
                ('entry', 'short', 'same'): 'ask',
1690
                ('entry', 'short', 'other'): 'bid',
1691
                ('exit', 'long', 'same'): 'ask',
1692
                ('exit', 'long', 'other'): 'bid',
1693
                ('exit', 'short', 'same'): 'bid',
1694
                ('exit', 'short', 'other'): 'ask',
1695
            }
1696
            price_side = price_map[(side, 'short' if is_short else 'long', price_side)]
1✔
1697
        return price_side
1✔
1698

1699
    def get_rate(self, pair: str, refresh: bool,
1✔
1700
                 side: EntryExit, is_short: bool,
1701
                 order_book: Optional[OrderBook] = None, ticker: Optional[Ticker] = None) -> float:
1702
        """
1703
        Calculates bid/ask target
1704
        bid rate - between current ask price and last price
1705
        ask rate - either using ticker bid or first bid based on orderbook
1706
        or remain static in any other case since it's not updating.
1707
        :param pair: Pair to get rate for
1708
        :param refresh: allow cached data
1709
        :param side: "buy" or "sell"
1710
        :return: float: Price
1711
        :raises PricingError if orderbook price could not be determined.
1712
        """
1713
        name = side.capitalize()
1✔
1714
        strat_name = 'entry_pricing' if side == "entry" else 'exit_pricing'
1✔
1715

1716
        cache_rate: TTLCache = self._entry_rate_cache if side == "entry" else self._exit_rate_cache
1✔
1717
        if not refresh:
1✔
1718
            with self._cache_lock:
1✔
1719
                rate = cache_rate.get(pair)
1✔
1720
            # Check if cache has been invalidated
1721
            if rate:
1✔
1722
                logger.debug(f"Using cached {side} rate for {pair}.")
1✔
1723
                return rate
1✔
1724

1725
        conf_strategy = self._config.get(strat_name, {})
1✔
1726

1727
        price_side = self._get_price_side(side, is_short, conf_strategy)
1✔
1728

1729
        if conf_strategy.get('use_order_book', False):
1✔
1730

1731
            order_book_top = conf_strategy.get('order_book_top', 1)
1✔
1732
            if order_book is None:
1✔
1733
                order_book = self.fetch_l2_order_book(pair, order_book_top)
1✔
1734
            rate = self._get_rate_from_ob(pair, side, order_book, name, price_side,
1✔
1735
                                          order_book_top)
1736
        else:
1737
            logger.debug(f"Using Last {price_side.capitalize()} / Last Price")
1✔
1738
            if ticker is None:
1✔
1739
                ticker = self.fetch_ticker(pair)
1✔
1740
            rate = self._get_rate_from_ticker(side, ticker, conf_strategy, price_side)
1✔
1741

1742
        if rate is None:
1✔
1743
            raise PricingError(f"{name}-Rate for {pair} was empty.")
1✔
1744
        with self._cache_lock:
1✔
1745
            cache_rate[pair] = rate
1✔
1746

1747
        return rate
1✔
1748

1749
    def _get_rate_from_ticker(self, side: EntryExit, ticker: Ticker, conf_strategy: Dict[str, Any],
1✔
1750
                              price_side: BidAsk) -> Optional[float]:
1751
        """
1752
        Get rate from ticker.
1753
        """
1754
        ticker_rate = ticker[price_side]
1✔
1755
        if ticker['last'] and ticker_rate:
1✔
1756
            if side == 'entry' and ticker_rate > ticker['last']:
1✔
1757
                balance = conf_strategy.get('price_last_balance', 0.0)
1✔
1758
                ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
1✔
1759
            elif side == 'exit' and ticker_rate < ticker['last']:
1✔
1760
                balance = conf_strategy.get('price_last_balance', 0.0)
1✔
1761
                ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
1✔
1762
        rate = ticker_rate
1✔
1763
        return rate
1✔
1764

1765
    def _get_rate_from_ob(self, pair: str, side: EntryExit, order_book: OrderBook, name: str,
1✔
1766
                          price_side: BidAsk, order_book_top: int) -> float:
1767
        """
1768
        Get rate from orderbook
1769
        :raises: PricingError if rate could not be determined.
1770
        """
1771
        logger.debug('order_book %s', order_book)
1✔
1772
        # top 1 = index 0
1773
        try:
1✔
1774
            obside: OBLiteral = 'bids' if price_side == 'bid' else 'asks'
1✔
1775
            rate = order_book[obside][order_book_top - 1][0]
1✔
1776
        except (IndexError, KeyError) as e:
1✔
1777
            logger.warning(
1✔
1778
                    f"{pair} - {name} Price at location {order_book_top} from orderbook "
1779
                    f"could not be determined. Orderbook: {order_book}"
1780
                )
1781
            raise PricingError from e
1✔
1782
        logger.debug(f"{pair} - {name} price from orderbook {price_side.capitalize()}"
1✔
1783
                     f"side - top {order_book_top} order book {side} rate {rate:.8f}")
1784
        return rate
1✔
1785

1786
    def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]:
1✔
1787
        entry_rate = None
1✔
1788
        exit_rate = None
1✔
1789
        if not refresh:
1✔
1790
            with self._cache_lock:
1✔
1791
                entry_rate = self._entry_rate_cache.get(pair)
1✔
1792
                exit_rate = self._exit_rate_cache.get(pair)
1✔
1793
            if entry_rate:
1✔
1794
                logger.debug(f"Using cached buy rate for {pair}.")
1✔
1795
            if exit_rate:
1✔
1796
                logger.debug(f"Using cached sell rate for {pair}.")
1✔
1797

1798
        entry_pricing = self._config.get('entry_pricing', {})
1✔
1799
        exit_pricing = self._config.get('exit_pricing', {})
1✔
1800
        order_book = ticker = None
1✔
1801
        if not entry_rate and entry_pricing.get('use_order_book', False):
1✔
1802
            order_book_top = max(entry_pricing.get('order_book_top', 1),
1✔
1803
                                 exit_pricing.get('order_book_top', 1))
1804
            order_book = self.fetch_l2_order_book(pair, order_book_top)
1✔
1805
            entry_rate = self.get_rate(pair, refresh, 'entry', is_short, order_book=order_book)
1✔
1806
        elif not entry_rate:
1✔
1807
            ticker = self.fetch_ticker(pair)
1✔
1808
            entry_rate = self.get_rate(pair, refresh, 'entry', is_short, ticker=ticker)
1✔
1809
        if not exit_rate:
1✔
1810
            exit_rate = self.get_rate(pair, refresh, 'exit',
1✔
1811
                                      is_short, order_book=order_book, ticker=ticker)
1812
        return entry_rate, exit_rate
1✔
1813

1814
    # Fee handling
1815

1816
    @retrier
1✔
1817
    def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
1✔
1818
                             params: Optional[Dict] = None) -> List:
1819
        """
1820
        Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
1821
        The "since" argument passed in is coming from the database and is in UTC,
1822
        as timezone-native datetime object.
1823
        From the python documentation:
1824
            > Naive datetime instances are assumed to represent local time
1825
        Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the
1826
        transformation from local timezone to UTC.
1827
        This works for timezones UTC+ since then the result will contain trades from a few hours
1828
        instead of from the last 5 seconds, however fails for UTC- timezones,
1829
        since we're then asking for trades with a "since" argument in the future.
1830

1831
        :param order_id order_id: Order-id as given when creating the order
1832
        :param pair: Pair the order is for
1833
        :param since: datetime object of the order creation time. Assumes object is in UTC.
1834
        """
1835
        if self._config['dry_run']:
1✔
1836
            return []
1✔
1837
        if not self.exchange_has('fetchMyTrades'):
1✔
1838
            return []
1✔
1839
        try:
1✔
1840
            # Allow 5s offset to catch slight time offsets (discovered in #1185)
1841
            # since needs to be int in milliseconds
1842
            _params = params if params else {}
1✔
1843
            my_trades = self._api.fetch_my_trades(
1✔
1844
                pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000),
1845
                params=_params)
1846
            matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
1✔
1847

1848
            self._log_exchange_response('get_trades_for_order', matched_trades)
1✔
1849

1850
            matched_trades = self._trades_contracts_to_amount(matched_trades)
1✔
1851

1852
            return matched_trades
1✔
1853
        except ccxt.DDoSProtection as e:
1✔
1854
            raise DDosProtection(e) from e
1✔
1855
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1856
            raise TemporaryError(
1✔
1857
                f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e
1858
        except ccxt.BaseError as e:
1✔
1859
            raise OperationalException(e) from e
1✔
1860

1861
    def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
1✔
1862
        return order['id']
1✔
1863

1864
    @retrier
1✔
1865
    def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
1✔
1866
                price: float = 1, taker_or_maker: MakerTaker = 'maker') -> float:
1867
        """
1868
        Retrieve fee from exchange
1869
        :param symbol: Pair
1870
        :param type: Type of order (market, limit, ...)
1871
        :param side: Side of order (buy, sell)
1872
        :param amount: Amount of order
1873
        :param price: Price of order
1874
        :param taker_or_maker: 'maker' or 'taker' (ignored if "type" is provided)
1875
        """
1876
        if type and type == 'market':
1✔
1877
            taker_or_maker = 'taker'
×
1878
        try:
1✔
1879
            if self._config['dry_run'] and self._config.get('fee', None) is not None:
1✔
1880
                return self._config['fee']
1✔
1881
            # validate that markets are loaded before trying to get fee
1882
            if self._api.markets is None or len(self._api.markets) == 0:
1✔
1883
                self._api.load_markets(params={})
1✔
1884

1885
            return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
1✔
1886
                                           price=price, takerOrMaker=taker_or_maker)['rate']
1887
        except ccxt.DDoSProtection as e:
1✔
1888
            raise DDosProtection(e) from e
1✔
1889
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1890
            raise TemporaryError(
1✔
1891
                f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e
1892
        except ccxt.BaseError as e:
1✔
1893
            raise OperationalException(e) from e
1✔
1894

1895
    @staticmethod
1✔
1896
    def order_has_fee(order: Dict) -> bool:
1✔
1897
        """
1898
        Verifies if the passed in order dict has the needed keys to extract fees,
1899
        and that these keys (currency, cost) are not empty.
1900
        :param order: Order or trade (one trade) dict
1901
        :return: True if the fee substructure contains currency and cost, false otherwise
1902
        """
1903
        if not isinstance(order, dict):
1✔
1904
            return False
1✔
1905
        return ('fee' in order and order['fee'] is not None
1✔
1906
                and (order['fee'].keys() >= {'currency', 'cost'})
1907
                and order['fee']['currency'] is not None
1908
                and order['fee']['cost'] is not None
1909
                )
1910

1911
    def calculate_fee_rate(
1✔
1912
            self, fee: Dict, symbol: str, cost: float, amount: float) -> Optional[float]:
1913
        """
1914
        Calculate fee rate if it's not given by the exchange.
1915
        :param fee: ccxt Fee dict - must contain cost / currency / rate
1916
        :param symbol: Symbol of the order
1917
        :param cost: Total cost of the order
1918
        :param amount: Amount of the order
1919
        """
1920
        if fee.get('rate') is not None:
1✔
1921
            return fee.get('rate')
1✔
1922
        fee_curr = fee.get('currency')
1✔
1923
        if fee_curr is None:
1✔
1924
            return None
1✔
1925
        fee_cost = float(fee['cost'])
1✔
1926

1927
        # Calculate fee based on order details
1928
        if fee_curr == self.get_pair_base_currency(symbol):
1✔
1929
            # Base currency - divide by amount
1930
            return round(fee_cost / amount, 8)
1✔
1931
        elif fee_curr == self.get_pair_quote_currency(symbol):
1✔
1932
            # Quote currency - divide by cost
1933
            return round(fee_cost / cost, 8) if cost else None
1✔
1934
        else:
1935
            # If Fee currency is a different currency
1936
            if not cost:
1✔
1937
                # If cost is None or 0.0 -> falsy, return None
1938
                return None
1✔
1939
            try:
1✔
1940
                comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency'])
1✔
1941
                tick = self.fetch_ticker(comb)
1✔
1942

1943
                fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
1✔
1944
            except (ValueError, ExchangeError):
1✔
1945
                fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
1✔
1946
                if not fee_to_quote_rate:
1✔
1947
                    return None
1✔
1948
            return round((fee_cost * fee_to_quote_rate) / cost, 8)
1✔
1949

1950
    def extract_cost_curr_rate(self, fee: Dict, symbol: str, cost: float,
1✔
1951
                               amount: float) -> Tuple[float, str, Optional[float]]:
1952
        """
1953
        Extract tuple of cost, currency, rate.
1954
        Requires order_has_fee to run first!
1955
        :param fee: ccxt Fee dict - must contain cost / currency / rate
1956
        :param symbol: Symbol of the order
1957
        :param cost: Total cost of the order
1958
        :param amount: Amount of the order
1959
        :return: Tuple with cost, currency, rate of the given fee dict
1960
        """
1961
        return (float(fee['cost']),
1✔
1962
                fee['currency'],
1963
                self.calculate_fee_rate(
1964
                    fee,
1965
                    symbol,
1966
                    cost,
1967
                    amount
1968
                    )
1969
                )
1970

1971
    # Historic data
1972

1973
    def get_historic_ohlcv(self, pair: str, timeframe: str,
1✔
1974
                           since_ms: int, candle_type: CandleType,
1975
                           is_new_pair: bool = False,
1976
                           until_ms: Optional[int] = None) -> List:
1977
        """
1978
        Get candle history using asyncio and returns the list of candles.
1979
        Handles all async work for this.
1980
        Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call.
1981
        :param pair: Pair to download
1982
        :param timeframe: Timeframe to get data for
1983
        :param since_ms: Timestamp in milliseconds to get history from
1984
        :param until_ms: Timestamp in milliseconds to get history up to
1985
        :param candle_type: '', mark, index, premiumIndex, or funding_rate
1986
        :return: List with candle (OHLCV) data
1987
        """
1988
        pair, _, _, data, _ = self.loop.run_until_complete(
1✔
1989
            self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
1990
                                           since_ms=since_ms, until_ms=until_ms,
1991
                                           is_new_pair=is_new_pair, candle_type=candle_type))
1992
        logger.info(f"Downloaded data for {pair} with length {len(data)}.")
1✔
1993
        return data
1✔
1994

1995
    async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
1✔
1996
                                        since_ms: int, candle_type: CandleType,
1997
                                        is_new_pair: bool = False, raise_: bool = False,
1998
                                        until_ms: Optional[int] = None
1999
                                        ) -> OHLCVResponse:
2000
        """
2001
        Download historic ohlcv
2002
        :param is_new_pair: used by binance subclass to allow "fast" new pair downloading
2003
        :param candle_type: Any of the enum CandleType (must match trading mode!)
2004
        """
2005

2006
        one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
1✔
2007
            timeframe, candle_type, since_ms)
2008
        logger.debug(
1✔
2009
            "one_call: %s msecs (%s)",
2010
            one_call,
2011
            dt_humanize_delta(dt_now() - timedelta(milliseconds=one_call))
2012
        )
2013
        input_coroutines = [self._async_get_candle_history(
1✔
2014
            pair, timeframe, candle_type, since) for since in
2015
            range(since_ms, until_ms or dt_ts(), one_call)]
2016

2017
        data: List = []
1✔
2018
        # Chunk requests into batches of 100 to avoid overwhelming ccxt Throttling
2019
        for input_coro in chunks(input_coroutines, 100):
1✔
2020

2021
            results = await asyncio.gather(*input_coro, return_exceptions=True)
1✔
2022
            for res in results:
1✔
2023
                if isinstance(res, BaseException):
1✔
2024
                    logger.warning(f"Async code raised an exception: {repr(res)}")
1✔
2025
                    if raise_:
1✔
2026
                        raise
1✔
2027
                    continue
1✔
2028
                else:
2029
                    # Deconstruct tuple if it's not an exception
2030
                    p, _, c, new_data, _ = res
1✔
2031
                    if p == pair and c == candle_type:
1✔
2032
                        data.extend(new_data)
1✔
2033
        # Sort data again after extending the result - above calls return in "async order"
2034
        data = sorted(data, key=lambda x: x[0])
1✔
2035
        return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
1✔
2036

2037
    def _build_coroutine(
1✔
2038
            self, pair: str, timeframe: str, candle_type: CandleType,
2039
            since_ms: Optional[int], cache: bool) -> Coroutine[Any, Any, OHLCVResponse]:
2040
        not_all_data = cache and self.required_candle_call_count > 1
1✔
2041
        if cache and (pair, timeframe, candle_type) in self._klines:
1✔
2042
            candle_limit = self.ohlcv_candle_limit(timeframe, candle_type)
1✔
2043
            min_date = date_minus_candles(timeframe, candle_limit - 5).timestamp()
1✔
2044
            # Check if 1 call can get us updated candles without hole in the data.
2045
            if min_date < self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0):
1✔
2046
                # Cache can be used - do one-off call.
2047
                not_all_data = False
1✔
2048
            else:
2049
                # Time jump detected, evict cache
2050
                logger.info(
1✔
2051
                    f"Time jump detected. Evicting cache for {pair}, {timeframe}, {candle_type}")
2052
                del self._klines[(pair, timeframe, candle_type)]
1✔
2053

2054
        if (not since_ms and (self._ft_has["ohlcv_require_since"] or not_all_data)):
1✔
2055
            # Multiple calls for one pair - to get more history
2056
            one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
1✔
2057
                timeframe, candle_type, since_ms)
2058
            move_to = one_call * self.required_candle_call_count
1✔
2059
            now = timeframe_to_next_date(timeframe)
1✔
2060
            since_ms = dt_ts(now - timedelta(seconds=move_to // 1000))
1✔
2061

2062
        if since_ms:
1✔
2063
            return self._async_get_historic_ohlcv(
1✔
2064
                pair, timeframe, since_ms=since_ms, raise_=True, candle_type=candle_type)
2065
        else:
2066
            # One call ... "regular" refresh
2067
            return self._async_get_candle_history(
1✔
2068
                pair, timeframe, since_ms=since_ms, candle_type=candle_type)
2069

2070
    def _build_ohlcv_dl_jobs(
1✔
2071
            self, pair_list: ListPairsWithTimeframes, since_ms: Optional[int],
2072
            cache: bool) -> Tuple[List[Coroutine], List[Tuple[str, str, CandleType]]]:
2073
        """
2074
        Build Coroutines to execute as part of refresh_latest_ohlcv
2075
        """
2076
        input_coroutines: List[Coroutine[Any, Any, OHLCVResponse]] = []
1✔
2077
        cached_pairs = []
1✔
2078
        for pair, timeframe, candle_type in set(pair_list):
1✔
2079
            if (timeframe not in self.timeframes
1✔
2080
                    and candle_type in (CandleType.SPOT, CandleType.FUTURES)):
2081
                logger.warning(
1✔
2082
                    f"Cannot download ({pair}, {timeframe}) combination as this timeframe is "
2083
                    f"not available on {self.name}. Available timeframes are "
2084
                    f"{', '.join(self.timeframes)}.")
2085
                continue
1✔
2086

2087
            if ((pair, timeframe, candle_type) not in self._klines or not cache
1✔
2088
                    or self._now_is_time_to_refresh(pair, timeframe, candle_type)):
2089

2090
                input_coroutines.append(
1✔
2091
                    self._build_coroutine(pair, timeframe, candle_type, since_ms, cache))
2092

2093
            else:
2094
                logger.debug(
1✔
2095
                    f"Using cached candle (OHLCV) data for {pair}, {timeframe}, {candle_type} ..."
2096
                )
2097
                cached_pairs.append((pair, timeframe, candle_type))
1✔
2098

2099
        return input_coroutines, cached_pairs
1✔
2100

2101
    def _process_ohlcv_df(self, pair: str, timeframe: str, c_type: CandleType, ticks: List[List],
1✔
2102
                          cache: bool, drop_incomplete: bool) -> DataFrame:
2103
        # keeping last candle time as last refreshed time of the pair
2104
        if ticks and cache:
1✔
2105
            idx = -2 if drop_incomplete and len(ticks) > 1 else -1
1✔
2106
            self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0] // 1000
1✔
2107
        # keeping parsed dataframe in cache
2108
        ohlcv_df = ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
1✔
2109
                                      drop_incomplete=drop_incomplete)
2110
        if cache:
1✔
2111
            if (pair, timeframe, c_type) in self._klines:
1✔
2112
                old = self._klines[(pair, timeframe, c_type)]
1✔
2113
                # Reassign so we return the updated, combined df
2114
                ohlcv_df = clean_ohlcv_dataframe(concat([old, ohlcv_df], axis=0), timeframe, pair,
1✔
2115
                                                 fill_missing=True, drop_incomplete=False)
2116
                candle_limit = self.ohlcv_candle_limit(timeframe, self._config['candle_type_def'])
1✔
2117
                # Age out old candles
2118
                ohlcv_df = ohlcv_df.tail(candle_limit + self._startup_candle_count)
1✔
2119
                ohlcv_df = ohlcv_df.reset_index(drop=True)
1✔
2120
                self._klines[(pair, timeframe, c_type)] = ohlcv_df
1✔
2121
            else:
2122
                self._klines[(pair, timeframe, c_type)] = ohlcv_df
1✔
2123
        return ohlcv_df
1✔
2124

2125
    def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
1✔
2126
                             since_ms: Optional[int] = None, cache: bool = True,
2127
                             drop_incomplete: Optional[bool] = None
2128
                             ) -> Dict[PairWithTimeframe, DataFrame]:
2129
        """
2130
        Refresh in-memory OHLCV asynchronously and set `_klines` with the result
2131
        Loops asynchronously over pair_list and downloads all pairs async (semi-parallel).
2132
        Only used in the dataprovider.refresh() method.
2133
        :param pair_list: List of 2 element tuples containing pair, interval to refresh
2134
        :param since_ms: time since when to download, in milliseconds
2135
        :param cache: Assign result to _klines. Useful for one-off downloads like for pairlists
2136
        :param drop_incomplete: Control candle dropping.
2137
            Specifying None defaults to _ohlcv_partial_candle
2138
        :return: Dict of [{(pair, timeframe): Dataframe}]
2139
        """
2140
        logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
1✔
2141

2142
        # Gather coroutines to run
2143
        input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache)
1✔
2144

2145
        results_df = {}
1✔
2146
        # Chunk requests into batches of 100 to avoid overwhelming ccxt Throttling
2147
        for input_coro in chunks(input_coroutines, 100):
1✔
2148
            async def gather_stuff():
1✔
2149
                return await asyncio.gather(*input_coro, return_exceptions=True)
1✔
2150

2151
            with self._loop_lock:
1✔
2152
                results = self.loop.run_until_complete(gather_stuff())
1✔
2153

2154
            for res in results:
1✔
2155
                if isinstance(res, Exception):
1✔
2156
                    logger.warning(f"Async code raised an exception: {repr(res)}")
1✔
2157
                    continue
1✔
2158
                # Deconstruct tuple (has 5 elements)
2159
                pair, timeframe, c_type, ticks, drop_hint = res
1✔
2160
                drop_incomplete_ = drop_hint if drop_incomplete is None else drop_incomplete
1✔
2161
                ohlcv_df = self._process_ohlcv_df(
1✔
2162
                    pair, timeframe, c_type, ticks, cache, drop_incomplete_)
2163

2164
                results_df[(pair, timeframe, c_type)] = ohlcv_df
1✔
2165

2166
        # Return cached klines
2167
        for pair, timeframe, c_type in cached_pairs:
1✔
2168
            results_df[(pair, timeframe, c_type)] = self.klines(
1✔
2169
                (pair, timeframe, c_type),
2170
                copy=False
2171
            )
2172

2173
        return results_df
1✔
2174

2175
    def refresh_ohlcv_with_cache(
1✔
2176
        self,
2177
        pairs: List[PairWithTimeframe],
2178
        since_ms: int
2179
    ) -> Dict[PairWithTimeframe, DataFrame]:
2180
        """
2181
        Refresh ohlcv data for all pairs in needed_pairs if necessary.
2182
        Caches data with expiring per timeframe.
2183
        Should only be used for pairlists which need "on time" expirarion, and no longer cache.
2184
        """
2185

2186
        timeframes = {p[1] for p in pairs}
1✔
2187
        for timeframe in timeframes:
1✔
2188
            if (timeframe, since_ms) not in self._expiring_candle_cache:
1✔
2189
                timeframe_in_sec = timeframe_to_seconds(timeframe)
1✔
2190
                # Initialise cache
2191
                self._expiring_candle_cache[(timeframe, since_ms)] = PeriodicCache(
1✔
2192
                    ttl=timeframe_in_sec, maxsize=1000)
2193

2194
        # Get candles from cache
2195
        candles = {
1✔
2196
            c: self._expiring_candle_cache[(c[1], since_ms)].get(c, None) for c in pairs
2197
            if c in self._expiring_candle_cache[(c[1], since_ms)]
2198
        }
2199
        pairs_to_download = [p for p in pairs if p not in candles]
1✔
2200
        if pairs_to_download:
1✔
2201
            candles = self.refresh_latest_ohlcv(
1✔
2202
                pairs_to_download, since_ms=since_ms, cache=False
2203
            )
2204
            for c, val in candles.items():
1✔
2205
                self._expiring_candle_cache[(c[1], since_ms)][c] = val
1✔
2206
        return candles
1✔
2207

2208
    def _now_is_time_to_refresh(self, pair: str, timeframe: str, candle_type: CandleType) -> bool:
1✔
2209
        # Timeframe in seconds
2210
        interval_in_sec = timeframe_to_seconds(timeframe)
1✔
2211
        plr = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + interval_in_sec
1✔
2212
        # current,active candle open date
2213
        now = int(timeframe_to_prev_date(timeframe).timestamp())
1✔
2214
        return plr < now
1✔
2215

2216
    @retrier_async
1✔
2217
    async def _async_get_candle_history(
1✔
2218
        self,
2219
        pair: str,
2220
        timeframe: str,
2221
        candle_type: CandleType,
2222
        since_ms: Optional[int] = None,
2223
    ) -> OHLCVResponse:
2224
        """
2225
        Asynchronously get candle history data using fetch_ohlcv
2226
        :param candle_type: '', mark, index, premiumIndex, or funding_rate
2227
        returns tuple: (pair, timeframe, ohlcv_list)
2228
        """
2229
        try:
1✔
2230
            # Fetch OHLCV asynchronously
2231
            s = '(' + dt_from_ts(since_ms).isoformat() + ') ' if since_ms is not None else ''
1✔
2232
            logger.debug(
1✔
2233
                "Fetching pair %s, %s, interval %s, since %s %s...",
2234
                pair, candle_type, timeframe, since_ms, s
2235
            )
2236
            params = deepcopy(self._ft_has.get('ohlcv_params', {}))
1✔
2237
            candle_limit = self.ohlcv_candle_limit(
1✔
2238
                timeframe, candle_type=candle_type, since_ms=since_ms)
2239

2240
            if candle_type and candle_type != CandleType.SPOT:
1✔
2241
                params.update({'price': candle_type.value})
1✔
2242
            if candle_type != CandleType.FUNDING_RATE:
1✔
2243
                data = await self._api_async.fetch_ohlcv(
1✔
2244
                    pair, timeframe=timeframe, since=since_ms,
2245
                    limit=candle_limit, params=params)
2246
            else:
2247
                # Funding rate
2248
                data = await self._fetch_funding_rate_history(
1✔
2249
                    pair=pair,
2250
                    timeframe=timeframe,
2251
                    limit=candle_limit,
2252
                    since_ms=since_ms,
2253
                )
2254
            # Some exchanges sort OHLCV in ASC order and others in DESC.
2255
            # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
2256
            # while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
2257
            # Only sort if necessary to save computing time
2258
            try:
1✔
2259
                if data and data[0][0] > data[-1][0]:
1✔
2260
                    data = sorted(data, key=lambda x: x[0])
1✔
2261
            except IndexError:
1✔
2262
                logger.exception("Error loading %s. Result was %s.", pair, data)
1✔
2263
                return pair, timeframe, candle_type, [], self._ohlcv_partial_candle
1✔
2264
            logger.debug("Done fetching pair %s, %s interval %s...", pair, candle_type, timeframe)
1✔
2265
            return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
1✔
2266

2267
        except ccxt.NotSupported as e:
1✔
2268
            raise OperationalException(
1✔
2269
                f'Exchange {self._api.name} does not support fetching historical '
2270
                f'candle (OHLCV) data. Message: {e}') from e
2271
        except ccxt.DDoSProtection as e:
1✔
2272
            raise DDosProtection(e) from e
1✔
2273
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2274
            raise TemporaryError(f'Could not fetch historical candle (OHLCV) data '
1✔
2275
                                 f'for pair {pair} due to {e.__class__.__name__}. '
2276
                                 f'Message: {e}') from e
2277
        except ccxt.BaseError as e:
1✔
2278
            raise OperationalException(f'Could not fetch historical candle (OHLCV) data '
1✔
2279
                                       f'for pair {pair}. Message: {e}') from e
2280

2281
    async def _fetch_funding_rate_history(
1✔
2282
        self,
2283
        pair: str,
2284
        timeframe: str,
2285
        limit: int,
2286
        since_ms: Optional[int] = None,
2287
    ) -> List[List]:
2288
        """
2289
        Fetch funding rate history - used to selectively override this by subclasses.
2290
        """
2291
        # Funding rate
2292
        data = await self._api_async.fetch_funding_rate_history(
1✔
2293
            pair, since=since_ms,
2294
            limit=limit)
2295
        # Convert funding rate to candle pattern
2296
        data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
1✔
2297
        return data
1✔
2298

2299
    # Fetch historic trades
2300

2301
    @retrier_async
1✔
2302
    async def _async_fetch_trades(self, pair: str,
1✔
2303
                                  since: Optional[int] = None,
2304
                                  params: Optional[dict] = None) -> Tuple[List[List], Any]:
2305
        """
2306
        Asynchronously gets trade history using fetch_trades.
2307
        Handles exchange errors, does one call to the exchange.
2308
        :param pair: Pair to fetch trade data for
2309
        :param since: Since as integer timestamp in milliseconds
2310
        returns: List of dicts containing trades, the next iteration value (new "since" or trade_id)
2311
        """
2312
        try:
1✔
2313
            # fetch trades asynchronously
2314
            if params:
1✔
2315
                logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
1✔
2316
                trades = await self._api_async.fetch_trades(pair, params=params, limit=1000)
1✔
2317
            else:
2318
                logger.debug(
1✔
2319
                    "Fetching trades for pair %s, since %s %s...",
2320
                    pair, since,
2321
                    '(' + dt_from_ts(since).isoformat() + ') ' if since is not None else ''
2322
                )
2323
                trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
1✔
2324
            trades = self._trades_contracts_to_amount(trades)
1✔
2325
            pagination_value = self._get_trade_pagination_next_value(trades)
1✔
2326
            return trades_dict_to_list(trades), pagination_value
1✔
2327
        except ccxt.NotSupported as e:
1✔
2328
            raise OperationalException(
1✔
2329
                f'Exchange {self._api.name} does not support fetching historical trade data.'
2330
                f'Message: {e}') from e
2331
        except ccxt.DDoSProtection as e:
1✔
2332
            raise DDosProtection(e) from e
1✔
2333
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2334
            raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. '
1✔
2335
                                 f'Message: {e}') from e
2336
        except ccxt.BaseError as e:
1✔
2337
            raise OperationalException(f'Could not fetch trade data. Msg: {e}') from e
1✔
2338

2339
    def _valid_trade_pagination_id(self, pair: str, from_id: str) -> bool:
1✔
2340
        """
2341
        Verify trade-pagination id is valid.
2342
        Workaround for odd Kraken issue where ID is sometimes wrong.
2343
        """
2344
        return True
1✔
2345

2346
    def _get_trade_pagination_next_value(self, trades: List[Dict]):
1✔
2347
        """
2348
        Extract pagination id for the next "from_id" value
2349
        Applies only to fetch_trade_history by id.
2350
        """
2351
        if not trades:
1✔
2352
            return None
×
2353
        if self._trades_pagination == 'id':
1✔
2354
            return trades[-1].get('id')
1✔
2355
        else:
2356
            return trades[-1].get('timestamp')
1✔
2357

2358
    async def _async_get_trade_history_id(self, pair: str,
1✔
2359
                                          until: int,
2360
                                          since: Optional[int] = None,
2361
                                          from_id: Optional[str] = None) -> Tuple[str, List[List]]:
2362
        """
2363
        Asynchronously gets trade history using fetch_trades
2364
        use this when exchange uses id-based iteration (check `self._trades_pagination`)
2365
        :param pair: Pair to fetch trade data for
2366
        :param since: Since as integer timestamp in milliseconds
2367
        :param until: Until as integer timestamp in milliseconds
2368
        :param from_id: Download data starting with ID (if id is known). Ignores "since" if set.
2369
        returns tuple: (pair, trades-list)
2370
        """
2371

2372
        trades: List[List] = []
1✔
2373
        # DEFAULT_TRADES_COLUMNS: 0 -> timestamp
2374
        # DEFAULT_TRADES_COLUMNS: 1 -> id
2375
        has_overlap = self._ft_has.get('trades_pagination_overlap', True)
1✔
2376
        # Skip last trade by default since its the key for the next call
2377
        x = slice(None, -1) if has_overlap else slice(None)
1✔
2378

2379
        if not from_id or not self._valid_trade_pagination_id(pair, from_id):
1✔
2380
            # Fetch first elements using timebased method to get an ID to paginate on
2381
            # Depending on the Exchange, this can introduce a drift at the start of the interval
2382
            # of up to an hour.
2383
            # e.g. Binance returns the "last 1000" candles within a 1h time interval
2384
            # - so we will miss the first trades.
2385
            t, from_id = await self._async_fetch_trades(pair, since=since)
1✔
2386
            trades.extend(t[x])
1✔
2387
        while True:
1✔
2388
            try:
1✔
2389
                t, from_id_next = await self._async_fetch_trades(
1✔
2390
                    pair, params={self._trades_pagination_arg: from_id})
2391
                if t:
1✔
2392
                    trades.extend(t[x])
1✔
2393
                    if from_id == from_id_next or t[-1][0] > until:
1✔
2394
                        logger.debug(f"Stopping because from_id did not change. "
1✔
2395
                                     f"Reached {t[-1][0]} > {until}")
2396
                        # Reached the end of the defined-download period - add last trade as well.
2397
                        if has_overlap:
1✔
2398
                            trades.extend(t[-1:])
1✔
2399
                        break
1✔
2400

2401
                    from_id = from_id_next
1✔
2402
                else:
2403
                    logger.debug("Stopping as no more trades were returned.")
×
2404
                    break
×
2405
            except asyncio.CancelledError:
×
2406
                logger.debug("Async operation Interrupted, breaking trades DL loop.")
×
2407
                break
×
2408

2409
        return (pair, trades)
1✔
2410

2411
    async def _async_get_trade_history_time(self, pair: str, until: int,
1✔
2412
                                            since: Optional[int] = None) -> Tuple[str, List[List]]:
2413
        """
2414
        Asynchronously gets trade history using fetch_trades,
2415
        when the exchange uses time-based iteration (check `self._trades_pagination`)
2416
        :param pair: Pair to fetch trade data for
2417
        :param since: Since as integer timestamp in milliseconds
2418
        :param until: Until as integer timestamp in milliseconds
2419
        returns tuple: (pair, trades-list)
2420
        """
2421

2422
        trades: List[List] = []
1✔
2423
        # DEFAULT_TRADES_COLUMNS: 0 -> timestamp
2424
        # DEFAULT_TRADES_COLUMNS: 1 -> id
2425
        while True:
1✔
2426
            try:
1✔
2427
                t, since_next = await self._async_fetch_trades(pair, since=since)
1✔
2428
                if t:
1✔
2429
                    # No more trades to download available at the exchange,
2430
                    # So we repeatedly get the same trade over and over again.
2431
                    if since == since_next and len(t) == 1:
1✔
2432
                        logger.debug("Stopping because no more trades are available.")
×
2433
                        break
×
2434
                    since = since_next
1✔
2435
                    trades.extend(t)
1✔
2436
                    # Reached the end of the defined-download period
2437
                    if until and since_next > until:
1✔
2438
                        logger.debug(
1✔
2439
                            f"Stopping because until was reached. {since_next} > {until}")
2440
                        break
1✔
2441
                else:
2442
                    logger.debug("Stopping as no more trades were returned.")
1✔
2443
                    break
1✔
2444
            except asyncio.CancelledError:
×
2445
                logger.debug("Async operation Interrupted, breaking trades DL loop.")
×
2446
                break
×
2447

2448
        return (pair, trades)
1✔
2449

2450
    async def _async_get_trade_history(self, pair: str,
1✔
2451
                                       since: Optional[int] = None,
2452
                                       until: Optional[int] = None,
2453
                                       from_id: Optional[str] = None) -> Tuple[str, List[List]]:
2454
        """
2455
        Async wrapper handling downloading trades using either time or id based methods.
2456
        """
2457

2458
        logger.debug(f"_async_get_trade_history(), pair: {pair}, "
1✔
2459
                     f"since: {since}, until: {until}, from_id: {from_id}")
2460

2461
        if until is None:
1✔
2462
            until = ccxt.Exchange.milliseconds()
×
2463
            logger.debug(f"Exchange milliseconds: {until}")
×
2464

2465
        if self._trades_pagination == 'time':
1✔
2466
            return await self._async_get_trade_history_time(
1✔
2467
                pair=pair, since=since, until=until)
2468
        elif self._trades_pagination == 'id':
1✔
2469
            return await self._async_get_trade_history_id(
1✔
2470
                pair=pair, since=since, until=until, from_id=from_id
2471
            )
2472
        else:
2473
            raise OperationalException(f"Exchange {self.name} does use neither time, "
×
2474
                                       f"nor id based pagination")
2475

2476
    def get_historic_trades(self, pair: str,
1✔
2477
                            since: Optional[int] = None,
2478
                            until: Optional[int] = None,
2479
                            from_id: Optional[str] = None) -> Tuple[str, List]:
2480
        """
2481
        Get trade history data using asyncio.
2482
        Handles all async work and returns the list of candles.
2483
        Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call.
2484
        :param pair: Pair to download
2485
        :param since: Timestamp in milliseconds to get history from
2486
        :param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.
2487
        :param from_id: Download data starting with ID (if id is known)
2488
        :returns List of trade data
2489
        """
2490
        if not self.exchange_has("fetchTrades"):
1✔
2491
            raise OperationalException("This exchange does not support downloading Trades.")
1✔
2492

2493
        with self._loop_lock:
1✔
2494
            task = asyncio.ensure_future(self._async_get_trade_history(
1✔
2495
                pair=pair, since=since, until=until, from_id=from_id))
2496

2497
            for sig in [signal.SIGINT, signal.SIGTERM]:
1✔
2498
                try:
1✔
2499
                    self.loop.add_signal_handler(sig, task.cancel)
1✔
2500
                except NotImplementedError:
×
2501
                    # Not all platforms implement signals (e.g. windows)
2502
                    pass
×
2503
            return self.loop.run_until_complete(task)
1✔
2504

2505
    @retrier
1✔
2506
    def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
1✔
2507
        """
2508
        Returns the sum of all funding fees that were exchanged for a pair within a timeframe
2509
        Dry-run handling happens as part of _calculate_funding_fees.
2510
        :param pair: (e.g. ADA/USDT)
2511
        :param since: The earliest time of consideration for calculating funding fees,
2512
            in unix time or as a datetime
2513
        """
2514
        if not self.exchange_has("fetchFundingHistory"):
1✔
2515
            raise OperationalException(
×
2516
                f"fetch_funding_history() is not available using {self.name}"
2517
            )
2518

2519
        if type(since) is datetime:
1✔
2520
            since = dt_ts(since)
1✔
2521

2522
        try:
1✔
2523
            funding_history = self._api.fetch_funding_history(
1✔
2524
                symbol=pair,
2525
                since=since
2526
            )
2527
            self._log_exchange_response('funding_history', funding_history,
1✔
2528
                                        add_info=f"pair: {pair}, since: {since}")
2529
            return sum(fee['amount'] for fee in funding_history)
1✔
2530
        except ccxt.DDoSProtection as e:
1✔
2531
            raise DDosProtection(e) from e
1✔
2532
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2533
            raise TemporaryError(
1✔
2534
                f'Could not get funding fees due to {e.__class__.__name__}. Message: {e}') from e
2535
        except ccxt.BaseError as e:
1✔
2536
            raise OperationalException(e) from e
1✔
2537

2538
    @retrier
1✔
2539
    def get_leverage_tiers(self) -> Dict[str, List[Dict]]:
1✔
2540
        try:
1✔
2541
            return self._api.fetch_leverage_tiers()
1✔
2542
        except ccxt.DDoSProtection as e:
1✔
2543
            raise DDosProtection(e) from e
1✔
2544
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2545
            raise TemporaryError(
1✔
2546
                f'Could not load leverage tiers due to {e.__class__.__name__}. Message: {e}'
2547
            ) from e
2548
        except ccxt.BaseError as e:
1✔
2549
            raise OperationalException(e) from e
1✔
2550

2551
    @retrier_async
1✔
2552
    async def get_market_leverage_tiers(self, symbol: str) -> Tuple[str, List[Dict]]:
1✔
2553
        """ Leverage tiers per symbol """
2554
        try:
1✔
2555
            tier = await self._api_async.fetch_market_leverage_tiers(symbol)
1✔
2556
            return symbol, tier
1✔
2557
        except ccxt.DDoSProtection as e:
1✔
2558
            raise DDosProtection(e) from e
1✔
2559
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2560
            raise TemporaryError(
1✔
2561
                f'Could not load leverage tiers for {symbol}'
2562
                f' due to {e.__class__.__name__}. Message: {e}'
2563
            ) from e
2564
        except ccxt.BaseError as e:
1✔
2565
            raise OperationalException(e) from e
1✔
2566

2567
    def load_leverage_tiers(self) -> Dict[str, List[Dict]]:
1✔
2568
        if self.trading_mode == TradingMode.FUTURES:
1✔
2569
            if self.exchange_has('fetchLeverageTiers'):
1✔
2570
                # Fetch all leverage tiers at once
2571
                return self.get_leverage_tiers()
1✔
2572
            elif self.exchange_has('fetchMarketLeverageTiers'):
1✔
2573
                # Must fetch the leverage tiers for each market separately
2574
                # * This is slow(~45s) on Okx, makes ~90 api calls to load all linear swap markets
2575
                markets = self.markets
1✔
2576

2577
                symbols = [
1✔
2578
                    symbol for symbol, market in markets.items()
2579
                    if (self.market_is_future(market)
2580
                        and market['quote'] == self._config['stake_currency'])
2581
                ]
2582

2583
                tiers: Dict[str, List[Dict]] = {}
1✔
2584

2585
                tiers_cached = self.load_cached_leverage_tiers(self._config['stake_currency'])
1✔
2586
                if tiers_cached:
1✔
2587
                    tiers = tiers_cached
1✔
2588

2589
                coros = [
1✔
2590
                    self.get_market_leverage_tiers(symbol)
2591
                    for symbol in sorted(symbols) if symbol not in tiers]
2592

2593
                # Be verbose here, as this delays startup by ~1 minute.
2594
                if coros:
1✔
2595
                    logger.info(
1✔
2596
                        f"Initializing leverage_tiers for {len(symbols)} markets. "
2597
                        "This will take about a minute.")
2598
                else:
2599
                    logger.info("Using cached leverage_tiers.")
1✔
2600

2601
                async def gather_results(input_coro):
1✔
2602
                    return await asyncio.gather(*input_coro, return_exceptions=True)
1✔
2603

2604
                for input_coro in chunks(coros, 100):
1✔
2605

2606
                    with self._loop_lock:
1✔
2607
                        results = self.loop.run_until_complete(gather_results(input_coro))
1✔
2608

2609
                    for res in results:
1✔
2610
                        if isinstance(res, Exception):
1✔
2611
                            logger.warning(f"Leverage tier exception: {repr(res)}")
1✔
2612
                            continue
1✔
2613
                        symbol, tier = res
1✔
2614
                        tiers[symbol] = tier
1✔
2615
                if len(coros) > 0:
1✔
2616
                    self.cache_leverage_tiers(tiers, self._config['stake_currency'])
1✔
2617
                logger.info(f"Done initializing {len(symbols)} markets.")
1✔
2618

2619
                return tiers
1✔
2620
        return {}
1✔
2621

2622
    def cache_leverage_tiers(self, tiers: Dict[str, List[Dict]], stake_currency: str) -> None:
1✔
2623

2624
        filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json"
1✔
2625
        if not filename.parent.is_dir():
1✔
2626
            filename.parent.mkdir(parents=True)
1✔
2627
        data = {
1✔
2628
            "updated": datetime.now(timezone.utc),
2629
            "data": tiers,
2630
        }
2631
        file_dump_json(filename, data)
1✔
2632

2633
    def load_cached_leverage_tiers(self, stake_currency: str) -> Optional[Dict[str, List[Dict]]]:
1✔
2634
        filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json"
1✔
2635
        if filename.is_file():
1✔
2636
            try:
1✔
2637
                tiers = file_load_json(filename)
1✔
2638
                updated = tiers.get('updated')
1✔
2639
                if updated:
1✔
2640
                    updated_dt = parser.parse(updated)
1✔
2641
                    if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4):
1✔
2642
                        logger.info("Cached leverage tiers are outdated. Will update.")
1✔
2643
                        return None
1✔
2644
                return tiers['data']
1✔
2645
            except Exception:
×
2646
                logger.exception("Error loading cached leverage tiers. Refreshing.")
×
2647
        return None
1✔
2648

2649
    def fill_leverage_tiers(self) -> None:
1✔
2650
        """
2651
        Assigns property _leverage_tiers to a dictionary of information about the leverage
2652
        allowed on each pair
2653
        """
2654
        leverage_tiers = self.load_leverage_tiers()
1✔
2655
        for pair, tiers in leverage_tiers.items():
1✔
2656
            pair_tiers = []
1✔
2657
            for tier in tiers:
1✔
2658
                pair_tiers.append(self.parse_leverage_tier(tier))
1✔
2659
            self._leverage_tiers[pair] = pair_tiers
1✔
2660

2661
    def parse_leverage_tier(self, tier) -> Dict:
1✔
2662
        info = tier.get('info', {})
1✔
2663
        return {
1✔
2664
            'minNotional': tier['minNotional'],
2665
            'maxNotional': tier['maxNotional'],
2666
            'maintenanceMarginRate': tier['maintenanceMarginRate'],
2667
            'maxLeverage': tier['maxLeverage'],
2668
            'maintAmt': float(info['cum']) if 'cum' in info else None,
2669
        }
2670

2671
    def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float:
1✔
2672
        """
2673
        Returns the maximum leverage that a pair can be traded at
2674
        :param pair: The base/quote currency pair being traded
2675
        :stake_amount: The total value of the traders margin_mode in quote currency
2676
        """
2677

2678
        if self.trading_mode == TradingMode.SPOT:
1✔
2679
            return 1.0
1✔
2680

2681
        if self.trading_mode == TradingMode.FUTURES:
1✔
2682

2683
            # Checks and edge cases
2684
            if stake_amount is None:
1✔
2685
                raise OperationalException(
×
2686
                    f'{self.name}.get_max_leverage requires argument stake_amount'
2687
                )
2688

2689
            if pair not in self._leverage_tiers:
1✔
2690
                # Maybe raise exception because it can't be traded on futures?
2691
                return 1.0
1✔
2692

2693
            pair_tiers = self._leverage_tiers[pair]
1✔
2694

2695
            if stake_amount == 0:
1✔
2696
                return self._leverage_tiers[pair][0]['maxLeverage']  # Max lev for lowest amount
1✔
2697

2698
            for tier_index in range(len(pair_tiers)):
1✔
2699

2700
                tier = pair_tiers[tier_index]
1✔
2701
                lev = tier['maxLeverage']
1✔
2702

2703
                if tier_index < len(pair_tiers) - 1:
1✔
2704
                    next_tier = pair_tiers[tier_index + 1]
1✔
2705
                    next_floor = next_tier['minNotional'] / next_tier['maxLeverage']
1✔
2706
                    if next_floor > stake_amount:  # Next tier min too high for stake amount
1✔
2707
                        return min((tier['maxNotional'] / stake_amount), lev)
1✔
2708
                        #
2709
                        # With the two leverage tiers below,
2710
                        # - a stake amount of 150 would mean a max leverage of (10000 / 150) = 66.66
2711
                        # - stakes below 133.33 = max_lev of 75
2712
                        # - stakes between 133.33-200 = max_lev of 10000/stake = 50.01-74.99
2713
                        # - stakes from 200 + 1000 = max_lev of 50
2714
                        #
2715
                        # {
2716
                        #     "min": 0,      # stake = 0.0
2717
                        #     "max": 10000,  # max_stake@75 = 10000/75 = 133.33333333333334
2718
                        #     "lev": 75,
2719
                        # },
2720
                        # {
2721
                        #     "min": 10000,  # stake = 200.0
2722
                        #     "max": 50000,  # max_stake@50 = 50000/50 = 1000.0
2723
                        #     "lev": 50,
2724
                        # }
2725
                        #
2726

2727
                else:  # if on the last tier
2728
                    if stake_amount > tier['maxNotional']:
1✔
2729
                        # If stake is > than max tradeable amount
2730
                        raise InvalidOrderException(f'Amount {stake_amount} too high for {pair}')
1✔
2731
                    else:
2732
                        return tier['maxLeverage']
1✔
2733

2734
            raise OperationalException(
×
2735
                'Looped through all tiers without finding a max leverage. Should never be reached'
2736
            )
2737

2738
        elif self.trading_mode == TradingMode.MARGIN:  # Search markets.limits for max lev
1✔
2739
            market = self.markets[pair]
1✔
2740
            if market['limits']['leverage']['max'] is not None:
1✔
2741
                return market['limits']['leverage']['max']
1✔
2742
            else:
2743
                return 1.0  # Default if max leverage cannot be found
1✔
2744
        else:
2745
            return 1.0
×
2746

2747
    @retrier
1✔
2748
    def _set_leverage(
1✔
2749
        self,
2750
        leverage: float,
2751
        pair: Optional[str] = None,
2752
        accept_fail: bool = False,
2753
    ):
2754
        """
2755
        Set's the leverage before making a trade, in order to not
2756
        have the same leverage on every trade
2757
        """
2758
        if self._config['dry_run'] or not self.exchange_has("setLeverage"):
1✔
2759
            # Some exchanges only support one margin_mode type
2760
            return
1✔
2761
        if self._ft_has.get('floor_leverage', False) is True:
1✔
2762
            # Rounding for binance ...
2763
            leverage = floor(leverage)
1✔
2764
        try:
1✔
2765
            res = self._api.set_leverage(symbol=pair, leverage=leverage)
1✔
2766
            self._log_exchange_response('set_leverage', res)
1✔
2767
        except ccxt.DDoSProtection as e:
1✔
2768
            raise DDosProtection(e) from e
1✔
2769
        except (ccxt.BadRequest, ccxt.OperationRejected, ccxt.InsufficientFunds) as e:
1✔
2770
            if not accept_fail:
×
2771
                raise TemporaryError(
×
2772
                    f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
2773
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2774
            raise TemporaryError(
1✔
2775
                f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
2776
        except ccxt.BaseError as e:
1✔
2777
            raise OperationalException(e) from e
1✔
2778

2779
    def get_interest_rate(self) -> float:
1✔
2780
        """
2781
        Retrieve interest rate - necessary for Margin trading.
2782
        Should not call the exchange directly when used from backtesting.
2783
        """
2784
        return 0.0
×
2785

2786
    def funding_fee_cutoff(self, open_date: datetime) -> bool:
1✔
2787
        """
2788
        Funding fees are only charged at full hours (usually every 4-8h).
2789
        Therefore a trade opening at 10:00:01 will not be charged a funding fee until the next hour.
2790
        :param open_date: The open date for a trade
2791
        :return: True if the date falls on a full hour, False otherwise
2792
        """
2793
        return open_date.minute == 0 and open_date.second == 0
1✔
2794

2795
    @retrier
1✔
2796
    def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False,
1✔
2797
                        params: Optional[Dict] = None):
2798
        """
2799
        Set's the margin mode on the exchange to cross or isolated for a specific pair
2800
        :param pair: base/quote currency pair (e.g. "ADA/USDT")
2801
        """
2802
        if self._config['dry_run'] or not self.exchange_has("setMarginMode"):
1✔
2803
            # Some exchanges only support one margin_mode type
2804
            return
1✔
2805

2806
        if params is None:
1✔
2807
            params = {}
1✔
2808
        try:
1✔
2809
            res = self._api.set_margin_mode(margin_mode.value, pair, params)
1✔
2810
            self._log_exchange_response('set_margin_mode', res)
×
2811
        except ccxt.DDoSProtection as e:
1✔
2812
            raise DDosProtection(e) from e
1✔
2813
        except (ccxt.BadRequest, ccxt.OperationRejected) as e:
1✔
2814
            if not accept_fail:
×
2815
                raise TemporaryError(
×
2816
                    f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
2817
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2818
            raise TemporaryError(
1✔
2819
                f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
2820
        except ccxt.BaseError as e:
1✔
2821
            raise OperationalException(e) from e
1✔
2822

2823
    def _fetch_and_calculate_funding_fees(
1✔
2824
        self,
2825
        pair: str,
2826
        amount: float,
2827
        is_short: bool,
2828
        open_date: datetime,
2829
        close_date: Optional[datetime] = None
2830
    ) -> float:
2831
        """
2832
        Fetches and calculates the sum of all funding fees that occurred for a pair
2833
        during a futures trade.
2834
        Only used during dry-run or if the exchange does not provide a funding_rates endpoint.
2835
        :param pair: The quote/base pair of the trade
2836
        :param amount: The quantity of the trade
2837
        :param is_short: trade direction
2838
        :param open_date: The date and time that the trade started
2839
        :param close_date: The date and time that the trade ended
2840
        """
2841

2842
        if self.funding_fee_cutoff(open_date):
1✔
2843
            # Shift back to 1h candle to avoid missing funding fees
2844
            # Only really relevant for trades very close to the full hour
2845
            open_date = timeframe_to_prev_date('1h', open_date)
1✔
2846
        timeframe = self._ft_has['mark_ohlcv_timeframe']
1✔
2847
        timeframe_ff = self._ft_has['funding_fee_timeframe']
1✔
2848
        mark_price_type = CandleType.from_string(self._ft_has["mark_ohlcv_price"])
1✔
2849

2850
        if not close_date:
1✔
2851
            close_date = datetime.now(timezone.utc)
1✔
2852
        since_ms = dt_ts(timeframe_to_prev_date(timeframe, open_date))
1✔
2853

2854
        mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type)
1✔
2855
        funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE)
1✔
2856

2857
        candle_histories = self.refresh_latest_ohlcv(
1✔
2858
            [mark_comb, funding_comb],
2859
            since_ms=since_ms,
2860
            cache=False,
2861
            drop_incomplete=False,
2862
        )
2863
        try:
1✔
2864
            # we can't assume we always get histories - for example during exchange downtimes
2865
            funding_rates = candle_histories[funding_comb]
1✔
2866
            mark_rates = candle_histories[mark_comb]
1✔
2867
        except KeyError:
1✔
2868
            raise ExchangeError("Could not find funding rates.") from None
1✔
2869

2870
        funding_mark_rates = self.combine_funding_and_mark(funding_rates, mark_rates)
1✔
2871

2872
        return self.calculate_funding_fees(
1✔
2873
            funding_mark_rates,
2874
            amount=amount,
2875
            is_short=is_short,
2876
            open_date=open_date,
2877
            close_date=close_date
2878
        )
2879

2880
    @staticmethod
1✔
2881
    def combine_funding_and_mark(funding_rates: DataFrame, mark_rates: DataFrame,
1✔
2882
                                 futures_funding_rate: Optional[int] = None) -> DataFrame:
2883
        """
2884
        Combine funding-rates and mark-rates dataframes
2885
        :param funding_rates: Dataframe containing Funding rates (Type FUNDING_RATE)
2886
        :param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price)
2887
        :param futures_funding_rate: Fake funding rate to use if funding_rates are not available
2888
        """
2889
        if futures_funding_rate is None:
1✔
2890
            return mark_rates.merge(
1✔
2891
                funding_rates, on='date', how="inner", suffixes=["_mark", "_fund"])
2892
        else:
2893
            if len(funding_rates) == 0:
1✔
2894
                # No funding rate candles - full fillup with fallback variable
2895
                mark_rates['open_fund'] = futures_funding_rate
1✔
2896
                return mark_rates.rename(
1✔
2897
                        columns={'open': 'open_mark',
2898
                                 'close': 'close_mark',
2899
                                 'high': 'high_mark',
2900
                                 'low': 'low_mark',
2901
                                 'volume': 'volume_mark'})
2902

2903
            else:
2904
                # Fill up missing funding_rate candles with fallback value
2905
                combined = mark_rates.merge(
1✔
2906
                    funding_rates, on='date', how="left", suffixes=["_mark", "_fund"]
2907
                    )
2908
                combined['open_fund'] = combined['open_fund'].fillna(futures_funding_rate)
1✔
2909
                return combined
1✔
2910

2911
    def calculate_funding_fees(
1✔
2912
        self,
2913
        df: DataFrame,
2914
        amount: float,
2915
        is_short: bool,
2916
        open_date: datetime,
2917
        close_date: datetime,
2918
        time_in_ratio: Optional[float] = None
2919
    ) -> float:
2920
        """
2921
        calculates the sum of all funding fees that occurred for a pair during a futures trade
2922
        :param df: Dataframe containing combined funding and mark rates
2923
                   as `open_fund` and `open_mark`.
2924
        :param amount: The quantity of the trade
2925
        :param is_short: trade direction
2926
        :param open_date: The date and time that the trade started
2927
        :param close_date: The date and time that the trade ended
2928
        :param time_in_ratio: Not used by most exchange classes
2929
        """
2930
        fees: float = 0
1✔
2931

2932
        if not df.empty:
1✔
2933
            df1 = df[(df['date'] >= open_date) & (df['date'] <= close_date)]
1✔
2934
            fees = sum(df1['open_fund'] * df1['open_mark'] * amount)
1✔
2935
        if isnan(fees):
1✔
2936
            fees = 0.0
1✔
2937
        # Negate fees for longs as funding_fees expects it this way based on live endpoints.
2938
        return fees if is_short else -fees
1✔
2939

2940
    def get_funding_fees(
1✔
2941
            self, pair: str, amount: float, is_short: bool, open_date: datetime) -> float:
2942
        """
2943
        Fetch funding fees, either from the exchange (live) or calculates them
2944
        based on funding rate/mark price history
2945
        :param pair: The quote/base pair of the trade
2946
        :param is_short: trade direction
2947
        :param amount: Trade amount
2948
        :param open_date: Open date of the trade
2949
        :return: funding fee since open_date
2950
        """
2951
        if self.trading_mode == TradingMode.FUTURES:
1✔
2952
            try:
1✔
2953
                if self._config['dry_run']:
1✔
2954
                    funding_fees = self._fetch_and_calculate_funding_fees(
1✔
2955
                        pair, amount, is_short, open_date)
2956
                else:
2957
                    funding_fees = self._get_funding_fees_from_exchange(pair, open_date)
×
2958
                return funding_fees
1✔
2959
            except ExchangeError:
1✔
2960
                logger.warning(f"Could not update funding fees for {pair}.")
1✔
2961

2962
        return 0.0
1✔
2963

2964
    def get_liquidation_price(
1✔
2965
        self,
2966
        pair: str,
2967
        # Dry-run
2968
        open_rate: float,   # Entry price of position
2969
        is_short: bool,
2970
        amount: float,  # Absolute value of position size
2971
        stake_amount: float,
2972
        leverage: float,
2973
        wallet_balance: float,
2974
        mm_ex_1: float = 0.0,  # (Binance) Cross only
2975
        upnl_ex_1: float = 0.0,  # (Binance) Cross only
2976
    ) -> Optional[float]:
2977
        """
2978
        Set's the margin mode on the exchange to cross or isolated for a specific pair
2979
        """
2980
        if self.trading_mode == TradingMode.SPOT:
1✔
2981
            return None
1✔
2982
        elif (self.trading_mode != TradingMode.FUTURES):
1✔
2983
            raise OperationalException(
1✔
2984
                f"{self.name} does not support {self.margin_mode} {self.trading_mode}")
2985

2986
        liquidation_price = None
1✔
2987
        if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
1✔
2988

2989
            liquidation_price = self.dry_run_liquidation_price(
1✔
2990
                pair=pair,
2991
                open_rate=open_rate,
2992
                is_short=is_short,
2993
                amount=amount,
2994
                leverage=leverage,
2995
                stake_amount=stake_amount,
2996
                wallet_balance=wallet_balance,
2997
                mm_ex_1=mm_ex_1,
2998
                upnl_ex_1=upnl_ex_1
2999
            )
3000
        else:
3001
            positions = self.fetch_positions(pair)
1✔
3002
            if len(positions) > 0:
1✔
3003
                pos = positions[0]
1✔
3004
                liquidation_price = pos['liquidationPrice']
1✔
3005

3006
        if liquidation_price is not None:
1✔
3007
            buffer_amount = abs(open_rate - liquidation_price) * self.liquidation_buffer
1✔
3008
            liquidation_price_buffer = (
1✔
3009
                liquidation_price - buffer_amount
3010
                if is_short else
3011
                liquidation_price + buffer_amount
3012
            )
3013
            return max(liquidation_price_buffer, 0.0)
1✔
3014
        else:
3015
            return None
1✔
3016

3017
    def dry_run_liquidation_price(
1✔
3018
        self,
3019
        pair: str,
3020
        open_rate: float,   # Entry price of position
3021
        is_short: bool,
3022
        amount: float,
3023
        stake_amount: float,
3024
        leverage: float,
3025
        wallet_balance: float,  # Or margin balance
3026
        mm_ex_1: float = 0.0,  # (Binance) Cross only
3027
        upnl_ex_1: float = 0.0,  # (Binance) Cross only
3028
    ) -> Optional[float]:
3029
        """
3030
        Important: Must be fetching data from cached values as this is used by backtesting!
3031
        PERPETUAL:
3032
         gate: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-price
3033
         > Liquidation Price = (Entry Price ± Margin / Contract Multiplier / Size) /
3034
                                [ 1 ± (Maintenance Margin Ratio + Taker Rate)]
3035
            Wherein, "+" or "-" depends on whether the contract goes long or short:
3036
            "-" for long, and "+" for short.
3037

3038
         okex: https://www.okex.com/support/hc/en-us/articles/
3039
            360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin
3040

3041
        :param pair: Pair to calculate liquidation price for
3042
        :param open_rate: Entry price of position
3043
        :param is_short: True if the trade is a short, false otherwise
3044
        :param amount: Absolute value of position size incl. leverage (in base currency)
3045
        :param stake_amount: Stake amount - Collateral in settle currency.
3046
        :param leverage: Leverage used for this position.
3047
        :param trading_mode: SPOT, MARGIN, FUTURES, etc.
3048
        :param margin_mode: Either ISOLATED or CROSS
3049
        :param wallet_balance: Amount of margin_mode in the wallet being used to trade
3050
            Cross-Margin Mode: crossWalletBalance
3051
            Isolated-Margin Mode: isolatedWalletBalance
3052

3053
        # * Not required by Gate or OKX
3054
        :param mm_ex_1:
3055
        :param upnl_ex_1:
3056
        """
3057

3058
        market = self.markets[pair]
1✔
3059
        taker_fee_rate = market['taker']
1✔
3060
        mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount)
1✔
3061

3062
        if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
1✔
3063

3064
            if market['inverse']:
1✔
3065
                raise OperationalException(
×
3066
                    "Freqtrade does not yet support inverse contracts")
3067

3068
            value = wallet_balance / amount
1✔
3069

3070
            mm_ratio_taker = (mm_ratio + taker_fee_rate)
1✔
3071
            if is_short:
1✔
3072
                return (open_rate + value) / (1 + mm_ratio_taker)
1✔
3073
            else:
3074
                return (open_rate - value) / (1 - mm_ratio_taker)
1✔
3075
        else:
3076
            raise OperationalException(
×
3077
                "Freqtrade only supports isolated futures for leverage trading")
3078

3079
    def get_maintenance_ratio_and_amt(
1✔
3080
        self,
3081
        pair: str,
3082
        nominal_value: float,
3083
    ) -> Tuple[float, Optional[float]]:
3084
        """
3085
        Important: Must be fetching data from cached values as this is used by backtesting!
3086
        :param pair: Market symbol
3087
        :param nominal_value: The total trade amount in quote currency including leverage
3088
        maintenance amount only on Binance
3089
        :return: (maintenance margin ratio, maintenance amount)
3090
        """
3091

3092
        if (self._config.get('runmode') in OPTIMIZE_MODES
1✔
3093
                or self.exchange_has('fetchLeverageTiers')
3094
                or self.exchange_has('fetchMarketLeverageTiers')):
3095

3096
            if pair not in self._leverage_tiers:
1✔
3097
                raise InvalidOrderException(
1✔
3098
                    f"Maintenance margin rate for {pair} is unavailable for {self.name}"
3099
                )
3100

3101
            pair_tiers = self._leverage_tiers[pair]
1✔
3102

3103
            for tier in reversed(pair_tiers):
1✔
3104
                if nominal_value >= tier['minNotional']:
1✔
3105
                    return (tier['maintenanceMarginRate'], tier['maintAmt'])
1✔
3106

3107
            raise ExchangeError("nominal value can not be lower than 0")
1✔
3108
            # The lowest notional_floor for any pair in fetch_leverage_tiers is always 0 because it
3109
            # describes the min amt for a tier, and the lowest tier will always go down to 0
3110
        else:
3111
            raise ExchangeError(f"Cannot get maintenance ratio using {self.name}")
1✔
3112
            raise ExchangeError(f"Cannot get maintenance ratio using {self.name}")
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