Merge pull request #5849 from freqtrade/isolated-liq

Isolated liq
This commit is contained in:
Matthias 2022-02-01 06:36:46 +01:00 committed by GitHub
commit 2141e04a19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 728 additions and 204 deletions

View File

@ -155,7 +155,7 @@ CONF_SCHEMA = {
'ignore_roi_if_buy_signal': {'type': 'boolean'},
'ignore_buying_expired_candle_after': {'type': 'number'},
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
'collateral_type': {'type': 'string', 'enum': COLLATERAL_TYPES},
'collateral': {'type': 'string', 'enum': COLLATERAL_TYPES},
'backtest_breakdown': {
'type': 'array',
'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS}

View File

@ -33,10 +33,9 @@ class Binance(Exchange):
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
# TradingMode.SPOT always supported and not required in this list
# TODO-lev: Uncomment once supported
# (TradingMode.MARGIN, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.ISOLATED)
(TradingMode.FUTURES, Collateral.ISOLATED)
]
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
@ -120,10 +119,25 @@ class Binance(Exchange):
raise OperationalException(e) from e
@retrier
def fill_leverage_brackets(self):
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:
@ -136,17 +150,21 @@ class Binance(Exchange):
else:
leverage_brackets = self._api.load_leverage_brackets()
for pair, brackets in leverage_brackets.items():
self._leverage_brackets[pair] = [
[
min_amount,
float(margin_req)
] for [
min_amount,
margin_req
] in 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:
@ -161,16 +179,22 @@ class Binance(Exchange):
:param pair: The base/quote currency pair being traded
:stake_amount: The total value of the traders collateral 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
min_amount = 0.0
for bracket_num in range(num_brackets):
[_, margin_req] = pair_brackets[bracket_num]
lev = 1/margin_req
[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
[min_amount, _, __] = pair_brackets[bracket_num+1] # Get min_amount of next bracket
else:
return lev
nominal_value = stake_amount * lev
@ -237,3 +261,90 @@ class Binance(Exchange):
:return: The cutoff open time for when a funding fee is charged
"""
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(
self,
pair: str,
open_rate: float, # Entry price of position
is_short: bool,
position: float, # Absolute value of position size
wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
:param exchange_name:
:param open_rate: (EP1) Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param position: Absolute value of position size (in base currency)
:param wallet_balance: (WB)
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
:param maintenance_amt:
# * Only required for Cross
:param mm_ex_1: (TMM)
Cross-Margin Mode: Maintenance Margin of all other contracts, excluding Contract 1
Isolated-Margin Mode: 0
:param upnl_ex_1: (UPNL)
Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1.
Isolated-Margin Mode: 0
"""
side_1 = -1 if is_short else 1
position = abs(position)
cross_vars = upnl_ex_1 - mm_ex_1 if self.collateral == Collateral.CROSS else 0.0
# mm_ratio: Binance's formula specifies maintenance margin rate which is mm_ratio * 100%
# maintenance_amt: (CUM) Maintenance Amount of position
mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt(pair, position)
if (maintenance_amt is None):
raise OperationalException(
"Parameter maintenance_amt is required by Binance.liquidation_price"
f"for {self.trading_mode.value}"
)
if self.trading_mode == TradingMode.FUTURES:
return (
(
(wallet_balance + cross_vars + maintenance_amt) -
(side_1 * position * open_rate)
) / (
(position * mm_ratio) - (side_1 * position)
)
)
else:
raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading")

View File

@ -90,7 +90,7 @@ class Exchange:
self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None
self._markets: Dict = {}
self._leverage_brackets: Dict = {}
self._leverage_brackets: Dict[str, List[List[float]]] = {}
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
@ -633,7 +633,6 @@ class Exchange:
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
based on our definitions.
"""
amount = self._amount_to_contracts(pair, amount)
if self.markets[pair]['precision']['amount']:
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
precision=self.markets[pair]['precision']['amount'],
@ -737,7 +736,7 @@ class Exchange:
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]:
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
_amount = self._contracts_to_amount(pair, self.amount_to_precision(pair, amount))
_amount = self.amount_to_precision(pair, amount)
dry_order: Dict[str, Any] = {
'id': order_id,
'symbol': pair,
@ -901,7 +900,7 @@ class Exchange:
try:
# Set the precision for amount and price(rate) as accepted by the exchange
amount = self.amount_to_precision(pair, amount)
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount))
needs_price = (ordertype != 'market'
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
@ -1983,6 +1982,119 @@ class Exchange:
else:
return 0.0
@retrier
def get_liquidation_price(
self,
pair: str,
# Dry-run
open_rate: float, # Entry price of position
is_short: bool,
position: float, # Absolute value of position size
wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
Set's the margin mode on the exchange to cross or isolated for a specific pair
:param pair: base/quote currency pair (e.g. "ADA/USDT")
"""
if self.trading_mode == TradingMode.SPOT:
return None
elif (self.collateral is None):
raise OperationalException(f'{self.name}.collateral must be set for liquidation_price')
elif (self.trading_mode != TradingMode.FUTURES and self.collateral != Collateral.ISOLATED):
raise OperationalException(
f"{self.name} does not support {self.collateral.value} {self.trading_mode.value}")
if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
return self.dry_run_liquidation_price(
pair=pair,
open_rate=open_rate,
is_short=is_short,
position=position,
wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1,
upnl_ex_1=upnl_ex_1
)
try:
positions = self._api.fetch_positions([pair])
if len(positions) > 0:
pos = positions[0]
return pos['liquidationPrice']
else:
return None
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
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(
self,
pair: str,
open_rate: float, # Entry price of position
is_short: bool,
position: float, # Absolute value of position size
wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
PERPETUAL:
gateio: https://www.gate.io/help/futures/perpetual/22160/calculation-of-liquidation-price
okex: https://www.okex.com/support/hc/en-us/articles/
360053909592-VI-Introduction-to-the-isolated-mode-of-Single-Multi-currency-Portfolio-margin
:param exchange_name:
:param open_rate: Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param position: Absolute value of position size (in base currency)
:param trading_mode: SPOT, MARGIN, FUTURES, etc.
:param collateral: Either ISOLATED or CROSS
:param wallet_balance: Amount of collateral in the wallet being used to trade
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
# * Not required by Gateio or OKX
:param mm_ex_1:
:param upnl_ex_1:
"""
market = self.markets[pair]
taker_fee_rate = market['taker']
mm_ratio, _ = self.get_maintenance_ratio_and_amt(pair, position)
if self.trading_mode == TradingMode.FUTURES and self.collateral == Collateral.ISOLATED:
if market['inverse']:
raise OperationalException(
"Freqtrade does not yet support inverse contracts")
value = wallet_balance / position
mm_ratio_taker = (mm_ratio + taker_fee_rate)
if is_short:
return (open_rate + value) / (1 + mm_ratio_taker)
else:
return (open_rate - value) / (1 - mm_ratio_taker)
else:
raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading")
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
return exchange_name in ccxt_exchanges(ccxt_module)

View File

@ -1,6 +1,6 @@
""" Gate.io exchange subclass """
import logging
from typing import Dict, List, Tuple
from typing import Dict, List, Optional, Tuple
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import OperationalException
@ -31,7 +31,7 @@ class Gateio(Exchange):
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.CROSS),
# (TradingMode.FUTURES, Collateral.ISOLATED)
(TradingMode.FUTURES, Collateral.ISOLATED)
]
def validate_ordertypes(self, order_types: Dict) -> None:
@ -40,3 +40,14 @@ class Gateio(Exchange):
if any(v == 'market' for k, v in order_types.items()):
raise OperationalException(
f'Exchange {self.name} does not support market orders.')
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
"""
info = self.markets[pair]['info']
return (float(info['maintenance_rate']), None)

View File

@ -19,7 +19,7 @@ from freqtrade.edge import Edge
from freqtrade.enums import (Collateral, RPCMessageType, RunMode, SellType, SignalDirection, State,
TradingMode)
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError)
InvalidOrderException, OperationalException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.mixins import LoggingMixin
@ -106,8 +106,8 @@ class FreqtradeBot(LoggingMixin):
self.trading_mode = TradingMode(self.config.get('trading_mode', 'spot'))
self.collateral_type: Optional[Collateral] = None
if 'collateral_type' in self.config:
self.collateral_type = Collateral(self.config['collateral_type'])
if 'collateral' in self.config:
self.collateral_type = Collateral(self.config['collateral'])
self._schedule = Scheduler()
@ -606,29 +606,32 @@ class FreqtradeBot(LoggingMixin):
is_short: bool
) -> Tuple[float, Optional[float]]:
interest_rate = 0.0
isolated_liq = None
# TODO-lev: Uncomment once liq and interest merged in
# if TradingMode == TradingMode.MARGIN:
# interest_rate = self.exchange.get_interest_rate(
# pair=pair,
# open_rate=open_rate,
# is_short=is_short
# )
# if self.collateral_type == Collateral.ISOLATED:
# isolated_liq = liquidation_price(
# exchange_name=self.exchange.name,
# trading_mode=self.trading_mode,
# open_rate=open_rate,
# amount=amount,
# leverage=leverage,
# is_short=is_short
# )
return interest_rate, isolated_liq
if self.trading_mode == TradingMode.SPOT:
return (0.0, None)
elif (
self.collateral_type == Collateral.ISOLATED and
self.trading_mode == TradingMode.FUTURES
):
wallet_balance = (amount * open_rate)/leverage
isolated_liq = self.exchange.get_liquidation_price(
pair=pair,
open_rate=open_rate,
is_short=is_short,
position=amount,
wallet_balance=wallet_balance,
mm_ex_1=0.0,
upnl_ex_1=0.0,
)
return (0.0, isolated_liq)
else:
raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading")
def execute_entry(
self,
@ -1174,8 +1177,8 @@ class FreqtradeBot(LoggingMixin):
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
if not_closed and (fully_cancelled or self.strategy.ft_check_timed_out(
time_method, trade, order, datetime.now(timezone.utc))
):
time_method, trade, order, datetime.now(timezone.utc))
):
if is_entering:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
else:

View File

@ -333,7 +333,7 @@ class LocalTrade():
for key in kwargs:
setattr(self, key, kwargs[key])
if self.isolated_liq:
self.set_isolated_liq(self.isolated_liq)
self.set_isolated_liq(isolated_liq=self.isolated_liq)
self.recalc_open_trade_value()
if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None:
raise OperationalException(

View File

@ -817,27 +817,42 @@ def get_markets():
'symbol': 'ETH/USDT',
'base': 'ETH',
'quote': 'USDT',
'spot': True,
'future': True,
'swap': True,
'margin': True,
'settle': None,
'baseId': 'ETH',
'quoteId': 'USDT',
'settleId': None,
'type': 'spot',
'spot': True,
'margin': True,
'swap': True,
'future': True,
'option': False,
'active': True,
'contract': None,
'linear': None,
'inverse': None,
'taker': 0.0006,
'maker': 0.0002,
'contractSize': None,
'expiry': None,
'expiryDateTime': None,
'strike': None,
'optionType': None,
'precision': {
'amount': 8,
'price': 8
'price': 8,
},
'limits': {
'leverage': {
'min': 1,
'max': 100,
},
'amount': {
'min': 0.02214286,
'max': None
'max': None,
},
'price': {
'min': 1e-08,
'max': None
},
'leverage': {
'min': None,
'max': None,
},
'cost': {
@ -845,8 +860,9 @@ def get_markets():
'max': None,
},
},
'active': True,
'info': {},
'info': {
'maintenance_rate': '0.005',
},
},
'LTC/USDT': {
'id': 'USDT-LTC',
@ -860,6 +876,8 @@ def get_markets():
'margin': True,
'type': 'spot',
'contractSize': None,
'taker': 0.0006,
'maker': 0.0002,
'precision': {
'amount': 8,
'price': 8
@ -892,6 +910,8 @@ def get_markets():
'active': True,
'spot': True,
'type': 'spot',
'taker': 0.0006,
'maker': 0.0002,
'precision': {
'price': 8,
'amount': 8,
@ -923,6 +943,8 @@ def get_markets():
'active': True,
'spot': True,
'type': 'spot',
'taker': 0.0006,
'maker': 0.0002,
'precision': {
'price': 8,
'amount': 8,
@ -955,6 +977,8 @@ def get_markets():
'spot': True,
'type': 'spot',
'contractSize': None,
'taker': 0.0006,
'maker': 0.0002,
'precision': {
'price': 8,
'amount': 8,
@ -1023,6 +1047,8 @@ def get_markets():
'spot': False,
'type': 'swap',
'contractSize': 0.01,
'taker': 0.0006,
'maker': 0.0002,
'precision': {
'amount': 8,
'price': 8
@ -1098,7 +1124,6 @@ def get_markets():
'swap': True,
'futures': False,
'option': False,
'derivative': True,
'contract': True,
'linear': True,
'inverse': False,

View File

@ -174,35 +174,35 @@ def test_get_max_leverage_binance(default_conf, mocker, pair, stake_amount, max_
exchange = get_patched_exchange(mocker, default_conf, id="binance")
exchange._leverage_brackets = {
'BNB/BUSD': [
[0.0, 0.025], # lev = 40.0
[100000.0, 0.05], # lev = 20.0
[500000.0, 0.1], # lev = 10.0
[1000000.0, 0.15], # lev = 6.666666666666667
[2000000.0, 0.25], # lev = 4.0
[5000000.0, 0.5], # lev = 2.0
[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], # lev = 153.84615384615384
[10000.0, 0.01], # lev = 100.0
[50000.0, 0.02], # lev = 50.0
[250000.0, 0.05], # lev = 20.0
[1000000.0, 0.1], # lev = 10.0
[2000000.0, 0.125], # lev = 8.0
[5000000.0, 0.15], # lev = 6.666666666666667
[10000000.0, 0.25], # lev = 4.0
[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], # lev = 250.0
[50000.0, 0.005], # lev = 200.0
[250000.0, 0.01], # lev = 100.0
[1000000.0, 0.025], # lev = 40.0
[5000000.0, 0.05], # lev = 20.0
[20000000.0, 0.1], # lev = 10.0
[50000000.0, 0.125], # lev = 8.0
[100000000.0, 0.15], # lev = 6.666666666666667
[200000000.0, 0.25], # lev = 4.0
[300000000.0, 0.5], # lev = 2.0
],
[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
@ -241,28 +241,28 @@ def test_fill_leverage_brackets_binance(default_conf, mocker):
exchange.fill_leverage_brackets()
assert exchange._leverage_brackets == {
'ADA/BUSD': [[0.0, 0.025],
[100000.0, 0.05],
[500000.0, 0.1],
[1000000.0, 0.15],
[2000000.0, 0.25],
[5000000.0, 0.5]],
'BTC/USDT': [[0.0, 0.004],
[50000.0, 0.005],
[250000.0, 0.01],
[1000000.0, 0.025],
[5000000.0, 0.05],
[20000000.0, 0.1],
[50000000.0, 0.125],
[100000000.0, 0.15],
[200000000.0, 0.25],
[300000000.0, 0.5]],
"ZEC/USDT": [[0.0, 0.01],
[5000.0, 0.025],
[25000.0, 0.05],
[100000.0, 0.1],
[250000.0, 0.125],
[1000000.0, 0.5]],
'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]],
'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.000000000002],
[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]],
"ZEC/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]]
}
api_mock = MagicMock()
@ -288,37 +288,37 @@ def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker):
leverage_brackets = {
"1000SHIB/USDT": [
[0.0, 0.01],
[5000.0, 0.025],
[25000.0, 0.05],
[100000.0, 0.1],
[250000.0, 0.125],
[1000000.0, 0.5]
[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],
[5000.0, 0.025],
[25000.0, 0.05],
[100000.0, 0.1],
[250000.0, 0.125],
[1000000.0, 0.5]
[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],
[50000.0, 0.02],
[250000.0, 0.05],
[1000000.0, 0.1],
[2000000.0, 0.125],
[5000000.0, 0.1665],
[10000000.0, 0.25]
[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],
[100000.0, 0.05],
[500000.0, 0.1],
[1000000.0, 0.15],
[2000000.0, 0.25],
[5000000.0, 0.5]
[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],
]
}
@ -395,3 +395,51 @@ def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config):
default_conf['collateral'] = collateral
exchange = get_patched_exchange(mocker, default_conf, id="binance")
assert exchange._ccxt_config == config
@pytest.mark.parametrize('pair,nominal_value,mm_ratio,amt', [
("BNB/BUSD", 0.0, 0.025, 0),
("BNB/USDT", 100.0, 0.0065, 0),
("BTC/USDT", 170.30, 0.004, 0),
("BNB/BUSD", 999999.9, 0.1, 27500.0),
("BNB/USDT", 5000000.0, 0.15, 233035.0),
("BTC/USDT", 300000000.1, 0.5, 99891300.0),
])
def test_get_maintenance_ratio_and_amt_binance(
default_conf,
mocker,
pair,
nominal_value,
mm_ratio,
amt,
):
exchange = get_patched_exchange(mocker, default_conf, id="binance")
exchange._leverage_brackets = {
'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)
assert (round(result_ratio, 8), round(result_amt, 8)) == (mm_ratio, amt)

View File

@ -26,6 +26,12 @@ from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has
# Make sure to always keep one exchange here which is NOT subclassed!!
EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx', 'gateio']
spot = TradingMode.SPOT
margin = TradingMode.MARGIN
futures = TradingMode.FUTURES
cross = Collateral.CROSS
isolated = Collateral.ISOLATED
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
@ -236,8 +242,8 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog):
(2.34559, 4, 0.001, 1, 2.345, 'spot'),
(2.9999, 4, 0.001, 1, 2.999, 'spot'),
(2.9909, 4, 0.001, 1, 2.990, 'spot'),
(2.9909, 4, 0.005, 0.01, 299.09, 'futures'),
(2.9999, 4, 0.005, 10, 0.295, 'futures'),
(2.9909, 4, 0.005, 0.01, 2.99, 'futures'),
(2.9999, 4, 0.005, 10, 2.995, 'futures'),
])
def test_amount_to_precision(
default_conf,
@ -3438,30 +3444,35 @@ def test_set_margin_mode(mocker, default_conf, collateral):
("bittrex", TradingMode.FUTURES, Collateral.CROSS, True),
("bittrex", TradingMode.FUTURES, Collateral.ISOLATED, True),
("gateio", TradingMode.MARGIN, Collateral.ISOLATED, True),
("okex", TradingMode.SPOT, None, False),
("okex", TradingMode.MARGIN, Collateral.CROSS, True),
("okex", TradingMode.MARGIN, Collateral.ISOLATED, True),
("okex", TradingMode.FUTURES, Collateral.CROSS, True),
# TODO-lev: Remove once implemented
("binance", TradingMode.FUTURES, Collateral.ISOLATED, False),
("gateio", TradingMode.FUTURES, Collateral.ISOLATED, False),
# * Remove once implemented
("okex", TradingMode.FUTURES, Collateral.ISOLATED, True),
("binance", TradingMode.MARGIN, Collateral.CROSS, True),
("binance", TradingMode.FUTURES, Collateral.CROSS, True),
("binance", TradingMode.FUTURES, Collateral.ISOLATED, True),
("kraken", TradingMode.MARGIN, Collateral.CROSS, True),
("kraken", TradingMode.FUTURES, Collateral.CROSS, True),
("ftx", TradingMode.MARGIN, Collateral.CROSS, True),
("ftx", TradingMode.FUTURES, Collateral.CROSS, True),
("gateio", TradingMode.MARGIN, Collateral.CROSS, True),
("gateio", TradingMode.FUTURES, Collateral.CROSS, True),
("gateio", TradingMode.FUTURES, Collateral.ISOLATED, True),
# TODO-lev: Uncomment once implemented
# * Uncomment once implemented
# ("okex", TradingMode.FUTURES, Collateral.ISOLATED, False),
# ("binance", TradingMode.MARGIN, Collateral.CROSS, False),
# ("binance", TradingMode.FUTURES, Collateral.CROSS, False),
# ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False),
# ("kraken", TradingMode.MARGIN, Collateral.CROSS, False),
# ("kraken", TradingMode.FUTURES, Collateral.CROSS, False),
# ("ftx", TradingMode.MARGIN, Collateral.CROSS, False),
# ("ftx", TradingMode.FUTURES, Collateral.CROSS, False),
# ("gateio", TradingMode.MARGIN, Collateral.CROSS, False),
# ("gateio", TradingMode.FUTURES, Collateral.CROSS, False),
# ("gateio", TradingMode.FUTURES, Collateral.ISOLATED, False),
])
def test_validate_trading_mode_and_collateral(
default_conf,
@ -3582,6 +3593,68 @@ def test_calculate_funding_fees(
) == kraken_fee
def test_get_liquidation_price(mocker, default_conf):
api_mock = MagicMock()
positions = [
{
'info': {},
'symbol': 'NEAR/USDT:USDT',
'timestamp': 1642164737148,
'datetime': '2022-01-14T12:52:17.148Z',
'initialMargin': 1.51072,
'initialMarginPercentage': 0.1,
'maintenanceMargin': 0.38916147,
'maintenanceMarginPercentage': 0.025,
'entryPrice': 18.884,
'notional': 15.1072,
'leverage': 9.97,
'unrealizedPnl': 0.0048,
'contracts': 8,
'contractSize': 0.1,
'marginRatio': None,
'liquidationPrice': 17.47,
'markPrice': 18.89,
'collateral': 1.52549075,
'marginType': 'isolated',
'side': 'buy',
'percentage': 0.003177292946409658
}
]
api_mock.fetch_positions = MagicMock(return_value=positions)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
exchange_has=MagicMock(return_value=True),
)
default_conf['dry_run'] = False
default_conf['trading_mode'] = 'futures'
default_conf['collateral'] = 'isolated'
exchange = get_patched_exchange(mocker, default_conf, api_mock)
liq_price = exchange.get_liquidation_price(
pair='NEAR/USDT:USDT',
open_rate=0.0,
is_short=False,
position=0.0,
wallet_balance=0.0,
)
assert liq_price == 17.47
ccxt_exceptionhandlers(
mocker,
default_conf,
api_mock,
"binance",
"get_liquidation_price",
"fetch_positions",
pair="XRP/USDT",
open_rate=0.0,
is_short=False,
position=0.0,
wallet_balance=0.0,
)
@pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [
('binance', 0, 2, "2021-09-01 00:00:00", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999),
('binance', 0, 2, "2021-09-01 00:00:15", "2021-09-01 08:00:00", 30.0, -0.0009140999999999999),
@ -3622,41 +3695,41 @@ def test__fetch_and_calculate_funding_fees(
amount,
expected_fees
):
'''
nominal_value = mark_price * size
funding_fee = nominal_value * funding_rate
size: 30
time: 0, mark: 2.77, nominal_value: 83.1, fundRate: -0.000008, fundFee: -0.0006648
time: 1, mark: 2.73, nominal_value: 81.9, fundRate: -0.000004, fundFee: -0.0003276
time: 2, mark: 2.74, nominal_value: 82.2, fundRate: 0.000012, fundFee: 0.0009864
time: 3, mark: 2.76, nominal_value: 82.8, fundRate: -0.000003, fundFee: -0.0002484
time: 4, mark: 2.76, nominal_value: 82.8, fundRate: -0.000007, fundFee: -0.0005796
time: 5, mark: 2.77, nominal_value: 83.1, fundRate: 0.000003, fundFee: 0.0002493
time: 6, mark: 2.78, nominal_value: 83.4, fundRate: 0.000019, fundFee: 0.0015846
time: 7, mark: 2.78, nominal_value: 83.4, fundRate: 0.000003, fundFee: 0.0002502
time: 8, mark: 2.77, nominal_value: 83.1, fundRate: -0.000003, fundFee: -0.0002493
time: 9, mark: 2.77, nominal_value: 83.1, fundRate: 0, fundFee: 0.0
time: 10, mark: 2.84, nominal_value: 85.2, fundRate: 0.000013, fundFee: 0.0011076
time: 11, mark: 2.81, nominal_value: 84.3, fundRate: 0.000077, fundFee: 0.0064911
time: 12, mark: 2.81, nominal_value: 84.3, fundRate: 0.000072, fundFee: 0.0060696
time: 13, mark: 2.82, nominal_value: 84.6, fundRate: 0.000097, fundFee: 0.0082062
"""
nominal_value = mark_price * size
funding_fee = nominal_value * funding_rate
size: 30
time: 0, mark: 2.77, nominal_value: 83.1, fundRate: -0.000008, fundFee: -0.0006648
time: 1, mark: 2.73, nominal_value: 81.9, fundRate: -0.000004, fundFee: -0.0003276
time: 2, mark: 2.74, nominal_value: 82.2, fundRate: 0.000012, fundFee: 0.0009864
time: 3, mark: 2.76, nominal_value: 82.8, fundRate: -0.000003, fundFee: -0.0002484
time: 4, mark: 2.76, nominal_value: 82.8, fundRate: -0.000007, fundFee: -0.0005796
time: 5, mark: 2.77, nominal_value: 83.1, fundRate: 0.000003, fundFee: 0.0002493
time: 6, mark: 2.78, nominal_value: 83.4, fundRate: 0.000019, fundFee: 0.0015846
time: 7, mark: 2.78, nominal_value: 83.4, fundRate: 0.000003, fundFee: 0.0002502
time: 8, mark: 2.77, nominal_value: 83.1, fundRate: -0.000003, fundFee: -0.0002493
time: 9, mark: 2.77, nominal_value: 83.1, fundRate: 0, fundFee: 0.0
time: 10, mark: 2.84, nominal_value: 85.2, fundRate: 0.000013, fundFee: 0.0011076
time: 11, mark: 2.81, nominal_value: 84.3, fundRate: 0.000077, fundFee: 0.0064911
time: 12, mark: 2.81, nominal_value: 84.3, fundRate: 0.000072, fundFee: 0.0060696
time: 13, mark: 2.82, nominal_value: 84.6, fundRate: 0.000097, fundFee: 0.0082062
size: 50
time: 0, mark: 2.77, nominal_value: 138.5, fundRate: -0.000008, fundFee: -0.001108
time: 1, mark: 2.73, nominal_value: 136.5, fundRate: -0.000004, fundFee: -0.000546
time: 2, mark: 2.74, nominal_value: 137.0, fundRate: 0.000012, fundFee: 0.001644
time: 3, mark: 2.76, nominal_value: 138.0, fundRate: -0.000003, fundFee: -0.000414
time: 4, mark: 2.76, nominal_value: 138.0, fundRate: -0.000007, fundFee: -0.000966
time: 5, mark: 2.77, nominal_value: 138.5, fundRate: 0.000003, fundFee: 0.0004155
time: 6, mark: 2.78, nominal_value: 139.0, fundRate: 0.000019, fundFee: 0.002641
time: 7, mark: 2.78, nominal_value: 139.0, fundRate: 0.000003, fundFee: 0.000417
time: 8, mark: 2.77, nominal_value: 138.5, fundRate: -0.000003, fundFee: -0.0004155
time: 9, mark: 2.77, nominal_value: 138.5, fundRate: 0, fundFee: 0.0
time: 10, mark: 2.84, nominal_value: 142.0, fundRate: 0.000013, fundFee: 0.001846
time: 11, mark: 2.81, nominal_value: 140.5, fundRate: 0.000077, fundFee: 0.0108185
time: 12, mark: 2.81, nominal_value: 140.5, fundRate: 0.000072, fundFee: 0.010116
time: 13, mark: 2.82, nominal_value: 141.0, fundRate: 0.000097, fundFee: 0.013677
'''
size: 50
time: 0, mark: 2.77, nominal_value: 138.5, fundRate: -0.000008, fundFee: -0.001108
time: 1, mark: 2.73, nominal_value: 136.5, fundRate: -0.000004, fundFee: -0.000546
time: 2, mark: 2.74, nominal_value: 137.0, fundRate: 0.000012, fundFee: 0.001644
time: 3, mark: 2.76, nominal_value: 138.0, fundRate: -0.000003, fundFee: -0.000414
time: 4, mark: 2.76, nominal_value: 138.0, fundRate: -0.000007, fundFee: -0.000966
time: 5, mark: 2.77, nominal_value: 138.5, fundRate: 0.000003, fundFee: 0.0004155
time: 6, mark: 2.78, nominal_value: 139.0, fundRate: 0.000019, fundFee: 0.002641
time: 7, mark: 2.78, nominal_value: 139.0, fundRate: 0.000003, fundFee: 0.000417
time: 8, mark: 2.77, nominal_value: 138.5, fundRate: -0.000003, fundFee: -0.0004155
time: 9, mark: 2.77, nominal_value: 138.5, fundRate: 0, fundFee: 0.0
time: 10, mark: 2.84, nominal_value: 142.0, fundRate: 0.000013, fundFee: 0.001846
time: 11, mark: 2.81, nominal_value: 140.5, fundRate: 0.000077, fundFee: 0.0108185
time: 12, mark: 2.81, nominal_value: 140.5, fundRate: 0.000072, fundFee: 0.010116
time: 13, mark: 2.82, nominal_value: 141.0, fundRate: 0.000097, fundFee: 0.013677
"""
d1 = datetime.strptime(f"{d1} +0000", '%Y-%m-%d %H:%M:%S %z')
d2 = datetime.strptime(f"{d2} +0000", '%Y-%m-%d %H:%M:%S %z')
funding_rate_history = {
@ -3909,3 +3982,69 @@ def test__amount_to_contracts(
assert result_size == param_size
result_amount = exchange._contracts_to_amount(pair, param_size)
assert result_amount == param_amount
@pytest.mark.parametrize('exchange_name,open_rate,is_short,trading_mode,collateral', [
# Bittrex
('bittrex', 2.0, False, 'spot', None),
('bittrex', 2.0, False, 'spot', 'cross'),
('bittrex', 2.0, True, 'spot', 'isolated'),
# Binance
('binance', 2.0, False, 'spot', None),
('binance', 2.0, False, 'spot', 'cross'),
('binance', 2.0, True, 'spot', 'isolated'),
])
def test_liquidation_price_is_none(
mocker,
default_conf,
exchange_name,
open_rate,
is_short,
trading_mode,
collateral
):
default_conf['trading_mode'] = trading_mode
default_conf['collateral'] = collateral
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
assert exchange.get_liquidation_price(
pair='DOGE/USDT',
open_rate=open_rate,
is_short=is_short,
position=71200.81144,
wallet_balance=-56354.57,
mm_ex_1=0.10,
upnl_ex_1=0.0
) is None
@pytest.mark.parametrize(
'exchange_name, is_short, trading_mode, collateral, wallet_balance, '
'mm_ex_1, upnl_ex_1, maintenance_amt, position, open_rate, '
'mm_ratio, expected',
[
("binance", False, 'futures', 'isolated', 1535443.01, 0.0,
0.0, 135365.00, 3683.979, 1456.84, 0.10, 1114.78),
("binance", False, 'futures', 'isolated', 1535443.01, 0.0,
0.0, 16300.000, 109.488, 32481.980, 0.025, 18778.73),
("binance", False, 'futures', 'cross', 1535443.01, 71200.81144,
-56354.57, 135365.00, 3683.979, 1456.84, 0.10, 1153.26),
("binance", False, 'futures', 'cross', 1535443.01, 356512.508,
-448192.89, 16300.000, 109.488, 32481.980, 0.025, 26316.89)
])
def test_liquidation_price(
mocker, default_conf, exchange_name, open_rate, is_short, trading_mode,
collateral, wallet_balance, mm_ex_1, upnl_ex_1, maintenance_amt, position, mm_ratio, expected
):
default_conf['trading_mode'] = trading_mode
default_conf['collateral'] = collateral
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(mm_ratio, maintenance_amt))
assert isclose(round(exchange.get_liquidation_price(
pair='DOGE/USDT',
open_rate=open_rate,
is_short=is_short,
wallet_balance=wallet_balance,
mm_ex_1=mm_ex_1,
upnl_ex_1=upnl_ex_1,
position=position,
), 2), expected)

View File

@ -1,8 +1,11 @@
from unittest.mock import MagicMock, PropertyMock
import pytest
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Gateio
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_patched_exchange
def test_validate_order_types_gateio(default_conf, mocker):
@ -26,3 +29,38 @@ def test_validate_order_types_gateio(default_conf, mocker):
with pytest.raises(OperationalException,
match=r'Exchange .* does not support market orders.'):
ExchangeResolver.load_exchange('gateio', default_conf, True)
@pytest.mark.parametrize('pair,mm_ratio', [
("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()
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="gateio")
mocker.patch(
'freqtrade.exchange.Exchange.markets',
PropertyMock(
return_value={
'ETH/USDT:USDT': {
'taker': 0.0000075,
'maker': -0.0000025,
'info': {
'maintenance_rate': '0.005',
},
'id': 'ETH_USDT',
'symbol': 'ETH/USDT:USDT',
},
'ADA/USDT:USDT': {
'taker': 0.0000075,
'maker': -0.0000025,
'info': {
'maintenance_rate': '0.003',
},
'id': 'ADA_USDT',
'symbol': 'ADA/USDT:USDT',
},
}
)
)
assert exchange.get_maintenance_ratio_and_amt(pair) == (mm_ratio, None)

View File

@ -707,23 +707,52 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker)
CandleType.SPOT) in refresh_mock.call_args[0][0]
@pytest.mark.parametrize("trading_mode", [
'spot',
# TODO-lev: Enable other modes
# 'margin', 'futures'
]
)
@pytest.mark.parametrize("is_short", [False, True])
@pytest.mark.parametrize("is_short,trading_mode,exchange_name,margin_mode,liq_price", [
(False, 'spot', 'binance', None, None),
(True, 'spot', 'binance', None, None),
(False, 'spot', 'gateio', None, None),
(True, 'spot', 'gateio', None, None),
(False, 'spot', 'okex', None, None),
(True, 'spot', 'okex', None, None),
(True, 'futures', 'binance', 'isolated', 11.89108910891089),
(False, 'futures', 'binance', 'isolated', 8.070707070707071),
(True, 'futures', 'gateio', 'isolated', 11.87413417771621),
(False, 'futures', 'gateio', 'isolated', 8.085708510208207),
# (True, 'futures', 'okex', 'isolated', 11.87413417771621),
# (False, 'futures', 'okex', 'isolated', 8.085708510208207),
])
def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
limit_order_open, is_short, trading_mode) -> None:
limit_order_open, is_short, trading_mode,
exchange_name, margin_mode, liq_price) -> None:
"""
exchange_name = binance, is_short = true
leverage = 5
position = 0.2 * 5
((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position))
((2 + 0.01) - ((-1) * 1 * 10)) / ((1 * 0.01) - ((-1) * 1)) = 11.89108910891089
exchange_name = binance, is_short = false
((wb + cum_b) - (side_1 * position * ep1)) / ((position * mmr_b) - (side_1 * position))
((2 + 0.01) - (1 * 1 * 10)) / ((1 * 0.01) - (1 * 1)) = 8.070707070707071
exchange_name = gateio/okex, is_short = true
(open_rate + (wallet_balance / position)) / (1 + (mm_ratio + taker_fee_rate))
(10 + (2 / 1)) / (1 + (0.01 + 0.0006)) = 11.87413417771621
exchange_name = gateio/okex, is_short = false
(open_rate - (wallet_balance / position)) / (1 - (mm_ratio + taker_fee_rate))
(10 - (2 / 1)) / (1 - (0.01 + 0.0006)) = 8.085708510208207
"""
open_order = limit_order_open[enter_side(is_short)]
order = limit_order[enter_side(is_short)]
default_conf_usdt['trading_mode'] = trading_mode
leverage = 1.0 if trading_mode == 'spot' else 3.0
default_conf_usdt['collateral'] = 'cross'
leverage = 1.0 if trading_mode == 'spot' else 5.0
default_conf_usdt['exchange']['name'] = exchange_name
if margin_mode:
default_conf_usdt['collateral'] = margin_mode
mocker.patch('freqtrade.exchange.Gateio.validate_ordertypes')
patch_RPCManager(mocker)
patch_exchange(mocker)
patch_exchange(mocker, id=exchange_name)
freqtrade = FreqtradeBot(default_conf_usdt)
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False)
freqtrade.strategy.leverage = MagicMock(return_value=leverage)
@ -743,6 +772,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
get_min_pair_stake_amount=MagicMock(return_value=1),
get_fee=fee,
get_funding_fees=MagicMock(return_value=0),
name=exchange_name
)
pair = 'ETH/USDT'
@ -886,14 +916,21 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
assert trade.open_rate_requested == 10
# In case of custom entry price not float type
freqtrade.exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(0.01, 0.01))
freqtrade.exchange.name = exchange_name
order['status'] = 'open'
order['id'] = '5568'
freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price"
assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
trade = Trade.query.all()[8]
# Trade(id=9, pair=ETH/USDT, amount=0.20000000, is_short=False,
# leverage=1.0, open_rate=10.00000000, open_since=...)
# Trade(id=9, pair=ETH/USDT, amount=0.60000000, is_short=True,
# leverage=3.0, open_rate=10.00000000, open_since=...)
trade.is_short = is_short
assert trade
assert trade.open_rate_requested == 10
assert trade.isolated_liq == liq_price
@pytest.mark.parametrize("is_short", [False, True])
@ -4794,23 +4831,23 @@ def test_update_funding_fees(
limit_order_open,
schedule_off
):
'''
"""
nominal_value = mark_price * size
funding_fee = nominal_value * funding_rate
size = 123
"LTC/BTC"
"LTC/USDT"
time: 0, mark: 3.3, fundRate: 0.00032583, nominal_value: 405.9, fundFee: 0.132254397
time: 8, mark: 3.2, fundRate: 0.00024472, nominal_value: 393.6, fundFee: 0.096321792
"ETH/BTC"
"ETH/USDT"
time: 0, mark: 2.4, fundRate: 0.0001, nominal_value: 295.2, fundFee: 0.02952
time: 8, mark: 2.5, fundRate: 0.0001, nominal_value: 307.5, fundFee: 0.03075
"ETC/BTC"
"ETC/USDT"
time: 0, mark: 4.3, fundRate: 0.00031077, nominal_value: 528.9, fundFee: 0.164366253
time: 8, mark: 4.1, fundRate: 0.00022655, nominal_value: 504.3, fundFee: 0.114249165
"XRP/BTC"
"XRP/USDT"
time: 0, mark: 1.2, fundRate: 0.00049426, nominal_value: 147.6, fundFee: 0.072952776
time: 8, mark: 1.2, fundRate: 0.00032715, nominal_value: 147.6, fundFee: 0.04828734
'''
"""
# SETUP
time_machine.move_to("2021-09-01 00:00:00 +00:00")
@ -4831,19 +4868,19 @@ def test_update_funding_fees(
# 16:00 entry is actually never used
# But should be kept in the test to ensure we're filtering correctly.
funding_rates = {
"LTC/BTC":
"LTC/USDT":
DataFrame([
[date_midnight, 0.00032583, 0, 0, 0, 0],
[date_eight, 0.00024472, 0, 0, 0, 0],
[date_sixteen, 0.00024472, 0, 0, 0, 0],
], columns=columns),
"ETH/BTC":
"ETH/USDT":
DataFrame([
[date_midnight, 0.0001, 0, 0, 0, 0],
[date_eight, 0.0001, 0, 0, 0, 0],
[date_sixteen, 0.0001, 0, 0, 0, 0],
], columns=columns),
"XRP/BTC":
"XRP/USDT":
DataFrame([
[date_midnight, 0.00049426, 0, 0, 0, 0],
[date_eight, 0.00032715, 0, 0, 0, 0],
@ -4852,19 +4889,19 @@ def test_update_funding_fees(
}
mark_prices = {
"LTC/BTC":
"LTC/USDT":
DataFrame([
[date_midnight, 3.3, 0, 0, 0, 0],
[date_eight, 3.2, 0, 0, 0, 0],
[date_sixteen, 3.2, 0, 0, 0, 0],
], columns=columns),
"ETH/BTC":
"ETH/USDT":
DataFrame([
[date_midnight, 2.4, 0, 0, 0, 0],
[date_eight, 2.5, 0, 0, 0, 0],
[date_sixteen, 2.5, 0, 0, 0, 0],
], columns=columns),
"XRP/BTC":
"XRP/USDT":
DataFrame([
[date_midnight, 1.2, 0, 0, 0, 0],
[date_eight, 1.2, 0, 0, 0, 0],
@ -4901,9 +4938,9 @@ def test_update_funding_fees(
freqtrade = get_patched_freqtradebot(mocker, default_conf)
# initial funding fees,
freqtrade.execute_entry('ETH/BTC', 123, is_short=is_short)
freqtrade.execute_entry('LTC/BTC', 2.0, is_short=is_short)
freqtrade.execute_entry('XRP/BTC', 123, is_short=is_short)
freqtrade.execute_entry('ETH/USDT', 123, is_short=is_short)
freqtrade.execute_entry('LTC/USDT', 2.0, is_short=is_short)
freqtrade.execute_entry('XRP/USDT', 123, is_short=is_short)
multipl = 1 if is_short else -1
trades = Trade.get_open_trades()
assert len(trades) == 3

View File

@ -148,7 +148,7 @@ def test_set_stop_loss_isolated_liq(fee):
trade.stop_loss = None
trade.initial_stop_loss = None
trade.set_isolated_liq(isolated_liq=0.09)
trade.set_isolated_liq(0.09)
assert trade.isolated_liq == 0.09
assert trade.stop_loss == 0.09
assert trade.initial_stop_loss == 0.09
@ -158,12 +158,12 @@ def test_set_stop_loss_isolated_liq(fee):
assert trade.stop_loss == 0.08
assert trade.initial_stop_loss == 0.09
trade.set_isolated_liq(isolated_liq=0.1)
trade.set_isolated_liq(0.1)
assert trade.isolated_liq == 0.1
assert trade.stop_loss == 0.08
assert trade.initial_stop_loss == 0.09
trade.set_isolated_liq(isolated_liq=0.07)
trade.set_isolated_liq(0.07)
assert trade.isolated_liq == 0.07
assert trade.stop_loss == 0.07
assert trade.initial_stop_loss == 0.09