ruff format: exchange classes

This commit is contained in:
Matthias 2024-05-12 16:58:33 +02:00
parent 7ea5e40919
commit 53eefb9442
18 changed files with 384 additions and 351 deletions

View File

@ -2,6 +2,7 @@
# isort: off # isort: off
from freqtrade.exchange.common import remove_exchange_credentials, MAP_EXCHANGE_CHILDCLASS from freqtrade.exchange.common import remove_exchange_credentials, MAP_EXCHANGE_CHILDCLASS
from freqtrade.exchange.exchange import Exchange from freqtrade.exchange.exchange import Exchange
# isort: on # isort: on
from freqtrade.exchange.binance import Binance from freqtrade.exchange.binance import Binance
from freqtrade.exchange.bingx import Bingx from freqtrade.exchange.bingx import Bingx

View File

@ -1,4 +1,5 @@
""" Binance exchange subclass """ """Binance exchange subclass"""
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
@ -18,7 +19,6 @@ logger = logging.getLogger(__name__)
class Binance(Exchange): class Binance(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
"stop_price_param": "stopPrice", "stop_price_param": "stopPrice",
@ -36,7 +36,7 @@ class Binance(Exchange):
"tickers_have_price": False, "tickers_have_price": False,
"floor_leverage": True, "floor_leverage": True,
"stop_price_type_field": "workingType", "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": { "stop_price_type_value_mapping": {
PriceType.LAST: "CONTRACT_PRICE", PriceType.LAST: "CONTRACT_PRICE",
PriceType.MARK: "MARK_PRICE", PriceType.MARK: "MARK_PRICE",
@ -67,36 +67,44 @@ class Binance(Exchange):
Must be overridden in child methods if required. Must be overridden in child methods if required.
""" """
try: 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() 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() assets_margin = self._api.fapiPrivateGetMultiAssetsMargin()
self._log_exchange_response('multi_asset_margin', assets_margin) self._log_exchange_response("multi_asset_margin", assets_margin)
msg = "" msg = ""
if position_side.get('dualSidePosition') is True: if position_side.get("dualSidePosition") is True:
msg += ( msg += (
"\nHedge Mode is not supported by freqtrade. " "\nHedge Mode is not supported by freqtrade. "
"Please change 'Position 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. " if assets_margin.get("multiAssetsMargin") is True:
"Please change 'Asset Mode' on your binance futures account.") msg += (
"\nMulti-Asset Mode is not supported by freqtrade. "
"Please change 'Asset Mode' on your binance futures account."
)
if msg: if msg:
raise OperationalException(msg) raise OperationalException(msg)
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e: except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}"
) from e ) from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, async def _async_get_historic_ohlcv(
since_ms: int, candle_type: CandleType, self,
is_new_pair: bool = False, raise_: bool = False, pair: str,
until_ms: Optional[int] = None timeframe: str,
) -> OHLCVResponse: 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 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" 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] since_ms = x[3][0][0]
logger.info( logger.info(
f"Candle-data for {pair} available starting with " 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( return await super()._async_get_historic_ohlcv(
pair=pair, pair=pair,
@ -135,7 +144,7 @@ class Binance(Exchange):
def dry_run_liquidation_price( def dry_run_liquidation_price(
self, self,
pair: str, pair: str,
open_rate: float, # Entry price of position open_rate: float, # Entry price of position
is_short: bool, is_short: bool,
amount: float, amount: float,
stake_amount: float, stake_amount: float,
@ -177,7 +186,7 @@ class Binance(Exchange):
# maintenance_amt: (CUM) Maintenance Amount of position # maintenance_amt: (CUM) Maintenance Amount of position
mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt(pair, stake_amount) 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( raise OperationalException(
"Parameter maintenance_amt is required by Binance.liquidation_price" "Parameter maintenance_amt is required by Binance.liquidation_price"
f"for {self.trading_mode.value}" f"for {self.trading_mode.value}"
@ -185,24 +194,18 @@ class Binance(Exchange):
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
return ( return (
( (wallet_balance + cross_vars + maintenance_amt) - (side_1 * amount * open_rate)
(wallet_balance + cross_vars + maintenance_amt) - ) / ((amount * mm_ratio) - (side_1 * amount))
(side_1 * amount * open_rate)
) / (
(amount * mm_ratio) - (side_1 * amount)
)
)
else: else:
raise OperationalException( raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading") "Freqtrade only supports isolated futures for leverage trading"
)
@retrier @retrier
def load_leverage_tiers(self) -> Dict[str, List[Dict]]: def load_leverage_tiers(self) -> Dict[str, List[Dict]]:
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
if self._config['dry_run']: if self._config["dry_run"]:
leverage_tiers_path = ( leverage_tiers_path = Path(__file__).parent / "binance_leverage_tiers.json"
Path(__file__).parent / 'binance_leverage_tiers.json'
)
with leverage_tiers_path.open() as json_file: with leverage_tiers_path.open() as json_file:
return json_load(json_file) return json_load(json_file)
else: else:
@ -211,8 +214,10 @@ class Binance(Exchange):
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e: except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError(f'Could not fetch leverage amounts due to' raise TemporaryError(
f'{e.__class__.__name__}. Message: {e}') from e f"Could not fetch leverage amounts due to"
f"{e.__class__.__name__}. Message: {e}"
) from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
else: else:

View File

@ -1,4 +1,5 @@
""" Bingx exchange subclass """ """Bingx exchange subclass"""
import logging import logging
from typing import Dict from typing import Dict

View File

@ -1,4 +1,5 @@
""" Bitmart exchange subclass """ """Bitmart exchange subclass"""
import logging import logging
from typing import Dict from typing import Dict

View File

@ -1,4 +1,5 @@
""" Bitpanda exchange subclass """ """Bitpanda exchange subclass"""
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Dict, List, Optional from typing import Dict, List, Optional
@ -15,8 +16,9 @@ class Bitpanda(Exchange):
with this exchange. with this exchange.
""" """
def get_trades_for_order(self, order_id: str, pair: str, since: datetime, def get_trades_for_order(
params: Optional[Dict] = None) -> List: 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. 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, 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 pair: Pair the order is for
:param since: datetime object of the order creation time. Assumes object is in UTC. :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) return super().get_trades_for_order(order_id, pair, since, params)

View File

@ -1,4 +1,5 @@
"""Kucoin exchange subclass.""" """Kucoin exchange subclass."""
import logging import logging
from typing import Dict from typing import Dict

View File

@ -1,4 +1,5 @@
""" Bybit exchange subclass """ """Bybit exchange subclass"""
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple 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 officially supported by the Freqtrade development team. So some features
may still not work as expected. may still not work as expected.
""" """
unified_account = False unified_account = False
_ft_has: Dict = { _ft_has: Dict = {
@ -60,20 +62,14 @@ class Bybit(Exchange):
# ccxt defaults to swap mode. # ccxt defaults to swap mode.
config = {} config = {}
if self.trading_mode == TradingMode.SPOT: if self.trading_mode == TradingMode.SPOT:
config.update({ config.update({"options": {"defaultType": "spot"}})
"options": {
"defaultType": "spot"
}
})
config.update(super()._ccxt_config) config.update(super()._ccxt_config)
return config return config
def market_is_future(self, market: Dict[str, Any]) -> bool: def market_is_future(self, market: Dict[str, Any]) -> bool:
main = super().market_is_future(market) main = super().market_is_future(market)
# For ByBit, we'll only support USDT markets for now. # For ByBit, we'll only support USDT markets for now.
return ( return main and market["settle"] == "USDT"
main and market['settle'] == 'USDT'
)
@retrier @retrier
def additional_exchange_init(self) -> None: def additional_exchange_init(self) -> None:
@ -83,17 +79,19 @@ class Bybit(Exchange):
Must be overridden in child methods if required. Must be overridden in child methods if required.
""" """
try: try:
if not self._config['dry_run']: if not self._config["dry_run"]:
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
position_mode = self._api.set_position_mode(False) 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() is_unified = self._api.is_unified_enabled()
# Returns a tuple of bools, first for margin, second for Account # Returns a tuple of bools, first for margin, second for Account
if is_unified and len(is_unified) > 1 and is_unified[1]: if is_unified and len(is_unified) > 1 and is_unified[1]:
self.unified_account = True self.unified_account = True
logger.info("Bybit: Unified account.") logger.info("Bybit: Unified account.")
raise OperationalException("Bybit: Unified account is not supported. " raise OperationalException(
"Please use a standard (sub)account.") "Bybit: Unified account is not supported. "
"Please use a standard (sub)account."
)
else: else:
self.unified_account = False self.unified_account = False
logger.info("Bybit: Standard account.") logger.info("Bybit: Standard account.")
@ -101,14 +99,14 @@ class Bybit(Exchange):
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e: except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}"
) from e ) from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def ohlcv_candle_limit( 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): if candle_type in (CandleType.FUNDING_RATE):
return 200 return 200
@ -116,7 +114,7 @@ class Bybit(Exchange):
def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False): def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False):
if self.trading_mode != TradingMode.SPOT: 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_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)
self._set_leverage(leverage, pair, accept_fail=True) self._set_leverage(leverage, pair, accept_fail=True)
@ -126,7 +124,7 @@ class Bybit(Exchange):
ordertype: str, ordertype: str,
leverage: float, leverage: float,
reduceOnly: bool, reduceOnly: bool,
time_in_force: str = 'GTC', time_in_force: str = "GTC",
) -> Dict: ) -> Dict:
params = super()._get_params( params = super()._get_params(
side=side, side=side,
@ -136,13 +134,13 @@ class Bybit(Exchange):
time_in_force=time_in_force, time_in_force=time_in_force,
) )
if self.trading_mode == TradingMode.FUTURES and self.margin_mode: if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['position_idx'] = 0 params["position_idx"] = 0
return params return params
def dry_run_liquidation_price( def dry_run_liquidation_price(
self, self,
pair: str, pair: str,
open_rate: float, # Entry price of position open_rate: float, # Entry price of position
is_short: bool, is_short: bool,
amount: float, amount: float,
stake_amount: float, stake_amount: float,
@ -185,10 +183,8 @@ class Bybit(Exchange):
mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount) mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, stake_amount)
if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED: if self.trading_mode == TradingMode.FUTURES and self.margin_mode == MarginMode.ISOLATED:
if market["inverse"]:
if market['inverse']: raise OperationalException("Freqtrade does not yet support inverse contracts")
raise OperationalException(
"Freqtrade does not yet support inverse contracts")
initial_margin_rate = 1 / leverage initial_margin_rate = 1 / leverage
# See docstring - ignores extra margin! # See docstring - ignores extra margin!
@ -199,10 +195,12 @@ class Bybit(Exchange):
else: else:
raise OperationalException( raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading") "Freqtrade only supports isolated futures for leverage trading"
)
def get_funding_fees( 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 Fetch funding fees, either from the exchange (live) or calculates them
based on funding rate/mark price history based on funding rate/mark price history
@ -216,8 +214,7 @@ class Bybit(Exchange):
# Bybit does not provide "applied" funding fees per position. # Bybit does not provide "applied" funding fees per position.
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
try: try:
return self._fetch_and_calculate_funding_fees( return self._fetch_and_calculate_funding_fees(pair, amount, is_short, open_date)
pair, amount, is_short, open_date)
except ExchangeError: except ExchangeError:
logger.warning(f"Could not update funding fees for {pair}.") logger.warning(f"Could not update funding fees for {pair}.")
return 0.0 return 0.0
@ -234,7 +231,7 @@ class Bybit(Exchange):
while since < dt_now(): while since < dt_now():
until = since + timedelta(days=7, minutes=-1) 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 since = until
return orders return orders
@ -242,12 +239,12 @@ class Bybit(Exchange):
def fetch_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: def fetch_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
order = super().fetch_order(order_id, pair, params) order = super().fetch_order(order_id, pair, params)
if ( if (
order.get('status') == 'canceled' order.get("status") == "canceled"
and order.get('filled') == 0.0 and order.get("filled") == 0.0
and order.get('remaining') == 0.0 and order.get("remaining") == 0.0
): ):
# Canceled orders will have "remaining=0" on bybit. # Canceled orders will have "remaining=0" on bybit.
order['remaining'] = None order["remaining"] = None
return order return order
@retrier @retrier
@ -258,7 +255,7 @@ class Bybit(Exchange):
""" """
# Load cached tiers # 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: if tiers_cached:
tiers = tiers_cached tiers = tiers_cached
return tiers return tiers
@ -268,12 +265,12 @@ class Bybit(Exchange):
symbols = self._api.market_symbols([]) symbols = self._api.market_symbols([])
def parse_resp(response): def parse_resp(response):
result = self._api.safe_dict(response, 'result', {}) result = self._api.safe_dict(response, "result", {})
data = self._api.safe_list(result, 'list', []) data = self._api.safe_list(result, "list", [])
return self._api.parse_leverage_tiers(data, symbols, 'symbol') return self._api.parse_leverage_tiers(data, symbols, "symbol")
params = { params = {
'category': 'linear', "category": "linear",
} }
tiers = {} tiers = {}
# 20 pairs ... should be sufficient assuming 30 pairs per page # 20 pairs ... should be sufficient assuming 30 pairs per page
@ -282,11 +279,9 @@ class Bybit(Exchange):
# Fetch from private endpoint # Fetch from private endpoint
response = self._api.publicGetV5MarketRiskLimit(params) response = self._api.publicGetV5MarketRiskLimit(params)
tiers = tiers | parse_resp(response) tiers = tiers | parse_resp(response)
if (cursor := response['result']['nextPageCursor']) == '': if (cursor := response["result"]["nextPageCursor"]) == "":
break break
params.update({ params.update({"cursor": cursor})
"cursor": cursor
})
self.cache_leverage_tiers(tiers, self._config['stake_currency']) self.cache_leverage_tiers(tiers, self._config["stake_currency"])
return tiers return tiers

View File

@ -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. and thus is not known for the Freqtrade at all.
""" """
if (config['runmode'] in [RunMode.PLOT, RunMode.UTIL_NO_EXCHANGE, RunMode.OTHER] if config["runmode"] in [
and not config.get('exchange', {}).get('name')): 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 # Skip checking exchange in plot mode, since it requires no exchange
return True return True
logger.info("Checking exchange...") logger.info("Checking exchange...")
exchange = config.get('exchange', {}).get('name', '').lower() exchange = config.get("exchange", {}).get("name", "").lower()
if not exchange: if not exchange:
raise OperationalException( raise OperationalException(
f'This command requires a configured exchange. You should either use ' 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) valid, reason = validate_exchange(exchange)
if not valid: if not valid:
if check_for_bad: if check_for_bad:
raise OperationalException(f'Exchange "{exchange}" will not work with Freqtrade. ' raise OperationalException(
f'Reason: {reason}') f'Exchange "{exchange}" will not work with Freqtrade. ' f"Reason: {reason}"
)
else: else:
logger.warning(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}') logger.warning(f'Exchange "{exchange}" will not work with Freqtrade. Reason: {reason}')
if MAP_EXCHANGE_CHILDCLASS.get(exchange, exchange) in SUPPORTED_EXCHANGES: if MAP_EXCHANGE_CHILDCLASS.get(exchange, exchange) in SUPPORTED_EXCHANGES:
logger.info(f'Exchange "{exchange}" is officially supported ' logger.info(
f'by the Freqtrade development team.') f'Exchange "{exchange}" is officially supported ' f"by the Freqtrade development team."
)
else: else:
logger.warning(f'Exchange "{exchange}" is known to the ccxt library, ' logger.warning(
f'available for the bot, but not officially supported ' f'Exchange "{exchange}" is known to the ccxt library, '
f'by the Freqtrade development team. ' f"available for the bot, but not officially supported "
f'It may work flawlessly (please report back) or have serious issues. ' f"by the Freqtrade development team. "
f'Use it at your own discretion.') f"It may work flawlessly (please report back) or have serious issues. "
f"Use it at your own discretion."
)
return True return True

View File

@ -1,4 +1,5 @@
""" CoinbasePro exchange subclass """ """CoinbasePro exchange subclass"""
import logging import logging
from typing import Dict from typing import Dict

View File

@ -43,46 +43,48 @@ BAD_EXCHANGES = {
} }
MAP_EXCHANGE_CHILDCLASS = { MAP_EXCHANGE_CHILDCLASS = {
'binanceus': 'binance', "binanceus": "binance",
'binanceje': 'binance', "binanceje": "binance",
'binanceusdm': 'binance', "binanceusdm": "binance",
'okex': 'okx', "okex": "okx",
'gateio': 'gate', "gateio": "gate",
'huboi': 'htx', "huboi": "htx",
} }
SUPPORTED_EXCHANGES = [ SUPPORTED_EXCHANGES = [
'binance', "binance",
'bitmart', "bitmart",
'gate', "gate",
'htx', "htx",
'kraken', "kraken",
'okx', "okx",
] ]
# either the main, or replacement methods (array) is required # either the main, or replacement methods (array) is required
EXCHANGE_HAS_REQUIRED: Dict[str, List[str]] = { EXCHANGE_HAS_REQUIRED: Dict[str, List[str]] = {
# Required / private # Required / private
'fetchOrder': ['fetchOpenOrder', 'fetchClosedOrder'], "fetchOrder": ["fetchOpenOrder", "fetchClosedOrder"],
'cancelOrder': [], "cancelOrder": [],
'createOrder': [], "createOrder": [],
'fetchBalance': [], "fetchBalance": [],
# Public endpoints # Public endpoints
'fetchOHLCV': [], "fetchOHLCV": [],
} }
EXCHANGE_HAS_OPTIONAL = [ EXCHANGE_HAS_OPTIONAL = [
# Private # Private
'fetchMyTrades', # Trades for order - fee detection "fetchMyTrades", # Trades for order - fee detection
'createLimitOrder', 'createMarketOrder', # Either OR for orders "createLimitOrder",
"createMarketOrder", # Either OR for orders
# 'setLeverage', # Margin/Futures trading # 'setLeverage', # Margin/Futures trading
# 'setMarginMode', # Margin/Futures trading # 'setMarginMode', # Margin/Futures trading
# 'fetchFundingHistory', # Futures trading # 'fetchFundingHistory', # Futures trading
# Public # Public
'fetchOrderBook', 'fetchL2OrderBook', 'fetchTicker', # OR for pricing "fetchOrderBook",
'fetchTickers', # For volumepairlist? "fetchL2OrderBook",
'fetchTrades', # Downloading trades data "fetchTicker", # OR for pricing
"fetchTickers", # For volumepairlist?
"fetchTrades", # Downloading trades data
# 'fetchFundingRateHistory', # Futures trading # 'fetchFundingRateHistory', # Futures trading
# 'fetchPositions', # Futures trading # 'fetchPositions', # Futures trading
# 'fetchLeverageTiers', # Futures initialization # 'fetchLeverageTiers', # Futures initialization
@ -99,11 +101,11 @@ def remove_exchange_credentials(exchange_config: ExchangeConfig, dry_run: bool)
Modifies the input dict! Modifies the input dict!
""" """
if dry_run: if dry_run:
exchange_config['key'] = '' exchange_config["key"] = ""
exchange_config['apiKey'] = '' exchange_config["apiKey"] = ""
exchange_config['secret'] = '' exchange_config["secret"] = ""
exchange_config['password'] = '' exchange_config["password"] = ""
exchange_config['uid'] = '' exchange_config["uid"] = ""
def calculate_backoff(retrycount, max_retries): def calculate_backoff(retrycount, max_retries):
@ -115,25 +117,27 @@ def calculate_backoff(retrycount, max_retries):
def retrier_async(f): def retrier_async(f):
async def wrapper(*args, **kwargs): 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. kucoin = args[0].name == "KuCoin" # Check if the exchange is KuCoin.
try: try:
return await f(*args, **kwargs) return await f(*args, **kwargs)
except TemporaryError as ex: except TemporaryError as ex:
msg = f'{f.__name__}() returned exception: "{ex}". ' msg = f'{f.__name__}() returned exception: "{ex}". '
if count > 0: if count > 0:
msg += f'Retrying still for {count} times.' msg += f"Retrying still for {count} times."
count -= 1 count -= 1
kwargs['count'] = count kwargs["count"] = count
if isinstance(ex, DDosProtection): if isinstance(ex, DDosProtection):
if kucoin and "429000" in str(ex): if kucoin and "429000" in str(ex):
# Temporary fix for 429000 error on kucoin # Temporary fix for 429000 error on kucoin
# see https://github.com/freqtrade/freqtrade/issues/5700 for details. # see https://github.com/freqtrade/freqtrade/issues/5700 for details.
_get_logging_mixin().log_once( _get_logging_mixin().log_once(
f"Kucoin 429 error, avoid triggering DDosProtection backoff delay. " 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. # Reset msg to avoid logging too many times.
msg = '' msg = ""
else: else:
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT) backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}") logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
@ -142,38 +146,37 @@ def retrier_async(f):
logger.warning(msg) logger.warning(msg)
return await wrapper(*args, **kwargs) return await wrapper(*args, **kwargs)
else: else:
logger.warning(msg + 'Giving up.') logger.warning(msg + "Giving up.")
raise ex raise ex
return wrapper return wrapper
F = TypeVar('F', bound=Callable[..., Any]) F = TypeVar("F", bound=Callable[..., Any])
# Type shenanigans # Type shenanigans
@overload @overload
def retrier(_func: F) -> F: def retrier(_func: F) -> F: ...
...
@overload @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 retrier(_func: Optional[F] = None, *, retries=API_RETRY_COUNT):
def decorator(f: F) -> F: def decorator(f: F) -> F:
@wraps(f) @wraps(f)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
count = kwargs.pop('count', retries) count = kwargs.pop("count", retries)
try: try:
return f(*args, **kwargs) return f(*args, **kwargs)
except (TemporaryError, RetryableOrderError) as ex: except (TemporaryError, RetryableOrderError) as ex:
msg = f'{f.__name__}() returned exception: "{ex}". ' msg = f'{f.__name__}() returned exception: "{ex}". '
if count > 0: if count > 0:
logger.warning(msg + f'Retrying still for {count} times.') logger.warning(msg + f"Retrying still for {count} times.")
count -= 1 count -= 1
kwargs.update({'count': count}) kwargs.update({"count": count})
if isinstance(ex, (DDosProtection, RetryableOrderError)): if isinstance(ex, (DDosProtection, RetryableOrderError)):
# increasing backoff # increasing backoff
backoff_delay = calculate_backoff(count + 1, retries) 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) time.sleep(backoff_delay)
return wrapper(*args, **kwargs) return wrapper(*args, **kwargs)
else: else:
logger.warning(msg + 'Giving up.') logger.warning(msg + "Giving up.")
raise ex raise ex
return cast(F, wrapper) return cast(F, wrapper)
# Support both @retrier and @retrier(retries=2) syntax # Support both @retrier and @retrier(retries=2) syntax
if _func is None: if _func is None:
return decorator return decorator

View File

@ -1,6 +1,7 @@
""" """
Exchange support utils Exchange support utils
""" """
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from math import ceil, floor from math import ceil, floor
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
@ -32,7 +33,8 @@ CcxtModuleType = Any
def is_exchange_known_ccxt( 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) 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())() ex_mod = getattr(ccxt, exchange.lower())()
result = True result = True
reason = '' reason = ""
if not ex_mod or not ex_mod.has: if not ex_mod or not ex_mod.has:
return False, '' return False, ""
missing = [ missing = [
k for k, v in EXCHANGE_HAS_REQUIRED.items() k
if ex_mod.has.get(k) is not True for k, v in EXCHANGE_HAS_REQUIRED.items()
and not (all(ex_mod.has.get(x) for x in v)) if ex_mod.has.get(k) is not True and not (all(ex_mod.has.get(x) for x in v))
] ]
if missing: if missing:
result = False result = False
@ -74,7 +76,7 @@ def validate_exchange(exchange: str) -> Tuple[bool, str]:
if exchange.lower() in BAD_EXCHANGES: if exchange.lower() in BAD_EXCHANGES:
result = False result = False
reason = BAD_EXCHANGES.get(exchange.lower(), '') reason = BAD_EXCHANGES.get(exchange.lower(), "")
if missing_opt: if missing_opt:
reason += f"{'. ' if reason else ''}missing opt: {', '.join(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( 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) valid, comment = validate_exchange(exchange_name)
result: ValidExchangesType = { result: ValidExchangesType = {
'name': exchange_name, "name": exchange_name,
'valid': valid, "valid": valid,
'supported': exchange_name.lower() in SUPPORTED_EXCHANGES, "supported": exchange_name.lower() in SUPPORTED_EXCHANGES,
'comment': comment, "comment": comment,
'trade_modes': [{'trading_mode': 'spot', 'margin_mode': ''}], "trade_modes": [{"trading_mode": "spot", "margin_mode": ""}],
} }
if resolved := exchangeClasses.get(exchange_name.lower()): if resolved := exchangeClasses.get(exchange_name.lower()):
supported_modes = [{'trading_mode': 'spot', 'margin_mode': ''}] + [ supported_modes = [{"trading_mode": "spot", "margin_mode": ""}] + [
{'trading_mode': tm.value, 'margin_mode': mm.value} {"trading_mode": tm.value, "margin_mode": mm.value}
for tm, mm in resolved['class']._supported_trading_mode_margin_pairs for tm, mm in resolved["class"]._supported_trading_mode_margin_pairs
] ]
result.update({ result.update(
'trade_modes': supported_modes, {
}) "trade_modes": supported_modes,
}
)
return result return result
@ -111,7 +116,7 @@ def list_available_exchanges(all_exchanges: bool) -> List[ValidExchangesType]:
exchanges = ccxt_exchanges() if all_exchanges else available_exchanges() exchanges = ccxt_exchanges() if all_exchanges else available_exchanges()
from freqtrade.resolvers.exchange_resolver import ExchangeResolver 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] = [ exchanges_valid: List[ValidExchangesType] = [
_build_exchange_list_entry(e, subclassed) for e in exchanges _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( 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. subtract X candles from a date.
:param timeframe: timeframe in string format (e.g. "5m") :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% )" # 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, # See https://github.com/ccxt/ccxt/issues/4874,
# https://github.com/ccxt/ccxt/issues/4075#issuecomment-434760520 # 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: 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 return num_contracts
def amount_to_precision(amount: float, amount_precision: Optional[float], def amount_to_precision(
precisionMode: Optional[int]) -> float: amount: float, amount_precision: Optional[float], precisionMode: Optional[int]
) -> float:
""" """
Returns the amount to buy or sell to a precision the Exchange accepts 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 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: if amount_precision is not None and precisionMode is not None:
precision = int(amount_precision) if precisionMode != TICK_SIZE else amount_precision precision = int(amount_precision) if precisionMode != TICK_SIZE else amount_precision
# precision must be an int for non-ticksize inputs. # precision must be an int for non-ticksize inputs.
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE, amount = float(
precision=precision, decimal_to_precision(
counting_mode=precisionMode, amount,
)) rounding_mode=TRUNCATE,
precision=precision,
counting_mode=precisionMode,
)
)
return amount return amount
def amount_to_contract_precision( def amount_to_contract_precision(
amount, amount_precision: Optional[float], precisionMode: Optional[int], amount,
contract_size: Optional[float]) -> float: 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 Returns the amount to buy or sell to a precision the Exchange accepts
including calculation to and from contracts. 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_DOWN as dec_ROUND_DOWN
from decimal import ROUND_UP as dec_ROUND_UP from decimal import ROUND_UP as dec_ROUND_UP
from decimal import Decimal from decimal import Decimal
dec = Decimal(str(price)) dec = Decimal(str(price))
string = f'{dec:f}' string = f"{dec:f}"
precision = round(price_precision) precision = round(price_precision)
q = precision - dec.adjusted() - 1 q = precision - dec.adjusted() - 1
sigfig = Decimal('10') ** -q sigfig = Decimal("10") ** -q
if q < 0: if q < 0:
string_to_precision = string[:precision] string_to_precision = string[:precision]
# string_to_precision is '' when we have zero 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 above = below + sigfig
res = above if rounding_mode == ROUND_UP else below res = above if rounding_mode == ROUND_UP else below
precise = f'{res:f}' precise = f"{res:f}"
else: else:
precise = '{:f}'.format(dec.quantize( precise = "{:f}".format(
sigfig, dec.quantize(
rounding=dec_ROUND_DOWN if rounding_mode == ROUND_DOWN else dec_ROUND_UP) sigfig, rounding=dec_ROUND_DOWN if rounding_mode == ROUND_DOWN else dec_ROUND_UP
)
) )
return float(precise) return float(precise)
@ -280,10 +296,14 @@ def price_to_precision(
if price_precision is not None and precisionMode is not None: if price_precision is not None and precisionMode is not None:
if rounding_mode not in (ROUND_UP, ROUND_DOWN): if rounding_mode not in (ROUND_UP, ROUND_DOWN):
# Use CCXT code where possible. # Use CCXT code where possible.
return float(decimal_to_precision(price, rounding_mode=rounding_mode, return float(
precision=price_precision, decimal_to_precision(
counting_mode=precisionMode price,
)) rounding_mode=rounding_mode,
precision=price_precision,
counting_mode=precisionMode,
)
)
if precisionMode == TICK_SIZE: if precisionMode == TICK_SIZE:
precision = FtPrecise(price_precision) precision = FtPrecise(price_precision)
@ -297,7 +317,6 @@ def price_to_precision(
return round(float(str(res)), 14) return round(float(str(res)), 14)
return price return price
elif precisionMode == DECIMAL_PLACES: elif precisionMode == DECIMAL_PLACES:
ndigits = round(price_precision) ndigits = round(price_precision)
ticks = price * (10**ndigits) ticks = price * (10**ndigits)
if rounding_mode == ROUND_UP: if rounding_mode == ROUND_UP:

View File

@ -36,16 +36,16 @@ def timeframe_to_resample_freq(timeframe: str) -> str:
form ('1m', '5m', '1h', '1d', '1w', etc.) to the resample frequency form ('1m', '5m', '1h', '1d', '1w', etc.) to the resample frequency
used by pandas ('1T', '5T', '1H', '1D', '1W', etc.) used by pandas ('1T', '5T', '1H', '1D', '1W', etc.)
""" """
if timeframe == '1y': if timeframe == "1y":
return '1YS' return "1YS"
timeframe_seconds = timeframe_to_seconds(timeframe) timeframe_seconds = timeframe_to_seconds(timeframe)
timeframe_minutes = timeframe_seconds // 60 timeframe_minutes = timeframe_seconds // 60
resample_interval = f'{timeframe_seconds}s' resample_interval = f"{timeframe_seconds}s"
if 10000 < timeframe_minutes < 43200: if 10000 < timeframe_minutes < 43200:
resample_interval = '1W-MON' resample_interval = "1W-MON"
elif timeframe_minutes >= 43200 and timeframe_minutes < 525600: elif timeframe_minutes >= 43200 and timeframe_minutes < 525600:
# Monthly candles need special treatment to stick to the 1st of the month # 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: elif timeframe_minutes > 43200:
resample_interval = timeframe resample_interval = timeframe
return resample_interval return resample_interval
@ -62,8 +62,7 @@ def timeframe_to_prev_date(timeframe: str, date: Optional[datetime] = None) -> d
if not date: if not date:
date = datetime.now(timezone.utc) date = datetime.now(timezone.utc)
new_timestamp = ccxt.Exchange.round_timeframe( new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_DOWN) // 1000
timeframe, dt_ts(date), ROUND_DOWN) // 1000
return dt_from_ts(new_timestamp) 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: if not date:
date = datetime.now(timezone.utc) date = datetime.now(timezone.utc)
new_timestamp = ccxt.Exchange.round_timeframe( new_timestamp = ccxt.Exchange.round_timeframe(timeframe, dt_ts(date), ROUND_UP) // 1000
timeframe, dt_ts(date), ROUND_UP) // 1000
return dt_from_ts(new_timestamp) return dt_from_ts(new_timestamp)

View File

@ -1,4 +1,5 @@
""" Gate.io exchange subclass """ """Gate.io exchange subclass"""
import logging import logging
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
@ -24,7 +25,7 @@ class Gate(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"ohlcv_candle_limit": 1000, "ohlcv_candle_limit": 1000,
"order_time_in_force": ['GTC', 'IOC'], "order_time_in_force": ["GTC", "IOC"],
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit"}, "stoploss_order_types": {"limit": "limit"},
"stop_price_param": "stopPrice", "stop_price_param": "stopPrice",
@ -51,13 +52,13 @@ class Gate(Exchange):
] ]
def _get_params( def _get_params(
self, self,
side: BuySell, side: BuySell,
ordertype: str, ordertype: str,
leverage: float, leverage: float,
reduceOnly: bool, reduceOnly: bool,
time_in_force: str = 'GTC', time_in_force: str = "GTC",
) -> Dict: ) -> Dict:
params = super()._get_params( params = super()._get_params(
side=side, side=side,
ordertype=ordertype, ordertype=ordertype,
@ -65,13 +66,14 @@ class Gate(Exchange):
reduceOnly=reduceOnly, reduceOnly=reduceOnly,
time_in_force=time_in_force, time_in_force=time_in_force,
) )
if ordertype == 'market' and self.trading_mode == TradingMode.FUTURES: if ordertype == "market" and self.trading_mode == TradingMode.FUTURES:
params['type'] = 'market' params["type"] = "market"
params.update({'timeInForce': 'IOC'}) params.update({"timeInForce": "IOC"})
return params return params
def get_trades_for_order(self, order_id: str, pair: str, since: datetime, def get_trades_for_order(
params: Optional[Dict] = None) -> List: self, order_id: str, pair: str, since: datetime, params: Optional[Dict] = None
) -> List:
trades = super().get_trades_for_order(order_id, pair, since, params) trades = super().get_trades_for_order(order_id, pair, since, params)
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
@ -84,45 +86,38 @@ class Gate(Exchange):
pair_fees = self._trading_fees.get(pair, {}) pair_fees = self._trading_fees.get(pair, {})
if pair_fees: if pair_fees:
for idx, trade in enumerate(trades): for idx, trade in enumerate(trades):
fee = trade.get('fee', {}) fee = trade.get("fee", {})
if fee and fee.get('cost') is None: if fee and fee.get("cost") is None:
takerOrMaker = trade.get('takerOrMaker', 'taker') takerOrMaker = trade.get("takerOrMaker", "taker")
if pair_fees.get(takerOrMaker) is not None: if pair_fees.get(takerOrMaker) is not None:
trades[idx]['fee'] = { trades[idx]["fee"] = {
'currency': self.get_pair_quote_currency(pair), "currency": self.get_pair_quote_currency(pair),
'cost': trade['cost'] * pair_fees[takerOrMaker], "cost": trade["cost"] * pair_fees[takerOrMaker],
'rate': pair_fees[takerOrMaker], "rate": pair_fees[takerOrMaker],
} }
return trades return trades
def get_order_id_conditional(self, order: Dict[str, Any]) -> str: 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: def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict:
order = self.fetch_order( order = self.fetch_order(order_id=order_id, pair=pair, params={"stop": True})
order_id=order_id, if order.get("status", "open") == "closed":
pair=pair,
params={'stop': True}
)
if order.get('status', 'open') == 'closed':
# Places a real order - which we need to fetch explicitly. # 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 = self.fetch_order(order_id=new_orderid, pair=pair, params=params)
order1['id_stop'] = order1['id'] order1["id_stop"] = order1["id"]
order1['id'] = order_id order1["id"] = order_id
order1['type'] = 'stoploss' order1["type"] = "stoploss"
order1['stopPrice'] = order.get('stopPrice') order1["stopPrice"] = order.get("stopPrice")
order1['status_stop'] = 'triggered' order1["status_stop"] = "triggered"
return order1 return order1
return order return order
def cancel_stoploss_order( def cancel_stoploss_order(
self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: self, order_id: str, pair: str, params: Optional[Dict] = None
return self.cancel_order( ) -> Dict:
order_id=order_id, return self.cancel_order(order_id=order_id, pair=pair, params={"stop": True})
pair=pair,
params={'stop': True}
)

View File

@ -1,4 +1,5 @@
""" HTX exchange subclass """ """HTX exchange subclass"""
import logging import logging
from typing import Dict from typing import Dict
@ -26,10 +27,11 @@ class Htx(Exchange):
} }
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy() params = self._params.copy()
params.update({ params.update(
"stopPrice": stop_price, {
"operator": "lte", "stopPrice": stop_price,
}) "operator": "lte",
}
)
return params return params

View File

@ -1,4 +1,5 @@
""" Idex exchange subclass """ """Idex exchange subclass"""
import logging import logging
from typing import Dict from typing import Dict

View File

@ -1,4 +1,5 @@
""" Kraken exchange subclass """ """Kraken exchange subclass"""
import logging import logging
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
@ -18,7 +19,6 @@ logger = logging.getLogger(__name__)
class Kraken(Exchange): class Kraken(Exchange):
_params: Dict = {"trading_agreement": "agree"} _params: Dict = {"trading_agreement": "agree"}
_ft_has: Dict = { _ft_has: Dict = {
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
@ -47,18 +47,17 @@ class Kraken(Exchange):
""" """
parent_check = super().market_is_tradable(market) parent_check = super().market_is_tradable(market)
return (parent_check and return parent_check and market.get("darkpool", False) is False
market.get('darkpool', False) is False)
def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers: def get_tickers(self, symbols: Optional[List[str]] = None, cached: bool = False) -> Tickers:
# Only fetch tickers for current stake currency # Only fetch tickers for current stake currency
# Otherwise the request for kraken becomes too large. # 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) return super().get_tickers(symbols=symbols, cached=cached)
@retrier @retrier
def get_balances(self) -> dict: def get_balances(self) -> dict:
if self._config['dry_run']: if self._config["dry_run"]:
return {} return {}
try: try:
@ -70,23 +69,28 @@ class Kraken(Exchange):
balances.pop("used", None) balances.pop("used", None)
orders = self._api.fetch_open_orders() orders = self._api.fetch_open_orders()
order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1], order_list = [
x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"], (
# Don't remove the below comment, this can be important for debugging x["symbol"].split("/")[0 if x["side"] == "sell" else 1],
# x["side"], x["amount"], x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"],
) for x in orders] # Don't remove the below comment, this can be important for debugging
# x["side"], x["amount"],
)
for x in orders
]
for bal in balances: for bal in balances:
if not isinstance(balances[bal], dict): if not isinstance(balances[bal], dict):
continue continue
balances[bal]['used'] = sum(order[1] for order in order_list if order[0] == bal) 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]["free"] = balances[bal]["total"] - balances[bal]["used"]
return balances return balances
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e: except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError( 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: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
@ -108,7 +112,7 @@ class Kraken(Exchange):
ordertype: str, ordertype: str,
leverage: float, leverage: float,
reduceOnly: bool, reduceOnly: bool,
time_in_force: str = 'GTC' time_in_force: str = "GTC",
) -> Dict: ) -> Dict:
params = super()._get_params( params = super()._get_params(
side=side, side=side,
@ -118,10 +122,10 @@ class Kraken(Exchange):
time_in_force=time_in_force, time_in_force=time_in_force,
) )
if leverage > 1.0: if leverage > 1.0:
params['leverage'] = round(leverage) params["leverage"] = round(leverage)
if time_in_force == 'PO': if time_in_force == "PO":
params.pop('timeInForce', None) params.pop("timeInForce", None)
params['postOnly'] = True params["postOnly"] = True
return params return params
def calculate_funding_fees( def calculate_funding_fees(
@ -131,7 +135,7 @@ class Kraken(Exchange):
is_short: bool, is_short: bool,
open_date: datetime, open_date: datetime,
close_date: datetime, close_date: datetime,
time_in_ratio: Optional[float] = None time_in_ratio: Optional[float] = None,
) -> float: ) -> float:
""" """
# ! This method will always error when run by Freqtrade because time_in_ratio is never # ! 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: if not time_in_ratio:
raise OperationalException( 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 fees: float = 0
if not df.empty: if not df.empty:
df = df[(df['date'] >= open_date) & (df['date'] <= close_date)] df = df[(df["date"] >= open_date) & (df["date"] <= close_date)]
fees = sum(df['open_fund'] * df['open_mark'] * amount * time_in_ratio) fees = sum(df["open_fund"] * df["open_mark"] * amount * time_in_ratio)
return fees if is_short else -fees return fees if is_short else -fees
@ -164,14 +169,11 @@ class Kraken(Exchange):
Applies only to fetch_trade_history by id. Applies only to fetch_trade_history by id.
""" """
if len(trades) > 0: if len(trades) > 0:
if ( if isinstance(trades[-1].get("info"), list) and len(trades[-1].get("info", [])) > 7:
isinstance(trades[-1].get('info'), list)
and len(trades[-1].get('info', [])) > 7
):
# Trade response's "last" value. # 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. # Fall back to timestamp if info is somehow empty.
return trades[-1].get('timestamp') return trades[-1].get("timestamp")
return None return None
def _valid_trade_pagination_id(self, pair: str, from_id: str) -> bool: def _valid_trade_pagination_id(self, pair: str, from_id: str) -> bool:

View File

@ -1,4 +1,5 @@
"""Kucoin exchange subclass.""" """Kucoin exchange subclass."""
import logging import logging
from typing import Dict from typing import Dict
@ -26,32 +27,27 @@ class Kucoin(Exchange):
"stoploss_order_types": {"limit": "limit", "market": "market"}, "stoploss_order_types": {"limit": "limit", "market": "market"},
"l2_limit_range": [20, 100], "l2_limit_range": [20, 100],
"l2_limit_range_required": False, "l2_limit_range_required": False,
"order_time_in_force": ['GTC', 'FOK', 'IOC'], "order_time_in_force": ["GTC", "FOK", "IOC"],
"ohlcv_candle_limit": 1500, "ohlcv_candle_limit": 1500,
} }
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy() params = self._params.copy()
params.update({ params.update({"stopPrice": stop_price, "stop": "loss"})
'stopPrice': stop_price,
'stop': 'loss'
})
return params return params
def create_order( def create_order(
self, self,
*, *,
pair: str, pair: str,
ordertype: str, ordertype: str,
side: BuySell, side: BuySell,
amount: float, amount: float,
rate: float, rate: float,
leverage: float, leverage: float,
reduceOnly: bool = False, reduceOnly: bool = False,
time_in_force: str = 'GTC', time_in_force: str = "GTC",
) -> Dict: ) -> Dict:
res = super().create_order( res = super().create_order(
pair=pair, pair=pair,
ordertype=ordertype, ordertype=ordertype,
@ -66,7 +62,7 @@ class Kucoin(Exchange):
# ccxt returns status = 'closed' at the moment - which is information ccxt invented. # 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. # 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) # ref: https://github.com/ccxt/ccxt/pull/16674, (https://github.com/ccxt/ccxt/pull/16553)
if not self._config['dry_run']: if not self._config["dry_run"]:
res['type'] = ordertype res["type"] = ordertype
res['status'] = 'open' res["status"] = "open"
return res return res

View File

@ -41,7 +41,7 @@ class Okx(Exchange):
PriceType.LAST: "last", PriceType.LAST: "last",
PriceType.MARK: "index", PriceType.MARK: "index",
PriceType.INDEX: "mark", PriceType.INDEX: "mark",
}, },
} }
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
@ -53,10 +53,11 @@ class Okx(Exchange):
net_only = True net_only = True
_ccxt_params: Dict = {'options': {'brokerId': 'ffb5405ad327SUDE'}} _ccxt_params: Dict = {"options": {"brokerId": "ffb5405ad327SUDE"}}
def ohlcv_candle_limit( 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 Exchange ohlcv candle limit
OKX has the following behaviour: OKX has the following behaviour:
@ -68,9 +69,8 @@ class Okx(Exchange):
:param since_ms: Starting timestamp :param since_ms: Starting timestamp
:return: Candle limit as integer :return: Candle limit as integer
""" """
if ( if candle_type in (CandleType.FUTURES, CandleType.SPOT) and (
candle_type in (CandleType.FUTURES, CandleType.SPOT) and not since_ms or since_ms > (date_minus_candles(timeframe, 300).timestamp() * 1000)
(not since_ms or since_ms > (date_minus_candles(timeframe, 300).timestamp() * 1000))
): ):
return 300 return 300
@ -84,29 +84,29 @@ class Okx(Exchange):
Must be overridden in child methods if required. Must be overridden in child methods if required.
""" """
try: 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() accounts = self._api.fetch_accounts()
self._log_exchange_response('fetch_accounts', accounts) self._log_exchange_response("fetch_accounts", accounts)
if len(accounts) > 0: 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: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.OperationFailed, ccxt.ExchangeError) as e: except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}' f"Error in additional_exchange_init due to {e.__class__.__name__}. Message: {e}"
) from e ) from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def _get_posSide(self, side: BuySell, reduceOnly: bool): def _get_posSide(self, side: BuySell, reduceOnly: bool):
if self.net_only: if self.net_only:
return 'net' return "net"
if not reduceOnly: if not reduceOnly:
# Enter # Enter
return 'long' if side == 'buy' else 'short' return "long" if side == "buy" else "short"
else: else:
# Exit # Exit
return 'long' if side == 'sell' else 'short' return "long" if side == "sell" else "short"
def _get_params( def _get_params(
self, self,
@ -114,7 +114,7 @@ class Okx(Exchange):
ordertype: str, ordertype: str,
leverage: float, leverage: float,
reduceOnly: bool, reduceOnly: bool,
time_in_force: str = 'GTC', time_in_force: str = "GTC",
) -> Dict: ) -> Dict:
params = super()._get_params( params = super()._get_params(
side=side, side=side,
@ -124,18 +124,21 @@ class Okx(Exchange):
time_in_force=time_in_force, time_in_force=time_in_force,
) )
if self.trading_mode == TradingMode.FUTURES and self.margin_mode: if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['tdMode'] = self.margin_mode.value params["tdMode"] = self.margin_mode.value
params['posSide'] = self._get_posSide(side, reduceOnly) params["posSide"] = self._get_posSide(side, reduceOnly)
return params return params
def __fetch_leverage_already_set(self, pair: str, leverage: float, side: BuySell) -> bool: def __fetch_leverage_already_set(self, pair: str, leverage: float, side: BuySell) -> bool:
try: try:
res_lev = self._api.fetch_leverage(symbol=pair, params={ res_lev = self._api.fetch_leverage(
symbol=pair,
params={
"mgnMode": self.margin_mode.value, "mgnMode": self.margin_mode.value,
"posSide": self._get_posSide(side, False), "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 return already_set
except ccxt.BaseError: except ccxt.BaseError:
@ -152,8 +155,9 @@ class Okx(Exchange):
params={ params={
"mgnMode": self.margin_mode.value, "mgnMode": self.margin_mode.value,
"posSide": self._get_posSide(side, False), "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: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
@ -161,84 +165,81 @@ class Okx(Exchange):
already_set = self.__fetch_leverage_already_set(pair, leverage, side) already_set = self.__fetch_leverage_already_set(pair, leverage, side)
if not already_set: if not already_set:
raise TemporaryError( raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}' f"Could not set leverage due to {e.__class__.__name__}. Message: {e}"
) from e ) from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def get_max_pair_stake_amount( def get_max_pair_stake_amount(self, pair: str, price: float, leverage: float = 1.0) -> float:
self,
pair: str,
price: float,
leverage: float = 1.0
) -> float:
if self.trading_mode == TradingMode.SPOT: 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: if pair not in self._leverage_tiers:
return float('inf') return float("inf")
pair_tiers = self._leverage_tiers[pair] 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: def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
params = super()._get_stop_params(side, ordertype, stop_price) params = super()._get_stop_params(side, ordertype, stop_price)
if self.trading_mode == TradingMode.FUTURES and self.margin_mode: if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['tdMode'] = self.margin_mode.value params["tdMode"] = self.margin_mode.value
params['posSide'] = self._get_posSide(side, True) params["posSide"] = self._get_posSide(side, True)
return params return params
def _convert_stop_order(self, pair: str, order_id: str, order: Dict) -> Dict: def _convert_stop_order(self, pair: str, order_id: str, order: Dict) -> Dict:
if ( if (
order.get('status', 'open') == 'closed' order.get("status", "open") == "closed"
and (real_order_id := order.get('info', {}).get('ordId')) is not None and (real_order_id := order.get("info", {}).get("ordId")) is not None
): ):
# Once a order triggered, we fetch the regular followup order. # Once a order triggered, we fetch the regular followup order.
order_reg = self.fetch_order(real_order_id, pair) order_reg = self.fetch_order(real_order_id, pair)
self._log_exchange_response('fetch_stoploss_order1', order_reg) self._log_exchange_response("fetch_stoploss_order1", order_reg)
order_reg['id_stop'] = order_reg['id'] order_reg["id_stop"] = order_reg["id"]
order_reg['id'] = order_id order_reg["id"] = order_id
order_reg['type'] = 'stoploss' order_reg["type"] = "stoploss"
order_reg['status_stop'] = 'triggered' order_reg["status_stop"] = "triggered"
return order_reg return order_reg
order = self._order_contracts_to_amount(order) order = self._order_contracts_to_amount(order)
order['type'] = 'stoploss' order["type"] = "stoploss"
return order return order
def fetch_stoploss_order(self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: 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) return self.fetch_dry_run_order(order_id)
try: try:
params1 = {'stop': True} params1 = {"stop": True}
order_reg = self._api.fetch_order(order_id, pair, params=params1) 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) return self._convert_stop_order(pair, order_id, order_reg)
except ccxt.OrderNotFound: except ccxt.OrderNotFound:
pass pass
params2 = {'stop': True, 'ordType': 'conditional'} params2 = {"stop": True, "ordType": "conditional"}
for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders, for method in (
self._api.fetch_canceled_orders): self._api.fetch_open_orders,
self._api.fetch_closed_orders,
self._api.fetch_canceled_orders,
):
try: try:
orders = method(pair, params=params2) 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: if orders_f:
order = orders_f[0] order = orders_f[0]
return self._convert_stop_order(pair, order_id, order) return self._convert_stop_order(pair, order_id, order)
except ccxt.BaseError: except ccxt.BaseError:
pass pass
raise RetryableOrderError( raise RetryableOrderError(f"StoplossOrder not found (pair: {pair} id: {order_id}).")
f'StoplossOrder not found (pair: {pair} id: {order_id}).')
def get_order_id_conditional(self, order: Dict[str, Any]) -> str: def get_order_id_conditional(self, order: Dict[str, Any]) -> str:
if order.get('type', '') == 'stop': if order.get("type", "") == "stop":
return safe_value_fallback2(order, order, 'id_stop', 'id') return safe_value_fallback2(order, order, "id_stop", "id")
return order['id'] return order["id"]
def cancel_stoploss_order( def cancel_stoploss_order(
self, order_id: str, pair: str, params: Optional[Dict] = None) -> Dict: self, order_id: str, pair: str, params: Optional[Dict] = None
params1 = {'stop': True} ) -> Dict:
params1 = {"stop": True}
# 'ordType': 'conditional' # 'ordType': 'conditional'
# #
return self.cancel_order( return self.cancel_order(
@ -251,10 +252,10 @@ class Okx(Exchange):
orders = [] orders = []
orders = self._api.fetch_closed_orders(pair, since=since_ms) 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. # Regular fetch_closed_orders only returns 7 days of data.
# Force usage of "archive" endpoint, which returns 3 months 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_hist = self._api.fetch_closed_orders(pair, since=since_ms, params=params)
orders.extend(orders_hist) orders.extend(orders_hist)