strengthened and fixed leverage_tier tests

This commit is contained in:
Sam Germain 2022-02-10 06:10:15 -06:00
parent a6043e6a85
commit 03b3756e4b
4 changed files with 140 additions and 54 deletions

View File

@ -1899,22 +1899,22 @@ class Exchange:
# When exchanges can load all their leverage tiers at once in the constructor # When exchanges can load all their leverage tiers at once in the constructor
# then this method does nothing, it should only be implemented when the leverage # then this method does nothing, it should only be implemented when the leverage
# tiers requires per symbol fetching to avoid excess api calls # tiers requires per symbol fetching to avoid excess api calls
if pair not in self._leverage_tiers:
self._leverage_tiers[pair] = []
if ( if (
self._api.has['fetchLeverageTiers'] and self._api.has['fetchLeverageTiers'] and
not self._ft_has['can_fetch_multiple_tiers'] and not self._ft_has['can_fetch_multiple_tiers'] and
self.trading_mode == TradingMode.FUTURES self.trading_mode == TradingMode.FUTURES
): ):
self._leverage_tiers[pair] = []
try: try:
tiers = self._api.fetch_leverage_tiers(pair) tiers = self._api.fetch_leverage_tiers(pair)
for tier in tiers[pair]: for tier in tiers[pair]:
self._leverage_tiers[pair].append(self.parse_leverage_tier(tier)) self._leverage_tiers[pair].append(self.parse_leverage_tier(tier))
return tiers
except ccxt.BadRequest: except ccxt.BadRequest:
return [] return []
else:
return [] return self._leverage_tiers[pair]
def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float: def get_max_leverage(self, pair: str, stake_amount: Optional[float]) -> float:
""" """
@ -1934,10 +1934,7 @@ class Exchange:
f'{self.name}.get_max_leverage requires argument stake_amount' f'{self.name}.get_max_leverage requires argument stake_amount'
) )
if pair not in self._leverage_tiers: pair_tiers = self.get_leverage_tiers_for_pair(pair)
self.get_leverage_tiers_for_pair(pair)
pair_tiers = self._leverage_tiers[pair]
num_tiers = len(pair_tiers) num_tiers = len(pair_tiers)
if num_tiers < 1: if num_tiers < 1:
return 1.0 return 1.0
@ -2282,23 +2279,30 @@ class Exchange:
""" """
if self._api.has['fetchLeverageTiers']: if self._api.has['fetchLeverageTiers']:
if pair not in self._leverage_tiers:
# Used when fetchLeverageTiers cannot fetch all symbols at once pair_tiers = self.get_leverage_tiers_for_pair(pair)
tiers = self.get_leverage_tiers_for_pair(pair)
if not bool(tiers): if len(pair_tiers) < 1:
raise InvalidOrderException(f"Cannot calculate liquidation price for {pair}") raise InvalidOrderException(
pair_tiers = self._leverage_tiers[pair] f"Maintenance margin rate for {pair} is unavailable for {self.name}"
)
for tier in reversed(pair_tiers): for tier in reversed(pair_tiers):
if nominal_value >= tier['min']: if nominal_value >= tier['min']:
return (tier['mmr'], tier['maintAmt']) return (tier['mmr'], tier['maintAmt'])
raise OperationalException("nominal value can not be lower than 0") 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 # 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 # describes the min amt for a tier, and the lowest tier will always go down to 0
else: else:
if pair not in self.markets:
raise InvalidOrderException(
f"{pair} is not tradeable on {self.name} {self.trading_mode.value}"
)
mmr = self.markets[pair]['maintenanceMarginRate'] mmr = self.markets[pair]['maintenanceMarginRate']
if mmr is None: if mmr is None:
raise OperationalException( raise InvalidOrderException(
f"Maintenance margin rate is unavailable for {self.name}" f"Maintenance margin rate for {pair} is unavailable for {self.name}"
) )
return (mmr, None) return (mmr, None)

View File

@ -403,7 +403,6 @@ def test_fill_leverage_tiers_binance(default_conf, mocker):
} }
} }
], ],
}) })
default_conf['dry_run'] = False default_conf['dry_run'] = False
default_conf['trading_mode'] = TradingMode.FUTURES default_conf['trading_mode'] = TradingMode.FUTURES

View File

@ -4300,15 +4300,63 @@ def test_parse_leverage_tier(mocker, default_conf):
} }
def test_get_leverage_tiers_for_pair(mocker, default_conf, leverage_tiers): def test_get_leverage_tiers_for_pair(
mocker,
default_conf,
leverage_tiers,
):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.fetch_leverage_tiers = MagicMock() api_mock.fetch_leverage_tiers = MagicMock(return_value={
'DOGE/USDT:USDT': [
{
'tier': 1,
'notionalFloor': 0,
'notionalCap': 500,
'maintenanceMarginRatio': 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': 'DOGE-USDT'
}
},
{
'tier': 2,
'notionalFloor': 501,
'notionalCap': 1000,
'maintenanceMarginRatio': 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': 'DOGE-USDT'
}
}
],
})
# Spot # Spot
type(api_mock)._ft_has = PropertyMock(return_value={'fetchLeverageTiers': True}) type(api_mock)._ft_has = PropertyMock(return_value={'fetchLeverageTiers': True})
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange._ft_has['can_fetch_multiple_tiers'] = False exchange._ft_has['can_fetch_multiple_tiers'] = False
assert exchange.get_leverage_tiers_for_pair('ADA/USDT') == [] assert exchange.get_leverage_tiers_for_pair('DOGE/USDT:USDT') == []
# 'can_fetch_multiple_tiers': True # 'can_fetch_multiple_tiers': True
default_conf['trading_mode'] = 'futures' default_conf['trading_mode'] = 'futures'
@ -4316,20 +4364,36 @@ def test_get_leverage_tiers_for_pair(mocker, default_conf, leverage_tiers):
type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True}) type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True})
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange._ft_has['can_fetch_multiple_tiers'] = True exchange._ft_has['can_fetch_multiple_tiers'] = True
assert exchange.get_leverage_tiers_for_pair('ADA/USDT:USDT') == [] assert exchange.get_leverage_tiers_for_pair('DOGE/USDT:USDT') == []
# 'fetchLeverageTiers': False # 'fetchLeverageTiers': False
type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': False}) type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': False})
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange._ft_has['can_fetch_multiple_tiers'] = False exchange._ft_has['can_fetch_multiple_tiers'] = False
assert exchange.get_leverage_tiers_for_pair('ADA/USDT:USDT') == [] assert exchange.get_leverage_tiers_for_pair('DOGE/USDT:USDT') == []
# 'fetchLeverageTiers': False # 'fetchLeverageTiers': True
type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True}) type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True})
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange._ft_has['can_fetch_multiple_tiers'] = False exchange._ft_has['can_fetch_multiple_tiers'] = False
assert exchange.get_leverage_tiers_for_pair('ADA/USDT:USDT') != [] assert exchange.get_leverage_tiers_for_pair('DOGE/USDT:USDT') == [
{
'min': 0,
'max': 500,
'mmr': 0.02,
'lev': 75,
'maintAmt': None
},
{
'min': 501,
'max': 1000,
'mmr': 0.025,
'lev': 50,
'maintAmt': None
}
]
# exception_handlers
type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True}) type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': True})
default_conf['dry_run'] = False default_conf['dry_run'] = False
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
@ -4341,7 +4405,7 @@ def test_get_leverage_tiers_for_pair(mocker, default_conf, leverage_tiers):
"binance", "binance",
"get_leverage_tiers_for_pair", "get_leverage_tiers_for_pair",
"fetch_leverage_tiers", "fetch_leverage_tiers",
pair='ETH/USDT:USDT', pair='DOGE/USDT:USDT',
) )
@ -4350,23 +4414,22 @@ def test_get_maintenance_ratio_and_amt_exceptions(mocker, default_conf, leverage
default_conf['trading_mode'] = 'futures' default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated' default_conf['margin_mode'] = 'isolated'
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
pair = '1000SHIB/USDT'
exchange._leverage_tiers = {}
exchange.get_leverage_tiers_for_pair = MagicMock(return_value=[])
with pytest.raises(
InvalidOrderException,
match=f"Cannot calculate liquidation price for {pair}",
):
exchange.get_maintenance_ratio_and_amt(pair, 10000)
exchange._leverage_tiers = leverage_tiers exchange._leverage_tiers = leverage_tiers
with pytest.raises( with pytest.raises(
OperationalException, OperationalException,
match='nominal value can not be lower than 0', match='nominal value can not be lower than 0',
): ):
exchange.get_maintenance_ratio_and_amt(pair, -1) exchange.get_maintenance_ratio_and_amt('1000SHIB/USDT', -1)
exchange._leverage_tiers = {}
exchange.get_leverage_tiers_for_pair = MagicMock(return_value=[])
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', [ @ pytest.mark.parametrize('pair,value,mmr,maintAmt', [

View File

@ -2,7 +2,7 @@ from unittest.mock import MagicMock, PropertyMock
import pytest import pytest
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import InvalidOrderException, OperationalException
from freqtrade.exchange import Gateio from freqtrade.exchange import Gateio
from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_patched_exchange from tests.conftest import get_patched_exchange
@ -31,12 +31,10 @@ def test_validate_order_types_gateio(default_conf, mocker):
ExchangeResolver.load_exchange('gateio', default_conf, True) ExchangeResolver.load_exchange('gateio', default_conf, True)
@pytest.mark.parametrize('pair,mm_ratio', [ def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker):
("ETH/USDT:USDT", 0.005),
("ADA/USDT:USDT", 0.003),
])
def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_ratio):
api_mock = MagicMock() api_mock = MagicMock()
default_conf['trading_mode'] = 'futures'
default_conf['margin_mode'] = 'isolated'
type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': False}) type(api_mock).has = PropertyMock(return_value={'fetchLeverageTiers': False})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio") exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio")
mocker.patch( mocker.patch(
@ -59,7 +57,29 @@ def test_get_maintenance_ratio_and_amt_gateio(default_conf, mocker, pair, mm_rat
'id': 'ADA_USDT', 'id': 'ADA_USDT',
'symbol': 'ADA/USDT:USDT', 'symbol': 'ADA/USDT:USDT',
}, },
'DOGE/USDT:USDT': {
'taker': 0.0000075,
'maker': -0.0000025,
'maintenanceMarginRate': None,
'info': {},
'id': 'ADA_USDT',
'symbol': 'ADA/USDT:USDT',
},
} }
) )
) )
assert exchange.get_maintenance_ratio_and_amt(pair) == (mm_ratio, None)
assert exchange.get_maintenance_ratio_and_amt("ETH/USDT:USDT") == (0.005, None)
assert exchange.get_maintenance_ratio_and_amt("ADA/USDT:USDT") == (0.003, None)
with pytest.raises(
InvalidOrderException,
match="Maintenance margin rate for DOGE/USDT:USDT is unavailable for Gateio",
):
exchange.get_maintenance_ratio_and_amt('DOGE/USDT:USDT')
with pytest.raises(
InvalidOrderException,
match="SHIB/USDT:USDT is not tradeable on Gateio futures",
):
exchange.get_maintenance_ratio_and_amt('SHIB/USDT:USDT')