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

freqtrade / freqtrade / 4131164979

pending completion
4131164979

push

github-actions

Matthias
filled-date shouldn't update again

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

17024 of 17946 relevant lines covered (94.86%)

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 inspect
1✔
7
import logging
1✔
8
from copy import deepcopy
1✔
9
from datetime import datetime, timedelta, timezone
1✔
10
from threading import Lock
1✔
11
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
1✔
12

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

21
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, BidAsk,
1✔
22
                                 BuySell, Config, EntryExit, ListPairsWithTimeframes, MakerTaker,
23
                                 PairWithTimeframe)
24
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
1✔
25
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, TradingMode
1✔
26
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
1✔
27
                                  InvalidOrderException, OperationalException, PricingError,
28
                                  RetryableOrderError, TemporaryError)
29
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_credentials, retrier,
1✔
30
                                       retrier_async)
31
from freqtrade.exchange.exchange_utils import (CcxtModuleType, amount_to_contract_precision,
1✔
32
                                               amount_to_contracts, amount_to_precision,
33
                                               contracts_to_amount, date_minus_candles,
34
                                               is_exchange_known_ccxt, market_is_active,
35
                                               price_to_precision, timeframe_to_minutes,
36
                                               timeframe_to_msecs, timeframe_to_next_date,
37
                                               timeframe_to_prev_date, timeframe_to_seconds)
38
from freqtrade.exchange.types import OHLCVResponse, Ticker, Tickers
1✔
39
from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_json,
1✔
40
                            safe_value_fallback2)
41
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
1✔
42

43

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

46

47
class Exchange:
1✔
48

49
    # Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
50
    _params: Dict = {}
1✔
51

52
    # Additional parameters - added to the ccxt object
53
    _ccxt_params: Dict = {}
1✔
54

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

85
    _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
1✔
86
        # TradingMode.SPOT always supported and not required in this list
87
    ]
88

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

108
        self._config.update(config)
1✔
109

110
        # Holds last candle refreshed time of each pair
111
        self._pairs_last_refresh_time: Dict[PairWithTimeframe, int] = {}
1✔
112
        # Timestamp of last markets refresh
113
        self._last_markets_refresh: int = 0
1✔
114

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

124
        # Holds candles
125
        self._klines: Dict[PairWithTimeframe, DataFrame] = {}
1✔
126

127
        # Holds all open sell orders for dry_run
128
        self._dry_run_open_orders: Dict[str, Any] = {}
1✔
129
        remove_credentials(config)
1✔
130

131
        if config['dry_run']:
1✔
132
            logger.info('Instance is running with dry_run enabled')
1✔
133
        logger.info(f"Using CCXT {ccxt.__version__}")
1✔
134
        exchange_config = config['exchange']
1✔
135
        self.log_responses = exchange_config.get('log_responses', False)
1✔
136

137
        # Leverage properties
138
        self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
1✔
139
        self.margin_mode: MarginMode = (
1✔
140
            MarginMode(config.get('margin_mode'))
141
            if config.get('margin_mode')
142
            else MarginMode.NONE
143
        )
144
        self.liquidation_buffer = config.get('liquidation_buffer', 0.05)
1✔
145

146
        # Deep merge ft_has with default ft_has options
147
        self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
1✔
148
        if self.trading_mode == TradingMode.FUTURES:
1✔
149
            self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
1✔
150
        if exchange_config.get('_ft_has_params'):
1✔
151
            self._ft_has = deep_merge_dicts(exchange_config.get('_ft_has_params'),
1✔
152
                                            self._ft_has)
153
            logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
1✔
154

155
        # Assign this directly for easy access
156
        self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle']
1✔
157

158
        self._trades_pagination = self._ft_has['trades_pagination']
1✔
159
        self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
1✔
160

161
        # Initialize ccxt objects
162
        ccxt_config = self._ccxt_config
1✔
163
        ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config)
1✔
164
        ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config)
1✔
165

166
        self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config)
1✔
167

168
        ccxt_async_config = self._ccxt_config
1✔
169
        ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}),
1✔
170
                                             ccxt_async_config)
171
        ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}),
1✔
172
                                             ccxt_async_config)
173
        self._api_async = self._init_ccxt(
1✔
174
            exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config)
175

176
        logger.info(f'Using Exchange "{self.name}"')
1✔
177
        self.required_candle_call_count = 1
1✔
178
        if validate:
1✔
179
            # Initial markets load
180
            self._load_markets()
1✔
181
            self.validate_config(config)
1✔
182
            self._startup_candle_count: int = config.get('startup_candle_count', 0)
1✔
183
            self.required_candle_call_count = self.validate_required_startup_candles(
1✔
184
                self._startup_candle_count, config.get('timeframe', ''))
185

186
        # Converts the interval provided in minutes in config to seconds
187
        self.markets_refresh_interval: int = exchange_config.get(
1✔
188
            "markets_refresh_interval", 60) * 60
189

190
        if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
1✔
191
            self.fill_leverage_tiers()
1✔
192
        self.additional_exchange_init()
1✔
193

194
    def __del__(self):
1✔
195
        """
196
        Destructor - clean up async stuff
197
        """
198
        self.close()
1✔
199

200
    def close(self):
1✔
201
        logger.debug("Exchange object destroyed, closing async loop")
1✔
202
        if (self._api_async and inspect.iscoroutinefunction(self._api_async.close)
1✔
203
                and self._api_async.session):
204
            logger.debug("Closing async ccxt session.")
×
205
            self.loop.run_until_complete(self._api_async.close())
×
206

207
    def validate_config(self, config):
1✔
208
        # Check if timeframe is available
209
        self.validate_timeframes(config.get('timeframe'))
1✔
210

211
        # Check if all pairs are available
212
        self.validate_stakecurrency(config['stake_currency'])
1✔
213
        if not config['exchange'].get('skip_pair_validation'):
1✔
214
            self.validate_pairs(config['exchange']['pair_whitelist'])
1✔
215
        self.validate_ordertypes(config.get('order_types', {}))
1✔
216
        self.validate_order_time_in_force(config.get('order_time_in_force', {}))
1✔
217
        self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
1✔
218
        self.validate_pricing(config['exit_pricing'])
1✔
219
        self.validate_pricing(config['entry_pricing'])
1✔
220

221
    def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
1✔
222
                   ccxt_kwargs: Dict = {}) -> ccxt.Exchange:
223
        """
224
        Initialize ccxt with given config and return valid
225
        ccxt instance.
226
        """
227
        # Find matching class for the given exchange name
228
        name = exchange_config['name']
1✔
229

230
        if not is_exchange_known_ccxt(name, ccxt_module):
1✔
231
            raise OperationalException(f'Exchange {name} is not supported by ccxt')
1✔
232

233
        ex_config = {
1✔
234
            'apiKey': exchange_config.get('key'),
235
            'secret': exchange_config.get('secret'),
236
            'password': exchange_config.get('password'),
237
            'uid': exchange_config.get('uid', ''),
238
        }
239
        if ccxt_kwargs:
1✔
240
            logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
1✔
241
        if self._ccxt_params:
1✔
242
            # Inject static options after the above output to not confuse users.
243
            ccxt_kwargs = deep_merge_dicts(self._ccxt_params, ccxt_kwargs)
1✔
244
        if ccxt_kwargs:
1✔
245
            ex_config.update(ccxt_kwargs)
1✔
246
        try:
1✔
247

248
            api = getattr(ccxt_module, name.lower())(ex_config)
1✔
249
        except (KeyError, AttributeError) as e:
1✔
250
            raise OperationalException(f'Exchange {name} is not supported') from e
1✔
251
        except ccxt.BaseError as e:
1✔
252
            raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
1✔
253

254
        self.set_sandbox(api, exchange_config, name)
1✔
255

256
        return api
1✔
257

258
    @property
1✔
259
    def _ccxt_config(self) -> Dict:
1✔
260
        # Parameters to add directly to ccxt sync/async initialization.
261
        if self.trading_mode == TradingMode.MARGIN:
1✔
262
            return {
1✔
263
                "options": {
264
                    "defaultType": "margin"
265
                }
266
            }
267
        elif self.trading_mode == TradingMode.FUTURES:
1✔
268
            return {
1✔
269
                "options": {
270
                    "defaultType": self._ft_has["ccxt_futures_name"]
271
                }
272
            }
273
        else:
274
            return {}
1✔
275

276
    @property
1✔
277
    def name(self) -> str:
1✔
278
        """exchange Name (from ccxt)"""
279
        return self._api.name
1✔
280

281
    @property
1✔
282
    def id(self) -> str:
1✔
283
        """exchange ccxt id"""
284
        return self._api.id
×
285

286
    @property
1✔
287
    def timeframes(self) -> List[str]:
1✔
288
        return list((self._api.timeframes or {}).keys())
1✔
289

290
    @property
1✔
291
    def markets(self) -> Dict:
1✔
292
        """exchange ccxt markets"""
293
        if not self._markets:
1✔
294
            logger.info("Markets were not loaded. Loading them now..")
1✔
295
            self._load_markets()
1✔
296
        return self._markets
1✔
297

298
    @property
1✔
299
    def precisionMode(self) -> int:
1✔
300
        """exchange ccxt precisionMode"""
301
        return self._api.precisionMode
1✔
302

303
    def additional_exchange_init(self) -> None:
1✔
304
        """
305
        Additional exchange initialization logic.
306
        .api will be available at this point.
307
        Must be overridden in child methods if required.
308
        """
309
        pass
1✔
310

311
    def _log_exchange_response(self, endpoint, response) -> None:
1✔
312
        """ Log exchange responses """
313
        if self.log_responses:
1✔
314
            logger.info(f"API {endpoint}: {response}")
1✔
315

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

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

342
        if base_currencies:
1✔
343
            markets = {k: v for k, v in markets.items() if v['base'] in base_currencies}
1✔
344
        if quote_currencies:
1✔
345
            markets = {k: v for k, v in markets.items() if v['quote'] in quote_currencies}
1✔
346
        if tradable_only:
1✔
347
            markets = {k: v for k, v in markets.items() if self.market_is_tradable(v)}
1✔
348
        if spot_only:
1✔
349
            markets = {k: v for k, v in markets.items() if self.market_is_spot(v)}
1✔
350
        if margin_only:
1✔
351
            markets = {k: v for k, v in markets.items() if self.market_is_margin(v)}
×
352
        if futures_only:
1✔
353
            markets = {k: v for k, v in markets.items() if self.market_is_future(v)}
1✔
354
        if active_only:
1✔
355
            markets = {k: v for k, v in markets.items() if market_is_active(v)}
1✔
356
        return markets
1✔
357

358
    def get_quote_currencies(self) -> List[str]:
1✔
359
        """
360
        Return a list of supported quote currencies
361
        """
362
        markets = self.markets
1✔
363
        return sorted(set([x['quote'] for _, x in markets.items()]))
1✔
364

365
    def get_pair_quote_currency(self, pair: str) -> str:
1✔
366
        """ Return a pair's quote currency (base/quote:settlement) """
367
        return self.markets.get(pair, {}).get('quote', '')
1✔
368

369
    def get_pair_base_currency(self, pair: str) -> str:
1✔
370
        """ Return a pair's base currency (base/quote:settlement) """
371
        return self.markets.get(pair, {}).get('base', '')
1✔
372

373
    def market_is_future(self, market: Dict[str, Any]) -> bool:
1✔
374
        return (
1✔
375
            market.get(self._ft_has["ccxt_futures_name"], False) is True and
376
            market.get('linear', False) is True
377
        )
378

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

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

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

401
    def klines(self, pair_interval: PairWithTimeframe, copy: bool = True) -> DataFrame:
1✔
402
        if pair_interval in self._klines:
1✔
403
            return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
1✔
404
        else:
405
            return DataFrame()
1✔
406

407
    def get_contract_size(self, pair: str) -> Optional[float]:
1✔
408
        if self.trading_mode == TradingMode.FUTURES:
1✔
409
            market = self.markets.get(pair, {})
1✔
410
            contract_size: float = 1.0
1✔
411
            if not market:
1✔
412
                return None
1✔
413
            if market.get('contractSize') is not None:
1✔
414
                # ccxt has contractSize in markets as string
415
                contract_size = float(market['contractSize'])
1✔
416
            return contract_size
1✔
417
        else:
418
            return 1
1✔
419

420
    def _trades_contracts_to_amount(self, trades: List) -> List:
1✔
421
        if len(trades) > 0 and 'symbol' in trades[0]:
1✔
422
            contract_size = self.get_contract_size(trades[0]['symbol'])
1✔
423
            if contract_size != 1:
1✔
424
                for trade in trades:
1✔
425
                    trade['amount'] = trade['amount'] * contract_size
1✔
426
        return trades
1✔
427

428
    def _order_contracts_to_amount(self, order: Dict) -> Dict:
1✔
429
        if 'symbol' in order and order['symbol'] is not None:
1✔
430
            contract_size = self.get_contract_size(order['symbol'])
1✔
431
            if contract_size != 1:
1✔
432
                for prop in self._ft_has.get('order_props_in_contracts', []):
1✔
433
                    if prop in order and order[prop] is not None:
1✔
434
                        order[prop] = order[prop] * contract_size
1✔
435
        return order
1✔
436

437
    def _amount_to_contracts(self, pair: str, amount: float) -> float:
1✔
438

439
        contract_size = self.get_contract_size(pair)
1✔
440
        return amount_to_contracts(amount, contract_size)
1✔
441

442
    def _contracts_to_amount(self, pair: str, num_contracts: float) -> float:
1✔
443

444
        contract_size = self.get_contract_size(pair)
1✔
445
        return contracts_to_amount(num_contracts, contract_size)
1✔
446

447
    def amount_to_contract_precision(self, pair: str, amount: float) -> float:
1✔
448
        """
449
        Helper wrapper around amount_to_contract_precision
450
        """
451
        contract_size = self.get_contract_size(pair)
1✔
452

453
        return amount_to_contract_precision(amount, self.get_precision_amount(pair),
1✔
454
                                            self.precisionMode, contract_size)
455

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

603
        if (order_types.get("stoploss_on_exchange")
1✔
604
                and not self._ft_has.get("stoploss_on_exchange", False)):
605
            raise OperationalException(
1✔
606
                f'On exchange stoploss is not supported for {self.name}.'
607
            )
608

609
    def validate_pricing(self, pricing: Dict) -> None:
1✔
610
        if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'):
1✔
611
            raise OperationalException(f'Orderbook not available for {self.name}.')
1✔
612
        if (not pricing.get('use_order_book', False) and (
1✔
613
                not self.exchange_has('fetchTicker')
614
                or not self._ft_has['tickers_have_price'])):
615
            raise OperationalException(f'Ticker pricing not available for {self.name}.')
1✔
616

617
    def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
1✔
618
        """
619
        Checks if order time in force configured in strategy/config are supported
620
        """
621
        if any(v.upper() not in self._ft_has["order_time_in_force"]
1✔
622
               for k, v in order_time_in_force.items()):
623
            raise OperationalException(
1✔
624
                f'Time in force policies are not supported for {self.name} yet.')
625

626
    def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
1✔
627
        """
628
        Checks if required startup_candles is more than ohlcv_candle_limit().
629
        Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
630
        """
631

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

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

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

678
    def get_option(self, param: str, default: Optional[Any] = None) -> Any:
1✔
679
        """
680
        Get parameter value from _ft_has
681
        """
682
        return self._ft_has.get(param, default)
1✔
683

684
    def exchange_has(self, endpoint: str) -> bool:
1✔
685
        """
686
        Checks if exchange implements a specific API endpoint.
687
        Wrapper around ccxt 'has' attribute
688
        :param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
689
        :return: bool
690
        """
691
        return endpoint in self._api.has and self._api.has[endpoint]
1✔
692

693
    def get_precision_amount(self, pair: str) -> Optional[float]:
1✔
694
        """
695
        Returns the amount precision of the exchange.
696
        :param pair: Pair to get precision for
697
        :return: precision for amount or None. Must be used in combination with precisionMode
698
        """
699
        return self.markets.get(pair, {}).get('precision', {}).get('amount', None)
1✔
700

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

709
    def amount_to_precision(self, pair: str, amount: float) -> float:
1✔
710
        """
711
        Returns the amount to buy or sell to a precision the Exchange accepts
712

713
        """
714
        return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
1✔
715

716
    def price_to_precision(self, pair: str, price: float) -> float:
1✔
717
        """
718
        Returns the price rounded up to the precision the Exchange accepts.
719
        Rounds up
720
        """
721
        return price_to_precision(price, self.get_precision_price(pair), self.precisionMode)
1✔
722

723
    def price_get_one_pip(self, pair: str, price: float) -> float:
1✔
724
        """
725
        Get's the "1 pip" value for this pair.
726
        Used in PriceFilter to calculate the 1pip movements.
727
        """
728
        precision = self.markets[pair]['precision']['price']
1✔
729
        if self.precisionMode == TICK_SIZE:
1✔
730
            return precision
1✔
731
        else:
732
            return 1 / pow(10, precision)
1✔
733

734
    def get_min_pair_stake_amount(
1✔
735
        self,
736
        pair: str,
737
        price: float,
738
        stoploss: float,
739
        leverage: Optional[float] = 1.0
740
    ) -> Optional[float]:
741
        return self._get_stake_amount_limit(pair, price, stoploss, 'min', leverage)
1✔
742

743
    def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
1✔
744
        max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max')
1✔
745
        if max_stake_amount is None:
1✔
746
            # * Should never be executed
747
            raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
×
748
                                       'never set max_stake_amount to None')
749
        return max_stake_amount / leverage
1✔
750

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

760
        isMin = limit == 'min'
1✔
761

762
        try:
1✔
763
            market = self.markets[pair]
1✔
764
        except KeyError:
1✔
765
            raise ValueError(f"Can't get market information for symbol {pair}")
1✔
766

767
        stake_limits = []
1✔
768
        limits = market['limits']
1✔
769
        if (limits['cost'][limit] is not None):
1✔
770
            stake_limits.append(
1✔
771
                self._contracts_to_amount(
772
                    pair,
773
                    limits['cost'][limit]
774
                )
775
            )
776

777
        if (limits['amount'][limit] is not None):
1✔
778
            stake_limits.append(
1✔
779
                self._contracts_to_amount(
780
                    pair,
781
                    limits['amount'][limit] * price
782
                )
783
            )
784

785
        if not stake_limits:
1✔
786
            return None if isMin else float('inf')
1✔
787

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

797
        # The value returned should satisfy both limits: for amount (base currency) and
798
        # for cost (quote, stake currency), so max() is used here.
799
        # See also #2575 at github.
800
        return self._get_stake_amount_considering_leverage(
1✔
801
            max(stake_limits) * amount_reserve_percent,
802
            leverage or 1.0
803
        ) if isMin else min(stake_limits)
804

805
    def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
1✔
806
        """
807
        Takes the minimum stake amount for a pair with no leverage and returns the minimum
808
        stake amount when leverage is considered
809
        :param stake_amount: The stake amount for a pair before leverage is considered
810
        :param leverage: The amount of leverage being used on the current trade
811
        """
812
        return stake_amount / leverage
1✔
813

814
    # Dry-run methods
815

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

847
        if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
1✔
848
            # Update market order pricing
849
            average = self.get_dry_market_fill_price(pair, side, amount, rate)
1✔
850
            dry_order.update({
1✔
851
                'average': average,
852
                'filled': _amount,
853
                'remaining': 0.0,
854
                'cost': (dry_order['amount'] * average) / leverage
855
            })
856
            # market orders will always incurr taker fees
857
            dry_order = self.add_dry_order_fee(pair, dry_order, 'taker')
1✔
858

859
        dry_order = self.check_dry_limit_order_filled(dry_order, immediate=True)
1✔
860

861
        self._dry_run_open_orders[dry_order["id"]] = dry_order
1✔
862
        # Copy order and close it - so the returned order is open unless it's a market order
863
        return dry_order
1✔
864

865
    def add_dry_order_fee(
1✔
866
        self,
867
        pair: str,
868
        dry_order: Dict[str, Any],
869
        taker_or_maker: MakerTaker,
870
    ) -> Dict[str, Any]:
871
        fee = self.get_fee(pair, taker_or_maker=taker_or_maker)
1✔
872
        dry_order.update({
1✔
873
            'fee': {
874
                'currency': self.get_pair_quote_currency(pair),
875
                'cost': dry_order['cost'] * fee,
876
                'rate': fee
877
            }
878
        })
879
        return dry_order
1✔
880

881
    def get_dry_market_fill_price(self, pair: str, side: str, amount: float, rate: float) -> float:
1✔
882
        """
883
        Get the market order fill price based on orderbook interpolation
884
        """
885
        if self.exchange_has('fetchL2OrderBook'):
1✔
886
            ob = self.fetch_l2_order_book(pair, 20)
1✔
887
            ob_type = 'asks' if side == 'buy' else 'bids'
1✔
888
            slippage = 0.05
1✔
889
            max_slippage_val = rate * ((1 + slippage) if side == 'buy' else (1 - slippage))
1✔
890

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

915
            else:
916
                forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
1✔
917

918
            return self.price_to_precision(pair, forecast_avg_filled_price)
1✔
919

920
        return rate
1✔
921

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

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

958
                self.add_dry_order_fee(
1✔
959
                    pair,
960
                    order,
961
                    'taker' if immediate else 'maker',
962
                )
963

964
        return order
1✔
965

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

986
    # Order handling
987

988
    def _lev_prep(self, pair: str, leverage: float, side: BuySell):
1✔
989
        if self.trading_mode != TradingMode.SPOT:
1✔
990
            self.set_margin_mode(pair, self.margin_mode)
1✔
991
            self._set_leverage(leverage, pair)
1✔
992

993
    def _get_params(
1✔
994
        self,
995
        side: BuySell,
996
        ordertype: str,
997
        leverage: float,
998
        reduceOnly: bool,
999
        time_in_force: str = 'GTC',
1000
    ) -> Dict:
1001
        params = self._params.copy()
1✔
1002
        if time_in_force != 'GTC' and ordertype != 'market':
1✔
1003
            param = self._ft_has.get('time_in_force_parameter', '')
1✔
1004
            params.update({param: time_in_force.upper()})
1✔
1005
        if reduceOnly:
1✔
1006
            params.update({'reduceOnly': True})
1✔
1007
        return params
1✔
1008

1009
    def create_order(
1✔
1010
        self,
1011
        *,
1012
        pair: str,
1013
        ordertype: str,
1014
        side: BuySell,
1015
        amount: float,
1016
        rate: float,
1017
        leverage: float,
1018
        reduceOnly: bool = False,
1019
        time_in_force: str = 'GTC',
1020
    ) -> Dict:
1021
        if self._config['dry_run']:
1✔
1022
            dry_order = self.create_dry_run_order(
1✔
1023
                pair, ordertype, side, amount, self.price_to_precision(pair, rate), leverage)
1024
            return dry_order
1✔
1025

1026
        params = self._get_params(side, ordertype, leverage, reduceOnly, time_in_force)
1✔
1027

1028
        try:
1✔
1029
            # Set the precision for amount and price(rate) as accepted by the exchange
1030
            amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
1✔
1031
            needs_price = (ordertype != 'market'
1✔
1032
                           or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
1033
            rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
1✔
1034

1035
            if not reduceOnly:
1✔
1036
                self._lev_prep(pair, leverage, side)
1✔
1037

1038
            order = self._api.create_order(
1✔
1039
                pair,
1040
                ordertype,
1041
                side,
1042
                amount,
1043
                rate_for_order,
1044
                params,
1045
            )
1046
            self._log_exchange_response('create_order', order)
1✔
1047
            order = self._order_contracts_to_amount(order)
1✔
1048
            return order
1✔
1049

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

1068
    def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
1✔
1069
        """
1070
        Verify stop_loss against stoploss-order value (limit or price)
1071
        Returns True if adjustment is necessary.
1072
        """
1073
        if not self._ft_has.get('stoploss_on_exchange'):
1✔
1074
            raise OperationalException(f"stoploss is not implemented for {self.name}.")
1✔
1075

1076
        return (
1✔
1077
            order.get('stopPrice', None) is None
1078
            or ((side == "sell" and stop_loss > float(order['stopPrice'])) or
1079
                (side == "buy" and stop_loss < float(order['stopPrice'])))
1080
        )
1081

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

1084
        available_order_Types: Dict[str, str] = self._ft_has["stoploss_order_types"]
1✔
1085

1086
        if user_order_type in available_order_Types.keys():
1✔
1087
            ordertype = available_order_Types[user_order_type]
1✔
1088
        else:
1089
            # Otherwise pick only one available
1090
            ordertype = list(available_order_Types.values())[0]
1✔
1091
            user_order_type = list(available_order_Types.keys())[0]
1✔
1092
        return ordertype, user_order_type
1✔
1093

1094
    def _get_stop_limit_rate(self, stop_price: float, order_types: Dict, side: str) -> float:
1✔
1095
        # Limit price threshold: As limit price should always be below stop-price
1096
        limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
1✔
1097
        if side == "sell":
1✔
1098
            limit_rate = stop_price * limit_price_pct
1✔
1099
        else:
1100
            limit_rate = stop_price * (2 - limit_price_pct)
1✔
1101

1102
        bad_stop_price = ((stop_price <= limit_rate) if side ==
1✔
1103
                          "sell" else (stop_price >= limit_rate))
1104
        # Ensure rate is less than stop price
1105
        if bad_stop_price:
1✔
1106
            raise OperationalException(
1✔
1107
                'In stoploss limit order, stop price should be more than limit price')
1108
        return limit_rate
1✔
1109

1110
    def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
1✔
1111
        params = self._params.copy()
1✔
1112
        # Verify if stopPrice works for your exchange!
1113
        params.update({'stopPrice': stop_price})
1✔
1114
        return params
1✔
1115

1116
    @retrier(retries=0)
1✔
1117
    def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict,
1✔
1118
                 side: BuySell, leverage: float) -> Dict:
1119
        """
1120
        creates a stoploss order.
1121
        requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
1122
            to the corresponding exchange type.
1123

1124
        The precise ordertype is determined by the order_types dict or exchange default.
1125

1126
        The exception below should never raise, since we disallow
1127
        starting the bot in validate_ordertypes()
1128

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

1137
        user_order_type = order_types.get('stoploss', 'market')
1✔
1138
        ordertype, user_order_type = self._get_stop_order_type(user_order_type)
1✔
1139

1140
        stop_price_norm = self.price_to_precision(pair, stop_price)
1✔
1141
        limit_rate = None
1✔
1142
        if user_order_type == 'limit':
1✔
1143
            limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
1✔
1144
            limit_rate = self.price_to_precision(pair, limit_rate)
1✔
1145

1146
        if self._config['dry_run']:
1✔
1147
            dry_order = self.create_dry_run_order(
1✔
1148
                pair,
1149
                ordertype,
1150
                side,
1151
                amount,
1152
                stop_price_norm,
1153
                stop_loss=True,
1154
                leverage=leverage,
1155
            )
1156
            return dry_order
1✔
1157

1158
        try:
1✔
1159
            params = self._get_stop_params(side=side, ordertype=ordertype,
1✔
1160
                                           stop_price=stop_price_norm)
1161
            if self.trading_mode == TradingMode.FUTURES:
1✔
1162
                params['reduceOnly'] = True
1✔
1163

1164
            amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
1✔
1165

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

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

1218
    def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
1✔
1219
        return self.fetch_order(order_id, pair, params)
1✔
1220

1221
    def fetch_order_or_stoploss_order(self, order_id: str, pair: str,
1✔
1222
                                      stoploss_order: bool = False) -> Dict:
1223
        """
1224
        Simple wrapper calling either fetch_order or fetch_stoploss_order depending on
1225
        the stoploss_order parameter
1226
        :param order_id: OrderId to fetch order
1227
        :param pair: Pair corresponding to order_id
1228
        :param stoploss_order: If true, uses fetch_stoploss_order, otherwise fetch_order.
1229
        """
1230
        if stoploss_order:
1✔
1231
            return self.fetch_stoploss_order(order_id, pair)
1✔
1232
        return self.fetch_order(order_id, pair)
1✔
1233

1234
    def check_order_canceled_empty(self, order: Dict) -> bool:
1✔
1235
        """
1236
        Verify if an order has been cancelled without being partially filled
1237
        :param order: Order dict as returned from fetch_order()
1238
        :return: True if order has been cancelled without being filled, False otherwise.
1239
        """
1240
        return (order.get('status') in NON_OPEN_EXCHANGE_STATES
1✔
1241
                and order.get('filled') == 0.0)
1242

1243
    @retrier
1✔
1244
    def cancel_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
1✔
1245
        if self._config['dry_run']:
1✔
1246
            try:
1✔
1247
                order = self.fetch_dry_run_order(order_id)
1✔
1248

1249
                order.update({'status': 'canceled', 'filled': 0.0, 'remaining': order['amount']})
1✔
1250
                return order
1✔
1251
            except InvalidOrderException:
1✔
1252
                return {}
1✔
1253

1254
        try:
1✔
1255
            order = self._api.cancel_order(order_id, pair, params=params)
1✔
1256
            self._log_exchange_response('cancel_order', order)
1✔
1257
            order = self._order_contracts_to_amount(order)
1✔
1258
            return order
1✔
1259
        except ccxt.InvalidOrder as e:
1✔
1260
            raise InvalidOrderException(
1✔
1261
                f'Could not cancel order. Message: {e}') from e
1262
        except ccxt.DDoSProtection as e:
1✔
1263
            raise DDosProtection(e) from e
1✔
1264
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1265
            raise TemporaryError(
1✔
1266
                f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
1267
        except ccxt.BaseError as e:
1✔
1268
            raise OperationalException(e) from e
1✔
1269

1270
    def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict:
1✔
1271
        return self.cancel_order(order_id, pair, params)
1✔
1272

1273
    def is_cancel_order_result_suitable(self, corder) -> bool:
1✔
1274
        if not isinstance(corder, dict):
1✔
1275
            return False
1✔
1276

1277
        required = ('fee', 'status', 'amount')
1✔
1278
        return all(corder.get(k, None) is not None for k in required)
1✔
1279

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

1309
        return order
1✔
1310

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

1330
        return order
1✔
1331

1332
    @retrier
1✔
1333
    def get_balances(self) -> dict:
1✔
1334

1335
        try:
1✔
1336
            balances = self._api.fetch_balance()
1✔
1337
            # Remove additional info from ccxt results
1338
            balances.pop("info", None)
1✔
1339
            balances.pop("free", None)
1✔
1340
            balances.pop("total", None)
1✔
1341
            balances.pop("used", None)
1✔
1342

1343
            return balances
1✔
1344
        except ccxt.DDoSProtection as e:
1✔
1345
            raise DDosProtection(e) from e
1✔
1346
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1347
            raise TemporaryError(
1✔
1348
                f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
1349
        except ccxt.BaseError as e:
1✔
1350
            raise OperationalException(e) from e
1✔
1351

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

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

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

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

1458
    # Pricing info
1459

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

1476
    @staticmethod
1✔
1477
    def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]],
1✔
1478
                               range_required: bool = True):
1479
        """
1480
        Get next greater value in the list.
1481
        Used by fetch_l2_order_book if the api only supports a limited range
1482
        """
1483
        if not limit_range:
1✔
1484
            return limit
1✔
1485

1486
        result = min([x for x in limit_range if limit <= x] + [max(limit_range)])
1✔
1487
        if not range_required and limit > result:
1✔
1488
            # Range is not required - we can use None as parameter.
1489
            return None
1✔
1490
        return result
1✔
1491

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

1504
            return self._api.fetch_l2_order_book(pair, limit1)
1✔
1505
        except ccxt.NotSupported as e:
1✔
1506
            raise OperationalException(
1✔
1507
                f'Exchange {self._api.name} does not support fetching order book.'
1508
                f'Message: {e}') from e
1509
        except ccxt.DDoSProtection as e:
1✔
1510
            raise DDosProtection(e) from e
×
1511
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1512
            raise TemporaryError(
1✔
1513
                f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e
1514
        except ccxt.BaseError as e:
1✔
1515
            raise OperationalException(e) from e
1✔
1516

1517
    def _get_price_side(self, side: str, is_short: bool, conf_strategy: Dict) -> BidAsk:
1✔
1518
        price_side = conf_strategy['price_side']
1✔
1519

1520
        if price_side in ('same', 'other'):
1✔
1521
            price_map = {
1✔
1522
                ('entry', 'long', 'same'): 'bid',
1523
                ('entry', 'long', 'other'): 'ask',
1524
                ('entry', 'short', 'same'): 'ask',
1525
                ('entry', 'short', 'other'): 'bid',
1526
                ('exit', 'long', 'same'): 'ask',
1527
                ('exit', 'long', 'other'): 'bid',
1528
                ('exit', 'short', 'same'): 'bid',
1529
                ('exit', 'short', 'other'): 'ask',
1530
            }
1531
            price_side = price_map[(side, 'short' if is_short else 'long', price_side)]
1✔
1532
        return price_side
1✔
1533

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

1551
        cache_rate: TTLCache = self._entry_rate_cache if side == "entry" else self._exit_rate_cache
1✔
1552
        if not refresh:
1✔
1553
            with self._cache_lock:
1✔
1554
                rate = cache_rate.get(pair)
1✔
1555
            # Check if cache has been invalidated
1556
            if rate:
1✔
1557
                logger.debug(f"Using cached {side} rate for {pair}.")
1✔
1558
                return rate
1✔
1559

1560
        conf_strategy = self._config.get(strat_name, {})
1✔
1561

1562
        price_side = self._get_price_side(side, is_short, conf_strategy)
1✔
1563

1564
        price_side_word = price_side.capitalize()
1✔
1565

1566
        if conf_strategy.get('use_order_book', False):
1✔
1567

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

1597
        if rate is None:
1✔
1598
            raise PricingError(f"{name}-Rate for {pair} was empty.")
1✔
1599
        with self._cache_lock:
1✔
1600
            cache_rate[pair] = rate
1✔
1601

1602
        return rate
1✔
1603

1604
    def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]:
1✔
1605
        entry_rate = None
1✔
1606
        exit_rate = None
1✔
1607
        if not refresh:
1✔
1608
            with self._cache_lock:
1✔
1609
                entry_rate = self._entry_rate_cache.get(pair)
1✔
1610
                exit_rate = self._exit_rate_cache.get(pair)
1✔
1611
            if entry_rate:
1✔
1612
                logger.debug(f"Using cached buy rate for {pair}.")
1✔
1613
            if exit_rate:
1✔
1614
                logger.debug(f"Using cached sell rate for {pair}.")
1✔
1615

1616
        entry_pricing = self._config.get('entry_pricing', {})
1✔
1617
        exit_pricing = self._config.get('exit_pricing', {})
1✔
1618
        order_book = ticker = None
1✔
1619
        if not entry_rate and entry_pricing.get('use_order_book', False):
1✔
1620
            order_book_top = max(entry_pricing.get('order_book_top', 1),
1✔
1621
                                 exit_pricing.get('order_book_top', 1))
1622
            order_book = self.fetch_l2_order_book(pair, order_book_top)
1✔
1623
            entry_rate = self.get_rate(pair, refresh, 'entry', is_short, order_book=order_book)
1✔
1624
        elif not entry_rate:
1✔
1625
            ticker = self.fetch_ticker(pair)
1✔
1626
            entry_rate = self.get_rate(pair, refresh, 'entry', is_short, ticker=ticker)
1✔
1627
        if not exit_rate:
1✔
1628
            exit_rate = self.get_rate(pair, refresh, 'exit',
1✔
1629
                                      is_short, order_book=order_book, ticker=ticker)
1630
        return entry_rate, exit_rate
1✔
1631

1632
    # Fee handling
1633

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

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

1666
            self._log_exchange_response('get_trades_for_order', matched_trades)
1✔
1667

1668
            matched_trades = self._trades_contracts_to_amount(matched_trades)
1✔
1669

1670
            return matched_trades
1✔
1671
        except ccxt.DDoSProtection as e:
1✔
1672
            raise DDosProtection(e) from e
1✔
1673
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1674
            raise TemporaryError(
1✔
1675
                f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e
1676
        except ccxt.BaseError as e:
1✔
1677
            raise OperationalException(e) from e
1✔
1678

1679
    def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
1✔
1680
        return order['id']
1✔
1681

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

1703
            return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
1✔
1704
                                           price=price, takerOrMaker=taker_or_maker)['rate']
1705
        except ccxt.DDoSProtection as e:
1✔
1706
            raise DDosProtection(e) from e
1✔
1707
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
1708
            raise TemporaryError(
1✔
1709
                f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e
1710
        except ccxt.BaseError as e:
1✔
1711
            raise OperationalException(e) from e
1✔
1712

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

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

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

1764
                fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask')
1✔
1765
            except ExchangeError:
1✔
1766
                fee_to_quote_rate = self._config['exchange'].get('unknown_fee_rate', None)
1✔
1767
                if not fee_to_quote_rate:
1✔
1768
                    return None
1✔
1769
            return round((fee_cost * fee_to_quote_rate) / cost, 8)
1✔
1770

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

1792
    # Historic data
1793

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

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

1827
        one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
1✔
1828
            timeframe, candle_type, since_ms)
1829
        logger.debug(
1✔
1830
            "one_call: %s msecs (%s)",
1831
            one_call,
1832
            arrow.utcnow().shift(seconds=one_call // 1000).humanize(only_distance=True)
1833
        )
1834
        input_coroutines = [self._async_get_candle_history(
1✔
1835
            pair, timeframe, candle_type, since) for since in
1836
            range(since_ms, until_ms or (arrow.utcnow().int_timestamp * 1000), one_call)]
1837

1838
        data: List = []
1✔
1839
        # Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
1840
        for input_coro in chunks(input_coroutines, 100):
1✔
1841

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

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

1875
        if (not since_ms and (self._ft_has["ohlcv_require_since"] or not_all_data)):
1✔
1876
            # Multiple calls for one pair - to get more history
1877
            one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
1✔
1878
                timeframe, candle_type, since_ms)
1879
            move_to = one_call * self.required_candle_call_count
1✔
1880
            now = timeframe_to_next_date(timeframe)
1✔
1881
            since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
1✔
1882

1883
        if since_ms:
1✔
1884
            return self._async_get_historic_ohlcv(
1✔
1885
                pair, timeframe, since_ms=since_ms, raise_=True, candle_type=candle_type)
1886
        else:
1887
            # One call ... "regular" refresh
1888
            return self._async_get_candle_history(
1✔
1889
                pair, timeframe, since_ms=since_ms, candle_type=candle_type)
1890

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

1908
            if ((pair, timeframe, candle_type) not in self._klines or not cache
1✔
1909
                    or self._now_is_time_to_refresh(pair, timeframe, candle_type)):
1910

1911
                input_coroutines.append(
1✔
1912
                    self._build_coroutine(pair, timeframe, candle_type, since_ms, cache))
1913

1914
            else:
1915
                logger.debug(
1✔
1916
                    f"Using cached candle (OHLCV) data for {pair}, {timeframe}, {candle_type} ..."
1917
                )
1918
                cached_pairs.append((pair, timeframe, candle_type))
1✔
1919

1920
        return input_coroutines, cached_pairs
1✔
1921

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

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

1962
        # Gather coroutines to run
1963
        input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache)
1✔
1964

1965
        results_df = {}
1✔
1966
        # Chunk requests into batches of 100 to avoid overwelming ccxt Throttling
1967
        for input_coro in chunks(input_coroutines, 100):
1✔
1968
            async def gather_stuff():
1✔
1969
                return await asyncio.gather(*input_coro, return_exceptions=True)
1✔
1970

1971
            with self._loop_lock:
1✔
1972
                results = self.loop.run_until_complete(gather_stuff())
1✔
1973

1974
            for res in results:
1✔
1975
                if isinstance(res, Exception):
1✔
1976
                    logger.warning(f"Async code raised an exception: {repr(res)}")
1✔
1977
                    continue
1✔
1978
                # Deconstruct tuple (has 5 elements)
1979
                pair, timeframe, c_type, ticks, drop_hint = res
1✔
1980
                drop_incomplete = drop_hint if drop_incomplete is None else drop_incomplete
1✔
1981
                ohlcv_df = self._process_ohlcv_df(
1✔
1982
                    pair, timeframe, c_type, ticks, cache, drop_incomplete)
1983

1984
                results_df[(pair, timeframe, c_type)] = ohlcv_df
1✔
1985

1986
        # Return cached klines
1987
        for pair, timeframe, c_type in cached_pairs:
1✔
1988
            results_df[(pair, timeframe, c_type)] = self.klines(
1✔
1989
                (pair, timeframe, c_type),
1990
                copy=False
1991
            )
1992

1993
        return results_df
1✔
1994

1995
    def _now_is_time_to_refresh(self, pair: str, timeframe: str, candle_type: CandleType) -> bool:
1✔
1996
        # Timeframe in seconds
1997
        interval_in_sec = timeframe_to_seconds(timeframe)
1✔
1998
        plr = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + interval_in_sec
1✔
1999
        return plr < arrow.utcnow().int_timestamp
1✔
2000

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

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

2052
        except ccxt.NotSupported as e:
1✔
2053
            raise OperationalException(
1✔
2054
                f'Exchange {self._api.name} does not support fetching historical '
2055
                f'candle (OHLCV) data. Message: {e}') from e
2056
        except ccxt.DDoSProtection as e:
1✔
2057
            raise DDosProtection(e) from e
1✔
2058
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2059
            raise TemporaryError(f'Could not fetch historical candle (OHLCV) data '
1✔
2060
                                 f'for pair {pair} due to {e.__class__.__name__}. '
2061
                                 f'Message: {e}') from e
2062
        except ccxt.BaseError as e:
1✔
2063
            raise OperationalException(f'Could not fetch historical candle (OHLCV) data '
1✔
2064
                                       f'for pair {pair}. Message: {e}') from e
2065

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

2084
    # Fetch historic trades
2085

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

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

2137
        trades: List[List] = []
1✔
2138

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

2163
                from_id = t[-1][1]
1✔
2164
            else:
2165
                break
×
2166

2167
        return (pair, trades)
1✔
2168

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

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

2196
        return (pair, trades)
1✔
2197

2198
    async def _async_get_trade_history(self, pair: str,
1✔
2199
                                       since: Optional[int] = None,
2200
                                       until: Optional[int] = None,
2201
                                       from_id: Optional[str] = None) -> Tuple[str, List[List]]:
2202
        """
2203
        Async wrapper handling downloading trades using either time or id based methods.
2204
        """
2205

2206
        logger.debug(f"_async_get_trade_history(), pair: {pair}, "
1✔
2207
                     f"since: {since}, until: {until}, from_id: {from_id}")
2208

2209
        if until is None:
1✔
2210
            until = ccxt.Exchange.milliseconds()
×
2211
            logger.debug(f"Exchange milliseconds: {until}")
×
2212

2213
        if self._trades_pagination == 'time':
1✔
2214
            return await self._async_get_trade_history_time(
1✔
2215
                pair=pair, since=since, until=until)
2216
        elif self._trades_pagination == 'id':
1✔
2217
            return await self._async_get_trade_history_id(
1✔
2218
                pair=pair, since=since, until=until, from_id=from_id
2219
            )
2220
        else:
2221
            raise OperationalException(f"Exchange {self.name} does use neither time, "
×
2222
                                       f"nor id based pagination")
2223

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

2241
        with self._loop_lock:
1✔
2242
            return self.loop.run_until_complete(
1✔
2243
                self._async_get_trade_history(pair=pair, since=since,
2244
                                              until=until, from_id=from_id))
2245

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

2260
        if type(since) is datetime:
1✔
2261
            since = int(since.timestamp()) * 1000   # * 1000 for ms
1✔
2262

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

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

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

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

2317
                for symbol, market in markets.items():
1✔
2318
                    if (self.market_is_future(market)
1✔
2319
                            and market['quote'] == self._config['stake_currency']):
2320
                        symbols.append(symbol)
1✔
2321

2322
                tiers: Dict[str, List[Dict]] = {}
1✔
2323

2324
                tiers_cached = self.load_cached_leverage_tiers(self._config['stake_currency'])
1✔
2325
                if tiers_cached:
1✔
2326
                    tiers = tiers_cached
1✔
2327

2328
                coros = [
1✔
2329
                    self.get_market_leverage_tiers(symbol)
2330
                    for symbol in sorted(symbols) if symbol not in tiers]
2331

2332
                # Be verbose here, as this delays startup by ~1 minute.
2333
                if coros:
1✔
2334
                    logger.info(
1✔
2335
                        f"Initializing leverage_tiers for {len(symbols)} markets. "
2336
                        "This will take about a minute.")
2337
                else:
2338
                    logger.info("Using cached leverage_tiers.")
1✔
2339

2340
                async def gather_results():
1✔
2341
                    return await asyncio.gather(*input_coro, return_exceptions=True)
1✔
2342

2343
                for input_coro in chunks(coros, 100):
1✔
2344

2345
                    with self._loop_lock:
1✔
2346
                        results = self.loop.run_until_complete(gather_results())
1✔
2347

2348
                    for symbol, res in results:
1✔
2349
                        tiers[symbol] = res
1✔
2350
                if len(coros) > 0:
1✔
2351
                    self.cache_leverage_tiers(tiers, self._config['stake_currency'])
1✔
2352
                logger.info(f"Done initializing {len(symbols)} markets.")
1✔
2353

2354
                return tiers
1✔
2355
            else:
2356
                return {}
1✔
2357
        else:
2358
            return {}
1✔
2359

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

2362
        filename = self._config['datadir'] / "futures" / f"leverage_tiers_{stake_currency}.json"
1✔
2363
        if not filename.parent.is_dir():
1✔
2364
            filename.parent.mkdir(parents=True)
1✔
2365
        data = {
1✔
2366
            "updated": datetime.now(timezone.utc),
2367
            "data": tiers,
2368
        }
2369
        file_dump_json(filename, data)
1✔
2370

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

2384
    def fill_leverage_tiers(self) -> None:
1✔
2385
        """
2386
        Assigns property _leverage_tiers to a dictionary of information about the leverage
2387
        allowed on each pair
2388
        """
2389
        leverage_tiers = self.load_leverage_tiers()
1✔
2390
        for pair, tiers in leverage_tiers.items():
1✔
2391
            pair_tiers = []
1✔
2392
            for tier in tiers:
1✔
2393
                pair_tiers.append(self.parse_leverage_tier(tier))
1✔
2394
            self._leverage_tiers[pair] = pair_tiers
1✔
2395

2396
    def parse_leverage_tier(self, tier) -> Dict:
1✔
2397
        info = tier.get('info', {})
1✔
2398
        return {
1✔
2399
            'minNotional': tier['minNotional'],
2400
            'maxNotional': tier['maxNotional'],
2401
            'maintenanceMarginRate': tier['maintenanceMarginRate'],
2402
            'maxLeverage': tier['maxLeverage'],
2403
            'maintAmt': float(info['cum']) if 'cum' in info else None,
2404
        }
2405

2406
    def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float:
1✔
2407
        """
2408
        Returns the maximum leverage that a pair can be traded at
2409
        :param pair: The base/quote currency pair being traded
2410
        :stake_amount: The total value of the traders margin_mode in quote currency
2411
        """
2412

2413
        if self.trading_mode == TradingMode.SPOT:
1✔
2414
            return 1.0
1✔
2415

2416
        if self.trading_mode == TradingMode.FUTURES:
1✔
2417

2418
            # Checks and edge cases
2419
            if stake_amount is None:
1✔
2420
                raise OperationalException(
×
2421
                    f'{self.name}.get_max_leverage requires argument stake_amount'
2422
                )
2423

2424
            if pair not in self._leverage_tiers:
1✔
2425
                # Maybe raise exception because it can't be traded on futures?
2426
                return 1.0
1✔
2427

2428
            pair_tiers = self._leverage_tiers[pair]
1✔
2429

2430
            if stake_amount == 0:
1✔
2431
                return self._leverage_tiers[pair][0]['maxLeverage']  # Max lev for lowest amount
1✔
2432

2433
            for tier_index in range(len(pair_tiers)):
1✔
2434

2435
                tier = pair_tiers[tier_index]
1✔
2436
                lev = tier['maxLeverage']
1✔
2437

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

2462
                else:  # if on the last tier
2463
                    if stake_amount > tier['maxNotional']:
1✔
2464
                        # If stake is > than max tradeable amount
2465
                        raise InvalidOrderException(f'Amount {stake_amount} too high for {pair}')
1✔
2466
                    else:
2467
                        return tier['maxLeverage']
1✔
2468

2469
            raise OperationalException(
×
2470
                'Looped through all tiers without finding a max leverage. Should never be reached'
2471
            )
2472

2473
        elif self.trading_mode == TradingMode.MARGIN:  # Search markets.limits for max lev
1✔
2474
            market = self.markets[pair]
1✔
2475
            if market['limits']['leverage']['max'] is not None:
1✔
2476
                return market['limits']['leverage']['max']
1✔
2477
            else:
2478
                return 1.0  # Default if max leverage cannot be found
1✔
2479
        else:
2480
            return 1.0
×
2481

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

2497
        try:
×
2498
            res = self._api.set_leverage(symbol=pair, leverage=leverage)
×
2499
            self._log_exchange_response('set_leverage', res)
×
2500
        except ccxt.DDoSProtection as e:
×
2501
            raise DDosProtection(e) from e
×
2502
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
×
2503
            raise TemporaryError(
×
2504
                f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
2505
        except ccxt.BaseError as e:
×
2506
            raise OperationalException(e) from e
×
2507

2508
    def get_interest_rate(self) -> float:
1✔
2509
        """
2510
        Retrieve interest rate - necessary for Margin trading.
2511
        Should not call the exchange directly when used from backtesting.
2512
        """
2513
        return 0.0
×
2514

2515
    def funding_fee_cutoff(self, open_date: datetime):
1✔
2516
        """
2517
        :param open_date: The open date for a trade
2518
        :return: The cutoff open time for when a funding fee is charged
2519
        """
2520
        return open_date.minute > 0 or open_date.second > 0
1✔
2521

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

2532
        try:
1✔
2533
            res = self._api.set_margin_mode(margin_mode.value, pair, params)
1✔
2534
            self._log_exchange_response('set_margin_mode', res)
×
2535
        except ccxt.DDoSProtection as e:
1✔
2536
            raise DDosProtection(e) from e
1✔
2537
        except (ccxt.NetworkError, ccxt.ExchangeError) as e:
1✔
2538
            raise TemporaryError(
1✔
2539
                f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
2540
        except ccxt.BaseError as e:
1✔
2541
            raise OperationalException(e) from e
1✔
2542

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

2562
        if self.funding_fee_cutoff(open_date):
1✔
2563
            open_date += timedelta(hours=1)
1✔
2564
        timeframe = self._ft_has['mark_ohlcv_timeframe']
1✔
2565
        timeframe_ff = self._ft_has.get('funding_fee_timeframe',
1✔
2566
                                        self._ft_has['mark_ohlcv_timeframe'])
2567

2568
        if not close_date:
1✔
2569
            close_date = datetime.now(timezone.utc)
1✔
2570
        open_timestamp = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000
1✔
2571
        # close_timestamp = int(close_date.timestamp()) * 1000
2572

2573
        mark_comb: PairWithTimeframe = (
1✔
2574
            pair, timeframe, CandleType.from_string(self._ft_has["mark_ohlcv_price"]))
2575

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

2590
        funding_mark_rates = self.combine_funding_and_mark(
1✔
2591
            funding_rates=funding_rates, mark_rates=mark_rates)
2592

2593
        return self.calculate_funding_fees(
1✔
2594
            funding_mark_rates,
2595
            amount=amount,
2596
            is_short=is_short,
2597
            open_date=open_date,
2598
            close_date=close_date
2599
        )
2600

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

2624
            else:
2625
                # Fill up missing funding_rate candles with fallback value
2626
                combined = mark_rates.merge(
1✔
2627
                    funding_rates, on='date', how="outer", suffixes=["_mark", "_fund"]
2628
                    )
2629
                combined['open_fund'] = combined['open_fund'].fillna(futures_funding_rate)
1✔
2630
                return combined
1✔
2631

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

2653
        if not df.empty:
1✔
2654
            df = df[(df['date'] >= open_date) & (df['date'] <= close_date)]
1✔
2655
            fees = sum(df['open_fund'] * df['open_mark'] * amount)
1✔
2656

2657
        # Negate fees for longs as funding_fees expects it this way based on live endpoints.
2658
        return fees if is_short else -fees
1✔
2659

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

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

2703
        isolated_liq = None
1✔
2704
        if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
1✔
2705

2706
            isolated_liq = self.dry_run_liquidation_price(
1✔
2707
                pair=pair,
2708
                open_rate=open_rate,
2709
                is_short=is_short,
2710
                amount=amount,
2711
                stake_amount=stake_amount,
2712
                wallet_balance=wallet_balance,
2713
                mm_ex_1=mm_ex_1,
2714
                upnl_ex_1=upnl_ex_1
2715
            )
2716
        else:
2717
            positions = self.fetch_positions(pair)
1✔
2718
            if len(positions) > 0:
1✔
2719
                pos = positions[0]
1✔
2720
                isolated_liq = pos['liquidationPrice']
1✔
2721

2722
        if isolated_liq:
1✔
2723
            buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
1✔
2724
            isolated_liq = (
1✔
2725
                isolated_liq - buffer_amount
2726
                if is_short else
2727
                isolated_liq + buffer_amount
2728
            )
2729
            return isolated_liq
1✔
2730
        else:
2731
            return None
1✔
2732

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

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

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

2767
        # * Not required by Gateio or OKX
2768
        :param mm_ex_1:
2769
        :param upnl_ex_1:
2770
        """
2771

2772
        market = self.markets[pair]
1✔
2773
        taker_fee_rate = market['taker']
1✔
2774
        mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount)
1✔
2775

2776
        if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
1✔
2777

2778
            if market['inverse']:
1✔
2779
                raise OperationalException(
×
2780
                    "Freqtrade does not yet support inverse contracts")
2781

2782
            value = wallet_balance / amount
1✔
2783

2784
            mm_ratio_taker = (mm_ratio + taker_fee_rate)
1✔
2785
            if is_short:
1✔
2786
                return (open_rate + value) / (1 + mm_ratio_taker)
1✔
2787
            else:
2788
                return (open_rate - value) / (1 - mm_ratio_taker)
1✔
2789
        else:
2790
            raise OperationalException(
×
2791
                "Freqtrade only supports isolated futures for leverage trading")
2792

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

2806
        if (self._config.get('runmode') in OPTIMIZE_MODES
1✔
2807
                or self.exchange_has('fetchLeverageTiers')
2808
                or self.exchange_has('fetchMarketLeverageTiers')):
2809

2810
            if pair not in self._leverage_tiers:
1✔
2811
                raise InvalidOrderException(
1✔
2812
                    f"Maintenance margin rate for {pair} is unavailable for {self.name}"
2813
                )
2814

2815
            pair_tiers = self._leverage_tiers[pair]
1✔
2816

2817
            for tier in reversed(pair_tiers):
1✔
2818
                if nominal_value >= tier['minNotional']:
1✔
2819
                    return (tier['maintenanceMarginRate'], tier['maintAmt'])
1✔
2820

2821
            raise OperationalException("nominal value can not be lower than 0")
1✔
2822
            # The lowest notional_floor for any pair in fetch_leverage_tiers is always 0 because it
2823
            # describes the min amt for a tier, and the lowest tier will always go down to 0
2824
        else:
2825
            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