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