diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 33d1e8c4b..6510e8d4f 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -2,6 +2,7 @@ # isort: off from freqtrade.exchange.common import remove_exchange_credentials, MAP_EXCHANGE_CHILDCLASS from freqtrade.exchange.exchange import Exchange + # isort: on from freqtrade.exchange.binance import Binance from freqtrade.exchange.bingx import Bingx diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 8cfe52d51..18fb23ebd 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,4 +1,5 @@ -""" Binance exchange subclass """ +"""Binance exchange subclass""" + import logging from datetime import datetime, timezone from pathlib import Path @@ -18,7 +19,6 @@ logger = logging.getLogger(__name__) class Binance(Exchange): - _ft_has: Dict = { "stoploss_on_exchange": True, "stop_price_param": "stopPrice", @@ -36,7 +36,7 @@ class Binance(Exchange): "tickers_have_price": False, "floor_leverage": True, "stop_price_type_field": "workingType", - "order_props_in_contracts": ['amount', 'cost', 'filled', 'remaining'], + "order_props_in_contracts": ["amount", "cost", "filled", "remaining"], "stop_price_type_value_mapping": { PriceType.LAST: "CONTRACT_PRICE", PriceType.MARK: "MARK_PRICE", @@ -67,36 +67,44 @@ class Binance(Exchange): Must be overridden in child methods if required. """ try: - if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']: + if self.trading_mode == TradingMode.FUTURES and not self._config["dry_run"]: position_side = self._api.fapiPrivateGetPositionSideDual() - self._log_exchange_response('position_side_setting', position_side) + self._log_exchange_response("position_side_setting", position_side) assets_margin = self._api.fapiPrivateGetMultiAssetsMargin() - self._log_exchange_response('multi_asset_margin', assets_margin) + self._log_exchange_response("multi_asset_margin", assets_margin) msg = "" - if position_side.get('dualSidePosition') is True: + if position_side.get("dualSidePosition") is True: msg += ( "\nHedge Mode is not supported by freqtrade. " - "Please change 'Position Mode' on your binance futures account.") - if assets_margin.get('multiAssetsMargin') is True: - msg += ("\nMulti-Asset Mode is not supported by freqtrade. " - "Please change 'Asset Mode' on your binance futures account.") + "Please change 'Position Mode' on your binance futures account." + ) + if assets_margin.get("multiAssetsMargin") is True: + msg += ( + "\nMulti-Asset Mode is not supported by freqtrade. " + "Please change 'Asset Mode' on your binance futures account." + ) if msg: raise OperationalException(msg) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( - f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' - ) from e + f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}" + ) from e except ccxt.BaseError as e: raise OperationalException(e) from e - async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, - since_ms: int, candle_type: CandleType, - is_new_pair: bool = False, raise_: bool = False, - until_ms: Optional[int] = None - ) -> OHLCVResponse: + async def _async_get_historic_ohlcv( + self, + pair: str, + timeframe: str, + since_ms: int, + candle_type: CandleType, + is_new_pair: bool = False, + raise_: bool = False, + until_ms: Optional[int] = None, + ) -> OHLCVResponse: """ Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date Does not work for other exchanges, which don't return the earliest data when called with "0" @@ -109,7 +117,8 @@ class Binance(Exchange): since_ms = x[3][0][0] logger.info( f"Candle-data for {pair} available starting with " - f"{datetime.fromtimestamp(since_ms // 1000, tz=timezone.utc).isoformat()}.") + f"{datetime.fromtimestamp(since_ms // 1000, tz=timezone.utc).isoformat()}." + ) return await super()._async_get_historic_ohlcv( pair=pair, @@ -135,7 +144,7 @@ class Binance(Exchange): def dry_run_liquidation_price( self, pair: str, - open_rate: float, # Entry price of position + open_rate: float, # Entry price of position is_short: bool, amount: float, stake_amount: float, @@ -177,7 +186,7 @@ class Binance(Exchange): # maintenance_amt: (CUM) Maintenance Amount of position mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt(pair, stake_amount) - if (maintenance_amt is None): + if maintenance_amt is None: raise OperationalException( "Parameter maintenance_amt is required by Binance.liquidation_price" f"for {self.trading_mode.value}" @@ -185,24 +194,18 @@ class Binance(Exchange): if self.trading_mode == TradingMode.FUTURES: return ( - ( - (wallet_balance + cross_vars + maintenance_amt) - - (side_1 * amount * open_rate) - ) / ( - (amount * mm_ratio) - (side_1 * amount) - ) - ) + (wallet_balance + cross_vars + maintenance_amt) - (side_1 * amount * open_rate) + ) / ((amount * mm_ratio) - (side_1 * amount)) else: raise OperationalException( - "Freqtrade only supports isolated futures for leverage trading") + "Freqtrade only supports isolated futures for leverage trading" + ) @retrier def load_leverage_tiers(self) -> Dict[str, List[Dict]]: if self.trading_mode == TradingMode.FUTURES: - if self._config['dry_run']: - leverage_tiers_path = ( - Path(__file__).parent / 'binance_leverage_tiers.json' - ) + if self._config["dry_run"]: + leverage_tiers_path = Path(__file__).parent / "binance_leverage_tiers.json" with leverage_tiers_path.open() as json_file: return json_load(json_file) else: @@ -211,8 +214,10 @@ class Binance(Exchange): except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.OperationFailed, ccxt.ExchangeError) as e: - raise TemporaryError(f'Could not fetch leverage amounts due to' - f'{e.__class__.__name__}. Message: {e}') from e + raise TemporaryError( + f"Could not fetch leverage amounts due to" + f"{e.__class__.__name__}. Message: {e}" + ) from e except ccxt.BaseError as e: raise OperationalException(e) from e else: diff --git a/freqtrade/exchange/bingx.py b/freqtrade/exchange/bingx.py index 0bbf4a19d..7ee08273c 100644 --- a/freqtrade/exchange/bingx.py +++ b/freqtrade/exchange/bingx.py @@ -1,4 +1,5 @@ -""" Bingx exchange subclass """ +"""Bingx exchange subclass""" + import logging from typing import Dict diff --git a/freqtrade/exchange/bitmart.py b/freqtrade/exchange/bitmart.py index 5d792b153..ffc8ac67a 100644 --- a/freqtrade/exchange/bitmart.py +++ b/freqtrade/exchange/bitmart.py @@ -1,4 +1,5 @@ -""" Bitmart exchange subclass """ +"""Bitmart exchange subclass""" + import logging from typing import Dict diff --git a/freqtrade/exchange/bitpanda.py b/freqtrade/exchange/bitpanda.py index 4cac35ce8..1e93256e7 100644 --- a/freqtrade/exchange/bitpanda.py +++ b/freqtrade/exchange/bitpanda.py @@ -1,4 +1,5 @@ -""" Bitpanda exchange subclass """ +"""Bitpanda exchange subclass""" + import logging from datetime import datetime, timezone from typing import Dict, List, Optional @@ -15,8 +16,9 @@ class Bitpanda(Exchange): with this exchange. """ - def get_trades_for_order(self, order_id: str, pair: str, since: datetime, - params: Optional[Dict] = None) -> List: + def get_trades_for_order( + self, order_id: str, pair: str, since: datetime, params: Optional[Dict] = None + ) -> List: """ Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id. The "since" argument passed in is coming from the database and is in UTC, @@ -33,5 +35,5 @@ class Bitpanda(Exchange): :param pair: Pair the order is for :param since: datetime object of the order creation time. Assumes object is in UTC. """ - params = {'to': int(datetime.now(timezone.utc).timestamp() * 1000)} + params = {"to": int(datetime.now(timezone.utc).timestamp() * 1000)} return super().get_trades_for_order(order_id, pair, since, params) diff --git a/freqtrade/exchange/bitvavo.py b/freqtrade/exchange/bitvavo.py index ba1d355cc..d088e3435 100644 --- a/freqtrade/exchange/bitvavo.py +++ b/freqtrade/exchange/bitvavo.py @@ -1,4 +1,5 @@ """Kucoin exchange subclass.""" + import logging from typing import Dict diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 7a1581233..1f810cf1f 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -1,4 +1,5 @@ -""" Bybit exchange subclass """ +"""Bybit exchange subclass""" + import logging from datetime import datetime, timedelta from typing import Any, Dict, List, Optional, Tuple @@ -25,6 +26,7 @@ class Bybit(Exchange): officially supported by the Freqtrade development team. So some features may still not work as expected. """ + unified_account = False _ft_has: Dict = { @@ -60,20 +62,14 @@ class Bybit(Exchange): # ccxt defaults to swap mode. config = {} if self.trading_mode == TradingMode.SPOT: - config.update({ - "options": { - "defaultType": "spot" - } - }) + config.update({"options": {"defaultType": "spot"}}) config.update(super()._ccxt_config) return config def market_is_future(self, market: Dict[str, Any]) -> bool: main = super().market_is_future(market) # For ByBit, we'll only support USDT markets for now. - return ( - main and market['settle'] == 'USDT' - ) + return main and market["settle"] == "USDT" @retrier def additional_exchange_init(self) -> None: @@ -83,17 +79,19 @@ class Bybit(Exchange): Must be overridden in child methods if required. """ try: - if not self._config['dry_run']: + if not self._config["dry_run"]: if self.trading_mode == TradingMode.FUTURES: position_mode = self._api.set_position_mode(False) - self._log_exchange_response('set_position_mode', position_mode) + self._log_exchange_response("set_position_mode", position_mode) is_unified = self._api.is_unified_enabled() # Returns a tuple of bools, first for margin, second for Account if is_unified and len(is_unified) > 1 and is_unified[1]: self.unified_account = True logger.info("Bybit: Unified account.") - raise OperationalException("Bybit: Unified account is not supported. " - "Please use a standard (sub)account.") + raise OperationalException( + "Bybit: Unified account is not supported. " + "Please use a standard (sub)account." + ) else: self.unified_account = False logger.info("Bybit: Standard account.") @@ -101,14 +99,14 @@ class Bybit(Exchange): raise DDosProtection(e) from e except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( - f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' - ) from e + f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}" + ) from e except ccxt.BaseError as e: raise OperationalException(e) from e def ohlcv_candle_limit( - self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int: - + self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None + ) -> int: if candle_type in (CandleType.FUNDING_RATE): return 200 @@ -116,7 +114,7 @@ class Bybit(Exchange): def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False): if self.trading_mode != TradingMode.SPOT: - params = {'leverage': leverage} + params = {"leverage": leverage} self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params) self._set_leverage(leverage, pair, accept_fail=True) @@ -126,7 +124,7 @@ class Bybit(Exchange): ordertype: str, leverage: float, reduceOnly: bool, - time_in_force: str = 'GTC', + time_in_force: str = "GTC", ) -> Dict: params = super()._get_params( side=side, @@ -136,13 +134,13 @@ class Bybit(Exchange): time_in_force=time_in_force, ) if self.trading_mode == TradingMode.FUTURES and self.margin_mode: - params['position_idx'] = 0 + params["position_idx"] = 0 return params def dry_run_liquidation_price( self, pair: str, - open_rate: float, # Entry price of position + open_rate: float, # Entry price of position is_short: bool, amount: float, stake_amount: float, @@ -185,10 +183,8 @@ class Bybit(Exchange): mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount) if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED: - - if market['inverse']: - raise OperationalException( - "Freqtrade does not yet support inverse contracts") + if market["inverse"]: + raise OperationalException("Freqtrade does not yet support inverse contracts") initial_margin_rate = 1 / leverage # See docstring - ignores extra margin! @@ -199,10 +195,12 @@ class Bybit(Exchange): else: raise OperationalException( - "Freqtrade only supports isolated futures for leverage trading") + "Freqtrade only supports isolated futures for leverage trading" + ) def get_funding_fees( - self, pair: str, amount: float, is_short: bool, open_date: datetime) -> float: + self, pair: str, amount: float, is_short: bool, open_date: datetime + ) -> float: """ Fetch funding fees, either from the exchange (live) or calculates them based on funding rate/mark price history @@ -216,8 +214,7 @@ class Bybit(Exchange): # Bybit does not provide "applied" funding fees per position. if self.trading_mode == TradingMode.FUTURES: try: - return self._fetch_and_calculate_funding_fees( - pair, amount, is_short, open_date) + return self._fetch_and_calculate_funding_fees(pair, amount, is_short, open_date) except ExchangeError: logger.warning(f"Could not update funding fees for {pair}.") return 0.0 @@ -234,7 +231,7 @@ class Bybit(Exchange): while since < dt_now(): until = since + timedelta(days=7, minutes=-1) - orders += super().fetch_orders(pair, since, params={'until': dt_ts(until)}) + orders += super().fetch_orders(pair, since, params={"until": dt_ts(until)}) since = until return orders @@ -242,12 +239,12 @@ class Bybit(Exchange): def fetch_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: order = super().fetch_order(order_id, pair, params) if ( - order.get('status') == 'canceled' - and order.get('filled') == 0.0 - and order.get('remaining') == 0.0 + order.get("status") == "canceled" + and order.get("filled") == 0.0 + and order.get("remaining") == 0.0 ): # Canceled orders will have "remaining=0" on bybit. - order['remaining'] = None + order["remaining"] = None return order @retrier @@ -258,7 +255,7 @@ class Bybit(Exchange): """ # Load cached tiers - tiers_cached = self.load_cached_leverage_tiers(self._config['stake_currency']) + tiers_cached = self.load_cached_leverage_tiers(self._config["stake_currency"]) if tiers_cached: tiers = tiers_cached return tiers @@ -268,12 +265,12 @@ class Bybit(Exchange): symbols = self._api.market_symbols([]) def parse_resp(response): - result = self._api.safe_dict(response, 'result', {}) - data = self._api.safe_list(result, 'list', []) - return self._api.parse_leverage_tiers(data, symbols, 'symbol') + result = self._api.safe_dict(response, "result", {}) + data = self._api.safe_list(result, "list", []) + return self._api.parse_leverage_tiers(data, symbols, "symbol") params = { - 'category': 'linear', + "category": "linear", } tiers = {} # 20 pairs ... should be sufficient assuming 30 pairs per page @@ -282,11 +279,9 @@ class Bybit(Exchange): # Fetch from private endpoint response = self._api.publicGetV5MarketRiskLimit(params) tiers = tiers | parse_resp(response) - if (cursor := response['result']['nextPageCursor']) == '': + if (cursor := response["result"]["nextPageCursor"]) == "": break - params.update({ - "cursor": cursor - }) + params.update({"cursor": cursor}) - self.cache_leverage_tiers(tiers, self._config['stake_currency']) + self.cache_leverage_tiers(tiers, self._config["stake_currency"]) return tiers diff --git a/freqtrade/exchange/check_exchange.py b/freqtrade/exchange/check_exchange.py index 94e330cc1..03f47de22 100644 --- a/freqtrade/exchange/check_exchange.py +++ b/freqtrade/exchange/check_exchange.py @@ -21,13 +21,16 @@ def check_exchange(config: Config, check_for_bad: bool = True) -> bool: and thus is not known for the Freqtrade at all. """ - if (config['runmode'] in [RunMode.PLOT, RunMode.UTIL_NO_EXCHANGE, RunMode.OTHER] - and not config.get('exchange', {}).get('name')): + if config["runmode"] in [ + RunMode.PLOT, + RunMode.UTIL_NO_EXCHANGE, + RunMode.OTHER, + ] and not config.get("exchange", {}).get("name"): # Skip checking exchange in plot mode, since it requires no exchange return True logger.info("Checking exchange...") - exchange = config.get('exchange', {}).get('name', '').lower() + exchange = config.get("exchange", {}).get("name", "").lower() if not exchange: raise OperationalException( f'This command requires a configured exchange. You should either use ' @@ -47,19 +50,23 @@ def check_exchange(config: Config, check_for_bad: bool = True) -> bool: valid, reason = validate_exchange(exchange) if not valid: if check_for_bad: - raise OperationalException(f'Exchange "{exchange}" will not work with Freqtrade. ' - f'Reason: {reason}') + raise OperationalException( + f'Exchange "{exchange}" will not work with Freqtrade. ' f"Reason: {reason}" + ) else: logger.warning(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}') if MAP_EXCHANGE_CHILDCLASS.get(exchange, exchange) in SUPPORTED_EXCHANGES: - logger.info(f'Exchange "{exchange}" is officially supported ' - f'by the Freqtrade development team.') + logger.info( + f'Exchange "{exchange}" is officially supported ' f"by the Freqtrade development team." + ) else: - logger.warning(f'Exchange "{exchange}" is known to the ccxt library, ' - f'available for the bot, but not officially supported ' - f'by the Freqtrade development team. ' - f'It may work flawlessly (please report back) or have serious issues. ' - f'Use it at your own discretion.') + logger.warning( + f'Exchange "{exchange}" is known to the ccxt library, ' + f"available for the bot, but not officially supported " + f"by the Freqtrade development team. " + f"It may work flawlessly (please report back) or have serious issues. " + f"Use it at your own discretion." + ) return True diff --git a/freqtrade/exchange/coinbasepro.py b/freqtrade/exchange/coinbasepro.py index 7dd9c80dc..e234002ad 100644 --- a/freqtrade/exchange/coinbasepro.py +++ b/freqtrade/exchange/coinbasepro.py @@ -1,4 +1,5 @@ -""" CoinbasePro exchange subclass """ +"""CoinbasePro exchange subclass""" + import logging from typing import Dict diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 8909ef5ff..5035d0dd8 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -43,46 +43,48 @@ BAD_EXCHANGES = { } MAP_EXCHANGE_CHILDCLASS = { - 'binanceus': 'binance', - 'binanceje': 'binance', - 'binanceusdm': 'binance', - 'okex': 'okx', - 'gateio': 'gate', - 'huboi': 'htx', + "binanceus": "binance", + "binanceje": "binance", + "binanceusdm": "binance", + "okex": "okx", + "gateio": "gate", + "huboi": "htx", } SUPPORTED_EXCHANGES = [ - 'binance', - 'bitmart', - 'gate', - 'htx', - 'kraken', - 'okx', + "binance", + "bitmart", + "gate", + "htx", + "kraken", + "okx", ] # either the main, or replacement methods (array) is required EXCHANGE_HAS_REQUIRED: Dict[str, List[str]] = { # Required / private - 'fetchOrder': ['fetchOpenOrder', 'fetchClosedOrder'], - 'cancelOrder': [], - 'createOrder': [], - 'fetchBalance': [], - + "fetchOrder": ["fetchOpenOrder", "fetchClosedOrder"], + "cancelOrder": [], + "createOrder": [], + "fetchBalance": [], # Public endpoints - 'fetchOHLCV': [], + "fetchOHLCV": [], } EXCHANGE_HAS_OPTIONAL = [ # Private - 'fetchMyTrades', # Trades for order - fee detection - 'createLimitOrder', 'createMarketOrder', # Either OR for orders + "fetchMyTrades", # Trades for order - fee detection + "createLimitOrder", + "createMarketOrder", # Either OR for orders # 'setLeverage', # Margin/Futures trading # 'setMarginMode', # Margin/Futures trading # 'fetchFundingHistory', # Futures trading # Public - 'fetchOrderBook', 'fetchL2OrderBook', 'fetchTicker', # OR for pricing - 'fetchTickers', # For volumepairlist? - 'fetchTrades', # Downloading trades data + "fetchOrderBook", + "fetchL2OrderBook", + "fetchTicker", # OR for pricing + "fetchTickers", # For volumepairlist? + "fetchTrades", # Downloading trades data # 'fetchFundingRateHistory', # Futures trading # 'fetchPositions', # Futures trading # 'fetchLeverageTiers', # Futures initialization @@ -99,11 +101,11 @@ def remove_exchange_credentials(exchange_config: ExchangeConfig, dry_run: bool) Modifies the input dict! """ if dry_run: - exchange_config['key'] = '' - exchange_config['apiKey'] = '' - exchange_config['secret'] = '' - exchange_config['password'] = '' - exchange_config['uid'] = '' + exchange_config["key"] = "" + exchange_config["apiKey"] = "" + exchange_config["secret"] = "" + exchange_config["password"] = "" + exchange_config["uid"] = "" def calculate_backoff(retrycount, max_retries): @@ -115,25 +117,27 @@ def calculate_backoff(retrycount, max_retries): def retrier_async(f): async def wrapper(*args, **kwargs): - count = kwargs.pop('count', API_RETRY_COUNT) + count = kwargs.pop("count", API_RETRY_COUNT) kucoin = args[0].name == "KuCoin" # Check if the exchange is KuCoin. try: return await f(*args, **kwargs) except TemporaryError as ex: msg = f'{f.__name__}() returned exception: "{ex}". ' if count > 0: - msg += f'Retrying still for {count} times.' + msg += f"Retrying still for {count} times." count -= 1 - kwargs['count'] = count + kwargs["count"] = count if isinstance(ex, DDosProtection): if kucoin and "429000" in str(ex): # Temporary fix for 429000 error on kucoin # see https://github.com/freqtrade/freqtrade/issues/5700 for details. _get_logging_mixin().log_once( f"Kucoin 429 error, avoid triggering DDosProtection backoff delay. " - f"{count} tries left before giving up", logmethod=logger.warning) + f"{count} tries left before giving up", + logmethod=logger.warning, + ) # Reset msg to avoid logging too many times. - msg = '' + msg = "" else: backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT) logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}") @@ -142,38 +146,37 @@ def retrier_async(f): logger.warning(msg) return await wrapper(*args, **kwargs) else: - logger.warning(msg + 'Giving up.') + logger.warning(msg + "Giving up.") raise ex + return wrapper -F = TypeVar('F', bound=Callable[..., Any]) +F = TypeVar("F", bound=Callable[..., Any]) # Type shenanigans @overload -def retrier(_func: F) -> F: - ... +def retrier(_func: F) -> F: ... @overload -def retrier(*, retries=API_RETRY_COUNT) -> Callable[[F], F]: - ... +def retrier(*, retries=API_RETRY_COUNT) -> Callable[[F], F]: ... def retrier(_func: Optional[F] = None, *, retries=API_RETRY_COUNT): def decorator(f: F) -> F: @wraps(f) def wrapper(*args, **kwargs): - count = kwargs.pop('count', retries) + count = kwargs.pop("count", retries) try: return f(*args, **kwargs) except (TemporaryError, RetryableOrderError) as ex: msg = f'{f.__name__}() returned exception: "{ex}". ' if count > 0: - logger.warning(msg + f'Retrying still for {count} times.') + logger.warning(msg + f"Retrying still for {count} times.") count -= 1 - kwargs.update({'count': count}) + kwargs.update({"count": count}) if isinstance(ex, (DDosProtection, RetryableOrderError)): # increasing backoff backoff_delay = calculate_backoff(count + 1, retries) @@ -181,9 +184,11 @@ def retrier(_func: Optional[F] = None, *, retries=API_RETRY_COUNT): time.sleep(backoff_delay) return wrapper(*args, **kwargs) else: - logger.warning(msg + 'Giving up.') + logger.warning(msg + "Giving up.") raise ex + return cast(F, wrapper) + # Support both @retrier and @retrier(retries=2) syntax if _func is None: return decorator diff --git a/freqtrade/exchange/exchange_utils.py b/freqtrade/exchange/exchange_utils.py index 0bb076e6b..dcae1ab3b 100644 --- a/freqtrade/exchange/exchange_utils.py +++ b/freqtrade/exchange/exchange_utils.py @@ -1,6 +1,7 @@ """ Exchange support utils """ + from datetime import datetime, timedelta, timezone from math import ceil, floor from typing import Any, Dict, List, Optional, Tuple @@ -32,7 +33,8 @@ CcxtModuleType = Any def is_exchange_known_ccxt( - exchange_name: str, ccxt_module: Optional[CcxtModuleType] = None) -> bool: + exchange_name: str, ccxt_module: Optional[CcxtModuleType] = None +) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) @@ -58,13 +60,13 @@ def validate_exchange(exchange: str) -> Tuple[bool, str]: """ ex_mod = getattr(ccxt, exchange.lower())() result = True - reason = '' + reason = "" if not ex_mod or not ex_mod.has: - return False, '' + return False, "" missing = [ - k for k, v in EXCHANGE_HAS_REQUIRED.items() - if ex_mod.has.get(k) is not True - and not (all(ex_mod.has.get(x) for x in v)) + k + for k, v in EXCHANGE_HAS_REQUIRED.items() + if ex_mod.has.get(k) is not True and not (all(ex_mod.has.get(x) for x in v)) ] if missing: result = False @@ -74,7 +76,7 @@ def validate_exchange(exchange: str) -> Tuple[bool, str]: if exchange.lower() in BAD_EXCHANGES: result = False - reason = BAD_EXCHANGES.get(exchange.lower(), '') + reason = BAD_EXCHANGES.get(exchange.lower(), "") if missing_opt: reason += f"{'. ' if reason else ''}missing opt: {', '.join(missing_opt)}. " @@ -83,23 +85,26 @@ def validate_exchange(exchange: str) -> Tuple[bool, str]: def _build_exchange_list_entry( - exchange_name: str, exchangeClasses: Dict[str, Any]) -> ValidExchangesType: + exchange_name: str, exchangeClasses: Dict[str, Any] +) -> ValidExchangesType: valid, comment = validate_exchange(exchange_name) result: ValidExchangesType = { - 'name': exchange_name, - 'valid': valid, - 'supported': exchange_name.lower() in SUPPORTED_EXCHANGES, - 'comment': comment, - 'trade_modes': [{'trading_mode': 'spot', 'margin_mode': ''}], + "name": exchange_name, + "valid": valid, + "supported": exchange_name.lower() in SUPPORTED_EXCHANGES, + "comment": comment, + "trade_modes": [{"trading_mode": "spot", "margin_mode": ""}], } if resolved := exchangeClasses.get(exchange_name.lower()): - supported_modes = [{'trading_mode': 'spot', 'margin_mode': ''}] + [ - {'trading_mode': tm.value, 'margin_mode': mm.value} - for tm, mm in resolved['class']._supported_trading_mode_margin_pairs + supported_modes = [{"trading_mode": "spot", "margin_mode": ""}] + [ + {"trading_mode": tm.value, "margin_mode": mm.value} + for tm, mm in resolved["class"]._supported_trading_mode_margin_pairs ] - result.update({ - 'trade_modes': supported_modes, - }) + result.update( + { + "trade_modes": supported_modes, + } + ) return result @@ -111,7 +116,7 @@ def list_available_exchanges(all_exchanges: bool) -> List[ValidExchangesType]: exchanges = ccxt_exchanges() if all_exchanges else available_exchanges() from freqtrade.resolvers.exchange_resolver import ExchangeResolver - subclassed = {e['name'].lower(): e for e in ExchangeResolver.search_all_objects({}, False)} + subclassed = {e["name"].lower(): e for e in ExchangeResolver.search_all_objects({}, False)} exchanges_valid: List[ValidExchangesType] = [ _build_exchange_list_entry(e, subclassed) for e in exchanges @@ -121,7 +126,8 @@ def list_available_exchanges(all_exchanges: bool) -> List[ValidExchangesType]: def date_minus_candles( - timeframe: str, candle_count: int, date: Optional[datetime] = None) -> datetime: + timeframe: str, candle_count: int, date: Optional[datetime] = None +) -> datetime: """ subtract X candles from a date. :param timeframe: timeframe in string format (e.g. "5m") @@ -145,7 +151,7 @@ def market_is_active(market: Dict) -> bool: # true then it's true. If it's undefined, then it's most likely true, but not 100% )" # See https://github.com/ccxt/ccxt/issues/4874, # https://github.com/ccxt/ccxt/issues/4075#issuecomment-434760520 - return market.get('active', True) is not False + return market.get("active", True) is not False def amount_to_contracts(amount: float, contract_size: Optional[float]) -> float: @@ -175,8 +181,9 @@ def contracts_to_amount(num_contracts: float, contract_size: Optional[float]) -> return num_contracts -def amount_to_precision(amount: float, amount_precision: Optional[float], - precisionMode: Optional[int]) -> float: +def amount_to_precision( + amount: float, amount_precision: Optional[float], precisionMode: Optional[int] +) -> float: """ Returns the amount to buy or sell to a precision the Exchange accepts Re-implementation of ccxt internal methods - ensuring we can test the result is correct @@ -191,17 +198,24 @@ def amount_to_precision(amount: float, amount_precision: Optional[float], if amount_precision is not None and precisionMode is not None: precision = int(amount_precision) if precisionMode != TICK_SIZE else amount_precision # precision must be an int for non-ticksize inputs. - amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE, - precision=precision, - counting_mode=precisionMode, - )) + amount = float( + decimal_to_precision( + amount, + rounding_mode=TRUNCATE, + precision=precision, + counting_mode=precisionMode, + ) + ) return amount def amount_to_contract_precision( - amount, amount_precision: Optional[float], precisionMode: Optional[int], - contract_size: Optional[float]) -> float: + amount, + amount_precision: Optional[float], + precisionMode: Optional[int], + contract_size: Optional[float], +) -> float: """ Returns the amount to buy or sell to a precision the Exchange accepts including calculation to and from contracts. @@ -234,23 +248,25 @@ def __price_to_precision_significant_digits( from decimal import ROUND_DOWN as dec_ROUND_DOWN from decimal import ROUND_UP as dec_ROUND_UP from decimal import Decimal + dec = Decimal(str(price)) - string = f'{dec:f}' + string = f"{dec:f}" precision = round(price_precision) q = precision - dec.adjusted() - 1 - sigfig = Decimal('10') ** -q + sigfig = Decimal("10") ** -q if q < 0: string_to_precision = string[:precision] # string_to_precision is '' when we have zero precision - below = sigfig * Decimal(string_to_precision if string_to_precision else '0') + below = sigfig * Decimal(string_to_precision if string_to_precision else "0") above = below + sigfig res = above if rounding_mode == ROUND_UP else below - precise = f'{res:f}' + precise = f"{res:f}" else: - precise = '{:f}'.format(dec.quantize( - sigfig, - rounding=dec_ROUND_DOWN if rounding_mode == ROUND_DOWN else dec_ROUND_UP) + precise = "{:f}".format( + dec.quantize( + sigfig, rounding=dec_ROUND_DOWN if rounding_mode == ROUND_DOWN else dec_ROUND_UP + ) ) return float(precise) @@ -280,10 +296,14 @@ def price_to_precision( if price_precision is not None and precisionMode is not None: if rounding_mode not in (ROUND_UP, ROUND_DOWN): # Use CCXT code where possible. - return float(decimal_to_precision(price, rounding_mode=rounding_mode, - precision=price_precision, - counting_mode=precisionMode - )) + return float( + decimal_to_precision( + price, + rounding_mode=rounding_mode, + precision=price_precision, + counting_mode=precisionMode, + ) + ) if precisionMode == TICK_SIZE: precision = FtPrecise(price_precision) @@ -297,7 +317,6 @@ def price_to_precision( return round(float(str(res)), 14) return price elif precisionMode == DECIMAL_PLACES: - ndigits = round(price_precision) ticks = price * (10**ndigits) if rounding_mode == ROUND_UP: diff --git a/freqtrade/exchange/exchange_utils_timeframe.py b/freqtrade/exchange/exchange_utils_timeframe.py index 9366bc7a1..67cf1b5d6 100644 --- a/freqtrade/exchange/exchange_utils_timeframe.py +++ b/freqtrade/exchange/exchange_utils_timeframe.py @@ -36,16 +36,16 @@ def timeframe_to_resample_freq(timeframe: str) -> str: form ('1m', '5m', '1h', '1d', '1w', etc.) to the resample frequency used by pandas ('1T', '5T', '1H', '1D', '1W', etc.) """ - if timeframe == '1y': - return '1YS' + if timeframe == "1y": + return "1YS" timeframe_seconds = timeframe_to_seconds(timeframe) timeframe_minutes = timeframe_seconds // 60 - resample_interval = f'{timeframe_seconds}s' + resample_interval = f"{timeframe_seconds}s" if 10000 < timeframe_minutes < 43200: - resample_interval = '1W-MON' + resample_interval = "1W-MON" elif timeframe_minutes >= 43200 and timeframe_minutes < 525600: # Monthly candles need special treatment to stick to the 1st of the month - resample_interval = f'{timeframe}S' + resample_interval = f"{timeframe}S" elif timeframe_minutes > 43200: resample_interval = timeframe return resample_interval @@ -62,8 +62,7 @@ def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> d if not date: date = datetime.now(timezone.utc) - new_timestamp = ccxt.Exchange.round_timeframe( - timeframe, dt_ts(date), ROUND_DOWN) // 1000 + new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_DOWN) // 1000 return dt_from_ts(new_timestamp) @@ -76,6 +75,5 @@ def timeframe_to_next_date(timeframe: str, date: Optional[datetime] = None) -> d """ if not date: date = datetime.now(timezone.utc) - new_timestamp = ccxt.Exchange.round_timeframe( - timeframe, dt_ts(date), ROUND_UP) // 1000 + new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_UP) // 1000 return dt_from_ts(new_timestamp) diff --git a/freqtrade/exchange/gate.py b/freqtrade/exchange/gate.py index 1d25e2df3..2408e306e 100644 --- a/freqtrade/exchange/gate.py +++ b/freqtrade/exchange/gate.py @@ -1,4 +1,5 @@ -""" Gate.io exchange subclass """ +"""Gate.io exchange subclass""" + import logging from datetime import datetime from typing import Any, Dict, List, Optional, Tuple @@ -24,7 +25,7 @@ class Gate(Exchange): _ft_has: Dict = { "ohlcv_candle_limit": 1000, - "order_time_in_force": ['GTC', 'IOC'], + "order_time_in_force": ["GTC", "IOC"], "stoploss_on_exchange": True, "stoploss_order_types": {"limit": "limit"}, "stop_price_param": "stopPrice", @@ -51,13 +52,13 @@ class Gate(Exchange): ] def _get_params( - self, - side: BuySell, - ordertype: str, - leverage: float, - reduceOnly: bool, - time_in_force: str = 'GTC', - ) -> Dict: + self, + side: BuySell, + ordertype: str, + leverage: float, + reduceOnly: bool, + time_in_force: str = "GTC", + ) -> Dict: params = super()._get_params( side=side, ordertype=ordertype, @@ -65,13 +66,14 @@ class Gate(Exchange): reduceOnly=reduceOnly, time_in_force=time_in_force, ) - if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES: - params['type'] = 'market' - params.update({'timeInForce': 'IOC'}) + if ordertype == "market" and self.trading_mode == TradingMode.FUTURES: + params["type"] = "market" + params.update({"timeInForce": "IOC"}) return params - def get_trades_for_order(self, order_id: str, pair: str, since: datetime, - params: Optional[Dict] = None) -> List: + def get_trades_for_order( + self, order_id: str, pair: str, since: datetime, params: Optional[Dict] = None + ) -> List: trades = super().get_trades_for_order(order_id, pair, since, params) if self.trading_mode == TradingMode.FUTURES: @@ -84,45 +86,38 @@ class Gate(Exchange): pair_fees = self._trading_fees.get(pair, {}) if pair_fees: for idx, trade in enumerate(trades): - fee = trade.get('fee', {}) - if fee and fee.get('cost') is None: - takerOrMaker = trade.get('takerOrMaker', 'taker') + fee = trade.get("fee", {}) + if fee and fee.get("cost") is None: + takerOrMaker = trade.get("takerOrMaker", "taker") if pair_fees.get(takerOrMaker) is not None: - trades[idx]['fee'] = { - 'currency': self.get_pair_quote_currency(pair), - 'cost': trade['cost'] * pair_fees[takerOrMaker], - 'rate': pair_fees[takerOrMaker], + trades[idx]["fee"] = { + "currency": self.get_pair_quote_currency(pair), + "cost": trade["cost"] * pair_fees[takerOrMaker], + "rate": pair_fees[takerOrMaker], } return trades def get_order_id_conditional(self, order: Dict[str, Any]) -> str: - return safe_value_fallback2(order, order, 'id_stop', 'id') + return safe_value_fallback2(order, order, "id_stop", "id") def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: - order = self.fetch_order( - order_id=order_id, - pair=pair, - params={'stop': True} - ) - if order.get('status', 'open') == 'closed': + order = self.fetch_order(order_id=order_id, pair=pair, params={"stop": True}) + if order.get("status", "open") == "closed": # Places a real order - which we need to fetch explicitly. - val = 'trade_id' if self.trading_mode == TradingMode.FUTURES else 'fired_order_id' + val = "trade_id" if self.trading_mode == TradingMode.FUTURES else "fired_order_id" - if new_orderid := order.get('info', {}).get(val): + if new_orderid := order.get("info", {}).get(val): order1 = self.fetch_order(order_id=new_orderid, pair=pair, params=params) - order1['id_stop'] = order1['id'] - order1['id'] = order_id - order1['type'] = 'stoploss' - order1['stopPrice'] = order.get('stopPrice') - order1['status_stop'] = 'triggered' + order1["id_stop"] = order1["id"] + order1["id"] = order_id + order1["type"] = "stoploss" + order1["stopPrice"] = order.get("stopPrice") + order1["status_stop"] = "triggered" return order1 return order def cancel_stoploss_order( - self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: - return self.cancel_order( - order_id=order_id, - pair=pair, - params={'stop': True} - ) + self, order_id: str, pair: str, params: Optional[Dict] = None + ) -> Dict: + return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True}) diff --git a/freqtrade/exchange/htx.py b/freqtrade/exchange/htx.py index 2e9aff77b..58eb919bc 100644 --- a/freqtrade/exchange/htx.py +++ b/freqtrade/exchange/htx.py @@ -1,4 +1,5 @@ -""" HTX exchange subclass """ +"""HTX exchange subclass""" + import logging from typing import Dict @@ -26,10 +27,11 @@ class Htx(Exchange): } def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: - params = self._params.copy() - params.update({ - "stopPrice": stop_price, - "operator": "lte", - }) + params.update( + { + "stopPrice": stop_price, + "operator": "lte", + } + ) return params diff --git a/freqtrade/exchange/idex.py b/freqtrade/exchange/idex.py index eae5ad155..b3bf12110 100644 --- a/freqtrade/exchange/idex.py +++ b/freqtrade/exchange/idex.py @@ -1,4 +1,5 @@ -""" Idex exchange subclass """ +"""Idex exchange subclass""" + import logging from typing import Dict diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index f30d79cba..4fbbe113c 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -1,4 +1,5 @@ -""" Kraken exchange subclass """ +"""Kraken exchange subclass""" + import logging from datetime import datetime from typing import Any, Dict, List, Optional, Tuple @@ -18,7 +19,6 @@ logger = logging.getLogger(__name__) class Kraken(Exchange): - _params: Dict = {"trading_agreement": "agree"} _ft_has: Dict = { "stoploss_on_exchange": True, @@ -47,18 +47,17 @@ class Kraken(Exchange): """ parent_check = super().market_is_tradable(market) - return (parent_check and - market.get('darkpool', False) is False) + return parent_check and market.get("darkpool", False) is False def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers: # Only fetch tickers for current stake currency # Otherwise the request for kraken becomes too large. - symbols = list(self.get_markets(quote_currencies=[self._config['stake_currency']])) + symbols = list(self.get_markets(quote_currencies=[self._config["stake_currency"]])) return super().get_tickers(symbols=symbols, cached=cached) @retrier def get_balances(self) -> dict: - if self._config['dry_run']: + if self._config["dry_run"]: return {} try: @@ -70,23 +69,28 @@ class Kraken(Exchange): balances.pop("used", None) orders = self._api.fetch_open_orders() - order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1], - x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"], - # Don't remove the below comment, this can be important for debugging - # x["side"], x["amount"], - ) for x in orders] + order_list = [ + ( + x["symbol"].split("/")[0 if x["side"] == "sell" else 1], + x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"], + # Don't remove the below comment, this can be important for debugging + # x["side"], x["amount"], + ) + for x in orders + ] for bal in balances: if not isinstance(balances[bal], dict): continue - balances[bal]['used'] = sum(order[1] for order in order_list if order[0] == bal) - balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used'] + balances[bal]["used"] = sum(order[1] for order in order_list if order[0] == bal) + balances[bal]["free"] = balances[bal]["total"] - balances[bal]["used"] return balances except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e + f"Could not get balance due to {e.__class__.__name__}. Message: {e}" + ) from e except ccxt.BaseError as e: raise OperationalException(e) from e @@ -108,7 +112,7 @@ class Kraken(Exchange): ordertype: str, leverage: float, reduceOnly: bool, - time_in_force: str = 'GTC' + time_in_force: str = "GTC", ) -> Dict: params = super()._get_params( side=side, @@ -118,10 +122,10 @@ class Kraken(Exchange): time_in_force=time_in_force, ) if leverage > 1.0: - params['leverage'] = round(leverage) - if time_in_force == 'PO': - params.pop('timeInForce', None) - params['postOnly'] = True + params["leverage"] = round(leverage) + if time_in_force == "PO": + params.pop("timeInForce", None) + params["postOnly"] = True return params def calculate_funding_fees( @@ -131,7 +135,7 @@ class Kraken(Exchange): is_short: bool, open_date: datetime, close_date: datetime, - time_in_ratio: Optional[float] = None + time_in_ratio: Optional[float] = None, ) -> float: """ # ! This method will always error when run by Freqtrade because time_in_ratio is never @@ -149,12 +153,13 @@ class Kraken(Exchange): """ if not time_in_ratio: raise OperationalException( - f"time_in_ratio is required for {self.name}._get_funding_fee") + f"time_in_ratio is required for {self.name}._get_funding_fee" + ) fees: float = 0 if not df.empty: - df = df[(df['date'] >= open_date) & (df['date'] <= close_date)] - fees = sum(df['open_fund'] * df['open_mark'] * amount * time_in_ratio) + df = df[(df["date"] >= open_date) & (df["date"] <= close_date)] + fees = sum(df["open_fund"] * df["open_mark"] * amount * time_in_ratio) return fees if is_short else -fees @@ -164,14 +169,11 @@ class Kraken(Exchange): Applies only to fetch_trade_history by id. """ if len(trades) > 0: - if ( - isinstance(trades[-1].get('info'), list) - and len(trades[-1].get('info', [])) > 7 - ): + if isinstance(trades[-1].get("info"), list) and len(trades[-1].get("info", [])) > 7: # Trade response's "last" value. - return trades[-1].get('info', [])[-1] + return trades[-1].get("info", [])[-1] # Fall back to timestamp if info is somehow empty. - return trades[-1].get('timestamp') + return trades[-1].get("timestamp") return None def _valid_trade_pagination_id(self, pair: str, from_id: str) -> bool: diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 7033f89ad..343904276 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -1,4 +1,5 @@ """Kucoin exchange subclass.""" + import logging from typing import Dict @@ -26,32 +27,27 @@ class Kucoin(Exchange): "stoploss_order_types": {"limit": "limit", "market": "market"}, "l2_limit_range": [20, 100], "l2_limit_range_required": False, - "order_time_in_force": ['GTC', 'FOK', 'IOC'], + "order_time_in_force": ["GTC", "FOK", "IOC"], "ohlcv_candle_limit": 1500, } def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: - params = self._params.copy() - params.update({ - 'stopPrice': stop_price, - 'stop': 'loss' - }) + params.update({"stopPrice": stop_price, "stop": "loss"}) return params def create_order( - self, - *, - pair: str, - ordertype: str, - side: BuySell, - amount: float, - rate: float, - leverage: float, - reduceOnly: bool = False, - time_in_force: str = 'GTC', - ) -> Dict: - + self, + *, + pair: str, + ordertype: str, + side: BuySell, + amount: float, + rate: float, + leverage: float, + reduceOnly: bool = False, + time_in_force: str = "GTC", + ) -> Dict: res = super().create_order( pair=pair, ordertype=ordertype, @@ -66,7 +62,7 @@ class Kucoin(Exchange): # ccxt returns status = 'closed' at the moment - which is information ccxt invented. # Since we rely on status heavily, we must set it to 'open' here. # ref: https://github.com/ccxt/ccxt/pull/16674, (https://github.com/ccxt/ccxt/pull/16553) - if not self._config['dry_run']: - res['type'] = ordertype - res['status'] = 'open' + if not self._config["dry_run"]: + res["type"] = ordertype + res["status"] = "open" return res diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index 3c9c782c1..1704117e6 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -41,7 +41,7 @@ class Okx(Exchange): PriceType.LAST: "last", PriceType.MARK: "index", PriceType.INDEX: "mark", - }, + }, } _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ @@ -53,10 +53,11 @@ class Okx(Exchange): net_only = True - _ccxt_params: Dict = {'options': {'brokerId': 'ffb5405ad327SUDE'}} + _ccxt_params: Dict = {"options": {"brokerId": "ffb5405ad327SUDE"}} def ohlcv_candle_limit( - self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int: + self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None + ) -> int: """ Exchange ohlcv candle limit OKX has the following behaviour: @@ -68,9 +69,8 @@ class Okx(Exchange): :param since_ms: Starting timestamp :return: Candle limit as integer """ - if ( - candle_type in (CandleType.FUTURES, CandleType.SPOT) and - (not since_ms or since_ms > (date_minus_candles(timeframe, 300).timestamp() * 1000)) + if candle_type in (CandleType.FUTURES, CandleType.SPOT) and ( + not since_ms or since_ms > (date_minus_candles(timeframe, 300).timestamp() * 1000) ): return 300 @@ -84,29 +84,29 @@ class Okx(Exchange): Must be overridden in child methods if required. """ try: - if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']: + if self.trading_mode == TradingMode.FUTURES and not self._config["dry_run"]: accounts = self._api.fetch_accounts() - self._log_exchange_response('fetch_accounts', accounts) + self._log_exchange_response("fetch_accounts", accounts) if len(accounts) > 0: - self.net_only = accounts[0].get('info', {}).get('posMode') == 'net_mode' + self.net_only = accounts[0].get("info", {}).get("posMode") == "net_mode" except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.OperationFailed, ccxt.ExchangeError) as e: raise TemporaryError( - f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' - ) from e + f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}" + ) from e except ccxt.BaseError as e: raise OperationalException(e) from e def _get_posSide(self, side: BuySell, reduceOnly: bool): if self.net_only: - return 'net' + return "net" if not reduceOnly: # Enter - return 'long' if side == 'buy' else 'short' + return "long" if side == "buy" else "short" else: # Exit - return 'long' if side == 'sell' else 'short' + return "long" if side == "sell" else "short" def _get_params( self, @@ -114,7 +114,7 @@ class Okx(Exchange): ordertype: str, leverage: float, reduceOnly: bool, - time_in_force: str = 'GTC', + time_in_force: str = "GTC", ) -> Dict: params = super()._get_params( side=side, @@ -124,18 +124,21 @@ class Okx(Exchange): time_in_force=time_in_force, ) if self.trading_mode == TradingMode.FUTURES and self.margin_mode: - params['tdMode'] = self.margin_mode.value - params['posSide'] = self._get_posSide(side, reduceOnly) + params["tdMode"] = self.margin_mode.value + params["posSide"] = self._get_posSide(side, reduceOnly) return params def __fetch_leverage_already_set(self, pair: str, leverage: float, side: BuySell) -> bool: try: - res_lev = self._api.fetch_leverage(symbol=pair, params={ + res_lev = self._api.fetch_leverage( + symbol=pair, + params={ "mgnMode": self.margin_mode.value, "posSide": self._get_posSide(side, False), - }) - self._log_exchange_response('get_leverage', res_lev) - already_set = all(float(x['lever']) == leverage for x in res_lev['data']) + }, + ) + self._log_exchange_response("get_leverage", res_lev) + already_set = all(float(x["lever"]) == leverage for x in res_lev["data"]) return already_set except ccxt.BaseError: @@ -152,8 +155,9 @@ class Okx(Exchange): params={ "mgnMode": self.margin_mode.value, "posSide": self._get_posSide(side, False), - }) - self._log_exchange_response('set_leverage', res) + }, + ) + self._log_exchange_response("set_leverage", res) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e @@ -161,84 +165,81 @@ class Okx(Exchange): already_set = self.__fetch_leverage_already_set(pair, leverage, side) if not already_set: raise TemporaryError( - f'Could not set leverage due to {e.__class__.__name__}. Message: {e}' - ) from e + f"Could not set leverage due to {e.__class__.__name__}. Message: {e}" + ) from e except ccxt.BaseError as e: raise OperationalException(e) from e - def get_max_pair_stake_amount( - self, - pair: str, - price: float, - leverage: float = 1.0 - ) -> float: - + def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float: if self.trading_mode == TradingMode.SPOT: - return float('inf') # Not actually inf, but this probably won't matter for SPOT + return float("inf") # Not actually inf, but this probably won't matter for SPOT if pair not in self._leverage_tiers: - return float('inf') + return float("inf") pair_tiers = self._leverage_tiers[pair] - return pair_tiers[-1]['maxNotional'] / leverage + return pair_tiers[-1]["maxNotional"] / leverage def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: params = super()._get_stop_params(side, ordertype, stop_price) if self.trading_mode == TradingMode.FUTURES and self.margin_mode: - params['tdMode'] = self.margin_mode.value - params['posSide'] = self._get_posSide(side, True) + params["tdMode"] = self.margin_mode.value + params["posSide"] = self._get_posSide(side, True) return params def _convert_stop_order(self, pair: str, order_id: str, order: Dict) -> Dict: if ( - order.get('status', 'open') == 'closed' - and (real_order_id := order.get('info', {}).get('ordId')) is not None + order.get("status", "open") == "closed" + and (real_order_id := order.get("info", {}).get("ordId")) is not None ): # Once a order triggered, we fetch the regular followup order. order_reg = self.fetch_order(real_order_id, pair) - self._log_exchange_response('fetch_stoploss_order1', order_reg) - order_reg['id_stop'] = order_reg['id'] - order_reg['id'] = order_id - order_reg['type'] = 'stoploss' - order_reg['status_stop'] = 'triggered' + self._log_exchange_response("fetch_stoploss_order1", order_reg) + order_reg["id_stop"] = order_reg["id"] + order_reg["id"] = order_id + order_reg["type"] = "stoploss" + order_reg["status_stop"] = "triggered" return order_reg order = self._order_contracts_to_amount(order) - order['type'] = 'stoploss' + order["type"] = "stoploss" return order def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: - if self._config['dry_run']: + if self._config["dry_run"]: return self.fetch_dry_run_order(order_id) try: - params1 = {'stop': True} + params1 = {"stop": True} order_reg = self._api.fetch_order(order_id, pair, params=params1) - self._log_exchange_response('fetch_stoploss_order', order_reg) + self._log_exchange_response("fetch_stoploss_order", order_reg) return self._convert_stop_order(pair, order_id, order_reg) except ccxt.OrderNotFound: pass - params2 = {'stop': True, 'ordType': 'conditional'} - for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders, - self._api.fetch_canceled_orders): + params2 = {"stop": True, "ordType": "conditional"} + for method in ( + self._api.fetch_open_orders, + self._api.fetch_closed_orders, + self._api.fetch_canceled_orders, + ): try: orders = method(pair, params=params2) - orders_f = [order for order in orders if order['id'] == order_id] + orders_f = [order for order in orders if order["id"] == order_id] if orders_f: order = orders_f[0] return self._convert_stop_order(pair, order_id, order) except ccxt.BaseError: pass - raise RetryableOrderError( - f'StoplossOrder not found (pair: {pair} id: {order_id}).') + raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).") def get_order_id_conditional(self, order: Dict[str, Any]) -> str: - if order.get('type', '') == 'stop': - return safe_value_fallback2(order, order, 'id_stop', 'id') - return order['id'] + if order.get("type", "") == "stop": + return safe_value_fallback2(order, order, "id_stop", "id") + return order["id"] def cancel_stoploss_order( - self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: - params1 = {'stop': True} + self, order_id: str, pair: str, params: Optional[Dict] = None + ) -> Dict: + params1 = {"stop": True} # 'ordType': 'conditional' # return self.cancel_order( @@ -251,10 +252,10 @@ class Okx(Exchange): orders = [] orders = self._api.fetch_closed_orders(pair, since=since_ms) - if (since_ms < dt_ts(dt_now() - timedelta(days=6, hours=23))): + if since_ms < dt_ts(dt_now() - timedelta(days=6, hours=23)): # Regular fetch_closed_orders only returns 7 days of data. # Force usage of "archive" endpoint, which returns 3 months of data. - params = {'method': 'privateGetTradeOrdersHistoryArchive'} + params = {"method": "privateGetTradeOrdersHistoryArchive"} orders_hist = self._api.fetch_closed_orders(pair, since=since_ms, params=params) orders.extend(orders_hist)