commit
2141e04a19
@ -155,7 +155,7 @@ CONF_SCHEMA = {
|
|||||||
'ignore_roi_if_buy_signal': {'type': 'boolean'},
|
'ignore_roi_if_buy_signal': {'type': 'boolean'},
|
||||||
'ignore_buying_expired_candle_after': {'type': 'number'},
|
'ignore_buying_expired_candle_after': {'type': 'number'},
|
||||||
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
|
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
|
||||||
'collateral_type': {'type': 'string', 'enum': COLLATERAL_TYPES},
|
'collateral': {'type': 'string', 'enum': COLLATERAL_TYPES},
|
||||||
'backtest_breakdown': {
|
'backtest_breakdown': {
|
||||||
'type': 'array',
|
'type': 'array',
|
||||||
'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS}
|
'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS}
|
||||||
|
@ -33,10 +33,9 @@ class Binance(Exchange):
|
|||||||
|
|
||||||
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
||||||
# TradingMode.SPOT always supported and not required in this list
|
# TradingMode.SPOT always supported and not required in this list
|
||||||
# TODO-lev: Uncomment once supported
|
|
||||||
# (TradingMode.MARGIN, Collateral.CROSS),
|
# (TradingMode.MARGIN, Collateral.CROSS),
|
||||||
# (TradingMode.FUTURES, 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:
|
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
||||||
@ -120,10 +119,25 @@ class Binance(Exchange):
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def fill_leverage_brackets(self):
|
def fill_leverage_brackets(self) -> None:
|
||||||
"""
|
"""
|
||||||
Assigns property _leverage_brackets to a dictionary of information about the leverage
|
Assigns property _leverage_brackets to a dictionary of information about the leverage
|
||||||
allowed on each pair
|
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:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
try:
|
try:
|
||||||
@ -136,17 +150,21 @@ class Binance(Exchange):
|
|||||||
else:
|
else:
|
||||||
leverage_brackets = self._api.load_leverage_brackets()
|
leverage_brackets = self._api.load_leverage_brackets()
|
||||||
|
|
||||||
for pair, brackets in leverage_brackets.items():
|
for pair, brkts in leverage_brackets.items():
|
||||||
self._leverage_brackets[pair] = [
|
[amt, old_ratio] = [0.0, 0.0]
|
||||||
[
|
brackets = []
|
||||||
min_amount,
|
for [notional_floor, mm_ratio] in brkts:
|
||||||
float(margin_req)
|
amt = (
|
||||||
] for [
|
(float(notional_floor) * (float(mm_ratio) - float(old_ratio)))
|
||||||
min_amount,
|
+ amt
|
||||||
margin_req
|
) if old_ratio else 0.0
|
||||||
] in brackets
|
old_ratio = mm_ratio
|
||||||
]
|
brackets.append([
|
||||||
|
float(notional_floor),
|
||||||
|
float(mm_ratio),
|
||||||
|
amt,
|
||||||
|
])
|
||||||
|
self._leverage_brackets[pair] = brackets
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
@ -161,16 +179,22 @@ class Binance(Exchange):
|
|||||||
:param pair: The base/quote currency pair being traded
|
:param pair: The base/quote currency pair being traded
|
||||||
:stake_amount: The total value of the traders collateral in quote currency
|
: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:
|
if pair not in self._leverage_brackets:
|
||||||
return 1.0
|
return 1.0
|
||||||
pair_brackets = self._leverage_brackets[pair]
|
pair_brackets = self._leverage_brackets[pair]
|
||||||
num_brackets = len(pair_brackets)
|
num_brackets = len(pair_brackets)
|
||||||
min_amount = 0
|
min_amount = 0.0
|
||||||
for bracket_num in range(num_brackets):
|
for bracket_num in range(num_brackets):
|
||||||
[_, margin_req] = pair_brackets[bracket_num]
|
[notional_floor, mm_ratio, _] = pair_brackets[bracket_num]
|
||||||
lev = 1/margin_req
|
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
|
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:
|
else:
|
||||||
return lev
|
return lev
|
||||||
nominal_value = stake_amount * 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: 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)
|
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: ccxt.Exchange = None
|
||||||
self._api_async: ccxt_async.Exchange = None
|
self._api_async: ccxt_async.Exchange = None
|
||||||
self._markets: Dict = {}
|
self._markets: Dict = {}
|
||||||
self._leverage_brackets: Dict = {}
|
self._leverage_brackets: Dict[str, List[List[float]]] = {}
|
||||||
self.loop = asyncio.new_event_loop()
|
self.loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(self.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
|
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
|
||||||
based on our definitions.
|
based on our definitions.
|
||||||
"""
|
"""
|
||||||
amount = self._amount_to_contracts(pair, amount)
|
|
||||||
if self.markets[pair]['precision']['amount']:
|
if self.markets[pair]['precision']['amount']:
|
||||||
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
|
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
|
||||||
precision=self.markets[pair]['precision']['amount'],
|
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,
|
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||||
rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]:
|
rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]:
|
||||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
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] = {
|
dry_order: Dict[str, Any] = {
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
'symbol': pair,
|
'symbol': pair,
|
||||||
@ -901,7 +900,7 @@ class Exchange:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
# 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'
|
needs_price = (ordertype != 'market'
|
||||||
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
||||||
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||||
@ -1983,6 +1982,119 @@ class Exchange:
|
|||||||
else:
|
else:
|
||||||
return 0.0
|
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:
|
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
||||||
return exchange_name in ccxt_exchanges(ccxt_module)
|
return exchange_name in ccxt_exchanges(ccxt_module)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
""" Gate.io exchange subclass """
|
""" Gate.io exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from freqtrade.enums import Collateral, TradingMode
|
from freqtrade.enums import Collateral, TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
@ -31,7 +31,7 @@ class Gateio(Exchange):
|
|||||||
# TradingMode.SPOT always supported and not required in this list
|
# TradingMode.SPOT always supported and not required in this list
|
||||||
# (TradingMode.MARGIN, Collateral.CROSS),
|
# (TradingMode.MARGIN, Collateral.CROSS),
|
||||||
# (TradingMode.FUTURES, Collateral.CROSS),
|
# (TradingMode.FUTURES, Collateral.CROSS),
|
||||||
# (TradingMode.FUTURES, Collateral.ISOLATED)
|
(TradingMode.FUTURES, Collateral.ISOLATED)
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate_ordertypes(self, order_types: Dict) -> None:
|
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()):
|
if any(v == 'market' for k, v in order_types.items()):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Exchange {self.name} does not support market orders.')
|
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,
|
from freqtrade.enums import (Collateral, RPCMessageType, RunMode, SellType, SignalDirection, State,
|
||||||
TradingMode)
|
TradingMode)
|
||||||
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, PricingError)
|
InvalidOrderException, OperationalException, PricingError)
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||||
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
|
from freqtrade.misc import safe_value_fallback, safe_value_fallback2
|
||||||
from freqtrade.mixins import LoggingMixin
|
from freqtrade.mixins import LoggingMixin
|
||||||
@ -106,8 +106,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.trading_mode = TradingMode(self.config.get('trading_mode', 'spot'))
|
self.trading_mode = TradingMode(self.config.get('trading_mode', 'spot'))
|
||||||
|
|
||||||
self.collateral_type: Optional[Collateral] = None
|
self.collateral_type: Optional[Collateral] = None
|
||||||
if 'collateral_type' in self.config:
|
if 'collateral' in self.config:
|
||||||
self.collateral_type = Collateral(self.config['collateral_type'])
|
self.collateral_type = Collateral(self.config['collateral'])
|
||||||
|
|
||||||
self._schedule = Scheduler()
|
self._schedule = Scheduler()
|
||||||
|
|
||||||
@ -606,29 +606,32 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
is_short: bool
|
is_short: bool
|
||||||
) -> Tuple[float, Optional[float]]:
|
) -> Tuple[float, Optional[float]]:
|
||||||
|
|
||||||
interest_rate = 0.0
|
|
||||||
isolated_liq = None
|
|
||||||
|
|
||||||
# TODO-lev: Uncomment once liq and interest merged in
|
|
||||||
# if TradingMode == TradingMode.MARGIN:
|
# if TradingMode == TradingMode.MARGIN:
|
||||||
# interest_rate = self.exchange.get_interest_rate(
|
# interest_rate = self.exchange.get_interest_rate(
|
||||||
# pair=pair,
|
# pair=pair,
|
||||||
# open_rate=open_rate,
|
# open_rate=open_rate,
|
||||||
# is_short=is_short
|
# is_short=is_short
|
||||||
# )
|
# )
|
||||||
|
if self.trading_mode == TradingMode.SPOT:
|
||||||
# if self.collateral_type == Collateral.ISOLATED:
|
return (0.0, None)
|
||||||
|
elif (
|
||||||
# isolated_liq = liquidation_price(
|
self.collateral_type == Collateral.ISOLATED and
|
||||||
# exchange_name=self.exchange.name,
|
self.trading_mode == TradingMode.FUTURES
|
||||||
# trading_mode=self.trading_mode,
|
):
|
||||||
# open_rate=open_rate,
|
wallet_balance = (amount * open_rate)/leverage
|
||||||
# amount=amount,
|
isolated_liq = self.exchange.get_liquidation_price(
|
||||||
# leverage=leverage,
|
pair=pair,
|
||||||
# is_short=is_short
|
open_rate=open_rate,
|
||||||
# )
|
is_short=is_short,
|
||||||
|
position=amount,
|
||||||
return interest_rate, isolated_liq
|
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(
|
def execute_entry(
|
||||||
self,
|
self,
|
||||||
@ -1174,8 +1177,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||||
|
|
||||||
if not_closed and (fully_cancelled or self.strategy.ft_check_timed_out(
|
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:
|
if is_entering:
|
||||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||||
else:
|
else:
|
||||||
|
@ -333,7 +333,7 @@ class LocalTrade():
|
|||||||
for key in kwargs:
|
for key in kwargs:
|
||||||
setattr(self, key, kwargs[key])
|
setattr(self, key, kwargs[key])
|
||||||
if self.isolated_liq:
|
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()
|
self.recalc_open_trade_value()
|
||||||
if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None:
|
if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
|
@ -817,27 +817,42 @@ def get_markets():
|
|||||||
'symbol': 'ETH/USDT',
|
'symbol': 'ETH/USDT',
|
||||||
'base': 'ETH',
|
'base': 'ETH',
|
||||||
'quote': 'USDT',
|
'quote': 'USDT',
|
||||||
'spot': True,
|
'settle': None,
|
||||||
'future': True,
|
'baseId': 'ETH',
|
||||||
'swap': True,
|
'quoteId': 'USDT',
|
||||||
'margin': True,
|
'settleId': None,
|
||||||
'type': 'spot',
|
'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,
|
'contractSize': None,
|
||||||
|
'expiry': None,
|
||||||
|
'expiryDateTime': None,
|
||||||
|
'strike': None,
|
||||||
|
'optionType': None,
|
||||||
'precision': {
|
'precision': {
|
||||||
'amount': 8,
|
'amount': 8,
|
||||||
'price': 8
|
'price': 8,
|
||||||
},
|
},
|
||||||
'limits': {
|
'limits': {
|
||||||
|
'leverage': {
|
||||||
|
'min': 1,
|
||||||
|
'max': 100,
|
||||||
|
},
|
||||||
'amount': {
|
'amount': {
|
||||||
'min': 0.02214286,
|
'min': 0.02214286,
|
||||||
'max': None
|
'max': None,
|
||||||
},
|
},
|
||||||
'price': {
|
'price': {
|
||||||
'min': 1e-08,
|
'min': 1e-08,
|
||||||
'max': None
|
|
||||||
},
|
|
||||||
'leverage': {
|
|
||||||
'min': None,
|
|
||||||
'max': None,
|
'max': None,
|
||||||
},
|
},
|
||||||
'cost': {
|
'cost': {
|
||||||
@ -845,8 +860,9 @@ def get_markets():
|
|||||||
'max': None,
|
'max': None,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'active': True,
|
'info': {
|
||||||
'info': {},
|
'maintenance_rate': '0.005',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'LTC/USDT': {
|
'LTC/USDT': {
|
||||||
'id': 'USDT-LTC',
|
'id': 'USDT-LTC',
|
||||||
@ -860,6 +876,8 @@ def get_markets():
|
|||||||
'margin': True,
|
'margin': True,
|
||||||
'type': 'spot',
|
'type': 'spot',
|
||||||
'contractSize': None,
|
'contractSize': None,
|
||||||
|
'taker': 0.0006,
|
||||||
|
'maker': 0.0002,
|
||||||
'precision': {
|
'precision': {
|
||||||
'amount': 8,
|
'amount': 8,
|
||||||
'price': 8
|
'price': 8
|
||||||
@ -892,6 +910,8 @@ def get_markets():
|
|||||||
'active': True,
|
'active': True,
|
||||||
'spot': True,
|
'spot': True,
|
||||||
'type': 'spot',
|
'type': 'spot',
|
||||||
|
'taker': 0.0006,
|
||||||
|
'maker': 0.0002,
|
||||||
'precision': {
|
'precision': {
|
||||||
'price': 8,
|
'price': 8,
|
||||||
'amount': 8,
|
'amount': 8,
|
||||||
@ -923,6 +943,8 @@ def get_markets():
|
|||||||
'active': True,
|
'active': True,
|
||||||
'spot': True,
|
'spot': True,
|
||||||
'type': 'spot',
|
'type': 'spot',
|
||||||
|
'taker': 0.0006,
|
||||||
|
'maker': 0.0002,
|
||||||
'precision': {
|
'precision': {
|
||||||
'price': 8,
|
'price': 8,
|
||||||
'amount': 8,
|
'amount': 8,
|
||||||
@ -955,6 +977,8 @@ def get_markets():
|
|||||||
'spot': True,
|
'spot': True,
|
||||||
'type': 'spot',
|
'type': 'spot',
|
||||||
'contractSize': None,
|
'contractSize': None,
|
||||||
|
'taker': 0.0006,
|
||||||
|
'maker': 0.0002,
|
||||||
'precision': {
|
'precision': {
|
||||||
'price': 8,
|
'price': 8,
|
||||||
'amount': 8,
|
'amount': 8,
|
||||||
@ -1023,6 +1047,8 @@ def get_markets():
|
|||||||
'spot': False,
|
'spot': False,
|
||||||
'type': 'swap',
|
'type': 'swap',
|
||||||
'contractSize': 0.01,
|
'contractSize': 0.01,
|
||||||
|
'taker': 0.0006,
|
||||||
|
'maker': 0.0002,
|
||||||
'precision': {
|
'precision': {
|
||||||
'amount': 8,
|
'amount': 8,
|
||||||
'price': 8
|
'price': 8
|
||||||
@ -1098,7 +1124,6 @@ def get_markets():
|
|||||||
'swap': True,
|
'swap': True,
|
||||||
'futures': False,
|
'futures': False,
|
||||||
'option': False,
|
'option': False,
|
||||||
'derivative': True,
|
|
||||||
'contract': True,
|
'contract': True,
|
||||||
'linear': True,
|
'linear': True,
|
||||||
'inverse': False,
|
'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 = get_patched_exchange(mocker, default_conf, id="binance")
|
||||||
exchange._leverage_brackets = {
|
exchange._leverage_brackets = {
|
||||||
'BNB/BUSD': [
|
'BNB/BUSD': [
|
||||||
[0.0, 0.025], # lev = 40.0
|
[0.0, 0.025, 0.0], # lev = 40.0
|
||||||
[100000.0, 0.05], # lev = 20.0
|
[100000.0, 0.05, 2500.0], # lev = 20.0
|
||||||
[500000.0, 0.1], # lev = 10.0
|
[500000.0, 0.1, 27500.0], # lev = 10.0
|
||||||
[1000000.0, 0.15], # lev = 6.666666666666667
|
[1000000.0, 0.15, 77500.0], # lev = 6.666666666666667
|
||||||
[2000000.0, 0.25], # lev = 4.0
|
[2000000.0, 0.25, 277500.0], # lev = 4.0
|
||||||
[5000000.0, 0.5], # lev = 2.0
|
[5000000.0, 0.5, 1527500.0], # lev = 2.0
|
||||||
],
|
],
|
||||||
'BNB/USDT': [
|
'BNB/USDT': [
|
||||||
[0.0, 0.0065], # lev = 153.84615384615384
|
[0.0, 0.0065, 0.0], # lev = 153.84615384615384
|
||||||
[10000.0, 0.01], # lev = 100.0
|
[10000.0, 0.01, 35.0], # lev = 100.0
|
||||||
[50000.0, 0.02], # lev = 50.0
|
[50000.0, 0.02, 535.0], # lev = 50.0
|
||||||
[250000.0, 0.05], # lev = 20.0
|
[250000.0, 0.05, 8035.0], # lev = 20.0
|
||||||
[1000000.0, 0.1], # lev = 10.0
|
[1000000.0, 0.1, 58035.0], # lev = 10.0
|
||||||
[2000000.0, 0.125], # lev = 8.0
|
[2000000.0, 0.125, 108035.0], # lev = 8.0
|
||||||
[5000000.0, 0.15], # lev = 6.666666666666667
|
[5000000.0, 0.15, 233035.0], # lev = 6.666666666666667
|
||||||
[10000000.0, 0.25], # lev = 4.0
|
[10000000.0, 0.25, 1233035.0], # lev = 4.0
|
||||||
],
|
],
|
||||||
'BTC/USDT': [
|
'BTC/USDT': [
|
||||||
[0.0, 0.004], # lev = 250.0
|
[0.0, 0.004, 0.0], # lev = 250.0
|
||||||
[50000.0, 0.005], # lev = 200.0
|
[50000.0, 0.005, 50.0], # lev = 200.0
|
||||||
[250000.0, 0.01], # lev = 100.0
|
[250000.0, 0.01, 1300.0], # lev = 100.0
|
||||||
[1000000.0, 0.025], # lev = 40.0
|
[1000000.0, 0.025, 16300.0], # lev = 40.0
|
||||||
[5000000.0, 0.05], # lev = 20.0
|
[5000000.0, 0.05, 141300.0], # lev = 20.0
|
||||||
[20000000.0, 0.1], # lev = 10.0
|
[20000000.0, 0.1, 1141300.0], # lev = 10.0
|
||||||
[50000000.0, 0.125], # lev = 8.0
|
[50000000.0, 0.125, 2391300.0], # lev = 8.0
|
||||||
[100000000.0, 0.15], # lev = 6.666666666666667
|
[100000000.0, 0.15, 4891300.0], # lev = 6.666666666666667
|
||||||
[200000000.0, 0.25], # lev = 4.0
|
[200000000.0, 0.25, 24891300.0], # lev = 4.0
|
||||||
[300000000.0, 0.5], # lev = 2.0
|
[300000000.0, 0.5, 99891300.0], # lev = 2.0
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
assert exchange.get_max_leverage(pair, stake_amount) == max_lev
|
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()
|
exchange.fill_leverage_brackets()
|
||||||
|
|
||||||
assert exchange._leverage_brackets == {
|
assert exchange._leverage_brackets == {
|
||||||
'ADA/BUSD': [[0.0, 0.025],
|
'ADA/BUSD': [[0.0, 0.025, 0.0],
|
||||||
[100000.0, 0.05],
|
[100000.0, 0.05, 2500.0],
|
||||||
[500000.0, 0.1],
|
[500000.0, 0.1, 27500.0],
|
||||||
[1000000.0, 0.15],
|
[1000000.0, 0.15, 77499.99999999999],
|
||||||
[2000000.0, 0.25],
|
[2000000.0, 0.25, 277500.0],
|
||||||
[5000000.0, 0.5]],
|
[5000000.0, 0.5, 1527500.0]],
|
||||||
'BTC/USDT': [[0.0, 0.004],
|
'BTC/USDT': [[0.0, 0.004, 0.0],
|
||||||
[50000.0, 0.005],
|
[50000.0, 0.005, 50.0],
|
||||||
[250000.0, 0.01],
|
[250000.0, 0.01, 1300.0],
|
||||||
[1000000.0, 0.025],
|
[1000000.0, 0.025, 16300.000000000002],
|
||||||
[5000000.0, 0.05],
|
[5000000.0, 0.05, 141300.0],
|
||||||
[20000000.0, 0.1],
|
[20000000.0, 0.1, 1141300.0],
|
||||||
[50000000.0, 0.125],
|
[50000000.0, 0.125, 2391300.0],
|
||||||
[100000000.0, 0.15],
|
[100000000.0, 0.15, 4891300.0],
|
||||||
[200000000.0, 0.25],
|
[200000000.0, 0.25, 24891300.0],
|
||||||
[300000000.0, 0.5]],
|
[300000000.0, 0.5, 99891300.0]],
|
||||||
"ZEC/USDT": [[0.0, 0.01],
|
"ZEC/USDT": [[0.0, 0.01, 0.0],
|
||||||
[5000.0, 0.025],
|
[5000.0, 0.025, 75.0],
|
||||||
[25000.0, 0.05],
|
[25000.0, 0.05, 700.0],
|
||||||
[100000.0, 0.1],
|
[100000.0, 0.1, 5700.0],
|
||||||
[250000.0, 0.125],
|
[250000.0, 0.125, 11949.999999999998],
|
||||||
[1000000.0, 0.5]],
|
[1000000.0, 0.5, 386950.0]]
|
||||||
}
|
}
|
||||||
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
@ -288,37 +288,37 @@ def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker):
|
|||||||
|
|
||||||
leverage_brackets = {
|
leverage_brackets = {
|
||||||
"1000SHIB/USDT": [
|
"1000SHIB/USDT": [
|
||||||
[0.0, 0.01],
|
[0.0, 0.01, 0.0],
|
||||||
[5000.0, 0.025],
|
[5000.0, 0.025, 75.0],
|
||||||
[25000.0, 0.05],
|
[25000.0, 0.05, 700.0],
|
||||||
[100000.0, 0.1],
|
[100000.0, 0.1, 5700.0],
|
||||||
[250000.0, 0.125],
|
[250000.0, 0.125, 11949.999999999998],
|
||||||
[1000000.0, 0.5]
|
[1000000.0, 0.5, 386950.0],
|
||||||
],
|
],
|
||||||
"1INCH/USDT": [
|
"1INCH/USDT": [
|
||||||
[0.0, 0.012],
|
[0.0, 0.012, 0.0],
|
||||||
[5000.0, 0.025],
|
[5000.0, 0.025, 65.0],
|
||||||
[25000.0, 0.05],
|
[25000.0, 0.05, 690.0],
|
||||||
[100000.0, 0.1],
|
[100000.0, 0.1, 5690.0],
|
||||||
[250000.0, 0.125],
|
[250000.0, 0.125, 11939.999999999998],
|
||||||
[1000000.0, 0.5]
|
[1000000.0, 0.5, 386940.0],
|
||||||
],
|
],
|
||||||
"AAVE/USDT": [
|
"AAVE/USDT": [
|
||||||
[0.0, 0.01],
|
[0.0, 0.01, 0.0],
|
||||||
[50000.0, 0.02],
|
[50000.0, 0.02, 500.0],
|
||||||
[250000.0, 0.05],
|
[250000.0, 0.05, 8000.000000000001],
|
||||||
[1000000.0, 0.1],
|
[1000000.0, 0.1, 58000.0],
|
||||||
[2000000.0, 0.125],
|
[2000000.0, 0.125, 107999.99999999999],
|
||||||
[5000000.0, 0.1665],
|
[5000000.0, 0.1665, 315500.00000000006],
|
||||||
[10000000.0, 0.25]
|
[10000000.0, 0.25, 1150500.0],
|
||||||
],
|
],
|
||||||
"ADA/BUSD": [
|
"ADA/BUSD": [
|
||||||
[0.0, 0.025],
|
[0.0, 0.025, 0.0],
|
||||||
[100000.0, 0.05],
|
[100000.0, 0.05, 2500.0],
|
||||||
[500000.0, 0.1],
|
[500000.0, 0.1, 27500.0],
|
||||||
[1000000.0, 0.15],
|
[1000000.0, 0.15, 77499.99999999999],
|
||||||
[2000000.0, 0.25],
|
[2000000.0, 0.25, 277500.0],
|
||||||
[5000000.0, 0.5]
|
[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
|
default_conf['collateral'] = collateral
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||||
assert exchange._ccxt_config == config
|
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!!
|
# Make sure to always keep one exchange here which is NOT subclassed!!
|
||||||
EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx', 'gateio']
|
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,
|
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.34559, 4, 0.001, 1, 2.345, 'spot'),
|
||||||
(2.9999, 4, 0.001, 1, 2.999, 'spot'),
|
(2.9999, 4, 0.001, 1, 2.999, 'spot'),
|
||||||
(2.9909, 4, 0.001, 1, 2.990, 'spot'),
|
(2.9909, 4, 0.001, 1, 2.990, 'spot'),
|
||||||
(2.9909, 4, 0.005, 0.01, 299.09, 'futures'),
|
(2.9909, 4, 0.005, 0.01, 2.99, 'futures'),
|
||||||
(2.9999, 4, 0.005, 10, 0.295, 'futures'),
|
(2.9999, 4, 0.005, 10, 2.995, 'futures'),
|
||||||
])
|
])
|
||||||
def test_amount_to_precision(
|
def test_amount_to_precision(
|
||||||
default_conf,
|
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.CROSS, True),
|
||||||
("bittrex", TradingMode.FUTURES, Collateral.ISOLATED, True),
|
("bittrex", TradingMode.FUTURES, Collateral.ISOLATED, True),
|
||||||
("gateio", TradingMode.MARGIN, 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.MARGIN, Collateral.CROSS, True),
|
||||||
("binance", TradingMode.FUTURES, Collateral.CROSS, True),
|
("binance", TradingMode.FUTURES, Collateral.CROSS, True),
|
||||||
("binance", TradingMode.FUTURES, Collateral.ISOLATED, True),
|
|
||||||
("kraken", TradingMode.MARGIN, Collateral.CROSS, True),
|
("kraken", TradingMode.MARGIN, Collateral.CROSS, True),
|
||||||
("kraken", TradingMode.FUTURES, Collateral.CROSS, True),
|
("kraken", TradingMode.FUTURES, Collateral.CROSS, True),
|
||||||
("ftx", TradingMode.MARGIN, Collateral.CROSS, True),
|
("ftx", TradingMode.MARGIN, Collateral.CROSS, True),
|
||||||
("ftx", TradingMode.FUTURES, Collateral.CROSS, True),
|
("ftx", TradingMode.FUTURES, Collateral.CROSS, True),
|
||||||
("gateio", TradingMode.MARGIN, Collateral.CROSS, True),
|
("gateio", TradingMode.MARGIN, Collateral.CROSS, True),
|
||||||
("gateio", TradingMode.FUTURES, 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.MARGIN, Collateral.CROSS, False),
|
||||||
# ("binance", TradingMode.FUTURES, Collateral.CROSS, False),
|
# ("binance", TradingMode.FUTURES, Collateral.CROSS, False),
|
||||||
# ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False),
|
|
||||||
# ("kraken", TradingMode.MARGIN, Collateral.CROSS, False),
|
# ("kraken", TradingMode.MARGIN, Collateral.CROSS, False),
|
||||||
# ("kraken", TradingMode.FUTURES, Collateral.CROSS, False),
|
# ("kraken", TradingMode.FUTURES, Collateral.CROSS, False),
|
||||||
# ("ftx", TradingMode.MARGIN, Collateral.CROSS, False),
|
# ("ftx", TradingMode.MARGIN, Collateral.CROSS, False),
|
||||||
# ("ftx", TradingMode.FUTURES, Collateral.CROSS, False),
|
# ("ftx", TradingMode.FUTURES, Collateral.CROSS, False),
|
||||||
# ("gateio", TradingMode.MARGIN, Collateral.CROSS, False),
|
# ("gateio", TradingMode.MARGIN, Collateral.CROSS, False),
|
||||||
# ("gateio", TradingMode.FUTURES, Collateral.CROSS, False),
|
# ("gateio", TradingMode.FUTURES, Collateral.CROSS, False),
|
||||||
# ("gateio", TradingMode.FUTURES, Collateral.ISOLATED, False),
|
|
||||||
])
|
])
|
||||||
def test_validate_trading_mode_and_collateral(
|
def test_validate_trading_mode_and_collateral(
|
||||||
default_conf,
|
default_conf,
|
||||||
@ -3582,6 +3593,68 @@ def test_calculate_funding_fees(
|
|||||||
) == kraken_fee
|
) == 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', [
|
@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: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),
|
('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,
|
amount,
|
||||||
expected_fees
|
expected_fees
|
||||||
):
|
):
|
||||||
'''
|
"""
|
||||||
nominal_value = mark_price * size
|
nominal_value = mark_price * size
|
||||||
funding_fee = nominal_value * funding_rate
|
funding_fee = nominal_value * funding_rate
|
||||||
size: 30
|
size: 30
|
||||||
time: 0, mark: 2.77, nominal_value: 83.1, fundRate: -0.000008, fundFee: -0.0006648
|
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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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
|
time: 13, mark: 2.82, nominal_value: 84.6, fundRate: 0.000097, fundFee: 0.0082062
|
||||||
|
|
||||||
size: 50
|
size: 50
|
||||||
time: 0, mark: 2.77, nominal_value: 138.5, fundRate: -0.000008, fundFee: -0.001108
|
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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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: 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
|
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')
|
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')
|
d2 = datetime.strptime(f"{d2} +0000", '%Y-%m-%d %H:%M:%S %z')
|
||||||
funding_rate_history = {
|
funding_rate_history = {
|
||||||
@ -3909,3 +3982,69 @@ def test__amount_to_contracts(
|
|||||||
assert result_size == param_size
|
assert result_size == param_size
|
||||||
result_amount = exchange._contracts_to_amount(pair, param_size)
|
result_amount = exchange._contracts_to_amount(pair, param_size)
|
||||||
assert result_amount == param_amount
|
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
|
import pytest
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import Gateio
|
from freqtrade.exchange import Gateio
|
||||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||||
|
from tests.conftest import get_patched_exchange
|
||||||
|
|
||||||
|
|
||||||
def test_validate_order_types_gateio(default_conf, mocker):
|
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,
|
with pytest.raises(OperationalException,
|
||||||
match=r'Exchange .* does not support market orders.'):
|
match=r'Exchange .* does not support market orders.'):
|
||||||
ExchangeResolver.load_exchange('gateio', default_conf, True)
|
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]
|
CandleType.SPOT) in refresh_mock.call_args[0][0]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("trading_mode", [
|
@pytest.mark.parametrize("is_short,trading_mode,exchange_name,margin_mode,liq_price", [
|
||||||
'spot',
|
(False, 'spot', 'binance', None, None),
|
||||||
# TODO-lev: Enable other modes
|
(True, 'spot', 'binance', None, None),
|
||||||
# 'margin', 'futures'
|
(False, 'spot', 'gateio', None, None),
|
||||||
]
|
(True, 'spot', 'gateio', None, None),
|
||||||
)
|
(False, 'spot', 'okex', None, None),
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
(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,
|
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)]
|
open_order = limit_order_open[enter_side(is_short)]
|
||||||
order = limit_order[enter_side(is_short)]
|
order = limit_order[enter_side(is_short)]
|
||||||
default_conf_usdt['trading_mode'] = trading_mode
|
default_conf_usdt['trading_mode'] = trading_mode
|
||||||
leverage = 1.0 if trading_mode == 'spot' else 3.0
|
leverage = 1.0 if trading_mode == 'spot' else 5.0
|
||||||
default_conf_usdt['collateral'] = 'cross'
|
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_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker, id=exchange_name)
|
||||||
freqtrade = FreqtradeBot(default_conf_usdt)
|
freqtrade = FreqtradeBot(default_conf_usdt)
|
||||||
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False)
|
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False)
|
||||||
freqtrade.strategy.leverage = MagicMock(return_value=leverage)
|
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_min_pair_stake_amount=MagicMock(return_value=1),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_funding_fees=MagicMock(return_value=0),
|
get_funding_fees=MagicMock(return_value=0),
|
||||||
|
name=exchange_name
|
||||||
)
|
)
|
||||||
pair = 'ETH/USDT'
|
pair = 'ETH/USDT'
|
||||||
|
|
||||||
@ -886,14 +916,21 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
|
|||||||
assert trade.open_rate_requested == 10
|
assert trade.open_rate_requested == 10
|
||||||
|
|
||||||
# In case of custom entry price not float type
|
# 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['status'] = 'open'
|
||||||
order['id'] = '5568'
|
order['id'] = '5568'
|
||||||
freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price"
|
freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price"
|
||||||
assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
|
assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
|
||||||
trade = Trade.query.all()[8]
|
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
|
trade.is_short = is_short
|
||||||
assert trade
|
assert trade
|
||||||
assert trade.open_rate_requested == 10
|
assert trade.open_rate_requested == 10
|
||||||
|
assert trade.isolated_liq == liq_price
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_short", [False, True])
|
@pytest.mark.parametrize("is_short", [False, True])
|
||||||
@ -4794,23 +4831,23 @@ def test_update_funding_fees(
|
|||||||
limit_order_open,
|
limit_order_open,
|
||||||
schedule_off
|
schedule_off
|
||||||
):
|
):
|
||||||
'''
|
"""
|
||||||
nominal_value = mark_price * size
|
nominal_value = mark_price * size
|
||||||
funding_fee = nominal_value * funding_rate
|
funding_fee = nominal_value * funding_rate
|
||||||
size = 123
|
size = 123
|
||||||
"LTC/BTC"
|
"LTC/USDT"
|
||||||
time: 0, mark: 3.3, fundRate: 0.00032583, nominal_value: 405.9, fundFee: 0.132254397
|
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
|
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: 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
|
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: 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
|
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: 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
|
time: 8, mark: 1.2, fundRate: 0.00032715, nominal_value: 147.6, fundFee: 0.04828734
|
||||||
'''
|
"""
|
||||||
# SETUP
|
# SETUP
|
||||||
time_machine.move_to("2021-09-01 00:00:00 +00:00")
|
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
|
# 16:00 entry is actually never used
|
||||||
# But should be kept in the test to ensure we're filtering correctly.
|
# But should be kept in the test to ensure we're filtering correctly.
|
||||||
funding_rates = {
|
funding_rates = {
|
||||||
"LTC/BTC":
|
"LTC/USDT":
|
||||||
DataFrame([
|
DataFrame([
|
||||||
[date_midnight, 0.00032583, 0, 0, 0, 0],
|
[date_midnight, 0.00032583, 0, 0, 0, 0],
|
||||||
[date_eight, 0.00024472, 0, 0, 0, 0],
|
[date_eight, 0.00024472, 0, 0, 0, 0],
|
||||||
[date_sixteen, 0.00024472, 0, 0, 0, 0],
|
[date_sixteen, 0.00024472, 0, 0, 0, 0],
|
||||||
], columns=columns),
|
], columns=columns),
|
||||||
"ETH/BTC":
|
"ETH/USDT":
|
||||||
DataFrame([
|
DataFrame([
|
||||||
[date_midnight, 0.0001, 0, 0, 0, 0],
|
[date_midnight, 0.0001, 0, 0, 0, 0],
|
||||||
[date_eight, 0.0001, 0, 0, 0, 0],
|
[date_eight, 0.0001, 0, 0, 0, 0],
|
||||||
[date_sixteen, 0.0001, 0, 0, 0, 0],
|
[date_sixteen, 0.0001, 0, 0, 0, 0],
|
||||||
], columns=columns),
|
], columns=columns),
|
||||||
"XRP/BTC":
|
"XRP/USDT":
|
||||||
DataFrame([
|
DataFrame([
|
||||||
[date_midnight, 0.00049426, 0, 0, 0, 0],
|
[date_midnight, 0.00049426, 0, 0, 0, 0],
|
||||||
[date_eight, 0.00032715, 0, 0, 0, 0],
|
[date_eight, 0.00032715, 0, 0, 0, 0],
|
||||||
@ -4852,19 +4889,19 @@ def test_update_funding_fees(
|
|||||||
}
|
}
|
||||||
|
|
||||||
mark_prices = {
|
mark_prices = {
|
||||||
"LTC/BTC":
|
"LTC/USDT":
|
||||||
DataFrame([
|
DataFrame([
|
||||||
[date_midnight, 3.3, 0, 0, 0, 0],
|
[date_midnight, 3.3, 0, 0, 0, 0],
|
||||||
[date_eight, 3.2, 0, 0, 0, 0],
|
[date_eight, 3.2, 0, 0, 0, 0],
|
||||||
[date_sixteen, 3.2, 0, 0, 0, 0],
|
[date_sixteen, 3.2, 0, 0, 0, 0],
|
||||||
], columns=columns),
|
], columns=columns),
|
||||||
"ETH/BTC":
|
"ETH/USDT":
|
||||||
DataFrame([
|
DataFrame([
|
||||||
[date_midnight, 2.4, 0, 0, 0, 0],
|
[date_midnight, 2.4, 0, 0, 0, 0],
|
||||||
[date_eight, 2.5, 0, 0, 0, 0],
|
[date_eight, 2.5, 0, 0, 0, 0],
|
||||||
[date_sixteen, 2.5, 0, 0, 0, 0],
|
[date_sixteen, 2.5, 0, 0, 0, 0],
|
||||||
], columns=columns),
|
], columns=columns),
|
||||||
"XRP/BTC":
|
"XRP/USDT":
|
||||||
DataFrame([
|
DataFrame([
|
||||||
[date_midnight, 1.2, 0, 0, 0, 0],
|
[date_midnight, 1.2, 0, 0, 0, 0],
|
||||||
[date_eight, 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)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
# initial funding fees,
|
# initial funding fees,
|
||||||
freqtrade.execute_entry('ETH/BTC', 123, is_short=is_short)
|
freqtrade.execute_entry('ETH/USDT', 123, is_short=is_short)
|
||||||
freqtrade.execute_entry('LTC/BTC', 2.0, is_short=is_short)
|
freqtrade.execute_entry('LTC/USDT', 2.0, is_short=is_short)
|
||||||
freqtrade.execute_entry('XRP/BTC', 123, is_short=is_short)
|
freqtrade.execute_entry('XRP/USDT', 123, is_short=is_short)
|
||||||
multipl = 1 if is_short else -1
|
multipl = 1 if is_short else -1
|
||||||
trades = Trade.get_open_trades()
|
trades = Trade.get_open_trades()
|
||||||
assert len(trades) == 3
|
assert len(trades) == 3
|
||||||
|
@ -148,7 +148,7 @@ def test_set_stop_loss_isolated_liq(fee):
|
|||||||
trade.stop_loss = None
|
trade.stop_loss = None
|
||||||
trade.initial_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.isolated_liq == 0.09
|
||||||
assert trade.stop_loss == 0.09
|
assert trade.stop_loss == 0.09
|
||||||
assert trade.initial_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.stop_loss == 0.08
|
||||||
assert trade.initial_stop_loss == 0.09
|
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.isolated_liq == 0.1
|
||||||
assert trade.stop_loss == 0.08
|
assert trade.stop_loss == 0.08
|
||||||
assert trade.initial_stop_loss == 0.09
|
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.isolated_liq == 0.07
|
||||||
assert trade.stop_loss == 0.07
|
assert trade.stop_loss == 0.07
|
||||||
assert trade.initial_stop_loss == 0.09
|
assert trade.initial_stop_loss == 0.09
|
||||||
|
Loading…
Reference in New Issue
Block a user