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

freqtrade / freqtrade / 6181253459

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

push

github-actions

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

remove old codes when we only can do partial entries

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

19114 of 20202 relevant lines covered (94.61%)

0.95 hits per line

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

97.35
/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
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, TradingMode
1✔
27
from freqtrade.enums.pricetype import PriceType
1✔
28
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
1✔
29
                                  InvalidOrderException, OperationalException, PricingError,
30
                                  RetryableOrderError, 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
                                               timeframe_to_minutes, timeframe_to_msecs,
39
                                               timeframe_to_next_date, timeframe_to_prev_date,
40
                                               timeframe_to_seconds)
41
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
1✔
42
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
1✔
43
                            safe_value_fallback2)
44
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
1✔
45
from freqtrade.util import dt_from_ts, dt_now
1✔
46
from freqtrade.util.datetime_helpers import dt_humanize, dt_ts
1✔
47

48

49
logger = logging.getLogger(__name__)
1✔
50

51

52
class Exchange:
1✔
53

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

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

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

92
    _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
1✔
93
        # TradingMode.SPOT always supported and not required in this list
94
    ]
95

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

114
        self._config.update(config)
1✔
115

116
        # Holds last candle refreshed time of each pair
117
        self._pairs_last_refresh_time: Dict[PairWithTimeframe, int] = {}
1✔
118
        # Timestamp of last markets refresh
119
        self._last_markets_refresh: int = 0
1✔
120

121
        # Cache for 10 minutes ...
122
        self._cache_lock = Lock()
1✔
123
        self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10)
1✔
124
        # Cache values for 1800 to avoid frequent polling of the exchange for prices
125
        # Caching only applies to RPC methods, so prices for open trades are still
126
        # refreshed once every iteration.
127
        self._exit_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
1✔
128
        self._entry_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800)
1✔
129

130
        # Holds candles
131
        self._klines: Dict[PairWithTimeframe, DataFrame] = {}
1✔
132

133
        # Holds all open sell orders for dry_run
134
        self._dry_run_open_orders: Dict[str, Any] = {}
1✔
135

136
        if config['dry_run']:
1✔
137
            logger.info('Instance is running with dry_run enabled')
1✔
138
        logger.info(f"Using CCXT {ccxt.__version__}")
1✔
139
        exchange_conf: Dict[str, Any] = exchange_config if exchange_config else config['exchange']
1✔
140
        remove_exchange_credentials(exchange_conf, config.get('dry_run', False))
1✔
141
        self.log_responses = exchange_conf.get('log_responses', False)
1✔
142

143
        # Leverage properties
144
        self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
1✔
145
        self.margin_mode: MarginMode = (
1✔
146
            MarginMode(config.get('margin_mode'))
147
            if config.get('margin_mode')
148
            else MarginMode.NONE
149
        )
150
        self.liquidation_buffer = config.get('liquidation_buffer', 0.05)
1✔
151

152
        # Deep merge ft_has with default ft_has options
153
        self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
1✔
154
        if self.trading_mode == TradingMode.FUTURES:
1✔
155
            self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
1✔
156
        if exchange_conf.get('_ft_has_params'):
1✔
157
            self._ft_has = deep_merge_dicts(exchange_conf.get('_ft_has_params'),
1✔
158
                                            self._ft_has)
159
            logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
1✔
160

161
        # Assign this directly for easy access
162
        self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle']
1✔
163

164
        self._trades_pagination = self._ft_has['trades_pagination']
1✔
165
        self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
1✔
166

167
        # Initialize ccxt objects
168
        ccxt_config = self._ccxt_config
1✔
169
        ccxt_config = deep_merge_dicts(exchange_conf.get('ccxt_config', {}), ccxt_config)
1✔
170
        ccxt_config = deep_merge_dicts(exchange_conf.get('ccxt_sync_config', {}), ccxt_config)
1✔
171

172
        self._api = self._init_ccxt(exchange_conf, ccxt_kwargs=ccxt_config)
1✔
173

174
        ccxt_async_config = self._ccxt_config
1✔
175
        ccxt_async_config = deep_merge_dicts(exchange_conf.get('ccxt_config', {}),
1✔
176
                                             ccxt_async_config)
177
        ccxt_async_config = deep_merge_dicts(exchange_conf.get('ccxt_async_config', {}),
1✔
178
                                             ccxt_async_config)
179
        self._api_async = self._init_ccxt(
1✔
180
            exchange_conf, ccxt_async, ccxt_kwargs=ccxt_async_config)
181

182
        logger.info(f'Using Exchange "{self.name}"')
1✔
183
        self.required_candle_call_count = 1
1✔
184
        if validate:
1✔
185
            # Initial markets load
186
            self._load_markets()
1✔
187
            self.validate_config(config)
1✔
188
            self._startup_candle_count: int = config.get('startup_candle_count', 0)
1✔
189
            self.required_candle_call_count = self.validate_required_startup_candles(
1✔
190
                self._startup_candle_count, config.get('timeframe', ''))
191

192
        # Converts the interval provided in minutes in config to seconds
193
        self.markets_refresh_interval: int = exchange_conf.get(
1✔
194
            "markets_refresh_interval", 60) * 60 * 1000
195

196
        if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
1✔
197
            self.fill_leverage_tiers()
1✔
198
        self.additional_exchange_init()
1✔
199

200
    def __del__(self):
1✔
201
        """
202
        Destructor - clean up async stuff
203
        """
204
        self.close()
1✔
205

206
    def close(self):
1✔
207
        logger.debug("Exchange object destroyed, closing async loop")
1✔
208
        if (self._api_async and inspect.iscoroutinefunction(self._api_async.close)
1✔
209
                and self._api_async.session):
210
            logger.debug("Closing async ccxt session.")
×
211
            self.loop.run_until_complete(self._api_async.close())
×
212
        if self.loop and not self.loop.is_closed():
1✔
213
            self.loop.close()
1✔
214

215
    def _init_async_loop(self) -> asyncio.AbstractEventLoop:
1✔
216
        loop = asyncio.new_event_loop()
1✔
217
        asyncio.set_event_loop(loop)
1✔
218
        return loop
1✔
219

220
    def validate_config(self, config):
1✔
221
        # Check if timeframe is available
222
        self.validate_timeframes(config.get('timeframe'))
1✔
223

224
        # Check if all pairs are available
225
        self.validate_stakecurrency(config['stake_currency'])
1✔
226
        if not config['exchange'].get('skip_pair_validation'):
1✔
227
            self.validate_pairs(config['exchange']['pair_whitelist'])
1✔
228
        self.validate_ordertypes(config.get('order_types', {}))
1✔
229
        self.validate_order_time_in_force(config.get('order_time_in_force', {}))
1✔
230
        self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
1✔
231
        self.validate_pricing(config['exit_pricing'])
1✔
232
        self.validate_pricing(config['entry_pricing'])
1✔
233

234
    def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
1✔
235
                   ccxt_kwargs: Dict = {}) -> ccxt.Exchange:
236
        """
237
        Initialize ccxt with given config and return valid
238
        ccxt instance.
239
        """
240
        # Find matching class for the given exchange name
241
        name = exchange_config['name']
1✔
242

243
        if not is_exchange_known_ccxt(name, ccxt_module):
1✔
244
            raise OperationalException(f'Exchange {name} is not supported by ccxt')
1✔
245

246
        ex_config = {
1✔
247
            'apiKey': exchange_config.get('key'),
248
            'secret': exchange_config.get('secret'),
249
            'password': exchange_config.get('password'),
250
            'uid': exchange_config.get('uid', ''),
251
        }
252
        if ccxt_kwargs:
1✔
253
            logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
1✔
254
        if self._ccxt_params:
1✔
255
            # Inject static options after the above output to not confuse users.
256
            ccxt_kwargs = deep_merge_dicts(self._ccxt_params, ccxt_kwargs)
1✔
257
        if ccxt_kwargs:
1✔
258
            ex_config.update(ccxt_kwargs)
1✔
259
        try:
1✔
260

261
            api = getattr(ccxt_module, name.lower())(ex_config)
1✔
262
        except (KeyError, AttributeError) as e:
1✔
263
            raise OperationalException(f'Exchange {name} is not supported') from e
1✔
264
        except ccxt.BaseError as e:
1✔
265
            raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
1✔
266

267
        return api
1✔
268

269
    @property
1✔
270
    def _ccxt_config(self) -> Dict:
1✔
271
        # Parameters to add directly to ccxt sync/async initialization.
272
        if self.trading_mode == TradingMode.MARGIN:
1✔
273
            return {
1✔
274
                "options": {
275
                    "defaultType": "margin"
276
                }
277
            }
278
        elif self.trading_mode == TradingMode.FUTURES:
1✔
279
            return {
1✔
280
                "options": {
281
                    "defaultType": self._ft_has["ccxt_futures_name"]
282
                }
283
            }
284
        else:
285
            return {}
1✔
286

287
    @property
1✔
288
    def name(self) -> str:
1✔
289
        """exchange Name (from ccxt)"""
290
        return self._api.name
1✔
291

292
    @property
1✔
293
    def id(self) -> str:
1✔
294
        """exchange ccxt id"""
295
        return self._api.id
×
296

297
    @property
1✔
298
    def timeframes(self) -> List[str]:
1✔
299
        return list((self._api.timeframes or {}).keys())
1✔
300

301
    @property
1✔
302
    def markets(self) -> Dict[str, Any]:
1✔
303
        """exchange ccxt markets"""
304
        if not self._markets:
1✔
305
            logger.info("Markets were not loaded. Loading them now..")
1✔
306
            self._load_markets()
1✔
307
        return self._markets
1✔
308

309
    @property
1✔
310
    def precisionMode(self) -> int:
1✔
311
        """exchange ccxt precisionMode"""
312
        return self._api.precisionMode
1✔
313

314
    def additional_exchange_init(self) -> None:
1✔
315
        """
316
        Additional exchange initialization logic.
317
        .api will be available at this point.
318
        Must be overridden in child methods if required.
319
        """
320
        pass
1✔
321

322
    def _log_exchange_response(self, endpoint, response) -> None:
1✔
323
        """ Log exchange responses """
324
        if self.log_responses:
1✔
325
            logger.info(f"API {endpoint}: {response}")
1✔
326

327
    def ohlcv_candle_limit(
1✔
328
            self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
329
        """
330
        Exchange ohlcv candle limit
331
        Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
332
        per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
333
        :param timeframe: Timeframe to check
334
        :param candle_type: Candle-type
335
        :param since_ms: Starting timestamp
336
        :return: Candle limit as integer
337
        """
338
        return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get(
1✔
339
            timeframe, self._ft_has.get('ohlcv_candle_limit')))
340

341
    def get_markets(self, base_currencies: List[str] = [], quote_currencies: List[str] = [],
1✔
342
                    spot_only: bool = False, margin_only: bool = False, futures_only: bool = False,
343
                    tradable_only: bool = True,
344
                    active_only: bool = False) -> Dict[str, Any]:
345
        """
346
        Return exchange ccxt markets, filtered out by base currency and quote currency
347
        if this was requested in parameters.
348
        """
349
        markets = self.markets
1✔
350
        if not markets:
1✔
351
            raise OperationalException("Markets were not loaded.")
1✔
352

353
        if base_currencies:
1✔
354
            markets = {k: v for k, v in markets.items() if v['base'] in base_currencies}
1✔
355
        if quote_currencies:
1✔
356
            markets = {k: v for k, v in markets.items() if v['quote'] in quote_currencies}
1✔
357
        if tradable_only:
1✔
358
            markets = {k: v for k, v in markets.items() if self.market_is_tradable(v)}
1✔
359
        if spot_only:
1✔
360
            markets = {k: v for k, v in markets.items() if self.market_is_spot(v)}
1✔
361
        if margin_only:
1✔
362
            markets = {k: v for k, v in markets.items() if self.market_is_margin(v)}
×
363
        if futures_only:
1✔
364
            markets = {k: v for k, v in markets.items() if self.market_is_future(v)}
1✔
365
        if active_only:
1✔
366
            markets = {k: v for k, v in markets.items() if market_is_active(v)}
1✔
367
        return markets
1✔
368

369
    def get_quote_currencies(self) -> List[str]:
1✔
370
        """
371
        Return a list of supported quote currencies
372
        """
373
        markets = self.markets
1✔
374
        return sorted(set([x['quote'] for _, x in markets.items()]))
1✔
375

376
    def get_pair_quote_currency(self, pair: str) -> str:
1✔
377
        """ Return a pair's quote currency (base/quote:settlement) """
378
        return self.markets.get(pair, {}).get('quote', '')
1✔
379

380
    def get_pair_base_currency(self, pair: str) -> str:
1✔
381
        """ Return a pair's base currency (base/quote:settlement) """
382
        return self.markets.get(pair, {}).get('base', '')
1✔
383

384
    def market_is_future(self, market: Dict[str, Any]) -> bool:
1✔
385
        return (
1✔
386
            market.get(self._ft_has["ccxt_futures_name"], False) is True and
387
            market.get('linear', False) is True
388
        )
389

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

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

396
    def market_is_tradable(self, market: Dict[str, Any]) -> bool:
1✔
397
        """
398
        Check if the market symbol is tradable by Freqtrade.
399
        Ensures that Configured mode aligns to
400
        """
401
        return (
1✔
402
            market.get('quote', None) is not None
403
            and market.get('base', None) is not None
404
            and (self.precisionMode != TICK_SIZE
405
                 # Too low precision will falsify calculations
406
                 or market.get('precision', {}).get('price') > 1e-11)
407
            and ((self.trading_mode == TradingMode.SPOT and self.market_is_spot(market))
408
                 or (self.trading_mode == TradingMode.MARGIN and self.market_is_margin(market))
409
                 or (self.trading_mode == TradingMode.FUTURES and self.market_is_future(market)))
410
        )
411

412
    def klines(self, pair_interval: PairWithTimeframe, copy: bool = True) -> DataFrame:
1✔
413
        if pair_interval in self._klines:
1✔
414
            return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
1✔
415
        else:
416
            return DataFrame()
1✔
417

418
    def get_contract_size(self, pair: str) -> Optional[float]:
1✔
419
        if self.trading_mode == TradingMode.FUTURES:
1✔
420
            market = self.markets.get(pair, {})
1✔
421
            contract_size: float = 1.0
1✔
422
            if not market:
1✔
423
                return None
1✔
424
            if market.get('contractSize') is not None:
1✔
425
                # ccxt has contractSize in markets as string
426
                contract_size = float(market['contractSize'])
1✔
427
            return contract_size
1✔
428
        else:
429
            return 1
1✔
430

431
    def _trades_contracts_to_amount(self, trades: List) -> List:
1✔
432
        if len(trades) > 0 and 'symbol' in trades[0]:
1✔
433
            contract_size = self.get_contract_size(trades[0]['symbol'])
1✔
434
            if contract_size != 1:
1✔
435
                for trade in trades:
1✔
436
                    trade['amount'] = trade['amount'] * contract_size
1✔
437
        return trades
1✔
438

439
    def _order_contracts_to_amount(self, order: Dict) -> Dict:
1✔
440
        if 'symbol' in order and order['symbol'] is not None:
1✔
441
            contract_size = self.get_contract_size(order['symbol'])
1✔
442
            if contract_size != 1:
1✔
443
                for prop in self._ft_has.get('order_props_in_contracts', []):
1✔
444
                    if prop in order and order[prop] is not None:
1✔
445
                        order[prop] = order[prop] * contract_size
1✔
446
        return order
1✔
447

448
    def _amount_to_contracts(self, pair: str, amount: float) -> float:
1✔
449

450
        contract_size = self.get_contract_size(pair)
1✔
451
        return amount_to_contracts(amount, contract_size)
1✔
452

453
    def _contracts_to_amount(self, pair: str, num_contracts: float) -> float:
1✔
454

455
        contract_size = self.get_contract_size(pair)
1✔
456
        return contracts_to_amount(num_contracts, contract_size)
1✔
457

458
    def amount_to_contract_precision(self, pair: str, amount: float) -> float:
1✔
459
        """
460
        Helper wrapper around amount_to_contract_precision
461
        """
462
        contract_size = self.get_contract_size(pair)
1✔
463

464
        return amount_to_contract_precision(amount, self.get_precision_amount(pair),
1✔
465
                                            self.precisionMode, contract_size)
466

467
    def _load_async_markets(self, reload: bool = False) -> None:
1✔
468
        try:
1✔
469
            if self._api_async:
1✔
470
                self.loop.run_until_complete(
1✔
471
                    self._api_async.load_markets(reload=reload, params={}))
472

473
        except (asyncio.TimeoutError, ccxt.BaseError) as e:
1✔
474
            logger.warning('Could not load async markets. Reason: %s', e)
1✔
475
            return
1✔
476

477
    def _load_markets(self) -> None:
1✔
478
        """ Initialize markets both sync and async """
479
        try:
1✔
480
            self._markets = self._api.load_markets(params={})
1✔
481
            self._load_async_markets()
1✔
482
            self._last_markets_refresh = dt_ts()
1✔
483
            if self._ft_has['needs_trading_fees']:
1✔
484
                self._trading_fees = self.fetch_trading_fees()
1✔
485

486
        except ccxt.BaseError:
1✔
487
            logger.exception('Unable to initialize markets.')
1✔
488

489
    def reload_markets(self) -> None:
1✔
490
        """Reload markets both sync and async if refresh interval has passed """
491
        # Check whether markets have to be reloaded
492
        if (self._last_markets_refresh > 0) and (
1✔
493
                self._last_markets_refresh + self.markets_refresh_interval > dt_ts()):
494
            return None
1✔
495
        logger.debug("Performing scheduled market reload..")
1✔
496
        try:
1✔
497
            self._markets = self._api.load_markets(reload=True, params={})
1✔
498
            # Also reload async markets to avoid issues with newly listed pairs
499
            self._load_async_markets(reload=True)
1✔
500
            self._last_markets_refresh = dt_ts()
1✔
501
            self.fill_leverage_tiers()
1✔
502
        except ccxt.BaseError:
1✔
503
            logger.exception("Could not reload markets.")
1✔
504

505
    def validate_stakecurrency(self, stake_currency: str) -> None:
1✔
506
        """
507
        Checks stake-currency against available currencies on the exchange.
508
        Only runs on startup. If markets have not been loaded, there's been a problem with
509
        the connection to the exchange.
510
        :param stake_currency: Stake-currency to validate
511
        :raise: OperationalException if stake-currency is not available.
512
        """
513
        if not self._markets:
1✔
514
            raise OperationalException(
1✔
515
                'Could not load markets, therefore cannot start. '
516
                'Please investigate the above error for more details.'
517
            )
518
        quote_currencies = self.get_quote_currencies()
1✔
519
        if stake_currency not in quote_currencies:
1✔
520
            raise OperationalException(
1✔
521
                f"{stake_currency} is not available as stake on {self.name}. "
522
                f"Available currencies are: {', '.join(quote_currencies)}")
523

524
    def validate_pairs(self, pairs: List[str]) -> None:
1✔
525
        """
526
        Checks if all given pairs are tradable on the current exchange.
527
        :param pairs: list of pairs
528
        :raise: OperationalException if one pair is not available
529
        :return: None
530
        """
531

532
        if not self.markets:
1✔
533
            logger.warning('Unable to validate pairs (assuming they are correct).')
1✔
534
            return
1✔
535
        extended_pairs = expand_pairlist(pairs, list(self.markets), keep_invalid=True)
1✔
536
        invalid_pairs = []
1✔
537
        for pair in extended_pairs:
1✔
538
            # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
539
            if self.markets and pair not in self.markets:
1✔
540
                raise OperationalException(
1✔
541
                    f'Pair {pair} is not available on {self.name} {self.trading_mode.value}. '
542
                    f'Please remove {pair} from your whitelist.')
543

544
                # From ccxt Documentation:
545
                # markets.info: An associative array of non-common market properties,
546
                # including fees, rates, limits and other general market information.
547
                # The internal info array is different for each particular market,
548
                # its contents depend on the exchange.
549
                # It can also be a string or similar ... so we need to verify that first.
550
            elif (isinstance(self.markets[pair].get('info'), dict)
1✔
551
                  and self.markets[pair].get('info', {}).get('prohibitedIn', False)):
552
                # Warn users about restricted pairs in whitelist.
553
                # We cannot determine reliably if Users are affected.
554
                logger.warning(f"Pair {pair} is restricted for some users on this exchange."
1✔
555
                               f"Please check if you are impacted by this restriction "
556
                               f"on the exchange and eventually remove {pair} from your whitelist.")
557
            if (self._config['stake_currency'] and
1✔
558
                    self.get_pair_quote_currency(pair) != self._config['stake_currency']):
559
                invalid_pairs.append(pair)
1✔
560
        if invalid_pairs:
1✔
561
            raise OperationalException(
1✔
562
                f"Stake-currency '{self._config['stake_currency']}' not compatible with "
563
                f"pair-whitelist. Please remove the following pairs: {invalid_pairs}")
564

565
    def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str:
1✔
566
        """
567
        Get valid pair combination of curr_1 and curr_2 by trying both combinations.
568
        """
569
        for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
1✔
570
            if pair in self.markets and self.markets[pair].get('active'):
1✔
571
                return pair
1✔
572
        raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
1✔
573

574
    def validate_timeframes(self, timeframe: Optional[str]) -> None:
1✔
575
        """
576
        Check if timeframe from config is a supported timeframe on the exchange
577
        """
578
        if not hasattr(self._api, "timeframes") or self._api.timeframes is None:
1✔
579
            # If timeframes attribute is missing (or is None), the exchange probably
580
            # has no fetchOHLCV method.
581
            # Therefore we also show that.
582
            raise OperationalException(
1✔
583
                f"The ccxt library does not provide the list of timeframes "
584
                f"for the exchange {self.name} and this exchange "
585
                f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}")
586

587
        if timeframe and (timeframe not in self.timeframes):
1✔
588
            raise OperationalException(
1✔
589
                f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}")
590

591
        if timeframe and timeframe_to_minutes(timeframe) < 1:
1✔
592
            raise OperationalException("Timeframes < 1m are currently not supported by Freqtrade.")
1✔
593

594
    def validate_ordertypes(self, order_types: Dict) -> None:
1✔
595
        """
596
        Checks if order-types configured in strategy/config are supported
597
        """
598
        if any(v == 'market' for k, v in order_types.items()):
1✔
599
            if not self.exchange_has('createMarketOrder'):
1✔
600
                raise OperationalException(
1✔
601
                    f'Exchange {self.name} does not support market orders.')
602
        self.validate_stop_ordertypes(order_types)
1✔
603

604
    def validate_stop_ordertypes(self, order_types: Dict) -> None:
1✔
605
        """
606
        Validate stoploss order types
607
        """
608
        if (order_types.get("stoploss_on_exchange")
1✔
609
                and not self._ft_has.get("stoploss_on_exchange", False)):
610
            raise OperationalException(
1✔
611
                f'On exchange stoploss is not supported for {self.name}.'
612
            )
613
        if self.trading_mode == TradingMode.FUTURES:
1✔
614
            price_mapping = self._ft_has.get('stop_price_type_value_mapping', {}).keys()
1✔
615
            if (
1✔
616
                order_types.get("stoploss_on_exchange", False) is True
617
                and 'stoploss_price_type' in order_types
618
                and order_types['stoploss_price_type'] not in price_mapping
619
            ):
620
                raise OperationalException(
1✔
621
                    f'On exchange stoploss price type is not supported for {self.name}.'
622
                )
623

624
    def validate_pricing(self, pricing: Dict) -> None:
1✔
625
        if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'):
1✔
626
            raise OperationalException(f'Orderbook not available for {self.name}.')
1✔
627
        if (not pricing.get('use_order_book', False) and (
1✔
628
                not self.exchange_has('fetchTicker')
629
                or not self._ft_has['tickers_have_price'])):
630
            raise OperationalException(f'Ticker pricing not available for {self.name}.')
1✔
631

632
    def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
1✔
633
        """
634
        Checks if order time in force configured in strategy/config are supported
635
        """
636
        if any(v.upper() not in self._ft_has["order_time_in_force"]
1✔
637
               for k, v in order_time_in_force.items()):
638
            raise OperationalException(
1✔
639
                f'Time in force policies are not supported for {self.name} yet.')
640

641
    def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
1✔
642
        """
643
        Checks if required startup_candles is more than ohlcv_candle_limit().
644
        Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
645
        """
646

647
        candle_limit = self.ohlcv_candle_limit(
1✔
648
            timeframe, self._config['candle_type_def'],
649
            int(date_minus_candles(timeframe, startup_candles).timestamp() * 1000)
650
            if timeframe else None)
651
        # Require one more candle - to account for the still open candle.
652
        candle_count = startup_candles + 1
1✔
653
        # Allow 5 calls to the exchange per pair
654
        required_candle_call_count = int(
1✔
655
            (candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1))
656
        if self._ft_has['ohlcv_has_history']:
1✔
657

658
            if required_candle_call_count > 5:
1✔
659
                # Only allow 5 calls per pair to somewhat limit the impact
660
                raise OperationalException(
1✔
661
                    f"This strategy requires {startup_candles} candles to start, "
662
                    "which is more than 5x "
663
                    f"the amount of candles {self.name} provides for {timeframe}.")
664
        elif required_candle_call_count > 1:
1✔
665
            raise OperationalException(
1✔
666
                f"This strategy requires {startup_candles} candles to start, which is more than "
667
                f"the amount of candles {self.name} provides for {timeframe}.")
668
        if required_candle_call_count > 1:
1✔
669
            logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. "
1✔
670
                           f"This can result in slower operations for the bot. Please check "
671
                           f"if you really need {startup_candles} candles for your strategy")
672
        return required_candle_call_count
1✔
673

674
    def validate_trading_mode_and_margin_mode(
1✔
675
        self,
676
        trading_mode: TradingMode,
677
        margin_mode: Optional[MarginMode]  # Only None when trading_mode = TradingMode.SPOT
678
    ):
679
        """
680
        Checks if freqtrade can perform trades using the configured
681
        trading mode(Margin, Futures) and MarginMode(Cross, Isolated)
682
        Throws OperationalException:
683
            If the trading_mode/margin_mode type are not supported by freqtrade on this exchange
684
        """
685
        if trading_mode != TradingMode.SPOT and (
1✔
686
            (trading_mode, margin_mode) not in self._supported_trading_mode_margin_pairs
687
        ):
688
            mm_value = margin_mode and margin_mode.value
1✔
689
            raise OperationalException(
1✔
690
                f"Freqtrade does not support {mm_value} {trading_mode.value} on {self.name}"
691
            )
692

693
    def get_option(self, param: str, default: Optional[Any] = None) -> Any:
1✔
694
        """
695
        Get parameter value from _ft_has
696
        """
697
        return self._ft_has.get(param, default)
1✔
698

699
    def exchange_has(self, endpoint: str) -> bool:
1✔
700
        """
701
        Checks if exchange implements a specific API endpoint.
702
        Wrapper around ccxt 'has' attribute
703
        :param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
704
        :return: bool
705
        """
706
        return endpoint in self._api.has and self._api.has[endpoint]
1✔
707

708
    def get_precision_amount(self, pair: str) -> Optional[float]:
1✔
709
        """
710
        Returns the amount precision of the exchange.
711
        :param pair: Pair to get precision for
712
        :return: precision for amount or None. Must be used in combination with precisionMode
713
        """
714
        return self.markets.get(pair, {}).get('precision', {}).get('amount', None)
1✔
715

716
    def get_precision_price(self, pair: str) -> Optional[float]:
1✔
717
        """
718
        Returns the price precision of the exchange.
719
        :param pair: Pair to get precision for
720
        :return: precision for price or None. Must be used in combination with precisionMode
721
        """
722
        return self.markets.get(pair, {}).get('precision', {}).get('price', None)
1✔
723

724
    def amount_to_precision(self, pair: str, amount: float) -> float:
1✔
725
        """
726
        Returns the amount to buy or sell to a precision the Exchange accepts
727

728
        """
729
        return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
1✔
730

731
    def price_to_precision(self, pair: str, price: float, *, rounding_mode: int = ROUND) -> float:
1✔
732
        """
733
        Returns the price rounded to the precision the Exchange accepts.
734
        The default price_rounding_mode in conf is ROUND.
735
        For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
736
        """
737
        return price_to_precision(price, self.get_precision_price(pair),
1✔
738
                                  self.precisionMode, rounding_mode=rounding_mode)
739

740
    def price_get_one_pip(self, pair: str, price: float) -> float:
1✔
741
        """
742
        Get's the "1 pip" value for this pair.
743
        Used in PriceFilter to calculate the 1pip movements.
744
        """
745
        precision = self.markets[pair]['precision']['price']
1✔
746
        if self.precisionMode == TICK_SIZE:
1✔
747
            return precision
1✔
748
        else:
749
            return 1 / pow(10, precision)
1✔
750

751
    def get_min_pair_stake_amount(
1✔
752
        self,
753
        pair: str,
754
        price: float,
755
        stoploss: float,
756
        leverage: Optional[float] = 1.0
757
    ) -> Optional[float]:
758
        return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage)
1✔
759

760
    def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
1✔
761
        max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max', leverage)
1✔
762
        if max_stake_amount is None:
1✔
763
            # * Should never be executed
764
            raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
×
765
                                       'never set max_stake_amount to None')
766
        return max_stake_amount
1✔
767

768
    def _get_stake_amount_limit(
1✔
769
        self,
770
        pair: str,
771
        price: float,
772
        stoploss: float,
773
        limit: Literal['min', 'max'],
774
        leverage: Optional[float] = 1.0
775
    ) -> Optional[float]:
776

777
        isMin = limit == 'min'
1✔
778

779
        try:
1✔
780
            market = self.markets[pair]
1✔
781
        except KeyError:
1✔
782
            raise ValueError(f"Can't get market information for symbol {pair}")
1✔
783

784
        if isMin:
1✔
785
            # reserve some percent defined in config (5% default) + stoploss
786
            margin_reserve: float = 1.0 + self._config.get('amount_reserve_percent',
1✔
787
                                                           DEFAULT_AMOUNT_RESERVE_PERCENT)
788
            stoploss_reserve = (
1✔
789
                margin_reserve / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
790
            )
791
            # it should not be more than 50%
792
            stoploss_reserve = max(min(stoploss_reserve, 1.5), 1)
1✔
793
        else:
794
            margin_reserve = 1.0
1✔
795
            stoploss_reserve = 1.0
1✔
796

797
        stake_limits = []
1✔
798
        limits = market['limits']
1✔
799
        if (limits['cost'][limit] is not None):
1✔
800
            stake_limits.append(
1✔
801
                self._contracts_to_amount(pair, limits['cost'][limit]) * stoploss_reserve
802
            )
803

804
        if (limits['amount'][limit] is not None):
1✔
805
            stake_limits.append(
1✔
806
                self._contracts_to_amount(pair, limits['amount'][limit]) * price * margin_reserve
807
            )
808

809
        if not stake_limits:
1✔
810
            return None if isMin else float('inf')
1✔
811

812
        # The value returned should satisfy both limits: for amount (base currency) and
813
        # for cost (quote, stake currency), so max() is used here.
814
        # See also #2575 at github.
815
        return self._get_stake_amount_considering_leverage(
1✔
816
            max(stake_limits) if isMin else min(stake_limits),
817
            leverage or 1.0
818
        )
819

820
    def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
1✔
821
        """
822
        Takes the minimum stake amount for a pair with no leverage and returns the minimum
823
        stake amount when leverage is considered
824
        :param stake_amount: The stake amount for a pair before leverage is considered
825
        :param leverage: The amount of leverage being used on the current trade
826
        """
827
        return stake_amount / leverage
1✔
828

829
    # Dry-run methods
830

831
    def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
1✔
832
                             rate: float, leverage: float, params: Dict = {},
833
                             stop_loss: bool = False) -> Dict[str, Any]:
834
        now = dt_now()
1✔
835
        order_id = f'dry_run_{side}_{pair}_{now.timestamp()}'
1✔
836
        # Rounding here must respect to contract sizes
837
        _amount = self._contracts_to_amount(
1✔
838
            pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)))
839
        dry_order: Dict[str, Any] = {
1✔
840
            'id': order_id,
841
            'symbol': pair,
842
            'price': rate,
843
            'average': rate,
844
            'amount': _amount,
845
            'cost': _amount * rate,
846
            'type': ordertype,
847
            'side': side,
848
            'filled': 0,
849
            'remaining': _amount,
850
            'datetime': now.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
851
            'timestamp': dt_ts(now),
852
            'status': "open",
853
            'fee': None,
854
            'info': {},
855
            'leverage': leverage
856
        }
857
        if stop_loss:
1✔
858
            dry_order["info"] = {"stopPrice": dry_order["price"]}
1✔
859
            dry_order[self._ft_has['stop_price_param']] = dry_order["price"]
1✔
860
            # Workaround to avoid filling stoploss orders immediately
861
            dry_order["ft_order_type"] = "stoploss"
1✔
862
        orderbook: Optional[OrderBook] = None
1✔
863
        if self.exchange_has('fetchL2OrderBook'):
1✔
864
            orderbook = self.fetch_l2_order_book(pair, 20)
1✔
865
        if ordertype == "limit" and orderbook:
1✔
866
            # Allow a 1% price difference
867
            allowed_diff = 0.01
1✔
868
            if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
1✔
869
                logger.info(
1✔
870
                    f"Converted order {pair} to market order due to price {rate} crossing spread "
871
                    f"by more than {allowed_diff:.2%}.")
872
                dry_order["type"] = "market"
1✔
873

874
        if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
1✔
875
            # Update market order pricing
876
            average = self.get_dry_market_fill_price(pair, side, amount, rate, orderbook)
1✔
877
            dry_order.update({
1✔
878
                'average': average,
879
                'filled': _amount,
880
                'remaining': 0.0,
881
                'status': "closed",
882
                'cost': (dry_order['amount'] * average)
883
            })
884
            # market orders will always incurr taker fees
885
            dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
1✔
886

887
        dry_order = self.check_dry_limit_order_filled(
1✔
888
            dry_order, immediate=True, orderbook=orderbook)
889

890
        self._dry_run_open_orders[dry_order["id"]] = dry_order
1✔
891
        # Copy order and close it - so the returned order is open unless it's a market order
892
        return dry_order
1✔
893

894
    def add_dry_order_fee(
1✔
895
        self,
896
        pair: str,
897
        dry_order: Dict[str, Any],
898
        taker_or_maker: MakerTaker,
899
    ) -> Dict[str, Any]:
900
        fee = self.get_fee(pair, taker_or_maker=taker_or_maker)
1✔
901
        dry_order.update({
1✔
902
            'fee': {
903
                'currency': self.get_pair_quote_currency(pair),
904
                'cost': dry_order['cost'] * fee,
905
                'rate': fee
906
            }
907
        })
908
        return dry_order
1✔
909

910
    def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float,
1✔
911
                                  orderbook: Optional[OrderBook]) -> float:
912
        """
913
        Get the market order fill price based on orderbook interpolation
914
        """
915
        if self.exchange_has('fetchL2OrderBook'):
1✔
916
            if not orderbook:
1✔
917
                orderbook = self.fetch_l2_order_book(pair, 20)
×
918
            ob_type: OBLiteral = 'asks' if side == 'buy' else 'bids'
1✔
919
            slippage = 0.05
1✔
920
            max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
1✔
921

922
            remaining_amount = amount
1✔
923
            filled_value = 0.0
1✔
924
            book_entry_price = 0.0
1✔
925
            for book_entry in orderbook[ob_type]:
1✔
926
                book_entry_price = book_entry[0]
1✔
927
                book_entry_coin_volume = book_entry[1]
1✔
928
                if remaining_amount > 0:
1✔
929
                    if remaining_amount < book_entry_coin_volume:
1✔
930
                        # Orderbook at this slot bigger than remaining amount
931
                        filled_value += remaining_amount * book_entry_price
1✔
932
                        break
1✔
933
                    else:
934
                        filled_value += book_entry_coin_volume * book_entry_price
1✔
935
                    remaining_amount -= book_entry_coin_volume
1✔
936
                else:
937
                    break
×
938
            else:
939
                # If remaining_amount wasn't consumed completely (break was not called)
940
                filled_value += remaining_amount * book_entry_price
1✔
941
            forecast_avg_filled_price = max(filled_value, 0) / amount
1✔
942
            # Limit max. slippage to specified value
943
            if side == 'buy':
1✔
944
                forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
1✔
945

946
            else:
947
                forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
1✔
948

949
            return self.price_to_precision(pair, forecast_avg_filled_price)
1✔
950

951
        return rate
1✔
952

953
    def _dry_is_price_crossed(self, pair: str, side: str, limit: float,
1✔
954
                              orderbook: Optional[OrderBook] = None, offset: float = 0.0) -> bool:
955
        if not self.exchange_has('fetchL2OrderBook'):
1✔
956
            return True
1✔
957
        if not orderbook:
1✔
958
            orderbook = self.fetch_l2_order_book(pair, 1)
1✔
959
        try:
1✔
960
            if side == 'buy':
1✔
961
                price = orderbook['asks'][0][0]
1✔
962
                if limit * (1 - offset) >= price:
1✔
963
                    return True
1✔
964
            else:
965
                price = orderbook['bids'][0][0]
1✔
966
                if limit * (1 + offset) <= price:
1✔
967
                    return True
1✔
968
        except IndexError:
1✔
969
            # Ignore empty orderbooks when filling - can be filled with the next iteration.
970
            pass
1✔
971
        return False
1✔
972

973
    def check_dry_limit_order_filled(
1✔
974
            self, order: Dict[str, Any], immediate: bool = False,
975
            orderbook: Optional[OrderBook] = None) -> Dict[str, Any]:
976
        """
977
        Check dry-run limit order fill and update fee (if it filled).
978
        """
979
        if (order['status'] != "closed"
1✔
980
                and order['type'] in ["limit"]
981
                and not order.get('ft_order_type')):
982
            pair = order['symbol']
1✔
983
            if self._dry_is_price_crossed(pair, order['side'], order['price'], orderbook):
1✔
984
                order.update({
1✔
985
                    'status': 'closed',
986
                    'filled': order['amount'],
987
                    'remaining': 0,
988
                })
989

990
                self.add_dry_order_fee(
1✔
991
                    pair,
992
                    order,
993
                    'taker' if immediate else 'maker',
994
                )
995

996
        return order
1✔
997

998
    def fetch_dry_run_order(self, order_id) -> Dict[str, Any]:
1✔
999
        """
1000
        Return dry-run order
1001
        Only call if running in dry-run mode.
1002
        """
1003
        try:
1✔
1004
            order = self._dry_run_open_orders[order_id]
1✔
1005
            order = self.check_dry_limit_order_filled(order)
1✔
1006
            return order
1✔
1007
        except KeyError as e:
1✔
1008
            from freqtrade.persistence import Order
1✔
1009
            order = Order.order_by_id(order_id)
1✔
1010
            if order:
1✔
1011
                ccxt_order = order.to_ccxt_object(self._ft_has['stop_price_param'])
1✔
1012
                self._dry_run_open_orders[order_id] = ccxt_order
1✔
1013
                return ccxt_order
1✔
1014
            # Gracefully handle errors with dry-run orders.
1015
            raise InvalidOrderException(
1✔
1016
                f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
1017

1018
    # Order handling
1019

1020
    def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
1✔
1021
        if self.trading_mode != TradingMode.SPOT:
1✔
1022
            self.set_margin_mode(pair, self.margin_mode, accept_fail)
1✔
1023
            self._set_leverage(leverage, pair, accept_fail)
1✔
1024

1025
    def _get_params(
1✔
1026
        self,
1027
        side: BuySell,
1028
        ordertype: str,
1029
        leverage: float,
1030
        reduceOnly: bool,
1031
        time_in_force: str = 'GTC',
1032
    ) -> Dict:
1033
        params = self._params.copy()
1✔
1034
        if time_in_force != 'GTC' and ordertype != 'market':
1✔
1035
            params.update({'timeInForce': time_in_force.upper()})
1✔
1036
        if reduceOnly:
1✔
1037
            params.update({'reduceOnly': True})
1✔
1038
        return params
1✔
1039

1040
    def _order_needs_price(self, ordertype: str) -> bool:
1✔
1041
        return (
1✔
1042
            ordertype != 'market'
1043
            or self._api.options.get("createMarketBuyOrderRequiresPrice", False)
1044
            or self._ft_has.get('marketOrderRequiresPrice', False)
1045
        )
1046

1047
    def create_order(
1✔
1048
        self,
1049
        *,
1050
        pair: str,
1051
        ordertype: str,
1052
        side: BuySell,
1053
        amount: float,
1054
        rate: float,
1055
        leverage: float,
1056
        reduceOnly: bool = False,
1057
        time_in_force: str = 'GTC',
1058
    ) -> Dict:
1059
        if self._config['dry_run']:
1✔
1060
            dry_order = self.create_dry_run_order(
1✔
1061
                pair, ordertype, side, amount, self.price_to_precision(pair, rate), leverage)
1062
            return dry_order
1✔
1063

1064
        params = self._get_params(side, ordertype, leverage, reduceOnly, time_in_force)
1✔
1065

1066
        try:
1✔
1067
            # Set the precision for amount and price(rate) as accepted by the exchange
1068
            amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
1✔
1069
            needs_price = self._order_needs_price(ordertype)
1✔
1070
            rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
1✔
1071

1072
            if not reduceOnly:
1✔
1073
                self._lev_prep(pair, leverage, side)
1✔
1074

1075
            order = self._api.create_order(
1✔
1076
                pair,
1077
                ordertype,
1078
                side,
1079
                amount,
1080
                rate_for_order,
1081
                params,
1082
            )
1083
            self._log_exchange_response('create_order', order)
1✔
1084
            order = self._order_contracts_to_amount(order)
1✔
1085
            return order
1✔
1086

1087
        except ccxt.InsufficientFunds as e:
1✔
1088
            raise InsufficientFundsError(
1✔
1089
                f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
1090
                f'Tried to {side} amount {amount} at rate {rate}.'
1091
                f'Message: {e}') from e
1092
        except ccxt.InvalidOrder as e:
1✔
1093
            raise InvalidOrderException(
1✔
1094
                f'Could not create {ordertype} {side} order on market {pair}. '
1095
                f'Tried to {side} amount {amount} at rate {rate}. '
1096
                f'Message: {e}') from e
1097
        except ccxt.DDoSProtection as e:
1✔
1098
            raise DDosProtection(e) from e
×
1099
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1100
            raise TemporaryError(
1✔
1101
                f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
1102
        except ccxt.BaseError as e:
1✔
1103
            raise OperationalException(e) from e
1✔
1104

1105
    def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
1✔
1106
        """
1107
        Verify stop_loss against stoploss-order value (limit or price)
1108
        Returns True if adjustment is necessary.
1109
        """
1110
        if not self._ft_has.get('stoploss_on_exchange'):
1✔
1111
            raise OperationalException(f"stoploss is not implemented for {self.name}.")
1✔
1112
        price_param = self._ft_has['stop_price_param']
1✔
1113
        return (
1✔
1114
            order.get(price_param, None) is None
1115
            or ((side == "sell" and stop_loss > float(order[price_param])) or
1116
                (side == "buy" and stop_loss < float(order[price_param])))
1117
        )
1118

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

1121
        available_order_Types: Dict[str, str] = self._ft_has["stoploss_order_types"]
1✔
1122

1123
        if user_order_type in available_order_Types.keys():
1✔
1124
            ordertype = available_order_Types[user_order_type]
1✔
1125
        else:
1126
            # Otherwise pick only one available
1127
            ordertype = list(available_order_Types.values())[0]
1✔
1128
            user_order_type = list(available_order_Types.keys())[0]
1✔
1129
        return ordertype, user_order_type
1✔
1130

1131
    def _get_stop_limit_rate(self, stop_price: float, order_types: Dict, side: str) -> float:
1✔
1132
        # Limit price threshold: As limit price should always be below stop-price
1133
        limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
1✔
1134
        if side == "sell":
1✔
1135
            limit_rate = stop_price * limit_price_pct
1✔
1136
        else:
1137
            limit_rate = stop_price * (2 - limit_price_pct)
1✔
1138

1139
        bad_stop_price = ((stop_price < limit_rate) if side ==
1✔
1140
                          "sell" else (stop_price > limit_rate))
1141
        # Ensure rate is less than stop price
1142
        if bad_stop_price:
1✔
1143
            # This can for example happen if the stop / liquidation price is set to 0
1144
            # Which is possible if a market-order closes right away.
1145
            # The InvalidOrderException will bubble up to exit_positions, where it will be
1146
            # handled gracefully.
1147
            raise InvalidOrderException(
1✔
1148
                "In stoploss limit order, stop price should be more than limit price. "
1149
                f"Stop price: {stop_price}, Limit price: {limit_rate}, "
1150
                f"Limit Price pct: {limit_price_pct}"
1151
                )
1152
        return limit_rate
1✔
1153

1154
    def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
1✔
1155
        params = self._params.copy()
1✔
1156
        # Verify if stopPrice works for your exchange, else configure stop_price_param
1157
        params.update({self._ft_has['stop_price_param']: stop_price})
1✔
1158
        return params
1✔
1159

1160
    @retrier(retries=0)
1✔
1161
    def create_stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict,
1✔
1162
                        side: BuySell, leverage: float) -> Dict:
1163
        """
1164
        creates a stoploss order.
1165
        requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
1166
            to the corresponding exchange type.
1167

1168
        The precise ordertype is determined by the order_types dict or exchange default.
1169

1170
        The exception below should never raise, since we disallow
1171
        starting the bot in validate_ordertypes()
1172

1173
        This may work with a limited number of other exchanges, but correct working
1174
            needs to be tested individually.
1175
        WARNING: setting `stoploss_on_exchange` to True will NOT auto-enable stoploss on exchange.
1176
            `stoploss_adjust` must still be implemented for this to work.
1177
        """
1178
        if not self._ft_has['stoploss_on_exchange']:
1✔
1179
            raise OperationalException(f"stoploss is not implemented for {self.name}.")
1✔
1180

1181
        user_order_type = order_types.get('stoploss', 'market')
1✔
1182
        ordertype, user_order_type = self._get_stop_order_type(user_order_type)
1✔
1183
        round_mode = ROUND_DOWN if side == 'buy' else ROUND_UP
1✔
1184
        stop_price_norm = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
1✔
1185
        limit_rate = None
1✔
1186
        if user_order_type == 'limit':
1✔
1187
            limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
1✔
1188
            limit_rate = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
1✔
1189

1190
        if self._config['dry_run']:
1✔
1191
            dry_order = self.create_dry_run_order(
1✔
1192
                pair,
1193
                ordertype,
1194
                side,
1195
                amount,
1196
                stop_price_norm,
1197
                stop_loss=True,
1198
                leverage=leverage,
1199
            )
1200
            return dry_order
1✔
1201

1202
        try:
1✔
1203
            params = self._get_stop_params(side=side, ordertype=ordertype,
1✔
1204
                                           stop_price=stop_price_norm)
1205
            if self.trading_mode == TradingMode.FUTURES:
1✔
1206
                params['reduceOnly'] = True
1✔
1207
                if 'stoploss_price_type' in order_types and 'stop_price_type_field' in self._ft_has:
1✔
1208
                    price_type = self._ft_has['stop_price_type_value_mapping'][
1✔
1209
                        order_types.get('stoploss_price_type', PriceType.LAST)]
1210
                    params[self._ft_has['stop_price_type_field']] = price_type
1✔
1211

1212
            amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
1✔
1213

1214
            self._lev_prep(pair, leverage, side, accept_fail=True)
1✔
1215
            order = self._api.create_order(symbol=pair, type=ordertype, side=side,
1✔
1216
                                           amount=amount, price=limit_rate, params=params)
1217
            self._log_exchange_response('create_stoploss_order', order)
1✔
1218
            order = self._order_contracts_to_amount(order)
1✔
1219
            logger.info(f"stoploss {user_order_type} order added for {pair}. "
1✔
1220
                        f"stop price: {stop_price}. limit: {limit_rate}")
1221
            return order
1✔
1222
        except ccxt.InsufficientFunds as e:
1✔
1223
            raise InsufficientFundsError(
1✔
1224
                f'Insufficient funds to create {ordertype} sell order on market {pair}. '
1225
                f'Tried to sell amount {amount} at rate {limit_rate}. '
1226
                f'Message: {e}') from e
1227
        except ccxt.InvalidOrder as e:
1✔
1228
            # Errors:
1229
            # `Order would trigger immediately.`
1230
            raise InvalidOrderException(
1✔
1231
                f'Could not create {ordertype} sell order on market {pair}. '
1232
                f'Tried to sell amount {amount} at rate {limit_rate}. '
1233
                f'Message: {e}') from e
1234
        except ccxt.DDoSProtection as e:
1✔
1235
            raise DDosProtection(e) from e
1✔
1236
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1237
            raise TemporaryError(
1✔
1238
                f"Could not place stoploss order due to {e.__class__.__name__}. "
1239
                f"Message: {e}") from e
1240
        except ccxt.BaseError as e:
1✔
1241
            raise OperationalException(e) from e
1✔
1242

1243
    @retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
1✔
1244
    def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
1✔
1245
        if self._config['dry_run']:
1✔
1246
            return self.fetch_dry_run_order(order_id)
1✔
1247
        try:
1✔
1248
            order = self._api.fetch_order(order_id, pair, params=params)
1✔
1249
            self._log_exchange_response('fetch_order', order)
1✔
1250
            order = self._order_contracts_to_amount(order)
1✔
1251
            return order
1✔
1252
        except ccxt.OrderNotFound as e:
1✔
1253
            raise RetryableOrderError(
1✔
1254
                f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
1255
        except ccxt.InvalidOrder as e:
1✔
1256
            raise InvalidOrderException(
1✔
1257
                f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e
1258
        except ccxt.DDoSProtection as e:
1✔
1259
            raise DDosProtection(e) from e
1✔
1260
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1261
            raise TemporaryError(
1✔
1262
                f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
1263
        except ccxt.BaseError as e:
1✔
1264
            raise OperationalException(e) from e
1✔
1265

1266
    def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
1✔
1267
        return self.fetch_order(order_id, pair, params)
1✔
1268

1269
    def fetch_order_or_stoploss_order(self, order_id: str, pair: str,
1✔
1270
                                      stoploss_order: bool = False) -> Dict:
1271
        """
1272
        Simple wrapper calling either fetch_order or fetch_stoploss_order depending on
1273
        the stoploss_order parameter
1274
        :param order_id: OrderId to fetch order
1275
        :param pair: Pair corresponding to order_id
1276
        :param stoploss_order: If true, uses fetch_stoploss_order, otherwise fetch_order.
1277
        """
1278
        if stoploss_order:
1✔
1279
            return self.fetch_stoploss_order(order_id, pair)
1✔
1280
        return self.fetch_order(order_id, pair)
1✔
1281

1282
    def check_order_canceled_empty(self, order: Dict) -> bool:
1✔
1283
        """
1284
        Verify if an order has been cancelled without being partially filled
1285
        :param order: Order dict as returned from fetch_order()
1286
        :return: True if order has been cancelled without being filled, False otherwise.
1287
        """
1288
        return (order.get('status') in NON_OPEN_EXCHANGE_STATES
1✔
1289
                and order.get('filled') == 0.0)
1290

1291
    @retrier
1✔
1292
    def cancel_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
1✔
1293
        if self._config['dry_run']:
1✔
1294
            try:
1✔
1295
                order = self.fetch_dry_run_order(order_id)
1✔
1296

1297
                order.update({'status': 'canceled', 'filled': 0.0, 'remaining': order['amount']})
1✔
1298
                return order
1✔
1299
            except InvalidOrderException:
1✔
1300
                return {}
1✔
1301

1302
        try:
1✔
1303
            order = self._api.cancel_order(order_id, pair, params=params)
1✔
1304
            self._log_exchange_response('cancel_order', order)
1✔
1305
            order = self._order_contracts_to_amount(order)
1✔
1306
            return order
1✔
1307
        except ccxt.InvalidOrder as e:
1✔
1308
            raise InvalidOrderException(
1✔
1309
                f'Could not cancel order. Message: {e}') from e
1310
        except ccxt.DDoSProtection as e:
1✔
1311
            raise DDosProtection(e) from e
1✔
1312
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1313
            raise TemporaryError(
1✔
1314
                f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
1315
        except ccxt.BaseError as e:
1✔
1316
            raise OperationalException(e) from e
1✔
1317

1318
    def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
1✔
1319
        return self.cancel_order(order_id, pair, params)
1✔
1320

1321
    def is_cancel_order_result_suitable(self, corder) -> bool:
1✔
1322
        if not isinstance(corder, dict):
1✔
1323
            return False
1✔
1324

1325
        required = ('fee', 'status', 'amount')
1✔
1326
        return all(corder.get(k, None) is not None for k in required)
1✔
1327

1328
    def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
1✔
1329
        """
1330
        Cancel order returning a result.
1331
        Creates a fake result if cancel order returns a non-usable result
1332
        and fetch_order does not work (certain exchanges don't return cancelled orders)
1333
        :param order_id: Orderid to cancel
1334
        :param pair: Pair corresponding to order_id
1335
        :param amount: Amount to use for fake response
1336
        :return: Result from either cancel_order if usable, or fetch_order
1337
        """
1338
        try:
1✔
1339
            corder = self.cancel_order(order_id, pair)
1✔
1340
            if self.is_cancel_order_result_suitable(corder):
1✔
1341
                return corder
1✔
1342
        except InvalidOrderException:
1✔
1343
            logger.warning(f"Could not cancel order {order_id} for {pair}.")
1✔
1344
        try:
1✔
1345
            order = self.fetch_order(order_id, pair)
1✔
1346
        except InvalidOrderException:
1✔
1347
            logger.warning(f"Could not fetch cancelled order {order_id}.")
1✔
1348
            order = {
1✔
1349
                'id': order_id,
1350
                'status': 'canceled',
1351
                'amount': amount,
1352
                'filled': 0.0,
1353
                'fee': {},
1354
                'info': {}
1355
            }
1356

1357
        return order
1✔
1358

1359
    def cancel_stoploss_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
1✔
1360
        """
1361
        Cancel stoploss order returning a result.
1362
        Creates a fake result if cancel order returns a non-usable result
1363
        and fetch_order does not work (certain exchanges don't return cancelled orders)
1364
        :param order_id: stoploss-order-id to cancel
1365
        :param pair: Pair corresponding to order_id
1366
        :param amount: Amount to use for fake response
1367
        :return: Result from either cancel_order if usable, or fetch_order
1368
        """
1369
        corder = self.cancel_stoploss_order(order_id, pair)
1✔
1370
        if self.is_cancel_order_result_suitable(corder):
1✔
1371
            return corder
1✔
1372
        try:
1✔
1373
            order = self.fetch_stoploss_order(order_id, pair)
1✔
1374
        except InvalidOrderException:
1✔
1375
            logger.warning(f"Could not fetch cancelled stoploss order {order_id}.")
1✔
1376
            order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
1✔
1377

1378
        return order
1✔
1379

1380
    @retrier
1✔
1381
    def get_balances(self) -> dict:
1✔
1382

1383
        try:
1✔
1384
            balances = self._api.fetch_balance()
1✔
1385
            # Remove additional info from ccxt results
1386
            balances.pop("info", None)
1✔
1387
            balances.pop("free", None)
1✔
1388
            balances.pop("total", None)
1✔
1389
            balances.pop("used", None)
1✔
1390

1391
            return balances
1✔
1392
        except ccxt.DDoSProtection as e:
1✔
1393
            raise DDosProtection(e) from e
1✔
1394
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1395
            raise TemporaryError(
1✔
1396
                f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
1397
        except ccxt.BaseError as e:
1✔
1398
            raise OperationalException(e) from e
1✔
1399

1400
    @retrier
1✔
1401
    def fetch_positions(self, pair: Optional[str] = None) -> List[Dict]:
1✔
1402
        """
1403
        Fetch positions from the exchange.
1404
        If no pair is given, all positions are returned.
1405
        :param pair: Pair for the query
1406
        """
1407
        if self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES:
1✔
1408
            return []
1✔
1409
        try:
1✔
1410
            symbols = []
1✔
1411
            if pair:
1✔
1412
                symbols.append(pair)
1✔
1413
            positions: List[Dict] = self._api.fetch_positions(symbols)
1✔
1414
            self._log_exchange_response('fetch_positions', positions)
1✔
1415
            return positions
1✔
1416
        except ccxt.DDoSProtection as e:
1✔
1417
            raise DDosProtection(e) from e
1✔
1418
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1419
            raise TemporaryError(
1✔
1420
                f'Could not get positions due to {e.__class__.__name__}. Message: {e}') from e
1421
        except ccxt.BaseError as e:
1✔
1422
            raise OperationalException(e) from e
1✔
1423

1424
    def __fetch_orders_emulate(self, pair: str, since_ms: int) -> List[Dict]:
1✔
1425
        orders = []
1✔
1426
        if self.exchange_has('fetchClosedOrders'):
1✔
1427
            orders = self._api.fetch_closed_orders(pair, since=since_ms)
1✔
1428
            if self.exchange_has('fetchOpenOrders'):
1✔
1429
                orders_open = self._api.fetch_open_orders(pair, since=since_ms)
1✔
1430
                orders.extend(orders_open)
1✔
1431
        return orders
1✔
1432

1433
    @retrier(retries=0)
1✔
1434
    def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None) -> List[Dict]:
1✔
1435
        """
1436
        Fetch all orders for a pair "since"
1437
        :param pair: Pair for the query
1438
        :param since: Starting time for the query
1439
        """
1440
        if self._config['dry_run']:
1✔
1441
            return []
1✔
1442

1443
        try:
1✔
1444
            since_ms = int((since.timestamp() - 10) * 1000)
1✔
1445

1446
            if self.exchange_has('fetchOrders'):
1✔
1447
                if not params:
1✔
1448
                    params = {}
1✔
1449
                try:
1✔
1450
                    orders: List[Dict] = self._api.fetch_orders(pair, since=since_ms, params=params)
1✔
1451
                except ccxt.NotSupported:
1✔
1452
                    # Some exchanges don't support fetchOrders
1453
                    # attempt to fetch open and closed orders separately
1454
                    orders = self.__fetch_orders_emulate(pair, since_ms)
1✔
1455
            else:
1456
                orders = self.__fetch_orders_emulate(pair, since_ms)
1✔
1457
            self._log_exchange_response('fetch_orders', orders)
1✔
1458
            orders = [self._order_contracts_to_amount(o) for o in orders]
1✔
1459
            return orders
1✔
1460
        except ccxt.DDoSProtection as e:
1✔
1461
            raise DDosProtection(e) from e
1✔
1462
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1463
            raise TemporaryError(
1✔
1464
                f'Could not fetch positions due to {e.__class__.__name__}. Message: {e}') from e
1465
        except ccxt.BaseError as e:
1✔
1466
            raise OperationalException(e) from e
1✔
1467

1468
    @retrier
1✔
1469
    def fetch_trading_fees(self) -> Dict[str, Any]:
1✔
1470
        """
1471
        Fetch user account trading fees
1472
        Can be cached, should not update often.
1473
        """
1474
        if (self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES
1✔
1475
                or not self.exchange_has('fetchTradingFees')):
1476
            return {}
1✔
1477
        try:
1✔
1478
            trading_fees: Dict[str, Any] = self._api.fetch_trading_fees()
1✔
1479
            self._log_exchange_response('fetch_trading_fees', trading_fees)
1✔
1480
            return trading_fees
1✔
1481
        except ccxt.DDoSProtection as e:
1✔
1482
            raise DDosProtection(e) from e
1✔
1483
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1484
            raise TemporaryError(
1✔
1485
                f'Could not fetch trading fees due to {e.__class__.__name__}. Message: {e}') from e
1486
        except ccxt.BaseError as e:
1✔
1487
            raise OperationalException(e) from e
1✔
1488

1489
    @retrier
1✔
1490
    def fetch_bids_asks(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
1✔
1491
        """
1492
        :param cached: Allow cached result
1493
        :return: fetch_tickers result
1494
        """
1495
        if not self.exchange_has('fetchBidsAsks'):
1✔
1496
            return {}
1✔
1497
        if cached:
1✔
1498
            with self._cache_lock:
1✔
1499
                tickers = self._fetch_tickers_cache.get('fetch_bids_asks')
1✔
1500
            if tickers:
1✔
1501
                return tickers
1✔
1502
        try:
1✔
1503
            tickers = self._api.fetch_bids_asks(symbols)
1✔
1504
            with self._cache_lock:
1✔
1505
                self._fetch_tickers_cache['fetch_bids_asks'] = tickers
1✔
1506
            return tickers
1✔
1507
        except ccxt.NotSupported as e:
1✔
1508
            raise OperationalException(
1✔
1509
                f'Exchange {self._api.name} does not support fetching bids/asks in batch. '
1510
                f'Message: {e}') from e
1511
        except ccxt.DDoSProtection as e:
1✔
1512
            raise DDosProtection(e) from e
1✔
1513
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1514
            raise TemporaryError(
1✔
1515
                f'Could not load bids/asks due to {e.__class__.__name__}. Message: {e}') from e
1516
        except ccxt.BaseError as e:
1✔
1517
            raise OperationalException(e) from e
1✔
1518

1519
    @retrier
1✔
1520
    def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
1✔
1521
        """
1522
        :param cached: Allow cached result
1523
        :return: fetch_tickers result
1524
        """
1525
        tickers: Tickers
1526
        if not self.exchange_has('fetchTickers'):
1✔
1527
            return {}
1✔
1528
        if cached:
1✔
1529
            with self._cache_lock:
1✔
1530
                tickers = self._fetch_tickers_cache.get('fetch_tickers')  # type: ignore
1✔
1531
            if tickers:
1✔
1532
                return tickers
1✔
1533
        try:
1✔
1534
            tickers = self._api.fetch_tickers(symbols)
1✔
1535
            with self._cache_lock:
1✔
1536
                self._fetch_tickers_cache['fetch_tickers'] = tickers
1✔
1537
            return tickers
1✔
1538
        except ccxt.NotSupported as e:
1✔
1539
            raise OperationalException(
1✔
1540
                f'Exchange {self._api.name} does not support fetching tickers in batch. '
1541
                f'Message: {e}') from e
1542
        except ccxt.DDoSProtection as e:
1✔
1543
            raise DDosProtection(e) from e
1✔
1544
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1545
            raise TemporaryError(
1✔
1546
                f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e
1547
        except ccxt.BaseError as e:
1✔
1548
            raise OperationalException(e) from e
1✔
1549

1550
    # Pricing info
1551

1552
    @retrier
1✔
1553
    def fetch_ticker(self, pair: str) -> Ticker:
1✔
1554
        try:
1✔
1555
            if (pair not in self.markets or
1✔
1556
                    self.markets[pair].get('active', False) is False):
1557
                raise ExchangeError(f"Pair {pair} not available")
1✔
1558
            data: Ticker = self._api.fetch_ticker(pair)
1✔
1559
            return data
1✔
1560
        except ccxt.DDoSProtection as e:
1✔
1561
            raise DDosProtection(e) from e
1✔
1562
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1563
            raise TemporaryError(
1✔
1564
                f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e
1565
        except ccxt.BaseError as e:
1✔
1566
            raise OperationalException(e) from e
1✔
1567

1568
    @staticmethod
1✔
1569
    def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]],
1✔
1570
                               range_required: bool = True):
1571
        """
1572
        Get next greater value in the list.
1573
        Used by fetch_l2_order_book if the api only supports a limited range
1574
        """
1575
        if not limit_range:
1✔
1576
            return limit
1✔
1577

1578
        result = min([x for x in limit_range if limit <= x] + [max(limit_range)])
1✔
1579
        if not range_required and limit > result:
1✔
1580
            # Range is not required - we can use None as parameter.
1581
            return None
1✔
1582
        return result
1✔
1583

1584
    @retrier
1✔
1585
    def fetch_l2_order_book(self, pair: str, limit: int = 100) -> OrderBook:
1✔
1586
        """
1587
        Get L2 order book from exchange.
1588
        Can be limited to a certain amount (if supported).
1589
        Returns a dict in the format
1590
        {'asks': [price, volume], 'bids': [price, volume]}
1591
        """
1592
        limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'],
1✔
1593
                                             self._ft_has['l2_limit_range_required'])
1594
        try:
1✔
1595

1596
            return self._api.fetch_l2_order_book(pair, limit1)
1✔
1597
        except ccxt.NotSupported as e:
1✔
1598
            raise OperationalException(
1✔
1599
                f'Exchange {self._api.name} does not support fetching order book.'
1600
                f'Message: {e}') from e
1601
        except ccxt.DDoSProtection as e:
1✔
1602
            raise DDosProtection(e) from e
×
1603
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1604
            raise TemporaryError(
1✔
1605
                f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e
1606
        except ccxt.BaseError as e:
1✔
1607
            raise OperationalException(e) from e
1✔
1608

1609
    def _get_price_side(self, side: str, is_short: bool, conf_strategy: Dict) -> BidAsk:
1✔
1610
        price_side = conf_strategy['price_side']
1✔
1611

1612
        if price_side in ('same', 'other'):
1✔
1613
            price_map = {
1✔
1614
                ('entry', 'long', 'same'): 'bid',
1615
                ('entry', 'long', 'other'): 'ask',
1616
                ('entry', 'short', 'same'): 'ask',
1617
                ('entry', 'short', 'other'): 'bid',
1618
                ('exit', 'long', 'same'): 'ask',
1619
                ('exit', 'long', 'other'): 'bid',
1620
                ('exit', 'short', 'same'): 'bid',
1621
                ('exit', 'short', 'other'): 'ask',
1622
            }
1623
            price_side = price_map[(side, 'short' if is_short else 'long', price_side)]
1✔
1624
        return price_side
1✔
1625

1626
    def get_rate(self, pair: str, refresh: bool,
1✔
1627
                 side: EntryExit, is_short: bool,
1628
                 order_book: Optional[OrderBook] = None, ticker: Optional[Ticker] = None) -> float:
1629
        """
1630
        Calculates bid/ask target
1631
        bid rate - between current ask price and last price
1632
        ask rate - either using ticker bid or first bid based on orderbook
1633
        or remain static in any other case since it's not updating.
1634
        :param pair: Pair to get rate for
1635
        :param refresh: allow cached data
1636
        :param side: "buy" or "sell"
1637
        :return: float: Price
1638
        :raises PricingError if orderbook price could not be determined.
1639
        """
1640
        name = side.capitalize()
1✔
1641
        strat_name = 'entry_pricing' if side == "entry" else 'exit_pricing'
1✔
1642

1643
        cache_rate: TTLCache = self._entry_rate_cache if side == "entry" else self._exit_rate_cache
1✔
1644
        if not refresh:
1✔
1645
            with self._cache_lock:
1✔
1646
                rate = cache_rate.get(pair)
1✔
1647
            # Check if cache has been invalidated
1648
            if rate:
1✔
1649
                logger.debug(f"Using cached {side} rate for {pair}.")
1✔
1650
                return rate
1✔
1651

1652
        conf_strategy = self._config.get(strat_name, {})
1✔
1653

1654
        price_side = self._get_price_side(side, is_short, conf_strategy)
1✔
1655

1656
        if conf_strategy.get('use_order_book', False):
1✔
1657

1658
            order_book_top = conf_strategy.get('order_book_top', 1)
1✔
1659
            if order_book is None:
1✔
1660
                order_book = self.fetch_l2_order_book(pair, order_book_top)
1✔
1661
            rate = self._get_rate_from_ob(pair, side, order_book, name, price_side,
1✔
1662
                                          order_book_top)
1663
        else:
1664
            logger.debug(f"Using Last {price_side.capitalize()} / Last Price")
1✔
1665
            if ticker is None:
1✔
1666
                ticker = self.fetch_ticker(pair)
1✔
1667
            rate = self._get_rate_from_ticker(side, ticker, conf_strategy, price_side)
1✔
1668

1669
        if rate is None:
1✔
1670
            raise PricingError(f"{name}-Rate for {pair} was empty.")
1✔
1671
        with self._cache_lock:
1✔
1672
            cache_rate[pair] = rate
1✔
1673

1674
        return rate
1✔
1675

1676
    def _get_rate_from_ticker(self, side: EntryExit, ticker: Ticker, conf_strategy: Dict[str, Any],
1✔
1677
                              price_side: BidAsk) -> Optional[float]:
1678
        """
1679
        Get rate from ticker.
1680
        """
1681
        ticker_rate = ticker[price_side]
1✔
1682
        if ticker['last'] and ticker_rate:
1✔
1683
            if side == 'entry' and ticker_rate > ticker['last']:
1✔
1684
                balance = conf_strategy.get('price_last_balance', 0.0)
1✔
1685
                ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
1✔
1686
            elif side == 'exit' and ticker_rate < ticker['last']:
1✔
1687
                balance = conf_strategy.get('price_last_balance', 0.0)
1✔
1688
                ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
1✔
1689
        rate = ticker_rate
1✔
1690
        return rate
1✔
1691

1692
    def _get_rate_from_ob(self, pair: str, side: EntryExit, order_book: OrderBook, name: str,
1✔
1693
                          price_side: BidAsk, order_book_top: int) -> float:
1694
        """
1695
        Get rate from orderbook
1696
        :raises: PricingError if rate could not be determined.
1697
        """
1698
        logger.debug('order_book %s', order_book)
1✔
1699
        # top 1 = index 0
1700
        try:
1✔
1701
            obside: OBLiteral = 'bids' if price_side == 'bid' else 'asks'
1✔
1702
            rate = order_book[obside][order_book_top - 1][0]
1✔
1703
        except (IndexError, KeyError) as e:
1✔
1704
            logger.warning(
1✔
1705
                    f"{pair} - {name} Price at location {order_book_top} from orderbook "
1706
                    f"could not be determined. Orderbook: {order_book}"
1707
                )
1708
            raise PricingError from e
1✔
1709
        logger.debug(f"{pair} - {name} price from orderbook {price_side.capitalize()}"
1✔
1710
                     f"side - top {order_book_top} order book {side} rate {rate:.8f}")
1711
        return rate
1✔
1712

1713
    def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]:
1✔
1714
        entry_rate = None
1✔
1715
        exit_rate = None
1✔
1716
        if not refresh:
1✔
1717
            with self._cache_lock:
1✔
1718
                entry_rate = self._entry_rate_cache.get(pair)
1✔
1719
                exit_rate = self._exit_rate_cache.get(pair)
1✔
1720
            if entry_rate:
1✔
1721
                logger.debug(f"Using cached buy rate for {pair}.")
1✔
1722
            if exit_rate:
1✔
1723
                logger.debug(f"Using cached sell rate for {pair}.")
1✔
1724

1725
        entry_pricing = self._config.get('entry_pricing', {})
1✔
1726
        exit_pricing = self._config.get('exit_pricing', {})
1✔
1727
        order_book = ticker = None
1✔
1728
        if not entry_rate and entry_pricing.get('use_order_book', False):
1✔
1729
            order_book_top = max(entry_pricing.get('order_book_top', 1),
1✔
1730
                                 exit_pricing.get('order_book_top', 1))
1731
            order_book = self.fetch_l2_order_book(pair, order_book_top)
1✔
1732
            entry_rate = self.get_rate(pair, refresh, 'entry', is_short, order_book=order_book)
1✔
1733
        elif not entry_rate:
1✔
1734
            ticker = self.fetch_ticker(pair)
1✔
1735
            entry_rate = self.get_rate(pair, refresh, 'entry', is_short, ticker=ticker)
1✔
1736
        if not exit_rate:
1✔
1737
            exit_rate = self.get_rate(pair, refresh, 'exit',
1✔
1738
                                      is_short, order_book=order_book, ticker=ticker)
1739
        return entry_rate, exit_rate
1✔
1740

1741
    # Fee handling
1742

1743
    @retrier
1✔
1744
    def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
1✔
1745
                             params: Optional[Dict] = None) -> List:
1746
        """
1747
        Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
1748
        The "since" argument passed in is coming from the database and is in UTC,
1749
        as timezone-native datetime object.
1750
        From the python documentation:
1751
            > Naive datetime instances are assumed to represent local time
1752
        Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the
1753
        transformation from local timezone to UTC.
1754
        This works for timezones UTC+ since then the result will contain trades from a few hours
1755
        instead of from the last 5 seconds, however fails for UTC- timezones,
1756
        since we're then asking for trades with a "since" argument in the future.
1757

1758
        :param order_id order_id: Order-id as given when creating the order
1759
        :param pair: Pair the order is for
1760
        :param since: datetime object of the order creation time. Assumes object is in UTC.
1761
        """
1762
        if self._config['dry_run']:
1✔
1763
            return []
1✔
1764
        if not self.exchange_has('fetchMyTrades'):
1✔
1765
            return []
1✔
1766
        try:
1✔
1767
            # Allow 5s offset to catch slight time offsets (discovered in #1185)
1768
            # since needs to be int in milliseconds
1769
            _params = params if params else {}
1✔
1770
            my_trades = self._api.fetch_my_trades(
1✔
1771
                pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000),
1772
                params=_params)
1773
            matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
1✔
1774

1775
            self._log_exchange_response('get_trades_for_order', matched_trades)
1✔
1776

1777
            matched_trades = self._trades_contracts_to_amount(matched_trades)
1✔
1778

1779
            return matched_trades
1✔
1780
        except ccxt.DDoSProtection as e:
1✔
1781
            raise DDosProtection(e) from e
1✔
1782
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1783
            raise TemporaryError(
1✔
1784
                f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e
1785
        except ccxt.BaseError as e:
1✔
1786
            raise OperationalException(e) from e
1✔
1787

1788
    def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
1✔
1789
        return order['id']
1✔
1790

1791
    @retrier
1✔
1792
    def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
1✔
1793
                price: float = 1, taker_or_maker: MakerTaker = 'maker') -> float:
1794
        """
1795
        Retrieve fee from exchange
1796
        :param symbol: Pair
1797
        :param type: Type of order (market, limit, ...)
1798
        :param side: Side of order (buy, sell)
1799
        :param amount: Amount of order
1800
        :param price: Price of order
1801
        :param taker_or_maker: 'maker' or 'taker' (ignored if "type" is provided)
1802
        """
1803
        if type and type == 'market':
1✔
1804
            taker_or_maker = 'taker'
×
1805
        try:
1✔
1806
            if self._config['dry_run'] and self._config.get('fee', None) is not None:
1✔
1807
                return self._config['fee']
1✔
1808
            # validate that markets are loaded before trying to get fee
1809
            if self._api.markets is None or len(self._api.markets) == 0:
1✔
1810
                self._api.load_markets(params={})
1✔
1811

1812
            return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
1✔
1813
                                           price=price, takerOrMaker=taker_or_maker)['rate']
1814
        except ccxt.DDoSProtection as e:
1✔
1815
            raise DDosProtection(e) from e
1✔
1816
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1817
            raise TemporaryError(
1✔
1818
                f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e
1819
        except ccxt.BaseError as e:
1✔
1820
            raise OperationalException(e) from e
1✔
1821

1822
    @staticmethod
1✔
1823
    def order_has_fee(order: Dict) -> bool:
1✔
1824
        """
1825
        Verifies if the passed in order dict has the needed keys to extract fees,
1826
        and that these keys (currency, cost) are not empty.
1827
        :param order: Order or trade (one trade) dict
1828
        :return: True if the fee substructure contains currency and cost, false otherwise
1829
        """
1830
        if not isinstance(order, dict):
1✔
1831
            return False
1✔
1832
        return ('fee' in order and order['fee'] is not None
1✔
1833
                and (order['fee'].keys() >= {'currency', 'cost'})
1834
                and order['fee']['currency'] is not None
1835
                and order['fee']['cost'] is not None
1836
                )
1837

1838
    def calculate_fee_rate(
1✔
1839
            self, fee: Dict, symbol: str, cost: float, amount: float) -> Optional[float]:
1840
        """
1841
        Calculate fee rate if it's not given by the exchange.
1842
        :param fee: ccxt Fee dict - must contain cost / currency / rate
1843
        :param symbol: Symbol of the order
1844
        :param cost: Total cost of the order
1845
        :param amount: Amount of the order
1846
        """
1847
        if fee.get('rate') is not None:
1✔
1848
            return fee.get('rate')
1✔
1849
        fee_curr = fee.get('currency')
1✔
1850
        if fee_curr is None:
1✔
1851
            return None
1✔
1852
        fee_cost = float(fee['cost'])
1✔
1853

1854
        # Calculate fee based on order details
1855
        if fee_curr == self.get_pair_base_currency(symbol):
1✔
1856
            # Base currency - divide by amount
1857
            return round(fee_cost / amount, 8)
1✔
1858
        elif fee_curr == self.get_pair_quote_currency(symbol):
1✔
1859
            # Quote currency - divide by cost
1860
            return round(fee_cost / cost, 8) if cost else None
1✔
1861
        else:
1862
            # If Fee currency is a different currency
1863
            if not cost:
1✔
1864
                # If cost is None or 0.0 -> falsy, return None
1865
                return None
1✔
1866
            try:
1✔
1867
                comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency'])
1✔
1868
                tick = self.fetch_ticker(comb)
1✔
1869

1870
                fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
1✔
1871
            except (ValueError, ExchangeError):
1✔
1872
                fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
1✔
1873
                if not fee_to_quote_rate:
1✔
1874
                    return None
1✔
1875
            return round((fee_cost * fee_to_quote_rate) / cost, 8)
1✔
1876

1877
    def extract_cost_curr_rate(self, fee: Dict, symbol: str, cost: float,
1✔
1878
                               amount: float) -> Tuple[float, str, Optional[float]]:
1879
        """
1880
        Extract tuple of cost, currency, rate.
1881
        Requires order_has_fee to run first!
1882
        :param fee: ccxt Fee dict - must contain cost / currency / rate
1883
        :param symbol: Symbol of the order
1884
        :param cost: Total cost of the order
1885
        :param amount: Amount of the order
1886
        :return: Tuple with cost, currency, rate of the given fee dict
1887
        """
1888
        return (float(fee['cost']),
1✔
1889
                fee['currency'],
1890
                self.calculate_fee_rate(
1891
                    fee,
1892
                    symbol,
1893
                    cost,
1894
                    amount
1895
                    )
1896
                )
1897

1898
    # Historic data
1899

1900
    def get_historic_ohlcv(self, pair: str, timeframe: str,
1✔
1901
                           since_ms: int, candle_type: CandleType,
1902
                           is_new_pair: bool = False,
1903
                           until_ms: Optional[int] = None) -> List:
1904
        """
1905
        Get candle history using asyncio and returns the list of candles.
1906
        Handles all async work for this.
1907
        Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call.
1908
        :param pair: Pair to download
1909
        :param timeframe: Timeframe to get data for
1910
        :param since_ms: Timestamp in milliseconds to get history from
1911
        :param until_ms: Timestamp in milliseconds to get history up to
1912
        :param candle_type: '', mark, index, premiumIndex, or funding_rate
1913
        :return: List with candle (OHLCV) data
1914
        """
1915
        pair, _, _, data, _ = self.loop.run_until_complete(
1✔
1916
            self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
1917
                                           since_ms=since_ms, until_ms=until_ms,
1918
                                           is_new_pair=is_new_pair, candle_type=candle_type))
1919
        logger.info(f"Downloaded data for {pair} with length {len(data)}.")
1✔
1920
        return data
1✔
1921

1922
    async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
1✔
1923
                                        since_ms: int, candle_type: CandleType,
1924
                                        is_new_pair: bool = False, raise_: bool = False,
1925
                                        until_ms: Optional[int] = None
1926
                                        ) -> OHLCVResponse:
1927
        """
1928
        Download historic ohlcv
1929
        :param is_new_pair: used by binance subclass to allow "fast" new pair downloading
1930
        :param candle_type: Any of the enum CandleType (must match trading mode!)
1931
        """
1932

1933
        one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
1✔
1934
            timeframe, candle_type, since_ms)
1935
        logger.debug(
1✔
1936
            "one_call: %s msecs (%s)",
1937
            one_call,
1938
            dt_humanize(dt_now() - timedelta(milliseconds=one_call), only_distance=True)
1939
        )
1940
        input_coroutines = [self._async_get_candle_history(
1✔
1941
            pair, timeframe, candle_type, since) for since in
1942
            range(since_ms, until_ms or dt_ts(), one_call)]
1943

1944
        data: List = []
1✔
1945
        # Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
1946
        for input_coro in chunks(input_coroutines, 100):
1✔
1947

1948
            results = await asyncio.gather(*input_coro, return_exceptions=True)
1✔
1949
            for res in results:
1✔
1950
                if isinstance(res, Exception):
1✔
1951
                    logger.warning(f"Async code raised an exception: {repr(res)}")
1✔
1952
                    if raise_:
1✔
1953
                        raise
1✔
1954
                    continue
1✔
1955
                else:
1956
                    # Deconstruct tuple if it's not an exception
1957
                    p, _, c, new_data, _ = res
1✔
1958
                    if p == pair and c == candle_type:
1✔
1959
                        data.extend(new_data)
1✔
1960
        # Sort data again after extending the result - above calls return in "async order"
1961
        data = sorted(data, key=lambda x: x[0])
1✔
1962
        return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
1✔
1963

1964
    def _build_coroutine(
1✔
1965
            self, pair: str, timeframe: str, candle_type: CandleType,
1966
            since_ms: Optional[int], cache: bool) -> Coroutine[Any, Any, OHLCVResponse]:
1967
        not_all_data = cache and self.required_candle_call_count > 1
1✔
1968
        if cache and (pair, timeframe, candle_type) in self._klines:
1✔
1969
            candle_limit = self.ohlcv_candle_limit(timeframe, candle_type)
1✔
1970
            min_date = date_minus_candles(timeframe, candle_limit - 5).timestamp()
1✔
1971
            # Check if 1 call can get us updated candles without hole in the data.
1972
            if min_date < self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0):
1✔
1973
                # Cache can be used - do one-off call.
1974
                not_all_data = False
1✔
1975
            else:
1976
                # Time jump detected, evict cache
1977
                logger.info(
1✔
1978
                    f"Time jump detected. Evicting cache for {pair}, {timeframe}, {candle_type}")
1979
                del self._klines[(pair, timeframe, candle_type)]
1✔
1980

1981
        if (not since_ms and (self._ft_has["ohlcv_require_since"] or not_all_data)):
1✔
1982
            # Multiple calls for one pair - to get more history
1983
            one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
1✔
1984
                timeframe, candle_type, since_ms)
1985
            move_to = one_call * self.required_candle_call_count
1✔
1986
            now = timeframe_to_next_date(timeframe)
1✔
1987
            since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
1✔
1988

1989
        if since_ms:
1✔
1990
            return self._async_get_historic_ohlcv(
1✔
1991
                pair, timeframe, since_ms=since_ms, raise_=True, candle_type=candle_type)
1992
        else:
1993
            # One call ... "regular" refresh
1994
            return self._async_get_candle_history(
1✔
1995
                pair, timeframe, since_ms=since_ms, candle_type=candle_type)
1996

1997
    def _build_ohlcv_dl_jobs(
1✔
1998
            self, pair_list: ListPairsWithTimeframes, since_ms: Optional[int],
1999
            cache: bool) -> Tuple[List[Coroutine], List[Tuple[str, str, CandleType]]]:
2000
        """
2001
        Build Coroutines to execute as part of refresh_latest_ohlcv
2002
        """
2003
        input_coroutines: List[Coroutine[Any, Any, OHLCVResponse]] = []
1✔
2004
        cached_pairs = []
1✔
2005
        for pair, timeframe, candle_type in set(pair_list):
1✔
2006
            if (timeframe not in self.timeframes
1✔
2007
                    and candle_type in (CandleType.SPOT, CandleType.FUTURES)):
2008
                logger.warning(
1✔
2009
                    f"Cannot download ({pair}, {timeframe}) combination as this timeframe is "
2010
                    f"not available on {self.name}. Available timeframes are "
2011
                    f"{', '.join(self.timeframes)}.")
2012
                continue
1✔
2013

2014
            if ((pair, timeframe, candle_type) not in self._klines or not cache
1✔
2015
                    or self._now_is_time_to_refresh(pair, timeframe, candle_type)):
2016

2017
                input_coroutines.append(
1✔
2018
                    self._build_coroutine(pair, timeframe, candle_type, since_ms, cache))
2019

2020
            else:
2021
                logger.debug(
1✔
2022
                    f"Using cached candle (OHLCV) data for {pair}, {timeframe}, {candle_type} ..."
2023
                )
2024
                cached_pairs.append((pair, timeframe, candle_type))
1✔
2025

2026
        return input_coroutines, cached_pairs
1✔
2027

2028
    def _process_ohlcv_df(self, pair: str, timeframe: str, c_type: CandleType, ticks: List[List],
1✔
2029
                          cache: bool, drop_incomplete: bool) -> DataFrame:
2030
        # keeping last candle time as last refreshed time of the pair
2031
        if ticks and cache:
1✔
2032
            idx = -2 if drop_incomplete and len(ticks) > 1 else -1
1✔
2033
            self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0] // 1000
1✔
2034
        # keeping parsed dataframe in cache
2035
        ohlcv_df = ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
1✔
2036
                                      drop_incomplete=drop_incomplete)
2037
        if cache:
1✔
2038
            if (pair, timeframe, c_type) in self._klines:
1✔
2039
                old = self._klines[(pair, timeframe, c_type)]
1✔
2040
                # Reassign so we return the updated, combined df
2041
                ohlcv_df = clean_ohlcv_dataframe(concat([old, ohlcv_df], axis=0), timeframe, pair,
1✔
2042
                                                 fill_missing=True, drop_incomplete=False)
2043
                candle_limit = self.ohlcv_candle_limit(timeframe, self._config['candle_type_def'])
1✔
2044
                # Age out old candles
2045
                ohlcv_df = ohlcv_df.tail(candle_limit + self._startup_candle_count)
1✔
2046
                ohlcv_df = ohlcv_df.reset_index(drop=True)
1✔
2047
                self._klines[(pair, timeframe, c_type)] = ohlcv_df
1✔
2048
            else:
2049
                self._klines[(pair, timeframe, c_type)] = ohlcv_df
1✔
2050
        return ohlcv_df
1✔
2051

2052
    def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
1✔
2053
                             since_ms: Optional[int] = None, cache: bool = True,
2054
                             drop_incomplete: Optional[bool] = None
2055
                             ) -> Dict[PairWithTimeframe, DataFrame]:
2056
        """
2057
        Refresh in-memory OHLCV asynchronously and set `_klines` with the result
2058
        Loops asynchronously over pair_list and downloads all pairs async (semi-parallel).
2059
        Only used in the dataprovider.refresh() method.
2060
        :param pair_list: List of 2 element tuples containing pair, interval to refresh
2061
        :param since_ms: time since when to download, in milliseconds
2062
        :param cache: Assign result to _klines. Usefull for one-off downloads like for pairlists
2063
        :param drop_incomplete: Control candle dropping.
2064
            Specifying None defaults to _ohlcv_partial_candle
2065
        :return: Dict of [{(pair, timeframe): Dataframe}]
2066
        """
2067
        logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
1✔
2068

2069
        # Gather coroutines to run
2070
        input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache)
1✔
2071

2072
        results_df = {}
1✔
2073
        # Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
2074
        for input_coro in chunks(input_coroutines, 100):
1✔
2075
            async def gather_stuff():
1✔
2076
                return await asyncio.gather(*input_coro, return_exceptions=True)
1✔
2077

2078
            with self._loop_lock:
1✔
2079
                results = self.loop.run_until_complete(gather_stuff())
1✔
2080

2081
            for res in results:
1✔
2082
                if isinstance(res, Exception):
1✔
2083
                    logger.warning(f"Async code raised an exception: {repr(res)}")
1✔
2084
                    continue
1✔
2085
                # Deconstruct tuple (has 5 elements)
2086
                pair, timeframe, c_type, ticks, drop_hint = res
1✔
2087
                drop_incomplete_ = drop_hint if drop_incomplete is None else drop_incomplete
1✔
2088
                ohlcv_df = self._process_ohlcv_df(
1✔
2089
                    pair, timeframe, c_type, ticks, cache, drop_incomplete_)
2090

2091
                results_df[(pair, timeframe, c_type)] = ohlcv_df
1✔
2092

2093
        # Return cached klines
2094
        for pair, timeframe, c_type in cached_pairs:
1✔
2095
            results_df[(pair, timeframe, c_type)] = self.klines(
1✔
2096
                (pair, timeframe, c_type),
2097
                copy=False
2098
            )
2099

2100
        return results_df
1✔
2101

2102
    def _now_is_time_to_refresh(self, pair: str, timeframe: str, candle_type: CandleType) -> bool:
1✔
2103
        # Timeframe in seconds
2104
        interval_in_sec = timeframe_to_seconds(timeframe)
1✔
2105
        plr = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + interval_in_sec
1✔
2106
        # current,active candle open date
2107
        now = int(timeframe_to_prev_date(timeframe).timestamp())
1✔
2108
        return plr < now
1✔
2109

2110
    @retrier_async
1✔
2111
    async def _async_get_candle_history(
1✔
2112
        self,
2113
        pair: str,
2114
        timeframe: str,
2115
        candle_type: CandleType,
2116
        since_ms: Optional[int] = None,
2117
    ) -> OHLCVResponse:
2118
        """
2119
        Asynchronously get candle history data using fetch_ohlcv
2120
        :param candle_type: '', mark, index, premiumIndex, or funding_rate
2121
        returns tuple: (pair, timeframe, ohlcv_list)
2122
        """
2123
        try:
1✔
2124
            # Fetch OHLCV asynchronously
2125
            s = '(' + dt_from_ts(since_ms).isoformat() + ') ' if since_ms is not None else ''
1✔
2126
            logger.debug(
1✔
2127
                "Fetching pair %s, %s, interval %s, since %s %s...",
2128
                pair, candle_type, timeframe, since_ms, s
2129
            )
2130
            params = deepcopy(self._ft_has.get('ohlcv_params', {}))
1✔
2131
            candle_limit = self.ohlcv_candle_limit(
1✔
2132
                timeframe, candle_type=candle_type, since_ms=since_ms)
2133

2134
            if candle_type and candle_type != CandleType.SPOT:
1✔
2135
                params.update({'price': candle_type.value})
1✔
2136
            if candle_type != CandleType.FUNDING_RATE:
1✔
2137
                data = await self._api_async.fetch_ohlcv(
1✔
2138
                    pair, timeframe=timeframe, since=since_ms,
2139
                    limit=candle_limit, params=params)
2140
            else:
2141
                # Funding rate
2142
                data = await self._fetch_funding_rate_history(
1✔
2143
                    pair=pair,
2144
                    timeframe=timeframe,
2145
                    limit=candle_limit,
2146
                    since_ms=since_ms,
2147
                )
2148
            # Some exchanges sort OHLCV in ASC order and others in DESC.
2149
            # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
2150
            # while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
2151
            # Only sort if necessary to save computing time
2152
            try:
1✔
2153
                if data and data[0][0] > data[-1][0]:
1✔
2154
                    data = sorted(data, key=lambda x: x[0])
1✔
2155
            except IndexError:
1✔
2156
                logger.exception("Error loading %s. Result was %s.", pair, data)
1✔
2157
                return pair, timeframe, candle_type, [], self._ohlcv_partial_candle
1✔
2158
            logger.debug("Done fetching pair %s, %s interval %s...", pair, candle_type, timeframe)
1✔
2159
            return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
1✔
2160

2161
        except ccxt.NotSupported as e:
1✔
2162
            raise OperationalException(
1✔
2163
                f'Exchange {self._api.name} does not support fetching historical '
2164
                f'candle (OHLCV) data. Message: {e}') from e
2165
        except ccxt.DDoSProtection as e:
1✔
2166
            raise DDosProtection(e) from e
1✔
2167
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2168
            raise TemporaryError(f'Could not fetch historical candle (OHLCV) data '
1✔
2169
                                 f'for pair {pair} due to {e.__class__.__name__}. '
2170
                                 f'Message: {e}') from e
2171
        except ccxt.BaseError as e:
1✔
2172
            raise OperationalException(f'Could not fetch historical candle (OHLCV) data '
1✔
2173
                                       f'for pair {pair}. Message: {e}') from e
2174

2175
    async def _fetch_funding_rate_history(
1✔
2176
        self,
2177
        pair: str,
2178
        timeframe: str,
2179
        limit: int,
2180
        since_ms: Optional[int] = None,
2181
    ) -> List[List]:
2182
        """
2183
        Fetch funding rate history - used to selectively override this by subclasses.
2184
        """
2185
        # Funding rate
2186
        data = await self._api_async.fetch_funding_rate_history(
1✔
2187
            pair, since=since_ms,
2188
            limit=limit)
2189
        # Convert funding rate to candle pattern
2190
        data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
1✔
2191
        return data
1✔
2192

2193
    # Fetch historic trades
2194

2195
    @retrier_async
1✔
2196
    async def _async_fetch_trades(self, pair: str,
1✔
2197
                                  since: Optional[int] = None,
2198
                                  params: Optional[dict] = None) -> List[List]:
2199
        """
2200
        Asyncronously gets trade history using fetch_trades.
2201
        Handles exchange errors, does one call to the exchange.
2202
        :param pair: Pair to fetch trade data for
2203
        :param since: Since as integer timestamp in milliseconds
2204
        returns: List of dicts containing trades
2205
        """
2206
        try:
1✔
2207
            # fetch trades asynchronously
2208
            if params:
1✔
2209
                logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
1✔
2210
                trades = await self._api_async.fetch_trades(pair, params=params, limit=1000)
1✔
2211
            else:
2212
                logger.debug(
1✔
2213
                    "Fetching trades for pair %s, since %s %s...",
2214
                    pair, since,
2215
                    '(' + dt_from_ts(since).isoformat() + ') ' if since is not None else ''
2216
                )
2217
                trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
1✔
2218
            trades = self._trades_contracts_to_amount(trades)
1✔
2219
            return trades_dict_to_list(trades)
1✔
2220
        except ccxt.NotSupported as e:
1✔
2221
            raise OperationalException(
1✔
2222
                f'Exchange {self._api.name} does not support fetching historical trade data.'
2223
                f'Message: {e}') from e
2224
        except ccxt.DDoSProtection as e:
1✔
2225
            raise DDosProtection(e) from e
1✔
2226
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2227
            raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. '
1✔
2228
                                 f'Message: {e}') from e
2229
        except ccxt.BaseError as e:
1✔
2230
            raise OperationalException(f'Could not fetch trade data. Msg: {e}') from e
1✔
2231

2232
    async def _async_get_trade_history_id(self, pair: str,
1✔
2233
                                          until: int,
2234
                                          since: Optional[int] = None,
2235
                                          from_id: Optional[str] = None) -> Tuple[str, List[List]]:
2236
        """
2237
        Asyncronously gets trade history using fetch_trades
2238
        use this when exchange uses id-based iteration (check `self._trades_pagination`)
2239
        :param pair: Pair to fetch trade data for
2240
        :param since: Since as integer timestamp in milliseconds
2241
        :param until: Until as integer timestamp in milliseconds
2242
        :param from_id: Download data starting with ID (if id is known). Ignores "since" if set.
2243
        returns tuple: (pair, trades-list)
2244
        """
2245

2246
        trades: List[List] = []
1✔
2247

2248
        if not from_id:
1✔
2249
            # Fetch first elements using timebased method to get an ID to paginate on
2250
            # Depending on the Exchange, this can introduce a drift at the start of the interval
2251
            # of up to an hour.
2252
            # e.g. Binance returns the "last 1000" candles within a 1h time interval
2253
            # - so we will miss the first trades.
2254
            t = await self._async_fetch_trades(pair, since=since)
1✔
2255
            # DEFAULT_TRADES_COLUMNS: 0 -> timestamp
2256
            # DEFAULT_TRADES_COLUMNS: 1 -> id
2257
            from_id = t[-1][1]
1✔
2258
            trades.extend(t[:-1])
1✔
2259
        while True:
1✔
2260
            try:
1✔
2261
                t = await self._async_fetch_trades(pair,
1✔
2262
                                                   params={self._trades_pagination_arg: from_id})
2263
                if t:
1✔
2264
                    # Skip last id since its the key for the next call
2265
                    trades.extend(t[:-1])
1✔
2266
                    if from_id == t[-1][1] or t[-1][0] > until:
1✔
2267
                        logger.debug(f"Stopping because from_id did not change. "
1✔
2268
                                     f"Reached {t[-1][0]} > {until}")
2269
                        # Reached the end of the defined-download period - add last trade as well.
2270
                        trades.extend(t[-1:])
1✔
2271
                        break
1✔
2272

2273
                    from_id = t[-1][1]
1✔
2274
                else:
2275
                    break
×
2276
            except asyncio.CancelledError:
×
2277
                logger.debug("Async operation Interrupted, breaking trades DL loop.")
×
2278
                break
×
2279

2280
        return (pair, trades)
1✔
2281

2282
    async def _async_get_trade_history_time(self, pair: str, until: int,
1✔
2283
                                            since: Optional[int] = None) -> Tuple[str, List[List]]:
2284
        """
2285
        Asyncronously gets trade history using fetch_trades,
2286
        when the exchange uses time-based iteration (check `self._trades_pagination`)
2287
        :param pair: Pair to fetch trade data for
2288
        :param since: Since as integer timestamp in milliseconds
2289
        :param until: Until as integer timestamp in milliseconds
2290
        returns tuple: (pair, trades-list)
2291
        """
2292

2293
        trades: List[List] = []
1✔
2294
        # DEFAULT_TRADES_COLUMNS: 0 -> timestamp
2295
        # DEFAULT_TRADES_COLUMNS: 1 -> id
2296
        while True:
1✔
2297
            try:
1✔
2298
                t = await self._async_fetch_trades(pair, since=since)
1✔
2299
                if t:
1✔
2300
                    since = t[-1][0]
1✔
2301
                    trades.extend(t)
1✔
2302
                    # Reached the end of the defined-download period
2303
                    if until and t[-1][0] > until:
1✔
2304
                        logger.debug(
1✔
2305
                            f"Stopping because until was reached. {t[-1][0]} > {until}")
2306
                        break
1✔
2307
                else:
2308
                    break
1✔
2309
            except asyncio.CancelledError:
×
2310
                logger.debug("Async operation Interrupted, breaking trades DL loop.")
×
2311
                break
×
2312

2313
        return (pair, trades)
1✔
2314

2315
    async def _async_get_trade_history(self, pair: str,
1✔
2316
                                       since: Optional[int] = None,
2317
                                       until: Optional[int] = None,
2318
                                       from_id: Optional[str] = None) -> Tuple[str, List[List]]:
2319
        """
2320
        Async wrapper handling downloading trades using either time or id based methods.
2321
        """
2322

2323
        logger.debug(f"_async_get_trade_history(), pair: {pair}, "
1✔
2324
                     f"since: {since}, until: {until}, from_id: {from_id}")
2325

2326
        if until is None:
1✔
2327
            until = ccxt.Exchange.milliseconds()
×
2328
            logger.debug(f"Exchange milliseconds: {until}")
×
2329

2330
        if self._trades_pagination == 'time':
1✔
2331
            return await self._async_get_trade_history_time(
1✔
2332
                pair=pair, since=since, until=until)
2333
        elif self._trades_pagination == 'id':
1✔
2334
            return await self._async_get_trade_history_id(
1✔
2335
                pair=pair, since=since, until=until, from_id=from_id
2336
            )
2337
        else:
2338
            raise OperationalException(f"Exchange {self.name} does use neither time, "
×
2339
                                       f"nor id based pagination")
2340

2341
    def get_historic_trades(self, pair: str,
1✔
2342
                            since: Optional[int] = None,
2343
                            until: Optional[int] = None,
2344
                            from_id: Optional[str] = None) -> Tuple[str, List]:
2345
        """
2346
        Get trade history data using asyncio.
2347
        Handles all async work and returns the list of candles.
2348
        Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call.
2349
        :param pair: Pair to download
2350
        :param since: Timestamp in milliseconds to get history from
2351
        :param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.
2352
        :param from_id: Download data starting with ID (if id is known)
2353
        :returns List of trade data
2354
        """
2355
        if not self.exchange_has("fetchTrades"):
1✔
2356
            raise OperationalException("This exchange does not support downloading Trades.")
1✔
2357

2358
        with self._loop_lock:
1✔
2359
            task = asyncio.ensure_future(self._async_get_trade_history(
1✔
2360
                pair=pair, since=since, until=until, from_id=from_id))
2361

2362
            for sig in [signal.SIGINT, signal.SIGTERM]:
1✔
2363
                try:
1✔
2364
                    self.loop.add_signal_handler(sig, task.cancel)
1✔
2365
                except NotImplementedError:
×
2366
                    # Not all platforms implement signals (e.g. windows)
2367
                    pass
×
2368
            return self.loop.run_until_complete(task)
1✔
2369

2370
    @retrier
1✔
2371
    def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
1✔
2372
        """
2373
        Returns the sum of all funding fees that were exchanged for a pair within a timeframe
2374
        Dry-run handling happens as part of _calculate_funding_fees.
2375
        :param pair: (e.g. ADA/USDT)
2376
        :param since: The earliest time of consideration for calculating funding fees,
2377
            in unix time or as a datetime
2378
        """
2379
        if not self.exchange_has("fetchFundingHistory"):
1✔
2380
            raise OperationalException(
×
2381
                f"fetch_funding_history() is not available using {self.name}"
2382
            )
2383

2384
        if type(since) is datetime:
1✔
2385
            since = int(since.timestamp()) * 1000   # * 1000 for ms
1✔
2386

2387
        try:
1✔
2388
            funding_history = self._api.fetch_funding_history(
1✔
2389
                symbol=pair,
2390
                since=since
2391
            )
2392
            return sum(fee['amount'] for fee in funding_history)
1✔
2393
        except ccxt.DDoSProtection as e:
1✔
2394
            raise DDosProtection(e) from e
1✔
2395
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2396
            raise TemporaryError(
1✔
2397
                f'Could not get funding fees due to {e.__class__.__name__}. Message: {e}') from e
2398
        except ccxt.BaseError as e:
1✔
2399
            raise OperationalException(e) from e
1✔
2400

2401
    @retrier
1✔
2402
    def get_leverage_tiers(self) -> Dict[str, List[Dict]]:
1✔
2403
        try:
1✔
2404
            return self._api.fetch_leverage_tiers()
1✔
2405
        except ccxt.DDoSProtection as e:
1✔
2406
            raise DDosProtection(e) from e
1✔
2407
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2408
            raise TemporaryError(
1✔
2409
                f'Could not load leverage tiers due to {e.__class__.__name__}. Message: {e}'
2410
            ) from e
2411
        except ccxt.BaseError as e:
1✔
2412
            raise OperationalException(e) from e
1✔
2413

2414
    @retrier_async
1✔
2415
    async def get_market_leverage_tiers(self, symbol: str) -> Tuple[str, List[Dict]]:
1✔
2416
        """ Leverage tiers per symbol """
2417
        try:
1✔
2418
            tier = await self._api_async.fetch_market_leverage_tiers(symbol)
1✔
2419
            return symbol, tier
1✔
2420
        except ccxt.DDoSProtection as e:
1✔
2421
            raise DDosProtection(e) from e
1✔
2422
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2423
            raise TemporaryError(
1✔
2424
                f'Could not load leverage tiers for {symbol}'
2425
                f' due to {e.__class__.__name__}. Message: {e}'
2426
            ) from e
2427
        except ccxt.BaseError as e:
1✔
2428
            raise OperationalException(e) from e
1✔
2429

2430
    def load_leverage_tiers(self) -> Dict[str, List[Dict]]:
1✔
2431
        if self.trading_mode == TradingMode.FUTURES:
1✔
2432
            if self.exchange_has('fetchLeverageTiers'):
1✔
2433
                # Fetch all leverage tiers at once
2434
                return self.get_leverage_tiers()
1✔
2435
            elif self.exchange_has('fetchMarketLeverageTiers'):
1✔
2436
                # Must fetch the leverage tiers for each market separately
2437
                # * This is slow(~45s) on Okx, makes ~90 api calls to load all linear swap markets
2438
                markets = self.markets
1✔
2439

2440
                symbols = [
1✔
2441
                    symbol for symbol, market in markets.items()
2442
                    if (self.market_is_future(market)
2443
                        and market['quote'] == self._config['stake_currency'])
2444
                ]
2445

2446
                tiers: Dict[str, List[Dict]] = {}
1✔
2447

2448
                tiers_cached = self.load_cached_leverage_tiers(self._config['stake_currency'])
1✔
2449
                if tiers_cached:
1✔
2450
                    tiers = tiers_cached
1✔
2451

2452
                coros = [
1✔
2453
                    self.get_market_leverage_tiers(symbol)
2454
                    for symbol in sorted(symbols) if symbol not in tiers]
2455

2456
                # Be verbose here, as this delays startup by ~1 minute.
2457
                if coros:
1✔
2458
                    logger.info(
1✔
2459
                        f"Initializing leverage_tiers for {len(symbols)} markets. "
2460
                        "This will take about a minute.")
2461
                else:
2462
                    logger.info("Using cached leverage_tiers.")
1✔
2463

2464
                async def gather_results(input_coro):
1✔
2465
                    return await asyncio.gather(*input_coro, return_exceptions=True)
1✔
2466

2467
                for input_coro in chunks(coros, 100):
1✔
2468

2469
                    with self._loop_lock:
1✔
2470
                        results = self.loop.run_until_complete(gather_results(input_coro))
1✔
2471

2472
                    for res in results:
1✔
2473
                        if isinstance(res, Exception):
1✔
2474
                            logger.warning(f"Leverage tier exception: {repr(res)}")
1✔
2475
                            continue
1✔
2476
                        symbol, tier = res
1✔
2477
                        tiers[symbol] = tier
1✔
2478
                if len(coros) > 0:
1✔
2479
                    self.cache_leverage_tiers(tiers, self._config['stake_currency'])
1✔
2480
                logger.info(f"Done initializing {len(symbols)} markets.")
1✔
2481

2482
                return tiers
1✔
2483
        return {}
1✔
2484

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

2487
        filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json"
1✔
2488
        if not filename.parent.is_dir():
1✔
2489
            filename.parent.mkdir(parents=True)
1✔
2490
        data = {
1✔
2491
            "updated": datetime.now(timezone.utc),
2492
            "data": tiers,
2493
        }
2494
        file_dump_json(filename, data)
1✔
2495

2496
    def load_cached_leverage_tiers(self, stake_currency: str) -> Optional[Dict[str, List[Dict]]]:
1✔
2497
        filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json"
1✔
2498
        if filename.is_file():
1✔
2499
            try:
1✔
2500
                tiers = file_load_json(filename)
1✔
2501
                updated = tiers.get('updated')
1✔
2502
                if updated:
1✔
2503
                    updated_dt = parser.parse(updated)
1✔
2504
                    if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4):
1✔
2505
                        logger.info("Cached leverage tiers are outdated. Will update.")
1✔
2506
                        return None
1✔
2507
                return tiers['data']
1✔
2508
            except Exception:
×
2509
                logger.exception("Error loading cached leverage tiers. Refreshing.")
×
2510
        return None
1✔
2511

2512
    def fill_leverage_tiers(self) -> None:
1✔
2513
        """
2514
        Assigns property _leverage_tiers to a dictionary of information about the leverage
2515
        allowed on each pair
2516
        """
2517
        leverage_tiers = self.load_leverage_tiers()
1✔
2518
        for pair, tiers in leverage_tiers.items():
1✔
2519
            pair_tiers = []
1✔
2520
            for tier in tiers:
1✔
2521
                pair_tiers.append(self.parse_leverage_tier(tier))
1✔
2522
            self._leverage_tiers[pair] = pair_tiers
1✔
2523

2524
    def parse_leverage_tier(self, tier) -> Dict:
1✔
2525
        info = tier.get('info', {})
1✔
2526
        return {
1✔
2527
            'minNotional': tier['minNotional'],
2528
            'maxNotional': tier['maxNotional'],
2529
            'maintenanceMarginRate': tier['maintenanceMarginRate'],
2530
            'maxLeverage': tier['maxLeverage'],
2531
            'maintAmt': float(info['cum']) if 'cum' in info else None,
2532
        }
2533

2534
    def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float:
1✔
2535
        """
2536
        Returns the maximum leverage that a pair can be traded at
2537
        :param pair: The base/quote currency pair being traded
2538
        :stake_amount: The total value of the traders margin_mode in quote currency
2539
        """
2540

2541
        if self.trading_mode == TradingMode.SPOT:
1✔
2542
            return 1.0
1✔
2543

2544
        if self.trading_mode == TradingMode.FUTURES:
1✔
2545

2546
            # Checks and edge cases
2547
            if stake_amount is None:
1✔
2548
                raise OperationalException(
×
2549
                    f'{self.name}.get_max_leverage requires argument stake_amount'
2550
                )
2551

2552
            if pair not in self._leverage_tiers:
1✔
2553
                # Maybe raise exception because it can't be traded on futures?
2554
                return 1.0
1✔
2555

2556
            pair_tiers = self._leverage_tiers[pair]
1✔
2557

2558
            if stake_amount == 0:
1✔
2559
                return self._leverage_tiers[pair][0]['maxLeverage']  # Max lev for lowest amount
1✔
2560

2561
            for tier_index in range(len(pair_tiers)):
1✔
2562

2563
                tier = pair_tiers[tier_index]
1✔
2564
                lev = tier['maxLeverage']
1✔
2565

2566
                if tier_index < len(pair_tiers) - 1:
1✔
2567
                    next_tier = pair_tiers[tier_index + 1]
1✔
2568
                    next_floor = next_tier['minNotional'] / next_tier['maxLeverage']
1✔
2569
                    if next_floor > stake_amount:  # Next tier min too high for stake amount
1✔
2570
                        return min((tier['maxNotional'] / stake_amount), lev)
1✔
2571
                        #
2572
                        # With the two leverage tiers below,
2573
                        # - a stake amount of 150 would mean a max leverage of (10000 / 150) = 66.66
2574
                        # - stakes below 133.33 = max_lev of 75
2575
                        # - stakes between 133.33-200 = max_lev of 10000/stake = 50.01-74.99
2576
                        # - stakes from 200 + 1000 = max_lev of 50
2577
                        #
2578
                        # {
2579
                        #     "min": 0,      # stake = 0.0
2580
                        #     "max": 10000,  # max_stake@75 = 10000/75 = 133.33333333333334
2581
                        #     "lev": 75,
2582
                        # },
2583
                        # {
2584
                        #     "min": 10000,  # stake = 200.0
2585
                        #     "max": 50000,  # max_stake@50 = 50000/50 = 1000.0
2586
                        #     "lev": 50,
2587
                        # }
2588
                        #
2589

2590
                else:  # if on the last tier
2591
                    if stake_amount > tier['maxNotional']:
1✔
2592
                        # If stake is > than max tradeable amount
2593
                        raise InvalidOrderException(f'Amount {stake_amount} too high for {pair}')
1✔
2594
                    else:
2595
                        return tier['maxLeverage']
1✔
2596

2597
            raise OperationalException(
×
2598
                'Looped through all tiers without finding a max leverage. Should never be reached'
2599
            )
2600

2601
        elif self.trading_mode == TradingMode.MARGIN:  # Search markets.limits for max lev
1✔
2602
            market = self.markets[pair]
1✔
2603
            if market['limits']['leverage']['max'] is not None:
1✔
2604
                return market['limits']['leverage']['max']
1✔
2605
            else:
2606
                return 1.0  # Default if max leverage cannot be found
1✔
2607
        else:
2608
            return 1.0
×
2609

2610
    @retrier
1✔
2611
    def _set_leverage(
1✔
2612
        self,
2613
        leverage: float,
2614
        pair: Optional[str] = None,
2615
        accept_fail: bool = False,
2616
    ):
2617
        """
2618
        Set's the leverage before making a trade, in order to not
2619
        have the same leverage on every trade
2620
        """
2621
        if self._config['dry_run'] or not self.exchange_has("setLeverage"):
1✔
2622
            # Some exchanges only support one margin_mode type
2623
            return
1✔
2624
        if self._ft_has.get('floor_leverage', False) is True:
1✔
2625
            # Rounding for binance ...
2626
            leverage = floor(leverage)
1✔
2627
        try:
1✔
2628
            res = self._api.set_leverage(symbol=pair, leverage=leverage)
1✔
2629
            self._log_exchange_response('set_leverage', res)
1✔
2630
        except ccxt.DDoSProtection as e:
1✔
2631
            raise DDosProtection(e) from e
1✔
2632
        except (ccxt.BadRequest, ccxt.InsufficientFunds) as e:
1✔
2633
            if not accept_fail:
×
2634
                raise TemporaryError(
×
2635
                    f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
2636
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2637
            raise TemporaryError(
1✔
2638
                f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
2639
        except ccxt.BaseError as e:
1✔
2640
            raise OperationalException(e) from e
1✔
2641

2642
    def get_interest_rate(self) -> float:
1✔
2643
        """
2644
        Retrieve interest rate - necessary for Margin trading.
2645
        Should not call the exchange directly when used from backtesting.
2646
        """
2647
        return 0.0
×
2648

2649
    def funding_fee_cutoff(self, open_date: datetime):
1✔
2650
        """
2651
        :param open_date: The open date for a trade
2652
        :return: The cutoff open time for when a funding fee is charged
2653
        """
2654
        return open_date.minute > 0 or open_date.second > 0
1✔
2655

2656
    @retrier
1✔
2657
    def set_margin_mode(self, pair: str, margin_mode: MarginMode, accept_fail: bool = False,
1✔
2658
                        params: dict = {}):
2659
        """
2660
        Set's the margin mode on the exchange to cross or isolated for a specific pair
2661
        :param pair: base/quote currency pair (e.g. "ADA/USDT")
2662
        """
2663
        if self._config['dry_run'] or not self.exchange_has("setMarginMode"):
1✔
2664
            # Some exchanges only support one margin_mode type
2665
            return
1✔
2666

2667
        try:
1✔
2668
            res = self._api.set_margin_mode(margin_mode.value, pair, params)
1✔
2669
            self._log_exchange_response('set_margin_mode', res)
×
2670
        except ccxt.DDoSProtection as e:
1✔
2671
            raise DDosProtection(e) from e
1✔
2672
        except ccxt.BadRequest as e:
1✔
2673
            if not accept_fail:
×
2674
                raise TemporaryError(
×
2675
                    f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
2676
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2677
            raise TemporaryError(
1✔
2678
                f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
2679
        except ccxt.BaseError as e:
1✔
2680
            raise OperationalException(e) from e
1✔
2681

2682
    def _fetch_and_calculate_funding_fees(
1✔
2683
        self,
2684
        pair: str,
2685
        amount: float,
2686
        is_short: bool,
2687
        open_date: datetime,
2688
        close_date: Optional[datetime] = None
2689
    ) -> float:
2690
        """
2691
        Fetches and calculates the sum of all funding fees that occurred for a pair
2692
        during a futures trade.
2693
        Only used during dry-run or if the exchange does not provide a funding_rates endpoint.
2694
        :param pair: The quote/base pair of the trade
2695
        :param amount: The quantity of the trade
2696
        :param is_short: trade direction
2697
        :param open_date: The date and time that the trade started
2698
        :param close_date: The date and time that the trade ended
2699
        """
2700

2701
        if self.funding_fee_cutoff(open_date):
1✔
2702
            open_date += timedelta(hours=1)
1✔
2703
        timeframe = self._ft_has['mark_ohlcv_timeframe']
1✔
2704
        timeframe_ff = self._ft_has.get('funding_fee_timeframe',
1✔
2705
                                        self._ft_has['mark_ohlcv_timeframe'])
2706

2707
        if not close_date:
1✔
2708
            close_date = datetime.now(timezone.utc)
1✔
2709
        open_timestamp = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000
1✔
2710
        # close_timestamp = int(close_date.timestamp()) * 1000
2711

2712
        mark_comb: PairWithTimeframe = (
1✔
2713
            pair, timeframe, CandleType.from_string(self._ft_has["mark_ohlcv_price"]))
2714

2715
        funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE)
1✔
2716
        candle_histories = self.refresh_latest_ohlcv(
1✔
2717
            [mark_comb, funding_comb],
2718
            since_ms=open_timestamp,
2719
            cache=False,
2720
            drop_incomplete=False,
2721
        )
2722
        try:
1✔
2723
            # we can't assume we always get histories - for example during exchange downtimes
2724
            funding_rates = candle_histories[funding_comb]
1✔
2725
            mark_rates = candle_histories[mark_comb]
1✔
2726
        except KeyError:
1✔
2727
            raise ExchangeError("Could not find funding rates.") from None
1✔
2728

2729
        funding_mark_rates = self.combine_funding_and_mark(
1✔
2730
            funding_rates=funding_rates, mark_rates=mark_rates)
2731

2732
        return self.calculate_funding_fees(
1✔
2733
            funding_mark_rates,
2734
            amount=amount,
2735
            is_short=is_short,
2736
            open_date=open_date,
2737
            close_date=close_date
2738
        )
2739

2740
    @staticmethod
1✔
2741
    def combine_funding_and_mark(funding_rates: DataFrame, mark_rates: DataFrame,
1✔
2742
                                 futures_funding_rate: Optional[int] = None) -> DataFrame:
2743
        """
2744
        Combine funding-rates and mark-rates dataframes
2745
        :param funding_rates: Dataframe containing Funding rates (Type FUNDING_RATE)
2746
        :param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price)
2747
        :param futures_funding_rate: Fake funding rate to use if funding_rates are not available
2748
        """
2749
        if futures_funding_rate is None:
1✔
2750
            return mark_rates.merge(
1✔
2751
                funding_rates, on='date', how="inner", suffixes=["_mark", "_fund"])
2752
        else:
2753
            if len(funding_rates) == 0:
1✔
2754
                # No funding rate candles - full fillup with fallback variable
2755
                mark_rates['open_fund'] = futures_funding_rate
1✔
2756
                return mark_rates.rename(
1✔
2757
                        columns={'open': 'open_mark',
2758
                                 'close': 'close_mark',
2759
                                 'high': 'high_mark',
2760
                                 'low': 'low_mark',
2761
                                 'volume': 'volume_mark'})
2762

2763
            else:
2764
                # Fill up missing funding_rate candles with fallback value
2765
                combined = mark_rates.merge(
1✔
2766
                    funding_rates, on='date', how="outer", suffixes=["_mark", "_fund"]
2767
                    )
2768
                combined['open_fund'] = combined['open_fund'].fillna(futures_funding_rate)
1✔
2769
                return combined
1✔
2770

2771
    def calculate_funding_fees(
1✔
2772
        self,
2773
        df: DataFrame,
2774
        amount: float,
2775
        is_short: bool,
2776
        open_date: datetime,
2777
        close_date: Optional[datetime] = None,
2778
        time_in_ratio: Optional[float] = None
2779
    ) -> float:
2780
        """
2781
        calculates the sum of all funding fees that occurred for a pair during a futures trade
2782
        :param df: Dataframe containing combined funding and mark rates
2783
                   as `open_fund` and `open_mark`.
2784
        :param amount: The quantity of the trade
2785
        :param is_short: trade direction
2786
        :param open_date: The date and time that the trade started
2787
        :param close_date: The date and time that the trade ended
2788
        :param time_in_ratio: Not used by most exchange classes
2789
        """
2790
        fees: float = 0
1✔
2791

2792
        if not df.empty:
1✔
2793
            df = df[(df['date'] >= open_date) & (df['date'] <= close_date)]
1✔
2794
            fees = sum(df['open_fund'] * df['open_mark'] * amount)
1✔
2795

2796
        # Negate fees for longs as funding_fees expects it this way based on live endpoints.
2797
        return fees if is_short else -fees
1✔
2798

2799
    def get_funding_fees(
1✔
2800
            self, pair: str, amount: float, is_short: bool, open_date: datetime) -> float:
2801
        """
2802
        Fetch funding fees, either from the exchange (live) or calculates them
2803
        based on funding rate/mark price history
2804
        :param pair: The quote/base pair of the trade
2805
        :param is_short: trade direction
2806
        :param amount: Trade amount
2807
        :param open_date: Open date of the trade
2808
        :return: funding fee since open_date
2809
        :raises: ExchangeError if something goes wrong.
2810
        """
2811
        if self.trading_mode == TradingMode.FUTURES:
1✔
2812
            if self._config['dry_run']:
1✔
2813
                funding_fees = self._fetch_and_calculate_funding_fees(
1✔
2814
                    pair, amount, is_short, open_date)
2815
            else:
2816
                funding_fees = self._get_funding_fees_from_exchange(pair, open_date)
×
2817
            return funding_fees
1✔
2818
        else:
2819
            return 0.0
1✔
2820

2821
    def get_liquidation_price(
1✔
2822
        self,
2823
        pair: str,
2824
        # Dry-run
2825
        open_rate: float,   # Entry price of position
2826
        is_short: bool,
2827
        amount: float,  # Absolute value of position size
2828
        stake_amount: float,
2829
        leverage: float,
2830
        wallet_balance: float,
2831
        mm_ex_1: float = 0.0,  # (Binance) Cross only
2832
        upnl_ex_1: float = 0.0,  # (Binance) Cross only
2833
    ) -> Optional[float]:
2834
        """
2835
        Set's the margin mode on the exchange to cross or isolated for a specific pair
2836
        """
2837
        if self.trading_mode == TradingMode.SPOT:
1✔
2838
            return None
1✔
2839
        elif (self.trading_mode != TradingMode.FUTURES):
1✔
2840
            raise OperationalException(
1✔
2841
                f"{self.name} does not support {self.margin_mode} {self.trading_mode}")
2842

2843
        liquidation_price = None
1✔
2844
        if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
1✔
2845

2846
            liquidation_price = self.dry_run_liquidation_price(
1✔
2847
                pair=pair,
2848
                open_rate=open_rate,
2849
                is_short=is_short,
2850
                amount=amount,
2851
                leverage=leverage,
2852
                stake_amount=stake_amount,
2853
                wallet_balance=wallet_balance,
2854
                mm_ex_1=mm_ex_1,
2855
                upnl_ex_1=upnl_ex_1
2856
            )
2857
        else:
2858
            positions = self.fetch_positions(pair)
1✔
2859
            if len(positions) > 0:
1✔
2860
                pos = positions[0]
1✔
2861
                liquidation_price = pos['liquidationPrice']
1✔
2862

2863
        if liquidation_price is not None:
1✔
2864
            buffer_amount = abs(open_rate - liquidation_price) * self.liquidation_buffer
1✔
2865
            liquidation_price_buffer = (
1✔
2866
                liquidation_price - buffer_amount
2867
                if is_short else
2868
                liquidation_price + buffer_amount
2869
            )
2870
            return max(liquidation_price_buffer, 0.0)
1✔
2871
        else:
2872
            return None
1✔
2873

2874
    def dry_run_liquidation_price(
1✔
2875
        self,
2876
        pair: str,
2877
        open_rate: float,   # Entry price of position
2878
        is_short: bool,
2879
        amount: float,
2880
        stake_amount: float,
2881
        leverage: float,
2882
        wallet_balance: float,  # Or margin balance
2883
        mm_ex_1: float = 0.0,  # (Binance) Cross only
2884
        upnl_ex_1: float = 0.0,  # (Binance) Cross only
2885
    ) -> Optional[float]:
2886
        """
2887
        Important: Must be fetching data from cached values as this is used by backtesting!
2888
        PERPETUAL:
2889
         gate: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-price
2890
         > Liquidation Price = (Entry Price ± Margin / Contract Multiplier / Size) /
2891
                                [ 1 ± (Maintenance Margin Ratio + Taker Rate)]
2892
            Wherein, "+" or "-" depends on whether the contract goes long or short:
2893
            "-" for long, and "+" for short.
2894

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

2898
        :param pair: Pair to calculate liquidation price for
2899
        :param open_rate: Entry price of position
2900
        :param is_short: True if the trade is a short, false otherwise
2901
        :param amount: Absolute value of position size incl. leverage (in base currency)
2902
        :param stake_amount: Stake amount - Collateral in settle currency.
2903
        :param leverage: Leverage used for this position.
2904
        :param trading_mode: SPOT, MARGIN, FUTURES, etc.
2905
        :param margin_mode: Either ISOLATED or CROSS
2906
        :param wallet_balance: Amount of margin_mode in the wallet being used to trade
2907
            Cross-Margin Mode: crossWalletBalance
2908
            Isolated-Margin Mode: isolatedWalletBalance
2909

2910
        # * Not required by Gate or OKX
2911
        :param mm_ex_1:
2912
        :param upnl_ex_1:
2913
        """
2914

2915
        market = self.markets[pair]
1✔
2916
        taker_fee_rate = market['taker']
1✔
2917
        mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount)
1✔
2918

2919
        if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
1✔
2920

2921
            if market['inverse']:
1✔
2922
                raise OperationalException(
×
2923
                    "Freqtrade does not yet support inverse contracts")
2924

2925
            value = wallet_balance / amount
1✔
2926

2927
            mm_ratio_taker = (mm_ratio + taker_fee_rate)
1✔
2928
            if is_short:
1✔
2929
                return (open_rate + value) / (1 + mm_ratio_taker)
1✔
2930
            else:
2931
                return (open_rate - value) / (1 - mm_ratio_taker)
1✔
2932
        else:
2933
            raise OperationalException(
×
2934
                "Freqtrade only supports isolated futures for leverage trading")
2935

2936
    def get_maintenance_ratio_and_amt(
1✔
2937
        self,
2938
        pair: str,
2939
        nominal_value: float,
2940
    ) -> Tuple[float, Optional[float]]:
2941
        """
2942
        Important: Must be fetching data from cached values as this is used by backtesting!
2943
        :param pair: Market symbol
2944
        :param nominal_value: The total trade amount in quote currency including leverage
2945
        maintenance amount only on Binance
2946
        :return: (maintenance margin ratio, maintenance amount)
2947
        """
2948

2949
        if (self._config.get('runmode') in OPTIMIZE_MODES
1✔
2950
                or self.exchange_has('fetchLeverageTiers')
2951
                or self.exchange_has('fetchMarketLeverageTiers')):
2952

2953
            if pair not in self._leverage_tiers:
1✔
2954
                raise InvalidOrderException(
1✔
2955
                    f"Maintenance margin rate for {pair} is unavailable for {self.name}"
2956
                )
2957

2958
            pair_tiers = self._leverage_tiers[pair]
1✔
2959

2960
            for tier in reversed(pair_tiers):
1✔
2961
                if nominal_value >= tier['minNotional']:
1✔
2962
                    return (tier['maintenanceMarginRate'], tier['maintAmt'])
1✔
2963

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