From a83e2989b3db7e2a52155aa993b8b6d5ee19fc7d Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Sat, 18 Nov 2023 12:08:08 +0100 Subject: [PATCH 1/3] [Staggered] use dynamic order fees when available --- .../staggered_orders_trading.py | 19 ++++-- .../test_staggered_orders_trading_mode.py | 59 +++++++++++++++---- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py index 323eae914..be6ab030d 100644 --- a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py +++ b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py @@ -704,7 +704,8 @@ async def order_filled_callback(self, filled_order): filled_price = decimal.Decimal(str(filled_order[trading_enums.ExchangeConstantsOrderColumns.PRICE.value])) filled_volume = decimal.Decimal(str(filled_order[trading_enums.ExchangeConstantsOrderColumns.FILLED.value])) price = filled_price + price_increment if now_selling else filled_price - price_increment - volume = self._compute_mirror_order_volume(now_selling, filled_price, price, filled_volume) + fee = filled_order[trading_enums.ExchangeConstantsOrderColumns.FEE.value] + volume = self._compute_mirror_order_volume(now_selling, filled_price, price, filled_volume, fee) new_order = OrderData(new_side, volume, price, self.symbol, False, associated_entry_id) self.logger.debug(f"Creating mirror order: {new_order} after filled order: {filled_order}") if self.mirror_order_delay == 0 or trading_api.get_is_backtesting(self.exchange_manager): @@ -717,7 +718,7 @@ async def order_filled_callback(self, filled_order): self._lock_portfolio_and_create_order_when_possible(new_order, filled_price) )) - def _compute_mirror_order_volume(self, now_selling, filled_price, target_price, filled_volume): + def _compute_mirror_order_volume(self, now_selling, filled_price, target_price, filled_volume, paid_fees: dict): # use target volumes if set if self.sell_volume_per_order != trading_constants.ZERO and now_selling: return self.sell_volume_per_order @@ -732,9 +733,13 @@ def _compute_mirror_order_volume(self, now_selling, filled_price, target_price, if self.reinvest_profits: return new_order_quantity # remove exchange fees - quantity_change = self.max_fees - quantity = new_order_quantity * (1 - quantity_change) - return quantity + if paid_fees: + base, quote = symbol_util.parse_symbol(self.symbol).base_and_quote() + fees_in_base = trading_personal_data.get_fees_for_currency(paid_fees, base) + fees_in_base += trading_personal_data.get_fees_for_currency(paid_fees, quote) / filled_price + else: + fees_in_base = new_order_quantity * self.max_fees + return new_order_quantity - fees_in_base async def _lock_portfolio_and_create_order_when_possible(self, new_order, filled_price): await asyncio.wait_for(self.allowed_mirror_orders.wait(), timeout=None) @@ -1335,7 +1340,9 @@ def _get_quantity_from_recent_trades(self, price, max_quantity, recent_trades, c if trade is None: return None now_selling = trade.side == trading_enums.TradeOrderSide.BUY - return self._compute_mirror_order_volume(now_selling, trade.executed_price, price, trade.executed_quantity) + return self._compute_mirror_order_volume( + now_selling, trade.executed_price, price, trade.executed_quantity, trade.fee + ) def _get_associated_trade(self, price, trades, selling): increment_window = self.flat_increment / 4 diff --git a/Trading/Mode/staggered_orders_trading_mode/tests/test_staggered_orders_trading_mode.py b/Trading/Mode/staggered_orders_trading_mode/tests/test_staggered_orders_trading_mode.py index 8890dc480..1255cc15d 100644 --- a/Trading/Mode/staggered_orders_trading_mode/tests/test_staggered_orders_trading_mode.py +++ b/Trading/Mode/staggered_orders_trading_mode/tests/test_staggered_orders_trading_mode.py @@ -1152,31 +1152,70 @@ async def test_compute_mirror_order_volume(): # no fixed volumes producer.reinvest_profits = producer.use_fixed_volume_for_mirror_orders = False # 1% max fees - producer.max_fees = 0.01 + producer.max_fees = decimal.Decimal("0.01") # take exchange fees into account - assert producer._compute_mirror_order_volume(True, 100, 120, 2) == 2 * (1 - producer.max_fees) - assert producer._compute_mirror_order_volume(False, 100, 80, 2) == 2 * (100 / 80) * (1 - producer.max_fees) + assert producer._compute_mirror_order_volume( + True, decimal.Decimal("100"), decimal.Decimal("120"), decimal.Decimal("2"), None + ) == 2 * (1 - producer.max_fees) + assert producer._compute_mirror_order_volume( + False, decimal.Decimal("100"), decimal.Decimal("80"), decimal.Decimal("2"), {} + ) == 2 * (decimal.Decimal("100") / decimal.Decimal("80")) * (1 - producer.max_fees) + # with given fees + fees = { + trading_enums.FeePropertyColumns.COST.value: decimal.Decimal("0.032"), + trading_enums.FeePropertyColumns.CURRENCY.value: "BTC" + } + assert producer._compute_mirror_order_volume( + False, decimal.Decimal("100"), decimal.Decimal("80"), decimal.Decimal("2"), fees + ) == 2 * (decimal.Decimal("100") / decimal.Decimal("80")) - decimal.Decimal("0.032") + fees = { + trading_enums.FeePropertyColumns.COST.value: decimal.Decimal("2.3"), + trading_enums.FeePropertyColumns.CURRENCY.value: "USD" + } + assert producer._compute_mirror_order_volume( + False, decimal.Decimal("100"), decimal.Decimal("80"), decimal.Decimal("2"), fees + ) == 2 * (decimal.Decimal("100") / decimal.Decimal("80")) - (decimal.Decimal("2.3") / decimal.Decimal("100")) # with profits reinvesting producer.reinvest_profits = True # consider fees already taken, sell everything - assert producer._compute_mirror_order_volume(True, 100, 120, 2) == 2 - assert producer._compute_mirror_order_volume(False, 100, 80, 2) == 2 * (100 / 80) + assert producer._compute_mirror_order_volume( + True, decimal.Decimal("100"), decimal.Decimal("120"), decimal.Decimal("2"), None + ) == 2 + assert producer._compute_mirror_order_volume( + False, decimal.Decimal("100"), decimal.Decimal("80"), decimal.Decimal("2"), {} + ) == 2 * (decimal.Decimal("100") / decimal.Decimal("80")) + assert producer._compute_mirror_order_volume( + False, decimal.Decimal("100"), decimal.Decimal("80"), decimal.Decimal("2"), fees + ) == 2 * (decimal.Decimal("100") / decimal.Decimal("80")) # with fixed volumes producer.reinvest_profits = False producer.sell_volume_per_order = 3 # consider fees already taken, sell everything - assert producer._compute_mirror_order_volume(True, 100, 120, 2) == 3 + assert producer._compute_mirror_order_volume( + True, decimal.Decimal("100"), decimal.Decimal("120"), decimal.Decimal("2"), fees + ) == 3 # buy order - assert producer._compute_mirror_order_volume(False, 100, 80, 2) == 2 * (100 / 80) * (1 - producer.max_fees) + assert producer._compute_mirror_order_volume( + False, decimal.Decimal("100"), decimal.Decimal("80"), decimal.Decimal("2"), None + ) == 2 * (decimal.Decimal("100") / decimal.Decimal("80")) * (1 - producer.max_fees) producer.buy_volume_per_order = 5 - assert producer._compute_mirror_order_volume(False, 100, 80, 2) == 5 + assert producer._compute_mirror_order_volume( + False, decimal.Decimal("100"), decimal.Decimal("80"), decimal.Decimal("2"), {} + ) == 5 # with fixed volumes and profits reinvesting producer.reinvest_profits = True - assert producer._compute_mirror_order_volume(True, 100, 120, 2) == 3 - assert producer._compute_mirror_order_volume(False, 100, 80, 2) == 5 + assert producer._compute_mirror_order_volume( + True, decimal.Decimal("100"), decimal.Decimal("120"), decimal.Decimal("2"), None + ) == 3 + assert producer._compute_mirror_order_volume( + False, decimal.Decimal("100"), decimal.Decimal("80"), decimal.Decimal("2"), {} + ) == 5 + assert producer._compute_mirror_order_volume( + False, decimal.Decimal("100"), decimal.Decimal("80"), decimal.Decimal("2"), fees + ) == 5 async def test_create_order(): From b9d1852f91f95b4f11e37890f6d6d03b2e03bbe5 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Sat, 18 Nov 2023 12:12:30 +0100 Subject: [PATCH 2/3] [StaggeredOrders] add mirror order fees logs --- .../staggered_orders_trading.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py index be6ab030d..3a25a62c9 100644 --- a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py +++ b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py @@ -737,7 +737,14 @@ def _compute_mirror_order_volume(self, now_selling, filled_price, target_price, base, quote = symbol_util.parse_symbol(self.symbol).base_and_quote() fees_in_base = trading_personal_data.get_fees_for_currency(paid_fees, base) fees_in_base += trading_personal_data.get_fees_for_currency(paid_fees, quote) / filled_price + if fees_in_base == trading_constants.ZERO: + self.logger.debug( + f"Zero fees for trade on {self.symbol}" + ) else: + self.logger.debug( + f"No fees given to compute {self.symbol} mirror order size, using default ratio of {self.max_fees}" + ) fees_in_base = new_order_quantity * self.max_fees return new_order_quantity - fees_in_base From 59d6cb85f991852d473a68e8e0d126e84fe7b83d Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Sat, 18 Nov 2023 12:32:56 +0100 Subject: [PATCH 3/3] [Staggered][Grid] rename reinvest_profits into ignore_exchange_fees --- .../Mode/grid_trading_mode/grid_trading.py | 21 +++++++++++-------- .../config/StaggeredOrdersTradingMode.json | 6 +++--- .../staggered_orders_trading.py | 19 ++++++++++------- .../test_staggered_orders_trading_mode.py | 14 ++++++------- .../specific_config/GridTradingMode.json | 6 +++--- .../StaggeredOrdersTradingMode.json | 6 +++--- 6 files changed, 39 insertions(+), 33 deletions(-) diff --git a/Trading/Mode/grid_trading_mode/grid_trading.py b/Trading/Mode/grid_trading_mode/grid_trading.py index 1794c3b81..bf315e5f0 100644 --- a/Trading/Mode/grid_trading_mode/grid_trading.py +++ b/Trading/Mode/grid_trading_mode/grid_trading.py @@ -137,12 +137,12 @@ def init_user_inputs(self, inputs: dict) -> None: "total order cost (price * volume).", ) self.UI.user_input( - self.CONFIG_REINVEST_PROFITS, commons_enums.UserInputTypes.BOOLEAN, - default_config[self.CONFIG_REINVEST_PROFITS], inputs, + self.CONFIG_IGNORE_EXCHANGE_FEES, commons_enums.UserInputTypes.BOOLEAN, + default_config[self.CONFIG_IGNORE_EXCHANGE_FEES], inputs, parent_input_name=self.CONFIG_PAIR_SETTINGS, - title="Reinvest profits: when checked, profits will be included in mirror orders resulting in maximum " - "size mirror orders. When unchecked, a part of the total volume will be reduced to take " - "exchange fees into account. WARNING: incompatible with fixed volume on mirror orders.", + title="Ignore exchange fees: when checked, exchange fees won't be considered when creating mirror orders. " + "When unchecked, a part of the total volume will be reduced to take exchange " + "fees into account.", ) self.UI.user_input( self.CONFIG_MIRROR_ORDER_DELAY, commons_enums.UserInputTypes.FLOAT, @@ -157,7 +157,7 @@ def init_user_inputs(self, inputs: dict) -> None: default_config[self.CONFIG_USE_FIXED_VOLUMES_FOR_MIRROR_ORDERS], inputs, parent_input_name=self.CONFIG_PAIR_SETTINGS, title="Fixed volume on mirror orders: when checked, sell and buy orders volume settings will be used for " - "mirror orders. WARNING: incompatible with profits reinvesting.", + "mirror orders. WARNING: incompatible with 'Ignore exchange fees'.", ) self.UI.user_input( self.CONFIG_USE_EXISTING_ORDERS_ONLY, commons_enums.UserInputTypes.BOOLEAN, @@ -188,7 +188,7 @@ def get_default_pair_config(self, symbol, flat_spread, flat_increment) -> dict: self.CONFIG_STARTING_PRICE: 0, self.CONFIG_BUY_VOLUME_PER_ORDER: 0, self.CONFIG_SELL_VOLUME_PER_ORDER: 0, - self.CONFIG_REINVEST_PROFITS: False, + self.CONFIG_IGNORE_EXCHANGE_FEES: False, self.CONFIG_MIRROR_ORDER_DELAY: 0, self.CONFIG_USE_FIXED_VOLUMES_FOR_MIRROR_ORDERS: False, self.CONFIG_USE_EXISTING_ORDERS_ONLY: False, @@ -278,8 +278,11 @@ def read_config(self): self.buy_volume_per_order))) self.limit_orders_count_if_necessary = \ self.symbol_trading_config.get(self.trading_mode.LIMIT_ORDERS_IF_NECESSARY, True) - self.reinvest_profits = self.symbol_trading_config.get(self.trading_mode.CONFIG_REINVEST_PROFITS, - self.reinvest_profits) + # tmp: ensure "reinvest_profits" legacy param still works + self.ignore_exchange_fees = self.symbol_trading_config.get("reinvest_profits", self.ignore_exchange_fees) + # end tmp + self.ignore_exchange_fees = self.symbol_trading_config.get(self.trading_mode.CONFIG_IGNORE_EXCHANGE_FEES, + self.ignore_exchange_fees) self.use_fixed_volume_for_mirror_orders = self.symbol_trading_config.get( self.trading_mode.CONFIG_USE_FIXED_VOLUMES_FOR_MIRROR_ORDERS, self.use_fixed_volume_for_mirror_orders diff --git a/Trading/Mode/staggered_orders_trading_mode/config/StaggeredOrdersTradingMode.json b/Trading/Mode/staggered_orders_trading_mode/config/StaggeredOrdersTradingMode.json index 2351ad54d..fb1be9c4c 100644 --- a/Trading/Mode/staggered_orders_trading_mode/config/StaggeredOrdersTradingMode.json +++ b/Trading/Mode/staggered_orders_trading_mode/config/StaggeredOrdersTradingMode.json @@ -11,7 +11,7 @@ "allow_instant_fill": true, "operational_depth": 100, "mirror_order_delay": 0, - "reinvest_profits": false, + "ignore_exchange_fees": false, "use_existing_orders_only": false }, { @@ -24,7 +24,7 @@ "allow_instant_fill": true, "operational_depth": 50, "mirror_order_delay": 0, - "reinvest_profits": false, + "ignore_exchange_fees": false, "use_existing_orders_only": false }, { @@ -37,7 +37,7 @@ "allow_instant_fill": true, "operational_depth": 50, "mirror_order_delay": 0, - "reinvest_profits": false, + "ignore_exchange_fees": false, "use_existing_orders_only": false } ] diff --git a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py index 3a25a62c9..e27d7d56a 100644 --- a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py +++ b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py @@ -118,7 +118,7 @@ class StaggeredOrdersTradingMode(trading_modes.AbstractTradingMode): CONFIG_SELL_FUNDS = "sell_funds" CONFIG_SELL_VOLUME_PER_ORDER = "sell_volume_per_order" CONFIG_BUY_VOLUME_PER_ORDER = "buy_volume_per_order" - CONFIG_REINVEST_PROFITS = "reinvest_profits" + CONFIG_IGNORE_EXCHANGE_FEES = "ignore_exchange_fees" CONFIG_USE_FIXED_VOLUMES_FOR_MIRROR_ORDERS = "use_fixed_volume_for_mirror_orders" CONFIG_DEFAULT_SPREAD_PERCENT = 1.5 CONFIG_DEFAULT_INCREMENT_PERCENT = 0.5 @@ -189,10 +189,10 @@ def init_user_inputs(self, inputs: dict) -> None: "is filled. This can generate extra profits on quick market moves.", ) self.UI.user_input( - self.CONFIG_REINVEST_PROFITS, commons_enums.UserInputTypes.BOOLEAN, False, inputs, + self.CONFIG_IGNORE_EXCHANGE_FEES, commons_enums.UserInputTypes.BOOLEAN, False, inputs, parent_input_name=self.CONFIG_PAIR_SETTINGS, - title="Reinvest profits: when checked, profits will be included in mirror orders resulting in maximum " - "size mirror orders. When unchecked, a part of the total volume will be reduced to take exchange " + title="Ignore exchange fees: when checked, exchange fees won't be considered when creating mirror orders. " + "When unchecked, a part of the total volume will be reduced to take exchange " "fees into account.", ) self.UI.user_input( @@ -509,7 +509,7 @@ def __init__(self, channel, config, trading_mode, exchange_manager): self.symbol_trading_config = None self.use_existing_orders_only = self.limit_orders_count_if_necessary = \ - self.reinvest_profits = self.use_fixed_volume_for_mirror_orders = False + self.ignore_exchange_fees = self.use_fixed_volume_for_mirror_orders = False self.mode = self.spread \ = self.increment = self.operational_depth \ = self.lowest_buy = self.highest_sell \ @@ -581,8 +581,11 @@ def read_config(self): self.buy_funds))) self.sell_funds = decimal.Decimal(str(self.symbol_trading_config.get(self.trading_mode.CONFIG_SELL_FUNDS, self.sell_funds))) - self.reinvest_profits = self.symbol_trading_config.get(self.trading_mode.CONFIG_REINVEST_PROFITS, - self.reinvest_profits) + # tmp: ensure "reinvest_profits" legacy param still works + self.ignore_exchange_fees = self.symbol_trading_config.get("reinvest_profits", self.ignore_exchange_fees) + # end tmp + self.ignore_exchange_fees = self.symbol_trading_config.get(self.trading_mode.CONFIG_IGNORE_EXCHANGE_FEES, + self.ignore_exchange_fees) async def start(self) -> None: await super().start() @@ -730,7 +733,7 @@ def _compute_mirror_order_volume(self, now_selling, filled_price, target_price, # buying => adapt order quantity new_order_quantity = filled_price / target_price * filled_volume # use max possible volume - if self.reinvest_profits: + if self.ignore_exchange_fees: return new_order_quantity # remove exchange fees if paid_fees: diff --git a/Trading/Mode/staggered_orders_trading_mode/tests/test_staggered_orders_trading_mode.py b/Trading/Mode/staggered_orders_trading_mode/tests/test_staggered_orders_trading_mode.py index 1255cc15d..56d6b37a1 100644 --- a/Trading/Mode/staggered_orders_trading_mode/tests/test_staggered_orders_trading_mode.py +++ b/Trading/Mode/staggered_orders_trading_mode/tests/test_staggered_orders_trading_mode.py @@ -1148,9 +1148,9 @@ async def test_order_fill_callback_with_mirror_delay(): async def test_compute_mirror_order_volume(): async with _get_tools("BTC/USD", fees=0) as tools: producer, _, exchange_manager = tools - # no profit reinvesting + # no ignore_exchange_fees # no fixed volumes - producer.reinvest_profits = producer.use_fixed_volume_for_mirror_orders = False + producer.ignore_exchange_fees = producer.use_fixed_volume_for_mirror_orders = False # 1% max fees producer.max_fees = decimal.Decimal("0.01") # take exchange fees into account @@ -1176,8 +1176,8 @@ async def test_compute_mirror_order_volume(): False, decimal.Decimal("100"), decimal.Decimal("80"), decimal.Decimal("2"), fees ) == 2 * (decimal.Decimal("100") / decimal.Decimal("80")) - (decimal.Decimal("2.3") / decimal.Decimal("100")) - # with profits reinvesting - producer.reinvest_profits = True + # with ignore_exchange_fees + producer.ignore_exchange_fees = True # consider fees already taken, sell everything assert producer._compute_mirror_order_volume( True, decimal.Decimal("100"), decimal.Decimal("120"), decimal.Decimal("2"), None @@ -1190,7 +1190,7 @@ async def test_compute_mirror_order_volume(): ) == 2 * (decimal.Decimal("100") / decimal.Decimal("80")) # with fixed volumes - producer.reinvest_profits = False + producer.ignore_exchange_fees = False producer.sell_volume_per_order = 3 # consider fees already taken, sell everything assert producer._compute_mirror_order_volume( @@ -1205,8 +1205,8 @@ async def test_compute_mirror_order_volume(): False, decimal.Decimal("100"), decimal.Decimal("80"), decimal.Decimal("2"), {} ) == 5 - # with fixed volumes and profits reinvesting - producer.reinvest_profits = True + # with fixed volumes and ignore_exchange_fees + producer.ignore_exchange_fees = True assert producer._compute_mirror_order_volume( True, decimal.Decimal("100"), decimal.Decimal("120"), decimal.Decimal("2"), None ) == 3 diff --git a/profiles/grid_trading/specific_config/GridTradingMode.json b/profiles/grid_trading/specific_config/GridTradingMode.json index 9c7aabaeb..0f978bfc2 100644 --- a/profiles/grid_trading/specific_config/GridTradingMode.json +++ b/profiles/grid_trading/specific_config/GridTradingMode.json @@ -12,7 +12,7 @@ "starting_price": 0, "buy_volume_per_order": 0, "sell_volume_per_order": 0, - "reinvest_profits": false, + "ignore_exchange_fees": false, "use_fixed_volume_for_mirror_orders": false, "mirror_order_delay": 0, "use_existing_orders_only": false @@ -28,7 +28,7 @@ "starting_price": 0, "buy_volume_per_order": 0, "sell_volume_per_order": 0, - "reinvest_profits": false, + "ignore_exchange_fees": false, "use_fixed_volume_for_mirror_orders": false, "mirror_order_delay": 0, "use_existing_orders_only": false @@ -44,7 +44,7 @@ "starting_price": 0, "buy_volume_per_order": 0, "sell_volume_per_order": 0, - "reinvest_profits": false, + "ignore_exchange_fees": false, "use_fixed_volume_for_mirror_orders": false, "mirror_order_delay": 0, "use_existing_orders_only": false diff --git a/profiles/staggered_orders_trading/specific_config/StaggeredOrdersTradingMode.json b/profiles/staggered_orders_trading/specific_config/StaggeredOrdersTradingMode.json index 6764c6733..dc15f2522 100644 --- a/profiles/staggered_orders_trading/specific_config/StaggeredOrdersTradingMode.json +++ b/profiles/staggered_orders_trading/specific_config/StaggeredOrdersTradingMode.json @@ -11,7 +11,7 @@ "allow_instant_fill": true, "operational_depth": 100, "mirror_order_delay": 0, - "reinvest_profits": false, + "ignore_exchange_fees": false, "use_existing_orders_only": false }, { @@ -24,7 +24,7 @@ "allow_instant_fill": true, "operational_depth": 50, "mirror_order_delay": 0, - "reinvest_profits": false, + "ignore_exchange_fees": false, "use_existing_orders_only": false }, { @@ -37,7 +37,7 @@ "allow_instant_fill": true, "operational_depth": 50, "mirror_order_delay": 0, - "reinvest_profits": false, + "ignore_exchange_fees": false, "use_existing_orders_only": false } ]