Merge pull request #6555 from freqtrade/inverse_leverage_minstake

Inverse leverage with stake detection
This commit is contained in:
Matthias 2022-03-20 08:38:59 +01:00 committed by GitHub
commit a64ca541a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 108 additions and 60 deletions

View File

@ -42,8 +42,8 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and
* Check if trade-slots are still available (if `max_open_trades` is reached). * Check if trade-slots are still available (if `max_open_trades` is reached).
* Verifies buy signal trying to enter new positions. * Verifies buy signal trying to enter new positions.
* Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback. * Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback.
* Determine stake size by calling the `custom_stake_amount()` callback.
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage. * In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
* Determine stake size by calling the `custom_stake_amount()` callback.
* Before a buy order is placed, `confirm_trade_entry()` strategy callback is called. * Before a buy order is placed, `confirm_trade_entry()` strategy callback is called.
This loop will be repeated again and again until the bot is stopped. This loop will be repeated again and again until the bot is stopped.
@ -59,8 +59,8 @@ This loop will be repeated again and again until the bot is stopped.
* Loops per candle simulating entry and exit points. * Loops per candle simulating entry and exit points.
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy). * Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
* Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle). * Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle).
* Determine stake size by calling the `custom_stake_amount()` callback.
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage. * In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
* Determine stake size by calling the `custom_stake_amount()` callback.
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested. * Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points. * Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
* For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle). * For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).

View File

@ -600,26 +600,12 @@ class FreqtradeBot(LoggingMixin):
trade_side = 'short' if is_short else 'long' trade_side = 'short' if is_short else 'long'
pos_adjust = trade is not None pos_adjust = trade is not None
enter_limit_requested, stake_amount = self.get_valid_enter_price_and_stake( enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake(
pair, price, stake_amount, side, trade_side, enter_tag, trade) pair, price, stake_amount, side, trade_side, enter_tag, trade)
if not stake_amount: if not stake_amount:
return False return False
if not pos_adjust:
max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair,
current_time=datetime.now(timezone.utc),
current_rate=enter_limit_requested,
proposed_leverage=1.0,
max_leverage=max_leverage,
side=trade_side,
) if self.trading_mode != TradingMode.SPOT else 1.0
# Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage)
else:
# Changing leverage currently not possible
leverage = trade.leverage if trade else 1.0
if pos_adjust: if pos_adjust:
logger.info(f"Position adjust: about to create a new order for {pair} with stake: " logger.info(f"Position adjust: about to create a new order for {pair} with stake: "
f"{stake_amount} for {trade}") f"{stake_amount} for {trade}")
@ -775,7 +761,7 @@ class FreqtradeBot(LoggingMixin):
side: str, trade_side: str, side: str, trade_side: str,
entry_tag: Optional[str], entry_tag: Optional[str],
trade: Optional[Trade] trade: Optional[Trade]
) -> Tuple[float, float]: ) -> Tuple[float, float, float]:
if price: if price:
enter_limit_requested = price enter_limit_requested = price
@ -792,13 +778,30 @@ class FreqtradeBot(LoggingMixin):
if not enter_limit_requested: if not enter_limit_requested:
raise PricingError(f'Could not determine {side} price.') raise PricingError(f'Could not determine {side} price.')
if trade is None:
max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair,
current_time=datetime.now(timezone.utc),
current_rate=enter_limit_requested,
proposed_leverage=1.0,
max_leverage=max_leverage,
side=trade_side,
) if self.trading_mode != TradingMode.SPOT else 1.0
# Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage)
else:
# Changing leverage currently not possible
leverage = trade.leverage if trade else 1.0
# Min-stake-amount should actually include Leverage - this way our "minimal" # Min-stake-amount should actually include Leverage - this way our "minimal"
# stake- amount might be higher than necessary. # stake- amount might be higher than necessary.
# We do however also need min-stake to determine leverage, therefore this is ignored as # We do however also need min-stake to determine leverage, therefore this is ignored as
# edge-case for now. # edge-case for now.
min_stake_amount = self.exchange.get_min_pair_stake_amount( min_stake_amount = self.exchange.get_min_pair_stake_amount(
pair, enter_limit_requested, self.strategy.stoploss) pair, enter_limit_requested, self.strategy.stoploss, leverage)
max_stake_amount = self.exchange.get_max_pair_stake_amount(pair, enter_limit_requested) max_stake_amount = self.exchange.get_max_pair_stake_amount(
pair, enter_limit_requested, leverage)
if not self.edge and trade is None: if not self.edge and trade is None:
stake_available = self.wallets.get_available_stake_amount() stake_available = self.wallets.get_available_stake_amount()
@ -817,7 +820,7 @@ class FreqtradeBot(LoggingMixin):
max_stake_amount=max_stake_amount, max_stake_amount=max_stake_amount,
) )
return enter_limit_requested, stake_amount return enter_limit_requested, stake_amount, leverage
def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None, def _notify_enter(self, trade: Trade, order: Dict, order_type: Optional[str] = None,
fill: bool = False) -> None: fill: bool = False) -> None:

View File

@ -635,18 +635,15 @@ class Backtesting:
else: else:
return self._get_sell_trade_entry_for_candle(trade, sell_row) return self._get_sell_trade_entry_for_candle(trade, sell_row)
def _enter_trade(self, pair: str, row: Tuple, direction: str, def get_valid_price_and_stake(
stake_amount: Optional[float] = None, self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]: direction: str, current_time: datetime, entry_tag: Optional[str],
trade: Optional[LocalTrade], order_type: str
) -> Tuple[float, float, float, float]:
current_time = row[DATE_IDX].to_pydatetime()
entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None
# let's call the custom entry price, using the open price as default price
order_type = self.strategy.order_types['entry']
propose_rate = row[OPEN_IDX]
if order_type == 'limit': if order_type == 'limit':
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=row[OPEN_IDX])( default_retval=propose_rate)(
pair=pair, current_time=current_time, pair=pair, current_time=current_time,
proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate
# We can't place orders higher than current high (otherwise it'd be a stop limit buy) # We can't place orders higher than current high (otherwise it'd be a stop limit buy)
@ -656,39 +653,14 @@ class Backtesting:
else: else:
propose_rate = min(propose_rate, row[HIGH_IDX]) propose_rate = min(propose_rate, row[HIGH_IDX])
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0
max_stake_amount = self.exchange.get_max_pair_stake_amount(pair, propose_rate)
stake_available = self.wallets.get_available_stake_amount()
pos_adjust = trade is not None pos_adjust = trade is not None
leverage = trade.leverage if trade else 1.0
if not pos_adjust: if not pos_adjust:
try: try:
stake_amount = self.wallets.get_trade_stake_amount(pair, None, update=False) stake_amount = self.wallets.get_trade_stake_amount(pair, None, update=False)
except DependencyException: except DependencyException:
return None return 0, 0, 0, 0
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)(
pair=pair, current_time=current_time, current_rate=propose_rate,
proposed_stake=stake_amount, min_stake=min_stake_amount,
max_stake=min(stake_available, max_stake_amount),
entry_tag=entry_tag, side=direction)
stake_amount = self.wallets.validate_stake_amount(
pair=pair,
stake_amount=stake_amount,
min_stake_amount=min_stake_amount,
max_stake_amount=max_stake_amount,
)
if not stake_amount:
# In case of pos adjust, still return the original trade
# If not pos adjust, trade is None
return trade
order_type = self.strategy.order_types['entry']
time_in_force = self.strategy.order_time_in_force['entry']
if not pos_adjust:
max_leverage = self.exchange.get_max_leverage(pair, stake_amount) max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)( leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair, pair=pair,
@ -701,14 +673,57 @@ class Backtesting:
# Cap leverage between 1.0 and max_leverage. # Cap leverage between 1.0 and max_leverage.
leverage = min(max(leverage, 1.0), max_leverage) leverage = min(max(leverage, 1.0), max_leverage)
min_stake_amount = self.exchange.get_min_pair_stake_amount(
pair, propose_rate, -0.05, leverage=leverage) or 0
max_stake_amount = self.exchange.get_max_pair_stake_amount(
pair, propose_rate, leverage=leverage)
stake_available = self.wallets.get_available_stake_amount()
if not pos_adjust:
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)(
pair=pair, current_time=current_time, current_rate=propose_rate,
proposed_stake=stake_amount, min_stake=min_stake_amount,
max_stake=min(stake_available, max_stake_amount),
entry_tag=entry_tag, side=direction)
stake_amount_val = self.wallets.validate_stake_amount(
pair=pair,
stake_amount=stake_amount,
min_stake_amount=min_stake_amount,
max_stake_amount=max_stake_amount,
)
return propose_rate, stake_amount_val, leverage, min_stake_amount
def _enter_trade(self, pair: str, row: Tuple, direction: str,
stake_amount: Optional[float] = None,
trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]:
current_time = row[DATE_IDX].to_pydatetime()
entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None
# let's call the custom entry price, using the open price as default price
order_type = self.strategy.order_types['entry']
pos_adjust = trade is not None
propose_rate, stake_amount, leverage, min_stake_amount = self.get_valid_price_and_stake(
pair, row, row[OPEN_IDX], stake_amount, direction, current_time, entry_tag, trade,
order_type
)
if not stake_amount:
# In case of pos adjust, still return the original trade
# If not pos adjust, trade is None
return trade
time_in_force = self.strategy.order_time_in_force['entry']
if not pos_adjust:
# Confirm trade entry: # Confirm trade entry:
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate, pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate,
time_in_force=time_in_force, current_time=current_time, time_in_force=time_in_force, current_time=current_time,
entry_tag=entry_tag, side=direction): entry_tag=entry_tag, side=direction):
return trade return trade
else:
leverage = trade.leverage if trade else 1.0
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
self.order_id_counter += 1 self.order_id_counter += 1

View File

@ -996,6 +996,36 @@ def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_order
assert not freqtrade.execute_entry(pair, stake_amount) assert not freqtrade.execute_entry(pair, stake_amount)
@pytest.mark.parametrize("is_short", [False, True])
def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order, is_short) -> None:
default_conf_usdt['trading_mode'] = 'futures'
default_conf_usdt['margin_mode'] = 'isolated'
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={
'bid': 1.9,
'ask': 2.2,
'last': 1.9
}),
create_order=MagicMock(return_value=limit_order[enter_side(is_short)]),
get_rate=MagicMock(return_value=0.11),
# Minimum stake-amount is ~5$
get_maintenance_ratio_and_amt=MagicMock(return_value=(0.0, 0.0)),
_fetch_and_calculate_funding_fees=MagicMock(return_value=0),
get_fee=fee,
get_max_leverage=MagicMock(return_value=5.0),
)
stake_amount = 2
pair = 'SOL/BUSD:BUSD'
freqtrade.strategy.leverage = MagicMock(return_value=5.0)
assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
trade = Trade.query.first()
assert trade.leverage == 5.0
# assert trade.stake_amount == 2
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short) -> None: def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)