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
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

View File

@ -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,35 +67,43 @@ 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}'
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
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
@ -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,
@ -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:

View File

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

View File

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

View File

@ -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)

View File

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

View File

@ -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}'
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,7 +134,7 @@ 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(
@ -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

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.
"""
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

View File

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

View File

@ -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

View File

@ -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,
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,
return float(
decimal_to_precision(
price,
rounding_mode=rounding_mode,
precision=price_precision,
counting_mode=precisionMode
))
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:

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
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)

View File

@ -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",
@ -56,7 +57,7 @@ class Gate(Exchange):
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'GTC',
time_in_force: str = "GTC",
) -> Dict:
params = super()._get_params(
side=side,
@ -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})

View File

@ -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({
params.update(
{
"stopPrice": stop_price,
"operator": "lte",
})
}
)
return params

View File

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

View File

@ -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],
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 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:

View File

@ -1,4 +1,5 @@
"""Kucoin exchange subclass."""
import logging
from typing import Dict
@ -26,17 +27,13 @@ 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(
@ -49,9 +46,8 @@ class Kucoin(Exchange):
rate: float,
leverage: float,
reduceOnly: bool = False,
time_in_force: str = 'GTC',
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

View File

@ -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}'
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}'
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)