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

freqtrade / freqtrade / 9116662491

16 May 2024 05:25PM UTC coverage: 94.683% (+0.005%) from 94.678%
9116662491

push

github

xmatthias
Bump ccxt min-version

20336 of 21478 relevant lines covered (94.68%)

0.95 hits per line

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

97.08
/freqtrade/exchange/exchange.py
1
# pragma pylint: disable=W0603
2
"""
1✔
3
Cryptocurrency Exchanges support
4
"""
5

6
import asyncio
1✔
7
import inspect
1✔
8
import logging
1✔
9
import signal
1✔
10
from copy import deepcopy
1✔
11
from datetime import datetime, timedelta, timezone
1✔
12
from math import floor, isnan
1✔
13
from threading import Lock
1✔
14
from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union
1✔
15

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

23
from freqtrade.constants import (
1✔
24
    DEFAULT_AMOUNT_RESERVE_PERCENT,
25
    NON_OPEN_EXCHANGE_STATES,
26
    BidAsk,
27
    BuySell,
28
    Config,
29
    EntryExit,
30
    ExchangeConfig,
31
    ListPairsWithTimeframes,
32
    MakerTaker,
33
    OBLiteral,
34
    PairWithTimeframe,
35
)
36
from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list
1✔
37
from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, RunMode, TradingMode
1✔
38
from freqtrade.exceptions import (
1✔
39
    ConfigurationError,
40
    DDosProtection,
41
    ExchangeError,
42
    InsufficientFundsError,
43
    InvalidOrderException,
44
    OperationalException,
45
    PricingError,
46
    RetryableOrderError,
47
    TemporaryError,
48
)
49
from freqtrade.exchange.common import (
1✔
50
    API_FETCH_ORDER_RETRY_COUNT,
51
    remove_exchange_credentials,
52
    retrier,
53
    retrier_async,
54
)
55
from freqtrade.exchange.exchange_utils import (
1✔
56
    ROUND,
57
    ROUND_DOWN,
58
    ROUND_UP,
59
    CcxtModuleType,
60
    amount_to_contract_precision,
61
    amount_to_contracts,
62
    amount_to_precision,
63
    contracts_to_amount,
64
    date_minus_candles,
65
    is_exchange_known_ccxt,
66
    market_is_active,
67
    price_to_precision,
68
)
69
from freqtrade.exchange.exchange_utils_timeframe import (
1✔
70
    timeframe_to_minutes,
71
    timeframe_to_msecs,
72
    timeframe_to_next_date,
73
    timeframe_to_prev_date,
74
    timeframe_to_seconds,
75
)
76
from freqtrade.exchange.types import OHLCVResponse, OrderBook, Ticker, Tickers
1✔
77
from freqtrade.misc import (
1✔
78
    chunks,
79
    deep_merge_dicts,
80
    file_dump_json,
81
    file_load_json,
82
    safe_value_fallback2,
83
)
84
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
1✔
85
from freqtrade.util import dt_from_ts, dt_now
1✔
86
from freqtrade.util.datetime_helpers import dt_humanize_delta, dt_ts
1✔
87
from freqtrade.util.periodic_cache import PeriodicCache
1✔
88

89

90
logger = logging.getLogger(__name__)
1✔
91

92

93
class Exchange:
1✔
94
    # Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
95
    _params: Dict = {}
1✔
96

97
    # Additional parameters - added to the ccxt object
98
    _ccxt_params: Dict = {}
1✔
99

100
    # Dict to specify which options each exchange implements
101
    # This defines defaults, which can be selectively overridden by subclasses using _ft_has
102
    # or by specifying them in the configuration.
103
    _ft_has_default: Dict = {
1✔
104
        "stoploss_on_exchange": False,
105
        "stop_price_param": "stopLossPrice",  # Used for stoploss_on_exchange request
106
        "stop_price_prop": "stopLossPrice",  # Used for stoploss_on_exchange response parsing
107
        "order_time_in_force": ["GTC"],
108
        "ohlcv_params": {},
109
        "ohlcv_candle_limit": 500,
110
        "ohlcv_has_history": True,  # Some exchanges (Kraken) don't provide history via ohlcv
111
        "ohlcv_partial_candle": True,
112
        "ohlcv_require_since": False,
113
        # Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
114
        "ohlcv_volume_currency": "base",  # "base" or "quote"
115
        "tickers_have_quoteVolume": True,
116
        "tickers_have_bid_ask": True,  # bid / ask empty for fetch_tickers
117
        "tickers_have_price": True,
118
        "trades_pagination": "time",  # Possible are "time" or "id"
119
        "trades_pagination_arg": "since",
120
        "l2_limit_range": None,
121
        "l2_limit_range_required": True,  # Allow Empty L2 limit (kucoin)
122
        "mark_ohlcv_price": "mark",
123
        "mark_ohlcv_timeframe": "8h",
124
        "funding_fee_timeframe": "8h",
125
        "ccxt_futures_name": "swap",
126
        "needs_trading_fees": False,  # use fetch_trading_fees to cache fees
127
        "order_props_in_contracts": ["amount", "filled", "remaining"],
128
        # Override createMarketBuyOrderRequiresPrice where ccxt has it wrong
129
        "marketOrderRequiresPrice": False,
130
        "exchange_has_overrides": {},  # Dictionary overriding ccxt's "has".
131
        # Expected to be in the format {"fetchOHLCV": True} or {"fetchOHLCV": False}
132
    }
133
    _ft_has: Dict = {}
1✔
134
    _ft_has_futures: Dict = {}
1✔
135

136
    _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
1✔
137
        # TradingMode.SPOT always supported and not required in this list
138
    ]
139

140
    def __init__(
1✔
141
        self,
142
        config: Config,
143
        *,
144
        exchange_config: Optional[ExchangeConfig] = None,
145
        validate: bool = True,
146
        load_leverage_tiers: bool = False,
147
    ) -> None:
148
        """
149
        Initializes this module with the given config,
150
        it does basic validation whether the specified exchange and pairs are valid.
151
        :return: None
152
        """
153
        self._api: ccxt.Exchange
1✔
154
        self._api_async: ccxt_async.Exchange = None
1✔
155
        self._markets: Dict = {}
1✔
156
        self._trading_fees: Dict[str, Any] = {}
1✔
157
        self._leverage_tiers: Dict[str, List[Dict]] = {}
1✔
158
        # Lock event loop. This is necessary to avoid race-conditions when using force* commands
159
        # Due to funding fee fetching.
160
        self._loop_lock = Lock()
1✔
161
        self.loop = self._init_async_loop()
1✔
162
        self._config: Config = {}
1✔
163

164
        self._config.update(config)
1✔
165

166
        # Holds last candle refreshed time of each pair
167
        self._pairs_last_refresh_time: Dict[PairWithTimeframe, int] = {}
1✔
168
        # Timestamp of last markets refresh
169
        self._last_markets_refresh: int = 0
1✔
170

171
        # Cache for 10 minutes ...
172
        self._cache_lock = Lock()
1✔
173
        self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10)
1✔
174
        # Cache values for 300 to avoid frequent polling of the exchange for prices
175
        # Caching only applies to RPC methods, so prices for open trades are still
176
        # refreshed once every iteration.
177
        # Shouldn't be too high either, as it'll freeze UI updates in case of open orders.
178
        self._exit_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=300)
1✔
179
        self._entry_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=300)
1✔
180

181
        # Holds candles
182
        self._klines: Dict[PairWithTimeframe, DataFrame] = {}
1✔
183
        self._expiring_candle_cache: Dict[Tuple[str, int], PeriodicCache] = {}
1✔
184

185
        # Holds all open sell orders for dry_run
186
        self._dry_run_open_orders: Dict[str, Any] = {}
1✔
187

188
        if config["dry_run"]:
1✔
189
            logger.info("Instance is running with dry_run enabled")
1✔
190
        logger.info(f"Using CCXT {ccxt.__version__}")
1✔
191
        exchange_conf: Dict[str, Any] = exchange_config if exchange_config else config["exchange"]
1✔
192
        remove_exchange_credentials(exchange_conf, config.get("dry_run", False))
1✔
193
        self.log_responses = exchange_conf.get("log_responses", False)
1✔
194

195
        # Leverage properties
196
        self.trading_mode: TradingMode = config.get("trading_mode", TradingMode.SPOT)
1✔
197
        self.margin_mode: MarginMode = (
1✔
198
            MarginMode(config.get("margin_mode")) if config.get("margin_mode") else MarginMode.NONE
199
        )
200
        self.liquidation_buffer = config.get("liquidation_buffer", 0.05)
1✔
201

202
        # Deep merge ft_has with default ft_has options
203
        self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
1✔
204
        if self.trading_mode == TradingMode.FUTURES:
1✔
205
            self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
1✔
206
        if exchange_conf.get("_ft_has_params"):
1✔
207
            self._ft_has = deep_merge_dicts(exchange_conf.get("_ft_has_params"), self._ft_has)
1✔
208
            logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
1✔
209

210
        # Assign this directly for easy access
211
        self._ohlcv_partial_candle = self._ft_has["ohlcv_partial_candle"]
1✔
212

213
        self._trades_pagination = self._ft_has["trades_pagination"]
1✔
214
        self._trades_pagination_arg = self._ft_has["trades_pagination_arg"]
1✔
215

216
        # Initialize ccxt objects
217
        ccxt_config = self._ccxt_config
1✔
218
        ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_config", {}), ccxt_config)
1✔
219
        ccxt_config = deep_merge_dicts(exchange_conf.get("ccxt_sync_config", {}), ccxt_config)
1✔
220

221
        self._api = self._init_ccxt(exchange_conf, ccxt_kwargs=ccxt_config)
1✔
222

223
        ccxt_async_config = self._ccxt_config
1✔
224
        ccxt_async_config = deep_merge_dicts(
1✔
225
            exchange_conf.get("ccxt_config", {}), ccxt_async_config
226
        )
227
        ccxt_async_config = deep_merge_dicts(
1✔
228
            exchange_conf.get("ccxt_async_config", {}), ccxt_async_config
229
        )
230
        self._api_async = self._init_ccxt(exchange_conf, ccxt_async, ccxt_kwargs=ccxt_async_config)
1✔
231

232
        logger.info(f'Using Exchange "{self.name}"')
1✔
233
        self.required_candle_call_count = 1
1✔
234
        if validate:
1✔
235
            # Initial markets load
236
            self._load_markets()
1✔
237
            self.validate_config(config)
1✔
238
            self._startup_candle_count: int = config.get("startup_candle_count", 0)
1✔
239
            self.required_candle_call_count = self.validate_required_startup_candles(
1✔
240
                self._startup_candle_count, config.get("timeframe", "")
241
            )
242

243
        # Converts the interval provided in minutes in config to seconds
244
        self.markets_refresh_interval: int = (
1✔
245
            exchange_conf.get("markets_refresh_interval", 60) * 60 * 1000
246
        )
247

248
        if self.trading_mode != TradingMode.SPOT and load_leverage_tiers:
1✔
249
            self.fill_leverage_tiers()
1✔
250
        self.additional_exchange_init()
1✔
251

252
    def __del__(self):
1✔
253
        """
254
        Destructor - clean up async stuff
255
        """
256
        self.close()
1✔
257

258
    def close(self):
1✔
259
        logger.debug("Exchange object destroyed, closing async loop")
1✔
260
        if (
1✔
261
            self._api_async
262
            and inspect.iscoroutinefunction(self._api_async.close)
263
            and self._api_async.session
264
        ):
265
            logger.debug("Closing async ccxt session.")
×
266
            self.loop.run_until_complete(self._api_async.close())
×
267
        if self.loop and not self.loop.is_closed():
1✔
268
            self.loop.close()
1✔
269

270
    def _init_async_loop(self) -> asyncio.AbstractEventLoop:
1✔
271
        loop = asyncio.new_event_loop()
1✔
272
        asyncio.set_event_loop(loop)
1✔
273
        return loop
1✔
274

275
    def validate_config(self, config):
1✔
276
        # Check if timeframe is available
277
        self.validate_timeframes(config.get("timeframe"))
1✔
278

279
        # Check if all pairs are available
280
        self.validate_stakecurrency(config["stake_currency"])
1✔
281
        if not config["exchange"].get("skip_pair_validation"):
1✔
282
            self.validate_pairs(config["exchange"]["pair_whitelist"])
1✔
283
        self.validate_ordertypes(config.get("order_types", {}))
1✔
284
        self.validate_order_time_in_force(config.get("order_time_in_force", {}))
1✔
285
        self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
1✔
286
        self.validate_pricing(config["exit_pricing"])
1✔
287
        self.validate_pricing(config["entry_pricing"])
1✔
288

289
    def _init_ccxt(
1✔
290
        self,
291
        exchange_config: Dict[str, Any],
292
        ccxt_module: CcxtModuleType = ccxt,
293
        *,
294
        ccxt_kwargs: Dict,
295
    ) -> ccxt.Exchange:
296
        """
297
        Initialize ccxt with given config and return valid
298
        ccxt instance.
299
        """
300
        # Find matching class for the given exchange name
301
        name = exchange_config["name"]
1✔
302

303
        if not is_exchange_known_ccxt(name, ccxt_module):
1✔
304
            raise OperationalException(f"Exchange {name} is not supported by ccxt")
1✔
305

306
        ex_config = {
1✔
307
            "apiKey": exchange_config.get("key"),
308
            "secret": exchange_config.get("secret"),
309
            "password": exchange_config.get("password"),
310
            "uid": exchange_config.get("uid", ""),
311
        }
312
        if ccxt_kwargs:
1✔
313
            logger.info("Applying additional ccxt config: %s", ccxt_kwargs)
1✔
314
        if self._ccxt_params:
1✔
315
            # Inject static options after the above output to not confuse users.
316
            ccxt_kwargs = deep_merge_dicts(self._ccxt_params, ccxt_kwargs)
1✔
317
        if ccxt_kwargs:
1✔
318
            ex_config.update(ccxt_kwargs)
1✔
319
        try:
1✔
320
            api = getattr(ccxt_module, name.lower())(ex_config)
1✔
321
        except (KeyError, AttributeError) as e:
1✔
322
            raise OperationalException(f"Exchange {name} is not supported") from e
1✔
323
        except ccxt.BaseError as e:
1✔
324
            raise OperationalException(f"Initialization of ccxt failed. Reason: {e}") from e
1✔
325

326
        return api
1✔
327

328
    @property
1✔
329
    def _ccxt_config(self) -> Dict:
1✔
330
        # Parameters to add directly to ccxt sync/async initialization.
331
        if self.trading_mode == TradingMode.MARGIN:
1✔
332
            return {"options": {"defaultType": "margin"}}
1✔
333
        elif self.trading_mode == TradingMode.FUTURES:
1✔
334
            return {"options": {"defaultType": self._ft_has["ccxt_futures_name"]}}
1✔
335
        else:
336
            return {}
1✔
337

338
    @property
1✔
339
    def name(self) -> str:
1✔
340
        """exchange Name (from ccxt)"""
341
        return self._api.name
1✔
342

343
    @property
1✔
344
    def id(self) -> str:
1✔
345
        """exchange ccxt id"""
346
        return self._api.id
×
347

348
    @property
1✔
349
    def timeframes(self) -> List[str]:
1✔
350
        return list((self._api.timeframes or {}).keys())
1✔
351

352
    @property
1✔
353
    def markets(self) -> Dict[str, Any]:
1✔
354
        """exchange ccxt markets"""
355
        if not self._markets:
1✔
356
            logger.info("Markets were not loaded. Loading them now..")
1✔
357
            self._load_markets()
1✔
358
        return self._markets
1✔
359

360
    @property
1✔
361
    def precisionMode(self) -> int:
1✔
362
        """exchange ccxt precisionMode"""
363
        return self._api.precisionMode
1✔
364

365
    def additional_exchange_init(self) -> None:
1✔
366
        """
367
        Additional exchange initialization logic.
368
        .api will be available at this point.
369
        Must be overridden in child methods if required.
370
        """
371
        pass
1✔
372

373
    def _log_exchange_response(self, endpoint: str, response, *, add_info=None) -> None:
1✔
374
        """Log exchange responses"""
375
        if self.log_responses:
1✔
376
            add_info_str = "" if add_info is None else f" {add_info}: "
1✔
377
            logger.info(f"API {endpoint}: {add_info_str}{response}")
1✔
378

379
    def ohlcv_candle_limit(
1✔
380
        self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None
381
    ) -> int:
382
        """
383
        Exchange ohlcv candle limit
384
        Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
385
        per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
386
        TODO: this is most likely no longer needed since only bittrex needed this.
387
        :param timeframe: Timeframe to check
388
        :param candle_type: Candle-type
389
        :param since_ms: Starting timestamp
390
        :return: Candle limit as integer
391
        """
392
        return int(
1✔
393
            self._ft_has.get("ohlcv_candle_limit_per_timeframe", {}).get(
394
                timeframe, self._ft_has.get("ohlcv_candle_limit")
395
            )
396
        )
397

398
    def get_markets(
1✔
399
        self,
400
        base_currencies: Optional[List[str]] = None,
401
        quote_currencies: Optional[List[str]] = None,
402
        spot_only: bool = False,
403
        margin_only: bool = False,
404
        futures_only: bool = False,
405
        tradable_only: bool = True,
406
        active_only: bool = False,
407
    ) -> Dict[str, Any]:
408
        """
409
        Return exchange ccxt markets, filtered out by base currency and quote currency
410
        if this was requested in parameters.
411
        """
412
        markets = self.markets
1✔
413
        if not markets:
1✔
414
            raise OperationalException("Markets were not loaded.")
1✔
415

416
        if base_currencies:
1✔
417
            markets = {k: v for k, v in markets.items() if v["base"] in base_currencies}
1✔
418
        if quote_currencies:
1✔
419
            markets = {k: v for k, v in markets.items() if v["quote"] in quote_currencies}
1✔
420
        if tradable_only:
1✔
421
            markets = {k: v for k, v in markets.items() if self.market_is_tradable(v)}
1✔
422
        if spot_only:
1✔
423
            markets = {k: v for k, v in markets.items() if self.market_is_spot(v)}
1✔
424
        if margin_only:
1✔
425
            markets = {k: v for k, v in markets.items() if self.market_is_margin(v)}
×
426
        if futures_only:
1✔
427
            markets = {k: v for k, v in markets.items() if self.market_is_future(v)}
1✔
428
        if active_only:
1✔
429
            markets = {k: v for k, v in markets.items() if market_is_active(v)}
1✔
430
        return markets
1✔
431

432
    def get_quote_currencies(self) -> List[str]:
1✔
433
        """
434
        Return a list of supported quote currencies
435
        """
436
        markets = self.markets
1✔
437
        return sorted(set([x["quote"] for _, x in markets.items()]))
1✔
438

439
    def get_pair_quote_currency(self, pair: str) -> str:
1✔
440
        """Return a pair's quote currency (base/quote:settlement)"""
441
        return self.markets.get(pair, {}).get("quote", "")
1✔
442

443
    def get_pair_base_currency(self, pair: str) -> str:
1✔
444
        """Return a pair's base currency (base/quote:settlement)"""
445
        return self.markets.get(pair, {}).get("base", "")
1✔
446

447
    def market_is_future(self, market: Dict[str, Any]) -> bool:
1✔
448
        return (
1✔
449
            market.get(self._ft_has["ccxt_futures_name"], False) is True
450
            and market.get("linear", False) is True
451
        )
452

453
    def market_is_spot(self, market: Dict[str, Any]) -> bool:
1✔
454
        return market.get("spot", False) is True
1✔
455

456
    def market_is_margin(self, market: Dict[str, Any]) -> bool:
1✔
457
        return market.get("margin", False) is True
1✔
458

459
    def market_is_tradable(self, market: Dict[str, Any]) -> bool:
1✔
460
        """
461
        Check if the market symbol is tradable by Freqtrade.
462
        Ensures that Configured mode aligns to
463
        """
464
        return (
1✔
465
            market.get("quote", None) is not None
466
            and market.get("base", None) is not None
467
            and (
468
                self.precisionMode != TICK_SIZE
469
                # Too low precision will falsify calculations
470
                or market.get("precision", {}).get("price") > 1e-11
471
            )
472
            and (
473
                (self.trading_mode == TradingMode.SPOT and self.market_is_spot(market))
474
                or (self.trading_mode == TradingMode.MARGIN and self.market_is_margin(market))
475
                or (self.trading_mode == TradingMode.FUTURES and self.market_is_future(market))
476
            )
477
        )
478

479
    def klines(self, pair_interval: PairWithTimeframe, copy: bool = True) -> DataFrame:
1✔
480
        if pair_interval in self._klines:
1✔
481
            return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
1✔
482
        else:
483
            return DataFrame()
1✔
484

485
    def get_contract_size(self, pair: str) -> Optional[float]:
1✔
486
        if self.trading_mode == TradingMode.FUTURES:
1✔
487
            market = self.markets.get(pair, {})
1✔
488
            contract_size: float = 1.0
1✔
489
            if not market:
1✔
490
                return None
1✔
491
            if market.get("contractSize") is not None:
1✔
492
                # ccxt has contractSize in markets as string
493
                contract_size = float(market["contractSize"])
1✔
494
            return contract_size
1✔
495
        else:
496
            return 1
1✔
497

498
    def _trades_contracts_to_amount(self, trades: List) -> List:
1✔
499
        if len(trades) > 0 and "symbol" in trades[0]:
1✔
500
            contract_size = self.get_contract_size(trades[0]["symbol"])
1✔
501
            if contract_size != 1:
1✔
502
                for trade in trades:
1✔
503
                    trade["amount"] = trade["amount"] * contract_size
1✔
504
        return trades
1✔
505

506
    def _order_contracts_to_amount(self, order: Dict) -> Dict:
1✔
507
        if "symbol" in order and order["symbol"] is not None:
1✔
508
            contract_size = self.get_contract_size(order["symbol"])
1✔
509
            if contract_size != 1:
1✔
510
                for prop in self._ft_has.get("order_props_in_contracts", []):
1✔
511
                    if prop in order and order[prop] is not None:
1✔
512
                        order[prop] = order[prop] * contract_size
1✔
513
        return order
1✔
514

515
    def _amount_to_contracts(self, pair: str, amount: float) -> float:
1✔
516
        contract_size = self.get_contract_size(pair)
1✔
517
        return amount_to_contracts(amount, contract_size)
1✔
518

519
    def _contracts_to_amount(self, pair: str, num_contracts: float) -> float:
1✔
520
        contract_size = self.get_contract_size(pair)
1✔
521
        return contracts_to_amount(num_contracts, contract_size)
1✔
522

523
    def amount_to_contract_precision(self, pair: str, amount: float) -> float:
1✔
524
        """
525
        Helper wrapper around amount_to_contract_precision
526
        """
527
        contract_size = self.get_contract_size(pair)
1✔
528

529
        return amount_to_contract_precision(
1✔
530
            amount, self.get_precision_amount(pair), self.precisionMode, contract_size
531
        )
532

533
    def _load_async_markets(self, reload: bool = False) -> None:
1✔
534
        try:
1✔
535
            if self._api_async:
1✔
536
                self.loop.run_until_complete(self._api_async.load_markets(reload=reload, params={}))
1✔
537

538
        except (asyncio.TimeoutError, ccxt.BaseError) as e:
1✔
539
            logger.warning("Could not load async markets. Reason: %s", e)
1✔
540
            return
1✔
541

542
    def _load_markets(self) -> None:
1✔
543
        """Initialize markets both sync and async"""
544
        try:
1✔
545
            self._markets = self._api.load_markets(params={})
1✔
546
            self._load_async_markets()
1✔
547
            self._last_markets_refresh = dt_ts()
1✔
548
            if self._ft_has["needs_trading_fees"]:
1✔
549
                self._trading_fees = self.fetch_trading_fees()
1✔
550

551
        except ccxt.BaseError:
1✔
552
            logger.exception("Unable to initialize markets.")
1✔
553

554
    def reload_markets(self, force: bool = False) -> None:
1✔
555
        """Reload markets both sync and async if refresh interval has passed"""
556
        # Check whether markets have to be reloaded
557
        if (
1✔
558
            not force
559
            and self._last_markets_refresh > 0
560
            and (self._last_markets_refresh + self.markets_refresh_interval > dt_ts())
561
        ):
562
            return None
1✔
563
        logger.debug("Performing scheduled market reload..")
1✔
564
        try:
1✔
565
            self._markets = self._api.load_markets(reload=True, params={})
1✔
566
            # Also reload async markets to avoid issues with newly listed pairs
567
            self._load_async_markets(reload=True)
1✔
568
            self._last_markets_refresh = dt_ts()
1✔
569
            self.fill_leverage_tiers()
1✔
570
        except ccxt.BaseError:
1✔
571
            logger.exception("Could not reload markets.")
1✔
572

573
    def validate_stakecurrency(self, stake_currency: str) -> None:
1✔
574
        """
575
        Checks stake-currency against available currencies on the exchange.
576
        Only runs on startup. If markets have not been loaded, there's been a problem with
577
        the connection to the exchange.
578
        :param stake_currency: Stake-currency to validate
579
        :raise: OperationalException if stake-currency is not available.
580
        """
581
        if not self._markets:
1✔
582
            raise OperationalException(
1✔
583
                "Could not load markets, therefore cannot start. "
584
                "Please investigate the above error for more details."
585
            )
586
        quote_currencies = self.get_quote_currencies()
1✔
587
        if stake_currency not in quote_currencies:
1✔
588
            raise ConfigurationError(
1✔
589
                f"{stake_currency} is not available as stake on {self.name}. "
590
                f"Available currencies are: {', '.join(quote_currencies)}"
591
            )
592

593
    def validate_pairs(self, pairs: List[str]) -> None:
1✔
594
        """
595
        Checks if all given pairs are tradable on the current exchange.
596
        :param pairs: list of pairs
597
        :raise: OperationalException if one pair is not available
598
        :return: None
599
        """
600

601
        if not self.markets:
1✔
602
            logger.warning("Unable to validate pairs (assuming they are correct).")
1✔
603
            return
1✔
604
        extended_pairs = expand_pairlist(pairs, list(self.markets), keep_invalid=True)
1✔
605
        invalid_pairs = []
1✔
606
        for pair in extended_pairs:
1✔
607
            # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs
608
            if self.markets and pair not in self.markets:
1✔
609
                raise OperationalException(
1✔
610
                    f"Pair {pair} is not available on {self.name} {self.trading_mode.value}. "
611
                    f"Please remove {pair} from your whitelist."
612
                )
613

614
                # From ccxt Documentation:
615
                # markets.info: An associative array of non-common market properties,
616
                # including fees, rates, limits and other general market information.
617
                # The internal info array is different for each particular market,
618
                # its contents depend on the exchange.
619
                # It can also be a string or similar ... so we need to verify that first.
620
            elif isinstance(self.markets[pair].get("info"), dict) and self.markets[pair].get(
1✔
621
                "info", {}
622
            ).get("prohibitedIn", False):
623
                # Warn users about restricted pairs in whitelist.
624
                # We cannot determine reliably if Users are affected.
625
                logger.warning(
1✔
626
                    f"Pair {pair} is restricted for some users on this exchange."
627
                    f"Please check if you are impacted by this restriction "
628
                    f"on the exchange and eventually remove {pair} from your whitelist."
629
                )
630
            if (
1✔
631
                self._config["stake_currency"]
632
                and self.get_pair_quote_currency(pair) != self._config["stake_currency"]
633
            ):
634
                invalid_pairs.append(pair)
1✔
635
        if invalid_pairs:
1✔
636
            raise OperationalException(
1✔
637
                f"Stake-currency '{self._config['stake_currency']}' not compatible with "
638
                f"pair-whitelist. Please remove the following pairs: {invalid_pairs}"
639
            )
640

641
    def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str:
1✔
642
        """
643
        Get valid pair combination of curr_1 and curr_2 by trying both combinations.
644
        """
645
        for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
1✔
646
            if pair in self.markets and self.markets[pair].get("active"):
1✔
647
                return pair
1✔
648
        raise ValueError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
1✔
649

650
    def validate_timeframes(self, timeframe: Optional[str]) -> None:
1✔
651
        """
652
        Check if timeframe from config is a supported timeframe on the exchange
653
        """
654
        if not hasattr(self._api, "timeframes") or self._api.timeframes is None:
1✔
655
            # If timeframes attribute is missing (or is None), the exchange probably
656
            # has no fetchOHLCV method.
657
            # Therefore we also show that.
658
            raise OperationalException(
1✔
659
                f"The ccxt library does not provide the list of timeframes "
660
                f"for the exchange {self.name} and this exchange "
661
                f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}"
662
            )
663

664
        if timeframe and (timeframe not in self.timeframes):
1✔
665
            raise ConfigurationError(
1✔
666
                f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}"
667
            )
668

669
        if (
1✔
670
            timeframe
671
            and self._config["runmode"] != RunMode.UTIL_EXCHANGE
672
            and timeframe_to_minutes(timeframe) < 1
673
        ):
674
            raise ConfigurationError("Timeframes < 1m are currently not supported by Freqtrade.")
1✔
675

676
    def validate_ordertypes(self, order_types: Dict) -> None:
1✔
677
        """
678
        Checks if order-types configured in strategy/config are supported
679
        """
680
        if any(v == "market" for k, v in order_types.items()):
1✔
681
            if not self.exchange_has("createMarketOrder"):
1✔
682
                raise ConfigurationError(f"Exchange {self.name} does not support market orders.")
1✔
683
        self.validate_stop_ordertypes(order_types)
1✔
684

685
    def validate_stop_ordertypes(self, order_types: Dict) -> None:
1✔
686
        """
687
        Validate stoploss order types
688
        """
689
        if order_types.get("stoploss_on_exchange") and not self._ft_has.get(
1✔
690
            "stoploss_on_exchange", False
691
        ):
692
            raise ConfigurationError(f"On exchange stoploss is not supported for {self.name}.")
1✔
693
        if self.trading_mode == TradingMode.FUTURES:
1✔
694
            price_mapping = self._ft_has.get("stop_price_type_value_mapping", {}).keys()
1✔
695
            if (
1✔
696
                order_types.get("stoploss_on_exchange", False) is True
697
                and "stoploss_price_type" in order_types
698
                and order_types["stoploss_price_type"] not in price_mapping
699
            ):
700
                raise ConfigurationError(
1✔
701
                    f"On exchange stoploss price type is not supported for {self.name}."
702
                )
703

704
    def validate_pricing(self, pricing: Dict) -> None:
1✔
705
        if pricing.get("use_order_book", False) and not self.exchange_has("fetchL2OrderBook"):
1✔
706
            raise ConfigurationError(f"Orderbook not available for {self.name}.")
1✔
707
        if not pricing.get("use_order_book", False) and (
1✔
708
            not self.exchange_has("fetchTicker") or not self._ft_has["tickers_have_price"]
709
        ):
710
            raise ConfigurationError(f"Ticker pricing not available for {self.name}.")
1✔
711

712
    def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
1✔
713
        """
714
        Checks if order time in force configured in strategy/config are supported
715
        """
716
        if any(
1✔
717
            v.upper() not in self._ft_has["order_time_in_force"]
718
            for k, v in order_time_in_force.items()
719
        ):
720
            raise ConfigurationError(
1✔
721
                f"Time in force policies are not supported for {self.name} yet."
722
            )
723

724
    def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
1✔
725
        """
726
        Checks if required startup_candles is more than ohlcv_candle_limit().
727
        Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
728
        """
729

730
        candle_limit = self.ohlcv_candle_limit(
1✔
731
            timeframe,
732
            self._config["candle_type_def"],
733
            dt_ts(date_minus_candles(timeframe, startup_candles)) if timeframe else None,
734
        )
735
        # Require one more candle - to account for the still open candle.
736
        candle_count = startup_candles + 1
1✔
737
        # Allow 5 calls to the exchange per pair
738
        required_candle_call_count = int(
1✔
739
            (candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1)
740
        )
741
        if self._ft_has["ohlcv_has_history"]:
1✔
742
            if required_candle_call_count > 5:
1✔
743
                # Only allow 5 calls per pair to somewhat limit the impact
744
                raise ConfigurationError(
1✔
745
                    f"This strategy requires {startup_candles} candles to start, "
746
                    "which is more than 5x "
747
                    f"the amount of candles {self.name} provides for {timeframe}."
748
                )
749
        elif required_candle_call_count > 1:
1✔
750
            raise ConfigurationError(
1✔
751
                f"This strategy requires {startup_candles} candles to start, which is more than "
752
                f"the amount of candles {self.name} provides for {timeframe}."
753
            )
754
        if required_candle_call_count > 1:
1✔
755
            logger.warning(
1✔
756
                f"Using {required_candle_call_count} calls to get OHLCV. "
757
                f"This can result in slower operations for the bot. Please check "
758
                f"if you really need {startup_candles} candles for your strategy"
759
            )
760
        return required_candle_call_count
1✔
761

762
    def validate_trading_mode_and_margin_mode(
1✔
763
        self,
764
        trading_mode: TradingMode,
765
        margin_mode: Optional[MarginMode],  # Only None when trading_mode = TradingMode.SPOT
766
    ):
767
        """
768
        Checks if freqtrade can perform trades using the configured
769
        trading mode(Margin, Futures) and MarginMode(Cross, Isolated)
770
        Throws OperationalException:
771
            If the trading_mode/margin_mode type are not supported by freqtrade on this exchange
772
        """
773
        if trading_mode != TradingMode.SPOT and (
1✔
774
            (trading_mode, margin_mode) not in self._supported_trading_mode_margin_pairs
775
        ):
776
            mm_value = margin_mode and margin_mode.value
1✔
777
            raise OperationalException(
1✔
778
                f"Freqtrade does not support {mm_value} {trading_mode.value} on {self.name}"
779
            )
780

781
    def get_option(self, param: str, default: Optional[Any] = None) -> Any:
1✔
782
        """
783
        Get parameter value from _ft_has
784
        """
785
        return self._ft_has.get(param, default)
1✔
786

787
    def exchange_has(self, endpoint: str) -> bool:
1✔
788
        """
789
        Checks if exchange implements a specific API endpoint.
790
        Wrapper around ccxt 'has' attribute
791
        :param endpoint: Name of endpoint (e.g. 'fetchOHLCV', 'fetchTickers')
792
        :return: bool
793
        """
794
        if endpoint in self._ft_has.get("exchange_has_overrides", {}):
1✔
795
            return self._ft_has["exchange_has_overrides"][endpoint]
1✔
796
        return endpoint in self._api.has and self._api.has[endpoint]
1✔
797

798
    def get_precision_amount(self, pair: str) -> Optional[float]:
1✔
799
        """
800
        Returns the amount precision of the exchange.
801
        :param pair: Pair to get precision for
802
        :return: precision for amount or None. Must be used in combination with precisionMode
803
        """
804
        return self.markets.get(pair, {}).get("precision", {}).get("amount", None)
1✔
805

806
    def get_precision_price(self, pair: str) -> Optional[float]:
1✔
807
        """
808
        Returns the price precision of the exchange.
809
        :param pair: Pair to get precision for
810
        :return: precision for price or None. Must be used in combination with precisionMode
811
        """
812
        return self.markets.get(pair, {}).get("precision", {}).get("price", None)
1✔
813

814
    def amount_to_precision(self, pair: str, amount: float) -> float:
1✔
815
        """
816
        Returns the amount to buy or sell to a precision the Exchange accepts
817

818
        """
819
        return amount_to_precision(amount, self.get_precision_amount(pair), self.precisionMode)
1✔
820

821
    def price_to_precision(self, pair: str, price: float, *, rounding_mode: int = ROUND) -> float:
1✔
822
        """
823
        Returns the price rounded to the precision the Exchange accepts.
824
        The default price_rounding_mode in conf is ROUND.
825
        For stoploss calculations, must use ROUND_UP for longs, and ROUND_DOWN for shorts.
826
        """
827
        return price_to_precision(
1✔
828
            price, self.get_precision_price(pair), self.precisionMode, rounding_mode=rounding_mode
829
        )
830

831
    def price_get_one_pip(self, pair: str, price: float) -> float:
1✔
832
        """
833
        Gets the "1 pip" value for this pair.
834
        Used in PriceFilter to calculate the 1pip movements.
835
        """
836
        precision = self.markets[pair]["precision"]["price"]
1✔
837
        if self.precisionMode == TICK_SIZE:
1✔
838
            return precision
1✔
839
        else:
840
            return 1 / pow(10, precision)
1✔
841

842
    def get_min_pair_stake_amount(
1✔
843
        self, pair: str, price: float, stoploss: float, leverage: Optional[float] = 1.0
844
    ) -> Optional[float]:
845
        return self._get_stake_amount_limit(pair, price, stoploss, "min", leverage)
1✔
846

847
    def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
1✔
848
        max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, "max", leverage)
1✔
849
        if max_stake_amount is None:
1✔
850
            # * Should never be executed
851
            raise OperationalException(
×
852
                f"{self.name}.get_max_pair_stake_amount should never set max_stake_amount to None"
853
            )
854
        return max_stake_amount
1✔
855

856
    def _get_stake_amount_limit(
1✔
857
        self,
858
        pair: str,
859
        price: float,
860
        stoploss: float,
861
        limit: Literal["min", "max"],
862
        leverage: Optional[float] = 1.0,
863
    ) -> Optional[float]:
864
        isMin = limit == "min"
1✔
865

866
        try:
1✔
867
            market = self.markets[pair]
1✔
868
        except KeyError:
1✔
869
            raise ValueError(f"Can't get market information for symbol {pair}")
1✔
870

871
        if isMin:
1✔
872
            # reserve some percent defined in config (5% default) + stoploss
873
            margin_reserve: float = 1.0 + self._config.get(
1✔
874
                "amount_reserve_percent", DEFAULT_AMOUNT_RESERVE_PERCENT
875
            )
876
            stoploss_reserve = margin_reserve / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
1✔
877
            # it should not be more than 50%
878
            stoploss_reserve = max(min(stoploss_reserve, 1.5), 1)
1✔
879
        else:
880
            margin_reserve = 1.0
1✔
881
            stoploss_reserve = 1.0
1✔
882

883
        stake_limits = []
1✔
884
        limits = market["limits"]
1✔
885
        if limits["cost"][limit] is not None:
1✔
886
            stake_limits.append(
1✔
887
                self._contracts_to_amount(pair, limits["cost"][limit]) * stoploss_reserve
888
            )
889

890
        if limits["amount"][limit] is not None:
1✔
891
            stake_limits.append(
1✔
892
                self._contracts_to_amount(pair, limits["amount"][limit]) * price * margin_reserve
893
            )
894

895
        if not stake_limits:
1✔
896
            return None if isMin else float("inf")
1✔
897

898
        # The value returned should satisfy both limits: for amount (base currency) and
899
        # for cost (quote, stake currency), so max() is used here.
900
        # See also #2575 at github.
901
        return self._get_stake_amount_considering_leverage(
1✔
902
            max(stake_limits) if isMin else min(stake_limits), leverage or 1.0
903
        )
904

905
    def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float) -> float:
1✔
906
        """
907
        Takes the minimum stake amount for a pair with no leverage and returns the minimum
908
        stake amount when leverage is considered
909
        :param stake_amount: The stake amount for a pair before leverage is considered
910
        :param leverage: The amount of leverage being used on the current trade
911
        """
912
        return stake_amount / leverage
1✔
913

914
    # Dry-run methods
915

916
    def create_dry_run_order(
1✔
917
        self,
918
        pair: str,
919
        ordertype: str,
920
        side: str,
921
        amount: float,
922
        rate: float,
923
        leverage: float,
924
        params: Optional[Dict] = None,
925
        stop_loss: bool = False,
926
    ) -> Dict[str, Any]:
927
        now = dt_now()
1✔
928
        order_id = f"dry_run_{side}_{pair}_{now.timestamp()}"
1✔
929
        # Rounding here must respect to contract sizes
930
        _amount = self._contracts_to_amount(
1✔
931
            pair, self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
932
        )
933
        dry_order: Dict[str, Any] = {
1✔
934
            "id": order_id,
935
            "symbol": pair,
936
            "price": rate,
937
            "average": rate,
938
            "amount": _amount,
939
            "cost": _amount * rate,
940
            "type": ordertype,
941
            "side": side,
942
            "filled": 0,
943
            "remaining": _amount,
944
            "datetime": now.strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
945
            "timestamp": dt_ts(now),
946
            "status": "open",
947
            "fee": None,
948
            "info": {},
949
            "leverage": leverage,
950
        }
951
        if stop_loss:
1✔
952
            dry_order["info"] = {"stopPrice": dry_order["price"]}
1✔
953
            dry_order[self._ft_has["stop_price_prop"]] = dry_order["price"]
1✔
954
            # Workaround to avoid filling stoploss orders immediately
955
            dry_order["ft_order_type"] = "stoploss"
1✔
956
        orderbook: Optional[OrderBook] = None
1✔
957
        if self.exchange_has("fetchL2OrderBook"):
1✔
958
            orderbook = self.fetch_l2_order_book(pair, 20)
1✔
959
        if ordertype == "limit" and orderbook:
1✔
960
            # Allow a 1% price difference
961
            allowed_diff = 0.01
1✔
962
            if self._dry_is_price_crossed(pair, side, rate, orderbook, allowed_diff):
1✔
963
                logger.info(
1✔
964
                    f"Converted order {pair} to market order due to price {rate} crossing spread "
965
                    f"by more than {allowed_diff:.2%}."
966
                )
967
                dry_order["type"] = "market"
1✔
968

969
        if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
1✔
970
            # Update market order pricing
971
            average = self.get_dry_market_fill_price(pair, side, amount, rate, orderbook)
1✔
972
            dry_order.update(
1✔
973
                {
974
                    "average": average,
975
                    "filled": _amount,
976
                    "remaining": 0.0,
977
                    "status": "closed",
978
                    "cost": (dry_order["amount"] * average),
979
                }
980
            )
981
            # market orders will always incurr taker fees
982
            dry_order = self.add_dry_order_fee(pair, dry_order, "taker")
1✔
983

984
        dry_order = self.check_dry_limit_order_filled(
1✔
985
            dry_order, immediate=True, orderbook=orderbook
986
        )
987

988
        self._dry_run_open_orders[dry_order["id"]] = dry_order
1✔
989
        # Copy order and close it - so the returned order is open unless it's a market order
990
        return dry_order
1✔
991

992
    def add_dry_order_fee(
1✔
993
        self,
994
        pair: str,
995
        dry_order: Dict[str, Any],
996
        taker_or_maker: MakerTaker,
997
    ) -> Dict[str, Any]:
998
        fee = self.get_fee(pair, taker_or_maker=taker_or_maker)
1✔
999
        dry_order.update(
1✔
1000
            {
1001
                "fee": {
1002
                    "currency": self.get_pair_quote_currency(pair),
1003
                    "cost": dry_order["cost"] * fee,
1004
                    "rate": fee,
1005
                }
1006
            }
1007
        )
1008
        return dry_order
1✔
1009

1010
    def get_dry_market_fill_price(
1✔
1011
        self, pair: str, side: str, amount: float, rate: float, orderbook: Optional[OrderBook]
1012
    ) -> float:
1013
        """
1014
        Get the market order fill price based on orderbook interpolation
1015
        """
1016
        if self.exchange_has("fetchL2OrderBook"):
1✔
1017
            if not orderbook:
1✔
1018
                orderbook = self.fetch_l2_order_book(pair, 20)
×
1019
            ob_type: OBLiteral = "asks" if side == "buy" else "bids"
1✔
1020
            slippage = 0.05
1✔
1021
            max_slippage_val = rate * ((1 + slippage) if side == "buy" else (1 - slippage))
1✔
1022

1023
            remaining_amount = amount
1✔
1024
            filled_value = 0.0
1✔
1025
            book_entry_price = 0.0
1✔
1026
            for book_entry in orderbook[ob_type]:
1✔
1027
                book_entry_price = book_entry[0]
1✔
1028
                book_entry_coin_volume = book_entry[1]
1✔
1029
                if remaining_amount > 0:
1✔
1030
                    if remaining_amount < book_entry_coin_volume:
1✔
1031
                        # Orderbook at this slot bigger than remaining amount
1032
                        filled_value += remaining_amount * book_entry_price
1✔
1033
                        break
1✔
1034
                    else:
1035
                        filled_value += book_entry_coin_volume * book_entry_price
1✔
1036
                    remaining_amount -= book_entry_coin_volume
1✔
1037
                else:
1038
                    break
×
1039
            else:
1040
                # If remaining_amount wasn't consumed completely (break was not called)
1041
                filled_value += remaining_amount * book_entry_price
1✔
1042
            forecast_avg_filled_price = max(filled_value, 0) / amount
1✔
1043
            # Limit max. slippage to specified value
1044
            if side == "buy":
1✔
1045
                forecast_avg_filled_price = min(forecast_avg_filled_price, max_slippage_val)
1✔
1046

1047
            else:
1048
                forecast_avg_filled_price = max(forecast_avg_filled_price, max_slippage_val)
1✔
1049

1050
            return self.price_to_precision(pair, forecast_avg_filled_price)
1✔
1051

1052
        return rate
1✔
1053

1054
    def _dry_is_price_crossed(
1✔
1055
        self,
1056
        pair: str,
1057
        side: str,
1058
        limit: float,
1059
        orderbook: Optional[OrderBook] = None,
1060
        offset: float = 0.0,
1061
    ) -> bool:
1062
        if not self.exchange_has("fetchL2OrderBook"):
1✔
1063
            return True
1✔
1064
        if not orderbook:
1✔
1065
            orderbook = self.fetch_l2_order_book(pair, 1)
1✔
1066
        try:
1✔
1067
            if side == "buy":
1✔
1068
                price = orderbook["asks"][0][0]
1✔
1069
                if limit * (1 - offset) >= price:
1✔
1070
                    return True
1✔
1071
            else:
1072
                price = orderbook["bids"][0][0]
1✔
1073
                if limit * (1 + offset) <= price:
1✔
1074
                    return True
1✔
1075
        except IndexError:
1✔
1076
            # Ignore empty orderbooks when filling - can be filled with the next iteration.
1077
            pass
1✔
1078
        return False
1✔
1079

1080
    def check_dry_limit_order_filled(
1✔
1081
        self, order: Dict[str, Any], immediate: bool = False, orderbook: Optional[OrderBook] = None
1082
    ) -> Dict[str, Any]:
1083
        """
1084
        Check dry-run limit order fill and update fee (if it filled).
1085
        """
1086
        if (
1✔
1087
            order["status"] != "closed"
1088
            and order["type"] in ["limit"]
1089
            and not order.get("ft_order_type")
1090
        ):
1091
            pair = order["symbol"]
1✔
1092
            if self._dry_is_price_crossed(pair, order["side"], order["price"], orderbook):
1✔
1093
                order.update(
1✔
1094
                    {
1095
                        "status": "closed",
1096
                        "filled": order["amount"],
1097
                        "remaining": 0,
1098
                    }
1099
                )
1100

1101
                self.add_dry_order_fee(
1✔
1102
                    pair,
1103
                    order,
1104
                    "taker" if immediate else "maker",
1105
                )
1106

1107
        return order
1✔
1108

1109
    def fetch_dry_run_order(self, order_id) -> Dict[str, Any]:
1✔
1110
        """
1111
        Return dry-run order
1112
        Only call if running in dry-run mode.
1113
        """
1114
        try:
1✔
1115
            order = self._dry_run_open_orders[order_id]
1✔
1116
            order = self.check_dry_limit_order_filled(order)
1✔
1117
            return order
1✔
1118
        except KeyError as e:
1✔
1119
            from freqtrade.persistence import Order
1✔
1120

1121
            order = Order.order_by_id(order_id)
1✔
1122
            if order:
1✔
1123
                ccxt_order = order.to_ccxt_object(self._ft_has["stop_price_prop"])
1✔
1124
                self._dry_run_open_orders[order_id] = ccxt_order
1✔
1125
                return ccxt_order
1✔
1126
            # Gracefully handle errors with dry-run orders.
1127
            raise InvalidOrderException(
1✔
1128
                f"Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}"
1129
            ) from e
1130

1131
    # Order handling
1132

1133
    def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
1✔
1134
        if self.trading_mode != TradingMode.SPOT:
1✔
1135
            self.set_margin_mode(pair, self.margin_mode, accept_fail)
1✔
1136
            self._set_leverage(leverage, pair, accept_fail)
1✔
1137

1138
    def _get_params(
1✔
1139
        self,
1140
        side: BuySell,
1141
        ordertype: str,
1142
        leverage: float,
1143
        reduceOnly: bool,
1144
        time_in_force: str = "GTC",
1145
    ) -> Dict:
1146
        params = self._params.copy()
1✔
1147
        if time_in_force != "GTC" and ordertype != "market":
1✔
1148
            params.update({"timeInForce": time_in_force.upper()})
1✔
1149
        if reduceOnly:
1✔
1150
            params.update({"reduceOnly": True})
1✔
1151
        return params
1✔
1152

1153
    def _order_needs_price(self, ordertype: str) -> bool:
1✔
1154
        return (
1✔
1155
            ordertype != "market"
1156
            or self._api.options.get("createMarketBuyOrderRequiresPrice", False)
1157
            or self._ft_has.get("marketOrderRequiresPrice", False)
1158
        )
1159

1160
    def create_order(
1✔
1161
        self,
1162
        *,
1163
        pair: str,
1164
        ordertype: str,
1165
        side: BuySell,
1166
        amount: float,
1167
        rate: float,
1168
        leverage: float,
1169
        reduceOnly: bool = False,
1170
        time_in_force: str = "GTC",
1171
    ) -> Dict:
1172
        if self._config["dry_run"]:
1✔
1173
            dry_order = self.create_dry_run_order(
1✔
1174
                pair, ordertype, side, amount, self.price_to_precision(pair, rate), leverage
1175
            )
1176
            return dry_order
1✔
1177

1178
        params = self._get_params(side, ordertype, leverage, reduceOnly, time_in_force)
1✔
1179

1180
        try:
1✔
1181
            # Set the precision for amount and price(rate) as accepted by the exchange
1182
            amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
1✔
1183
            needs_price = self._order_needs_price(ordertype)
1✔
1184
            rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
1✔
1185

1186
            if not reduceOnly:
1✔
1187
                self._lev_prep(pair, leverage, side)
1✔
1188

1189
            order = self._api.create_order(
1✔
1190
                pair,
1191
                ordertype,
1192
                side,
1193
                amount,
1194
                rate_for_order,
1195
                params,
1196
            )
1197
            if order.get("status") is None:
1✔
1198
                # Map empty status to open.
1199
                order["status"] = "open"
1✔
1200

1201
            if order.get("type") is None:
1✔
1202
                order["type"] = ordertype
1✔
1203

1204
            self._log_exchange_response("create_order", order)
1✔
1205
            order = self._order_contracts_to_amount(order)
1✔
1206
            return order
1✔
1207

1208
        except ccxt.InsufficientFunds as e:
1✔
1209
            raise InsufficientFundsError(
1✔
1210
                f"Insufficient funds to create {ordertype} {side} order on market {pair}. "
1211
                f"Tried to {side} amount {amount} at rate {rate}."
1212
                f"Message: {e}"
1213
            ) from e
1214
        except ccxt.InvalidOrder as e:
1✔
1215
            raise InvalidOrderException(
1✔
1216
                f"Could not create {ordertype} {side} order on market {pair}. "
1217
                f"Tried to {side} amount {amount} at rate {rate}. "
1218
                f"Message: {e}"
1219
            ) from e
1220
        except ccxt.DDoSProtection as e:
1✔
1221
            raise DDosProtection(e) from e
×
1222
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1223
            raise TemporaryError(
1✔
1224
                f"Could not place {side} order due to {e.__class__.__name__}. Message: {e}"
1225
            ) from e
1226
        except ccxt.BaseError as e:
1✔
1227
            raise OperationalException(e) from e
1✔
1228

1229
    def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
1✔
1230
        """
1231
        Verify stop_loss against stoploss-order value (limit or price)
1232
        Returns True if adjustment is necessary.
1233
        """
1234
        if not self._ft_has.get("stoploss_on_exchange"):
1✔
1235
            raise OperationalException(f"stoploss is not implemented for {self.name}.")
1✔
1236
        price_param = self._ft_has["stop_price_prop"]
1✔
1237
        return order.get(price_param, None) is None or (
1✔
1238
            (side == "sell" and stop_loss > float(order[price_param]))
1239
            or (side == "buy" and stop_loss < float(order[price_param]))
1240
        )
1241

1242
    def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
1✔
1243
        available_order_Types: Dict[str, str] = self._ft_has["stoploss_order_types"]
1✔
1244

1245
        if user_order_type in available_order_Types.keys():
1✔
1246
            ordertype = available_order_Types[user_order_type]
1✔
1247
        else:
1248
            # Otherwise pick only one available
1249
            ordertype = list(available_order_Types.values())[0]
1✔
1250
            user_order_type = list(available_order_Types.keys())[0]
1✔
1251
        return ordertype, user_order_type
1✔
1252

1253
    def _get_stop_limit_rate(self, stop_price: float, order_types: Dict, side: str) -> float:
1✔
1254
        # Limit price threshold: As limit price should always be below stop-price
1255
        limit_price_pct = order_types.get("stoploss_on_exchange_limit_ratio", 0.99)
1✔
1256
        if side == "sell":
1✔
1257
            limit_rate = stop_price * limit_price_pct
1✔
1258
        else:
1259
            limit_rate = stop_price * (2 - limit_price_pct)
1✔
1260

1261
        bad_stop_price = (stop_price < limit_rate) if side == "sell" else (stop_price > limit_rate)
1✔
1262
        # Ensure rate is less than stop price
1263
        if bad_stop_price:
1✔
1264
            # This can for example happen if the stop / liquidation price is set to 0
1265
            # Which is possible if a market-order closes right away.
1266
            # The InvalidOrderException will bubble up to exit_positions, where it will be
1267
            # handled gracefully.
1268
            raise InvalidOrderException(
1✔
1269
                "In stoploss limit order, stop price should be more than limit price. "
1270
                f"Stop price: {stop_price}, Limit price: {limit_rate}, "
1271
                f"Limit Price pct: {limit_price_pct}"
1272
            )
1273
        return limit_rate
1✔
1274

1275
    def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
1✔
1276
        params = self._params.copy()
1✔
1277
        # Verify if stopPrice works for your exchange, else configure stop_price_param
1278
        params.update({self._ft_has["stop_price_param"]: stop_price})
1✔
1279
        return params
1✔
1280

1281
    @retrier(retries=0)
1✔
1282
    def create_stoploss(
1✔
1283
        self,
1284
        pair: str,
1285
        amount: float,
1286
        stop_price: float,
1287
        order_types: Dict,
1288
        side: BuySell,
1289
        leverage: float,
1290
    ) -> Dict:
1291
        """
1292
        creates a stoploss order.
1293
        requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
1294
            to the corresponding exchange type.
1295

1296
        The precise ordertype is determined by the order_types dict or exchange default.
1297

1298
        The exception below should never raise, since we disallow
1299
        starting the bot in validate_ordertypes()
1300

1301
        This may work with a limited number of other exchanges, but correct working
1302
            needs to be tested individually.
1303
        WARNING: setting `stoploss_on_exchange` to True will NOT auto-enable stoploss on exchange.
1304
            `stoploss_adjust` must still be implemented for this to work.
1305
        """
1306
        if not self._ft_has["stoploss_on_exchange"]:
1✔
1307
            raise OperationalException(f"stoploss is not implemented for {self.name}.")
1✔
1308

1309
        user_order_type = order_types.get("stoploss", "market")
1✔
1310
        ordertype, user_order_type = self._get_stop_order_type(user_order_type)
1✔
1311
        round_mode = ROUND_DOWN if side == "buy" else ROUND_UP
1✔
1312
        stop_price_norm = self.price_to_precision(pair, stop_price, rounding_mode=round_mode)
1✔
1313
        limit_rate = None
1✔
1314
        if user_order_type == "limit":
1✔
1315
            limit_rate = self._get_stop_limit_rate(stop_price, order_types, side)
1✔
1316
            limit_rate = self.price_to_precision(pair, limit_rate, rounding_mode=round_mode)
1✔
1317

1318
        if self._config["dry_run"]:
1✔
1319
            dry_order = self.create_dry_run_order(
1✔
1320
                pair,
1321
                ordertype,
1322
                side,
1323
                amount,
1324
                stop_price_norm,
1325
                stop_loss=True,
1326
                leverage=leverage,
1327
            )
1328
            return dry_order
1✔
1329

1330
        try:
1✔
1331
            params = self._get_stop_params(
1✔
1332
                side=side, ordertype=ordertype, stop_price=stop_price_norm
1333
            )
1334
            if self.trading_mode == TradingMode.FUTURES:
1✔
1335
                params["reduceOnly"] = True
1✔
1336
                if "stoploss_price_type" in order_types and "stop_price_type_field" in self._ft_has:
1✔
1337
                    price_type = self._ft_has["stop_price_type_value_mapping"][
1✔
1338
                        order_types.get("stoploss_price_type", PriceType.LAST)
1339
                    ]
1340
                    params[self._ft_has["stop_price_type_field"]] = price_type
1✔
1341

1342
            amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
1✔
1343

1344
            self._lev_prep(pair, leverage, side, accept_fail=True)
1✔
1345
            order = self._api.create_order(
1✔
1346
                symbol=pair,
1347
                type=ordertype,
1348
                side=side,
1349
                amount=amount,
1350
                price=limit_rate,
1351
                params=params,
1352
            )
1353
            self._log_exchange_response("create_stoploss_order", order)
1✔
1354
            order = self._order_contracts_to_amount(order)
1✔
1355
            logger.info(
1✔
1356
                f"stoploss {user_order_type} order added for {pair}. "
1357
                f"stop price: {stop_price}. limit: {limit_rate}"
1358
            )
1359
            return order
1✔
1360
        except ccxt.InsufficientFunds as e:
1✔
1361
            raise InsufficientFundsError(
1✔
1362
                f"Insufficient funds to create {ordertype} {side} order on market {pair}. "
1363
                f"Tried to {side} amount {amount} at rate {limit_rate} with "
1364
                f"stop-price {stop_price_norm}. Message: {e}"
1365
            ) from e
1366
        except (ccxt.InvalidOrder, ccxt.BadRequest, ccxt.OperationRejected) as e:
1✔
1367
            # Errors:
1368
            # `Order would trigger immediately.`
1369
            raise InvalidOrderException(
1✔
1370
                f"Could not create {ordertype} {side} order on market {pair}. "
1371
                f"Tried to {side} amount {amount} at rate {limit_rate} with "
1372
                f"stop-price {stop_price_norm}. Message: {e}"
1373
            ) from e
1374
        except ccxt.DDoSProtection as e:
1✔
1375
            raise DDosProtection(e) from e
1✔
1376
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1377
            raise TemporaryError(
1✔
1378
                f"Could not place stoploss order due to {e.__class__.__name__}. Message: {e}"
1379
            ) from e
1380
        except ccxt.BaseError as e:
1✔
1381
            raise OperationalException(e) from e
1✔
1382

1383
    def fetch_order_emulated(self, order_id: str, pair: str, params: Dict) -> Dict:
1✔
1384
        """
1385
        Emulated fetch_order if the exchange doesn't support fetch_order, but requires separate
1386
        calls for open and closed orders.
1387
        """
1388
        try:
1✔
1389
            order = self._api.fetch_open_order(order_id, pair, params=params)
1✔
1390
            self._log_exchange_response("fetch_open_order", order)
1✔
1391
            order = self._order_contracts_to_amount(order)
1✔
1392
            return order
1✔
1393
        except ccxt.OrderNotFound:
1✔
1394
            try:
1✔
1395
                order = self._api.fetch_closed_order(order_id, pair, params=params)
1✔
1396
                self._log_exchange_response("fetch_closed_order", order)
1✔
1397
                order = self._order_contracts_to_amount(order)
1✔
1398
                return order
1✔
1399
            except ccxt.OrderNotFound as e:
×
1400
                raise RetryableOrderError(
×
1401
                    f"Order not found (pair: {pair} id: {order_id}). Message: {e}"
1402
                ) from e
1403
        except ccxt.InvalidOrder as e:
1✔
1404
            raise InvalidOrderException(
1✔
1405
                f"Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}"
1406
            ) from e
1407
        except ccxt.DDoSProtection as e:
1✔
1408
            raise DDosProtection(e) from e
1✔
1409
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1410
            raise TemporaryError(
1✔
1411
                f"Could not get order due to {e.__class__.__name__}. Message: {e}"
1412
            ) from e
1413
        except ccxt.BaseError as e:
1✔
1414
            raise OperationalException(e) from e
1✔
1415

1416
    @retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
1✔
1417
    def fetch_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
1✔
1418
        if self._config["dry_run"]:
1✔
1419
            return self.fetch_dry_run_order(order_id)
1✔
1420
        if params is None:
1✔
1421
            params = {}
1✔
1422
        try:
1✔
1423
            if not self.exchange_has("fetchOrder"):
1✔
1424
                return self.fetch_order_emulated(order_id, pair, params)
1✔
1425
            order = self._api.fetch_order(order_id, pair, params=params)
1✔
1426
            self._log_exchange_response("fetch_order", order)
1✔
1427
            order = self._order_contracts_to_amount(order)
1✔
1428
            return order
1✔
1429
        except ccxt.OrderNotFound as e:
1✔
1430
            raise RetryableOrderError(
1✔
1431
                f"Order not found (pair: {pair} id: {order_id}). Message: {e}"
1432
            ) from e
1433
        except ccxt.InvalidOrder as e:
1✔
1434
            raise InvalidOrderException(
1✔
1435
                f"Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}"
1436
            ) from e
1437
        except ccxt.DDoSProtection as e:
1✔
1438
            raise DDosProtection(e) from e
1✔
1439
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1440
            raise TemporaryError(
1✔
1441
                f"Could not get order due to {e.__class__.__name__}. Message: {e}"
1442
            ) from e
1443
        except ccxt.BaseError as e:
1✔
1444
            raise OperationalException(e) from e
1✔
1445

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

1449
    def fetch_order_or_stoploss_order(
1✔
1450
        self, order_id: str, pair: str, stoploss_order: bool = False
1451
    ) -> Dict:
1452
        """
1453
        Simple wrapper calling either fetch_order or fetch_stoploss_order depending on
1454
        the stoploss_order parameter
1455
        :param order_id: OrderId to fetch order
1456
        :param pair: Pair corresponding to order_id
1457
        :param stoploss_order: If true, uses fetch_stoploss_order, otherwise fetch_order.
1458
        """
1459
        if stoploss_order:
1✔
1460
            return self.fetch_stoploss_order(order_id, pair)
1✔
1461
        return self.fetch_order(order_id, pair)
1✔
1462

1463
    def check_order_canceled_empty(self, order: Dict) -> bool:
1✔
1464
        """
1465
        Verify if an order has been cancelled without being partially filled
1466
        :param order: Order dict as returned from fetch_order()
1467
        :return: True if order has been cancelled without being filled, False otherwise.
1468
        """
1469
        return order.get("status") in NON_OPEN_EXCHANGE_STATES and order.get("filled") == 0.0
1✔
1470

1471
    @retrier
1✔
1472
    def cancel_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
1✔
1473
        if self._config["dry_run"]:
1✔
1474
            try:
1✔
1475
                order = self.fetch_dry_run_order(order_id)
1✔
1476

1477
                order.update({"status": "canceled", "filled": 0.0, "remaining": order["amount"]})
1✔
1478
                return order
1✔
1479
            except InvalidOrderException:
1✔
1480
                return {}
1✔
1481

1482
        if params is None:
1✔
1483
            params = {}
1✔
1484
        try:
1✔
1485
            order = self._api.cancel_order(order_id, pair, params=params)
1✔
1486
            self._log_exchange_response("cancel_order", order)
1✔
1487
            order = self._order_contracts_to_amount(order)
1✔
1488
            return order
1✔
1489
        except ccxt.InvalidOrder as e:
1✔
1490
            raise InvalidOrderException(f"Could not cancel order. Message: {e}") from e
1✔
1491
        except ccxt.DDoSProtection as e:
1✔
1492
            raise DDosProtection(e) from e
1✔
1493
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1494
            raise TemporaryError(
1✔
1495
                f"Could not cancel order due to {e.__class__.__name__}. Message: {e}"
1496
            ) from e
1497
        except ccxt.BaseError as e:
1✔
1498
            raise OperationalException(e) from e
1✔
1499

1500
    def cancel_stoploss_order(
1✔
1501
        self, order_id: str, pair: str, params: Optional[Dict] = None
1502
    ) -> Dict:
1503
        return self.cancel_order(order_id, pair, params)
1✔
1504

1505
    def is_cancel_order_result_suitable(self, corder) -> bool:
1✔
1506
        if not isinstance(corder, dict):
1✔
1507
            return False
1✔
1508

1509
        required = ("fee", "status", "amount")
1✔
1510
        return all(corder.get(k, None) is not None for k in required)
1✔
1511

1512
    def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
1✔
1513
        """
1514
        Cancel order returning a result.
1515
        Creates a fake result if cancel order returns a non-usable result
1516
        and fetch_order does not work (certain exchanges don't return cancelled orders)
1517
        :param order_id: Orderid to cancel
1518
        :param pair: Pair corresponding to order_id
1519
        :param amount: Amount to use for fake response
1520
        :return: Result from either cancel_order if usable, or fetch_order
1521
        """
1522
        try:
1✔
1523
            corder = self.cancel_order(order_id, pair)
1✔
1524
            if self.is_cancel_order_result_suitable(corder):
1✔
1525
                return corder
1✔
1526
        except InvalidOrderException:
1✔
1527
            logger.warning(f"Could not cancel order {order_id} for {pair}.")
1✔
1528
        try:
1✔
1529
            order = self.fetch_order(order_id, pair)
1✔
1530
        except InvalidOrderException:
1✔
1531
            logger.warning(f"Could not fetch cancelled order {order_id}.")
1✔
1532
            order = {
1✔
1533
                "id": order_id,
1534
                "status": "canceled",
1535
                "amount": amount,
1536
                "filled": 0.0,
1537
                "fee": {},
1538
                "info": {},
1539
            }
1540

1541
        return order
1✔
1542

1543
    def cancel_stoploss_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
1✔
1544
        """
1545
        Cancel stoploss order returning a result.
1546
        Creates a fake result if cancel order returns a non-usable result
1547
        and fetch_order does not work (certain exchanges don't return cancelled orders)
1548
        :param order_id: stoploss-order-id to cancel
1549
        :param pair: Pair corresponding to order_id
1550
        :param amount: Amount to use for fake response
1551
        :return: Result from either cancel_order if usable, or fetch_order
1552
        """
1553
        corder = self.cancel_stoploss_order(order_id, pair)
1✔
1554
        if self.is_cancel_order_result_suitable(corder):
1✔
1555
            return corder
1✔
1556
        try:
1✔
1557
            order = self.fetch_stoploss_order(order_id, pair)
1✔
1558
        except InvalidOrderException:
1✔
1559
            logger.warning(f"Could not fetch cancelled stoploss order {order_id}.")
1✔
1560
            order = {"id": order_id, "fee": {}, "status": "canceled", "amount": amount, "info": {}}
1✔
1561

1562
        return order
1✔
1563

1564
    @retrier
1✔
1565
    def get_balances(self) -> dict:
1✔
1566
        try:
1✔
1567
            balances = self._api.fetch_balance()
1✔
1568
            # Remove additional info from ccxt results
1569
            balances.pop("info", None)
1✔
1570
            balances.pop("free", None)
1✔
1571
            balances.pop("total", None)
1✔
1572
            balances.pop("used", None)
1✔
1573

1574
            return balances
1✔
1575
        except ccxt.DDoSProtection as e:
1✔
1576
            raise DDosProtection(e) from e
1✔
1577
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1578
            raise TemporaryError(
1✔
1579
                f"Could not get balance due to {e.__class__.__name__}. Message: {e}"
1580
            ) from e
1581
        except ccxt.BaseError as e:
1✔
1582
            raise OperationalException(e) from e
1✔
1583

1584
    @retrier
1✔
1585
    def fetch_positions(self, pair: Optional[str] = None) -> List[Dict]:
1✔
1586
        """
1587
        Fetch positions from the exchange.
1588
        If no pair is given, all positions are returned.
1589
        :param pair: Pair for the query
1590
        """
1591
        if self._config["dry_run"] or self.trading_mode != TradingMode.FUTURES:
1✔
1592
            return []
1✔
1593
        try:
1✔
1594
            symbols = []
1✔
1595
            if pair:
1✔
1596
                symbols.append(pair)
1✔
1597
            positions: List[Dict] = self._api.fetch_positions(symbols)
1✔
1598
            self._log_exchange_response("fetch_positions", positions)
1✔
1599
            return positions
1✔
1600
        except ccxt.DDoSProtection as e:
1✔
1601
            raise DDosProtection(e) from e
1✔
1602
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1603
            raise TemporaryError(
1✔
1604
                f"Could not get positions due to {e.__class__.__name__}. Message: {e}"
1605
            ) from e
1606
        except ccxt.BaseError as e:
1✔
1607
            raise OperationalException(e) from e
1✔
1608

1609
    def _fetch_orders_emulate(self, pair: str, since_ms: int) -> List[Dict]:
1✔
1610
        orders = []
1✔
1611
        if self.exchange_has("fetchClosedOrders"):
1✔
1612
            orders = self._api.fetch_closed_orders(pair, since=since_ms)
1✔
1613
            if self.exchange_has("fetchOpenOrders"):
1✔
1614
                orders_open = self._api.fetch_open_orders(pair, since=since_ms)
1✔
1615
                orders.extend(orders_open)
1✔
1616
        return orders
1✔
1617

1618
    @retrier(retries=0)
1✔
1619
    def fetch_orders(self, pair: str, since: datetime, params: Optional[Dict] = None) -> List[Dict]:
1✔
1620
        """
1621
        Fetch all orders for a pair "since"
1622
        :param pair: Pair for the query
1623
        :param since: Starting time for the query
1624
        """
1625
        if self._config["dry_run"]:
1✔
1626
            return []
1✔
1627

1628
        try:
1✔
1629
            since_ms = int((since.timestamp() - 10) * 1000)
1✔
1630

1631
            if self.exchange_has("fetchOrders"):
1✔
1632
                if not params:
1✔
1633
                    params = {}
1✔
1634
                try:
1✔
1635
                    orders: List[Dict] = self._api.fetch_orders(pair, since=since_ms, params=params)
1✔
1636
                except ccxt.NotSupported:
1✔
1637
                    # Some exchanges don't support fetchOrders
1638
                    # attempt to fetch open and closed orders separately
1639
                    orders = self._fetch_orders_emulate(pair, since_ms)
1✔
1640
            else:
1641
                orders = self._fetch_orders_emulate(pair, since_ms)
1✔
1642
            self._log_exchange_response("fetch_orders", orders)
1✔
1643
            orders = [self._order_contracts_to_amount(o) for o in orders]
1✔
1644
            return orders
1✔
1645
        except ccxt.DDoSProtection as e:
1✔
1646
            raise DDosProtection(e) from e
1✔
1647
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1648
            raise TemporaryError(
1✔
1649
                f"Could not fetch positions due to {e.__class__.__name__}. Message: {e}"
1650
            ) from e
1651
        except ccxt.BaseError as e:
1✔
1652
            raise OperationalException(e) from e
1✔
1653

1654
    @retrier
1✔
1655
    def fetch_trading_fees(self) -> Dict[str, Any]:
1✔
1656
        """
1657
        Fetch user account trading fees
1658
        Can be cached, should not update often.
1659
        """
1660
        if (
1✔
1661
            self._config["dry_run"]
1662
            or self.trading_mode != TradingMode.FUTURES
1663
            or not self.exchange_has("fetchTradingFees")
1664
        ):
1665
            return {}
1✔
1666
        try:
1✔
1667
            trading_fees: Dict[str, Any] = self._api.fetch_trading_fees()
1✔
1668
            self._log_exchange_response("fetch_trading_fees", trading_fees)
1✔
1669
            return trading_fees
1✔
1670
        except ccxt.DDoSProtection as e:
1✔
1671
            raise DDosProtection(e) from e
1✔
1672
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1673
            raise TemporaryError(
1✔
1674
                f"Could not fetch trading fees due to {e.__class__.__name__}. Message: {e}"
1675
            ) from e
1676
        except ccxt.BaseError as e:
1✔
1677
            raise OperationalException(e) from e
1✔
1678

1679
    @retrier
1✔
1680
    def fetch_bids_asks(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Dict:
1✔
1681
        """
1682
        :param symbols: List of symbols to fetch
1683
        :param cached: Allow cached result
1684
        :return: fetch_bids_asks result
1685
        """
1686
        if not self.exchange_has("fetchBidsAsks"):
1✔
1687
            return {}
1✔
1688
        if cached:
1✔
1689
            with self._cache_lock:
1✔
1690
                tickers = self._fetch_tickers_cache.get("fetch_bids_asks")
1✔
1691
            if tickers:
1✔
1692
                return tickers
1✔
1693
        try:
1✔
1694
            tickers = self._api.fetch_bids_asks(symbols)
1✔
1695
            with self._cache_lock:
1✔
1696
                self._fetch_tickers_cache["fetch_bids_asks"] = tickers
1✔
1697
            return tickers
1✔
1698
        except ccxt.NotSupported as e:
1✔
1699
            raise OperationalException(
1✔
1700
                f"Exchange {self._api.name} does not support fetching bids/asks in batch. "
1701
                f"Message: {e}"
1702
            ) from e
1703
        except ccxt.DDoSProtection as e:
1✔
1704
            raise DDosProtection(e) from e
1✔
1705
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1706
            raise TemporaryError(
1✔
1707
                f"Could not load bids/asks due to {e.__class__.__name__}. Message: {e}"
1708
            ) from e
1709
        except ccxt.BaseError as e:
1✔
1710
            raise OperationalException(e) from e
1✔
1711

1712
    @retrier
1✔
1713
    def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
1✔
1714
        """
1715
        :param cached: Allow cached result
1716
        :return: fetch_tickers result
1717
        """
1718
        tickers: Tickers
1719
        if not self.exchange_has("fetchTickers"):
1✔
1720
            return {}
1✔
1721
        if cached:
1✔
1722
            with self._cache_lock:
1✔
1723
                tickers = self._fetch_tickers_cache.get("fetch_tickers")  # type: ignore
1✔
1724
            if tickers:
1✔
1725
                return tickers
1✔
1726
        try:
1✔
1727
            tickers = self._api.fetch_tickers(symbols)
1✔
1728
            with self._cache_lock:
1✔
1729
                self._fetch_tickers_cache["fetch_tickers"] = tickers
1✔
1730
            return tickers
1✔
1731
        except ccxt.NotSupported as e:
1✔
1732
            raise OperationalException(
1✔
1733
                f"Exchange {self._api.name} does not support fetching tickers in batch. "
1734
                f"Message: {e}"
1735
            ) from e
1736
        except ccxt.BadSymbol as e:
1✔
1737
            logger.warning(
1✔
1738
                f"Could not load tickers due to {e.__class__.__name__}. Message: {e} ."
1739
                "Reloading markets."
1740
            )
1741
            self.reload_markets(True)
1✔
1742
            # Re-raise exception to repeat the call.
1743
            raise TemporaryError from e
1✔
1744
        except ccxt.DDoSProtection as e:
1✔
1745
            raise DDosProtection(e) from e
1✔
1746
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1747
            raise TemporaryError(
1✔
1748
                f"Could not load tickers due to {e.__class__.__name__}. Message: {e}"
1749
            ) from e
1750
        except ccxt.BaseError as e:
1✔
1751
            raise OperationalException(e) from e
1✔
1752

1753
    # Pricing info
1754

1755
    @retrier
1✔
1756
    def fetch_ticker(self, pair: str) -> Ticker:
1✔
1757
        try:
1✔
1758
            if pair not in self.markets or self.markets[pair].get("active", False) is False:
1✔
1759
                raise ExchangeError(f"Pair {pair} not available")
1✔
1760
            data: Ticker = self._api.fetch_ticker(pair)
1✔
1761
            return data
1✔
1762
        except ccxt.DDoSProtection as e:
1✔
1763
            raise DDosProtection(e) from e
1✔
1764
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1765
            raise TemporaryError(
1✔
1766
                f"Could not load ticker due to {e.__class__.__name__}. Message: {e}"
1767
            ) from e
1768
        except ccxt.BaseError as e:
1✔
1769
            raise OperationalException(e) from e
1✔
1770

1771
    @staticmethod
1✔
1772
    def get_next_limit_in_list(
1✔
1773
        limit: int, limit_range: Optional[List[int]], range_required: bool = True
1774
    ):
1775
        """
1776
        Get next greater value in the list.
1777
        Used by fetch_l2_order_book if the api only supports a limited range
1778
        """
1779
        if not limit_range:
1✔
1780
            return limit
1✔
1781

1782
        result = min([x for x in limit_range if limit <= x] + [max(limit_range)])
1✔
1783
        if not range_required and limit > result:
1✔
1784
            # Range is not required - we can use None as parameter.
1785
            return None
1✔
1786
        return result
1✔
1787

1788
    @retrier
1✔
1789
    def fetch_l2_order_book(self, pair: str, limit: int = 100) -> OrderBook:
1✔
1790
        """
1791
        Get L2 order book from exchange.
1792
        Can be limited to a certain amount (if supported).
1793
        Returns a dict in the format
1794
        {'asks': [price, volume], 'bids': [price, volume]}
1795
        """
1796
        limit1 = self.get_next_limit_in_list(
1✔
1797
            limit, self._ft_has["l2_limit_range"], self._ft_has["l2_limit_range_required"]
1798
        )
1799
        try:
1✔
1800
            return self._api.fetch_l2_order_book(pair, limit1)
1✔
1801
        except ccxt.NotSupported as e:
1✔
1802
            raise OperationalException(
1✔
1803
                f"Exchange {self._api.name} does not support fetching order book. Message: {e}"
1804
            ) from e
1805
        except ccxt.DDoSProtection as e:
1✔
1806
            raise DDosProtection(e) from e
×
1807
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
1808
            raise TemporaryError(
1✔
1809
                f"Could not get order book due to {e.__class__.__name__}. Message: {e}"
1810
            ) from e
1811
        except ccxt.BaseError as e:
1✔
1812
            raise OperationalException(e) from e
1✔
1813

1814
    def _get_price_side(self, side: str, is_short: bool, conf_strategy: Dict) -> BidAsk:
1✔
1815
        price_side = conf_strategy["price_side"]
1✔
1816

1817
        if price_side in ("same", "other"):
1✔
1818
            price_map = {
1✔
1819
                ("entry", "long", "same"): "bid",
1820
                ("entry", "long", "other"): "ask",
1821
                ("entry", "short", "same"): "ask",
1822
                ("entry", "short", "other"): "bid",
1823
                ("exit", "long", "same"): "ask",
1824
                ("exit", "long", "other"): "bid",
1825
                ("exit", "short", "same"): "bid",
1826
                ("exit", "short", "other"): "ask",
1827
            }
1828
            price_side = price_map[(side, "short" if is_short else "long", price_side)]
1✔
1829
        return price_side
1✔
1830

1831
    def get_rate(
1✔
1832
        self,
1833
        pair: str,
1834
        refresh: bool,
1835
        side: EntryExit,
1836
        is_short: bool,
1837
        order_book: Optional[OrderBook] = None,
1838
        ticker: Optional[Ticker] = None,
1839
    ) -> float:
1840
        """
1841
        Calculates bid/ask target
1842
        bid rate - between current ask price and last price
1843
        ask rate - either using ticker bid or first bid based on orderbook
1844
        or remain static in any other case since it's not updating.
1845
        :param pair: Pair to get rate for
1846
        :param refresh: allow cached data
1847
        :param side: "buy" or "sell"
1848
        :return: float: Price
1849
        :raises PricingError if orderbook price could not be determined.
1850
        """
1851
        name = side.capitalize()
1✔
1852
        strat_name = "entry_pricing" if side == "entry" else "exit_pricing"
1✔
1853

1854
        cache_rate: TTLCache = self._entry_rate_cache if side == "entry" else self._exit_rate_cache
1✔
1855
        if not refresh:
1✔
1856
            with self._cache_lock:
1✔
1857
                rate = cache_rate.get(pair)
1✔
1858
            # Check if cache has been invalidated
1859
            if rate:
1✔
1860
                logger.debug(f"Using cached {side} rate for {pair}.")
1✔
1861
                return rate
1✔
1862

1863
        conf_strategy = self._config.get(strat_name, {})
1✔
1864

1865
        price_side = self._get_price_side(side, is_short, conf_strategy)
1✔
1866

1867
        if conf_strategy.get("use_order_book", False):
1✔
1868
            order_book_top = conf_strategy.get("order_book_top", 1)
1✔
1869
            if order_book is None:
1✔
1870
                order_book = self.fetch_l2_order_book(pair, order_book_top)
1✔
1871
            rate = self._get_rate_from_ob(pair, side, order_book, name, price_side, order_book_top)
1✔
1872
        else:
1873
            logger.debug(f"Using Last {price_side.capitalize()} / Last Price")
1✔
1874
            if ticker is None:
1✔
1875
                ticker = self.fetch_ticker(pair)
1✔
1876
            rate = self._get_rate_from_ticker(side, ticker, conf_strategy, price_side)
1✔
1877

1878
        if rate is None:
1✔
1879
            raise PricingError(f"{name}-Rate for {pair} was empty.")
1✔
1880
        with self._cache_lock:
1✔
1881
            cache_rate[pair] = rate
1✔
1882

1883
        return rate
1✔
1884

1885
    def _get_rate_from_ticker(
1✔
1886
        self, side: EntryExit, ticker: Ticker, conf_strategy: Dict[str, Any], price_side: BidAsk
1887
    ) -> Optional[float]:
1888
        """
1889
        Get rate from ticker.
1890
        """
1891
        ticker_rate = ticker[price_side]
1✔
1892
        if ticker["last"] and ticker_rate:
1✔
1893
            if side == "entry" and ticker_rate > ticker["last"]:
1✔
1894
                balance = conf_strategy.get("price_last_balance", 0.0)
1✔
1895
                ticker_rate = ticker_rate + balance * (ticker["last"] - ticker_rate)
1✔
1896
            elif side == "exit" and ticker_rate < ticker["last"]:
1✔
1897
                balance = conf_strategy.get("price_last_balance", 0.0)
1✔
1898
                ticker_rate = ticker_rate - balance * (ticker_rate - ticker["last"])
1✔
1899
        rate = ticker_rate
1✔
1900
        return rate
1✔
1901

1902
    def _get_rate_from_ob(
1✔
1903
        self,
1904
        pair: str,
1905
        side: EntryExit,
1906
        order_book: OrderBook,
1907
        name: str,
1908
        price_side: BidAsk,
1909
        order_book_top: int,
1910
    ) -> float:
1911
        """
1912
        Get rate from orderbook
1913
        :raises: PricingError if rate could not be determined.
1914
        """
1915
        logger.debug("order_book %s", order_book)
1✔
1916
        # top 1 = index 0
1917
        try:
1✔
1918
            obside: OBLiteral = "bids" if price_side == "bid" else "asks"
1✔
1919
            rate = order_book[obside][order_book_top - 1][0]
1✔
1920
        except (IndexError, KeyError) as e:
1✔
1921
            logger.warning(
1✔
1922
                f"{pair} - {name} Price at location {order_book_top} from orderbook "
1923
                f"could not be determined. Orderbook: {order_book}"
1924
            )
1925
            raise PricingError from e
1✔
1926
        logger.debug(
1✔
1927
            f"{pair} - {name} price from orderbook {price_side.capitalize()}"
1928
            f"side - top {order_book_top} order book {side} rate {rate:.8f}"
1929
        )
1930
        return rate
1✔
1931

1932
    def get_rates(self, pair: str, refresh: bool, is_short: bool) -> Tuple[float, float]:
1✔
1933
        entry_rate = None
1✔
1934
        exit_rate = None
1✔
1935
        if not refresh:
1✔
1936
            with self._cache_lock:
1✔
1937
                entry_rate = self._entry_rate_cache.get(pair)
1✔
1938
                exit_rate = self._exit_rate_cache.get(pair)
1✔
1939
            if entry_rate:
1✔
1940
                logger.debug(f"Using cached buy rate for {pair}.")
1✔
1941
            if exit_rate:
1✔
1942
                logger.debug(f"Using cached sell rate for {pair}.")
1✔
1943

1944
        entry_pricing = self._config.get("entry_pricing", {})
1✔
1945
        exit_pricing = self._config.get("exit_pricing", {})
1✔
1946
        order_book = ticker = None
1✔
1947
        if not entry_rate and entry_pricing.get("use_order_book", False):
1✔
1948
            order_book_top = max(
1✔
1949
                entry_pricing.get("order_book_top", 1), exit_pricing.get("order_book_top", 1)
1950
            )
1951
            order_book = self.fetch_l2_order_book(pair, order_book_top)
1✔
1952
            entry_rate = self.get_rate(pair, refresh, "entry", is_short, order_book=order_book)
1✔
1953
        elif not entry_rate:
1✔
1954
            ticker = self.fetch_ticker(pair)
1✔
1955
            entry_rate = self.get_rate(pair, refresh, "entry", is_short, ticker=ticker)
1✔
1956
        if not exit_rate:
1✔
1957
            exit_rate = self.get_rate(
1✔
1958
                pair, refresh, "exit", is_short, order_book=order_book, ticker=ticker
1959
            )
1960
        return entry_rate, exit_rate
1✔
1961

1962
    # Fee handling
1963

1964
    @retrier
1✔
1965
    def get_trades_for_order(
1✔
1966
        self, order_id: str, pair: str, since: datetime, params: Optional[Dict] = None
1967
    ) -> List:
1968
        """
1969
        Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id.
1970
        The "since" argument passed in is coming from the database and is in UTC,
1971
        as timezone-native datetime object.
1972
        From the python documentation:
1973
            > Naive datetime instances are assumed to represent local time
1974
        Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the
1975
        transformation from local timezone to UTC.
1976
        This works for timezones UTC+ since then the result will contain trades from a few hours
1977
        instead of from the last 5 seconds, however fails for UTC- timezones,
1978
        since we're then asking for trades with a "since" argument in the future.
1979

1980
        :param order_id order_id: Order-id as given when creating the order
1981
        :param pair: Pair the order is for
1982
        :param since: datetime object of the order creation time. Assumes object is in UTC.
1983
        """
1984
        if self._config["dry_run"]:
1✔
1985
            return []
1✔
1986
        if not self.exchange_has("fetchMyTrades"):
1✔
1987
            return []
1✔
1988
        try:
1✔
1989
            # Allow 5s offset to catch slight time offsets (discovered in #1185)
1990
            # since needs to be int in milliseconds
1991
            _params = params if params else {}
1✔
1992
            my_trades = self._api.fetch_my_trades(
1✔
1993
                pair,
1994
                int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000),
1995
                params=_params,
1996
            )
1997
            matched_trades = [trade for trade in my_trades if trade["order"] == order_id]
1✔
1998

1999
            self._log_exchange_response("get_trades_for_order", matched_trades)
1✔
2000

2001
            matched_trades = self._trades_contracts_to_amount(matched_trades)
1✔
2002

2003
            return matched_trades
1✔
2004
        except ccxt.DDoSProtection as e:
1✔
2005
            raise DDosProtection(e) from e
1✔
2006
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2007
            raise TemporaryError(
1✔
2008
                f"Could not get trades due to {e.__class__.__name__}. Message: {e}"
2009
            ) from e
2010
        except ccxt.BaseError as e:
1✔
2011
            raise OperationalException(e) from e
1✔
2012

2013
    def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
1✔
2014
        return order["id"]
1✔
2015

2016
    @retrier
1✔
2017
    def get_fee(
1✔
2018
        self,
2019
        symbol: str,
2020
        type: str = "",
2021
        side: str = "",
2022
        amount: float = 1,
2023
        price: float = 1,
2024
        taker_or_maker: MakerTaker = "maker",
2025
    ) -> float:
2026
        """
2027
        Retrieve fee from exchange
2028
        :param symbol: Pair
2029
        :param type: Type of order (market, limit, ...)
2030
        :param side: Side of order (buy, sell)
2031
        :param amount: Amount of order
2032
        :param price: Price of order
2033
        :param taker_or_maker: 'maker' or 'taker' (ignored if "type" is provided)
2034
        """
2035
        if type and type == "market":
1✔
2036
            taker_or_maker = "taker"
×
2037
        try:
1✔
2038
            if self._config["dry_run"] and self._config.get("fee", None) is not None:
1✔
2039
                return self._config["fee"]
1✔
2040
            # validate that markets are loaded before trying to get fee
2041
            if self._api.markets is None or len(self._api.markets) == 0:
1✔
2042
                self._api.load_markets(params={})
1✔
2043

2044
            return self._api.calculate_fee(
1✔
2045
                symbol=symbol,
2046
                type=type,
2047
                side=side,
2048
                amount=amount,
2049
                price=price,
2050
                takerOrMaker=taker_or_maker,
2051
            )["rate"]
2052
        except ccxt.DDoSProtection as e:
1✔
2053
            raise DDosProtection(e) from e
1✔
2054
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2055
            raise TemporaryError(
1✔
2056
                f"Could not get fee info due to {e.__class__.__name__}. Message: {e}"
2057
            ) from e
2058
        except ccxt.BaseError as e:
1✔
2059
            raise OperationalException(e) from e
1✔
2060

2061
    @staticmethod
1✔
2062
    def order_has_fee(order: Dict) -> bool:
1✔
2063
        """
2064
        Verifies if the passed in order dict has the needed keys to extract fees,
2065
        and that these keys (currency, cost) are not empty.
2066
        :param order: Order or trade (one trade) dict
2067
        :return: True if the fee substructure contains currency and cost, false otherwise
2068
        """
2069
        if not isinstance(order, dict):
1✔
2070
            return False
1✔
2071
        return (
1✔
2072
            "fee" in order
2073
            and order["fee"] is not None
2074
            and (order["fee"].keys() >= {"currency", "cost"})
2075
            and order["fee"]["currency"] is not None
2076
            and order["fee"]["cost"] is not None
2077
        )
2078

2079
    def calculate_fee_rate(
1✔
2080
        self, fee: Dict, symbol: str, cost: float, amount: float
2081
    ) -> Optional[float]:
2082
        """
2083
        Calculate fee rate if it's not given by the exchange.
2084
        :param fee: ccxt Fee dict - must contain cost / currency / rate
2085
        :param symbol: Symbol of the order
2086
        :param cost: Total cost of the order
2087
        :param amount: Amount of the order
2088
        """
2089
        if fee.get("rate") is not None:
1✔
2090
            return fee.get("rate")
1✔
2091
        fee_curr = fee.get("currency")
1✔
2092
        if fee_curr is None:
1✔
2093
            return None
1✔
2094
        fee_cost = float(fee["cost"])
1✔
2095

2096
        # Calculate fee based on order details
2097
        if fee_curr == self.get_pair_base_currency(symbol):
1✔
2098
            # Base currency - divide by amount
2099
            return round(fee_cost / amount, 8)
1✔
2100
        elif fee_curr == self.get_pair_quote_currency(symbol):
1✔
2101
            # Quote currency - divide by cost
2102
            return round(fee_cost / cost, 8) if cost else None
1✔
2103
        else:
2104
            # If Fee currency is a different currency
2105
            if not cost:
1✔
2106
                # If cost is None or 0.0 -> falsy, return None
2107
                return None
1✔
2108
            try:
1✔
2109
                comb = self.get_valid_pair_combination(fee_curr, self._config["stake_currency"])
1✔
2110
                tick = self.fetch_ticker(comb)
1✔
2111

2112
                fee_to_quote_rate = safe_value_fallback2(tick, tick, "last", "ask")
1✔
2113
            except (ValueError, ExchangeError):
1✔
2114
                fee_to_quote_rate = self._config["exchange"].get("unknown_fee_rate", None)
1✔
2115
                if not fee_to_quote_rate:
1✔
2116
                    return None
1✔
2117
            return round((fee_cost * fee_to_quote_rate) / cost, 8)
1✔
2118

2119
    def extract_cost_curr_rate(
1✔
2120
        self, fee: Dict, symbol: str, cost: float, amount: float
2121
    ) -> Tuple[float, str, Optional[float]]:
2122
        """
2123
        Extract tuple of cost, currency, rate.
2124
        Requires order_has_fee to run first!
2125
        :param fee: ccxt Fee dict - must contain cost / currency / rate
2126
        :param symbol: Symbol of the order
2127
        :param cost: Total cost of the order
2128
        :param amount: Amount of the order
2129
        :return: Tuple with cost, currency, rate of the given fee dict
2130
        """
2131
        return (
1✔
2132
            float(fee["cost"]),
2133
            fee["currency"],
2134
            self.calculate_fee_rate(fee, symbol, cost, amount),
2135
        )
2136

2137
    # Historic data
2138

2139
    def get_historic_ohlcv(
1✔
2140
        self,
2141
        pair: str,
2142
        timeframe: str,
2143
        since_ms: int,
2144
        candle_type: CandleType,
2145
        is_new_pair: bool = False,
2146
        until_ms: Optional[int] = None,
2147
    ) -> List:
2148
        """
2149
        Get candle history using asyncio and returns the list of candles.
2150
        Handles all async work for this.
2151
        Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call.
2152
        :param pair: Pair to download
2153
        :param timeframe: Timeframe to get data for
2154
        :param since_ms: Timestamp in milliseconds to get history from
2155
        :param until_ms: Timestamp in milliseconds to get history up to
2156
        :param candle_type: '', mark, index, premiumIndex, or funding_rate
2157
        :return: List with candle (OHLCV) data
2158
        """
2159
        pair, _, _, data, _ = self.loop.run_until_complete(
1✔
2160
            self._async_get_historic_ohlcv(
2161
                pair=pair,
2162
                timeframe=timeframe,
2163
                since_ms=since_ms,
2164
                until_ms=until_ms,
2165
                is_new_pair=is_new_pair,
2166
                candle_type=candle_type,
2167
            )
2168
        )
2169
        logger.info(f"Downloaded data for {pair} with length {len(data)}.")
1✔
2170
        return data
1✔
2171

2172
    async def _async_get_historic_ohlcv(
1✔
2173
        self,
2174
        pair: str,
2175
        timeframe: str,
2176
        since_ms: int,
2177
        candle_type: CandleType,
2178
        is_new_pair: bool = False,
2179
        raise_: bool = False,
2180
        until_ms: Optional[int] = None,
2181
    ) -> OHLCVResponse:
2182
        """
2183
        Download historic ohlcv
2184
        :param is_new_pair: used by binance subclass to allow "fast" new pair downloading
2185
        :param candle_type: Any of the enum CandleType (must match trading mode!)
2186
        """
2187

2188
        one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
1✔
2189
            timeframe, candle_type, since_ms
2190
        )
2191
        logger.debug(
1✔
2192
            "one_call: %s msecs (%s)",
2193
            one_call,
2194
            dt_humanize_delta(dt_now() - timedelta(milliseconds=one_call)),
2195
        )
2196
        input_coroutines = [
1✔
2197
            self._async_get_candle_history(pair, timeframe, candle_type, since)
2198
            for since in range(since_ms, until_ms or dt_ts(), one_call)
2199
        ]
2200

2201
        data: List = []
1✔
2202
        # Chunk requests into batches of 100 to avoid overwhelming ccxt Throttling
2203
        for input_coro in chunks(input_coroutines, 100):
1✔
2204
            results = await asyncio.gather(*input_coro, return_exceptions=True)
1✔
2205
            for res in results:
1✔
2206
                if isinstance(res, BaseException):
1✔
2207
                    logger.warning(f"Async code raised an exception: {repr(res)}")
1✔
2208
                    if raise_:
1✔
2209
                        raise
1✔
2210
                    continue
1✔
2211
                else:
2212
                    # Deconstruct tuple if it's not an exception
2213
                    p, _, c, new_data, _ = res
1✔
2214
                    if p == pair and c == candle_type:
1✔
2215
                        data.extend(new_data)
1✔
2216
        # Sort data again after extending the result - above calls return in "async order"
2217
        data = sorted(data, key=lambda x: x[0])
1✔
2218
        return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
1✔
2219

2220
    def _build_coroutine(
1✔
2221
        self,
2222
        pair: str,
2223
        timeframe: str,
2224
        candle_type: CandleType,
2225
        since_ms: Optional[int],
2226
        cache: bool,
2227
    ) -> Coroutine[Any, Any, OHLCVResponse]:
2228
        not_all_data = cache and self.required_candle_call_count > 1
1✔
2229
        if cache and (pair, timeframe, candle_type) in self._klines:
1✔
2230
            candle_limit = self.ohlcv_candle_limit(timeframe, candle_type)
1✔
2231
            min_date = date_minus_candles(timeframe, candle_limit - 5).timestamp()
1✔
2232
            # Check if 1 call can get us updated candles without hole in the data.
2233
            if min_date < self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0):
1✔
2234
                # Cache can be used - do one-off call.
2235
                not_all_data = False
1✔
2236
            else:
2237
                # Time jump detected, evict cache
2238
                logger.info(
1✔
2239
                    f"Time jump detected. Evicting cache for {pair}, {timeframe}, {candle_type}"
2240
                )
2241
                del self._klines[(pair, timeframe, candle_type)]
1✔
2242

2243
        if not since_ms and (self._ft_has["ohlcv_require_since"] or not_all_data):
1✔
2244
            # Multiple calls for one pair - to get more history
2245
            one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
1✔
2246
                timeframe, candle_type, since_ms
2247
            )
2248
            move_to = one_call * self.required_candle_call_count
1✔
2249
            now = timeframe_to_next_date(timeframe)
1✔
2250
            since_ms = dt_ts(now - timedelta(seconds=move_to // 1000))
1✔
2251

2252
        if since_ms:
1✔
2253
            return self._async_get_historic_ohlcv(
1✔
2254
                pair, timeframe, since_ms=since_ms, raise_=True, candle_type=candle_type
2255
            )
2256
        else:
2257
            # One call ... "regular" refresh
2258
            return self._async_get_candle_history(
1✔
2259
                pair, timeframe, since_ms=since_ms, candle_type=candle_type
2260
            )
2261

2262
    def _build_ohlcv_dl_jobs(
1✔
2263
        self, pair_list: ListPairsWithTimeframes, since_ms: Optional[int], cache: bool
2264
    ) -> Tuple[List[Coroutine], List[Tuple[str, str, CandleType]]]:
2265
        """
2266
        Build Coroutines to execute as part of refresh_latest_ohlcv
2267
        """
2268
        input_coroutines: List[Coroutine[Any, Any, OHLCVResponse]] = []
1✔
2269
        cached_pairs = []
1✔
2270
        for pair, timeframe, candle_type in set(pair_list):
1✔
2271
            if timeframe not in self.timeframes and candle_type in (
1✔
2272
                CandleType.SPOT,
2273
                CandleType.FUTURES,
2274
            ):
2275
                logger.warning(
1✔
2276
                    f"Cannot download ({pair}, {timeframe}) combination as this timeframe is "
2277
                    f"not available on {self.name}. Available timeframes are "
2278
                    f"{', '.join(self.timeframes)}."
2279
                )
2280
                continue
1✔
2281

2282
            if (
1✔
2283
                (pair, timeframe, candle_type) not in self._klines
2284
                or not cache
2285
                or self._now_is_time_to_refresh(pair, timeframe, candle_type)
2286
            ):
2287
                input_coroutines.append(
1✔
2288
                    self._build_coroutine(pair, timeframe, candle_type, since_ms, cache)
2289
                )
2290

2291
            else:
2292
                logger.debug(
1✔
2293
                    f"Using cached candle (OHLCV) data for {pair}, {timeframe}, {candle_type} ..."
2294
                )
2295
                cached_pairs.append((pair, timeframe, candle_type))
1✔
2296

2297
        return input_coroutines, cached_pairs
1✔
2298

2299
    def _process_ohlcv_df(
1✔
2300
        self,
2301
        pair: str,
2302
        timeframe: str,
2303
        c_type: CandleType,
2304
        ticks: List[List],
2305
        cache: bool,
2306
        drop_incomplete: bool,
2307
    ) -> DataFrame:
2308
        # keeping last candle time as last refreshed time of the pair
2309
        if ticks and cache:
1✔
2310
            idx = -2 if drop_incomplete and len(ticks) > 1 else -1
1✔
2311
            self._pairs_last_refresh_time[(pair, timeframe, c_type)] = ticks[idx][0] // 1000
1✔
2312
        # keeping parsed dataframe in cache
2313
        ohlcv_df = ohlcv_to_dataframe(
1✔
2314
            ticks, timeframe, pair=pair, fill_missing=True, drop_incomplete=drop_incomplete
2315
        )
2316
        if cache:
1✔
2317
            if (pair, timeframe, c_type) in self._klines:
1✔
2318
                old = self._klines[(pair, timeframe, c_type)]
1✔
2319
                # Reassign so we return the updated, combined df
2320
                ohlcv_df = clean_ohlcv_dataframe(
1✔
2321
                    concat([old, ohlcv_df], axis=0),
2322
                    timeframe,
2323
                    pair,
2324
                    fill_missing=True,
2325
                    drop_incomplete=False,
2326
                )
2327
                candle_limit = self.ohlcv_candle_limit(timeframe, self._config["candle_type_def"])
1✔
2328
                # Age out old candles
2329
                ohlcv_df = ohlcv_df.tail(candle_limit + self._startup_candle_count)
1✔
2330
                ohlcv_df = ohlcv_df.reset_index(drop=True)
1✔
2331
                self._klines[(pair, timeframe, c_type)] = ohlcv_df
1✔
2332
            else:
2333
                self._klines[(pair, timeframe, c_type)] = ohlcv_df
1✔
2334
        return ohlcv_df
1✔
2335

2336
    def refresh_latest_ohlcv(
1✔
2337
        self,
2338
        pair_list: ListPairsWithTimeframes,
2339
        *,
2340
        since_ms: Optional[int] = None,
2341
        cache: bool = True,
2342
        drop_incomplete: Optional[bool] = None,
2343
    ) -> Dict[PairWithTimeframe, DataFrame]:
2344
        """
2345
        Refresh in-memory OHLCV asynchronously and set `_klines` with the result
2346
        Loops asynchronously over pair_list and downloads all pairs async (semi-parallel).
2347
        Only used in the dataprovider.refresh() method.
2348
        :param pair_list: List of 2 element tuples containing pair, interval to refresh
2349
        :param since_ms: time since when to download, in milliseconds
2350
        :param cache: Assign result to _klines. Useful for one-off downloads like for pairlists
2351
        :param drop_incomplete: Control candle dropping.
2352
            Specifying None defaults to _ohlcv_partial_candle
2353
        :return: Dict of [{(pair, timeframe): Dataframe}]
2354
        """
2355
        logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
1✔
2356

2357
        # Gather coroutines to run
2358
        input_coroutines, cached_pairs = self._build_ohlcv_dl_jobs(pair_list, since_ms, cache)
1✔
2359

2360
        results_df = {}
1✔
2361
        # Chunk requests into batches of 100 to avoid overwhelming ccxt Throttling
2362
        for input_coro in chunks(input_coroutines, 100):
1✔
2363

2364
            async def gather_stuff(coro):
1✔
2365
                return await asyncio.gather(*coro, return_exceptions=True)
1✔
2366

2367
            with self._loop_lock:
1✔
2368
                results = self.loop.run_until_complete(gather_stuff(input_coro))
1✔
2369

2370
            for res in results:
1✔
2371
                if isinstance(res, Exception):
1✔
2372
                    logger.warning(f"Async code raised an exception: {repr(res)}")
1✔
2373
                    continue
1✔
2374
                # Deconstruct tuple (has 5 elements)
2375
                pair, timeframe, c_type, ticks, drop_hint = res
1✔
2376
                drop_incomplete_ = drop_hint if drop_incomplete is None else drop_incomplete
1✔
2377
                ohlcv_df = self._process_ohlcv_df(
1✔
2378
                    pair, timeframe, c_type, ticks, cache, drop_incomplete_
2379
                )
2380

2381
                results_df[(pair, timeframe, c_type)] = ohlcv_df
1✔
2382

2383
        # Return cached klines
2384
        for pair, timeframe, c_type in cached_pairs:
1✔
2385
            results_df[(pair, timeframe, c_type)] = self.klines(
1✔
2386
                (pair, timeframe, c_type), copy=False
2387
            )
2388

2389
        return results_df
1✔
2390

2391
    def refresh_ohlcv_with_cache(
1✔
2392
        self, pairs: List[PairWithTimeframe], since_ms: int
2393
    ) -> Dict[PairWithTimeframe, DataFrame]:
2394
        """
2395
        Refresh ohlcv data for all pairs in needed_pairs if necessary.
2396
        Caches data with expiring per timeframe.
2397
        Should only be used for pairlists which need "on time" expirarion, and no longer cache.
2398
        """
2399

2400
        timeframes = {p[1] for p in pairs}
1✔
2401
        for timeframe in timeframes:
1✔
2402
            if (timeframe, since_ms) not in self._expiring_candle_cache:
1✔
2403
                timeframe_in_sec = timeframe_to_seconds(timeframe)
1✔
2404
                # Initialise cache
2405
                self._expiring_candle_cache[(timeframe, since_ms)] = PeriodicCache(
1✔
2406
                    ttl=timeframe_in_sec, maxsize=1000
2407
                )
2408

2409
        # Get candles from cache
2410
        candles = {
1✔
2411
            c: self._expiring_candle_cache[(c[1], since_ms)].get(c, None)
2412
            for c in pairs
2413
            if c in self._expiring_candle_cache[(c[1], since_ms)]
2414
        }
2415
        pairs_to_download = [p for p in pairs if p not in candles]
1✔
2416
        if pairs_to_download:
1✔
2417
            candles = self.refresh_latest_ohlcv(pairs_to_download, since_ms=since_ms, cache=False)
1✔
2418
            for c, val in candles.items():
1✔
2419
                self._expiring_candle_cache[(c[1], since_ms)][c] = val
1✔
2420
        return candles
1✔
2421

2422
    def _now_is_time_to_refresh(self, pair: str, timeframe: str, candle_type: CandleType) -> bool:
1✔
2423
        # Timeframe in seconds
2424
        interval_in_sec = timeframe_to_seconds(timeframe)
1✔
2425
        plr = self._pairs_last_refresh_time.get((pair, timeframe, candle_type), 0) + interval_in_sec
1✔
2426
        # current,active candle open date
2427
        now = int(timeframe_to_prev_date(timeframe).timestamp())
1✔
2428
        return plr < now
1✔
2429

2430
    @retrier_async
1✔
2431
    async def _async_get_candle_history(
1✔
2432
        self,
2433
        pair: str,
2434
        timeframe: str,
2435
        candle_type: CandleType,
2436
        since_ms: Optional[int] = None,
2437
    ) -> OHLCVResponse:
2438
        """
2439
        Asynchronously get candle history data using fetch_ohlcv
2440
        :param candle_type: '', mark, index, premiumIndex, or funding_rate
2441
        returns tuple: (pair, timeframe, ohlcv_list)
2442
        """
2443
        try:
1✔
2444
            # Fetch OHLCV asynchronously
2445
            s = "(" + dt_from_ts(since_ms).isoformat() + ") " if since_ms is not None else ""
1✔
2446
            logger.debug(
1✔
2447
                "Fetching pair %s, %s, interval %s, since %s %s...",
2448
                pair,
2449
                candle_type,
2450
                timeframe,
2451
                since_ms,
2452
                s,
2453
            )
2454
            params = deepcopy(self._ft_has.get("ohlcv_params", {}))
1✔
2455
            candle_limit = self.ohlcv_candle_limit(
1✔
2456
                timeframe, candle_type=candle_type, since_ms=since_ms
2457
            )
2458

2459
            if candle_type and candle_type != CandleType.SPOT:
1✔
2460
                params.update({"price": candle_type.value})
1✔
2461
            if candle_type != CandleType.FUNDING_RATE:
1✔
2462
                data = await self._api_async.fetch_ohlcv(
1✔
2463
                    pair, timeframe=timeframe, since=since_ms, limit=candle_limit, params=params
2464
                )
2465
            else:
2466
                # Funding rate
2467
                data = await self._fetch_funding_rate_history(
1✔
2468
                    pair=pair,
2469
                    timeframe=timeframe,
2470
                    limit=candle_limit,
2471
                    since_ms=since_ms,
2472
                )
2473
            # Some exchanges sort OHLCV in ASC order and others in DESC.
2474
            # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
2475
            # while GDAX returns the list of OHLCV in DESC order (newest first, oldest last)
2476
            # Only sort if necessary to save computing time
2477
            try:
1✔
2478
                if data and data[0][0] > data[-1][0]:
1✔
2479
                    data = sorted(data, key=lambda x: x[0])
1✔
2480
            except IndexError:
1✔
2481
                logger.exception("Error loading %s. Result was %s.", pair, data)
1✔
2482
                return pair, timeframe, candle_type, [], self._ohlcv_partial_candle
1✔
2483
            logger.debug("Done fetching pair %s, %s interval %s...", pair, candle_type, timeframe)
1✔
2484
            return pair, timeframe, candle_type, data, self._ohlcv_partial_candle
1✔
2485

2486
        except ccxt.NotSupported as e:
1✔
2487
            raise OperationalException(
1✔
2488
                f"Exchange {self._api.name} does not support fetching historical "
2489
                f"candle (OHLCV) data. Message: {e}"
2490
            ) from e
2491
        except ccxt.DDoSProtection as e:
1✔
2492
            raise DDosProtection(e) from e
1✔
2493
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2494
            raise TemporaryError(
1✔
2495
                f"Could not fetch historical candle (OHLCV) data "
2496
                f"for pair {pair} due to {e.__class__.__name__}. "
2497
                f"Message: {e}"
2498
            ) from e
2499
        except ccxt.BaseError as e:
1✔
2500
            raise OperationalException(
1✔
2501
                f"Could not fetch historical candle (OHLCV) data for pair {pair}. Message: {e}"
2502
            ) from e
2503

2504
    async def _fetch_funding_rate_history(
1✔
2505
        self,
2506
        pair: str,
2507
        timeframe: str,
2508
        limit: int,
2509
        since_ms: Optional[int] = None,
2510
    ) -> List[List]:
2511
        """
2512
        Fetch funding rate history - used to selectively override this by subclasses.
2513
        """
2514
        # Funding rate
2515
        data = await self._api_async.fetch_funding_rate_history(pair, since=since_ms, limit=limit)
1✔
2516
        # Convert funding rate to candle pattern
2517
        data = [[x["timestamp"], x["fundingRate"], 0, 0, 0, 0] for x in data]
1✔
2518
        return data
1✔
2519

2520
    # Fetch historic trades
2521

2522
    @retrier_async
1✔
2523
    async def _async_fetch_trades(
1✔
2524
        self, pair: str, since: Optional[int] = None, params: Optional[dict] = None
2525
    ) -> Tuple[List[List], Any]:
2526
        """
2527
        Asynchronously gets trade history using fetch_trades.
2528
        Handles exchange errors, does one call to the exchange.
2529
        :param pair: Pair to fetch trade data for
2530
        :param since: Since as integer timestamp in milliseconds
2531
        returns: List of dicts containing trades, the next iteration value (new "since" or trade_id)
2532
        """
2533
        try:
1✔
2534
            # fetch trades asynchronously
2535
            if params:
1✔
2536
                logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
1✔
2537
                trades = await self._api_async.fetch_trades(pair, params=params, limit=1000)
1✔
2538
            else:
2539
                logger.debug(
1✔
2540
                    "Fetching trades for pair %s, since %s %s...",
2541
                    pair,
2542
                    since,
2543
                    "(" + dt_from_ts(since).isoformat() + ") " if since is not None else "",
2544
                )
2545
                trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
1✔
2546
            trades = self._trades_contracts_to_amount(trades)
1✔
2547
            pagination_value = self._get_trade_pagination_next_value(trades)
1✔
2548
            return trades_dict_to_list(trades), pagination_value
1✔
2549
        except ccxt.NotSupported as e:
1✔
2550
            raise OperationalException(
1✔
2551
                f"Exchange {self._api.name} does not support fetching historical trade data."
2552
                f"Message: {e}"
2553
            ) from e
2554
        except ccxt.DDoSProtection as e:
1✔
2555
            raise DDosProtection(e) from e
1✔
2556
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2557
            raise TemporaryError(
1✔
2558
                f"Could not load trade history due to {e.__class__.__name__}. Message: {e}"
2559
            ) from e
2560
        except ccxt.BaseError as e:
1✔
2561
            raise OperationalException(f"Could not fetch trade data. Msg: {e}") from e
1✔
2562

2563
    def _valid_trade_pagination_id(self, pair: str, from_id: str) -> bool:
1✔
2564
        """
2565
        Verify trade-pagination id is valid.
2566
        Workaround for odd Kraken issue where ID is sometimes wrong.
2567
        """
2568
        return True
1✔
2569

2570
    def _get_trade_pagination_next_value(self, trades: List[Dict]):
1✔
2571
        """
2572
        Extract pagination id for the next "from_id" value
2573
        Applies only to fetch_trade_history by id.
2574
        """
2575
        if not trades:
1✔
2576
            return None
×
2577
        if self._trades_pagination == "id":
1✔
2578
            return trades[-1].get("id")
1✔
2579
        else:
2580
            return trades[-1].get("timestamp")
1✔
2581

2582
    async def _async_get_trade_history_id(
1✔
2583
        self, pair: str, until: int, since: Optional[int] = None, from_id: Optional[str] = None
2584
    ) -> Tuple[str, List[List]]:
2585
        """
2586
        Asynchronously gets trade history using fetch_trades
2587
        use this when exchange uses id-based iteration (check `self._trades_pagination`)
2588
        :param pair: Pair to fetch trade data for
2589
        :param since: Since as integer timestamp in milliseconds
2590
        :param until: Until as integer timestamp in milliseconds
2591
        :param from_id: Download data starting with ID (if id is known). Ignores "since" if set.
2592
        returns tuple: (pair, trades-list)
2593
        """
2594

2595
        trades: List[List] = []
1✔
2596
        # DEFAULT_TRADES_COLUMNS: 0 -> timestamp
2597
        # DEFAULT_TRADES_COLUMNS: 1 -> id
2598
        has_overlap = self._ft_has.get("trades_pagination_overlap", True)
1✔
2599
        # Skip last trade by default since its the key for the next call
2600
        x = slice(None, -1) if has_overlap else slice(None)
1✔
2601

2602
        if not from_id or not self._valid_trade_pagination_id(pair, from_id):
1✔
2603
            # Fetch first elements using timebased method to get an ID to paginate on
2604
            # Depending on the Exchange, this can introduce a drift at the start of the interval
2605
            # of up to an hour.
2606
            # e.g. Binance returns the "last 1000" candles within a 1h time interval
2607
            # - so we will miss the first trades.
2608
            t, from_id = await self._async_fetch_trades(pair, since=since)
1✔
2609
            trades.extend(t[x])
1✔
2610
        while True:
1✔
2611
            try:
1✔
2612
                t, from_id_next = await self._async_fetch_trades(
1✔
2613
                    pair, params={self._trades_pagination_arg: from_id}
2614
                )
2615
                if t:
1✔
2616
                    trades.extend(t[x])
1✔
2617
                    if from_id == from_id_next or t[-1][0] > until:
1✔
2618
                        logger.debug(
1✔
2619
                            f"Stopping because from_id did not change. "
2620
                            f"Reached {t[-1][0]} > {until}"
2621
                        )
2622
                        # Reached the end of the defined-download period - add last trade as well.
2623
                        if has_overlap:
1✔
2624
                            trades.extend(t[-1:])
1✔
2625
                        break
1✔
2626

2627
                    from_id = from_id_next
1✔
2628
                else:
2629
                    logger.debug("Stopping as no more trades were returned.")
×
2630
                    break
×
2631
            except asyncio.CancelledError:
×
2632
                logger.debug("Async operation Interrupted, breaking trades DL loop.")
×
2633
                break
×
2634

2635
        return (pair, trades)
1✔
2636

2637
    async def _async_get_trade_history_time(
1✔
2638
        self, pair: str, until: int, since: Optional[int] = None
2639
    ) -> Tuple[str, List[List]]:
2640
        """
2641
        Asynchronously gets trade history using fetch_trades,
2642
        when the exchange uses time-based iteration (check `self._trades_pagination`)
2643
        :param pair: Pair to fetch trade data for
2644
        :param since: Since as integer timestamp in milliseconds
2645
        :param until: Until as integer timestamp in milliseconds
2646
        returns tuple: (pair, trades-list)
2647
        """
2648

2649
        trades: List[List] = []
1✔
2650
        # DEFAULT_TRADES_COLUMNS: 0 -> timestamp
2651
        # DEFAULT_TRADES_COLUMNS: 1 -> id
2652
        while True:
1✔
2653
            try:
1✔
2654
                t, since_next = await self._async_fetch_trades(pair, since=since)
1✔
2655
                if t:
1✔
2656
                    # No more trades to download available at the exchange,
2657
                    # So we repeatedly get the same trade over and over again.
2658
                    if since == since_next and len(t) == 1:
1✔
2659
                        logger.debug("Stopping because no more trades are available.")
×
2660
                        break
×
2661
                    since = since_next
1✔
2662
                    trades.extend(t)
1✔
2663
                    # Reached the end of the defined-download period
2664
                    if until and since_next > until:
1✔
2665
                        logger.debug(f"Stopping because until was reached. {since_next} > {until}")
1✔
2666
                        break
1✔
2667
                else:
2668
                    logger.debug("Stopping as no more trades were returned.")
1✔
2669
                    break
1✔
2670
            except asyncio.CancelledError:
×
2671
                logger.debug("Async operation Interrupted, breaking trades DL loop.")
×
2672
                break
×
2673

2674
        return (pair, trades)
1✔
2675

2676
    async def _async_get_trade_history(
1✔
2677
        self,
2678
        pair: str,
2679
        since: Optional[int] = None,
2680
        until: Optional[int] = None,
2681
        from_id: Optional[str] = None,
2682
    ) -> Tuple[str, List[List]]:
2683
        """
2684
        Async wrapper handling downloading trades using either time or id based methods.
2685
        """
2686

2687
        logger.debug(
1✔
2688
            f"_async_get_trade_history(), pair: {pair}, "
2689
            f"since: {since}, until: {until}, from_id: {from_id}"
2690
        )
2691

2692
        if until is None:
1✔
2693
            until = ccxt.Exchange.milliseconds()
×
2694
            logger.debug(f"Exchange milliseconds: {until}")
×
2695

2696
        if self._trades_pagination == "time":
1✔
2697
            return await self._async_get_trade_history_time(pair=pair, since=since, until=until)
1✔
2698
        elif self._trades_pagination == "id":
1✔
2699
            return await self._async_get_trade_history_id(
1✔
2700
                pair=pair, since=since, until=until, from_id=from_id
2701
            )
2702
        else:
2703
            raise OperationalException(
×
2704
                f"Exchange {self.name} does use neither time, nor id based pagination"
2705
            )
2706

2707
    def get_historic_trades(
1✔
2708
        self,
2709
        pair: str,
2710
        since: Optional[int] = None,
2711
        until: Optional[int] = None,
2712
        from_id: Optional[str] = None,
2713
    ) -> Tuple[str, List]:
2714
        """
2715
        Get trade history data using asyncio.
2716
        Handles all async work and returns the list of candles.
2717
        Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call.
2718
        :param pair: Pair to download
2719
        :param since: Timestamp in milliseconds to get history from
2720
        :param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.
2721
        :param from_id: Download data starting with ID (if id is known)
2722
        :returns List of trade data
2723
        """
2724
        if not self.exchange_has("fetchTrades"):
1✔
2725
            raise OperationalException("This exchange does not support downloading Trades.")
1✔
2726

2727
        with self._loop_lock:
1✔
2728
            task = asyncio.ensure_future(
1✔
2729
                self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)
2730
            )
2731

2732
            for sig in [signal.SIGINT, signal.SIGTERM]:
1✔
2733
                try:
1✔
2734
                    self.loop.add_signal_handler(sig, task.cancel)
1✔
2735
                except NotImplementedError:
×
2736
                    # Not all platforms implement signals (e.g. windows)
2737
                    pass
×
2738
            return self.loop.run_until_complete(task)
1✔
2739

2740
    @retrier
1✔
2741
    def _get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float:
1✔
2742
        """
2743
        Returns the sum of all funding fees that were exchanged for a pair within a timeframe
2744
        Dry-run handling happens as part of _calculate_funding_fees.
2745
        :param pair: (e.g. ADA/USDT)
2746
        :param since: The earliest time of consideration for calculating funding fees,
2747
            in unix time or as a datetime
2748
        """
2749
        if not self.exchange_has("fetchFundingHistory"):
1✔
2750
            raise OperationalException(
×
2751
                f"fetch_funding_history() is not available using {self.name}"
2752
            )
2753

2754
        if type(since) is datetime:
1✔
2755
            since = dt_ts(since)
1✔
2756

2757
        try:
1✔
2758
            funding_history = self._api.fetch_funding_history(symbol=pair, since=since)
1✔
2759
            self._log_exchange_response(
1✔
2760
                "funding_history", funding_history, add_info=f"pair: {pair}, since: {since}"
2761
            )
2762
            return sum(fee["amount"] for fee in funding_history)
1✔
2763
        except ccxt.DDoSProtection as e:
1✔
2764
            raise DDosProtection(e) from e
1✔
2765
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2766
            raise TemporaryError(
1✔
2767
                f"Could not get funding fees due to {e.__class__.__name__}. Message: {e}"
2768
            ) from e
2769
        except ccxt.BaseError as e:
1✔
2770
            raise OperationalException(e) from e
1✔
2771

2772
    @retrier
1✔
2773
    def get_leverage_tiers(self) -> Dict[str, List[Dict]]:
1✔
2774
        try:
1✔
2775
            return self._api.fetch_leverage_tiers()
1✔
2776
        except ccxt.DDoSProtection as e:
1✔
2777
            raise DDosProtection(e) from e
1✔
2778
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2779
            raise TemporaryError(
1✔
2780
                f"Could not load leverage tiers due to {e.__class__.__name__}. Message: {e}"
2781
            ) from e
2782
        except ccxt.BaseError as e:
1✔
2783
            raise OperationalException(e) from e
1✔
2784

2785
    @retrier_async
1✔
2786
    async def get_market_leverage_tiers(self, symbol: str) -> Tuple[str, List[Dict]]:
1✔
2787
        """Leverage tiers per symbol"""
2788
        try:
1✔
2789
            tier = await self._api_async.fetch_market_leverage_tiers(symbol)
1✔
2790
            return symbol, tier
1✔
2791
        except ccxt.DDoSProtection as e:
1✔
2792
            raise DDosProtection(e) from e
1✔
2793
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
2794
            raise TemporaryError(
1✔
2795
                f"Could not load leverage tiers for {symbol}"
2796
                f" due to {e.__class__.__name__}. Message: {e}"
2797
            ) from e
2798
        except ccxt.BaseError as e:
1✔
2799
            raise OperationalException(e) from e
1✔
2800

2801
    def load_leverage_tiers(self) -> Dict[str, List[Dict]]:
1✔
2802
        if self.trading_mode == TradingMode.FUTURES:
1✔
2803
            if self.exchange_has("fetchLeverageTiers"):
1✔
2804
                # Fetch all leverage tiers at once
2805
                return self.get_leverage_tiers()
1✔
2806
            elif self.exchange_has("fetchMarketLeverageTiers"):
1✔
2807
                # Must fetch the leverage tiers for each market separately
2808
                # * This is slow(~45s) on Okx, makes ~90 api calls to load all linear swap markets
2809
                markets = self.markets
1✔
2810

2811
                symbols = [
1✔
2812
                    symbol
2813
                    for symbol, market in markets.items()
2814
                    if (
2815
                        self.market_is_future(market)
2816
                        and market["quote"] == self._config["stake_currency"]
2817
                    )
2818
                ]
2819

2820
                tiers: Dict[str, List[Dict]] = {}
1✔
2821

2822
                tiers_cached = self.load_cached_leverage_tiers(self._config["stake_currency"])
1✔
2823
                if tiers_cached:
1✔
2824
                    tiers = tiers_cached
1✔
2825

2826
                coros = [
1✔
2827
                    self.get_market_leverage_tiers(symbol)
2828
                    for symbol in sorted(symbols)
2829
                    if symbol not in tiers
2830
                ]
2831

2832
                # Be verbose here, as this delays startup by ~1 minute.
2833
                if coros:
1✔
2834
                    logger.info(
1✔
2835
                        f"Initializing leverage_tiers for {len(symbols)} markets. "
2836
                        "This will take about a minute."
2837
                    )
2838
                else:
2839
                    logger.info("Using cached leverage_tiers.")
1✔
2840

2841
                async def gather_results(input_coro):
1✔
2842
                    return await asyncio.gather(*input_coro, return_exceptions=True)
1✔
2843

2844
                for input_coro in chunks(coros, 100):
1✔
2845
                    with self._loop_lock:
1✔
2846
                        results = self.loop.run_until_complete(gather_results(input_coro))
1✔
2847

2848
                    for res in results:
1✔
2849
                        if isinstance(res, Exception):
1✔
2850
                            logger.warning(f"Leverage tier exception: {repr(res)}")
1✔
2851
                            continue
1✔
2852
                        symbol, tier = res
1✔
2853
                        tiers[symbol] = tier
1✔
2854
                if len(coros) > 0:
1✔
2855
                    self.cache_leverage_tiers(tiers, self._config["stake_currency"])
1✔
2856
                logger.info(f"Done initializing {len(symbols)} markets.")
1✔
2857

2858
                return tiers
1✔
2859
        return {}
1✔
2860

2861
    def cache_leverage_tiers(self, tiers: Dict[str, List[Dict]], stake_currency: str) -> None:
1✔
2862
        filename = self._config["datadir"] / "futures" / f"leverage_tiers_{stake_currency}.json"
1✔
2863
        if not filename.parent.is_dir():
1✔
2864
            filename.parent.mkdir(parents=True)
1✔
2865
        data = {
1✔
2866
            "updated": datetime.now(timezone.utc),
2867
            "data": tiers,
2868
        }
2869
        file_dump_json(filename, data)
1✔
2870

2871
    def load_cached_leverage_tiers(
1✔
2872
        self, stake_currency: str, cache_time: Optional[timedelta] = None
2873
    ) -> Optional[Dict[str, List[Dict]]]:
2874
        """
2875
        Load cached leverage tiers from disk
2876
        :param cache_time: The maximum age of the cache before it is considered outdated
2877
        """
2878
        if not cache_time:
1✔
2879
            # Default to 4 weeks
2880
            cache_time = timedelta(weeks=4)
1✔
2881
        filename = self._config["datadir"] / "futures" / f"leverage_tiers_{stake_currency}.json"
1✔
2882
        if filename.is_file():
1✔
2883
            try:
1✔
2884
                tiers = file_load_json(filename)
1✔
2885
                updated = tiers.get("updated")
1✔
2886
                if updated:
1✔
2887
                    updated_dt = parser.parse(updated)
1✔
2888
                    if updated_dt < datetime.now(timezone.utc) - cache_time:
1✔
2889
                        logger.info("Cached leverage tiers are outdated. Will update.")
1✔
2890
                        return None
1✔
2891
                return tiers["data"]
1✔
2892
            except Exception:
×
2893
                logger.exception("Error loading cached leverage tiers. Refreshing.")
×
2894
        return None
1✔
2895

2896
    def fill_leverage_tiers(self) -> None:
1✔
2897
        """
2898
        Assigns property _leverage_tiers to a dictionary of information about the leverage
2899
        allowed on each pair
2900
        """
2901
        leverage_tiers = self.load_leverage_tiers()
1✔
2902
        for pair, tiers in leverage_tiers.items():
1✔
2903
            pair_tiers = []
1✔
2904
            for tier in tiers:
1✔
2905
                pair_tiers.append(self.parse_leverage_tier(tier))
1✔
2906
            self._leverage_tiers[pair] = pair_tiers
1✔
2907

2908
    def parse_leverage_tier(self, tier) -> Dict:
1✔
2909
        info = tier.get("info", {})
1✔
2910
        return {
1✔
2911
            "minNotional": tier["minNotional"],
2912
            "maxNotional": tier["maxNotional"],
2913
            "maintenanceMarginRate": tier["maintenanceMarginRate"],
2914
            "maxLeverage": tier["maxLeverage"],
2915
            "maintAmt": float(info["cum"]) if "cum" in info else None,
2916
        }
2917

2918
    def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float:
1✔
2919
        """
2920
        Returns the maximum leverage that a pair can be traded at
2921
        :param pair: The base/quote currency pair being traded
2922
        :stake_amount: The total value of the traders margin_mode in quote currency
2923
        """
2924

2925
        if self.trading_mode == TradingMode.SPOT:
1✔
2926
            return 1.0
1✔
2927

2928
        if self.trading_mode == TradingMode.FUTURES:
1✔
2929
            # Checks and edge cases
2930
            if stake_amount is None:
1✔
2931
                raise OperationalException(
×
2932
                    f"{self.name}.get_max_leverage requires argument stake_amount"
2933
                )
2934

2935
            if pair not in self._leverage_tiers:
1✔
2936
                # Maybe raise exception because it can't be traded on futures?
2937
                return 1.0
1✔
2938

2939
            pair_tiers = self._leverage_tiers[pair]
1✔
2940

2941
            if stake_amount == 0:
1✔
2942
                return self._leverage_tiers[pair][0]["maxLeverage"]  # Max lev for lowest amount
1✔
2943

2944
            for tier_index in range(len(pair_tiers)):
1✔
2945
                tier = pair_tiers[tier_index]
1✔
2946
                lev = tier["maxLeverage"]
1✔
2947

2948
                if tier_index < len(pair_tiers) - 1:
1✔
2949
                    next_tier = pair_tiers[tier_index + 1]
1✔
2950
                    next_floor = next_tier["minNotional"] / next_tier["maxLeverage"]
1✔
2951
                    if next_floor > stake_amount:  # Next tier min too high for stake amount
1✔
2952
                        return min((tier["maxNotional"] / stake_amount), lev)
1✔
2953
                        #
2954
                        # With the two leverage tiers below,
2955
                        # - a stake amount of 150 would mean a max leverage of (10000 / 150) = 66.66
2956
                        # - stakes below 133.33 = max_lev of 75
2957
                        # - stakes between 133.33-200 = max_lev of 10000/stake = 50.01-74.99
2958
                        # - stakes from 200 + 1000 = max_lev of 50
2959
                        #
2960
                        # {
2961
                        #     "min": 0,      # stake = 0.0
2962
                        #     "max": 10000,  # max_stake@75 = 10000/75 = 133.33333333333334
2963
                        #     "lev": 75,
2964
                        # },
2965
                        # {
2966
                        #     "min": 10000,  # stake = 200.0
2967
                        #     "max": 50000,  # max_stake@50 = 50000/50 = 1000.0
2968
                        #     "lev": 50,
2969
                        # }
2970
                        #
2971

2972
                else:  # if on the last tier
2973
                    if stake_amount > tier["maxNotional"]:
1✔
2974
                        # If stake is > than max tradeable amount
2975
                        raise InvalidOrderException(f"Amount {stake_amount} too high for {pair}")
1✔
2976
                    else:
2977
                        return tier["maxLeverage"]
1✔
2978

2979
            raise OperationalException(
×
2980
                "Looped through all tiers without finding a max leverage. Should never be reached"
2981
            )
2982

2983
        elif self.trading_mode == TradingMode.MARGIN:  # Search markets.limits for max lev
1✔
2984
            market = self.markets[pair]
1✔
2985
            if market["limits"]["leverage"]["max"] is not None:
1✔
2986
                return market["limits"]["leverage"]["max"]
1✔
2987
            else:
2988
                return 1.0  # Default if max leverage cannot be found
1✔
2989
        else:
2990
            return 1.0
×
2991

2992
    @retrier
1✔
2993
    def _set_leverage(
1✔
2994
        self,
2995
        leverage: float,
2996
        pair: Optional[str] = None,
2997
        accept_fail: bool = False,
2998
    ):
2999
        """
3000
        Set's the leverage before making a trade, in order to not
3001
        have the same leverage on every trade
3002
        """
3003
        if self._config["dry_run"] or not self.exchange_has("setLeverage"):
1✔
3004
            # Some exchanges only support one margin_mode type
3005
            return
1✔
3006
        if self._ft_has.get("floor_leverage", False) is True:
1✔
3007
            # Rounding for binance ...
3008
            leverage = floor(leverage)
1✔
3009
        try:
1✔
3010
            res = self._api.set_leverage(symbol=pair, leverage=leverage)
1✔
3011
            self._log_exchange_response("set_leverage", res)
1✔
3012
        except ccxt.DDoSProtection as e:
1✔
3013
            raise DDosProtection(e) from e
1✔
3014
        except (ccxt.BadRequest, ccxt.OperationRejected, ccxt.InsufficientFunds) as e:
1✔
3015
            if not accept_fail:
×
3016
                raise TemporaryError(
×
3017
                    f"Could not set leverage due to {e.__class__.__name__}. Message: {e}"
3018
                ) from e
3019
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
3020
            raise TemporaryError(
1✔
3021
                f"Could not set leverage due to {e.__class__.__name__}. Message: {e}"
3022
            ) from e
3023
        except ccxt.BaseError as e:
1✔
3024
            raise OperationalException(e) from e
1✔
3025

3026
    def get_interest_rate(self) -> float:
1✔
3027
        """
3028
        Retrieve interest rate - necessary for Margin trading.
3029
        Should not call the exchange directly when used from backtesting.
3030
        """
3031
        return 0.0
×
3032

3033
    def funding_fee_cutoff(self, open_date: datetime) -> bool:
1✔
3034
        """
3035
        Funding fees are only charged at full hours (usually every 4-8h).
3036
        Therefore a trade opening at 10:00:01 will not be charged a funding fee until the next hour.
3037
        :param open_date: The open date for a trade
3038
        :return: True if the date falls on a full hour, False otherwise
3039
        """
3040
        return open_date.minute == 0 and open_date.second == 0
1✔
3041

3042
    @retrier
1✔
3043
    def set_margin_mode(
1✔
3044
        self,
3045
        pair: str,
3046
        margin_mode: MarginMode,
3047
        accept_fail: bool = False,
3048
        params: Optional[Dict] = None,
3049
    ):
3050
        """
3051
        Set's the margin mode on the exchange to cross or isolated for a specific pair
3052
        :param pair: base/quote currency pair (e.g. "ADA/USDT")
3053
        """
3054
        if self._config["dry_run"] or not self.exchange_has("setMarginMode"):
1✔
3055
            # Some exchanges only support one margin_mode type
3056
            return
1✔
3057

3058
        if params is None:
1✔
3059
            params = {}
1✔
3060
        try:
1✔
3061
            res = self._api.set_margin_mode(margin_mode.value, pair, params)
1✔
3062
            self._log_exchange_response("set_margin_mode", res)
×
3063
        except ccxt.DDoSProtection as e:
1✔
3064
            raise DDosProtection(e) from e
1✔
3065
        except (ccxt.BadRequest, ccxt.OperationRejected) as e:
1✔
3066
            if not accept_fail:
×
3067
                raise TemporaryError(
×
3068
                    f"Could not set margin mode due to {e.__class__.__name__}. Message: {e}"
3069
                ) from e
3070
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
1✔
3071
            raise TemporaryError(
1✔
3072
                f"Could not set margin mode due to {e.__class__.__name__}. Message: {e}"
3073
            ) from e
3074
        except ccxt.BaseError as e:
1✔
3075
            raise OperationalException(e) from e
1✔
3076

3077
    def _fetch_and_calculate_funding_fees(
1✔
3078
        self,
3079
        pair: str,
3080
        amount: float,
3081
        is_short: bool,
3082
        open_date: datetime,
3083
        close_date: Optional[datetime] = None,
3084
    ) -> float:
3085
        """
3086
        Fetches and calculates the sum of all funding fees that occurred for a pair
3087
        during a futures trade.
3088
        Only used during dry-run or if the exchange does not provide a funding_rates endpoint.
3089
        :param pair: The quote/base pair of the trade
3090
        :param amount: The quantity of the trade
3091
        :param is_short: trade direction
3092
        :param open_date: The date and time that the trade started
3093
        :param close_date: The date and time that the trade ended
3094
        """
3095

3096
        if self.funding_fee_cutoff(open_date):
1✔
3097
            # Shift back to 1h candle to avoid missing funding fees
3098
            # Only really relevant for trades very close to the full hour
3099
            open_date = timeframe_to_prev_date("1h", open_date)
1✔
3100
        timeframe = self._ft_has["mark_ohlcv_timeframe"]
1✔
3101
        timeframe_ff = self._ft_has["funding_fee_timeframe"]
1✔
3102
        mark_price_type = CandleType.from_string(self._ft_has["mark_ohlcv_price"])
1✔
3103

3104
        if not close_date:
1✔
3105
            close_date = datetime.now(timezone.utc)
1✔
3106
        since_ms = dt_ts(timeframe_to_prev_date(timeframe, open_date))
1✔
3107

3108
        mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type)
1✔
3109
        funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE)
1✔
3110

3111
        candle_histories = self.refresh_latest_ohlcv(
1✔
3112
            [mark_comb, funding_comb],
3113
            since_ms=since_ms,
3114
            cache=False,
3115
            drop_incomplete=False,
3116
        )
3117
        try:
1✔
3118
            # we can't assume we always get histories - for example during exchange downtimes
3119
            funding_rates = candle_histories[funding_comb]
1✔
3120
            mark_rates = candle_histories[mark_comb]
1✔
3121
        except KeyError:
1✔
3122
            raise ExchangeError("Could not find funding rates.") from None
1✔
3123

3124
        funding_mark_rates = self.combine_funding_and_mark(funding_rates, mark_rates)
1✔
3125

3126
        return self.calculate_funding_fees(
1✔
3127
            funding_mark_rates,
3128
            amount=amount,
3129
            is_short=is_short,
3130
            open_date=open_date,
3131
            close_date=close_date,
3132
        )
3133

3134
    @staticmethod
1✔
3135
    def combine_funding_and_mark(
1✔
3136
        funding_rates: DataFrame, mark_rates: DataFrame, futures_funding_rate: Optional[int] = None
3137
    ) -> DataFrame:
3138
        """
3139
        Combine funding-rates and mark-rates dataframes
3140
        :param funding_rates: Dataframe containing Funding rates (Type FUNDING_RATE)
3141
        :param mark_rates: Dataframe containing Mark rates (Type mark_ohlcv_price)
3142
        :param futures_funding_rate: Fake funding rate to use if funding_rates are not available
3143
        """
3144
        if futures_funding_rate is None:
1✔
3145
            return mark_rates.merge(
1✔
3146
                funding_rates, on="date", how="inner", suffixes=["_mark", "_fund"]
3147
            )
3148
        else:
3149
            if len(funding_rates) == 0:
1✔
3150
                # No funding rate candles - full fillup with fallback variable
3151
                mark_rates["open_fund"] = futures_funding_rate
1✔
3152
                return mark_rates.rename(
1✔
3153
                    columns={
3154
                        "open": "open_mark",
3155
                        "close": "close_mark",
3156
                        "high": "high_mark",
3157
                        "low": "low_mark",
3158
                        "volume": "volume_mark",
3159
                    }
3160
                )
3161

3162
            else:
3163
                # Fill up missing funding_rate candles with fallback value
3164
                combined = mark_rates.merge(
1✔
3165
                    funding_rates, on="date", how="left", suffixes=["_mark", "_fund"]
3166
                )
3167
                combined["open_fund"] = combined["open_fund"].fillna(futures_funding_rate)
1✔
3168
                return combined
1✔
3169

3170
    def calculate_funding_fees(
1✔
3171
        self,
3172
        df: DataFrame,
3173
        amount: float,
3174
        is_short: bool,
3175
        open_date: datetime,
3176
        close_date: datetime,
3177
        time_in_ratio: Optional[float] = None,
3178
    ) -> float:
3179
        """
3180
        calculates the sum of all funding fees that occurred for a pair during a futures trade
3181
        :param df: Dataframe containing combined funding and mark rates
3182
                   as `open_fund` and `open_mark`.
3183
        :param amount: The quantity of the trade
3184
        :param is_short: trade direction
3185
        :param open_date: The date and time that the trade started
3186
        :param close_date: The date and time that the trade ended
3187
        :param time_in_ratio: Not used by most exchange classes
3188
        """
3189
        fees: float = 0
1✔
3190

3191
        if not df.empty:
1✔
3192
            df1 = df[(df["date"] >= open_date) & (df["date"] <= close_date)]
1✔
3193
            fees = sum(df1["open_fund"] * df1["open_mark"] * amount)
1✔
3194
        if isnan(fees):
1✔
3195
            fees = 0.0
1✔
3196
        # Negate fees for longs as funding_fees expects it this way based on live endpoints.
3197
        return fees if is_short else -fees
1✔
3198

3199
    def get_funding_fees(
1✔
3200
        self, pair: str, amount: float, is_short: bool, open_date: datetime
3201
    ) -> float:
3202
        """
3203
        Fetch funding fees, either from the exchange (live) or calculates them
3204
        based on funding rate/mark price history
3205
        :param pair: The quote/base pair of the trade
3206
        :param is_short: trade direction
3207
        :param amount: Trade amount
3208
        :param open_date: Open date of the trade
3209
        :return: funding fee since open_date
3210
        """
3211
        if self.trading_mode == TradingMode.FUTURES:
1✔
3212
            try:
1✔
3213
                if self._config["dry_run"]:
1✔
3214
                    funding_fees = self._fetch_and_calculate_funding_fees(
1✔
3215
                        pair, amount, is_short, open_date
3216
                    )
3217
                else:
3218
                    funding_fees = self._get_funding_fees_from_exchange(pair, open_date)
×
3219
                return funding_fees
1✔
3220
            except ExchangeError:
1✔
3221
                logger.warning(f"Could not update funding fees for {pair}.")
1✔
3222

3223
        return 0.0
1✔
3224

3225
    def get_liquidation_price(
1✔
3226
        self,
3227
        pair: str,
3228
        # Dry-run
3229
        open_rate: float,  # Entry price of position
3230
        is_short: bool,
3231
        amount: float,  # Absolute value of position size
3232
        stake_amount: float,
3233
        leverage: float,
3234
        wallet_balance: float,
3235
        mm_ex_1: float = 0.0,  # (Binance) Cross only
3236
        upnl_ex_1: float = 0.0,  # (Binance) Cross only
3237
    ) -> Optional[float]:
3238
        """
3239
        Set's the margin mode on the exchange to cross or isolated for a specific pair
3240
        """
3241
        if self.trading_mode == TradingMode.SPOT:
1✔
3242
            return None
1✔
3243
        elif self.trading_mode != TradingMode.FUTURES:
1✔
3244
            raise OperationalException(
1✔
3245
                f"{self.name} does not support {self.margin_mode} {self.trading_mode}"
3246
            )
3247

3248
        liquidation_price = None
1✔
3249
        if self._config["dry_run"] or not self.exchange_has("fetchPositions"):
1✔
3250
            liquidation_price = self.dry_run_liquidation_price(
1✔
3251
                pair=pair,
3252
                open_rate=open_rate,
3253
                is_short=is_short,
3254
                amount=amount,
3255
                leverage=leverage,
3256
                stake_amount=stake_amount,
3257
                wallet_balance=wallet_balance,
3258
                mm_ex_1=mm_ex_1,
3259
                upnl_ex_1=upnl_ex_1,
3260
            )
3261
        else:
3262
            positions = self.fetch_positions(pair)
1✔
3263
            if len(positions) > 0:
1✔
3264
                pos = positions[0]
1✔
3265
                liquidation_price = pos["liquidationPrice"]
1✔
3266

3267
        if liquidation_price is not None:
1✔
3268
            buffer_amount = abs(open_rate - liquidation_price) * self.liquidation_buffer
1✔
3269
            liquidation_price_buffer = (
1✔
3270
                liquidation_price - buffer_amount if is_short else liquidation_price + buffer_amount
3271
            )
3272
            return max(liquidation_price_buffer, 0.0)
1✔
3273
        else:
3274
            return None
1✔
3275

3276
    def dry_run_liquidation_price(
1✔
3277
        self,
3278
        pair: str,
3279
        open_rate: float,  # Entry price of position
3280
        is_short: bool,
3281
        amount: float,
3282
        stake_amount: float,
3283
        leverage: float,
3284
        wallet_balance: float,  # Or margin balance
3285
        mm_ex_1: float = 0.0,  # (Binance) Cross only
3286
        upnl_ex_1: float = 0.0,  # (Binance) Cross only
3287
    ) -> Optional[float]:
3288
        """
3289
        Important: Must be fetching data from cached values as this is used by backtesting!
3290
        PERPETUAL:
3291
         gate: https://www.gate.io/help/futures/futures/27724/liquidation-price-bankruptcy-price
3292
         > Liquidation Price = (Entry Price ± Margin / Contract Multiplier / Size) /
3293
                                [ 1 ± (Maintenance Margin Ratio + Taker Rate)]
3294
            Wherein, "+" or "-" depends on whether the contract goes long or short:
3295
            "-" for long, and "+" for short.
3296

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

3300
        :param pair: Pair to calculate liquidation price for
3301
        :param open_rate: Entry price of position
3302
        :param is_short: True if the trade is a short, false otherwise
3303
        :param amount: Absolute value of position size incl. leverage (in base currency)
3304
        :param stake_amount: Stake amount - Collateral in settle currency.
3305
        :param leverage: Leverage used for this position.
3306
        :param trading_mode: SPOT, MARGIN, FUTURES, etc.
3307
        :param margin_mode: Either ISOLATED or CROSS
3308
        :param wallet_balance: Amount of margin_mode in the wallet being used to trade
3309
            Cross-Margin Mode: crossWalletBalance
3310
            Isolated-Margin Mode: isolatedWalletBalance
3311

3312
        # * Not required by Gate or OKX
3313
        :param mm_ex_1:
3314
        :param upnl_ex_1:
3315
        """
3316

3317
        market = self.markets[pair]
1✔
3318
        taker_fee_rate = market["taker"]
1✔
3319
        mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount)
1✔
3320

3321
        if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
1✔
3322
            if market["inverse"]:
1✔
3323
                raise OperationalException("Freqtrade does not yet support inverse contracts")
×
3324

3325
            value = wallet_balance / amount
1✔
3326

3327
            mm_ratio_taker = mm_ratio + taker_fee_rate
1✔
3328
            if is_short:
1✔
3329
                return (open_rate + value) / (1 + mm_ratio_taker)
1✔
3330
            else:
3331
                return (open_rate - value) / (1 - mm_ratio_taker)
1✔
3332
        else:
3333
            raise OperationalException(
×
3334
                "Freqtrade only supports isolated futures for leverage trading"
3335
            )
3336

3337
    def get_maintenance_ratio_and_amt(
1✔
3338
        self,
3339
        pair: str,
3340
        nominal_value: float,
3341
    ) -> Tuple[float, Optional[float]]:
3342
        """
3343
        Important: Must be fetching data from cached values as this is used by backtesting!
3344
        :param pair: Market symbol
3345
        :param nominal_value: The total trade amount in quote currency including leverage
3346
        maintenance amount only on Binance
3347
        :return: (maintenance margin ratio, maintenance amount)
3348
        """
3349

3350
        if (
1✔
3351
            self._config.get("runmode") in OPTIMIZE_MODES
3352
            or self.exchange_has("fetchLeverageTiers")
3353
            or self.exchange_has("fetchMarketLeverageTiers")
3354
        ):
3355
            if pair not in self._leverage_tiers:
1✔
3356
                raise InvalidOrderException(
1✔
3357
                    f"Maintenance margin rate for {pair} is unavailable for {self.name}"
3358
                )
3359

3360
            pair_tiers = self._leverage_tiers[pair]
1✔
3361

3362
            for tier in reversed(pair_tiers):
1✔
3363
                if nominal_value >= tier["minNotional"]:
1✔
3364
                    return (tier["maintenanceMarginRate"], tier["maintAmt"])
1✔
3365

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

© 2025 Coveralls, Inc