diff --git a/packages/trading/octobot_trading/constants.py b/packages/trading/octobot_trading/constants.py index 3e335d2312..50830c00e4 100644 --- a/packages/trading/octobot_trading/constants.py +++ b/packages/trading/octobot_trading/constants.py @@ -50,6 +50,9 @@ INCLUDE_DUSTS_IN_SELL_ORDERS_WHEN_POSSIBLE = os_util.parse_boolean_environment_var( "INCLUDE_DUSTS_IN_SELL_ORDERS_WHEN_POSSIBLE", "true" ) +ENABLE_CHAINED_ORDER_UPDATE_WITH_TRIGGERING_ORDER_FEES = os_util.parse_boolean_environment_var( + "ENABLE_CHAINED_ORDER_UPDATE_WITH_TRIGGERING_ORDER_FEES", "True" +) # Portfolio MAX_PORTFOLIO_SYNC_ATTEMPTS = 1 diff --git a/packages/trading/octobot_trading/personal_data/orders/order_util.py b/packages/trading/octobot_trading/personal_data/orders/order_util.py index 23dc755e7e..5423495a03 100644 --- a/packages/trading/octobot_trading/personal_data/orders/order_util.py +++ b/packages/trading/octobot_trading/personal_data/orders/order_util.py @@ -831,6 +831,12 @@ async def create_and_register_chained_order_on_base_order( # do not reduce chained order amounts to account for fees when trading futures if update_with_triggering_order_fees is None: update_with_triggering_order_fees = not exchange_manager.is_future + if update_with_triggering_order_fees and not constants.ENABLE_CHAINED_ORDER_UPDATE_WITH_TRIGGERING_ORDER_FEES: + logging.get_logger(LOGGER_NAME).info( + "Chained order update with triggering order fees is disabled, " + "update_with_triggering_order_fees will be set to False." + ) + update_with_triggering_order_fees = False if allow_bundling: params = await exchange_manager.trader.bundle_chained_order_with_uncreated_order( base_order, chained_order, update_with_triggering_order_fees diff --git a/packages/trading/tests/personal_data/orders/test_order_util.py b/packages/trading/tests/personal_data/orders/test_order_util.py index 78c5dc712f..3b1c5d7a27 100644 --- a/packages/trading/tests/personal_data/orders/test_order_util.py +++ b/packages/trading/tests/personal_data/orders/test_order_util.py @@ -1035,3 +1035,104 @@ def test_ensure_orders_limit(): order_util.ensure_orders_limit(exchange_manager, symbol, [enums.TraderOrderType.STOP_LOSS]*3) with pytest.raises(octobot_trading.errors.MaxOpenOrderReachedForSymbolError): order_util.ensure_orders_limit(exchange_manager, symbol, [enums.TraderOrderType.STOP_LOSS_LIMIT]*10) + + +class TestCreateAndRegisterChainedOrderOnBaseOrder: + pytestmark = pytest.mark.asyncio + + _PRICE = decimal.Decimal("100") + _ORDER_TYPE = enums.TraderOrderType.SELL_LIMIT + _SIDE = enums.TradeOrderSide.SELL + _DISABLED_FEES_LOG_MESSAGE = ( + "Chained order update with triggering order fees is disabled, " + "update_with_triggering_order_fees will be set to False." + ) + + @staticmethod + def _mock_base_order_for_chained_order_registration(): + base_order = mock.Mock() + exchange_manager = mock.Mock() + exchange_manager.is_future = False + trader = mock.Mock() + trader.bundle_chained_order_with_uncreated_order = mock.AsyncMock(return_value={}) + trader.chain_order = mock.AsyncMock() + exchange_manager.trader = trader + base_order.exchange_manager = exchange_manager + base_order.symbol = "BTC/USDT" + base_order.origin_quantity = decimal.Decimal("1") + base_order.order_id = "base_order_id" + return base_order, trader + + async def _run_create_and_register_chained_order_on_base_order( + self, + *, + constant_enabled: bool, + update_with_triggering_order_fees: bool, + allow_bundling: bool = True, + ): + base_order, trader = self._mock_base_order_for_chained_order_registration() + chained_order = mock.Mock() + logger_mock = mock.Mock() + + with mock.patch.object(order_factory, "create_order_instance", mock.Mock(return_value=chained_order)), \ + mock.patch.object( + order_util.constants, + "ENABLE_CHAINED_ORDER_UPDATE_WITH_TRIGGERING_ORDER_FEES", + constant_enabled, + ), \ + mock.patch.object(order_util.logging, "get_logger", mock.Mock(return_value=logger_mock)): + params, returned_chained_order = await order_util.create_and_register_chained_order_on_base_order( + base_order, + self._PRICE, + self._ORDER_TYPE, + self._SIDE, + allow_bundling=allow_bundling, + update_with_triggering_order_fees=update_with_triggering_order_fees, + ) + + return base_order, trader, chained_order, logger_mock, params, returned_chained_order + + @pytest.mark.parametrize( + "constant_enabled, update_with_triggering_order_fees, allow_bundling, " + "expected_passed_update_with_triggering_order_fees, expect_disabled_fees_log", + [ + pytest.param(False, True, True, False, True, id="disables_fees_when_constant_disabled"), + pytest.param(True, True, True, True, False, id="keeps_fees_when_constant_enabled"), + pytest.param(False, False, True, False, False, id="no_log_when_fees_already_false"), + pytest.param(False, True, False, False, True, id="disables_fees_on_chain_order_when_constant_disabled"), + ], + ) + async def test_update_with_triggering_order_fees_constant_gate( + self, + constant_enabled, + update_with_triggering_order_fees, + allow_bundling, + expected_passed_update_with_triggering_order_fees, + expect_disabled_fees_log, + ): + base_order, trader, chained_order, logger_mock, params, returned_chained_order = ( + await self._run_create_and_register_chained_order_on_base_order( + constant_enabled=constant_enabled, + update_with_triggering_order_fees=update_with_triggering_order_fees, + allow_bundling=allow_bundling, + ) + ) + + assert params == {} + assert returned_chained_order is chained_order + + if allow_bundling: + trader.bundle_chained_order_with_uncreated_order.assert_awaited_once_with( + base_order, chained_order, expected_passed_update_with_triggering_order_fees + ) + trader.chain_order.assert_not_called() + else: + trader.chain_order.assert_awaited_once_with( + base_order, chained_order, expected_passed_update_with_triggering_order_fees, False + ) + trader.bundle_chained_order_with_uncreated_order.assert_not_called() + + if expect_disabled_fees_log: + logger_mock.info.assert_called_once_with(self._DISABLED_FEES_LOG_MESSAGE) + else: + logger_mock.info.assert_not_called()