Merge pull request #6386 from samgermain/liq-buffer
Added liquidation_buffer to freqtradebot
This commit is contained in:
commit
50e474ded2
@ -101,6 +101,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
|
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
|
||||||
| `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md). <br>*Defaults to `"spot"`.* <br> **Datatype:** String
|
| `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md). <br>*Defaults to `"spot"`.* <br> **Datatype:** String
|
||||||
| `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md). <br> **Datatype:** String
|
| `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md). <br> **Datatype:** String
|
||||||
|
| `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md). <br>*Defaults to `0.05`.* <br> **Datatype:** Float
|
||||||
| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||||
| `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
| `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
|
||||||
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
|
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
## Understand `trading_mode`
|
## Understand `trading_mode`
|
||||||
|
|
||||||
The possible values are: `spot` (default), `margin`(*coming soon*) or `futures`.
|
The possible values are: `spot` (default), `margin`(*Currently unavailable*) or `futures`.
|
||||||
|
|
||||||
### Spot
|
### Spot
|
||||||
|
|
||||||
@ -69,6 +69,18 @@ One account is used to share collateral between markets (trading pairs). Margin
|
|||||||
"margin_mode": "cross"
|
"margin_mode": "cross"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Understand `liquidation_buffer`
|
||||||
|
*Defaults to `0.05`.*
|
||||||
|
|
||||||
|
A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price
|
||||||
|
|
||||||
|
Possible values are any floats between 0.0 and 0.99
|
||||||
|
|
||||||
|
**ex:** If a trade is entered at a price of 10 coin/USDT, and the liquidation price of this trade is 8 coin/USDT, then with `liquidation_buffer` set to `0.05` the minimum stoploss for this trade would be 8 + ((10 - 8) * 0.05) = 8 + 0.1 = 8.1
|
||||||
|
|
||||||
|
!!! Danger "A `liquidation_buffer` of 0.0, or a low `liquidation_buffer` is likely to result in liquidations, and liquidation fees"
|
||||||
|
Currently Freqtrade is able to calculate liquidation prices, but does not calculate liquidation fees. Setting your `liquidation_buffer` to 0.0, or using a low `liquidation_buffer` could result in your positions being liquidated. Freqtrade does not track liquidation fees, so liquidations will result in inaccurate profit/loss results for your bot. If you use a low `liquidation_buffer`, it is recommended to use `stoploss_on_exchange` if your exchange supports this.
|
||||||
|
|
||||||
### Developer
|
### Developer
|
||||||
|
|
||||||
#### Margin mode
|
#### Margin mode
|
||||||
@ -82,3 +94,4 @@ All Fees are included in `current_profit` calculations during the trade.
|
|||||||
#### FUTURES MODE
|
#### FUTURES MODE
|
||||||
|
|
||||||
Funding fees are either added or subtracted from the total amount of a trade
|
Funding fees are either added or subtracted from the total amount of a trade
|
||||||
|
|
||||||
|
@ -156,6 +156,7 @@ CONF_SCHEMA = {
|
|||||||
'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},
|
||||||
'margin_mode': {'type': 'string', 'enum': MARGIN_MODES},
|
'margin_mode': {'type': 'string', 'enum': MARGIN_MODES},
|
||||||
|
'liquidation_buffer': {'type': 'number', 'minimum': 0.0, 'maximum': 0.99},
|
||||||
'backtest_breakdown': {
|
'backtest_breakdown': {
|
||||||
'type': 'array',
|
'type': 'array',
|
||||||
'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS}
|
'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS}
|
||||||
|
@ -135,13 +135,14 @@ class Exchange:
|
|||||||
self._trades_pagination = self._ft_has['trades_pagination']
|
self._trades_pagination = self._ft_has['trades_pagination']
|
||||||
self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
|
self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
|
||||||
|
|
||||||
|
# Leverage properties
|
||||||
self.trading_mode = TradingMode(config.get('trading_mode', 'spot'))
|
self.trading_mode = TradingMode(config.get('trading_mode', 'spot'))
|
||||||
|
|
||||||
self.margin_mode: Optional[MarginMode] = (
|
self.margin_mode: Optional[MarginMode] = (
|
||||||
MarginMode(config.get('margin_mode'))
|
MarginMode(config.get('margin_mode'))
|
||||||
if config.get('margin_mode')
|
if config.get('margin_mode')
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
self.liquidation_buffer = config.get('liquidation_buffer', 0.05)
|
||||||
|
|
||||||
# Initialize ccxt objects
|
# Initialize ccxt objects
|
||||||
ccxt_config = self._ccxt_config
|
ccxt_config = self._ccxt_config
|
||||||
@ -2062,7 +2063,7 @@ class Exchange:
|
|||||||
|
|
||||||
if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
|
if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
|
||||||
|
|
||||||
return self.dry_run_liquidation_price(
|
isolated_liq = self.dry_run_liquidation_price(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
open_rate=open_rate,
|
open_rate=open_rate,
|
||||||
is_short=is_short,
|
is_short=is_short,
|
||||||
@ -2071,21 +2072,32 @@ class Exchange:
|
|||||||
mm_ex_1=mm_ex_1,
|
mm_ex_1=mm_ex_1,
|
||||||
upnl_ex_1=upnl_ex_1
|
upnl_ex_1=upnl_ex_1
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
positions = self._api.fetch_positions([pair])
|
||||||
|
if len(positions) > 0:
|
||||||
|
pos = positions[0]
|
||||||
|
isolated_liq = 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
|
||||||
|
|
||||||
try:
|
if isolated_liq:
|
||||||
positions = self._api.fetch_positions([pair])
|
buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
|
||||||
if len(positions) > 0:
|
isolated_liq = (
|
||||||
pos = positions[0]
|
isolated_liq - buffer_amount
|
||||||
return pos['liquidationPrice']
|
if is_short else
|
||||||
else:
|
isolated_liq + buffer_amount
|
||||||
return None
|
)
|
||||||
except ccxt.DDoSProtection as e:
|
return isolated_liq
|
||||||
raise DDosProtection(e) from e
|
else:
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
return None
|
||||||
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(
|
def get_maintenance_ratio_and_amt(
|
||||||
self,
|
self,
|
||||||
|
@ -103,8 +103,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self._exit_lock = Lock()
|
self._exit_lock = Lock()
|
||||||
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
||||||
|
|
||||||
|
self.liquidation_buffer = float(self.config.get('liquidation_buffer', '0.05'))
|
||||||
self.trading_mode = TradingMode(self.config.get('trading_mode', 'spot'))
|
self.trading_mode = TradingMode(self.config.get('trading_mode', 'spot'))
|
||||||
|
|
||||||
self.margin_mode_type: Optional[MarginMode] = None
|
self.margin_mode_type: Optional[MarginMode] = None
|
||||||
if 'margin_mode' in self.config:
|
if 'margin_mode' in self.config:
|
||||||
self.margin_mode = MarginMode(self.config['margin_mode'])
|
self.margin_mode = MarginMode(self.config['margin_mode'])
|
||||||
|
@ -3664,17 +3664,29 @@ def test_get_liquidation_price(mocker, default_conf):
|
|||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
default_conf['trading_mode'] = 'futures'
|
default_conf['trading_mode'] = 'futures'
|
||||||
default_conf['margin_mode'] = 'isolated'
|
default_conf['margin_mode'] = 'isolated'
|
||||||
|
default_conf['liquidation_buffer'] = 0.0
|
||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
liq_price = exchange.get_liquidation_price(
|
liq_price = exchange.get_liquidation_price(
|
||||||
pair='NEAR/USDT:USDT',
|
pair='NEAR/USDT:USDT',
|
||||||
open_rate=0.0,
|
open_rate=18.884,
|
||||||
is_short=False,
|
is_short=False,
|
||||||
position=0.0,
|
position=0.8,
|
||||||
wallet_balance=0.0,
|
wallet_balance=0.8,
|
||||||
)
|
)
|
||||||
assert liq_price == 17.47
|
assert liq_price == 17.47
|
||||||
|
|
||||||
|
default_conf['liquidation_buffer'] = 0.05
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
liq_price = exchange.get_liquidation_price(
|
||||||
|
pair='NEAR/USDT:USDT',
|
||||||
|
open_rate=18.884,
|
||||||
|
is_short=False,
|
||||||
|
position=0.8,
|
||||||
|
wallet_balance=0.8,
|
||||||
|
)
|
||||||
|
assert liq_price == 17.540699999999998
|
||||||
|
|
||||||
ccxt_exceptionhandlers(
|
ccxt_exceptionhandlers(
|
||||||
mocker,
|
mocker,
|
||||||
default_conf,
|
default_conf,
|
||||||
@ -4073,6 +4085,7 @@ def test_liquidation_price(
|
|||||||
):
|
):
|
||||||
default_conf['trading_mode'] = trading_mode
|
default_conf['trading_mode'] = trading_mode
|
||||||
default_conf['margin_mode'] = margin_mode
|
default_conf['margin_mode'] = margin_mode
|
||||||
|
default_conf['liquidation_buffer'] = 0.0
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(mm_ratio, maintenance_amt))
|
exchange.get_maintenance_ratio_and_amt = MagicMock(return_value=(mm_ratio, maintenance_amt))
|
||||||
assert isclose(round(exchange.get_liquidation_price(
|
assert isclose(round(exchange.get_liquidation_price(
|
||||||
|
@ -707,23 +707,27 @@ 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("is_short,trading_mode,exchange_name,margin_mode,liq_price", [
|
@pytest.mark.parametrize("is_short,trading_mode,exchange_name,margin_mode,liq_buffer,liq_price", [
|
||||||
(False, 'spot', 'binance', None, None),
|
(False, 'spot', 'binance', None, 0.0, None),
|
||||||
(True, 'spot', 'binance', None, None),
|
(True, 'spot', 'binance', None, 0.0, None),
|
||||||
(False, 'spot', 'gateio', None, None),
|
(False, 'spot', 'gateio', None, 0.0, None),
|
||||||
(True, 'spot', 'gateio', None, None),
|
(True, 'spot', 'gateio', None, 0.0, None),
|
||||||
(False, 'spot', 'okx', None, None),
|
(False, 'spot', 'okx', None, 0.0, None),
|
||||||
(True, 'spot', 'okx', None, None),
|
(True, 'spot', 'okx', None, 0.0, None),
|
||||||
(True, 'futures', 'binance', 'isolated', 11.89108910891089),
|
(True, 'futures', 'binance', 'isolated', 0.0, 11.89108910891089),
|
||||||
(False, 'futures', 'binance', 'isolated', 8.070707070707071),
|
(False, 'futures', 'binance', 'isolated', 0.0, 8.070707070707071),
|
||||||
(True, 'futures', 'gateio', 'isolated', 11.87413417771621),
|
(True, 'futures', 'gateio', 'isolated', 0.0, 11.87413417771621),
|
||||||
(False, 'futures', 'gateio', 'isolated', 8.085708510208207),
|
(False, 'futures', 'gateio', 'isolated', 0.0, 8.085708510208207),
|
||||||
# (True, 'futures', 'okx', 'isolated', 11.87413417771621),
|
(True, 'futures', 'binance', 'isolated', 0.05, 11.796534653465345),
|
||||||
# (False, 'futures', 'okx', 'isolated', 8.085708510208207),
|
(False, 'futures', 'binance', 'isolated', 0.05, 8.167171717171717),
|
||||||
|
(True, 'futures', 'gateio', 'isolated', 0.05, 11.7804274688304),
|
||||||
|
(False, 'futures', 'gateio', 'isolated', 0.05, 8.181423084697796),
|
||||||
|
# (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,
|
limit_order_open, is_short, trading_mode,
|
||||||
exchange_name, margin_mode, liq_price) -> None:
|
exchange_name, margin_mode, liq_buffer, liq_price) -> None:
|
||||||
"""
|
"""
|
||||||
exchange_name = binance, is_short = true
|
exchange_name = binance, is_short = true
|
||||||
leverage = 5
|
leverage = 5
|
||||||
@ -747,6 +751,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
|
|||||||
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
|
||||||
|
default_conf_usdt['liquidation_buffer'] = liq_buffer
|
||||||
leverage = 1.0 if trading_mode == 'spot' else 5.0
|
leverage = 1.0 if trading_mode == 'spot' else 5.0
|
||||||
default_conf_usdt['exchange']['name'] = exchange_name
|
default_conf_usdt['exchange']['name'] = exchange_name
|
||||||
if margin_mode:
|
if margin_mode:
|
||||||
@ -4808,6 +4813,7 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None:
|
|||||||
assert valid_price_at_min_alwd < proposed_price
|
assert valid_price_at_min_alwd < proposed_price
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('liquidation_buffer', [0.0, 0.05])
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"is_short,trading_mode,exchange_name,margin_mode,leverage,open_rate,amount,expected_liq", [
|
"is_short,trading_mode,exchange_name,margin_mode,leverage,open_rate,amount,expected_liq", [
|
||||||
(False, 'spot', 'binance', '', 5.0, 10.0, 1.0, None),
|
(False, 'spot', 'binance', '', 5.0, 10.0, 1.0, None),
|
||||||
@ -4849,6 +4855,7 @@ def test_leverage_prep(
|
|||||||
open_rate,
|
open_rate,
|
||||||
amount,
|
amount,
|
||||||
expected_liq,
|
expected_liq,
|
||||||
|
liquidation_buffer,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
position = 0.2 * 5
|
position = 0.2 * 5
|
||||||
@ -4902,6 +4909,7 @@ def test_leverage_prep(
|
|||||||
leverage = 5, open_rate = 8, amount = 1.0
|
leverage = 5, open_rate = 8, amount = 1.0
|
||||||
(8 - (1.6 / 1.0)) / (1 + (0.01 + 0.0006)) = 6.332871561448645
|
(8 - (1.6 / 1.0)) / (1 + (0.01 + 0.0006)) = 6.332871561448645
|
||||||
"""
|
"""
|
||||||
|
default_conf_usdt['liquidation_buffer'] = liquidation_buffer
|
||||||
default_conf_usdt['trading_mode'] = trading_mode
|
default_conf_usdt['trading_mode'] = trading_mode
|
||||||
default_conf_usdt['exchange']['name'] = exchange_name
|
default_conf_usdt['exchange']['name'] = exchange_name
|
||||||
default_conf_usdt['margin_mode'] = margin_mode
|
default_conf_usdt['margin_mode'] = margin_mode
|
||||||
@ -4926,6 +4934,8 @@ def test_leverage_prep(
|
|||||||
if expected_liq is None:
|
if expected_liq is None:
|
||||||
assert liq is None
|
assert liq is None
|
||||||
else:
|
else:
|
||||||
|
buffer_amount = liquidation_buffer * abs(open_rate - expected_liq)
|
||||||
|
expected_liq = expected_liq - buffer_amount if is_short else expected_liq + buffer_amount
|
||||||
isclose(expected_liq, liq)
|
isclose(expected_liq, liq)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user