commit
2141e04a19
@ -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}
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user