mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 02:12:01 +00:00
ruff format: exchange classes
This commit is contained in:
parent
7ea5e40919
commit
53eefb9442
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" Bingx exchange subclass """
|
||||
"""Bingx exchange subclass"""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" Bitmart exchange subclass """
|
||||
"""Bitmart exchange subclass"""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Kucoin exchange subclass."""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" CoinbasePro exchange subclass """
|
||||
"""CoinbasePro exchange subclass"""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" Idex exchange subclass """
|
||||
"""Idex exchange subclass"""
|
||||
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user