diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 9f4ef8277..170ba8a07 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -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). * 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 stake size by calling the `custom_stake_amount()` callback. * 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. 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. * 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). - * 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. + * 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. * 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). diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c0af6e0e7..2d6b46745 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -600,26 +600,12 @@ class FreqtradeBot(LoggingMixin): trade_side = 'short' if is_short else 'long' 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) if not stake_amount: 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: logger.info(f"Position adjust: about to create a new order for {pair} with stake: " f"{stake_amount} for {trade}") @@ -775,7 +761,7 @@ class FreqtradeBot(LoggingMixin): side: str, trade_side: str, entry_tag: Optional[str], trade: Optional[Trade] - ) -> Tuple[float, float]: + ) -> Tuple[float, float, float]: if price: enter_limit_requested = price @@ -792,13 +778,30 @@ class FreqtradeBot(LoggingMixin): if not enter_limit_requested: 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" # stake- amount might be higher than necessary. # We do however also need min-stake to determine leverage, therefore this is ignored as # edge-case for now. min_stake_amount = self.exchange.get_min_pair_stake_amount( - pair, enter_limit_requested, self.strategy.stoploss) - max_stake_amount = self.exchange.get_max_pair_stake_amount(pair, enter_limit_requested) + pair, enter_limit_requested, self.strategy.stoploss, leverage) + max_stake_amount = self.exchange.get_max_pair_stake_amount( + pair, enter_limit_requested, leverage) if not self.edge and trade is None: stake_available = self.wallets.get_available_stake_amount() @@ -817,7 +820,7 @@ class FreqtradeBot(LoggingMixin): 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, fill: bool = False) -> None: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6c5d098dc..95acb0190 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -635,18 +635,15 @@ class Backtesting: else: return self._get_sell_trade_entry_for_candle(trade, sell_row) - def _enter_trade(self, pair: str, row: Tuple, direction: str, - stake_amount: Optional[float] = None, - trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]: + def get_valid_price_and_stake( + self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float], + 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': 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, 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) @@ -656,39 +653,14 @@ class Backtesting: else: 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 + leverage = trade.leverage if trade else 1.0 if not pos_adjust: try: stake_amount = self.wallets.get_trade_stake_amount(pair, None, update=False) 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) leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)( pair=pair, @@ -701,14 +673,57 @@ class Backtesting: # Cap leverage between 1.0 and 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: 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, time_in_force=time_in_force, current_time=current_time, entry_tag=entry_tag, side=direction): 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): self.order_id_counter += 1 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 637cae83f..6e8b1afbf 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -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) +@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]) def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short) -> None: patch_RPCManager(mocker)