Merge pull request #6373 from samgermain/leverage-tiers

Leverage tiers
This commit is contained in:
Matthias 2022-02-17 20:23:33 +01:00 committed by GitHub
commit f0cbc47bb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 18578 additions and 1662 deletions

View File

@ -128,91 +128,6 @@ class Binance(Exchange):
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier
def fill_leverage_brackets(self) -> None:
"""
Assigns property _leverage_brackets to a dictionary of information about the leverage
allowed on each pair
After exectution, self._leverage_brackets = {
"pair_name": [
[notional_floor, maintenenace_margin_ratio, maintenance_amt],
...
],
...
}
e.g. {
"ETH/USDT:USDT": [
[0.0, 0.01, 0.0],
[10000, 0.02, 0.01],
...
],
...
}
"""
if self.trading_mode == TradingMode.FUTURES:
try:
if self._config['dry_run']:
leverage_brackets_path = (
Path(__file__).parent / 'binance_leverage_brackets.json'
)
with open(leverage_brackets_path) as json_file:
leverage_brackets = json.load(json_file)
else:
leverage_brackets = self._api.load_leverage_brackets()
for pair, brkts in leverage_brackets.items():
[amt, old_ratio] = [0.0, 0.0]
brackets = []
for [notional_floor, mm_ratio] in brkts:
amt = (
(float(notional_floor) * (float(mm_ratio) - float(old_ratio)))
+ amt
) if old_ratio else 0.0
old_ratio = mm_ratio
brackets.append([
float(notional_floor),
float(mm_ratio),
amt,
])
self._leverage_brackets[pair] = brackets
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as 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
def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float:
"""
Returns the maximum leverage that a pair can be traded at
:param pair: The base/quote currency pair being traded
:stake_amount: The total value of the traders margin_mode in quote currency
"""
if stake_amount is None:
raise OperationalException('binance.get_max_leverage requires argument stake_amount')
if pair not in self._leverage_brackets:
return 1.0
pair_brackets = self._leverage_brackets[pair]
num_brackets = len(pair_brackets)
min_amount = 0.0
for bracket_num in range(num_brackets):
[notional_floor, mm_ratio, _] = pair_brackets[bracket_num]
lev = 1.0
if mm_ratio != 0:
lev = 1.0/mm_ratio
else:
logger.warning(f"mm_ratio for {pair} with notional floor {notional_floor} is 0")
if bracket_num+1 != num_brackets: # If not on last bracket
[min_amount, _, __] = pair_brackets[bracket_num+1] # Get min_amount of next bracket
else:
return lev
nominal_value = stake_amount * lev
# Bracket is good if the leveraged trade value doesnt exceed min_amount of next bracket
if nominal_value < min_amount:
return lev
return 1.0 # default leverage
@retrier @retrier
def _set_leverage( def _set_leverage(
self, self,
@ -272,34 +187,6 @@ class Binance(Exchange):
""" """
return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15) return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15)
def get_maintenance_ratio_and_amt(
self,
pair: str,
nominal_value: Optional[float] = 0.0,
) -> Tuple[float, Optional[float]]:
"""
Formula: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
Maintenance amt = Floor of Position Bracket on Level n *
difference between
Maintenance Margin Rate on Level n and
Maintenance Margin Rate on Level n-1)
+ Maintenance Amount on Level n-1
:return: The maintenance margin ratio and maintenance amount
"""
if nominal_value is None:
raise OperationalException(
"nominal value is required for binance.get_maintenance_ratio_and_amt")
if pair not in self._leverage_brackets:
raise InvalidOrderException(f"Cannot calculate liquidation price for {pair}")
pair_brackets = self._leverage_brackets[pair]
for [notional_floor, mm_ratio, amt] in reversed(pair_brackets):
if nominal_value >= notional_floor:
return (mm_ratio, amt)
raise OperationalException("nominal value can not be lower than 0")
# The lowest notional_floor for any pair in loadLeverageBrackets is always 0 because it
# describes the min amount for a bracket, and the lowest bracket will always go down to 0
def dry_run_liquidation_price( def dry_run_liquidation_price(
self, self,
pair: str, pair: str,
@ -358,3 +245,25 @@ class Binance(Exchange):
else: else:
raise OperationalException( 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'
)
with open(leverage_tiers_path) as json_file:
return json.load(json_file)
else:
try:
return self._api.fetch_leverage_tiers()
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as 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:
return {}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -73,7 +73,8 @@ class Exchange:
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
"mark_ohlcv_price": "mark", "mark_ohlcv_price": "mark",
"mark_ohlcv_timeframe": "8h", "mark_ohlcv_timeframe": "8h",
"ccxt_futures_name": "swap" "ccxt_futures_name": "swap",
"can_fetch_multiple_tiers": True,
} }
_ft_has: Dict = {} _ft_has: Dict = {}
@ -90,7 +91,7 @@ class Exchange:
self._api: ccxt.Exchange = None self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None self._api_async: ccxt_async.Exchange = None
self._markets: Dict = {} self._markets: Dict = {}
self._leverage_brackets: Dict[str, List[List[float]]] = {} self._leverage_tiers: Dict[str, List[Dict]] = {}
self.loop = asyncio.new_event_loop() self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop) asyncio.set_event_loop(self.loop)
@ -183,7 +184,7 @@ class Exchange:
"markets_refresh_interval", 60) * 60 "markets_refresh_interval", 60) * 60
if self.trading_mode != TradingMode.SPOT: if self.trading_mode != TradingMode.SPOT:
self.fill_leverage_brackets() self.fill_leverage_tiers()
def __del__(self): def __del__(self):
""" """
@ -346,7 +347,10 @@ class Exchange:
return self.markets.get(pair, {}).get('base', '') return self.markets.get(pair, {}).get('base', '')
def market_is_future(self, market: Dict[str, Any]) -> bool: def market_is_future(self, market: Dict[str, Any]) -> bool:
return market.get(self._ft_has["ccxt_futures_name"], False) is True return (
market.get(self._ft_has["ccxt_futures_name"], False) is True and
market.get('linear', False) is True
)
def market_is_spot(self, market: Dict[str, Any]) -> bool: def market_is_spot(self, market: Dict[str, Any]) -> bool:
return market.get('spot', False) is True return market.get('spot', False) is True
@ -459,7 +463,7 @@ class Exchange:
# Also reload async markets to avoid issues with newly listed pairs # Also reload async markets to avoid issues with newly listed pairs
self._load_async_markets(reload=True) self._load_async_markets(reload=True)
self._last_markets_refresh = arrow.utcnow().int_timestamp self._last_markets_refresh = arrow.utcnow().int_timestamp
self.fill_leverage_brackets() self.fill_leverage_tiers()
except ccxt.BaseError: except ccxt.BaseError:
logger.exception("Could not reload markets.") logger.exception("Could not reload markets.")
@ -691,13 +695,14 @@ class Exchange:
self, self,
pair: str, pair: str,
price: float, price: float,
leverage: float = 1.0
) -> float: ) -> float:
max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max') max_stake_amount = self._get_stake_amount_limit(pair, price, 0.0, 'max')
if max_stake_amount is None: if max_stake_amount is None:
# * Should never be executed # * Should never be executed
raise OperationalException(f'{self.name}.get_max_pair_stake_amount should' raise OperationalException(f'{self.name}.get_max_pair_stake_amount should'
'never set max_stake_amount to None') 'never set max_stake_amount to None')
return max_stake_amount return max_stake_amount / leverage
def _get_stake_amount_limit( def _get_stake_amount_limit(
self, self,
@ -1852,23 +1857,117 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def fill_leverage_brackets(self): @retrier
def load_leverage_tiers(self) -> Dict[str, List[Dict]]:
if self.trading_mode == TradingMode.FUTURES and self.exchange_has('fetchLeverageTiers'):
try:
return self._api.fetch_leverage_tiers()
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load leverage tiers due to {e.__class__.__name__}.'
f'Message: {e}'
) from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
else:
return {}
def fill_leverage_tiers(self) -> None:
""" """
Assigns property _leverage_brackets to a dictionary of information about the leverage Assigns property _leverage_tiers to a dictionary of information about the leverage
allowed on each pair allowed on each pair
Not used if the exchange has a static max leverage value for the account or each pair
""" """
return leverage_tiers = self.load_leverage_tiers()
for pair, tiers in leverage_tiers.items():
pair_tiers = []
for tier in tiers:
pair_tiers.append(self.parse_leverage_tier(tier))
self._leverage_tiers[pair] = pair_tiers
def parse_leverage_tier(self, tier) -> Dict:
info = tier.get('info', {})
return {
'min': tier['notionalFloor'],
'max': tier['notionalCap'],
'mmr': tier['maintenanceMarginRate'],
'lev': tier['maxLeverage'],
'maintAmt': float(info['cum']) if 'cum' in info else None,
}
def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float: def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float:
""" """
Returns the maximum leverage that a pair can be traded at Returns the maximum leverage that a pair can be traded at
:param pair: The base/quote currency pair being traded :param pair: The base/quote currency pair being traded
:param nominal_value: The total value of the trade in quote currency (margin_mode + debt) :stake_amount: The total value of the traders margin_mode in quote currency
""" """
if self.trading_mode == TradingMode.SPOT:
return 1.0
if self.trading_mode == TradingMode.FUTURES:
# Checks and edge cases
if stake_amount is None:
raise OperationalException(
f'{self.name}.get_max_leverage requires argument stake_amount'
)
if pair not in self._leverage_tiers:
# Maybe raise exception because it can't be traded on futures?
return 1.0
pair_tiers = self._leverage_tiers[pair]
if stake_amount == 0:
return self._leverage_tiers[pair][0]['lev'] # Max lev for lowest amount
for tier_index in range(len(pair_tiers)):
tier = pair_tiers[tier_index]
lev = tier['lev']
if tier_index < len(pair_tiers) - 1:
next_tier = pair_tiers[tier_index+1]
next_floor = next_tier['min'] / next_tier['lev']
if next_floor > stake_amount: # Next tier min too high for stake amount
return min((tier['max'] / stake_amount), lev)
#
# With the two leverage tiers below,
# - a stake amount of 150 would mean a max leverage of (10000 / 150) = 66.66
# - stakes below 133.33 = max_lev of 75
# - stakes between 133.33-200 = max_lev of 10000/stake = 50.01-74.99
# - stakes from 200 + 1000 = max_lev of 50
#
# {
# "min": 0, # stake = 0.0
# "max": 10000, # max_stake@75 = 10000/75 = 133.33333333333334
# "lev": 75,
# },
# {
# "min": 10000, # stake = 200.0
# "max": 50000, # max_stake@50 = 50000/50 = 1000.0
# "lev": 50,
# }
#
else: # if on the last tier
if stake_amount > tier['max']: # If stake is > than max tradeable amount
raise InvalidOrderException(f'Amount {stake_amount} too high for {pair}')
else:
return tier['lev']
raise OperationalException(
'Looped through all tiers without finding a max leverage. Should never be reached'
)
elif self.trading_mode == TradingMode.MARGIN: # Search markets.limits for max lev
market = self.markets[pair] market = self.markets[pair]
if market['limits']['leverage']['max'] is not None: if market['limits']['leverage']['max'] is not None:
return market['limits']['leverage']['max'] return market['limits']['leverage']['max']
else:
return 1.0 # Default if max leverage cannot be found
else: else:
return 1.0 return 1.0
@ -2098,16 +2197,6 @@ class Exchange:
else: else:
return None return None
def get_maintenance_ratio_and_amt(
self,
pair: str,
nominal_value: Optional[float] = 0.0,
) -> Tuple[float, Optional[float]]:
"""
:return: The maintenance margin ratio and maintenance amount
"""
raise OperationalException(self.name + ' does not support leverage futures trading')
def dry_run_liquidation_price( def dry_run_liquidation_price(
self, self,
pair: str, pair: str,
@ -2160,6 +2249,37 @@ class Exchange:
raise OperationalException( raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading") "Freqtrade only supports isolated futures for leverage trading")
def get_maintenance_ratio_and_amt(
self,
pair: str,
nominal_value: float = 0.0,
) -> Tuple[float, Optional[float]]:
"""
:param pair: Market symbol
:param nominal_value: The total trade amount in quote currency including leverage
maintenance amount only on Binance
:return: (maintenance margin ratio, maintenance amount)
"""
if self.exchange_has('fetchLeverageTiers'):
if pair not in self._leverage_tiers:
raise InvalidOrderException(
f"Maintenance margin rate for {pair} is unavailable for {self.name}"
)
pair_tiers = self._leverage_tiers[pair]
for tier in reversed(pair_tiers):
if nominal_value >= tier['min']:
return (tier['mmr'], tier['maintAmt'])
raise OperationalException("nominal value can not be lower than 0")
# The lowest notional_floor for any pair in fetch_leverage_tiers is always 0 because it
# describes the min amt for a tier, and the lowest tier will always go down to 0
else:
raise OperationalException(f"Cannot get maintenance ratio using {self.name}")
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
return exchange_name in ccxt_exchanges(ccxt_module) return exchange_name in ccxt_exchanges(ccxt_module)

View File

@ -51,3 +51,15 @@ class Gateio(Exchange):
""" """
info = self.markets[pair]['info'] info = self.markets[pair]['info']
return (float(info['maintenance_rate']), None) return (float(info['maintenance_rate']), None)
def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float:
"""
Returns the maximum leverage that a pair can be traded at
:param pair: The base/quote currency pair being traded
:param nominal_value: The total value of the trade in quote currency (margin_mode + debt)
"""
market = self.markets[pair]
if market['limits']['leverage']['max'] is not None:
return market['limits']['leverage']['max']
else:
return 1.0

View File

@ -1,9 +1,12 @@
import logging import logging
from typing import Dict, List, Tuple from typing import Dict, List, Tuple
import ccxt
from freqtrade.enums import MarginMode, TradingMode from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -19,15 +22,34 @@ class Okx(Exchange):
"ohlcv_candle_limit": 300, "ohlcv_candle_limit": 300,
"mark_ohlcv_timeframe": "4h", "mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h", "funding_fee_timeframe": "8h",
"can_fetch_multiple_tiers": False,
} }
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list # TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, MarginMode.CROSS), # (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS), # (TradingMode.FUTURES, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.ISOLATED) (TradingMode.FUTURES, MarginMode.ISOLATED),
] ]
def _get_params(
self,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'gtc',
) -> Dict:
params = super()._get_params(
ordertype=ordertype,
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
)
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['tdMode'] = self.margin_mode.value
return params
@retrier
def _lev_prep( def _lev_prep(
self, self,
pair: str, pair: str,
@ -39,10 +61,63 @@ class Okx(Exchange):
raise OperationalException( raise OperationalException(
f"{self.name}.margin_mode must be set for {self.trading_mode.value}" f"{self.name}.margin_mode must be set for {self.trading_mode.value}"
) )
try:
# TODO-lev: Test me properly (check mgnMode passed)
self._api.set_leverage( self._api.set_leverage(
leverage, leverage=leverage,
pair, symbol=pair,
params={ params={
"mgnMode": self.margin_mode.value, "mgnMode": self.margin_mode.value,
"posSide": "long" if side == "buy" else "short", # "posSide": "net"",
}) })
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
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:
if self.trading_mode == TradingMode.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')
pair_tiers = self._leverage_tiers[pair]
return pair_tiers[-1]['max'] / leverage
@retrier
def load_leverage_tiers(self) -> Dict[str, List[Dict]]:
# * This is slow(~45s) on Okex, must make 90-some api calls to load all linear swap markets
if self.trading_mode == TradingMode.FUTURES:
markets = self.markets
symbols = []
for symbol, market in markets.items():
if (self.market_is_future(market)
and market['quote'] == self._config['stake_currency']):
symbols.append(symbol)
tiers: Dict[str, List[Dict]] = {}
# Be verbose here, as this delays startup by ~1 minute.
logger.info(
f"Initializing leverage_tiers for {len(symbols)} markets. "
"This will take about a minute.")
for symbol in sorted(symbols):
res = self._api.fetch_leverage_tiers(symbol)
tiers[symbol] = res[symbol]
logger.info(f"Done initializing {len(symbols)} markets.")
return tiers
else:
return {}

View File

@ -2,7 +2,7 @@ numpy==1.22.2
pandas==1.4.0 pandas==1.4.0
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==1.72.36 ccxt==1.73.17
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==36.0.1 cryptography==36.0.1
aiohttp==3.8.1 aiohttp==3.8.1

View File

@ -42,7 +42,7 @@ setup(
], ],
install_requires=[ install_requires=[
# from requirements.txt # from requirements.txt
'ccxt>=1.72.29', 'ccxt>=1.73.1',
'SQLAlchemy', 'SQLAlchemy',
'python-telegram-bot>=13.4', 'python-telegram-bot>=13.4',
'arrow>=0.17.0', 'arrow>=0.17.0',

View File

@ -231,9 +231,9 @@ def test_list_markets(mocker, markets_static, capsys):
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 10 active markets: " assert ("Exchange Bittrex has 12 active markets: "
"BLK/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, NEO/BTC, " "ADA/USDT:USDT, BLK/BTC, ETH/BTC, ETH/USDT, ETH/USDT:USDT, LTC/BTC, "
"TKN/BTC, XLTCUSDT, XRP/BTC.\n" "LTC/ETH, LTC/USD, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n"
in captured.out) in captured.out)
patch_exchange(mocker, api_mock=api_mock, id="binance", mock_markets=markets_static) patch_exchange(mocker, api_mock=api_mock, id="binance", mock_markets=markets_static)
@ -246,7 +246,7 @@ def test_list_markets(mocker, markets_static, capsys):
pargs['config'] = None pargs['config'] = None
start_list_markets(pargs, False) start_list_markets(pargs, False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert re.match("\nExchange Binance has 10 active markets:\n", assert re.match("\nExchange Binance has 12 active markets:\n",
captured.out) captured.out)
patch_exchange(mocker, api_mock=api_mock, id="bittrex", mock_markets=markets_static) patch_exchange(mocker, api_mock=api_mock, id="bittrex", mock_markets=markets_static)
@ -258,9 +258,9 @@ def test_list_markets(mocker, markets_static, capsys):
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 12 markets: " assert ("Exchange Bittrex has 14 markets: "
"BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, LTC/USDT, NEO/BTC, " "ADA/USDT:USDT, BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, ETH/USDT:USDT, "
"TKN/BTC, XLTCUSDT, XRP/BTC.\n" "LTC/BTC, LTC/ETH, LTC/USD, LTC/USDT, NEO/BTC, TKN/BTC, XLTCUSDT, XRP/BTC.\n"
in captured.out) in captured.out)
# Test list-pairs subcommand: active pairs # Test list-pairs subcommand: active pairs
@ -297,8 +297,8 @@ def test_list_markets(mocker, markets_static, capsys):
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 6 active markets with ETH, LTC as base currencies: " assert ("Exchange Bittrex has 7 active markets with ETH, LTC as base currencies: "
"ETH/BTC, ETH/USDT, LTC/BTC, LTC/ETH, LTC/USD, XLTCUSDT.\n" "ETH/BTC, ETH/USDT, ETH/USDT:USDT, LTC/BTC, LTC/ETH, LTC/USD, XLTCUSDT.\n"
in captured.out) in captured.out)
# active markets, base=LTC # active markets, base=LTC
@ -323,8 +323,8 @@ def test_list_markets(mocker, markets_static, capsys):
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 3 active markets with USDT, USD as quote currencies: " assert ("Exchange Bittrex has 5 active markets with USDT, USD as quote currencies: "
"ETH/USDT, LTC/USD, XLTCUSDT.\n" "ADA/USDT:USDT, ETH/USDT, ETH/USDT:USDT, LTC/USD, XLTCUSDT.\n"
in captured.out) in captured.out)
# active markets, quote=USDT # active markets, quote=USDT
@ -336,8 +336,8 @@ def test_list_markets(mocker, markets_static, capsys):
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 2 active markets with USDT as quote currency: " assert ("Exchange Bittrex has 4 active markets with USDT as quote currency: "
"ETH/USDT, XLTCUSDT.\n" "ADA/USDT:USDT, ETH/USDT, ETH/USDT:USDT, XLTCUSDT.\n"
in captured.out) in captured.out)
# active markets, base=LTC, quote=USDT # active markets, base=LTC, quote=USDT
@ -399,7 +399,7 @@ def test_list_markets(mocker, markets_static, capsys):
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Exchange Bittrex has 10 active markets:\n" assert ("Exchange Bittrex has 12 active markets:\n"
in captured.out) in captured.out)
# Test tabular output, no markets found # Test tabular output, no markets found
@ -422,8 +422,8 @@ def test_list_markets(mocker, markets_static, capsys):
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ('["BLK/BTC","ETH/BTC","ETH/USDT","LTC/BTC","LTC/ETH","LTC/USD","NEO/BTC",' assert ('["ADA/USDT:USDT","BLK/BTC","ETH/BTC","ETH/USDT","ETH/USDT:USDT",'
'"TKN/BTC","XLTCUSDT","XRP/BTC"]' '"LTC/BTC","LTC/ETH","LTC/USD","NEO/BTC","TKN/BTC","XLTCUSDT","XRP/BTC"]'
in captured.out) in captured.out)
# Test --print-csv # Test --print-csv

View File

@ -579,6 +579,8 @@ def get_markets():
'quote': 'BTC', 'quote': 'BTC',
'active': True, 'active': True,
'spot': True, 'spot': True,
'swap': False,
'linear': None,
'type': 'spot', 'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
@ -614,6 +616,8 @@ def get_markets():
# According to ccxt, markets without active item set are also active # According to ccxt, markets without active item set are also active
# 'active': True, # 'active': True,
'spot': True, 'spot': True,
'swap': False,
'linear': None,
'type': 'spot', 'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
@ -648,6 +652,8 @@ def get_markets():
'quote': 'BTC', 'quote': 'BTC',
'active': True, 'active': True,
'spot': True, 'spot': True,
'swap': False,
'linear': None,
'type': 'spot', 'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
@ -682,6 +688,8 @@ def get_markets():
'quote': 'BTC', 'quote': 'BTC',
'active': True, 'active': True,
'spot': True, 'spot': True,
'swap': False,
'linear': None,
'type': 'spot', 'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
@ -717,6 +725,8 @@ def get_markets():
'quote': 'BTC', 'quote': 'BTC',
'active': True, 'active': True,
'spot': True, 'spot': True,
'swap': False,
'linear': None,
'type': 'spot', 'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
@ -752,6 +762,8 @@ def get_markets():
'quote': 'BTC', 'quote': 'BTC',
'active': True, 'active': True,
'spot': True, 'spot': True,
'swap': False,
'linear': None,
'type': 'spot', 'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
@ -787,6 +799,8 @@ def get_markets():
'quote': 'BTC', 'quote': 'BTC',
'active': False, 'active': False,
'spot': True, 'spot': True,
'swap': False,
'linear': None,
'type': 'spot', 'type': 'spot',
'contractSize': None, 'contractSize': None,
'precision': { 'precision': {
@ -877,6 +891,8 @@ def get_markets():
'future': True, 'future': True,
'swap': True, 'swap': True,
'margin': True, 'margin': True,
'linear': None,
'inverse': False,
'type': 'spot', 'type': 'spot',
'contractSize': None, 'contractSize': None,
'taker': 0.0006, 'taker': 0.0006,
@ -912,6 +928,8 @@ def get_markets():
'quote': 'USDT', 'quote': 'USDT',
'active': True, 'active': True,
'spot': True, 'spot': True,
'swap': False,
'linear': None,
'type': 'spot', 'type': 'spot',
'taker': 0.0006, 'taker': 0.0006,
'maker': 0.0002, 'maker': 0.0002,
@ -943,19 +961,40 @@ def get_markets():
'symbol': 'NEO/USDT', 'symbol': 'NEO/USDT',
'base': 'NEO', 'base': 'NEO',
'quote': 'USDT', 'quote': 'USDT',
'active': True, 'settle': '',
'spot': True, 'baseId': 'NEO',
'quoteId': 'USDT',
'settleId': '',
'type': 'spot', 'type': 'spot',
'spot': True,
'margin': True,
'swap': False,
'futures': False,
'option': False,
'active': True,
'contract': False,
'linear': None,
'inverse': None,
'taker': 0.0006, 'taker': 0.0006,
'maker': 0.0002, 'maker': 0.0002,
'contractSize': None,
'expiry': None,
'expiryDatetime': None,
'strike': None,
'optionType': None,
'tierBased': None,
'percentage': None,
'lot': 0.00000001,
'precision': { 'precision': {
'price': 8, 'price': 8,
'amount': 8, 'amount': 8,
'cost': 8, 'cost': 8,
}, },
'lot': 0.00000001,
'contractSize': None,
'limits': { 'limits': {
"leverage": {
'min': 1,
'max': 10
},
'amount': { 'amount': {
'min': 0.01, 'min': 0.01,
'max': 1000, 'max': 1000,
@ -978,6 +1017,8 @@ def get_markets():
'quote': 'USDT', 'quote': 'USDT',
'active': True, 'active': True,
'spot': True, 'spot': True,
'swap': False,
'linear': None,
'type': 'spot', 'type': 'spot',
'contractSize': None, 'contractSize': None,
'taker': 0.0006, 'taker': 0.0006,
@ -1015,6 +1056,8 @@ def get_markets():
'quote': 'USD', 'quote': 'USD',
'active': True, 'active': True,
'spot': True, 'spot': True,
'swap': False,
'linear': None,
'type': 'spot', 'type': 'spot',
'contractSize': None, 'contractSize': None,
'precision': { 'precision': {
@ -1050,6 +1093,8 @@ def get_markets():
'spot': False, 'spot': False,
'type': 'swap', 'type': 'swap',
'contractSize': 0.01, 'contractSize': 0.01,
'swap': False,
'linear': False,
'taker': 0.0006, 'taker': 0.0006,
'maker': 0.0002, 'maker': 0.0002,
'precision': { 'precision': {
@ -1083,6 +1128,8 @@ def get_markets():
'quote': 'ETH', 'quote': 'ETH',
'active': True, 'active': True,
'spot': True, 'spot': True,
'swap': False,
'linear': None,
'type': 'spot', 'type': 'spot',
'contractSize': None, 'contractSize': None,
'precision': { 'precision': {
@ -1163,7 +1210,185 @@ def get_markets():
'amount': 1 'amount': 1
}, },
'info': {} 'info': {}
},
'ADA/USDT:USDT': {
'limits': {
'leverage': {
'min': 1,
'max': 20,
},
'amount': {
'min': 1,
'max': 1000000,
},
'price': {
'min': 0.52981,
'max': 1.58943,
},
'cost': {
'min': None,
'max': None,
} }
},
'precision': {
'amount': 1,
'price': 0.00001
},
'tierBased': True,
'percentage': True,
'taker': 0.0000075,
'maker': -0.0000025,
'feeSide': 'get',
'tiers': {
'maker': [
[0, 0.002], [1.5, 0.00185],
[3, 0.00175], [6, 0.00165],
[12.5, 0.00155], [25, 0.00145],
[75, 0.00135], [200, 0.00125],
[500, 0.00115], [1250, 0.00105],
[2500, 0.00095], [3000, 0.00085],
[6000, 0.00075], [11000, 0.00065],
[20000, 0.00055], [40000, 0.00055],
[75000, 0.00055]
],
'taker': [
[0, 0.002], [1.5, 0.00195],
[3, 0.00185], [6, 0.00175],
[12.5, 0.00165], [25, 0.00155],
[75, 0.00145], [200, 0.00135],
[500, 0.00125], [1250, 0.00115],
[2500, 0.00105], [3000, 0.00095],
[6000, 0.00085], [11000, 0.00075],
[20000, 0.00065], [40000, 0.00065],
[75000, 0.00065]
]
},
'id': 'ADA_USDT',
'symbol': 'ADA/USDT:USDT',
'base': 'ADA',
'quote': 'USDT',
'settle': 'USDT',
'baseId': 'ADA',
'quoteId': 'USDT',
'settleId': 'usdt',
'type': 'swap',
'spot': False,
'margin': False,
'swap': True,
'future': False,
'option': False,
'active': True,
'contract': True,
'linear': True,
'inverse': False,
'contractSize': 0.01,
'expiry': None,
'expiryDatetime': None,
'strike': None,
'optionType': None,
'info': {}
},
'SOL/BUSD:BUSD': {
'limits': {
'leverage': {'min': None, 'max': None},
'amount': {'min': 1, 'max': 1000000},
'price': {'min': 0.04, 'max': 100000},
'cost': {'min': 5, 'max': None},
'market': {'min': 1, 'max': 1500}
},
'precision': {'amount': 0, 'price': 2, 'base': 8, 'quote': 8},
'tierBased': False,
'percentage': True,
'taker': 0.0004,
'maker': 0.0002,
'feeSide': 'get',
'id': 'SOLBUSD',
'lowercaseId': 'solbusd',
'symbol': 'SOL/BUSD',
'base': 'SOL',
'quote': 'BUSD',
'settle': 'BUSD',
'baseId': 'SOL',
'quoteId': 'BUSD',
'settleId': 'BUSD',
'type': 'future',
'spot': False,
'margin': False,
'future': True,
'delivery': False,
'option': False,
'active': True,
'contract': True,
'linear': True,
'inverse': False,
'contractSize': 1,
'expiry': None,
'expiryDatetime': None,
'strike': None,
'optionType': None,
'info': {
'symbol': 'SOLBUSD',
'pair': 'SOLBUSD',
'contractType': 'PERPETUAL',
'deliveryDate': '4133404800000',
'onboardDate': '1630566000000',
'status': 'TRADING',
'maintMarginPercent': '2.5000',
'requiredMarginPercent': '5.0000',
'baseAsset': 'SOL',
'quoteAsset': 'BUSD',
'marginAsset': 'BUSD',
'pricePrecision': '4',
'quantityPrecision': '0',
'baseAssetPrecision': '8',
'quotePrecision': '8',
'underlyingType': 'COIN',
'underlyingSubType': [],
'settlePlan': '0',
'triggerProtect': '0.0500',
'liquidationFee': '0.005000',
'marketTakeBound': '0.05',
'filters': [
{
'minPrice': '0.0400',
'maxPrice': '100000',
'filterType': 'PRICE_FILTER',
'tickSize': '0.0100'
},
{
'stepSize': '1',
'filterType': 'LOT_SIZE',
'maxQty': '1000000',
'minQty': '1'
},
{
'stepSize': '1',
'filterType': 'MARKET_LOT_SIZE',
'maxQty': '1500',
'minQty': '1'
},
{'limit': '200', 'filterType': 'MAX_NUM_ORDERS'},
{'limit': '10', 'filterType': 'MAX_NUM_ALGO_ORDERS'},
{'notional': '5', 'filterType': 'MIN_NOTIONAL'},
{
'multiplierDown': '0.9500',
'multiplierUp': '1.0500',
'multiplierDecimal': '4',
'filterType': 'PERCENT_PRICE'
}
],
'orderTypes': [
'LIMIT',
'MARKET',
'STOP',
'STOP_MARKET',
'TAKE_PROFIT',
'TAKE_PROFIT_MARKET',
'TRAILING_STOP_MARKET'
],
'timeInForce': ['GTC', 'IOC', 'FOK', 'GTX']
}
},
} }
@ -1173,7 +1398,9 @@ def markets_static():
# market list. Do not modify this list without a good reason! Do not modify market parameters # market list. Do not modify this list without a good reason! Do not modify market parameters
# of listed pairs in get_markets() without a good reason either! # of listed pairs in get_markets() without a good reason either!
static_markets = ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', static_markets = ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD',
'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC'] 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC',
'ADA/USDT:USDT', 'ETH/USDT:USDT',
]
all_markets = get_markets() all_markets = get_markets()
return {m: all_markets[m] for m in static_markets} return {m: all_markets[m] for m in static_markets}
@ -2841,3 +3068,438 @@ def funding_rate_history_octohourly():
"datetime": "2021-09-01T08:00:00.000Z" "datetime": "2021-09-01T08:00:00.000Z"
} }
] ]
@pytest.fixture(scope='function')
def leverage_tiers():
return {
"1000SHIB/USDT": [
{
'min': 0,
'max': 50000,
'mmr': 0.01,
'lev': 50,
'maintAmt': 0.0
},
{
'min': 50000,
'max': 150000,
'mmr': 0.025,
'lev': 20,
'maintAmt': 750.0
},
{
'min': 150000,
'max': 250000,
'mmr': 0.05,
'lev': 10,
'maintAmt': 4500.0
},
{
'min': 250000,
'max': 500000,
'mmr': 0.1,
'lev': 5,
'maintAmt': 17000.0
},
{
'min': 500000,
'max': 1000000,
'mmr': 0.125,
'lev': 4,
'maintAmt': 29500.0
},
{
'min': 1000000,
'max': 2000000,
'mmr': 0.25,
'lev': 2,
'maintAmt': 154500.0
},
{
'min': 2000000,
'max': 30000000,
'mmr': 0.5,
'lev': 1,
'maintAmt': 654500.0
},
],
"1INCH/USDT": [
{
'min': 0,
'max': 5000,
'mmr': 0.012,
'lev': 50,
'maintAmt': 0.0
},
{
'min': 5000,
'max': 25000,
'mmr': 0.025,
'lev': 20,
'maintAmt': 65.0
},
{
'min': 25000,
'max': 100000,
'mmr': 0.05,
'lev': 10,
'maintAmt': 690.0
},
{
'min': 100000,
'max': 250000,
'mmr': 0.1,
'lev': 5,
'maintAmt': 5690.0
},
{
'min': 250000,
'max': 1000000,
'mmr': 0.125,
'lev': 2,
'maintAmt': 11940.0
},
{
'min': 1000000,
'max': 100000000,
'mmr': 0.5,
'lev': 1,
'maintAmt': 386940.0
},
],
"AAVE/USDT": [
{
'min': 0,
'max': 50000,
'mmr': 0.01,
'lev': 50,
'maintAmt': 0.0
},
{
'min': 50000,
'max': 250000,
'mmr': 0.02,
'lev': 25,
'maintAmt': 500.0
},
{
'min': 250000,
'max': 1000000,
'mmr': 0.05,
'lev': 10,
'maintAmt': 8000.0
},
{
'min': 1000000,
'max': 2000000,
'mmr': 0.1,
'lev': 5,
'maintAmt': 58000.0
},
{
'min': 2000000,
'max': 5000000,
'mmr': 0.125,
'lev': 4,
'maintAmt': 108000.0
},
{
'min': 5000000,
'max': 10000000,
'mmr': 0.1665,
'lev': 3,
'maintAmt': 315500.0
},
{
'min': 10000000,
'max': 20000000,
'mmr': 0.25,
'lev': 2,
'maintAmt': 1150500.0
},
{
"min": 20000000,
"max": 50000000,
"mmr": 0.5,
"lev": 1,
"maintAmt": 6150500.0
}
],
"ADA/BUSD": [
{
"min": 0,
"max": 100000,
"mmr": 0.025,
"lev": 20,
"maintAmt": 0.0
},
{
"min": 100000,
"max": 500000,
"mmr": 0.05,
"lev": 10,
"maintAmt": 2500.0
},
{
"min": 500000,
"max": 1000000,
"mmr": 0.1,
"lev": 5,
"maintAmt": 27500.0
},
{
"min": 1000000,
"max": 2000000,
"mmr": 0.15,
"lev": 3,
"maintAmt": 77500.0
},
{
"min": 2000000,
"max": 5000000,
"mmr": 0.25,
"lev": 2,
"maintAmt": 277500.0
},
{
"min": 5000000,
"max": 30000000,
"mmr": 0.5,
"lev": 1,
"maintAmt": 1527500.0
},
],
'BNB/BUSD': [
{
"min": 0, # stake(before leverage) = 0
"max": 100000, # max stake(before leverage) = 5000
"mmr": 0.025,
"lev": 20,
"maintAmt": 0.0
},
{
"min": 100000, # stake = 10000.0
"max": 500000, # max_stake = 50000.0
"mmr": 0.05,
"lev": 10,
"maintAmt": 2500.0
},
{
"min": 500000, # stake = 100000.0
"max": 1000000, # max_stake = 200000.0
"mmr": 0.1,
"lev": 5,
"maintAmt": 27500.0
},
{
"min": 1000000, # stake = 333333.3333333333
"max": 2000000, # max_stake = 666666.6666666666
"mmr": 0.15,
"lev": 3,
"maintAmt": 77500.0
},
{
"min": 2000000, # stake = 1000000.0
"max": 5000000, # max_stake = 2500000.0
"mmr": 0.25,
"lev": 2,
"maintAmt": 277500.0
},
{
"min": 5000000, # stake = 5000000.0
"max": 30000000, # max_stake = 30000000.0
"mmr": 0.5,
"lev": 1,
"maintAmt": 1527500.0
}
],
'BNB/USDT': [
{
"min": 0, # stake = 0.0
"max": 10000, # max_stake = 133.33333333333334
"mmr": 0.0065,
"lev": 75,
"maintAmt": 0.0
},
{
"min": 10000, # stake = 200.0
"max": 50000, # max_stake = 1000.0
"mmr": 0.01,
"lev": 50,
"maintAmt": 35.0
},
{
"min": 50000, # stake = 2000.0
"max": 250000, # max_stake = 10000.0
"mmr": 0.02,
"lev": 25,
"maintAmt": 535.0
},
{
"min": 250000, # stake = 25000.0
"max": 1000000, # max_stake = 100000.0
"mmr": 0.05,
"lev": 10,
"maintAmt": 8035.0
},
{
"min": 1000000, # stake = 200000.0
"max": 2000000, # max_stake = 400000.0
"mmr": 0.1,
"lev": 5,
"maintAmt": 58035.0
},
{
"min": 2000000, # stake = 500000.0
"max": 5000000, # max_stake = 1250000.0
"mmr": 0.125,
"lev": 4,
"maintAmt": 108035.0
},
{
"min": 5000000, # stake = 1666666.6666666667
"max": 10000000, # max_stake = 3333333.3333333335
"mmr": 0.15,
"lev": 3,
"maintAmt": 233035.0
},
{
"min": 10000000, # stake = 5000000.0
"max": 20000000, # max_stake = 10000000.0
"mmr": 0.25,
"lev": 2,
"maintAmt": 1233035.0
},
{
"min": 20000000, # stake = 20000000.0
"max": 50000000, # max_stake = 50000000.0
"mmr": 0.5,
"lev": 1,
"maintAmt": 6233035.0
},
],
'BTC/USDT': [
{
"min": 0, # stake = 0.0
"max": 50000, # max_stake = 400.0
"mmr": 0.004,
"lev": 125,
"maintAmt": 0.0
},
{
"min": 50000, # stake = 500.0
"max": 250000, # max_stake = 2500.0
"mmr": 0.005,
"lev": 100,
"maintAmt": 50.0
},
{
"min": 250000, # stake = 5000.0
"max": 1000000, # max_stake = 20000.0
"mmr": 0.01,
"lev": 50,
"maintAmt": 1300.0
},
{
"min": 1000000, # stake = 50000.0
"max": 7500000, # max_stake = 375000.0
"mmr": 0.025,
"lev": 20,
"maintAmt": 16300.0
},
{
"min": 7500000, # stake = 750000.0
"max": 40000000, # max_stake = 4000000.0
"mmr": 0.05,
"lev": 10,
"maintAmt": 203800.0
},
{
"min": 40000000, # stake = 8000000.0
"max": 100000000, # max_stake = 20000000.0
"mmr": 0.1,
"lev": 5,
"maintAmt": 2203800.0
},
{
"min": 100000000, # stake = 25000000.0
"max": 200000000, # max_stake = 50000000.0
"mmr": 0.125,
"lev": 4,
"maintAmt": 4703800.0
},
{
"min": 200000000, # stake = 66666666.666666664
"max": 400000000, # max_stake = 133333333.33333333
"mmr": 0.15,
"lev": 3,
"maintAmt": 9703800.0
},
{
"min": 400000000, # stake = 200000000.0
"max": 600000000, # max_stake = 300000000.0
"mmr": 0.25,
"lev": 2,
"maintAmt": 4.97038E7
},
{
"min": 600000000, # stake = 600000000.0
"max": 1000000000, # max_stake = 1000000000.0
"mmr": 0.5,
"lev": 1,
"maintAmt": 1.997038E8
},
],
"ZEC/USDT": [
{
'min': 0,
'max': 50000,
'mmr': 0.01,
'lev': 50,
'maintAmt': 0.0
},
{
'min': 50000,
'max': 150000,
'mmr': 0.025,
'lev': 20,
'maintAmt': 750.0
},
{
'min': 150000,
'max': 250000,
'mmr': 0.05,
'lev': 10,
'maintAmt': 4500.0
},
{
'min': 250000,
'max': 500000,
'mmr': 0.1,
'lev': 5,
'maintAmt': 17000.0
},
{
'min': 500000,
'max': 1000000,
'mmr': 0.125,
'lev': 4,
'maintAmt': 29500.0
},
{
'min': 1000000,
'max': 2000000,
'mmr': 0.25,
'lev': 2,
'maintAmt': 154500.0
},
{
'min': 2000000,
'max': 30000000,
'mmr': 0.5,
'lev': 1,
'maintAmt': 654500.0
},
]
}

View File

@ -162,168 +162,338 @@ def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side):
assert not exchange.stoploss_adjust(sl3, order, side=side) assert not exchange.stoploss_adjust(sl3, order, side=side)
@pytest.mark.parametrize('pair,stake_amount,max_lev', [ def test_fill_leverage_tiers_binance(default_conf, mocker):
("BNB/BUSD", 0.0, 40.0),
("BNB/USDT", 100.0, 100.0),
("BTC/USDT", 170.30, 250.0),
("BNB/BUSD", 99999.9, 10.0),
("BNB/USDT", 750000, 6.666666666666667),
("BTC/USDT", 150000000.1, 2.0),
])
def test_get_max_leverage_binance(default_conf, mocker, pair, stake_amount, max_lev):
exchange = get_patched_exchange(mocker, default_conf, id="binance")
exchange._leverage_brackets = {
'BNB/BUSD': [
[0.0, 0.025, 0.0], # lev = 40.0
[100000.0, 0.05, 2500.0], # lev = 20.0
[500000.0, 0.1, 27500.0], # lev = 10.0
[1000000.0, 0.15, 77500.0], # lev = 6.666666666666667
[2000000.0, 0.25, 277500.0], # lev = 4.0
[5000000.0, 0.5, 1527500.0], # lev = 2.0
],
'BNB/USDT': [
[0.0, 0.0065, 0.0], # lev = 153.84615384615384
[10000.0, 0.01, 35.0], # lev = 100.0
[50000.0, 0.02, 535.0], # lev = 50.0
[250000.0, 0.05, 8035.0], # lev = 20.0
[1000000.0, 0.1, 58035.0], # lev = 10.0
[2000000.0, 0.125, 108035.0], # lev = 8.0
[5000000.0, 0.15, 233035.0], # lev = 6.666666666666667
[10000000.0, 0.25, 1233035.0], # lev = 4.0
],
'BTC/USDT': [
[0.0, 0.004, 0.0], # lev = 250.0
[50000.0, 0.005, 50.0], # lev = 200.0
[250000.0, 0.01, 1300.0], # lev = 100.0
[1000000.0, 0.025, 16300.0], # lev = 40.0
[5000000.0, 0.05, 141300.0], # lev = 20.0
[20000000.0, 0.1, 1141300.0], # lev = 10.0
[50000000.0, 0.125, 2391300.0], # lev = 8.0
[100000000.0, 0.15, 4891300.0], # lev = 6.666666666666667
[200000000.0, 0.25, 24891300.0], # lev = 4.0
[300000000.0, 0.5, 99891300.0], # lev = 2.0
]
}
assert exchange.get_max_leverage(pair, stake_amount) == max_lev
def test_fill_leverage_brackets_binance(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_leverage_brackets = MagicMock(return_value={ api_mock.fetch_leverage_tiers = MagicMock(return_value={
'ADA/BUSD': [[0.0, 0.025], 'ADA/BUSD': [
[100000.0, 0.05], {
[500000.0, 0.1], "tier": 1,
[1000000.0, 0.15], "notionalFloor": 0,
[2000000.0, 0.25], "notionalCap": 100000,
[5000000.0, 0.5]], "maintenanceMarginRate": 0.025,
'BTC/USDT': [[0.0, 0.004], "maxLeverage": 20,
[50000.0, 0.005], "info": {
[250000.0, 0.01], "bracket": "1",
[1000000.0, 0.025], "initialLeverage": "20",
[5000000.0, 0.05], "notionalCap": "100000",
[20000000.0, 0.1], "notionalFloor": "0",
[50000000.0, 0.125], "maintMarginRatio": "0.025",
[100000000.0, 0.15], "cum": "0.0"
[200000000.0, 0.25], }
[300000000.0, 0.5]], },
"ZEC/USDT": [[0.0, 0.01], {
[5000.0, 0.025], "tier": 2,
[25000.0, 0.05], "notionalFloor": 100000,
[100000.0, 0.1], "notionalCap": 500000,
[250000.0, 0.125], "maintenanceMarginRate": 0.05,
[1000000.0, 0.5]], "maxLeverage": 10,
"info": {
"bracket": "2",
"initialLeverage": "10",
"notionalCap": "500000",
"notionalFloor": "100000",
"maintMarginRatio": "0.05",
"cum": "2500.0"
}
},
{
"tier": 3,
"notionalFloor": 500000,
"notionalCap": 1000000,
"maintenanceMarginRate": 0.1,
"maxLeverage": 5,
"info": {
"bracket": "3",
"initialLeverage": "5",
"notionalCap": "1000000",
"notionalFloor": "500000",
"maintMarginRatio": "0.1",
"cum": "27500.0"
}
},
{
"tier": 4,
"notionalFloor": 1000000,
"notionalCap": 2000000,
"maintenanceMarginRate": 0.15,
"maxLeverage": 3,
"info": {
"bracket": "4",
"initialLeverage": "3",
"notionalCap": "2000000",
"notionalFloor": "1000000",
"maintMarginRatio": "0.15",
"cum": "77500.0"
}
},
{
"tier": 5,
"notionalFloor": 2000000,
"notionalCap": 5000000,
"maintenanceMarginRate": 0.25,
"maxLeverage": 2,
"info": {
"bracket": "5",
"initialLeverage": "2",
"notionalCap": "5000000",
"notionalFloor": "2000000",
"maintMarginRatio": "0.25",
"cum": "277500.0"
}
},
{
"tier": 6,
"notionalFloor": 5000000,
"notionalCap": 30000000,
"maintenanceMarginRate": 0.5,
"maxLeverage": 1,
"info": {
"bracket": "6",
"initialLeverage": "1",
"notionalCap": "30000000",
"notionalFloor": "5000000",
"maintMarginRatio": "0.5",
"cum": "1527500.0"
}
}
],
"ZEC/USDT": [
{
"tier": 1,
"notionalFloor": 0,
"notionalCap": 50000,
"maintenanceMarginRate": 0.01,
"maxLeverage": 50,
"info": {
"bracket": "1",
"initialLeverage": "50",
"notionalCap": "50000",
"notionalFloor": "0",
"maintMarginRatio": "0.01",
"cum": "0.0"
}
},
{
"tier": 2,
"notionalFloor": 50000,
"notionalCap": 150000,
"maintenanceMarginRate": 0.025,
"maxLeverage": 20,
"info": {
"bracket": "2",
"initialLeverage": "20",
"notionalCap": "150000",
"notionalFloor": "50000",
"maintMarginRatio": "0.025",
"cum": "750.0"
}
},
{
"tier": 3,
"notionalFloor": 150000,
"notionalCap": 250000,
"maintenanceMarginRate": 0.05,
"maxLeverage": 10,
"info": {
"bracket": "3",
"initialLeverage": "10",
"notionalCap": "250000",
"notionalFloor": "150000",
"maintMarginRatio": "0.05",
"cum": "4500.0"
}
},
{
"tier": 4,
"notionalFloor": 250000,
"notionalCap": 500000,
"maintenanceMarginRate": 0.1,
"maxLeverage": 5,
"info": {
"bracket": "4",
"initialLeverage": "5",
"notionalCap": "500000",
"notionalFloor": "250000",
"maintMarginRatio": "0.1",
"cum": "17000.0"
}
},
{
"tier": 5,
"notionalFloor": 500000,
"notionalCap": 1000000,
"maintenanceMarginRate": 0.125,
"maxLeverage": 4,
"info": {
"bracket": "5",
"initialLeverage": "4",
"notionalCap": "1000000",
"notionalFloor": "500000",
"maintMarginRatio": "0.125",
"cum": "29500.0"
}
},
{
"tier": 6,
"notionalFloor": 1000000,
"notionalCap": 2000000,
"maintenanceMarginRate": 0.25,
"maxLeverage": 2,
"info": {
"bracket": "6",
"initialLeverage": "2",
"notionalCap": "2000000",
"notionalFloor": "1000000",
"maintMarginRatio": "0.25",
"cum": "154500.0"
}
},
{
"tier": 7,
"notionalFloor": 2000000,
"notionalCap": 30000000,
"maintenanceMarginRate": 0.5,
"maxLeverage": 1,
"info": {
"bracket": "7",
"initialLeverage": "1",
"notionalCap": "30000000",
"notionalFloor": "2000000",
"maintMarginRatio": "0.5",
"cum": "654500.0"
}
}
],
}) })
default_conf['dry_run'] = False default_conf['dry_run'] = False
default_conf['trading_mode'] = TradingMode.FUTURES default_conf['trading_mode'] = TradingMode.FUTURES
default_conf['margin_mode'] = MarginMode.ISOLATED default_conf['margin_mode'] = MarginMode.ISOLATED
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
exchange.fill_leverage_brackets() exchange.fill_leverage_tiers()
assert exchange._leverage_brackets == { assert exchange._leverage_tiers == {
'ADA/BUSD': [[0.0, 0.025, 0.0], 'ADA/BUSD': [
[100000.0, 0.05, 2500.0], {
[500000.0, 0.1, 27500.0], "min": 0,
[1000000.0, 0.15, 77499.99999999999], "max": 100000,
[2000000.0, 0.25, 277500.0], "mmr": 0.025,
[5000000.0, 0.5, 1527500.0]], "lev": 20,
'BTC/USDT': [[0.0, 0.004, 0.0], "maintAmt": 0.0
[50000.0, 0.005, 50.0], },
[250000.0, 0.01, 1300.0], {
[1000000.0, 0.025, 16300.000000000002], "min": 100000,
[5000000.0, 0.05, 141300.0], "max": 500000,
[20000000.0, 0.1, 1141300.0], "mmr": 0.05,
[50000000.0, 0.125, 2391300.0], "lev": 10,
[100000000.0, 0.15, 4891300.0], "maintAmt": 2500.0
[200000000.0, 0.25, 24891300.0], },
[300000000.0, 0.5, 99891300.0]], {
"ZEC/USDT": [[0.0, 0.01, 0.0], "min": 500000,
[5000.0, 0.025, 75.0], "max": 1000000,
[25000.0, 0.05, 700.0], "mmr": 0.1,
[100000.0, 0.1, 5700.0], "lev": 5,
[250000.0, 0.125, 11949.999999999998], "maintAmt": 27500.0
[1000000.0, 0.5, 386950.0]] },
{
"min": 1000000,
"max": 2000000,
"mmr": 0.15,
"lev": 3,
"maintAmt": 77500.0
},
{
"min": 2000000,
"max": 5000000,
"mmr": 0.25,
"lev": 2,
"maintAmt": 277500.0
},
{
"min": 5000000,
"max": 30000000,
"mmr": 0.5,
"lev": 1,
"maintAmt": 1527500.0
}
],
"ZEC/USDT": [
{
'min': 0,
'max': 50000,
'mmr': 0.01,
'lev': 50,
'maintAmt': 0.0
},
{
'min': 50000,
'max': 150000,
'mmr': 0.025,
'lev': 20,
'maintAmt': 750.0
},
{
'min': 150000,
'max': 250000,
'mmr': 0.05,
'lev': 10,
'maintAmt': 4500.0
},
{
'min': 250000,
'max': 500000,
'mmr': 0.1,
'lev': 5,
'maintAmt': 17000.0
},
{
'min': 500000,
'max': 1000000,
'mmr': 0.125,
'lev': 4,
'maintAmt': 29500.0
},
{
'min': 1000000,
'max': 2000000,
'mmr': 0.25,
'lev': 2,
'maintAmt': 154500.0
},
{
'min': 2000000,
'max': 30000000,
'mmr': 0.5,
'lev': 1,
'maintAmt': 654500.0
},
]
} }
api_mock = MagicMock() api_mock = MagicMock()
api_mock.load_leverage_brackets = MagicMock() api_mock.load_leverage_tiers = MagicMock()
type(api_mock).has = PropertyMock(return_value={'loadLeverageBrackets': True}) type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True})
ccxt_exceptionhandlers( ccxt_exceptionhandlers(
mocker, mocker,
default_conf, default_conf,
api_mock, api_mock,
"binance", "binance",
"fill_leverage_brackets", "fill_leverage_tiers",
"load_leverage_brackets" "fetch_leverage_tiers",
) )
def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker): def test_fill_leverage_tiers_binance_dryrun(default_conf, mocker, leverage_tiers):
api_mock = MagicMock() api_mock = MagicMock()
default_conf['trading_mode'] = TradingMode.FUTURES default_conf['trading_mode'] = TradingMode.FUTURES
default_conf['margin_mode'] = MarginMode.ISOLATED default_conf['margin_mode'] = MarginMode.ISOLATED
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
exchange.fill_leverage_brackets() exchange.fill_leverage_tiers()
leverage_brackets = { leverage_tiers = leverage_tiers
"1000SHIB/USDT": [
[0.0, 0.01, 0.0],
[5000.0, 0.025, 75.0],
[25000.0, 0.05, 700.0],
[100000.0, 0.1, 5700.0],
[250000.0, 0.125, 11949.999999999998],
[1000000.0, 0.5, 386950.0],
],
"1INCH/USDT": [
[0.0, 0.012, 0.0],
[5000.0, 0.025, 65.0],
[25000.0, 0.05, 690.0],
[100000.0, 0.1, 5690.0],
[250000.0, 0.125, 11939.999999999998],
[1000000.0, 0.5, 386940.0],
],
"AAVE/USDT": [
[0.0, 0.01, 0.0],
[50000.0, 0.02, 500.0],
[250000.0, 0.05, 8000.000000000001],
[1000000.0, 0.1, 58000.0],
[2000000.0, 0.125, 107999.99999999999],
[5000000.0, 0.1665, 315500.00000000006],
[10000000.0, 0.25, 1150500.0],
],
"ADA/BUSD": [
[0.0, 0.025, 0.0],
[100000.0, 0.05, 2500.0],
[500000.0, 0.1, 27500.0],
[1000000.0, 0.15, 77499.99999999999],
[2000000.0, 0.25, 277500.0],
[5000000.0, 0.5, 1527500.0],
]
}
for key, value in leverage_brackets.items(): for key, value in leverage_tiers.items():
assert exchange._leverage_brackets[key] == value assert exchange._leverage_tiers[key] == value
def test__set_leverage_binance(mocker, default_conf): def test__set_leverage_binance(mocker, default_conf):
@ -403,43 +573,19 @@ def test__ccxt_config(default_conf, mocker, trading_mode, margin_mode, config):
("BTC/USDT", 170.30, 0.004, 0), ("BTC/USDT", 170.30, 0.004, 0),
("BNB/BUSD", 999999.9, 0.1, 27500.0), ("BNB/BUSD", 999999.9, 0.1, 27500.0),
("BNB/USDT", 5000000.0, 0.15, 233035.0), ("BNB/USDT", 5000000.0, 0.15, 233035.0),
("BTC/USDT", 300000000.1, 0.5, 99891300.0), ("BTC/USDT", 600000000, 0.5, 1.997038E8),
]) ])
def test_get_maintenance_ratio_and_amt_binance( def test_get_maintenance_ratio_and_amt_binance(
default_conf, default_conf,
mocker, mocker,
leverage_tiers,
pair, pair,
nominal_value, nominal_value,
mm_ratio, mm_ratio,
amt, amt,
): ):
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange = get_patched_exchange(mocker, default_conf, id="binance")
exchange._leverage_brackets = { exchange._leverage_tiers = leverage_tiers
'BNB/BUSD': [[0.0, 0.025, 0.0],
[100000.0, 0.05, 2500.0],
[500000.0, 0.1, 27500.0],
[1000000.0, 0.15, 77500.0],
[2000000.0, 0.25, 277500.0],
[5000000.0, 0.5, 1527500.0]],
'BNB/USDT': [[0.0, 0.0065, 0.0],
[10000.0, 0.01, 35.0],
[50000.0, 0.02, 535.0],
[250000.0, 0.05, 8035.0],
[1000000.0, 0.1, 58035.0],
[2000000.0, 0.125, 108035.0],
[5000000.0, 0.15, 233035.0],
[10000000.0, 0.25, 1233035.0]],
'BTC/USDT': [[0.0, 0.004, 0.0],
[50000.0, 0.005, 50.0],
[250000.0, 0.01, 1300.0],
[1000000.0, 0.025, 16300.0],
[5000000.0, 0.05, 141300.0],
[20000000.0, 0.1, 1141300.0],
[50000000.0, 0.125, 2391300.0],
[100000000.0, 0.15, 4891300.0],
[200000000.0, 0.25, 24891300.0],
[300000000.0, 0.5, 99891300.0]
]
}
(result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, nominal_value) (result_ratio, result_amt) = exchange.get_maintenance_ratio_and_amt(pair, nominal_value)
assert (round(result_ratio, 8), round(result_amt, 8)) == (mm_ratio, amt) assert (round(result_ratio, 8), round(result_amt, 8)) == (mm_ratio, amt)

View File

@ -24,10 +24,8 @@ EXCHANGES = {
'stake_currency': 'USDT', 'stake_currency': 'USDT',
'hasQuoteVolume': False, 'hasQuoteVolume': False,
'timeframe': '1h', 'timeframe': '1h',
'leverage_in_market': { 'leverage_tiers_public': False,
'spot': False, 'leverage_in_spot_market': False,
'futures': False,
}
}, },
'binance': { 'binance': {
'pair': 'BTC/USDT', 'pair': 'BTC/USDT',
@ -35,20 +33,16 @@ EXCHANGES = {
'hasQuoteVolume': True, 'hasQuoteVolume': True,
'timeframe': '5m', 'timeframe': '5m',
'futures': True, 'futures': True,
'leverage_in_market': { 'leverage_tiers_public': False,
'spot': False, 'leverage_in_spot_market': False,
'futures': False,
}
}, },
'kraken': { 'kraken': {
'pair': 'BTC/USDT', 'pair': 'BTC/USDT',
'stake_currency': 'USDT', 'stake_currency': 'USDT',
'hasQuoteVolume': True, 'hasQuoteVolume': True,
'timeframe': '5m', 'timeframe': '5m',
'leverage_in_market': { 'leverage_tiers_public': False,
'spot': True, 'leverage_in_spot_market': True,
'futures': True,
}
}, },
'ftx': { 'ftx': {
'pair': 'BTC/USD', 'pair': 'BTC/USD',
@ -57,20 +51,16 @@ EXCHANGES = {
'timeframe': '5m', 'timeframe': '5m',
'futures_pair': 'BTC/USD:USD', 'futures_pair': 'BTC/USD:USD',
'futures': True, 'futures': True,
'leverage_in_market': { 'leverage_tiers_public': False, # TODO: Set to True once implemented on CCXT
'spot': True, 'leverage_in_spot_market': True,
'futures': True,
}
}, },
'kucoin': { 'kucoin': {
'pair': 'BTC/USDT', 'pair': 'BTC/USDT',
'stake_currency': 'USDT', 'stake_currency': 'USDT',
'hasQuoteVolume': True, 'hasQuoteVolume': True,
'timeframe': '5m', 'timeframe': '5m',
'leverage_in_market': { 'leverage_tiers_public': False,
'spot': False, 'leverage_in_spot_market': True,
'futures': False,
}
}, },
'gateio': { 'gateio': {
'pair': 'BTC/USDT', 'pair': 'BTC/USDT',
@ -79,10 +69,8 @@ EXCHANGES = {
'timeframe': '5m', 'timeframe': '5m',
'futures': True, 'futures': True,
'futures_pair': 'BTC/USDT:USDT', 'futures_pair': 'BTC/USDT:USDT',
'leverage_in_market': { 'leverage_tiers_public': False, # TODO-lev: Set to True once implemented on CCXT
'spot': True, 'leverage_in_spot_market': True,
'futures': True,
}
}, },
'okx': { 'okx': {
'pair': 'BTC/USDT', 'pair': 'BTC/USDT',
@ -91,20 +79,16 @@ EXCHANGES = {
'timeframe': '5m', 'timeframe': '5m',
'futures_pair': 'BTC/USDT:USDT', 'futures_pair': 'BTC/USDT:USDT',
'futures': True, 'futures': True,
'leverage_in_market': { 'leverage_tiers_public': True,
'spot': True, 'leverage_in_spot_market': True,
'futures': True,
}
}, },
'bitvavo': { 'bitvavo': {
'pair': 'BTC/EUR', 'pair': 'BTC/EUR',
'stake_currency': 'EUR', 'stake_currency': 'EUR',
'hasQuoteVolume': True, 'hasQuoteVolume': True,
'timeframe': '5m', 'timeframe': '5m',
'leverage_in_market': { 'leverage_tiers_public': False,
'spot': False, 'leverage_in_spot_market': False,
'futures': False,
}
}, },
} }
@ -136,14 +120,14 @@ def exchange_futures(request, exchange_conf, class_mocker):
exchange_conf = deepcopy(exchange_conf) exchange_conf = deepcopy(exchange_conf)
exchange_conf['exchange']['name'] = request.param exchange_conf['exchange']['name'] = request.param
exchange_conf['trading_mode'] = 'futures' exchange_conf['trading_mode'] = 'futures'
exchange_conf['margin_mode'] = 'cross' exchange_conf['margin_mode'] = 'isolated'
exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency'] exchange_conf['stake_currency'] = EXCHANGES[request.param]['stake_currency']
# TODO-lev: This mock should no longer be necessary once futures are enabled. # TODO-lev: This mock should no longer be necessary once futures are enabled.
class_mocker.patch( class_mocker.patch(
'freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_margin_mode') 'freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_margin_mode')
class_mocker.patch( class_mocker.patch(
'freqtrade.exchange.binance.Binance.fill_leverage_brackets') 'freqtrade.exchange.binance.Binance.fill_leverage_tiers')
exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True) exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True)
@ -329,21 +313,21 @@ class TestCCXTExchange():
assert 0 < exchange.get_fee(pair, 'market', 'buy') < threshold assert 0 < exchange.get_fee(pair, 'market', 'buy') < threshold
assert 0 < exchange.get_fee(pair, 'market', 'sell') < threshold assert 0 < exchange.get_fee(pair, 'market', 'sell') < threshold
def test_get_max_leverage_spot(self, exchange): def test_ccxt_get_max_leverage_spot(self, exchange):
spot, spot_name = exchange spot, spot_name = exchange
if spot: if spot:
leverage_in_market_spot = EXCHANGES[spot_name]['leverage_in_market']['spot'] leverage_in_market_spot = EXCHANGES[spot_name]['leverage_in_spot_market']
if leverage_in_market_spot: if leverage_in_market_spot:
spot_pair = EXCHANGES[spot_name].get('pair', EXCHANGES[spot_name]['pair']) spot_pair = EXCHANGES[spot_name].get('pair', EXCHANGES[spot_name]['pair'])
spot_leverage = spot.get_max_leverage(spot_pair, 20) spot_leverage = spot.get_max_leverage(spot_pair, 20)
assert (isinstance(spot_leverage, float) or isinstance(spot_leverage, int)) assert (isinstance(spot_leverage, float) or isinstance(spot_leverage, int))
assert spot_leverage >= 1.0 assert spot_leverage >= 1.0
def test_get_max_leverage_futures(self, exchange_futures): def test_ccxt_get_max_leverage_futures(self, exchange_futures):
futures, futures_name = exchange_futures futures, futures_name = exchange_futures
if futures: if futures:
leverage_in_market_futures = EXCHANGES[futures_name]['leverage_in_market']['futures'] leverage_tiers_public = EXCHANGES[futures_name]['leverage_tiers_public']
if leverage_in_market_futures: if leverage_tiers_public:
futures_pair = EXCHANGES[futures_name].get( futures_pair = EXCHANGES[futures_name].get(
'futures_pair', 'futures_pair',
EXCHANGES[futures_name]['pair'] EXCHANGES[futures_name]['pair']
@ -362,3 +346,76 @@ class TestCCXTExchange():
contract_size = futures._get_contract_size(futures_pair) contract_size = futures._get_contract_size(futures_pair)
assert (isinstance(contract_size, float) or isinstance(contract_size, int)) assert (isinstance(contract_size, float) or isinstance(contract_size, int))
assert contract_size >= 0.0 assert contract_size >= 0.0
def test_ccxt_load_leverage_tiers(self, exchange_futures):
futures, futures_name = exchange_futures
if futures and EXCHANGES[futures_name]['leverage_tiers_public']:
leverage_tiers = futures.load_leverage_tiers()
futures_pair = EXCHANGES[futures_name].get(
'futures_pair',
EXCHANGES[futures_name]['pair']
)
assert (isinstance(leverage_tiers, dict))
assert futures_pair in leverage_tiers
pair_tiers = leverage_tiers[futures_pair]
assert len(pair_tiers) > 0
oldLeverage = float('inf')
oldMaintenanceMarginRate = oldNotionalFloor = oldNotionalCap = -1
for tier in pair_tiers:
for key in [
'maintenanceMarginRate',
'notionalFloor',
'notionalCap',
'maxLeverage'
]:
assert key in tier
assert tier[key] >= 0.0
assert tier['notionalCap'] > tier['notionalFloor']
assert tier['maxLeverage'] <= oldLeverage
assert tier['maintenanceMarginRate'] >= oldMaintenanceMarginRate
assert tier['notionalFloor'] > oldNotionalFloor
assert tier['notionalCap'] > oldNotionalCap
oldLeverage = tier['maxLeverage']
oldMaintenanceMarginRate = tier['maintenanceMarginRate']
oldNotionalFloor = tier['notionalFloor']
oldNotionalCap = tier['notionalCap']
def test_ccxt_dry_run_liquidation_price(self, exchange_futures):
futures, futures_name = exchange_futures
if futures and EXCHANGES[futures_name]['leverage_tiers_public']:
futures_pair = EXCHANGES[futures_name].get(
'futures_pair',
EXCHANGES[futures_name]['pair']
)
liquidation_price = futures.dry_run_liquidation_price(
futures_pair,
40000,
False,
100,
100,
)
assert (isinstance(liquidation_price, float))
assert liquidation_price >= 0.0
liquidation_price = futures.dry_run_liquidation_price(
futures_pair,
40000,
False,
100,
100,
)
assert (isinstance(liquidation_price, float))
assert liquidation_price >= 0.0
def test_ccxt_get_max_pair_stake_amount(self, exchange_futures):
futures, futures_name = exchange_futures
if futures:
futures_pair = EXCHANGES[futures_name].get(
'futures_pair',
EXCHANGES[futures_name]['pair']
)
max_stake_amount = futures.get_max_pair_stake_amount(futures_pair, 40000)
assert (isinstance(max_stake_amount, float))
assert max_stake_amount >= 0.0

View File

@ -689,7 +689,7 @@ def test_validate_stakecurrency_error(default_conf, mocker, caplog):
def test_get_quote_currencies(default_conf, mocker): def test_get_quote_currencies(default_conf, mocker):
ex = get_patched_exchange(mocker, default_conf) ex = get_patched_exchange(mocker, default_conf)
assert set(ex.get_quote_currencies()) == set(['USD', 'ETH', 'BTC', 'USDT']) assert set(ex.get_quote_currencies()) == set(['USD', 'ETH', 'BTC', 'USDT', 'BUSD'])
@pytest.mark.parametrize('pair,expected', [ @pytest.mark.parametrize('pair,expected', [
@ -1207,9 +1207,20 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
assert exchange._set_leverage.call_count == 0 assert exchange._set_leverage.call_count == 0
assert exchange.set_margin_mode.call_count == 0 assert exchange.set_margin_mode.call_count == 0
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
},
'symbol': 'ADA/USDT:USDT',
'amount': 1
})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.trading_mode = TradingMode.FUTURES exchange.trading_mode = TradingMode.FUTURES
exchange._set_leverage = MagicMock()
exchange.set_margin_mode = MagicMock()
order = exchange.create_order( order = exchange.create_order(
pair='XLTCUSDT', pair='ADA/USDT:USDT',
ordertype=ordertype, ordertype=ordertype,
side=side, side=side,
amount=1, amount=1,
@ -2977,7 +2988,7 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"base_currencies,quote_currencies,tradable_only,active_only,spot_only," "base_currencies,quote_currencies,tradable_only,active_only,spot_only,"
"futures_only,expected_keys", [ "futures_only,expected_keys,test_comment", [
# Testing markets (in conftest.py): # Testing markets (in conftest.py):
# 'BLK/BTC': 'active': True # 'BLK/BTC': 'active': True
# 'BTT/BTC': 'active': True # 'BTT/BTC': 'active': True
@ -2991,64 +3002,67 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
# 'TKN/BTC': 'active' not set # 'TKN/BTC': 'active' not set
# 'XLTCUSDT': 'active': True, not a pair # 'XLTCUSDT': 'active': True, not a pair
# 'XRP/BTC': 'active': False # 'XRP/BTC': 'active': False
# all markets
([], [], False, False, False, False, ([], [], False, False, False, False,
['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT',
'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']), 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC', 'ADA/USDT:USDT',
# all markets, only spot pairs 'ETH/USDT:USDT'],
'all markets'),
([], [], False, False, True, False, ([], [], False, False, True, False,
['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD',
'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']), 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC'],
# active markets 'all markets, only spot pairs'),
([], [], False, True, False, False, ([], [], False, True, False, False,
['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'NEO/BTC', ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'NEO/BTC',
'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']), 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC', 'ADA/USDT:USDT', 'ETH/USDT:USDT'],
# all pairs 'active markets'),
([], [], True, False, False, False, ([], [], True, False, False, False,
['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD',
'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']), 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC'],
# active pairs 'all pairs'),
([], [], True, True, False, False, ([], [], True, True, False, False,
['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'NEO/BTC', ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'NEO/BTC',
'TKN/BTC', 'XRP/BTC']), 'TKN/BTC', 'XRP/BTC'],
# all markets, base=ETH, LTC 'active pairs'),
(['ETH', 'LTC'], [], False, False, False, False, (['ETH', 'LTC'], [], False, False, False, False,
['ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), ['ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT',
# all markets, base=LTC 'ETH/USDT:USDT'],
'all markets, base=ETH, LTC'),
(['LTC'], [], False, False, False, False, (['LTC'], [], False, False, False, False,
['LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), ['LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT'],
# spot markets, base=LTC 'all markets, base=LTC'),
(['LTC'], [], False, False, True, False, (['LTC'], [], False, False, True, False,
['LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT']), ['LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT'],
# all markets, quote=USDT 'spot markets, base=LTC'),
([], ['USDT'], False, False, False, False, ([], ['USDT'], False, False, False, False,
['ETH/USDT', 'LTC/USDT', 'XLTCUSDT']), ['ETH/USDT', 'LTC/USDT', 'XLTCUSDT', 'ADA/USDT:USDT', 'ETH/USDT:USDT'],
# Futures markets, quote=USDT 'all markets, quote=USDT'),
([], ['USDT'], False, False, False, True, ([], ['USDT'], False, False, False, True,
['ETH/USDT', 'LTC/USDT']), ['ADA/USDT:USDT', 'ETH/USDT:USDT'],
# all markets, quote=USDT, USD 'Futures markets, quote=USDT'),
([], ['USDT', 'USD'], False, False, False, False, ([], ['USDT', 'USD'], False, False, False, False,
['ETH/USDT', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), ['ETH/USDT', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT', 'ADA/USDT:USDT', 'ETH/USDT:USDT'],
# spot markets, quote=USDT, USD 'all markets, quote=USDT, USD'),
([], ['USDT', 'USD'], False, False, True, False, ([], ['USDT', 'USD'], False, False, True, False,
['ETH/USDT', 'LTC/USD', 'LTC/USDT']), ['ETH/USDT', 'LTC/USD', 'LTC/USDT'],
# all markets, base=LTC, quote=USDT 'spot markets, quote=USDT, USD'),
(['LTC'], ['USDT'], False, False, False, False, (['LTC'], ['USDT'], False, False, False, False,
['LTC/USDT', 'XLTCUSDT']), ['LTC/USDT', 'XLTCUSDT'],
# all pairs, base=LTC, quote=USDT 'all markets, base=LTC, quote=USDT'),
(['LTC'], ['USDT'], True, False, False, False, (['LTC'], ['USDT'], True, False, False, False,
['LTC/USDT']), ['LTC/USDT'],
# all markets, base=LTC, quote=USDT, NONEXISTENT 'all pairs, base=LTC, quote=USDT'),
(['LTC'], ['USDT', 'NONEXISTENT'], False, False, False, False, (['LTC'], ['USDT', 'NONEXISTENT'], False, False, False, False,
['LTC/USDT', 'XLTCUSDT']), ['LTC/USDT', 'XLTCUSDT'],
# all markets, base=LTC, quote=NONEXISTENT 'all markets, base=LTC, quote=USDT, NONEXISTENT'),
(['LTC'], ['NONEXISTENT'], False, False, False, False, (['LTC'], ['NONEXISTENT'], False, False, False, False,
[]), [],
'all markets, base=LTC, quote=NONEXISTENT'),
]) ])
def test_get_markets(default_conf, mocker, markets_static, def test_get_markets(default_conf, mocker, markets_static,
base_currencies, quote_currencies, tradable_only, active_only, base_currencies, quote_currencies, tradable_only, active_only,
spot_only, futures_only, spot_only, futures_only, expected_keys,
expected_keys): test_comment # Here for debugging purposes (Not used within method)
):
mocker.patch.multiple('freqtrade.exchange.Exchange', mocker.patch.multiple('freqtrade.exchange.Exchange',
_init_ccxt=MagicMock(return_value=MagicMock()), _init_ccxt=MagicMock(return_value=MagicMock()),
_load_async_markets=MagicMock(), _load_async_markets=MagicMock(),
@ -3221,6 +3235,7 @@ def test_market_is_tradable(
'future': futures, 'future': futures,
'swap': futures, 'swap': futures,
'margin': margin, 'margin': margin,
'linear': True,
**(add_dict), **(add_dict),
} }
assert ex.market_is_tradable(market) == expected_result assert ex.market_is_tradable(market) == expected_result
@ -3486,9 +3501,9 @@ def test_set_margin_mode(mocker, default_conf, margin_mode):
("binance", TradingMode.FUTURES, MarginMode.ISOLATED, False), ("binance", TradingMode.FUTURES, MarginMode.ISOLATED, False),
("gateio", TradingMode.FUTURES, MarginMode.ISOLATED, False), ("gateio", TradingMode.FUTURES, MarginMode.ISOLATED, False),
("okx", TradingMode.FUTURES, MarginMode.ISOLATED, False),
# * Remove once implemented # * Remove once implemented
("okx", TradingMode.FUTURES, MarginMode.ISOLATED, True),
("binance", TradingMode.MARGIN, MarginMode.CROSS, True), ("binance", TradingMode.MARGIN, MarginMode.CROSS, True),
("binance", TradingMode.FUTURES, MarginMode.CROSS, True), ("binance", TradingMode.FUTURES, MarginMode.CROSS, True),
("kraken", TradingMode.MARGIN, MarginMode.CROSS, True), ("kraken", TradingMode.MARGIN, MarginMode.CROSS, True),
@ -3499,7 +3514,6 @@ def test_set_margin_mode(mocker, default_conf, margin_mode):
("gateio", TradingMode.FUTURES, MarginMode.CROSS, True), ("gateio", TradingMode.FUTURES, MarginMode.CROSS, True),
# * Uncomment once implemented # * Uncomment once implemented
# ("okx", TradingMode.FUTURES, MarginMode.ISOLATED, False),
# ("binance", TradingMode.MARGIN, MarginMode.CROSS, False), # ("binance", TradingMode.MARGIN, MarginMode.CROSS, False),
# ("binance", TradingMode.FUTURES, MarginMode.CROSS, False), # ("binance", TradingMode.FUTURES, MarginMode.CROSS, False),
# ("kraken", TradingMode.MARGIN, MarginMode.CROSS, False), # ("kraken", TradingMode.MARGIN, MarginMode.CROSS, False),
@ -3561,9 +3575,12 @@ def test__ccxt_config(
("LTC/BTC", 0.0, 1.0), ("LTC/BTC", 0.0, 1.0),
("TKN/USDT", 210.30, 1.0), ("TKN/USDT", 210.30, 1.0),
]) ])
def test_get_max_leverage(default_conf, mocker, pair, nominal_value, max_lev): def test_get_max_leverage_from_margin(default_conf, mocker, pair, nominal_value, max_lev):
# Binance has a different method of getting the max leverage default_conf['trading_mode'] = 'margin'
exchange = get_patched_exchange(mocker, default_conf, id="kraken") default_conf['margin_mode'] = 'isolated'
api_mock = MagicMock()
type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': False})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio")
assert exchange.get_max_leverage(pair, nominal_value) == max_lev assert exchange.get_max_leverage(pair, nominal_value) == max_lev
@ -3834,13 +3851,13 @@ def test__fetch_and_calculate_funding_fees_datetime_called(
('XLTCUSDT', 1, 'spot'), ('XLTCUSDT', 1, 'spot'),
('LTC/USD', 1, 'futures'), ('LTC/USD', 1, 'futures'),
('XLTCUSDT', 0.01, 'futures'), ('XLTCUSDT', 0.01, 'futures'),
('LTC/ETH', 1, 'futures'),
('ETH/USDT:USDT', 10, 'futures') ('ETH/USDT:USDT', 10, 'futures')
]) ])
def test__get_contract_size(mocker, default_conf, pair, expected_size, trading_mode): def test__get_contract_size(mocker, default_conf, pair, expected_size, trading_mode):
api_mock = MagicMock() api_mock = MagicMock()
default_conf['trading_mode'] = trading_mode default_conf['trading_mode'] = trading_mode
default_conf['margin_mode'] = 'isolated' default_conf['margin_mode'] = 'isolated'
exchange = get_patched_exchange(mocker, default_conf, api_mock)
mocker.patch('freqtrade.exchange.Exchange.markets', { mocker.patch('freqtrade.exchange.Exchange.markets', {
'LTC/USD': { 'LTC/USD': {
'symbol': 'LTC/USD', 'symbol': 'LTC/USD',
@ -3850,15 +3867,11 @@ def test__get_contract_size(mocker, default_conf, pair, expected_size, trading_m
'symbol': 'XLTCUSDT', 'symbol': 'XLTCUSDT',
'contractSize': '0.01', 'contractSize': '0.01',
}, },
'LTC/ETH': {
'symbol': 'LTC/ETH',
},
'ETH/USDT:USDT': { 'ETH/USDT:USDT': {
'symbol': 'ETH/USDT:USDT', 'symbol': 'ETH/USDT:USDT',
'contractSize': '10', 'contractSize': '10',
} }
}) })
exchange = get_patched_exchange(mocker, default_conf, api_mock)
size = exchange._get_contract_size(pair) size = exchange._get_contract_size(pair)
assert expected_size == size assert expected_size == size
@ -3866,7 +3879,7 @@ def test__get_contract_size(mocker, default_conf, pair, expected_size, trading_m
@pytest.mark.parametrize('pair,contract_size,trading_mode', [ @pytest.mark.parametrize('pair,contract_size,trading_mode', [
('XLTCUSDT', 1, 'spot'), ('XLTCUSDT', 1, 'spot'),
('LTC/USD', 1, 'futures'), ('LTC/USD', 1, 'futures'),
('XLTCUSDT', 0.01, 'futures'), ('ADA/USDT:USDT', 0.01, 'futures'),
('LTC/ETH', 1, 'futures'), ('LTC/ETH', 1, 'futures'),
('ETH/USDT:USDT', 10, 'futures'), ('ETH/USDT:USDT', 10, 'futures'),
]) ])
@ -3950,7 +3963,7 @@ def test__order_contracts_to_amount(
@pytest.mark.parametrize('pair,contract_size,trading_mode', [ @pytest.mark.parametrize('pair,contract_size,trading_mode', [
('XLTCUSDT', 1, 'spot'), ('XLTCUSDT', 1, 'spot'),
('LTC/USD', 1, 'futures'), ('LTC/USD', 1, 'futures'),
('XLTCUSDT', 0.01, 'futures'), ('ADA/USDT:USDT', 0.01, 'futures'),
('LTC/ETH', 1, 'futures'), ('LTC/ETH', 1, 'futures'),
('ETH/USDT:USDT', 10, 'futures'), ('ETH/USDT:USDT', 10, 'futures'),
]) ])
@ -3985,7 +3998,7 @@ def test__trades_contracts_to_amount(
@pytest.mark.parametrize('pair,param_amount,param_size', [ @pytest.mark.parametrize('pair,param_amount,param_size', [
('XLTCUSDT', 40, 4000), ('ADA/USDT:USDT', 40, 4000),
('LTC/ETH', 30, 30), ('LTC/ETH', 30, 30),
('LTC/USD', 30, 30), ('LTC/USD', 30, 30),
('ETH/USDT:USDT', 10, 1), ('ETH/USDT:USDT', 10, 1),
@ -4001,6 +4014,7 @@ def test__amount_to_contracts(
api_mock = MagicMock() api_mock = MagicMock()
default_conf['trading_mode'] = 'spot' default_conf['trading_mode'] = 'spot'
default_conf['margin_mode'] = 'isolated' default_conf['margin_mode'] = 'isolated'
exchange = get_patched_exchange(mocker, default_conf, api_mock)
mocker.patch('freqtrade.exchange.Exchange.markets', { mocker.patch('freqtrade.exchange.Exchange.markets', {
'LTC/USD': { 'LTC/USD': {
'symbol': 'LTC/USD', 'symbol': 'LTC/USD',
@ -4018,7 +4032,6 @@ def test__amount_to_contracts(
'contractSize': '10', 'contractSize': '10',
} }
}) })
exchange = get_patched_exchange(mocker, default_conf, api_mock)
result_size = exchange._amount_to_contracts(pair, param_amount) result_size = exchange._amount_to_contracts(pair, param_amount)
assert result_size == param_amount assert result_size == param_amount
result_amount = exchange._contracts_to_amount(pair, param_size) result_amount = exchange._contracts_to_amount(pair, param_size)
@ -4210,6 +4223,7 @@ def test_get_max_pair_stake_amount(
mocker.patch('freqtrade.exchange.Exchange.markets', markets) mocker.patch('freqtrade.exchange.Exchange.markets', markets)
assert exchange.get_max_pair_stake_amount('XRP/USDT:USDT', 2.0) == 20000 assert exchange.get_max_pair_stake_amount('XRP/USDT:USDT', 2.0) == 20000
assert exchange.get_max_pair_stake_amount('XRP/USDT:USDT', 2.0, 5) == 4000
assert exchange.get_max_pair_stake_amount('LTC/USDT:USDT', 2.0) == float('inf') assert exchange.get_max_pair_stake_amount('LTC/USDT:USDT', 2.0) == float('inf')
assert exchange.get_max_pair_stake_amount('ETH/USDT:USDT', 2.0) == 200 assert exchange.get_max_pair_stake_amount('ETH/USDT:USDT', 2.0) == 200
assert exchange.get_max_pair_stake_amount('DOGE/USDT:USDT', 2.0) == 500 assert exchange.get_max_pair_stake_amount('DOGE/USDT:USDT', 2.0) == 500
@ -4220,3 +4234,275 @@ def test_get_max_pair_stake_amount(
mocker.patch('freqtrade.exchange.Exchange.markets', markets) mocker.patch('freqtrade.exchange.Exchange.markets', markets)
assert exchange.get_max_pair_stake_amount('BTC/USDT', 2.0) == 20000 assert exchange.get_max_pair_stake_amount('BTC/USDT', 2.0) == 20000
assert exchange.get_max_pair_stake_amount('ADA/USDT', 2.0) == 500 assert exchange.get_max_pair_stake_amount('ADA/USDT', 2.0) == 500
@pytest.mark.parametrize('exchange_name', EXCHANGES)
def test_load_leverage_tiers(mocker, default_conf, leverage_tiers, exchange_name):
api_mock = MagicMock()
api_mock.fetch_leverage_tiers = MagicMock()
type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_margin_mode')
api_mock.fetch_leverage_tiers = MagicMock(return_value={
'ADA/USDT:USDT': [
{
'tier': 1,
'notionalFloor': 0,
'notionalCap': 500,
'maintenanceMarginRate': 0.02,
'maxLeverage': 75,
'info': {
'baseMaxLoan': '',
'imr': '0.013',
'instId': '',
'maxLever': '75',
'maxSz': '500',
'minSz': '0',
'mmr': '0.01',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '1',
'uly': 'ADA-USDT'
}
},
]
})
# SPOT
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
assert exchange.load_leverage_tiers() == {}
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
if exchange_name != 'binance':
# FUTURES has.fetchLeverageTiers == False
type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': False})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
assert exchange.load_leverage_tiers() == {}
# FUTURES regular
type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
assert exchange.load_leverage_tiers() == {
'ADA/USDT:USDT': [
{
'tier': 1,
'notionalFloor': 0,
'notionalCap': 500,
'maintenanceMarginRate': 0.02,
'maxLeverage': 75,
'info': {
'baseMaxLoan': '',
'imr': '0.013',
'instId': '',
'maxLever': '75',
'maxSz': '500',
'minSz': '0',
'mmr': '0.01',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '1',
'uly': 'ADA-USDT'
}
},
]
}
ccxt_exceptionhandlers(
mocker,
default_conf,
api_mock,
exchange_name,
"load_leverage_tiers",
"fetch_leverage_tiers",
)
def test_parse_leverage_tier(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf)
tier = {
"tier": 1,
"notionalFloor": 0,
"notionalCap": 100000,
"maintenanceMarginRate": 0.025,
"maxLeverage": 20,
"info": {
"bracket": "1",
"initialLeverage": "20",
"notionalCap": "100000",
"notionalFloor": "0",
"maintMarginRatio": "0.025",
"cum": "0.0"
}
}
assert exchange.parse_leverage_tier(tier) == {
"min": 0,
"max": 100000,
"mmr": 0.025,
"lev": 20,
"maintAmt": 0.0,
}
tier2 = {
'tier': 1,
'notionalFloor': 0,
'notionalCap': 2000,
'maintenanceMarginRate': 0.01,
'maxLeverage': 75,
'info': {
'baseMaxLoan': '',
'imr': '0.013',
'instId': '',
'maxLever': '75',
'maxSz': '2000',
'minSz': '0',
'mmr': '0.01',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '1',
'uly': 'SHIB-USDT'
}
}
assert exchange.parse_leverage_tier(tier2) == {
'min': 0,
'max': 2000,
'mmr': 0.01,
'lev': 75,
"maintAmt": None,
}
def test_get_maintenance_ratio_and_amt_exceptions(mocker, default_conf, leverage_tiers):
api_mock = MagicMock()
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange._leverage_tiers = leverage_tiers
with pytest.raises(
OperationalException,
match='nominal value can not be lower than 0',
):
exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT', -1)
exchange._leverage_tiers = {}
with pytest.raises(
InvalidOrderException,
match="Maintenance margin rate for 1000SHIB/USDT is unavailable for",
):
exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT', 10000)
@pytest.mark.parametrize('pair,value,mmr,maintAmt', [
('ADA/BUSD', 500, 0.025, 0.0),
('ADA/BUSD', 20000000, 0.5, 1527500.0),
('ZEC/USDT', 500, 0.01, 0.0),
('ZEC/USDT', 20000000, 0.5, 654500.0),
])
def test_get_maintenance_ratio_and_amt(
mocker,
default_conf,
leverage_tiers,
pair,
value,
mmr,
maintAmt
):
api_mock = MagicMock()
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange._leverage_tiers = leverage_tiers
exchange.get_maintenance_ratio_and_amt(pair, value) == (mmr, maintAmt)
def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers):
# Test Spot
exchange = get_patched_exchange(mocker, default_conf, id="binance")
assert exchange.get_max_leverage("BNB/USDT", 100.0) == 1.0
# Test Futures
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
exchange = get_patched_exchange(mocker, default_conf, id="binance")
exchange._leverage_tiers = leverage_tiers
assert exchange.get_max_leverage("BNB/BUSD", 1.0) == 20.0
assert exchange.get_max_leverage("BNB/USDT", 100.0) == 75.0
assert exchange.get_max_leverage("BTC/USDT", 170.30) == 125.0
assert isclose(exchange.get_max_leverage("BNB/BUSD", 99999.9), 5.000005)
assert isclose(exchange.get_max_leverage("BNB/USDT", 1500), 33.333333333333333)
assert exchange.get_max_leverage("BTC/USDT", 300000000) == 2.0
assert exchange.get_max_leverage("BTC/USDT", 600000000) == 1.0 # Last tier
assert exchange.get_max_leverage("SPONGE/USDT", 200) == 1.0 # Pair not in leverage_tiers
assert exchange.get_max_leverage("BTC/USDT", 0.0) == 125.0 # No stake amount
with pytest.raises(
InvalidOrderException,
match=r'Amount 1000000000.01 too high for BTC/USDT'
):
exchange.get_max_leverage("BTC/USDT", 1000000000.01)
@pytest.mark.parametrize("exchange_name", ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okx'])
def test__get_params(mocker, default_conf, exchange_name):
api_mock = MagicMock()
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange._params = {'test': True}
params1 = {'test': True}
params2 = {
'test': True,
'timeInForce': 'ioc',
'reduceOnly': True,
}
if exchange_name == 'kraken':
params2['leverage'] = 3.0
if exchange_name == 'okx':
params2['tdMode'] = 'isolated'
assert exchange._get_params(
ordertype='market',
reduceOnly=False,
time_in_force='gtc',
leverage=1.0,
) == params1
assert exchange._get_params(
ordertype='market',
reduceOnly=False,
time_in_force='ioc',
leverage=1.0,
) == params1
assert exchange._get_params(
ordertype='limit',
reduceOnly=False,
time_in_force='gtc',
leverage=1.0,
) == params1
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange._params = {'test': True}
assert exchange._get_params(
ordertype='limit',
reduceOnly=True,
time_in_force='ioc',
leverage=3.0,
) == params2

View File

@ -64,3 +64,16 @@ def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_rat
) )
) )
assert exchange.get_maintenance_ratio_and_amt(pair) == (mm_ratio, None) assert exchange.get_maintenance_ratio_and_amt(pair) == (mm_ratio, None)
@pytest.mark.parametrize('pair,nominal_value,max_lev', [
("ETH/BTC", 0.0, 2.0),
("TKN/BTC", 100.0, 5.0),
("BLK/BTC", 173.31, 3.0),
("LTC/BTC", 0.0, 1.0),
("TKN/USDT", 210.30, 1.0),
])
def test_get_max_leverage_gateio(default_conf, mocker, pair, nominal_value, max_lev):
# Binance has a different method of getting the max leverage
exchange = get_patched_exchange(mocker, default_conf, id="gateio")
assert exchange.get_max_leverage(pair, nominal_value) == max_lev

360
tests/exchange/test_okx.py Normal file
View File

@ -0,0 +1,360 @@
from unittest.mock import MagicMock # , PropertyMock
from freqtrade.enums import MarginMode, TradingMode
from tests.conftest import get_patched_exchange
def test_get_maintenance_ratio_and_amt_okx(
default_conf,
mocker,
):
api_mock = MagicMock()
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
default_conf['dry_run'] = False
mocker.patch.multiple(
'freqtrade.exchange.Okx',
exchange_has=MagicMock(return_value=True),
load_leverage_tiers=MagicMock(return_value={
'ETH/USDT:USDT': [
{
'tier': 1,
'notionalFloor': 0,
'notionalCap': 2000,
'maintenanceMarginRate': 0.01,
'maxLeverage': 75,
'info': {
'baseMaxLoan': '',
'imr': '0.013',
'instId': '',
'maxLever': '75',
'maxSz': '2000',
'minSz': '0',
'mmr': '0.01',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '1',
'uly': 'ETH-USDT'
}
},
{
'tier': 2,
'notionalFloor': 2001,
'notionalCap': 4000,
'maintenanceMarginRate': 0.015,
'maxLeverage': 50,
'info': {
'baseMaxLoan': '',
'imr': '0.02',
'instId': '',
'maxLever': '50',
'maxSz': '4000',
'minSz': '2001',
'mmr': '0.015',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '2',
'uly': 'ETH-USDT'
}
},
{
'tier': 3,
'notionalFloor': 4001,
'notionalCap': 8000,
'maintenanceMarginRate': 0.02,
'maxLeverage': 20,
'info': {
'baseMaxLoan': '',
'imr': '0.05',
'instId': '',
'maxLever': '20',
'maxSz': '8000',
'minSz': '4001',
'mmr': '0.02',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '3',
'uly': 'ETH-USDT'
}
},
],
'ADA/USDT:USDT': [
{
'tier': 1,
'notionalFloor': 0,
'notionalCap': 500,
'maintenanceMarginRate': 0.02,
'maxLeverage': 75,
'info': {
'baseMaxLoan': '',
'imr': '0.013',
'instId': '',
'maxLever': '75',
'maxSz': '500',
'minSz': '0',
'mmr': '0.01',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '1',
'uly': 'ADA-USDT'
}
},
{
'tier': 2,
'notionalFloor': 501,
'notionalCap': 1000,
'maintenanceMarginRate': 0.025,
'maxLeverage': 50,
'info': {
'baseMaxLoan': '',
'imr': '0.02',
'instId': '',
'maxLever': '50',
'maxSz': '1000',
'minSz': '501',
'mmr': '0.015',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '2',
'uly': 'ADA-USDT'
}
},
{
'tier': 3,
'notionalFloor': 1001,
'notionalCap': 2000,
'maintenanceMarginRate': 0.03,
'maxLeverage': 20,
'info': {
'baseMaxLoan': '',
'imr': '0.05',
'instId': '',
'maxLever': '20',
'maxSz': '2000',
'minSz': '1001',
'mmr': '0.02',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '3',
'uly': 'ADA-USDT'
}
},
]
})
)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="okx")
assert exchange.get_maintenance_ratio_and_amt('ETH/USDT:USDT', 2000) == (0.01, None)
assert exchange.get_maintenance_ratio_and_amt('ETH/USDT:USDT', 2001) == (0.015, None)
assert exchange.get_maintenance_ratio_and_amt('ETH/USDT:USDT', 4001) == (0.02, None)
assert exchange.get_maintenance_ratio_and_amt('ETH/USDT:USDT', 8000) == (0.02, None)
assert exchange.get_maintenance_ratio_and_amt('ADA/USDT:USDT', 1) == (0.02, None)
assert exchange.get_maintenance_ratio_and_amt('ADA/USDT:USDT', 2000) == (0.03, None)
def test_get_max_pair_stake_amount_okx(default_conf, mocker, leverage_tiers):
exchange = get_patched_exchange(mocker, default_conf, id="okx")
assert exchange.get_max_pair_stake_amount('BNB/BUSD', 1.0) == float('inf')
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
exchange = get_patched_exchange(mocker, default_conf, id="okx")
exchange._leverage_tiers = leverage_tiers
assert exchange.get_max_pair_stake_amount('BNB/BUSD', 1.0) == 30000000
assert exchange.get_max_pair_stake_amount('BNB/USDT', 1.0) == 50000000
assert exchange.get_max_pair_stake_amount('BTC/USDT', 1.0) == 1000000000
assert exchange.get_max_pair_stake_amount('BTC/USDT', 1.0, 10.0) == 100000000
assert exchange.get_max_pair_stake_amount('TTT/USDT', 1.0) == float('inf') # Not in tiers
def test_load_leverage_tiers_okx(default_conf, mocker, markets):
api_mock = MagicMock()
api_mock.fetch_leverage_tiers = MagicMock(side_effect=[
{
'ADA/USDT:USDT': [
{
'tier': 1,
'notionalFloor': 0,
'notionalCap': 500,
'maintenanceMarginRate': 0.02,
'maxLeverage': 75,
'info': {
'baseMaxLoan': '',
'imr': '0.013',
'instId': '',
'maxLever': '75',
'maxSz': '500',
'minSz': '0',
'mmr': '0.01',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '1',
'uly': 'ADA-USDT'
}
},
{
'tier': 2,
'notionalFloor': 501,
'notionalCap': 1000,
'maintenanceMarginRate': 0.025,
'maxLeverage': 50,
'info': {
'baseMaxLoan': '',
'imr': '0.02',
'instId': '',
'maxLever': '50',
'maxSz': '1000',
'minSz': '501',
'mmr': '0.015',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '2',
'uly': 'ADA-USDT'
}
},
{
'tier': 3,
'notionalFloor': 1001,
'notionalCap': 2000,
'maintenanceMarginRate': 0.03,
'maxLeverage': 20,
'info': {
'baseMaxLoan': '',
'imr': '0.05',
'instId': '',
'maxLever': '20',
'maxSz': '2000',
'minSz': '1001',
'mmr': '0.02',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '3',
'uly': 'ADA-USDT'
}
},
]
},
{
'ETH/USDT:USDT': [
{
'tier': 1,
'notionalFloor': 0,
'notionalCap': 2000,
'maintenanceMarginRate': 0.01,
'maxLeverage': 75,
'info': {
'baseMaxLoan': '',
'imr': '0.013',
'instId': '',
'maxLever': '75',
'maxSz': '2000',
'minSz': '0',
'mmr': '0.01',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '1',
'uly': 'ETH-USDT'
}
},
{
'tier': 2,
'notionalFloor': 2001,
'notionalCap': 4000,
'maintenanceMarginRate': 0.015,
'maxLeverage': 50,
'info': {
'baseMaxLoan': '',
'imr': '0.02',
'instId': '',
'maxLever': '50',
'maxSz': '4000',
'minSz': '2001',
'mmr': '0.015',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '2',
'uly': 'ETH-USDT'
}
},
{
'tier': 3,
'notionalFloor': 4001,
'notionalCap': 8000,
'maintenanceMarginRate': 0.02,
'maxLeverage': 20,
'info': {
'baseMaxLoan': '',
'imr': '0.05',
'instId': '',
'maxLever': '20',
'maxSz': '8000',
'minSz': '4001',
'mmr': '0.02',
'optMgnFactor': '0',
'quoteMaxLoan': '',
'tier': '3',
'uly': 'ETH-USDT'
}
},
]
},
])
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
default_conf['stake_currency'] = 'USDT'
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="okx")
exchange.trading_mode = TradingMode.FUTURES
exchange.margin_mode = MarginMode.ISOLATED
exchange.markets = markets
# Initialization of load_leverage_tiers happens as part of exchange init.
assert exchange._leverage_tiers == {
'ADA/USDT:USDT': [
{
'min': 0,
'max': 500,
'mmr': 0.02,
'lev': 75,
'maintAmt': None
},
{
'min': 501,
'max': 1000,
'mmr': 0.025,
'lev': 50,
'maintAmt': None
},
{
'min': 1001,
'max': 2000,
'mmr': 0.03,
'lev': 20,
'maintAmt': None
},
],
'ETH/USDT:USDT': [
{
'min': 0,
'max': 2000,
'mmr': 0.01,
'lev': 75,
'maintAmt': None
},
{
'min': 2001,
'max': 4000,
'mmr': 0.015,
'lev': 50,
'maintAmt': None
},
{
'min': 4001,
'max': 8000,
'mmr': 0.02,
'lev': 20,
'maintAmt': None
},
],
}

View File

@ -852,6 +852,7 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None:
'spaces': ['all'] 'spaces': ['all']
}) })
hyperopt = Hyperopt(hyperopt_conf) hyperopt = Hyperopt(hyperopt_conf)
hyperopt.backtesting.exchange.get_max_leverage = MagicMock(return_value=1.0)
assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto) assert isinstance(hyperopt.custom_hyperopt, HyperOptAuto)
assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter) assert isinstance(hyperopt.backtesting.strategy.buy_rsi, IntParameter)

View File

@ -720,8 +720,8 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker)
(False, 'futures', 'binance', 'isolated', 0.05, 8.167171717171717), (False, 'futures', 'binance', 'isolated', 0.05, 8.167171717171717),
(True, 'futures', 'gateio', 'isolated', 0.05, 11.7804274688304), (True, 'futures', 'gateio', 'isolated', 0.05, 11.7804274688304),
(False, 'futures', 'gateio', 'isolated', 0.05, 8.181423084697796), (False, 'futures', 'gateio', 'isolated', 0.05, 8.181423084697796),
# (True, 'futures', 'okex', 'isolated', 11.87413417771621), (True, 'futures', 'okx', 'isolated', 0.0, 11.87413417771621),
# (False, 'futures', 'okex', 'isolated', 8.085708510208207), (False, 'futures', 'okx', 'isolated', 0.0, 8.085708510208207),
]) ])
def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
limit_order_open, is_short, trading_mode, limit_order_open, is_short, trading_mode,
@ -774,9 +774,16 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
}), }),
create_order=enter_mm, create_order=enter_mm,
get_min_pair_stake_amount=MagicMock(return_value=1), get_min_pair_stake_amount=MagicMock(return_value=1),
get_max_pair_stake_amount=MagicMock(return_value=500000),
get_fee=fee, get_fee=fee,
get_funding_fees=MagicMock(return_value=0), get_funding_fees=MagicMock(return_value=0),
name=exchange_name name=exchange_name,
get_maintenance_ratio_and_amt=MagicMock(return_value=(0.01, 0.01)),
get_max_leverage=MagicMock(return_value=10),
)
mocker.patch.multiple(
'freqtrade.exchange.Okx',
get_max_pair_stake_amount=MagicMock(return_value=500000),
) )
pair = 'ETH/USDT' pair = 'ETH/USDT'
@ -5088,6 +5095,7 @@ def test_update_funding_fees(
create_order=enter_mm, create_order=enter_mm,
get_min_pair_stake_amount=MagicMock(return_value=1), get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee, get_fee=fee,
get_maintenance_ratio_and_amt=MagicMock(return_value=(0.01, 0.01)),
) )
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)