diff --git a/docs/configuration.md b/docs/configuration.md index 340ae2e72..172ad468d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -173,6 +173,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `dataformat_ohlcv` | Data format to use to store historical candle (OHLCV) data.
*Defaults to `json`*.
**Datatype:** String | `dataformat_trades` | Data format to use to store historical trades data.
*Defaults to `jsongz`*.
**Datatype:** String | `position_adjustment_enable` | Enables the strategy to use position adjustments (additional buys or sells). [More information here](strategy-callbacks.md#adjust-trade-position).
[Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean +| `max_entry_position_adjustment` | Maximum additional buy(s) for each open trade on top of the first entry Order. Set it to `-1` for unlimited additional additional orders. [More information here](strategy-callbacks.md#adjust-trade-position).
[Strategy Override](#parameters-in-the-strategy).
*Defaults to `-1`.*
**Datatype:** Positive Integer or -1 ### Parameters in the strategy @@ -198,6 +199,7 @@ Values set in the configuration file always overwrite values set in the strategy * `ignore_roi_if_buy_signal` * `ignore_buying_expired_candle_after` * `position_adjustment_enable` +* `max_entry_position_adjustment` ### Configuring amount per trade diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index e83fee46f..3a30a2a28 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -579,11 +579,13 @@ The `position_adjustment_enable` strategy property enables the usage of `adjust_ For performance reasons, it's disabled by default and freqtrade will show a warning message on startup if enabled. `adjust_trade_position()` can be used to perform additional orders, for example to manage risk with DCA (Dollar Cost Averaging). +`max_entry_position_adjustment` property is used to limit the number of additional buys per trade (on top of the first buy) that the bot can execute. By default, the value is -1 which means the bot have no limit on number of adjustment buys. + The strategy is expected to return a stake_amount (in stake currency) between `min_stake` and `max_stake` if and when an additional buy order should be made (position is increased). If there are not enough funds in the wallet (the return value is above `max_stake`) then the signal will be ignored. Additional orders also result in additional fees and those orders don't count towards `max_open_trades`. -This callback is **not** called when there is an open order (either buy or sell) waiting for execution. +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. !!! Note "About stake size" @@ -614,7 +616,7 @@ class DigDeeperStrategy(IStrategy): # ... populate_* methods # Example specific variables - max_dca_orders = 3 + max_entry_position_adjustment = 3 # This number is explained a bit further down max_dca_multiplier = 5.5 @@ -656,8 +658,7 @@ class DigDeeperStrategy(IStrategy): return None filled_buys = trade.select_filled_orders('buy') - count_of_buys = len(filled_buys) - + count_of_buys = trade.nr_of_successful_buys # 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% @@ -666,15 +667,14 @@ class DigDeeperStrategy(IStrategy): # Total stake for this trade would be 1 + 1.25 + 1.5 + 1.75 = 5.5x of the initial allowed stake. # That is why max_dca_multiplier is 5.5 # Hope you have a deep wallet! - if 0 < count_of_buys <= self.max_dca_orders: - try: - # 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)) - return stake_amount - except Exception as exception: - return None + try: + # 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)) + return stake_amount + except Exception as exception: + return None return None diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 504c7dce9..f02e39792 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -371,7 +371,9 @@ CONF_SCHEMA = { 'type': 'string', 'enum': AVAILABLE_DATAHANDLERS, 'default': 'jsongz' - } + }, + 'position_adjustment_enable': {'type': 'boolean', 'default': False}, + 'max_entry_position_adjustment': {'type': ['integer', 'number'], 'minimum': -1}, }, 'definitions': { 'exchange': { diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 972b6f6b7..c3c03ed67 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -462,8 +462,8 @@ class FreqtradeBot(LoggingMixin): try: self.check_and_call_adjust_trade_position(trade) except DependencyException as exception: - logger.warning('Unable to adjust position of trade for %s: %s', - trade.pair, exception) + logger.warning( + f"Unable to adjust position of trade for {trade.pair}: {exception}") def check_and_call_adjust_trade_position(self, trade: Trade): """ @@ -471,6 +471,13 @@ class FreqtradeBot(LoggingMixin): If the strategy triggers the adjustment, a new order gets issued. Once that completes, the existing trade is modified to match new data. """ + if self.strategy.max_entry_position_adjustment > -1: + count_of_buys = trade.nr_of_successful_buys + if count_of_buys > self.strategy.max_entry_position_adjustment: + logger.debug(f"Max adjustment entries for {trade.pair} has been reached.") + return + else: + logger.debug("Max adjustment entries is set to unlimited.") current_rate = self.exchange.get_rate(trade.pair, refresh=True, side="buy") current_profit = trade.calc_profit_ratio(current_rate) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8e52a62fa..e173c3367 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -381,7 +381,12 @@ class Backtesting: # Check if we need to adjust our current positions if self.strategy.position_adjustment_enable: - trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) + check_adjust_buy = 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: + trade = self._get_adjust_trade_entry_for_candle(trade, sell_row) sell_candle_time = sell_row[DATE_IDX].to_pydatetime() sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 95177c000..e9fcc3496 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -97,7 +97,8 @@ class StrategyResolver(IResolver): ("sell_profit_offset", 0.0), ("disable_dataframe_checks", False), ("ignore_buying_expired_candle_after", 0), - ("position_adjustment_enable", False) + ("position_adjustment_enable", False), + ("max_entry_position_adjustment", -1), ] for attribute, default in attributes: StrategyResolver._override_attribute_helper(strategy, config, diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index bbd858795..96421ed32 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -173,6 +173,8 @@ class ShowConfig(BaseModel): bot_name: str state: str runmode: str + position_adjustment_enable: bool + max_entry_position_adjustment: int class TradeSchema(BaseModel): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c78ff1079..2374dbd39 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -136,7 +136,12 @@ class RPC: 'ask_strategy': config.get('ask_strategy', {}), 'bid_strategy': config.get('bid_strategy', {}), 'state': str(botstate), - 'runmode': config['runmode'].value + 'runmode': config['runmode'].value, + 'position_adjustment_enable': config.get('position_adjustment_enable', False), + 'max_entry_position_adjustment': ( + config['max_entry_position_adjustment'] + if config['max_entry_position_adjustment'] != float('inf') + else -1) } return val @@ -247,8 +252,9 @@ class RPC: profit_str ] if self._config.get('position_adjustment_enable', False): - filled_buys = trade.select_filled_orders('buy') - detail_trade.append(str(len(filled_buys))) + max_buy = self._config['max_entry_position_adjustment'] + 1 + filled_buys = trade.nr_of_successful_buys + detail_trade.append(f"{filled_buys}/{max_buy}") trades_list.append(detail_trade) profitcol = "Profit" if self._fiat_converter: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 716694a81..0f0bd7432 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1347,6 +1347,14 @@ class Telegram(RPCHandler): else: sl_info = f"*Stoploss:* `{val['stoploss']}`\n" + if val['position_adjustment_enable']: + pa_info = ( + f"*Position adjustment:* On\n" + f"*Max enter position adjustment:* `{val['max_entry_position_adjustment']}`\n" + ) + else: + pa_info = "*Position adjustment:* Off\n" + self._send_msg( f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Exchange:* `{val['exchange']}`\n" @@ -1356,6 +1364,7 @@ class Telegram(RPCHandler): f"*Ask strategy:* ```\n{json.dumps(val['ask_strategy'])}```\n" f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n" f"{sl_info}" + f"{pa_info}" f"*Timeframe:* `{val['timeframe']}`\n" f"*Strategy:* `{val['strategy']}`\n" f"*Current state:* `{val['state']}`" diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 6f139026e..78dae6c5d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -108,6 +108,7 @@ class IStrategy(ABC, HyperStrategyMixin): # Position adjustment is disabled by default position_adjustment_enable: bool = False + max_entry_position_adjustment: int = -1 # Number of seconds after which the candle will no longer result in a buy on expired candles ignore_buying_expired_candle_after: int = 0 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d226280fe..523696759 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4550,3 +4550,32 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: # Make sure the closed order is found as the second order. order = trade.select_order('buy', False) assert order.order_id == '652' + + +def test_process_open_trade_positions_exception(mocker, default_conf_usdt, fee, caplog) -> None: + default_conf_usdt.update({ + "position_adjustment_enable": True, + }) + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.check_and_call_adjust_trade_position', + side_effect=DependencyException()) + + create_mock_trades(fee) + + freqtrade.process_open_trade_positions() + assert log_has_re(r"Unable to adjust position of trade for .*", caplog) + + +def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, caplog) -> None: + default_conf_usdt.update({ + "position_adjustment_enable": True, + "max_entry_position_adjustment": 0, + }) + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + + create_mock_trades(fee) + caplog.set_level(logging.DEBUG) + + freqtrade.process_open_trade_positions() + assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog)