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

freqtrade / freqtrade / 4131167254

pending completion
4131167254

push

github-actions

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

16866 of 17748 relevant lines covered (95.03%)

0.95 hits per line

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

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

14
import arrow
1✔
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, ListPairsWithTimeframes, MakerTaker,
24
                                 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.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
1✔
28
                                  InvalidOrderException, OperationalException, PricingError,
29
                                  RetryableOrderError, TemporaryError)
30
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier,
1✔
31
                                       retrier_async)
32
from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contract_precision,
1✔
33
                                               amount_to_contracts, amount_to_precision,
34
                                               contracts_to_amount, date_minus_candles,
35
                                               is_exchange_known_ccxt, market_is_active,
36
                                               price_to_precision, timeframe_to_minutes,
37
                                               timeframe_to_msecs, timeframe_to_next_date,
38
                                               timeframe_to_prev_date, timeframe_to_seconds)
39
from freqtrade.exchange.types import OHLCVResponse, Ticker, Tickers
1✔
40
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
1✔
41
                            safe_value_fallback2)
42
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
1✔
43

44

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

47

48
# Workaround for adding samesite support to pre 3.8 python
49
# Only applies to python3.7, and only on certain exchanges (kraken)
50
# Replicates the fix from starlette (which is actually causing this problem)
51
http.cookies.Morsel._reserved["samesite"] = "SameSite"  # type: ignore
1✔
52

53

54
class Exchange:
1✔
55

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

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

62
    # Dict to specify which options each exchange implements
63
    # This defines defaults, which can be selectively overridden by subclasses using _ft_has
64
    # or by specifying them in the configuration.
65
    _ft_has_default: Dict = {
1✔
66
        "stoploss_on_exchange": False,
67
        "order_time_in_force": ["GTC"],
68
        "time_in_force_parameter": "timeInForce",
69
        "ohlcv_params": {},
70
        "ohlcv_candle_limit": 500,
71
        "ohlcv_has_history": True,  # Some exchanges (Kraken) don't provide history via ohlcv
72
        "ohlcv_partial_candle": True,
73
        "ohlcv_require_since": False,
74
        # Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
75
        "ohlcv_volume_currency": "base",  # "base" or "quote"
76
        "tickers_have_quoteVolume": True,
77
        "tickers_have_price": True,
78
        "trades_pagination": "time",  # Possible are "time" or "id"
79
        "trades_pagination_arg": "since",
80
        "l2_limit_range": None,
81
        "l2_limit_range_required": True,  # Allow Empty L2 limit (kucoin)
82
        "mark_ohlcv_price": "mark",
83
        "mark_ohlcv_timeframe": "8h",
84
        "ccxt_futures_name": "swap",
85
        "fee_cost_in_contracts": False,  # Fee cost needs contract conversion
86
        "needs_trading_fees": False,  # use fetch_trading_fees to cache fees
87
        "order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'],
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, validate: bool = True,
1✔
97
                 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 = asyncio.new_event_loop()
1✔
112
        asyncio.set_event_loop(self.loop)
1✔
113
        self._config: Config = {}
1✔
114

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

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

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

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

134
        # Holds all open sell orders for dry_run
135
        self._dry_run_open_orders: Dict[str, Any] = {}
1✔
136
        remove_credentials(config)
1✔
137

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

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

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

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

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

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

173
        self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config)
1✔
174

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

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

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

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

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

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

214
    def validate_config(self, config):
1✔
215
        # Check if timeframe is available
216
        self.validate_timeframes(config.get('timeframe'))
1✔
217

218
        # Check if all pairs are available
219
        self.validate_stakecurrency(config['stake_currency'])
1✔
220
        if not config['exchange'].get('skip_pair_validation'):
1✔
221
            self.validate_pairs(config['exchange']['pair_whitelist'])
1✔
222
        self.validate_ordertypes(config.get('order_types', {}))
1✔
223
        self.validate_order_time_in_force(config.get('order_time_in_force', {}))
1✔
224
        self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
1✔
225
        self.validate_pricing(config['exit_pricing'])
1✔
226
        self.validate_pricing(config['entry_pricing'])
1✔
227

228
    def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
1✔
229
                   ccxt_kwargs: Dict = {}) -> ccxt.Exchange:
230
        """
231
        Initialize ccxt with given config and return valid
232
        ccxt instance.
233
        """
234
        # Find matching class for the given exchange name
235
        name = exchange_config['name']
1✔
236

237
        if not is_exchange_known_ccxt(name, ccxt_module):
1✔
238
            raise OperationalException(f'Exchange {name} is not supported by ccxt')
1✔
239

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

255
            api = getattr(ccxt_module, name.lower())(ex_config)
1✔
256
        except (KeyError, AttributeError) as e:
1✔
257
            raise OperationalException(f'Exchange {name} is not supported') from e
1✔
258
        except ccxt.BaseError as e:
1✔
259
            raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
1✔
260

261
        self.set_sandbox(api, exchange_config, name)
1✔
262

263
        return api
1✔
264

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

283
    @property
1✔
284
    def name(self) -> str:
1✔
285
        """exchange Name (from ccxt)"""
286
        return self._api.name
1✔
287

288
    @property
1✔
289
    def id(self) -> str:
1✔
290
        """exchange ccxt id"""
291
        return self._api.id
×
292

293
    @property
1✔
294
    def timeframes(self) -> List[str]:
1✔
295
        return list((self._api.timeframes or {}).keys())
1✔
296

297
    @property
1✔
298
    def markets(self) -> Dict:
1✔
299
        """exchange ccxt markets"""
300
        if not self._markets:
1✔
301
            logger.info("Markets were not loaded. Loading them now..")
1✔
302
            self._load_markets()
1✔
303
        return self._markets
1✔
304

305
    @property
1✔
306
    def precisionMode(self) -> int:
1✔
307
        """exchange ccxt precisionMode"""
308
        return self._api.precisionMode
1✔
309

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

318
    def _log_exchange_response(self, endpoint, response) -> None:
1✔
319
        """ Log exchange responses """
320
        if self.log_responses:
1✔
321
            logger.info(f"API {endpoint}: {response}")
1✔
322

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

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

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

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

372
    def get_pair_quote_currency(self, pair: str) -> str:
1✔
373
        """ Return a pair's quote currency (base/quote:settlement) """
374
        return self.markets.get(pair, {}).get('quote', '')
1✔
375

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

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

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

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

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

408
    def klines(self, pair_interval: PairWithTimeframe, copy: bool = True) -> DataFrame:
1✔
409
        if pair_interval in self._klines:
1✔
410
            return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
1✔
411
        else:
412
            return DataFrame()
1✔
413

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

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

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

444
    def _amount_to_contracts(self, pair: str, amount: float) -> float:
1✔
445

446
        contract_size = self.get_contract_size(pair)
1✔
447
        return amount_to_contracts(amount, contract_size)
1✔
448

449
    def _contracts_to_amount(self, pair: str, num_contracts: float) -> float:
1✔
450

451
        contract_size = self.get_contract_size(pair)
1✔
452
        return contracts_to_amount(num_contracts, contract_size)
1✔
453

454
    def amount_to_contract_precision(self, pair: str, amount: float) -> float:
1✔
455
        """
456
        Helper wrapper around amount_to_contract_precision
457
        """
458
        contract_size = self.get_contract_size(pair)
1✔
459

460
        return amount_to_contract_precision(amount, self.get_precision_amount(pair),
1✔
461
                                            self.precisionMode, contract_size)
462

463
    def set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None:
1✔
464
        if exchange_config.get('sandbox'):
1✔
465
            if api.urls.get('test'):
1✔
466
                api.urls['api'] = api.urls['test']
1✔
467
                logger.info("Enabled Sandbox API on %s", name)
1✔
468
            else:
469
                logger.warning(
1✔
470
                    f"No Sandbox URL in CCXT for {name}, exiting. Please check your config.json")
471
                raise OperationalException(f'Exchange {name} does not provide a sandbox api')
1✔
472

473
    def _load_async_markets(self, reload: bool = False) -> None:
1✔
474
        try:
1✔
475
            if self._api_async:
1✔
476
                self.loop.run_until_complete(
1✔
477
                    self._api_async.load_markets(reload=reload))
478

479
        except (asyncio.TimeoutError, ccxt.BaseError) as e:
1✔
480
            logger.warning('Could not load async markets. Reason: %s', e)
1✔
481
            return
1✔
482

483
    def _load_markets(self) -> None:
1✔
484
        """ Initialize markets both sync and async """
485
        try:
1✔
486
            self._markets = self._api.load_markets()
1✔
487
            self._load_async_markets()
1✔
488
            self._last_markets_refresh = arrow.utcnow().int_timestamp
1✔
489
            if self._ft_has['needs_trading_fees']:
1✔
490
                self._trading_fees = self.fetch_trading_fees()
1✔
491

492
        except ccxt.BaseError:
1✔
493
            logger.exception('Unable to initialize markets.')
1✔
494

495
    def reload_markets(self) -> None:
1✔
496
        """Reload markets both sync and async if refresh interval has passed """
497
        # Check whether markets have to be reloaded
498
        if (self._last_markets_refresh > 0) and (
1✔
499
                self._last_markets_refresh + self.markets_refresh_interval
500
                > arrow.utcnow().int_timestamp):
501
            return None
1✔
502
        logger.debug("Performing scheduled market reload..")
1✔
503
        try:
1✔
504
            self._markets = self._api.load_markets(reload=True)
1✔
505
            # Also reload async markets to avoid issues with newly listed pairs
506
            self._load_async_markets(reload=True)
1✔
507
            self._last_markets_refresh = arrow.utcnow().int_timestamp
1✔
508
            self.fill_leverage_tiers()
1✔
509
        except ccxt.BaseError:
1✔
510
            logger.exception("Could not reload markets.")
1✔
511

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

531
    def validate_pairs(self, pairs: List[str]) -> None:
1✔
532
        """
533
        Checks if all given pairs are tradable on the current exchange.
534
        :param pairs: list of pairs
535
        :raise: OperationalException if one pair is not available
536
        :return: None
537
        """
538

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

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

572
    def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str:
1✔
573
        """
574
        Get valid pair combination of curr_1 and curr_2 by trying both combinations.
575
        """
576
        for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
1✔
577
            if pair in self.markets and self.markets[pair].get('active'):
1✔
578
                return pair
1✔
579
        raise ExchangeError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
1✔
580

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

594
        if timeframe and (timeframe not in self.timeframes):
1✔
595
            raise OperationalException(
1✔
596
                f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}")
597

598
        if timeframe and timeframe_to_minutes(timeframe) < 1:
1✔
599
            raise OperationalException("Timeframes < 1m are currently not supported by Freqtrade.")
1✔
600

601
    def validate_ordertypes(self, order_types: Dict) -> None:
1✔
602
        """
603
        Checks if order-types configured in strategy/config are supported
604
        """
605
        if any(v == 'market' for k, v in order_types.items()):
1✔
606
            if not self.exchange_has('createMarketOrder'):
1✔
607
                raise OperationalException(
1✔
608
                    f'Exchange {self.name} does not support market orders.')
609

610
        if (order_types.get("stoploss_on_exchange")
1✔
611
                and not self._ft_has.get("stoploss_on_exchange", False)):
612
            raise OperationalException(
1✔
613
                f'On exchange stoploss is not supported for {self.name}.'
614
            )
615

616
    def validate_pricing(self, pricing: Dict) -> None:
1✔
617
        if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'):
1✔
618
            raise OperationalException(f'Orderbook not available for {self.name}.')
1✔
619
        if (not pricing.get('use_order_book', False) and (
1✔
620
                not self.exchange_has('fetchTicker')
621
                or not self._ft_has['tickers_have_price'])):
622
            raise OperationalException(f'Ticker pricing not available for {self.name}.')
1✔
623

624
    def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
1✔
625
        """
626
        Checks if order time in force configured in strategy/config are supported
627
        """
628
        if any(v.upper() not in self._ft_has["order_time_in_force"]
1✔
629
               for k, v in order_time_in_force.items()):
630
            raise OperationalException(
1✔
631
                f'Time in force policies are not supported for {self.name} yet.')
632

633
    def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
1✔
634
        """
635
        Checks if required startup_candles is more than ohlcv_candle_limit().
636
        Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
637
        """
638

639
        candle_limit = self.ohlcv_candle_limit(
1✔
640
            timeframe, self._config['candle_type_def'],
641
            int(date_minus_candles(timeframe, startup_candles).timestamp() * 1000)
642
            if timeframe else None)
643
        # Require one more candle - to account for the still open candle.
644
        candle_count = startup_candles + 1
1✔
645
        # Allow 5 calls to the exchange per pair
646
        required_candle_call_count = int(
1✔
647
            (candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1))
648
        if self._ft_has['ohlcv_has_history']:
1✔
649

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

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

685
    def get_option(self, param: str, default: Any = None) -> Any:
1✔
686
        """
687
        Get parameter value from _ft_has
688
        """
689
        return self._ft_has.get(param, default)
1✔
690

691
    def exchange_has(self, endpoint: str) -> bool:
1✔
692
        """
693
        Checks if exchange implements a specific API endpoint.
694
        Wrapper around ccxt 'has' attribute
695
        :param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
696
        :return: bool
697
        """
698
        return endpoint in self._api.has and self._api.has[endpoint]
1✔
699

700
    def get_precision_amount(self, pair: str) -> Optional[float]:
1✔
701
        """
702
        Returns the amount precision of the exchange.
703
        :param pair: Pair to get precision for
704
        :return: precision for amount or None. Must be used in combination with precisionMode
705
        """
706
        return self.markets.get(pair, {}).get('precision', {}).get('amount', None)
1✔
707

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

716
    def amount_to_precision(self, pair: str, amount: float) -> float:
1✔
717
        """
718
        Returns the amount to buy or sell to a precision the Exchange accepts
719

720
        """
721
        return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
1✔
722

723
    def price_to_precision(self, pair: str, price: float) -> float:
1✔
724
        """
725
        Returns the price rounded up to the precision the Exchange accepts.
726
        Rounds up
727
        """
728
        return price_to_precision(price, self.get_precision_price(pair), self.precisionMode)
1✔
729

730
    def price_get_one_pip(self, pair: str, price: float) -> float:
1✔
731
        """
732
        Get's the "1 pip" value for this pair.
733
        Used in PriceFilter to calculate the 1pip movements.
734
        """
735
        precision = self.markets[pair]['precision']['price']
1✔
736
        if self.precisionMode == TICK_SIZE:
1✔
737
            return precision
1✔
738
        else:
739
            return 1 / pow(10, precision)
1✔
740

741
    def get_min_pair_stake_amount(
1✔
742
        self,
743
        pair: str,
744
        price: float,
745
        stoploss: float,
746
        leverage: Optional[float] = 1.0
747
    ) -> Optional[float]:
748
        return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage)
1✔
749

750
    def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
1✔
751
        max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max')
1✔
752
        if max_stake_amount is None:
1✔
753
            # * Should never be executed
754
            raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
×
755
                                       'never set max_stake_amount to None')
756
        return max_stake_amount / leverage
1✔
757

758
    def _get_stake_amount_limit(
1✔
759
        self,
760
        pair: str,
761
        price: float,
762
        stoploss: float,
763
        limit: Literal['min', 'max'],
764
        leverage: Optional[float] = 1.0
765
    ) -> Optional[float]:
766

767
        isMin = limit == 'min'
1✔
768

769
        try:
1✔
770
            market = self.markets[pair]
1✔
771
        except KeyError:
1✔
772
            raise ValueError(f"Can't get market information for symbol {pair}")
1✔
773

774
        stake_limits = []
1✔
775
        limits = market['limits']
1✔
776
        if (limits['cost'][limit] is not None):
1✔
777
            stake_limits.append(
1✔
778
                self._contracts_to_amount(
779
                    pair,
780
                    limits['cost'][limit]
781
                )
782
            )
783

784
        if (limits['amount'][limit] is not None):
1✔
785
            stake_limits.append(
1✔
786
                self._contracts_to_amount(
787
                    pair,
788
                    limits['amount'][limit] * price
789
                )
790
            )
791

792
        if not stake_limits:
1✔
793
            return None if isMin else float('inf')
1✔
794

795
        # reserve some percent defined in config (5% default) + stoploss
796
        amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent',
1✔
797
                                                        DEFAULT_AMOUNT_RESERVE_PERCENT)
798
        amount_reserve_percent = (
1✔
799
            amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
800
        )
801
        # it should not be more than 50%
802
        amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1)
1✔
803

804
        # The value returned should satisfy both limits: for amount (base currency) and
805
        # for cost (quote, stake currency), so max() is used here.
806
        # See also #2575 at github.
807
        return self._get_stake_amount_considering_leverage(
1✔
808
            max(stake_limits) * amount_reserve_percent,
809
            leverage or 1.0
810
        ) if isMin else min(stake_limits)
811

812
    def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
1✔
813
        """
814
        Takes the minimum stake amount for a pair with no leverage and returns the minimum
815
        stake amount when leverage is considered
816
        :param stake_amount: The stake amount for a pair before leverage is considered
817
        :param leverage: The amount of leverage being used on the current trade
818
        """
819
        return stake_amount / leverage
1✔
820

821
    # Dry-run methods
822

823
    def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
1✔
824
                             rate: float, leverage: float, params: Dict = {},
825
                             stop_loss: bool = False) -> Dict[str, Any]:
826
        order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
1✔
827
        # Rounding here must respect to contract sizes
828
        _amount = self._contracts_to_amount(
1✔
829
            pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)))
830
        dry_order: Dict[str, Any] = {
1✔
831
            'id': order_id,
832
            'symbol': pair,
833
            'price': rate,
834
            'average': rate,
835
            'amount': _amount,
836
            'cost': _amount * rate,
837
            'type': ordertype,
838
            'side': side,
839
            'filled': 0,
840
            'remaining': _amount,
841
            'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
842
            'timestamp': arrow.utcnow().int_timestamp * 1000,
843
            'status': "closed" if ordertype == "market" and not stop_loss else "open",
844
            'fee': None,
845
            'info': {},
846
            'leverage': leverage
847
        }
848
        if stop_loss:
1✔
849
            dry_order["info"] = {"stopPrice": dry_order["price"]}
1✔
850
            dry_order["stopPrice"] = dry_order["price"]
1✔
851
            # Workaround to avoid filling stoploss orders immediately
852
            dry_order["ft_order_type"] = "stoploss"
1✔
853

854
        if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
1✔
855
            # Update market order pricing
856
            average = self.get_dry_market_fill_price(pair, side, amount, rate)
1✔
857
            dry_order.update({
1✔
858
                'average': average,
859
                'filled': _amount,
860
                'remaining': 0.0,
861
                'cost': (dry_order['amount'] * average) / leverage
862
            })
863
            # market orders will always incurr taker fees
864
            dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
1✔
865

866
        dry_order = self.check_dry_limit_order_filled(dry_order, immediate=True)
1✔
867

868
        self._dry_run_open_orders[dry_order["id"]] = dry_order
1✔
869
        # Copy order and close it - so the returned order is open unless it's a market order
870
        return dry_order
1✔
871

872
    def add_dry_order_fee(
1✔
873
        self,
874
        pair: str,
875
        dry_order: Dict[str, Any],
876
        taker_or_maker: MakerTaker,
877
    ) -> Dict[str, Any]:
878
        fee = self.get_fee(pair, taker_or_maker=taker_or_maker)
1✔
879
        dry_order.update({
1✔
880
            'fee': {
881
                'currency': self.get_pair_quote_currency(pair),
882
                'cost': dry_order['cost'] * fee,
883
                'rate': fee
884
            }
885
        })
886
        return dry_order
1✔
887

888
    def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float) -> float:
1✔
889
        """
890
        Get the market order fill price based on orderbook interpolation
891
        """
892
        if self.exchange_has('fetchL2OrderBook'):
1✔
893
            ob = self.fetch_l2_order_book(pair, 20)
1✔
894
            ob_type = 'asks' if side == 'buy' else 'bids'
1✔
895
            slippage = 0.05
1✔
896
            max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
1✔
897

898
            remaining_amount = amount
1✔
899
            filled_amount = 0.0
1✔
900
            book_entry_price = 0.0
1✔
901
            for book_entry in ob[ob_type]:
1✔
902
                book_entry_price = book_entry[0]
1✔
903
                book_entry_coin_volume = book_entry[1]
1✔
904
                if remaining_amount > 0:
1✔
905
                    if remaining_amount < book_entry_coin_volume:
1✔
906
                        # Orderbook at this slot bigger than remaining amount
907
                        filled_amount += remaining_amount * book_entry_price
1✔
908
                        break
1✔
909
                    else:
910
                        filled_amount += book_entry_coin_volume * book_entry_price
1✔
911
                    remaining_amount -= book_entry_coin_volume
1✔
912
                else:
913
                    break
×
914
            else:
915
                # If remaining_amount wasn't consumed completely (break was not called)
916
                filled_amount += remaining_amount * book_entry_price
1✔
917
            forecast_avg_filled_price = max(filled_amount, 0) / amount
1✔
918
            # Limit max. slippage to specified value
919
            if side == 'buy':
1✔
920
                forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
1✔
921

922
            else:
923
                forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
1✔
924

925
            return self.price_to_precision(pair, forecast_avg_filled_price)
1✔
926

927
        return rate
1✔
928

929
    def _is_dry_limit_order_filled(self, pair: str, side: str, limit: float) -> bool:
1✔
930
        if not self.exchange_has('fetchL2OrderBook'):
1✔
931
            return True
1✔
932
        ob = self.fetch_l2_order_book(pair, 1)
1✔
933
        try:
1✔
934
            if side == 'buy':
1✔
935
                price = ob['asks'][0][0]
1✔
936
                logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}")
1✔
937
                if limit >= price:
1✔
938
                    return True
1✔
939
            else:
940
                price = ob['bids'][0][0]
1✔
941
                logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}")
1✔
942
                if limit <= price:
1✔
943
                    return True
1✔
944
        except IndexError:
1✔
945
            # Ignore empty orderbooks when filling - can be filled with the next iteration.
946
            pass
1✔
947
        return False
1✔
948

949
    def check_dry_limit_order_filled(
1✔
950
            self, order: Dict[str, Any], immediate: bool = False) -> Dict[str, Any]:
951
        """
952
        Check dry-run limit order fill and update fee (if it filled).
953
        """
954
        if (order['status'] != "closed"
1✔
955
                and order['type'] in ["limit"]
956
                and not order.get('ft_order_type')):
957
            pair = order['symbol']
1✔
958
            if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
1✔
959
                order.update({
1✔
960
                    'status': 'closed',
961
                    'filled': order['amount'],
962
                    'remaining': 0,
963
                })
964

965
                self.add_dry_order_fee(
1✔
966
                    pair,
967
                    order,
968
                    'taker' if immediate else 'maker',
969
                )
970

971
        return order
1✔
972

973
    def fetch_dry_run_order(self, order_id) -> Dict[str, Any]:
1✔
974
        """
975
        Return dry-run order
976
        Only call if running in dry-run mode.
977
        """
978
        try:
1✔
979
            order = self._dry_run_open_orders[order_id]
1✔
980
            order = self.check_dry_limit_order_filled(order)
1✔
981
            return order
1✔
982
        except KeyError as e:
1✔
983
            from freqtrade.persistence import Order
1✔
984
            order = Order.order_by_id(order_id)
1✔
985
            if order:
1✔
986
                ccxt_order = order.to_ccxt_object()
1✔
987
                self._dry_run_open_orders[order_id] = ccxt_order
1✔
988
                return ccxt_order
1✔
989
            # Gracefully handle errors with dry-run orders.
990
            raise InvalidOrderException(
1✔
991
                f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
992

993
    # Order handling
994

995
    def _lev_prep(self, pair: str, leverage: float, side: BuySell):
1✔
996
        if self.trading_mode != TradingMode.SPOT:
1✔
997
            self.set_margin_mode(pair, self.margin_mode)
1✔
998
            self._set_leverage(leverage, pair)
1✔
999

1000
    def _get_params(
1✔
1001
        self,
1002
        side: BuySell,
1003
        ordertype: str,
1004
        leverage: float,
1005
        reduceOnly: bool,
1006
        time_in_force: str = 'GTC',
1007
    ) -> Dict:
1008
        params = self._params.copy()
1✔
1009
        if time_in_force != 'GTC' and ordertype != 'market':
1✔
1010
            param = self._ft_has.get('time_in_force_parameter', '')
1✔
1011
            params.update({param: time_in_force.upper()})
1✔
1012
        if reduceOnly:
1✔
1013
            params.update({'reduceOnly': True})
1✔
1014
        return params
1✔
1015

1016
    def create_order(
1✔
1017
        self,
1018
        *,
1019
        pair: str,
1020
        ordertype: str,
1021
        side: BuySell,
1022
        amount: float,
1023
        rate: float,
1024
        leverage: float,
1025
        reduceOnly: bool = False,
1026
        time_in_force: str = 'GTC',
1027
    ) -> Dict:
1028
        if self._config['dry_run']:
1✔
1029
            dry_order = self.create_dry_run_order(
1✔
1030
                pair, ordertype, side, amount, self.price_to_precision(pair, rate), leverage)
1031
            return dry_order
1✔
1032

1033
        params = self._get_params(side, ordertype, leverage, reduceOnly, time_in_force)
1✔
1034

1035
        try:
1✔
1036
            # Set the precision for amount and price(rate) as accepted by the exchange
1037
            amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
1✔
1038
            needs_price = (ordertype != 'market'
1✔
1039
                           or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
1040
            rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
1✔
1041

1042
            if not reduceOnly:
1✔
1043
                self._lev_prep(pair, leverage, side)
1✔
1044

1045
            order = self._api.create_order(
1✔
1046
                pair,
1047
                ordertype,
1048
                side,
1049
                amount,
1050
                rate_for_order,
1051
                params,
1052
            )
1053
            self._log_exchange_response('create_order', order)
1✔
1054
            order = self._order_contracts_to_amount(order)
1✔
1055
            return order
1✔
1056

1057
        except ccxt.InsufficientFunds as e:
1✔
1058
            raise InsufficientFundsError(
1✔
1059
                f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
1060
                f'Tried to {side} amount {amount} at rate {rate}.'
1061
                f'Message: {e}') from e
1062
        except ccxt.InvalidOrder as e:
1✔
1063
            raise ExchangeError(
1✔
1064
                f'Could not create {ordertype} {side} order on market {pair}. '
1065
                f'Tried to {side} amount {amount} at rate {rate}. '
1066
                f'Message: {e}') from e
1067
        except ccxt.DDoSProtection as e:
1✔
1068
            raise DDosProtection(e) from e
×
1069
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1070
            raise TemporaryError(
1✔
1071
                f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
1072
        except ccxt.BaseError as e:
1✔
1073
            raise OperationalException(e) from e
1✔
1074

1075
    def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
1✔
1076
        """
1077
        Verify stop_loss against stoploss-order value (limit or price)
1078
        Returns True if adjustment is necessary.
1079
        """
1080
        if not self._ft_has.get('stoploss_on_exchange'):
1✔
1081
            raise OperationalException(f"stoploss is not implemented for {self.name}.")
1✔
1082

1083
        return (
1✔
1084
            order.get('stopPrice', None) is None
1085
            or ((side == "sell" and stop_loss > float(order['stopPrice'])) or
1086
                (side == "buy" and stop_loss < float(order['stopPrice'])))
1087
        )
1088

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

1091
        available_order_Types: Dict[str, str] = self._ft_has["stoploss_order_types"]
1✔
1092

1093
        if user_order_type in available_order_Types.keys():
1✔
1094
            ordertype = available_order_Types[user_order_type]
1✔
1095
        else:
1096
            # Otherwise pick only one available
1097
            ordertype = list(available_order_Types.values())[0]
1✔
1098
            user_order_type = list(available_order_Types.keys())[0]
1✔
1099
        return ordertype, user_order_type
1✔
1100

1101
    def _get_stop_limit_rate(self, stop_price: float, order_types: Dict, side: str) -> float:
1✔
1102
        # Limit price threshold: As limit price should always be below stop-price
1103
        limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
1✔
1104
        if side == "sell":
1✔
1105
            limit_rate = stop_price * limit_price_pct
1✔
1106
        else:
1107
            limit_rate = stop_price * (2 - limit_price_pct)
1✔
1108

1109
        bad_stop_price = ((stop_price <= limit_rate) if side ==
1✔
1110
                          "sell" else (stop_price >= limit_rate))
1111
        # Ensure rate is less than stop price
1112
        if bad_stop_price:
1✔
1113
            raise OperationalException(
1✔
1114
                'In stoploss limit order, stop price should be more than limit price')
1115
        return limit_rate
1✔
1116

1117
    def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
1✔
1118
        params = self._params.copy()
1✔
1119
        # Verify if stopPrice works for your exchange!
1120
        params.update({'stopPrice': stop_price})
1✔
1121
        return params
1✔
1122

1123
    @retrier(retries=0)
1✔
1124
    def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict,
1✔
1125
                 side: BuySell, leverage: float) -> Dict:
1126
        """
1127
        creates a stoploss order.
1128
        requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
1129
            to the corresponding exchange type.
1130

1131
        The precise ordertype is determined by the order_types dict or exchange default.
1132

1133
        The exception below should never raise, since we disallow
1134
        starting the bot in validate_ordertypes()
1135

1136
        This may work with a limited number of other exchanges, but correct working
1137
            needs to be tested individually.
1138
        WARNING: setting `stoploss_on_exchange` to True will NOT auto-enable stoploss on exchange.
1139
            `stoploss_adjust` must still be implemented for this to work.
1140
        """
1141
        if not self._ft_has['stoploss_on_exchange']:
1✔
1142
            raise OperationalException(f"stoploss is not implemented for {self.name}.")
1✔
1143

1144
        user_order_type = order_types.get('stoploss', 'market')
1✔
1145
        ordertype, user_order_type = self._get_stop_order_type(user_order_type)
1✔
1146

1147
        stop_price_norm = self.price_to_precision(pair, stop_price)
1✔
1148
        limit_rate = None
1✔
1149
        if user_order_type == 'limit':
1✔
1150
            limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
1✔
1151
            limit_rate = self.price_to_precision(pair, limit_rate)
1✔
1152

1153
        if self._config['dry_run']:
1✔
1154
            dry_order = self.create_dry_run_order(
1✔
1155
                pair,
1156
                ordertype,
1157
                side,
1158
                amount,
1159
                stop_price_norm,
1160
                stop_loss=True,
1161
                leverage=leverage,
1162
            )
1163
            return dry_order
1✔
1164

1165
        try:
1✔
1166
            params = self._get_stop_params(side=side, ordertype=ordertype,
1✔
1167
                                           stop_price=stop_price_norm)
1168
            if self.trading_mode == TradingMode.FUTURES:
1✔
1169
                params['reduceOnly'] = True
1✔
1170

1171
            amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
1✔
1172

1173
            self._lev_prep(pair, leverage, side)
1✔
1174
            order = self._api.create_order(symbol=pair, type=ordertype, side=side,
1✔
1175
                                           amount=amount, price=limit_rate, params=params)
1176
            self._log_exchange_response('create_stoploss_order', order)
1✔
1177
            order = self._order_contracts_to_amount(order)
1✔
1178
            logger.info(f"stoploss {user_order_type} order added for {pair}. "
1✔
1179
                        f"stop price: {stop_price}. limit: {limit_rate}")
1180
            return order
1✔
1181
        except ccxt.InsufficientFunds as e:
1✔
1182
            raise InsufficientFundsError(
1✔
1183
                f'Insufficient funds to create {ordertype} sell order on market {pair}. '
1184
                f'Tried to sell amount {amount} at rate {limit_rate}. '
1185
                f'Message: {e}') from e
1186
        except ccxt.InvalidOrder as e:
1✔
1187
            # Errors:
1188
            # `Order would trigger immediately.`
1189
            raise InvalidOrderException(
1✔
1190
                f'Could not create {ordertype} sell order on market {pair}. '
1191
                f'Tried to sell amount {amount} at rate {limit_rate}. '
1192
                f'Message: {e}') from e
1193
        except ccxt.DDoSProtection as e:
1✔
1194
            raise DDosProtection(e) from e
1✔
1195
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1196
            raise TemporaryError(
1✔
1197
                f"Could not place stoploss order due to {e.__class__.__name__}. "
1198
                f"Message: {e}") from e
1199
        except ccxt.BaseError as e:
1✔
1200
            raise OperationalException(e) from e
1✔
1201

1202
    @retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
1✔
1203
    def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
1✔
1204
        if self._config['dry_run']:
1✔
1205
            return self.fetch_dry_run_order(order_id)
1✔
1206
        try:
1✔
1207
            order = self._api.fetch_order(order_id, pair, params=params)
1✔
1208
            self._log_exchange_response('fetch_order', order)
1✔
1209
            order = self._order_contracts_to_amount(order)
1✔
1210
            return order
1✔
1211
        except ccxt.OrderNotFound as e:
1✔
1212
            raise RetryableOrderError(
1✔
1213
                f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
1214
        except ccxt.InvalidOrder as e:
1✔
1215
            raise InvalidOrderException(
1✔
1216
                f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e
1217
        except ccxt.DDoSProtection as e:
1✔
1218
            raise DDosProtection(e) from e
1✔
1219
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1220
            raise TemporaryError(
1✔
1221
                f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
1222
        except ccxt.BaseError as e:
1✔
1223
            raise OperationalException(e) from e
1✔
1224

1225
    def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
1✔
1226
        return self.fetch_order(order_id, pair, params)
1✔
1227

1228
    def fetch_order_or_stoploss_order(self, order_id: str, pair: str,
1✔
1229
                                      stoploss_order: bool = False) -> Dict:
1230
        """
1231
        Simple wrapper calling either fetch_order or fetch_stoploss_order depending on
1232
        the stoploss_order parameter
1233
        :param order_id: OrderId to fetch order
1234
        :param pair: Pair corresponding to order_id
1235
        :param stoploss_order: If true, uses fetch_stoploss_order, otherwise fetch_order.
1236
        """
1237
        if stoploss_order:
1✔
1238
            return self.fetch_stoploss_order(order_id, pair)
1✔
1239
        return self.fetch_order(order_id, pair)
1✔
1240

1241
    def check_order_canceled_empty(self, order: Dict) -> bool:
1✔
1242
        """
1243
        Verify if an order has been cancelled without being partially filled
1244
        :param order: Order dict as returned from fetch_order()
1245
        :return: True if order has been cancelled without being filled, False otherwise.
1246
        """
1247
        return (order.get('status') in NON_OPEN_EXCHANGE_STATES
1✔
1248
                and order.get('filled') == 0.0)
1249

1250
    @retrier
1✔
1251
    def cancel_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
1✔
1252
        if self._config['dry_run']:
1✔
1253
            try:
1✔
1254
                order = self.fetch_dry_run_order(order_id)
1✔
1255

1256
                order.update({'status': 'canceled', 'filled': 0.0, 'remaining': order['amount']})
1✔
1257
                return order
1✔
1258
            except InvalidOrderException:
1✔
1259
                return {}
1✔
1260

1261
        try:
1✔
1262
            order = self._api.cancel_order(order_id, pair, params=params)
1✔
1263
            self._log_exchange_response('cancel_order', order)
1✔
1264
            order = self._order_contracts_to_amount(order)
1✔
1265
            return order
1✔
1266
        except ccxt.InvalidOrder as e:
1✔
1267
            raise InvalidOrderException(
1✔
1268
                f'Could not cancel order. Message: {e}') from e
1269
        except ccxt.DDoSProtection as e:
1✔
1270
            raise DDosProtection(e) from e
1✔
1271
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1272
            raise TemporaryError(
1✔
1273
                f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
1274
        except ccxt.BaseError as e:
1✔
1275
            raise OperationalException(e) from e
1✔
1276

1277
    def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
1✔
1278
        return self.cancel_order(order_id, pair, params)
1✔
1279

1280
    def is_cancel_order_result_suitable(self, corder) -> bool:
1✔
1281
        if not isinstance(corder, dict):
1✔
1282
            return False
1✔
1283

1284
        required = ('fee', 'status', 'amount')
1✔
1285
        return all(corder.get(k, None) is not None for k in required)
1✔
1286

1287
    def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
1✔
1288
        """
1289
        Cancel order returning a result.
1290
        Creates a fake result if cancel order returns a non-usable result
1291
        and fetch_order does not work (certain exchanges don't return cancelled orders)
1292
        :param order_id: Orderid to cancel
1293
        :param pair: Pair corresponding to order_id
1294
        :param amount: Amount to use for fake response
1295
        :return: Result from either cancel_order if usable, or fetch_order
1296
        """
1297
        try:
1✔
1298
            corder = self.cancel_order(order_id, pair)
1✔
1299
            if self.is_cancel_order_result_suitable(corder):
1✔
1300
                return corder
1✔
1301
        except InvalidOrderException:
1✔
1302
            logger.warning(f"Could not cancel order {order_id} for {pair}.")
1✔
1303
        try:
1✔
1304
            order = self.fetch_order(order_id, pair)
1✔
1305
        except InvalidOrderException:
1✔
1306
            logger.warning(f"Could not fetch cancelled order {order_id}.")
1✔
1307
            order = {
1✔
1308
                'id': order_id,
1309
                'status': 'canceled',
1310
                'amount': amount,
1311
                'filled': 0.0,
1312
                'fee': {},
1313
                'info': {}
1314
            }
1315

1316
        return order
1✔
1317

1318
    def cancel_stoploss_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
1✔
1319
        """
1320
        Cancel stoploss order returning a result.
1321
        Creates a fake result if cancel order returns a non-usable result
1322
        and fetch_order does not work (certain exchanges don't return cancelled orders)
1323
        :param order_id: stoploss-order-id to cancel
1324
        :param pair: Pair corresponding to order_id
1325
        :param amount: Amount to use for fake response
1326
        :return: Result from either cancel_order if usable, or fetch_order
1327
        """
1328
        corder = self.cancel_stoploss_order(order_id, pair)
1✔
1329
        if self.is_cancel_order_result_suitable(corder):
1✔
1330
            return corder
1✔
1331
        try:
1✔
1332
            order = self.fetch_stoploss_order(order_id, pair)
1✔
1333
        except InvalidOrderException:
1✔
1334
            logger.warning(f"Could not fetch cancelled stoploss order {order_id}.")
1✔
1335
            order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
1✔
1336

1337
        return order
1✔
1338

1339
    @retrier
1✔
1340
    def get_balances(self) -> dict:
1✔
1341

1342
        try:
1✔
1343
            balances = self._api.fetch_balance()
1✔
1344
            # Remove additional info from ccxt results
1345
            balances.pop("info", None)
1✔
1346
            balances.pop("free", None)
1✔
1347
            balances.pop("total", None)
1✔
1348
            balances.pop("used", None)
1✔
1349

1350
            return balances
1✔
1351
        except ccxt.DDoSProtection as e:
1✔
1352
            raise DDosProtection(e) from e
1✔
1353
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1354
            raise TemporaryError(
1✔
1355
                f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
1356
        except ccxt.BaseError as e:
1✔
1357
            raise OperationalException(e) from e
1✔
1358

1359
    @retrier
1✔
1360
    def fetch_positions(self, pair: str = None) -> List[Dict]:
1✔
1361
        """
1362
        Fetch positions from the exchange.
1363
        If no pair is given, all positions are returned.
1364
        :param pair: Pair for the query
1365
        """
1366
        if self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES:
1✔
1367
            return []
1✔
1368
        try:
1✔
1369
            symbols = []
1✔
1370
            if pair:
1✔
1371
                symbols.append(pair)
1✔
1372
            positions: List[Dict] = self._api.fetch_positions(symbols)
1✔
1373
            self._log_exchange_response('fetch_positions', positions)
1✔
1374
            return positions
1✔
1375
        except ccxt.DDoSProtection as e:
1✔
1376
            raise DDosProtection(e) from e
1✔
1377
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1378
            raise TemporaryError(
1✔
1379
                f'Could not get positions due to {e.__class__.__name__}. Message: {e}') from e
1380
        except ccxt.BaseError as e:
1✔
1381
            raise OperationalException(e) from e
1✔
1382

1383
    @retrier
1✔
1384
    def fetch_trading_fees(self) -> Dict[str, Any]:
1✔
1385
        """
1386
        Fetch user account trading fees
1387
        Can be cached, should not update often.
1388
        """
1389
        if (self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES
1✔
1390
                or not self.exchange_has('fetchTradingFees')):
1391
            return {}
1✔
1392
        try:
1✔
1393
            trading_fees: Dict[str, Any] = self._api.fetch_trading_fees()
1✔
1394
            self._log_exchange_response('fetch_trading_fees', trading_fees)
1✔
1395
            return trading_fees
1✔
1396
        except ccxt.DDoSProtection as e:
1✔
1397
            raise DDosProtection(e) from e
1✔
1398
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1399
            raise TemporaryError(
1✔
1400
                f'Could not fetch trading fees due to {e.__class__.__name__}. Message: {e}') from e
1401
        except ccxt.BaseError as e:
1✔
1402
            raise OperationalException(e) from e
1✔
1403

1404
    @retrier
1✔
1405
    def fetch_bids_asks(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
1✔
1406
        """
1407
        :param cached: Allow cached result
1408
        :return: fetch_tickers result
1409
        """
1410
        if not self.exchange_has('fetchBidsAsks'):
1✔
1411
            return {}
1✔
1412
        if cached:
1✔
1413
            with self._cache_lock:
1✔
1414
                tickers = self._fetch_tickers_cache.get('fetch_bids_asks')
1✔
1415
            if tickers:
1✔
1416
                return tickers
1✔
1417
        try:
1✔
1418
            tickers = self._api.fetch_bids_asks(symbols)
1✔
1419
            with self._cache_lock:
1✔
1420
                self._fetch_tickers_cache['fetch_bids_asks'] = tickers
1✔
1421
            return tickers
1✔
1422
        except ccxt.NotSupported as e:
1✔
1423
            raise OperationalException(
1✔
1424
                f'Exchange {self._api.name} does not support fetching bids/asks in batch. '
1425
                f'Message: {e}') from e
1426
        except ccxt.DDoSProtection as e:
1✔
1427
            raise DDosProtection(e) from e
1✔
1428
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1429
            raise TemporaryError(
1✔
1430
                f'Could not load bids/asks due to {e.__class__.__name__}. Message: {e}') from e
1431
        except ccxt.BaseError as e:
1✔
1432
            raise OperationalException(e) from e
1✔
1433

1434
    @retrier
1✔
1435
    def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
1✔
1436
        """
1437
        :param cached: Allow cached result
1438
        :return: fetch_tickers result
1439
        """
1440
        tickers: Tickers
1441
        if not self.exchange_has('fetchTickers'):
1✔
1442
            return {}
1✔
1443
        if cached:
1✔
1444
            with self._cache_lock:
1✔
1445
                tickers = self._fetch_tickers_cache.get('fetch_tickers')  # type: ignore
1✔
1446
            if tickers:
1✔
1447
                return tickers
1✔
1448
        try:
1✔
1449
            tickers = self._api.fetch_tickers(symbols)
1✔
1450
            with self._cache_lock:
1✔
1451
                self._fetch_tickers_cache['fetch_tickers'] = tickers
1✔
1452
            return tickers
1✔
1453
        except ccxt.NotSupported as e:
1✔
1454
            raise OperationalException(
1✔
1455
                f'Exchange {self._api.name} does not support fetching tickers in batch. '
1456
                f'Message: {e}') from e
1457
        except ccxt.DDoSProtection as e:
1✔
1458
            raise DDosProtection(e) from e
1✔
1459
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1460
            raise TemporaryError(
1✔
1461
                f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e
1462
        except ccxt.BaseError as e:
1✔
1463
            raise OperationalException(e) from e
1✔
1464

1465
    # Pricing info
1466

1467
    @retrier
1✔
1468
    def fetch_ticker(self, pair: str) -> Ticker:
1✔
1469
        try:
1✔
1470
            if (pair not in self.markets or
1✔
1471
                    self.markets[pair].get('active', False) is False):
1472
                raise ExchangeError(f"Pair {pair} not available")
1✔
1473
            data: Ticker = self._api.fetch_ticker(pair)
1✔
1474
            return data
1✔
1475
        except ccxt.DDoSProtection as e:
1✔
1476
            raise DDosProtection(e) from e
1✔
1477
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1478
            raise TemporaryError(
1✔
1479
                f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e
1480
        except ccxt.BaseError as e:
1✔
1481
            raise OperationalException(e) from e
1✔
1482

1483
    @staticmethod
1✔
1484
    def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]],
1✔
1485
                               range_required: bool = True):
1486
        """
1487
        Get next greater value in the list.
1488
        Used by fetch_l2_order_book if the api only supports a limited range
1489
        """
1490
        if not limit_range:
1✔
1491
            return limit
1✔
1492

1493
        result = min([x for x in limit_range if limit <= x] + [max(limit_range)])
1✔
1494
        if not range_required and limit > result:
1✔
1495
            # Range is not required - we can use None as parameter.
1496
            return None
1✔
1497
        return result
1✔
1498

1499
    @retrier
1✔
1500
    def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
1✔
1501
        """
1502
        Get L2 order book from exchange.
1503
        Can be limited to a certain amount (if supported).
1504
        Returns a dict in the format
1505
        {'asks': [price, volume], 'bids': [price, volume]}
1506
        """
1507
        limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'],
1✔
1508
                                             self._ft_has['l2_limit_range_required'])
1509
        try:
1✔
1510

1511
            return self._api.fetch_l2_order_book(pair, limit1)
1✔
1512
        except ccxt.NotSupported as e:
1✔
1513
            raise OperationalException(
1✔
1514
                f'Exchange {self._api.name} does not support fetching order book.'
1515
                f'Message: {e}') from e
1516
        except ccxt.DDoSProtection as e:
1✔
1517
            raise DDosProtection(e) from e
×
1518
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1519
            raise TemporaryError(
1✔
1520
                f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e
1521
        except ccxt.BaseError as e:
1✔
1522
            raise OperationalException(e) from e
1✔
1523

1524
    def _get_price_side(self, side: str, is_short: bool, conf_strategy: Dict) -> BidAsk:
1✔
1525
        price_side = conf_strategy['price_side']
1✔
1526

1527
        if price_side in ('same', 'other'):
1✔
1528
            price_map = {
1✔
1529
                ('entry', 'long', 'same'): 'bid',
1530
                ('entry', 'long', 'other'): 'ask',
1531
                ('entry', 'short', 'same'): 'ask',
1532
                ('entry', 'short', 'other'): 'bid',
1533
                ('exit', 'long', 'same'): 'ask',
1534
                ('exit', 'long', 'other'): 'bid',
1535
                ('exit', 'short', 'same'): 'bid',
1536
                ('exit', 'short', 'other'): 'ask',
1537
            }
1538
            price_side = price_map[(side, 'short' if is_short else 'long', price_side)]
1✔
1539
        return price_side
1✔
1540

1541
    def get_rate(self, pair: str, refresh: bool,
1✔
1542
                 side: EntryExit, is_short: bool,
1543
                 order_book: Optional[dict] = None, ticker: Optional[Ticker] = None) -> float:
1544
        """
1545
        Calculates bid/ask target
1546
        bid rate - between current ask price and last price
1547
        ask rate - either using ticker bid or first bid based on orderbook
1548
        or remain static in any other case since it's not updating.
1549
        :param pair: Pair to get rate for
1550
        :param refresh: allow cached data
1551
        :param side: "buy" or "sell"
1552
        :return: float: Price
1553
        :raises PricingError if orderbook price could not be determined.
1554
        """
1555
        name = side.capitalize()
1✔
1556
        strat_name = 'entry_pricing' if side == "entry" else 'exit_pricing'
1✔
1557

1558
        cache_rate: TTLCache = self._entry_rate_cache if side == "entry" else self._exit_rate_cache
1✔
1559
        if not refresh:
1✔
1560
            with self._cache_lock:
1✔
1561
                rate = cache_rate.get(pair)
1✔
1562
            # Check if cache has been invalidated
1563
            if rate:
1✔
1564
                logger.debug(f"Using cached {side} rate for {pair}.")
1✔
1565
                return rate
1✔
1566

1567
        conf_strategy = self._config.get(strat_name, {})
1✔
1568

1569
        price_side = self._get_price_side(side, is_short, conf_strategy)
1✔
1570

1571
        price_side_word = price_side.capitalize()
1✔
1572

1573
        if conf_strategy.get('use_order_book', False):
1✔
1574

1575
            order_book_top = conf_strategy.get('order_book_top', 1)
1✔
1576
            if order_book is None:
1✔
1577
                order_book = self.fetch_l2_order_book(pair, order_book_top)
1✔
1578
            logger.debug('order_book %s', order_book)
1✔
1579
            # top 1 = index 0
1580
            try:
1✔
1581
                rate = order_book[f"{price_side}s"][order_book_top - 1][0]
1✔
1582
            except (IndexError, KeyError) as e:
1✔
1583
                logger.warning(
1✔
1584
                    f"{pair} - {name} Price at location {order_book_top} from orderbook "
1585
                    f"could not be determined. Orderbook: {order_book}"
1586
                )
1587
                raise PricingError from e
1✔
1588
            logger.debug(f"{pair} - {name} price from orderbook {price_side_word}"
1✔
1589
                         f"side - top {order_book_top} order book {side} rate {rate:.8f}")
1590
        else:
1591
            logger.debug(f"Using Last {price_side_word} / Last Price")
1✔
1592
            if ticker is None:
1✔
1593
                ticker = self.fetch_ticker(pair)
1✔
1594
            ticker_rate = ticker[price_side]
1✔
1595
            if ticker['last'] and ticker_rate:
1✔
1596
                if side == 'entry' and ticker_rate > ticker['last']:
1✔
1597
                    balance = conf_strategy.get('price_last_balance', 0.0)
1✔
1598
                    ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
1✔
1599
                elif side == 'exit' and ticker_rate < ticker['last']:
1✔
1600
                    balance = conf_strategy.get('price_last_balance', 0.0)
1✔
1601
                    ticker_rate = ticker_rate - balance * (ticker_rate - ticker['last'])
1✔
1602
            rate = ticker_rate
1✔
1603

1604
        if rate is None:
1✔
1605
            raise PricingError(f"{name}-Rate for {pair} was empty.")
1✔
1606
        with self._cache_lock:
1✔
1607
            cache_rate[pair] = rate
1✔
1608

1609
        return rate
1✔
1610

1611
    def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]:
1✔
1612
        entry_rate = None
1✔
1613
        exit_rate = None
1✔
1614
        if not refresh:
1✔
1615
            with self._cache_lock:
1✔
1616
                entry_rate = self._entry_rate_cache.get(pair)
1✔
1617
                exit_rate = self._exit_rate_cache.get(pair)
1✔
1618
            if entry_rate:
1✔
1619
                logger.debug(f"Using cached buy rate for {pair}.")
1✔
1620
            if exit_rate:
1✔
1621
                logger.debug(f"Using cached sell rate for {pair}.")
1✔
1622

1623
        entry_pricing = self._config.get('entry_pricing', {})
1✔
1624
        exit_pricing = self._config.get('exit_pricing', {})
1✔
1625
        order_book = ticker = None
1✔
1626
        if not entry_rate and entry_pricing.get('use_order_book', False):
1✔
1627
            order_book_top = max(entry_pricing.get('order_book_top', 1),
1✔
1628
                                 exit_pricing.get('order_book_top', 1))
1629
            order_book = self.fetch_l2_order_book(pair, order_book_top)
1✔
1630
            entry_rate = self.get_rate(pair, refresh, 'entry', is_short, order_book=order_book)
1✔
1631
        elif not entry_rate:
1✔
1632
            ticker = self.fetch_ticker(pair)
1✔
1633
            entry_rate = self.get_rate(pair, refresh, 'entry', is_short, ticker=ticker)
1✔
1634
        if not exit_rate:
1✔
1635
            exit_rate = self.get_rate(pair, refresh, 'exit',
1✔
1636
                                      is_short, order_book=order_book, ticker=ticker)
1637
        return entry_rate, exit_rate
1✔
1638

1639
    # Fee handling
1640

1641
    @retrier
1✔
1642
    def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
1✔
1643
                             params: Optional[Dict] = None) -> List:
1644
        """
1645
        Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
1646
        The "since" argument passed in is coming from the database and is in UTC,
1647
        as timezone-native datetime object.
1648
        From the python documentation:
1649
            > Naive datetime instances are assumed to represent local time
1650
        Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the
1651
        transformation from local timezone to UTC.
1652
        This works for timezones UTC+ since then the result will contain trades from a few hours
1653
        instead of from the last 5 seconds, however fails for UTC- timezones,
1654
        since we're then asking for trades with a "since" argument in the future.
1655

1656
        :param order_id order_id: Order-id as given when creating the order
1657
        :param pair: Pair the order is for
1658
        :param since: datetime object of the order creation time. Assumes object is in UTC.
1659
        """
1660
        if self._config['dry_run']:
1✔
1661
            return []
1✔
1662
        if not self.exchange_has('fetchMyTrades'):
1✔
1663
            return []
1✔
1664
        try:
1✔
1665
            # Allow 5s offset to catch slight time offsets (discovered in #1185)
1666
            # since needs to be int in milliseconds
1667
            _params = params if params else {}
1✔
1668
            my_trades = self._api.fetch_my_trades(
1✔
1669
                pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000),
1670
                params=_params)
1671
            matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
1✔
1672

1673
            self._log_exchange_response('get_trades_for_order', matched_trades)
1✔
1674

1675
            matched_trades = self._trades_contracts_to_amount(matched_trades)
1✔
1676

1677
            return matched_trades
1✔
1678
        except ccxt.DDoSProtection as e:
1✔
1679
            raise DDosProtection(e) from e
1✔
1680
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1681
            raise TemporaryError(
1✔
1682
                f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e
1683
        except ccxt.BaseError as e:
1✔
1684
            raise OperationalException(e) from e
1✔
1685

1686
    def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
1✔
1687
        return order['id']
1✔
1688

1689
    @retrier
1✔
1690
    def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
1✔
1691
                price: float = 1, taker_or_maker: MakerTaker = 'maker') -> float:
1692
        """
1693
        Retrieve fee from exchange
1694
        :param symbol: Pair
1695
        :param type: Type of order (market, limit, ...)
1696
        :param side: Side of order (buy, sell)
1697
        :param amount: Amount of order
1698
        :param price: Price of order
1699
        :param taker_or_maker: 'maker' or 'taker' (ignored if "type" is provided)
1700
        """
1701
        if type and type == 'market':
1✔
1702
            taker_or_maker = 'taker'
×
1703
        try:
1✔
1704
            if self._config['dry_run'] and self._config.get('fee', None) is not None:
1✔
1705
                return self._config['fee']
1✔
1706
            # validate that markets are loaded before trying to get fee
1707
            if self._api.markets is None or len(self._api.markets) == 0:
1✔
1708
                self._api.load_markets()
1✔
1709

1710
            return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
1✔
1711
                                           price=price, takerOrMaker=taker_or_maker)['rate']
1712
        except ccxt.DDoSProtection as e:
1✔
1713
            raise DDosProtection(e) from e
1✔
1714
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1715
            raise TemporaryError(
1✔
1716
                f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e
1717
        except ccxt.BaseError as e:
1✔
1718
            raise OperationalException(e) from e
1✔
1719

1720
    @staticmethod
1✔
1721
    def order_has_fee(order: Dict) -> bool:
1✔
1722
        """
1723
        Verifies if the passed in order dict has the needed keys to extract fees,
1724
        and that these keys (currency, cost) are not empty.
1725
        :param order: Order or trade (one trade) dict
1726
        :return: True if the fee substructure contains currency and cost, false otherwise
1727
        """
1728
        if not isinstance(order, dict):
1✔
1729
            return False
1✔
1730
        return ('fee' in order and order['fee'] is not None
1✔
1731
                and (order['fee'].keys() >= {'currency', 'cost'})
1732
                and order['fee']['currency'] is not None
1733
                and order['fee']['cost'] is not None
1734
                )
1735

1736
    def calculate_fee_rate(
1✔
1737
            self, fee: Dict, symbol: str, cost: float, amount: float) -> Optional[float]:
1738
        """
1739
        Calculate fee rate if it's not given by the exchange.
1740
        :param fee: ccxt Fee dict - must contain cost / currency / rate
1741
        :param symbol: Symbol of the order
1742
        :param cost: Total cost of the order
1743
        :param amount: Amount of the order
1744
        """
1745
        if fee.get('rate') is not None:
1✔
1746
            return fee.get('rate')
1✔
1747
        fee_curr = fee.get('currency')
1✔
1748
        if fee_curr is None:
1✔
1749
            return None
1✔
1750
        fee_cost = float(fee['cost'])
1✔
1751
        if self._ft_has['fee_cost_in_contracts']:
1✔
1752
            # Convert cost via "contracts" conversion
1753
            fee_cost = self._contracts_to_amount(symbol, fee['cost'])
×
1754

1755
        # Calculate fee based on order details
1756
        if fee_curr == self.get_pair_base_currency(symbol):
1✔
1757
            # Base currency - divide by amount
1758
            return round(fee_cost / amount, 8)
1✔
1759
        elif fee_curr == self.get_pair_quote_currency(symbol):
1✔
1760
            # Quote currency - divide by cost
1761
            return round(fee_cost / cost, 8) if cost else None
1✔
1762
        else:
1763
            # If Fee currency is a different currency
1764
            if not cost:
1✔
1765
                # If cost is None or 0.0 -> falsy, return None
1766
                return None
1✔
1767
            try:
1✔
1768
                comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency'])
1✔
1769
                tick = self.fetch_ticker(comb)
1✔
1770

1771
                fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
1✔
1772
            except ExchangeError:
1✔
1773
                fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
1✔
1774
                if not fee_to_quote_rate:
1✔
1775
                    return None
1✔
1776
            return round((fee_cost * fee_to_quote_rate) / cost, 8)
1✔
1777

1778
    def extract_cost_curr_rate(self, fee: Dict, symbol: str, cost: float,
1✔
1779
                               amount: float) -> Tuple[float, str, Optional[float]]:
1780
        """
1781
        Extract tuple of cost, currency, rate.
1782
        Requires order_has_fee to run first!
1783
        :param fee: ccxt Fee dict - must contain cost / currency / rate
1784
        :param symbol: Symbol of the order
1785
        :param cost: Total cost of the order
1786
        :param amount: Amount of the order
1787
        :return: Tuple with cost, currency, rate of the given fee dict
1788
        """
1789
        return (float(fee['cost']),
1✔
1790
                fee['currency'],
1791
                self.calculate_fee_rate(
1792
                    fee,
1793
                    symbol,
1794
                    cost,
1795
                    amount
1796
                    )
1797
                )
1798

1799
    # Historic data
1800

1801
    def get_historic_ohlcv(self, pair: str, timeframe: str,
1✔
1802
                           since_ms: int, candle_type: CandleType,
1803
                           is_new_pair: bool = False,
1804
                           until_ms: int = None) -> List:
1805
        """
1806
        Get candle history using asyncio and returns the list of candles.
1807
        Handles all async work for this.
1808
        Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call.
1809
        :param pair: Pair to download
1810
        :param timeframe: Timeframe to get data for
1811
        :param since_ms: Timestamp in milliseconds to get history from
1812
        :param until_ms: Timestamp in milliseconds to get history up to
1813
        :param candle_type: '', mark, index, premiumIndex, or funding_rate
1814
        :return: List with candle (OHLCV) data
1815
        """
1816
        pair, _, _, data, _ = self.loop.run_until_complete(
1✔
1817
            self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
1818
                                           since_ms=since_ms, until_ms=until_ms,
1819
                                           is_new_pair=is_new_pair, candle_type=candle_type))
1820
        logger.info(f"Downloaded data for {pair} with length {len(data)}.")
1✔
1821
        return data
1✔
1822

1823
    async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
1✔
1824
                                        since_ms: int, candle_type: CandleType,
1825
                                        is_new_pair: bool = False, raise_: bool = False,
1826
                                        until_ms: Optional[int] = None
1827
                                        ) -> OHLCVResponse:
1828
        """
1829
        Download historic ohlcv
1830
        :param is_new_pair: used by binance subclass to allow "fast" new pair downloading
1831
        :param candle_type: Any of the enum CandleType (must match trading mode!)
1832
        """
1833

1834
        one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
1✔
1835
            timeframe, candle_type, since_ms)
1836
        logger.debug(
1✔
1837
            "one_call: %s msecs (%s)",
1838
            one_call,
1839
            arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True)
1840
        )
1841
        input_coroutines = [self._async_get_candle_history(
1✔
1842
            pair, timeframe, candle_type, since) for since in
1843
            range(since_ms, until_ms or (arrow.utcnow().int_timestamp * 1000), one_call)]
1844

1845
        data: List = []
1✔
1846
        # Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
1847
        for input_coro in chunks(input_coroutines, 100):
1✔
1848

1849
            results = await asyncio.gather(*input_coro, return_exceptions=True)
1✔
1850
            for res in results:
1✔
1851
                if isinstance(res, Exception):
1✔
1852
                    logger.warning(f"Async code raised an exception: {repr(res)}")
1✔
1853
                    if raise_:
1✔
1854
                        raise
×
1855
                    continue
1✔
1856
                else:
1857
                    # Deconstruct tuple if it's not an exception
1858
                    p, _, c, new_data, _ = res
1✔
1859
                    if p == pair and c == candle_type:
1✔
1860
                        data.extend(new_data)
1✔
1861
        # Sort data again after extending the result - above calls return in "async order"
1862
        data = sorted(data, key=lambda x: x[0])
1✔
1863
        return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
1✔
1864

1865
    def _build_coroutine(
1✔
1866
            self, pair: str, timeframe: str, candle_type: CandleType,
1867
            since_ms: Optional[int], cache: bool) -> Coroutine[Any, Any, OHLCVResponse]:
1868
        not_all_data = cache and self.required_candle_call_count > 1
1✔
1869
        if cache and (pair, timeframe, candle_type) in self._klines:
1✔
1870
            candle_limit = self.ohlcv_candle_limit(timeframe, candle_type)
1✔
1871
            min_date = date_minus_candles(timeframe, candle_limit - 5).timestamp()
1✔
1872
            # Check if 1 call can get us updated candles without hole in the data.
1873
            if min_date < self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0):
1✔
1874
                # Cache can be used - do one-off call.
1875
                not_all_data = False
1✔
1876
            else:
1877
                # Time jump detected, evict cache
1878
                logger.info(
1✔
1879
                    f"Time jump detected. Evicting cache for {pair}, {timeframe}, {candle_type}")
1880
                del self._klines[(pair, timeframe, candle_type)]
1✔
1881

1882
        if (not since_ms and (self._ft_has["ohlcv_require_since"] or not_all_data)):
1✔
1883
            # Multiple calls for one pair - to get more history
1884
            one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
1✔
1885
                timeframe, candle_type, since_ms)
1886
            move_to = one_call * self.required_candle_call_count
1✔
1887
            now = timeframe_to_next_date(timeframe)
1✔
1888
            since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
1✔
1889

1890
        if since_ms:
1✔
1891
            return self._async_get_historic_ohlcv(
1✔
1892
                pair, timeframe, since_ms=since_ms, raise_=True, candle_type=candle_type)
1893
        else:
1894
            # One call ... "regular" refresh
1895
            return self._async_get_candle_history(
1✔
1896
                pair, timeframe, since_ms=since_ms, candle_type=candle_type)
1897

1898
    def _build_ohlcv_dl_jobs(
1✔
1899
            self, pair_list: ListPairsWithTimeframes, since_ms: Optional[int],
1900
            cache: bool) -> Tuple[List[Coroutine], List[Tuple[str, str, CandleType]]]:
1901
        """
1902
        Build Coroutines to execute as part of refresh_latest_ohlcv
1903
        """
1904
        input_coroutines: List[Coroutine[Any, Any, OHLCVResponse]] = []
1✔
1905
        cached_pairs = []
1✔
1906
        for pair, timeframe, candle_type in set(pair_list):
1✔
1907
            if (timeframe not in self.timeframes
1✔
1908
                    and candle_type in (CandleType.SPOT, CandleType.FUTURES)):
1909
                logger.warning(
1✔
1910
                    f"Cannot download ({pair}, {timeframe}) combination as this timeframe is "
1911
                    f"not available on {self.name}. Available timeframes are "
1912
                    f"{', '.join(self.timeframes)}.")
1913
                continue
1✔
1914

1915
            if ((pair, timeframe, candle_type) not in self._klines or not cache
1✔
1916
                    or self._now_is_time_to_refresh(pair, timeframe, candle_type)):
1917

1918
                input_coroutines.append(
1✔
1919
                    self._build_coroutine(pair, timeframe, candle_type, since_ms, cache))
1920

1921
            else:
1922
                logger.debug(
1✔
1923
                    f"Using cached candle (OHLCV) data for {pair}, {timeframe}, {candle_type} ..."
1924
                )
1925
                cached_pairs.append((pair, timeframe, candle_type))
1✔
1926

1927
        return input_coroutines, cached_pairs
1✔
1928

1929
    def _process_ohlcv_df(self, pair: str, timeframe: str, c_type: CandleType, ticks: List[List],
1✔
1930
                          cache: bool, drop_incomplete: bool) -> DataFrame:
1931
        # keeping last candle time as last refreshed time of the pair
1932
        if ticks and cache:
1✔
1933
            self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[-1][0] // 1000
1✔
1934
        # keeping parsed dataframe in cache
1935
        ohlcv_df = ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
1✔
1936
                                      drop_incomplete=drop_incomplete)
1937
        if cache:
1✔
1938
            if (pair, timeframe, c_type) in self._klines:
1✔
1939
                old = self._klines[(pair, timeframe, c_type)]
1✔
1940
                # Reassign so we return the updated, combined df
1941
                ohlcv_df = clean_ohlcv_dataframe(concat([old, ohlcv_df], axis=0), timeframe, pair,
1✔
1942
                                                 fill_missing=True, drop_incomplete=False)
1943
                candle_limit = self.ohlcv_candle_limit(timeframe, self._config['candle_type_def'])
1✔
1944
                # Age out old candles
1945
                ohlcv_df = ohlcv_df.tail(candle_limit + self._startup_candle_count)
1✔
1946
                ohlcv_df = ohlcv_df.reset_index(drop=True)
1✔
1947
                self._klines[(pair, timeframe, c_type)] = ohlcv_df
1✔
1948
            else:
1949
                self._klines[(pair, timeframe, c_type)] = ohlcv_df
1✔
1950
        return ohlcv_df
1✔
1951

1952
    def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
1✔
1953
                             since_ms: Optional[int] = None, cache: bool = True,
1954
                             drop_incomplete: Optional[bool] = None
1955
                             ) -> Dict[PairWithTimeframe, DataFrame]:
1956
        """
1957
        Refresh in-memory OHLCV asynchronously and set `_klines` with the result
1958
        Loops asynchronously over pair_list and downloads all pairs async (semi-parallel).
1959
        Only used in the dataprovider.refresh() method.
1960
        :param pair_list: List of 2 element tuples containing pair, interval to refresh
1961
        :param since_ms: time since when to download, in milliseconds
1962
        :param cache: Assign result to _klines. Usefull for one-off downloads like for pairlists
1963
        :param drop_incomplete: Control candle dropping.
1964
            Specifying None defaults to _ohlcv_partial_candle
1965
        :return: Dict of [{(pair, timeframe): Dataframe}]
1966
        """
1967
        logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
1✔
1968

1969
        # Gather coroutines to run
1970
        input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache)
1✔
1971

1972
        results_df = {}
1✔
1973
        # Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
1974
        for input_coro in chunks(input_coroutines, 100):
1✔
1975
            async def gather_stuff():
1✔
1976
                return await asyncio.gather(*input_coro, return_exceptions=True)
1✔
1977

1978
            with self._loop_lock:
1✔
1979
                results = self.loop.run_until_complete(gather_stuff())
1✔
1980

1981
            for res in results:
1✔
1982
                if isinstance(res, Exception):
1✔
1983
                    logger.warning(f"Async code raised an exception: {repr(res)}")
1✔
1984
                    continue
1✔
1985
                # Deconstruct tuple (has 5 elements)
1986
                pair, timeframe, c_type, ticks, drop_hint = res
1✔
1987
                drop_incomplete = drop_hint if drop_incomplete is None else drop_incomplete
1✔
1988
                ohlcv_df = self._process_ohlcv_df(
1✔
1989
                    pair, timeframe, c_type, ticks, cache, drop_incomplete)
1990

1991
                results_df[(pair, timeframe, c_type)] = ohlcv_df
1✔
1992

1993
        # Return cached klines
1994
        for pair, timeframe, c_type in cached_pairs:
1✔
1995
            results_df[(pair, timeframe, c_type)] = self.klines(
1✔
1996
                (pair, timeframe, c_type),
1997
                copy=False
1998
            )
1999

2000
        return results_df
1✔
2001

2002
    def _now_is_time_to_refresh(self, pair: str, timeframe: str, candle_type: CandleType) -> bool:
1✔
2003
        # Timeframe in seconds
2004
        interval_in_sec = timeframe_to_seconds(timeframe)
1✔
2005
        plr = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + interval_in_sec
1✔
2006
        return plr < arrow.utcnow().int_timestamp
1✔
2007

2008
    @retrier_async
1✔
2009
    async def _async_get_candle_history(
1✔
2010
        self,
2011
        pair: str,
2012
        timeframe: str,
2013
        candle_type: CandleType,
2014
        since_ms: Optional[int] = None,
2015
    ) -> OHLCVResponse:
2016
        """
2017
        Asynchronously get candle history data using fetch_ohlcv
2018
        :param candle_type: '', mark, index, premiumIndex, or funding_rate
2019
        returns tuple: (pair, timeframe, ohlcv_list)
2020
        """
2021
        try:
1✔
2022
            # Fetch OHLCV asynchronously
2023
            s = '(' + arrow.get(since_ms // 1000).isoformat() + ') ' if since_ms is not None else ''
1✔
2024
            logger.debug(
1✔
2025
                "Fetching pair %s, %s, interval %s, since %s %s...",
2026
                pair, candle_type, timeframe, since_ms, s
2027
            )
2028
            params = deepcopy(self._ft_has.get('ohlcv_params', {}))
1✔
2029
            candle_limit = self.ohlcv_candle_limit(
1✔
2030
                timeframe, candle_type=candle_type, since_ms=since_ms)
2031

2032
            if candle_type and candle_type != CandleType.SPOT:
1✔
2033
                params.update({'price': candle_type.value})
1✔
2034
            if candle_type != CandleType.FUNDING_RATE:
1✔
2035
                data = await self._api_async.fetch_ohlcv(
1✔
2036
                    pair, timeframe=timeframe, since=since_ms,
2037
                    limit=candle_limit, params=params)
2038
            else:
2039
                # Funding rate
2040
                data = await self._fetch_funding_rate_history(
1✔
2041
                    pair=pair,
2042
                    timeframe=timeframe,
2043
                    limit=candle_limit,
2044
                    since_ms=since_ms,
2045
                )
2046
            # Some exchanges sort OHLCV in ASC order and others in DESC.
2047
            # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
2048
            # while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
2049
            # Only sort if necessary to save computing time
2050
            try:
1✔
2051
                if data and data[0][0] > data[-1][0]:
1✔
2052
                    data = sorted(data, key=lambda x: x[0])
1✔
2053
            except IndexError:
1✔
2054
                logger.exception("Error loading %s. Result was %s.", pair, data)
1✔
2055
                return pair, timeframe, candle_type, [], self._ohlcv_partial_candle
1✔
2056
            logger.debug("Done fetching pair %s, interval %s ...", pair, timeframe)
1✔
2057
            return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
1✔
2058

2059
        except ccxt.NotSupported as e:
1✔
2060
            raise OperationalException(
1✔
2061
                f'Exchange {self._api.name} does not support fetching historical '
2062
                f'candle (OHLCV) data. Message: {e}') from e
2063
        except ccxt.DDoSProtection as e:
1✔
2064
            raise DDosProtection(e) from e
1✔
2065
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2066
            raise TemporaryError(f'Could not fetch historical candle (OHLCV) data '
1✔
2067
                                 f'for pair {pair} due to {e.__class__.__name__}. '
2068
                                 f'Message: {e}') from e
2069
        except ccxt.BaseError as e:
1✔
2070
            raise OperationalException(f'Could not fetch historical candle (OHLCV) data '
1✔
2071
                                       f'for pair {pair}. Message: {e}') from e
2072

2073
    async def _fetch_funding_rate_history(
1✔
2074
        self,
2075
        pair: str,
2076
        timeframe: str,
2077
        limit: int,
2078
        since_ms: Optional[int] = None,
2079
    ) -> List[List]:
2080
        """
2081
        Fetch funding rate history - used to selectively override this by subclasses.
2082
        """
2083
        # Funding rate
2084
        data = await self._api_async.fetch_funding_rate_history(
1✔
2085
            pair, since=since_ms,
2086
            limit=limit)
2087
        # Convert funding rate to candle pattern
2088
        data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
1✔
2089
        return data
1✔
2090

2091
    # Fetch historic trades
2092

2093
    @retrier_async
1✔
2094
    async def _async_fetch_trades(self, pair: str,
1✔
2095
                                  since: Optional[int] = None,
2096
                                  params: Optional[dict] = None) -> List[List]:
2097
        """
2098
        Asyncronously gets trade history using fetch_trades.
2099
        Handles exchange errors, does one call to the exchange.
2100
        :param pair: Pair to fetch trade data for
2101
        :param since: Since as integer timestamp in milliseconds
2102
        returns: List of dicts containing trades
2103
        """
2104
        try:
1✔
2105
            # fetch trades asynchronously
2106
            if params:
1✔
2107
                logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
1✔
2108
                trades = await self._api_async.fetch_trades(pair, params=params, limit=1000)
1✔
2109
            else:
2110
                logger.debug(
1✔
2111
                    "Fetching trades for pair %s, since %s %s...",
2112
                    pair, since,
2113
                    '(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else ''
2114
                )
2115
                trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
1✔
2116
            trades = self._trades_contracts_to_amount(trades)
1✔
2117
            return trades_dict_to_list(trades)
1✔
2118
        except ccxt.NotSupported as e:
1✔
2119
            raise OperationalException(
1✔
2120
                f'Exchange {self._api.name} does not support fetching historical trade data.'
2121
                f'Message: {e}') from e
2122
        except ccxt.DDoSProtection as e:
1✔
2123
            raise DDosProtection(e) from e
1✔
2124
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2125
            raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. '
1✔
2126
                                 f'Message: {e}') from e
2127
        except ccxt.BaseError as e:
1✔
2128
            raise OperationalException(f'Could not fetch trade data. Msg: {e}') from e
1✔
2129

2130
    async def _async_get_trade_history_id(self, pair: str,
1✔
2131
                                          until: int,
2132
                                          since: Optional[int] = None,
2133
                                          from_id: Optional[str] = None) -> Tuple[str, List[List]]:
2134
        """
2135
        Asyncronously gets trade history using fetch_trades
2136
        use this when exchange uses id-based iteration (check `self._trades_pagination`)
2137
        :param pair: Pair to fetch trade data for
2138
        :param since: Since as integer timestamp in milliseconds
2139
        :param until: Until as integer timestamp in milliseconds
2140
        :param from_id: Download data starting with ID (if id is known). Ignores "since" if set.
2141
        returns tuple: (pair, trades-list)
2142
        """
2143

2144
        trades: List[List] = []
1✔
2145

2146
        if not from_id:
1✔
2147
            # Fetch first elements using timebased method to get an ID to paginate on
2148
            # Depending on the Exchange, this can introduce a drift at the start of the interval
2149
            # of up to an hour.
2150
            # e.g. Binance returns the "last 1000" candles within a 1h time interval
2151
            # - so we will miss the first trades.
2152
            t = await self._async_fetch_trades(pair, since=since)
1✔
2153
            # DEFAULT_TRADES_COLUMNS: 0 -> timestamp
2154
            # DEFAULT_TRADES_COLUMNS: 1 -> id
2155
            from_id = t[-1][1]
1✔
2156
            trades.extend(t[:-1])
1✔
2157
        while True:
1✔
2158
            t = await self._async_fetch_trades(pair,
1✔
2159
                                               params={self._trades_pagination_arg: from_id})
2160
            if t:
1✔
2161
                # Skip last id since its the key for the next call
2162
                trades.extend(t[:-1])
1✔
2163
                if from_id == t[-1][1] or t[-1][0] > until:
1✔
2164
                    logger.debug(f"Stopping because from_id did not change. "
1✔
2165
                                 f"Reached {t[-1][0]} > {until}")
2166
                    # Reached the end of the defined-download period - add last trade as well.
2167
                    trades.extend(t[-1:])
1✔
2168
                    break
1✔
2169

2170
                from_id = t[-1][1]
1✔
2171
            else:
2172
                break
×
2173

2174
        return (pair, trades)
1✔
2175

2176
    async def _async_get_trade_history_time(self, pair: str, until: int,
1✔
2177
                                            since: Optional[int] = None) -> Tuple[str, List[List]]:
2178
        """
2179
        Asyncronously gets trade history using fetch_trades,
2180
        when the exchange uses time-based iteration (check `self._trades_pagination`)
2181
        :param pair: Pair to fetch trade data for
2182
        :param since: Since as integer timestamp in milliseconds
2183
        :param until: Until as integer timestamp in milliseconds
2184
        returns tuple: (pair, trades-list)
2185
        """
2186

2187
        trades: List[List] = []
1✔
2188
        # DEFAULT_TRADES_COLUMNS: 0 -> timestamp
2189
        # DEFAULT_TRADES_COLUMNS: 1 -> id
2190
        while True:
1✔
2191
            t = await self._async_fetch_trades(pair, since=since)
1✔
2192
            if t:
1✔
2193
                since = t[-1][0]
1✔
2194
                trades.extend(t)
1✔
2195
                # Reached the end of the defined-download period
2196
                if until and t[-1][0] > until:
1✔
2197
                    logger.debug(
1✔
2198
                        f"Stopping because until was reached. {t[-1][0]} > {until}")
2199
                    break
1✔
2200
            else:
2201
                break
1✔
2202

2203
        return (pair, trades)
1✔
2204

2205
    async def _async_get_trade_history(self, pair: str,
1✔
2206
                                       since: Optional[int] = None,
2207
                                       until: Optional[int] = None,
2208
                                       from_id: Optional[str] = None) -> Tuple[str, List[List]]:
2209
        """
2210
        Async wrapper handling downloading trades using either time or id based methods.
2211
        """
2212

2213
        logger.debug(f"_async_get_trade_history(), pair: {pair}, "
1✔
2214
                     f"since: {since}, until: {until}, from_id: {from_id}")
2215

2216
        if until is None:
1✔
2217
            until = ccxt.Exchange.milliseconds()
×
2218
            logger.debug(f"Exchange milliseconds: {until}")
×
2219

2220
        if self._trades_pagination == 'time':
1✔
2221
            return await self._async_get_trade_history_time(
1✔
2222
                pair=pair, since=since, until=until)
2223
        elif self._trades_pagination == 'id':
1✔
2224
            return await self._async_get_trade_history_id(
1✔
2225
                pair=pair, since=since, until=until, from_id=from_id
2226
            )
2227
        else:
2228
            raise OperationalException(f"Exchange {self.name} does use neither time, "
×
2229
                                       f"nor id based pagination")
2230

2231
    def get_historic_trades(self, pair: str,
1✔
2232
                            since: Optional[int] = None,
2233
                            until: Optional[int] = None,
2234
                            from_id: Optional[str] = None) -> Tuple[str, List]:
2235
        """
2236
        Get trade history data using asyncio.
2237
        Handles all async work and returns the list of candles.
2238
        Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call.
2239
        :param pair: Pair to download
2240
        :param since: Timestamp in milliseconds to get history from
2241
        :param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.
2242
        :param from_id: Download data starting with ID (if id is known)
2243
        :returns List of trade data
2244
        """
2245
        if not self.exchange_has("fetchTrades"):
1✔
2246
            raise OperationalException("This exchange does not support downloading Trades.")
1✔
2247

2248
        with self._loop_lock:
1✔
2249
            return self.loop.run_until_complete(
1✔
2250
                self._async_get_trade_history(pair=pair, since=since,
2251
                                              until=until, from_id=from_id))
2252

2253
    @retrier
1✔
2254
    def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
1✔
2255
        """
2256
        Returns the sum of all funding fees that were exchanged for a pair within a timeframe
2257
        Dry-run handling happens as part of _calculate_funding_fees.
2258
        :param pair: (e.g. ADA/USDT)
2259
        :param since: The earliest time of consideration for calculating funding fees,
2260
            in unix time or as a datetime
2261
        """
2262
        if not self.exchange_has("fetchFundingHistory"):
1✔
2263
            raise OperationalException(
×
2264
                f"fetch_funding_history() is not available using {self.name}"
2265
            )
2266

2267
        if type(since) is datetime:
1✔
2268
            since = int(since.timestamp()) * 1000   # * 1000 for ms
1✔
2269

2270
        try:
1✔
2271
            funding_history = self._api.fetch_funding_history(
1✔
2272
                symbol=pair,
2273
                since=since
2274
            )
2275
            return sum(fee['amount'] for fee in funding_history)
1✔
2276
        except ccxt.DDoSProtection as e:
1✔
2277
            raise DDosProtection(e) from e
1✔
2278
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2279
            raise TemporaryError(
1✔
2280
                f'Could not get funding fees due to {e.__class__.__name__}. Message: {e}') from e
2281
        except ccxt.BaseError as e:
1✔
2282
            raise OperationalException(e) from e
1✔
2283

2284
    @retrier
1✔
2285
    def get_leverage_tiers(self) -> Dict[str, List[Dict]]:
1✔
2286
        try:
1✔
2287
            return self._api.fetch_leverage_tiers()
1✔
2288
        except ccxt.DDoSProtection as e:
1✔
2289
            raise DDosProtection(e) from e
1✔
2290
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2291
            raise TemporaryError(
1✔
2292
                f'Could not load leverage tiers due to {e.__class__.__name__}. Message: {e}'
2293
            ) from e
2294
        except ccxt.BaseError as e:
1✔
2295
            raise OperationalException(e) from e
1✔
2296

2297
    @retrier_async
1✔
2298
    async def get_market_leverage_tiers(self, symbol: str) -> Tuple[str, List[Dict]]:
1✔
2299
        """ Leverage tiers per symbol """
2300
        try:
1✔
2301
            tier = await self._api_async.fetch_market_leverage_tiers(symbol)
1✔
2302
            return symbol, tier
1✔
2303
        except ccxt.DDoSProtection as e:
1✔
2304
            raise DDosProtection(e) from e
1✔
2305
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2306
            raise TemporaryError(
1✔
2307
                f'Could not load leverage tiers for {symbol}'
2308
                f' due to {e.__class__.__name__}. Message: {e}'
2309
            ) from e
2310
        except ccxt.BaseError as e:
1✔
2311
            raise OperationalException(e) from e
1✔
2312

2313
    def load_leverage_tiers(self) -> Dict[str, List[Dict]]:
1✔
2314
        if self.trading_mode == TradingMode.FUTURES:
1✔
2315
            if self.exchange_has('fetchLeverageTiers'):
1✔
2316
                # Fetch all leverage tiers at once
2317
                return self.get_leverage_tiers()
1✔
2318
            elif self.exchange_has('fetchMarketLeverageTiers'):
1✔
2319
                # Must fetch the leverage tiers for each market separately
2320
                # * This is slow(~45s) on Okx, makes ~90 api calls to load all linear swap markets
2321
                markets = self.markets
1✔
2322
                symbols = []
1✔
2323

2324
                for symbol, market in markets.items():
1✔
2325
                    if (self.market_is_future(market)
1✔
2326
                            and market['quote'] == self._config['stake_currency']):
2327
                        symbols.append(symbol)
1✔
2328

2329
                tiers: Dict[str, List[Dict]] = {}
1✔
2330

2331
                tiers_cached = self.load_cached_leverage_tiers(self._config['stake_currency'])
1✔
2332
                if tiers_cached:
1✔
2333
                    tiers = tiers_cached
1✔
2334

2335
                coros = [
1✔
2336
                    self.get_market_leverage_tiers(symbol)
2337
                    for symbol in sorted(symbols) if symbol not in tiers]
2338

2339
                # Be verbose here, as this delays startup by ~1 minute.
2340
                if coros:
1✔
2341
                    logger.info(
1✔
2342
                        f"Initializing leverage_tiers for {len(symbols)} markets. "
2343
                        "This will take about a minute.")
2344
                else:
2345
                    logger.info("Using cached leverage_tiers.")
1✔
2346

2347
                async def gather_results():
1✔
2348
                    return await asyncio.gather(*input_coro, return_exceptions=True)
1✔
2349

2350
                for input_coro in chunks(coros, 100):
1✔
2351

2352
                    with self._loop_lock:
1✔
2353
                        results = self.loop.run_until_complete(gather_results())
1✔
2354

2355
                    for symbol, res in results:
1✔
2356
                        tiers[symbol] = res
1✔
2357
                if len(coros) > 0:
1✔
2358
                    self.cache_leverage_tiers(tiers, self._config['stake_currency'])
1✔
2359
                logger.info(f"Done initializing {len(symbols)} markets.")
1✔
2360

2361
                return tiers
1✔
2362
            else:
2363
                return {}
1✔
2364
        else:
2365
            return {}
1✔
2366

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

2369
        filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json"
1✔
2370
        if not filename.parent.is_dir():
1✔
2371
            filename.parent.mkdir(parents=True)
1✔
2372
        data = {
1✔
2373
            "updated": datetime.now(timezone.utc),
2374
            "data": tiers,
2375
        }
2376
        file_dump_json(filename, data)
1✔
2377

2378
    def load_cached_leverage_tiers(self, stake_currency: str) -> Optional[Dict[str, List[Dict]]]:
1✔
2379
        filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json"
1✔
2380
        if filename.is_file():
1✔
2381
            tiers = file_load_json(filename)
1✔
2382
            updated = tiers.get('updated')
1✔
2383
            if updated:
1✔
2384
                updated_dt = parser.parse(updated)
1✔
2385
                if updated_dt < datetime.now(timezone.utc) - timedelta(weeks=4):
1✔
2386
                    logger.info("Cached leverage tiers are outdated. Will update.")
1✔
2387
                    return None
1✔
2388
            return tiers['data']
1✔
2389
        return None
1✔
2390

2391
    def fill_leverage_tiers(self) -> None:
1✔
2392
        """
2393
        Assigns property _leverage_tiers to a dictionary of information about the leverage
2394
        allowed on each pair
2395
        """
2396
        leverage_tiers = self.load_leverage_tiers()
1✔
2397
        for pair, tiers in leverage_tiers.items():
1✔
2398
            pair_tiers = []
1✔
2399
            for tier in tiers:
1✔
2400
                pair_tiers.append(self.parse_leverage_tier(tier))
1✔
2401
            self._leverage_tiers[pair] = pair_tiers
1✔
2402

2403
    def parse_leverage_tier(self, tier) -> Dict:
1✔
2404
        info = tier.get('info', {})
1✔
2405
        return {
1✔
2406
            'minNotional': tier['minNotional'],
2407
            'maxNotional': tier['maxNotional'],
2408
            'maintenanceMarginRate': tier['maintenanceMarginRate'],
2409
            'maxLeverage': tier['maxLeverage'],
2410
            'maintAmt': float(info['cum']) if 'cum' in info else None,
2411
        }
2412

2413
    def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float:
1✔
2414
        """
2415
        Returns the maximum leverage that a pair can be traded at
2416
        :param pair: The base/quote currency pair being traded
2417
        :stake_amount: The total value of the traders margin_mode in quote currency
2418
        """
2419

2420
        if self.trading_mode == TradingMode.SPOT:
1✔
2421
            return 1.0
1✔
2422

2423
        if self.trading_mode == TradingMode.FUTURES:
1✔
2424

2425
            # Checks and edge cases
2426
            if stake_amount is None:
1✔
2427
                raise OperationalException(
×
2428
                    f'{self.name}.get_max_leverage requires argument stake_amount'
2429
                )
2430

2431
            if pair not in self._leverage_tiers:
1✔
2432
                # Maybe raise exception because it can't be traded on futures?
2433
                return 1.0
1✔
2434

2435
            pair_tiers = self._leverage_tiers[pair]
1✔
2436

2437
            if stake_amount == 0:
1✔
2438
                return self._leverage_tiers[pair][0]['maxLeverage']  # Max lev for lowest amount
1✔
2439

2440
            for tier_index in range(len(pair_tiers)):
1✔
2441

2442
                tier = pair_tiers[tier_index]
1✔
2443
                lev = tier['maxLeverage']
1✔
2444

2445
                if tier_index < len(pair_tiers) - 1:
1✔
2446
                    next_tier = pair_tiers[tier_index + 1]
1✔
2447
                    next_floor = next_tier['minNotional'] / next_tier['maxLeverage']
1✔
2448
                    if next_floor > stake_amount:  # Next tier min too high for stake amount
1✔
2449
                        return min((tier['maxNotional'] / stake_amount), lev)
1✔
2450
                        #
2451
                        # With the two leverage tiers below,
2452
                        # - a stake amount of 150 would mean a max leverage of (10000 / 150) = 66.66
2453
                        # - stakes below 133.33 = max_lev of 75
2454
                        # - stakes between 133.33-200 = max_lev of 10000/stake = 50.01-74.99
2455
                        # - stakes from 200 + 1000 = max_lev of 50
2456
                        #
2457
                        # {
2458
                        #     "min": 0,      # stake = 0.0
2459
                        #     "max": 10000,  # max_stake@75 = 10000/75 = 133.33333333333334
2460
                        #     "lev": 75,
2461
                        # },
2462
                        # {
2463
                        #     "min": 10000,  # stake = 200.0
2464
                        #     "max": 50000,  # max_stake@50 = 50000/50 = 1000.0
2465
                        #     "lev": 50,
2466
                        # }
2467
                        #
2468

2469
                else:  # if on the last tier
2470
                    if stake_amount > tier['maxNotional']:
1✔
2471
                        # If stake is > than max tradeable amount
2472
                        raise InvalidOrderException(f'Amount {stake_amount} too high for {pair}')
1✔
2473
                    else:
2474
                        return tier['maxLeverage']
1✔
2475

2476
            raise OperationalException(
×
2477
                'Looped through all tiers without finding a max leverage. Should never be reached'
2478
            )
2479

2480
        elif self.trading_mode == TradingMode.MARGIN:  # Search markets.limits for max lev
1✔
2481
            market = self.markets[pair]
1✔
2482
            if market['limits']['leverage']['max'] is not None:
1✔
2483
                return market['limits']['leverage']['max']
1✔
2484
            else:
2485
                return 1.0  # Default if max leverage cannot be found
1✔
2486
        else:
2487
            return 1.0
×
2488

2489
    @retrier
1✔
2490
    def _set_leverage(
1✔
2491
        self,
2492
        leverage: float,
2493
        pair: Optional[str] = None,
2494
        trading_mode: Optional[TradingMode] = None
2495
    ):
2496
        """
2497
        Set's the leverage before making a trade, in order to not
2498
        have the same leverage on every trade
2499
        """
2500
        if self._config['dry_run'] or not self.exchange_has("setLeverage"):
×
2501
            # Some exchanges only support one margin_mode type
2502
            return
×
2503

2504
        try:
×
2505
            res = self._api.set_leverage(symbol=pair, leverage=leverage)
×
2506
            self._log_exchange_response('set_leverage', res)
×
2507
        except ccxt.DDoSProtection as e:
×
2508
            raise DDosProtection(e) from e
×
2509
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
×
2510
            raise TemporaryError(
×
2511
                f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
2512
        except ccxt.BaseError as e:
×
2513
            raise OperationalException(e) from e
×
2514

2515
    def get_interest_rate(self) -> float:
1✔
2516
        """
2517
        Retrieve interest rate - necessary for Margin trading.
2518
        Should not call the exchange directly when used from backtesting.
2519
        """
2520
        return 0.0
×
2521

2522
    def funding_fee_cutoff(self, open_date: datetime):
1✔
2523
        """
2524
        :param open_date: The open date for a trade
2525
        :return: The cutoff open time for when a funding fee is charged
2526
        """
2527
        return open_date.minute > 0 or open_date.second > 0
1✔
2528

2529
    @retrier
1✔
2530
    def set_margin_mode(self, pair: str, margin_mode: MarginMode, params: dict = {}):
1✔
2531
        """
2532
        Set's the margin mode on the exchange to cross or isolated for a specific pair
2533
        :param pair: base/quote currency pair (e.g. "ADA/USDT")
2534
        """
2535
        if self._config['dry_run'] or not self.exchange_has("setMarginMode"):
1✔
2536
            # Some exchanges only support one margin_mode type
2537
            return
1✔
2538

2539
        try:
1✔
2540
            res = self._api.set_margin_mode(margin_mode.value, pair, params)
1✔
2541
            self._log_exchange_response('set_margin_mode', res)
×
2542
        except ccxt.DDoSProtection as e:
1✔
2543
            raise DDosProtection(e) from e
1✔
2544
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2545
            raise TemporaryError(
1✔
2546
                f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
2547
        except ccxt.BaseError as e:
1✔
2548
            raise OperationalException(e) from e
1✔
2549

2550
    def _fetch_and_calculate_funding_fees(
1✔
2551
        self,
2552
        pair: str,
2553
        amount: float,
2554
        is_short: bool,
2555
        open_date: datetime,
2556
        close_date: Optional[datetime] = None
2557
    ) -> float:
2558
        """
2559
        Fetches and calculates the sum of all funding fees that occurred for a pair
2560
        during a futures trade.
2561
        Only used during dry-run or if the exchange does not provide a funding_rates endpoint.
2562
        :param pair: The quote/base pair of the trade
2563
        :param amount: The quantity of the trade
2564
        :param is_short: trade direction
2565
        :param open_date: The date and time that the trade started
2566
        :param close_date: The date and time that the trade ended
2567
        """
2568

2569
        if self.funding_fee_cutoff(open_date):
1✔
2570
            open_date += timedelta(hours=1)
1✔
2571
        timeframe = self._ft_has['mark_ohlcv_timeframe']
1✔
2572
        timeframe_ff = self._ft_has.get('funding_fee_timeframe',
1✔
2573
                                        self._ft_has['mark_ohlcv_timeframe'])
2574

2575
        if not close_date:
1✔
2576
            close_date = datetime.now(timezone.utc)
1✔
2577
        open_timestamp = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000
1✔
2578
        # close_timestamp = int(close_date.timestamp()) * 1000
2579

2580
        mark_comb: PairWithTimeframe = (
1✔
2581
            pair, timeframe, CandleType.from_string(self._ft_has["mark_ohlcv_price"]))
2582

2583
        funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE)
1✔
2584
        candle_histories = self.refresh_latest_ohlcv(
1✔
2585
            [mark_comb, funding_comb],
2586
            since_ms=open_timestamp,
2587
            cache=False,
2588
            drop_incomplete=False,
2589
        )
2590
        try:
1✔
2591
            # we can't assume we always get histories - for example during exchange downtimes
2592
            funding_rates = candle_histories[funding_comb]
1✔
2593
            mark_rates = candle_histories[mark_comb]
1✔
2594
        except KeyError:
1✔
2595
            raise ExchangeError("Could not find funding rates.") from None
1✔
2596

2597
        funding_mark_rates = self.combine_funding_and_mark(
1✔
2598
            funding_rates=funding_rates, mark_rates=mark_rates)
2599

2600
        return self.calculate_funding_fees(
1✔
2601
            funding_mark_rates,
2602
            amount=amount,
2603
            is_short=is_short,
2604
            open_date=open_date,
2605
            close_date=close_date
2606
        )
2607

2608
    @staticmethod
1✔
2609
    def combine_funding_and_mark(funding_rates: DataFrame, mark_rates: DataFrame,
1✔
2610
                                 futures_funding_rate: Optional[int] = None) -> DataFrame:
2611
        """
2612
        Combine funding-rates and mark-rates dataframes
2613
        :param funding_rates: Dataframe containing Funding rates (Type FUNDING_RATE)
2614
        :param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price)
2615
        :param futures_funding_rate: Fake funding rate to use if funding_rates are not available
2616
        """
2617
        if futures_funding_rate is None:
1✔
2618
            return mark_rates.merge(
1✔
2619
                funding_rates, on='date', how="inner", suffixes=["_mark", "_fund"])
2620
        else:
2621
            if len(funding_rates) == 0:
1✔
2622
                # No funding rate candles - full fillup with fallback variable
2623
                mark_rates['open_fund'] = futures_funding_rate
1✔
2624
                return mark_rates.rename(
1✔
2625
                        columns={'open': 'open_mark',
2626
                                 'close': 'close_mark',
2627
                                 'high': 'high_mark',
2628
                                 'low': 'low_mark',
2629
                                 'volume': 'volume_mark'})
2630

2631
            else:
2632
                # Fill up missing funding_rate candles with fallback value
2633
                combined = mark_rates.merge(
1✔
2634
                    funding_rates, on='date', how="outer", suffixes=["_mark", "_fund"]
2635
                    )
2636
                combined['open_fund'] = combined['open_fund'].fillna(futures_funding_rate)
1✔
2637
                return combined
1✔
2638

2639
    def calculate_funding_fees(
1✔
2640
        self,
2641
        df: DataFrame,
2642
        amount: float,
2643
        is_short: bool,
2644
        open_date: datetime,
2645
        close_date: Optional[datetime] = None,
2646
        time_in_ratio: Optional[float] = None
2647
    ) -> float:
2648
        """
2649
        calculates the sum of all funding fees that occurred for a pair during a futures trade
2650
        :param df: Dataframe containing combined funding and mark rates
2651
                   as `open_fund` and `open_mark`.
2652
        :param amount: The quantity of the trade
2653
        :param is_short: trade direction
2654
        :param open_date: The date and time that the trade started
2655
        :param close_date: The date and time that the trade ended
2656
        :param time_in_ratio: Not used by most exchange classes
2657
        """
2658
        fees: float = 0
1✔
2659

2660
        if not df.empty:
1✔
2661
            df = df[(df['date'] >= open_date) & (df['date'] <= close_date)]
1✔
2662
            fees = sum(df['open_fund'] * df['open_mark'] * amount)
1✔
2663

2664
        # Negate fees for longs as funding_fees expects it this way based on live endpoints.
2665
        return fees if is_short else -fees
1✔
2666

2667
    def get_funding_fees(
1✔
2668
            self, pair: str, amount: float, is_short: bool, open_date: datetime) -> float:
2669
        """
2670
        Fetch funding fees, either from the exchange (live) or calculates them
2671
        based on funding rate/mark price history
2672
        :param pair: The quote/base pair of the trade
2673
        :param is_short: trade direction
2674
        :param amount: Trade amount
2675
        :param open_date: Open date of the trade
2676
        :return: funding fee since open_date
2677
        :raies: ExchangeError if something goes wrong.
2678
        """
2679
        if self.trading_mode == TradingMode.FUTURES:
1✔
2680
            if self._config['dry_run']:
1✔
2681
                funding_fees = self._fetch_and_calculate_funding_fees(
1✔
2682
                    pair, amount, is_short, open_date)
2683
            else:
2684
                funding_fees = self._get_funding_fees_from_exchange(pair, open_date)
×
2685
            return funding_fees
1✔
2686
        else:
2687
            return 0.0
1✔
2688

2689
    def get_liquidation_price(
1✔
2690
        self,
2691
        pair: str,
2692
        # Dry-run
2693
        open_rate: float,   # Entry price of position
2694
        is_short: bool,
2695
        amount: float,  # Absolute value of position size
2696
        stake_amount: float,
2697
        wallet_balance: float,
2698
        mm_ex_1: float = 0.0,  # (Binance) Cross only
2699
        upnl_ex_1: float = 0.0,  # (Binance) Cross only
2700
    ) -> Optional[float]:
2701
        """
2702
        Set's the margin mode on the exchange to cross or isolated for a specific pair
2703
        """
2704
        if self.trading_mode == TradingMode.SPOT:
1✔
2705
            return None
1✔
2706
        elif (self.trading_mode != TradingMode.FUTURES):
1✔
2707
            raise OperationalException(
1✔
2708
                f"{self.name} does not support {self.margin_mode} {self.trading_mode}")
2709

2710
        isolated_liq = None
1✔
2711
        if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
1✔
2712

2713
            isolated_liq = self.dry_run_liquidation_price(
1✔
2714
                pair=pair,
2715
                open_rate=open_rate,
2716
                is_short=is_short,
2717
                amount=amount,
2718
                stake_amount=stake_amount,
2719
                wallet_balance=wallet_balance,
2720
                mm_ex_1=mm_ex_1,
2721
                upnl_ex_1=upnl_ex_1
2722
            )
2723
        else:
2724
            positions = self.fetch_positions(pair)
1✔
2725
            if len(positions) > 0:
1✔
2726
                pos = positions[0]
1✔
2727
                isolated_liq = pos['liquidationPrice']
1✔
2728

2729
        if isolated_liq:
1✔
2730
            buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
1✔
2731
            isolated_liq = (
1✔
2732
                isolated_liq - buffer_amount
2733
                if is_short else
2734
                isolated_liq + buffer_amount
2735
            )
2736
            return isolated_liq
1✔
2737
        else:
2738
            return None
1✔
2739

2740
    def dry_run_liquidation_price(
1✔
2741
        self,
2742
        pair: str,
2743
        open_rate: float,   # Entry price of position
2744
        is_short: bool,
2745
        amount: float,
2746
        stake_amount: float,
2747
        wallet_balance: float,  # Or margin balance
2748
        mm_ex_1: float = 0.0,  # (Binance) Cross only
2749
        upnl_ex_1: float = 0.0,  # (Binance) Cross only
2750
    ) -> Optional[float]:
2751
        """
2752
        Important: Must be fetching data from cached values as this is used by backtesting!
2753
        PERPETUAL:
2754
         gateio: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-price
2755
         > Liquidation Price = (Entry Price ± Margin / Contract Multiplier / Size) /
2756
                                [ 1 ± (Maintenance Margin Ratio + Taker Rate)]
2757
            Wherein, "+" or "-" depends on whether the contract goes long or short:
2758
            "-" for long, and "+" for short.
2759

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

2763
        :param pair: Pair to calculate liquidation price for
2764
        :param open_rate: Entry price of position
2765
        :param is_short: True if the trade is a short, false otherwise
2766
        :param amount: Absolute value of position size incl. leverage (in base currency)
2767
        :param stake_amount: Stake amount - Collateral in settle currency.
2768
        :param trading_mode: SPOT, MARGIN, FUTURES, etc.
2769
        :param margin_mode: Either ISOLATED or CROSS
2770
        :param wallet_balance: Amount of margin_mode in the wallet being used to trade
2771
            Cross-Margin Mode: crossWalletBalance
2772
            Isolated-Margin Mode: isolatedWalletBalance
2773

2774
        # * Not required by Gateio or OKX
2775
        :param mm_ex_1:
2776
        :param upnl_ex_1:
2777
        """
2778

2779
        market = self.markets[pair]
1✔
2780
        taker_fee_rate = market['taker']
1✔
2781
        mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount)
1✔
2782

2783
        if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
1✔
2784

2785
            if market['inverse']:
1✔
2786
                raise OperationalException(
×
2787
                    "Freqtrade does not yet support inverse contracts")
2788

2789
            value = wallet_balance / amount
1✔
2790

2791
            mm_ratio_taker = (mm_ratio + taker_fee_rate)
1✔
2792
            if is_short:
1✔
2793
                return (open_rate + value) / (1 + mm_ratio_taker)
1✔
2794
            else:
2795
                return (open_rate - value) / (1 - mm_ratio_taker)
1✔
2796
        else:
2797
            raise OperationalException(
×
2798
                "Freqtrade only supports isolated futures for leverage trading")
2799

2800
    def get_maintenance_ratio_and_amt(
1✔
2801
        self,
2802
        pair: str,
2803
        nominal_value: float,
2804
    ) -> Tuple[float, Optional[float]]:
2805
        """
2806
        Important: Must be fetching data from cached values as this is used by backtesting!
2807
        :param pair: Market symbol
2808
        :param nominal_value: The total trade amount in quote currency including leverage
2809
        maintenance amount only on Binance
2810
        :return: (maintenance margin ratio, maintenance amount)
2811
        """
2812

2813
        if (self._config.get('runmode') in OPTIMIZE_MODES
1✔
2814
                or self.exchange_has('fetchLeverageTiers')
2815
                or self.exchange_has('fetchMarketLeverageTiers')):
2816

2817
            if pair not in self._leverage_tiers:
1✔
2818
                raise InvalidOrderException(
1✔
2819
                    f"Maintenance margin rate for {pair} is unavailable for {self.name}"
2820
                )
2821

2822
            pair_tiers = self._leverage_tiers[pair]
1✔
2823

2824
            for tier in reversed(pair_tiers):
1✔
2825
                if nominal_value >= tier['minNotional']:
1✔
2826
                    return (tier['maintenanceMarginRate'], tier['maintAmt'])
1✔
2827

2828
            raise OperationalException("nominal value can not be lower than 0")
1✔
2829
            # The lowest notional_floor for any pair in fetch_leverage_tiers is always 0 because it
2830
            # describes the min amt for a tier, and the lowest tier will always go down to 0
2831
        else:
2832
            raise OperationalException(f"Cannot get maintenance ratio using {self.name}")
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc