diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 9e0b33ab1..fb7bea5f3 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -593,6 +593,8 @@ Additional orders also result in additional fees and those orders don't count to This callback is **not** called when there is an open order (either buy or sell) waiting for execution, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`. `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. +Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position, no matter if it's a long or short trade. Modifications to leverage are not possible. + !!! Note "About stake size" Using fixed stake size means it will be the amount used for the first order, just like without position adjustment. If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that. @@ -663,7 +665,7 @@ class DigDeeperStrategy(IStrategy): return None filled_buys = trade.select_filled_orders('buy') - count_of_buys = trade.nr_of_successful_buys + count_of_entries = trade.nr_of_successful_entries # Allow up to 3 additional increasingly larger buys (4 in total) # Initial buy is 1x # If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2% @@ -676,7 +678,7 @@ class DigDeeperStrategy(IStrategy): # This returns first order stake size stake_amount = filled_buys[0].cost # This then calculates current safety order size - stake_amount = stake_amount * (1 + (count_of_buys * 0.25)) + stake_amount = stake_amount * (1 + (count_of_entries * 0.25)) return stake_amount except Exception as exception: return None diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4d0316bb8..0026667d3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -103,7 +103,6 @@ class FreqtradeBot(LoggingMixin): self._exit_lock = Lock() 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', TradingMode.SPOT) self.margin_mode_type: Optional[MarginMode] = None if 'margin_mode' in self.config: @@ -510,7 +509,7 @@ class FreqtradeBot(LoggingMixin): """ # TODO-lev: Check what changes are necessary for DCA in relation to shorts. if self.strategy.max_entry_position_adjustment > -1: - count_of_buys = trade.nr_of_successful_buys + count_of_buys = trade.nr_of_successful_entries if count_of_buys > self.strategy.max_entry_position_adjustment: logger.debug(f"Max adjustment entries for {trade.pair} has been reached.") return @@ -533,7 +532,7 @@ class FreqtradeBot(LoggingMixin): if stake_amount is not None and stake_amount > 0.0: # We should increase our position - self.execute_entry(trade.pair, stake_amount, trade=trade) + self.execute_entry(trade.pair, stake_amount, trade=trade, is_short=trade.is_short) if stake_amount is not None and stake_amount < 0.0: # We should decrease our position @@ -643,18 +642,21 @@ class FreqtradeBot(LoggingMixin): if not stake_amount: return False - - 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) + 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}") @@ -724,6 +726,7 @@ class FreqtradeBot(LoggingMixin): amount = safe_value_fallback(order, 'filled', 'amount') enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') + # TODO: this might be unnecessary, as we're calling it in update_trade_state. interest_rate, isolated_liq = self.leverage_prep( leverage=leverage, pair=pair, @@ -1596,9 +1599,19 @@ class FreqtradeBot(LoggingMixin): Trade.commit() if order['status'] in constants.NON_OPEN_EXCHANGE_STATES: - # If a buy order was closed, force update on stoploss on exchange - if order.get('side', None) == 'buy': + # If a entry order was closed, force update on stoploss on exchange + if order.get('side', None) == trade.enter_side: trade = self.cancel_stoploss_on_exchange(trade) + # TODO: Margin will need to use interest_rate as well. + _, isolated_liq = self.leverage_prep( + leverage=trade.leverage, + pair=trade.pair, + amount=trade.amount, + open_rate=trade.open_rate, + is_short=trade.is_short + ) + if isolated_liq: + trade.set_isolated_liq(isolated_liq) # Updating wallets when order is closed self.wallets.update() @@ -1607,7 +1620,7 @@ class FreqtradeBot(LoggingMixin): self._notify_exit(trade, '', True) self.handle_protections(trade.pair) elif send_msg and not trade.open_order_id: - # Buy fill + # Enter fill self._notify_enter(trade, order, fill=True) return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0e3a70a93..4e881d271 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -464,11 +464,11 @@ class Backtesting: # Check if we need to adjust our current positions if self.strategy.position_adjustment_enable: - check_adjust_buy = True + check_adjust_entry = True if self.strategy.max_entry_position_adjustment > -1: - count_of_buys = trade.nr_of_successful_buys - check_adjust_buy = (count_of_buys <= self.strategy.max_entry_position_adjustment) - if check_adjust_buy: + entry_count = trade.nr_of_successful_entries + check_adjust_entry = (entry_count <= self.strategy.max_entry_position_adjustment) + if check_adjust_entry: trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime() @@ -639,17 +639,20 @@ class Backtesting: # If not pos adjust, trade is None return trade - 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=current_time, - current_rate=row[OPEN_IDX], - proposed_leverage=1.0, - max_leverage=max_leverage, - side=direction, - ) if self._can_short else 1.0 - # Cap leverage between 1.0 and max_leverage. - leverage = min(max(leverage, 1.0), max_leverage) + 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=current_time, + current_rate=row[OPEN_IDX], + proposed_leverage=1.0, + max_leverage=max_leverage, + side=direction, + ) if self._can_short else 1.0 + # Cap leverage between 1.0 and max_leverage. + leverage = min(max(leverage, 1.0), max_leverage) + else: + leverage = trade.leverage if trade else 1.0 order_type = self.strategy.order_types['buy'] time_in_force = self.strategy.order_time_in_force['buy'] @@ -729,7 +732,7 @@ class Backtesting: for pair in open_trades.keys(): if len(open_trades[pair]) > 0: for trade in open_trades[pair]: - if trade.open_order_id and trade.nr_of_successful_buys == 0: + if trade.open_order_id and trade.nr_of_successful_entries == 0: # Ignore trade if buy-order did not fill yet continue sell_row = data[pair][-1] @@ -782,7 +785,7 @@ class Backtesting: if timedout: if order.side == 'buy': self.timedout_entry_orders += 1 - if trade.nr_of_successful_buys == 0: + if trade.nr_of_successful_entries == 0: # Remove trade due to buy timeout expiration. return True else: diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index ae4265374..8d85c775b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -867,7 +867,7 @@ class LocalTrade(): def recalc_trade_from_orders(self): # We need at least 2 entry orders for averaging amounts and rates. - if len(self.select_filled_orders('buy')) < 2: + if len(self.select_filled_orders(self.enter_side)) < 2: # Just in case, still recalc open trade value self.recalc_open_trade_value() return @@ -889,8 +889,9 @@ class LocalTrade(): total_stake += tmp_price * tmp_amount if total_amount > 0: + # Leverage not updated, as we don't allow changing leverage through DCA at the moment. self.open_rate = total_stake / total_amount - self.stake_amount = total_stake + self.stake_amount = total_stake / (self.leverage or 1.0) self.amount = total_amount self.fee_open_cost = self.fee_open * self.stake_amount self.recalc_open_trade_value() @@ -936,10 +937,28 @@ class LocalTrade(): (o.filled or 0) > 0 and o.status in NON_OPEN_EXCHANGE_STATES] + @property + def nr_of_successful_entries(self) -> int: + """ + Helper function to count the number of entry orders that have been filled. + :return: int count of entry orders that have been filled for this trade. + """ + + return len(self.select_filled_orders(self.enter_side)) + + @property + def nr_of_successful_exits(self) -> int: + """ + Helper function to count the number of exit orders that have been filled. + :return: int count of exit orders that have been filled for this trade. + """ + return len(self.select_filled_orders(self.exit_side)) + @property def nr_of_successful_buys(self) -> int: """ Helper function to count the number of buy orders that have been filled. + WARNING: Please use nr_of_successful_entries for short support. :return: int count of buy orders that have been filled for this trade. """ @@ -949,6 +968,7 @@ class LocalTrade(): def nr_of_successful_sells(self) -> int: """ Helper function to count the number of sell orders that have been filled. + WARNING: Please use nr_of_successful_exits for short support. :return: int count of sell orders that have been filled for this trade. """ return len(self.select_filled_orders('sell')) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9b780d88d..98207bfea 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -261,11 +261,11 @@ class RPC: profit_str ] if self._config.get('position_adjustment_enable', False): - max_buy_str = '' + max_entry_str = '' if self._config.get('max_entry_position_adjustment', -1) > 0: - max_buy_str = f"/{self._config['max_entry_position_adjustment'] + 1}" - filled_buys = trade.nr_of_successful_buys - detail_trade.append(f"{filled_buys}{max_buy_str}") + max_entry_str = f"/{self._config['max_entry_position_adjustment'] + 1}" + filled_entries = trade.nr_of_successful_entries + detail_trade.append(f"{filled_entries}{max_entry_str}") trades_list.append(detail_trade) profitcol = "Profit" if self._fiat_converter: @@ -696,19 +696,18 @@ class RPC: if trade.open_order_id: order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) - if order['side'] == 'buy': + if order['side'] == trade.enter_side: fully_canceled = self._freqtrade.handle_cancel_enter( trade, order, CANCEL_REASON['FORCE_SELL']) - if order['side'] == 'sell': + if order['side'] == trade.exit_side: # Cancel order - so it is placed anew with a fresh price. self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL']) if not fully_canceled: # Get current rate and execute sell - closing_side = "buy" if trade.is_short else "sell" current_rate = self._freqtrade.exchange.get_rate( - trade.pair, refresh=False, side=closing_side) + trade.pair, refresh=False, side=trade.exit_side) sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) order_type = ordertype or self._freqtrade.strategy.order_types.get( "forcesell", self._freqtrade.strategy.order_types["sell"]) @@ -769,8 +768,10 @@ class RPC: # check if valid pair # check if pair already has an open pair - trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() + trade: Trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() + is_short = (order_side == SignalDirection.SHORT) if trade: + is_short = trade.is_short if not self._freqtrade.strategy.position_adjustment_enable: raise RPCException(f'position for {pair} already open - id: {trade.id}') @@ -784,7 +785,7 @@ class RPC: 'forcebuy', self._freqtrade.strategy.order_types['buy']) if self._freqtrade.execute_entry(pair, stake_amount, price, ordertype=order_type, trade=trade, - is_short=(order_side == SignalDirection.SHORT), + is_short=is_short, enter_tag=enter_tag, ): Trade.commit() diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index a056b316c..0b73c1271 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -183,7 +183,7 @@ class StrategyTestV3(IStrategy): current_profit: float, min_stake: float, max_stake: float, **kwargs): if current_profit < -0.0075: - orders = trade.select_filled_orders('buy') + orders = trade.select_filled_orders(trade.enter_side) return round(orders[0].cost, 0) return None diff --git a/tests/test_integration.py b/tests/test_integration.py index bb14aa03d..70ee1c52c 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -231,13 +231,13 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert len(Trade.get_trades().all()) == 1 trade = Trade.get_trades().first() assert len(trade.orders) == 1 - assert trade.stake_amount == 60 + assert pytest.approx(trade.stake_amount) == 60 assert trade.open_rate == 2.0 # No adjustment freqtrade.process() trade = Trade.get_trades().first() assert len(trade.orders) == 1 - assert trade.stake_amount == 60 + assert pytest.approx(trade.stake_amount) == 60 # Reduce bid amount ticker_usdt_modif = ticker_usdt.return_value @@ -266,6 +266,7 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.amount == trade.orders[0].amount + trade.orders[1].amount assert trade.nr_of_successful_buys == 2 + assert trade.nr_of_successful_entries == 2 # Sell patch_get_signal(freqtrade, enter_long=False, exit_long=True) @@ -280,3 +281,75 @@ def test_dca_buying(default_conf_usdt, ticker_usdt, fee, mocker) -> None: assert trade.orders[2].amount == trade.amount assert trade.nr_of_successful_buys == 2 + assert trade.nr_of_successful_entries == 2 + + +def test_dca_short(default_conf_usdt, ticker_usdt, fee, mocker) -> None: + default_conf_usdt['position_adjustment_enable'] = True + + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker_usdt, + get_fee=fee, + amount_to_precision=lambda s, x, y: y, + price_to_precision=lambda s, x, y: y, + ) + + patch_get_signal(freqtrade, enter_long=False, enter_short=True) + freqtrade.enter_positions() + + assert len(Trade.get_trades().all()) == 1 + trade = Trade.get_trades().first() + assert len(trade.orders) == 1 + assert pytest.approx(trade.stake_amount) == 60 + assert trade.open_rate == 2.02 + # No adjustment + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 1 + assert pytest.approx(trade.stake_amount) == 60 + + # Reduce bid amount + ticker_usdt_modif = ticker_usdt.return_value + ticker_usdt_modif['ask'] = ticker_usdt_modif['ask'] * 1.015 + ticker_usdt_modif['bid'] = ticker_usdt_modif['bid'] * 1.0125 + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value=ticker_usdt_modif) + + # additional buy order + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 2 + for o in trade.orders: + assert o.status == "closed" + assert pytest.approx(trade.stake_amount) == 120 + + # Open-rate averaged between 2.0 and 2.0 * 1.015 + assert trade.open_rate >= 2.02 + assert trade.open_rate < 2.02 * 1.015 + + # No action - profit raised above 1% (the bar set in the strategy). + freqtrade.process() + trade = Trade.get_trades().first() + assert len(trade.orders) == 2 + assert pytest.approx(trade.stake_amount) == 120 + # assert trade.orders[0].amount == 30 + assert trade.orders[1].amount == 60 / ticker_usdt_modif['ask'] + + assert trade.amount == trade.orders[0].amount + trade.orders[1].amount + assert trade.nr_of_successful_entries == 2 + + # Buy + patch_get_signal(freqtrade, enter_long=False, exit_short=True) + freqtrade.process() + trade = Trade.get_trades().first() + assert trade.is_open is False + # assert trade.orders[0].amount == 30 + assert trade.orders[0].side == 'sell' + assert trade.orders[1].amount == 60 / ticker_usdt_modif['ask'] + # Sold everything + assert trade.orders[-1].side == 'buy' + assert trade.orders[2].amount == trade.amount + + assert trade.nr_of_successful_entries == 2 + assert trade.nr_of_successful_exits == 1 diff --git a/tests/test_persistence.py b/tests/test_persistence.py index efba25550..c287bf4fd 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -2371,13 +2371,16 @@ def test_recalc_trade_from_orders(fee): assert pytest.approx(trade.open_trade_value) == o1_trade_val + o2_trade_val + o3_trade_val -def test_recalc_trade_from_orders_ignores_bad_orders(fee): +@pytest.mark.parametrize('is_short', [True, False]) +def test_recalc_trade_from_orders_ignores_bad_orders(fee, is_short): o1_amount = 100 o1_rate = 1 o1_cost = o1_amount * o1_rate o1_fee_cost = o1_cost * fee.return_value - o1_trade_val = o1_cost + o1_fee_cost + o1_trade_val = o1_cost - o1_fee_cost if is_short else o1_cost + o1_fee_cost + enter_side = "sell" if is_short else "buy" + exit_side = "buy" if is_short else "sell" trade = Trade( pair='ADA/USDT', @@ -2389,17 +2392,18 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): exchange='binance', open_rate=o1_rate, max_rate=o1_rate, + is_short=is_short, ) - trade.update_fee(o1_fee_cost, 'BNB', fee.return_value, 'buy') + trade.update_fee(o1_fee_cost, 'BNB', fee.return_value, enter_side) # Check with 1 order order1 = Order( - ft_order_side='buy', + ft_order_side=enter_side, ft_pair=trade.pair, ft_is_open=False, status="closed", symbol=trade.pair, order_type="market", - side="buy", + side=enter_side, price=o1_rate, average=o1_rate, filled=o1_amount, @@ -2417,16 +2421,16 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == o1_fee_cost assert trade.open_trade_value == o1_trade_val - assert trade.nr_of_successful_buys == 1 + assert trade.nr_of_successful_entries == 1 order2 = Order( - ft_order_side='buy', + ft_order_side=enter_side, ft_pair=trade.pair, ft_is_open=True, status="open", symbol=trade.pair, order_type="market", - side="buy", + side=enter_side, price=o1_rate, average=o1_rate, filled=o1_amount, @@ -2444,17 +2448,17 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == o1_fee_cost assert trade.open_trade_value == o1_trade_val - assert trade.nr_of_successful_buys == 1 + assert trade.nr_of_successful_entries == 1 # Let's try with some other orders order3 = Order( - ft_order_side='buy', + ft_order_side=enter_side, ft_pair=trade.pair, ft_is_open=False, status="cancelled", symbol=trade.pair, order_type="market", - side="buy", + side=enter_side, price=1, average=2, filled=0, @@ -2472,16 +2476,16 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == o1_fee_cost assert trade.open_trade_value == o1_trade_val - assert trade.nr_of_successful_buys == 1 + assert trade.nr_of_successful_entries == 1 order4 = Order( - ft_order_side='buy', + ft_order_side=enter_side, ft_pair=trade.pair, ft_is_open=False, status="closed", symbol=trade.pair, order_type="market", - side="buy", + side=enter_side, price=o1_rate, average=o1_rate, filled=o1_amount, @@ -2499,17 +2503,17 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == 2 * o1_fee_cost assert trade.open_trade_value == 2 * o1_trade_val - assert trade.nr_of_successful_buys == 2 + assert trade.nr_of_successful_entries == 2 - # Just to make sure sell orders are ignored, let's calculate one more time. + # Just to make sure exit orders are ignored, let's calculate one more time. sell1 = Order( - ft_order_side='sell', + ft_order_side=exit_side, ft_pair=trade.pair, ft_is_open=False, status="closed", symbol=trade.pair, order_type="market", - side="sell", + side=exit_side, price=4, average=3, filled=2, @@ -2526,16 +2530,17 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == 2 * o1_fee_cost assert trade.open_trade_value == 2 * o1_trade_val - assert trade.nr_of_successful_buys == 2 + assert trade.nr_of_successful_entries == 2 + # Check with 1 order order_noavg = Order( - ft_order_side='buy', + ft_order_side=enter_side, ft_pair=trade.pair, ft_is_open=False, status="closed", symbol=trade.pair, order_type="market", - side="buy", + side=enter_side, price=o1_rate, average=None, filled=o1_amount, @@ -2553,7 +2558,7 @@ def test_recalc_trade_from_orders_ignores_bad_orders(fee): assert trade.open_rate == o1_rate assert trade.fee_open_cost == 3 * o1_fee_cost assert trade.open_trade_value == 3 * o1_trade_val - assert trade.nr_of_successful_buys == 3 + assert trade.nr_of_successful_entries == 3 @pytest.mark.usefixtures("init_persistence")