From c290c5b20b94ee25aea50dd301979ae33dfd7650 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Wed, 30 Aug 2023 01:32:24 +0200 Subject: [PATCH 01/14] [Exchanges] move order id parse in trade in default parser --- Trading/Exchange/binance/binance_exchange.py | 3 --- Trading/Exchange/coinbase/coinbase_exchange.py | 3 --- Trading/Exchange/coinbase_pro/coinbase_pro_exchange.py | 3 --- 3 files changed, 9 deletions(-) diff --git a/Trading/Exchange/binance/binance_exchange.py b/Trading/Exchange/binance/binance_exchange.py index 484fcbdee..af9eefacc 100644 --- a/Trading/Exchange/binance/binance_exchange.py +++ b/Trading/Exchange/binance/binance_exchange.py @@ -245,9 +245,6 @@ def fix_trades(self, raw, **kwargs): raw = super().fix_trades(raw, **kwargs) for trade in raw: trade[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = trading_enums.OrderStatus.CLOSED.value - trade[trading_enums.ExchangeConstantsOrderColumns.EXCHANGE_ID.value] = trade[ - trading_enums.ExchangeConstantsOrderColumns.ORDER.value - ] return raw def parse_position(self, fixed, force_empty=False, **kwargs): diff --git a/Trading/Exchange/coinbase/coinbase_exchange.py b/Trading/Exchange/coinbase/coinbase_exchange.py index 97e3d1afc..0cb931a00 100644 --- a/Trading/Exchange/coinbase/coinbase_exchange.py +++ b/Trading/Exchange/coinbase/coinbase_exchange.py @@ -85,9 +85,6 @@ def fix_trades(self, raw, **kwargs): raw = super().fix_trades(raw, **kwargs) for trade in raw: trade[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = trading_enums.OrderStatus.CLOSED.value - trade[trading_enums.ExchangeConstantsOrderColumns.EXCHANGE_ID.value] = trade[ - trading_enums.ExchangeConstantsOrderColumns.ORDER.value - ] try: if trade[trading_enums.ExchangeConstantsOrderColumns.AMOUNT.value] is None and \ trade[trading_enums.ExchangeConstantsOrderColumns.COST.value] and \ diff --git a/Trading/Exchange/coinbase_pro/coinbase_pro_exchange.py b/Trading/Exchange/coinbase_pro/coinbase_pro_exchange.py index 88e087e8c..ca470fb37 100644 --- a/Trading/Exchange/coinbase_pro/coinbase_pro_exchange.py +++ b/Trading/Exchange/coinbase_pro/coinbase_pro_exchange.py @@ -71,7 +71,4 @@ def fix_trades(self, raw, **kwargs): raw = super().fix_trades(raw, **kwargs) for trade in raw: trade[trading_enums.ExchangeConstantsOrderColumns.STATUS.value] = trading_enums.OrderStatus.CLOSED.value - trade[trading_enums.ExchangeConstantsOrderColumns.EXCHANGE_ID.value] = trade[ - trading_enums.ExchangeConstantsOrderColumns.ORDER.value - ] return raw From 7d4ad473c93422e77100b02e6c5c3a182bb2b1a7 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Thu, 31 Aug 2023 12:23:17 +0200 Subject: [PATCH 02/14] [WebInterface] sandbox badge --- .../Interfaces/web_interface/controllers/home.py | 2 ++ .../Interfaces/web_interface/models/__init__.py | 2 ++ .../web_interface/models/configuration.py | 8 ++++++++ .../web_interface/templates/index.html | 16 ++++++++++++++-- .../Services_bases/telegram_service/telegram.py | 2 +- 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Services/Interfaces/web_interface/controllers/home.py b/Services/Interfaces/web_interface/controllers/home.py index a11b385f5..d169abba8 100644 --- a/Services/Interfaces/web_interface/controllers/home.py +++ b/Services/Interfaces/web_interface/controllers/home.py @@ -41,6 +41,7 @@ def home(): all_time_frames = models.get_all_watched_time_frames() display_time_frame = models.get_display_timeframe() display_orders = models.get_display_orders() + sandbox_exchanges = models.get_sandbox_exchanges() try: user_id = models.get_user_account_id() display_feedback_form = form_to_display and not models.has_filled_form(form_to_display) @@ -63,6 +64,7 @@ def home(): user_id=user_id, form_to_display=form_to_display, display_feedback_form=display_feedback_form, + sandbox_exchanges=sandbox_exchanges ) else: return flask.redirect(flask.url_for("terms")) diff --git a/Services/Interfaces/web_interface/models/__init__.py b/Services/Interfaces/web_interface/models/__init__.py index 13ee959f4..f3b9c2c6e 100644 --- a/Services/Interfaces/web_interface/models/__init__.py +++ b/Services/Interfaces/web_interface/models/__init__.py @@ -158,6 +158,7 @@ reload_tentacle_config, update_config_currencies, get_config_required_candles_count, + get_sandbox_exchanges, ) from tentacles.Services.Interfaces.web_interface.models.dashboard import ( parse_get_symbol, @@ -449,5 +450,6 @@ "reload_tentacle_config", "update_config_currencies", "get_config_required_candles_count", + "get_sandbox_exchanges", "WebInterfaceTab", ] diff --git a/Services/Interfaces/web_interface/models/configuration.py b/Services/Interfaces/web_interface/models/configuration.py index 8004dd067..09e022cf7 100644 --- a/Services/Interfaces/web_interface/models/configuration.py +++ b/Services/Interfaces/web_interface/models/configuration.py @@ -1386,6 +1386,14 @@ def get_current_exchange(): return DEFAULT_EXCHANGE +def get_sandbox_exchanges() -> list: + return [ + trading_api.get_exchange_name(exchange_manager) + for exchange_manager in interfaces_util.get_exchange_managers() + if trading_api.get_exchange_manager_is_sandboxed(exchange_manager) + ] + + def change_reference_market_on_config_currencies(old_base_currency: str, new_quote_currency: str) -> bool: """ Change the base currency from old to new for all configured pair diff --git a/Services/Interfaces/web_interface/templates/index.html b/Services/Interfaces/web_interface/templates/index.html index 9369fe0c3..624abc785 100644 --- a/Services/Interfaces/web_interface/templates/index.html +++ b/Services/Interfaces/web_interface/templates/index.html @@ -107,8 +107,20 @@

- -

Watched markets

+ {% if sandbox_exchanges %} +
+ {{sandbox_exchanges[0] | capitalize}} sandbox +
+ {% endif %} +
+

Watched markets

diff --git a/Services/Services_bases/telegram_service/telegram.py b/Services/Services_bases/telegram_service/telegram.py index de22e906c..cd39c33b6 100644 --- a/Services/Services_bases/telegram_service/telegram.py +++ b/Services/Services_bases/telegram_service/telegram.py @@ -28,7 +28,7 @@ class TelegramService(services.AbstractService): CONNECT_TIMEOUT = 7 # default is 5, use 7 to take slow connections into account CHAT_ID = "chat-id" LOGGERS = ["telegram._bot", "telegram.ext.Updater", "telegram.ext.ExtBot", - "hpack.hpack", "hpack.table", "httpx._client"] + "hpack.hpack", "hpack.table"] def __init__(self): super().__init__() From 8c4d498f6ac476b3130b8ef7470b6668b8148c2b Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Fri, 1 Sep 2023 14:48:56 +0200 Subject: [PATCH 03/14] [StaggeredOrders] improve logs --- .../staggered_orders_trading.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 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 3814e3bc9..6c9d869a2 100644 --- a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py +++ b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py @@ -295,13 +295,15 @@ async def create_order(self, order_data, current_price, symbol_market): if selling: available = trading_api.get_portfolio_currency(self.exchange_manager, currency).available if available < order_quantity: - self.logger.error(f"Skipping order creation: not enough {currency}: available: {available}, " - f"required: {order_quantity}") + self.logger.error( + f"Skipping {order_data.side.value} order creation at {float(order_data.price)}: " + f"not enough {currency}: available: {available}, required: {order_quantity}" + ) return [] elif market_available < order_quantity * order_price: self.logger.error( - f"Skipping order creation: not enough {market}: available: {market_available}, " - f"required: {order_quantity * order_price}" + f"Skipping {order_data.side.value} order creation at {float(order_data.price)}: " + f"not enough {market}: available: {market_available}, required: {order_quantity * order_price}" ) return [] order_type = trading_enums.TraderOrderType.SELL_LIMIT if selling \ @@ -878,7 +880,8 @@ async def _pack_and_balance_missing_orders(self, trades_with_missing_mirror_orde return self.logger.info( f"{len(trades_with_missing_mirror_order_fills)} missed order fills on {self.symbol}, " - f"creating a {order_type.value} order of size {float(order_amount)} to compensate" + f"creating a {order_type.value} order of size {float(order_amount)} to compensate. " + f"Missed fills: {[trade.to_dict() for trade in trades_with_missing_mirror_order_fills]}" ) balancing_order = trading_personal_data.create_order_instance( @@ -1130,7 +1133,7 @@ def _get_spread_missing_order_quantity( ): self.logger.info( f"Using boundary orders to compute restored order quantity for {'sell' if selling else 'buy'} " - f"order at price: {price}: recent trades are not available." + f"order at {price}: no equivalent order for in recent trades (recent trades: {recent_trades})." ) return quantity self.logger.error( From 8d936fc928f2e5c41737e987ddb59696cd9bc9cd Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Fri, 1 Sep 2023 16:00:15 +0200 Subject: [PATCH 04/14] [Exchanges] use seconds ticker timestamp --- Trading/Exchange/ascendex/ascendex_exchange.py | 3 ++- Trading/Exchange/bybit/bybit_exchange.py | 3 ++- Trading/Exchange/gateio/gateio_exchange.py | 3 ++- Trading/Exchange/wavesexchange/wavesexchange_exchange.py | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Trading/Exchange/ascendex/ascendex_exchange.py b/Trading/Exchange/ascendex/ascendex_exchange.py index 540f9539c..12407d8a9 100644 --- a/Trading/Exchange/ascendex/ascendex_exchange.py +++ b/Trading/Exchange/ascendex/ascendex_exchange.py @@ -69,5 +69,6 @@ class AscendexCCXTAdapter(exchanges.CCXTAdapter): def fix_ticker(self, raw, **kwargs): fixed = super().fix_ticker(raw, **kwargs) - fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = self.connector.client.milliseconds() + fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \ + fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds() return fixed diff --git a/Trading/Exchange/bybit/bybit_exchange.py b/Trading/Exchange/bybit/bybit_exchange.py index 79858dcb9..bcc5e978e 100644 --- a/Trading/Exchange/bybit/bybit_exchange.py +++ b/Trading/Exchange/bybit/bybit_exchange.py @@ -401,7 +401,8 @@ def _adapt_order_type(self, fixed): def fix_ticker(self, raw, **kwargs): fixed = super().fix_ticker(raw, **kwargs) - fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = self.connector.client.milliseconds() + fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \ + fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds() return fixed def parse_position(self, fixed, **kwargs): diff --git a/Trading/Exchange/gateio/gateio_exchange.py b/Trading/Exchange/gateio/gateio_exchange.py index 8c1b1935a..41b0de2c9 100644 --- a/Trading/Exchange/gateio/gateio_exchange.py +++ b/Trading/Exchange/gateio/gateio_exchange.py @@ -43,5 +43,6 @@ class GateioCCXTAdapter(exchanges.CCXTAdapter): def fix_ticker(self, raw, **kwargs): fixed = super().fix_ticker(raw, **kwargs) - fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = self.connector.client.milliseconds() + fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \ + fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds() return fixed diff --git a/Trading/Exchange/wavesexchange/wavesexchange_exchange.py b/Trading/Exchange/wavesexchange/wavesexchange_exchange.py index d335aa4be..a32ef8085 100644 --- a/Trading/Exchange/wavesexchange/wavesexchange_exchange.py +++ b/Trading/Exchange/wavesexchange/wavesexchange_exchange.py @@ -44,5 +44,6 @@ class WavesCCXTAdapter(exchanges.CCXTAdapter): def fix_ticker(self, raw, **kwargs): fixed = super().fix_ticker(raw, **kwargs) - fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = self.connector.client.milliseconds() + fixed[trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value] = \ + fixed.get(trading_enums.ExchangeConstantsTickersColumns.TIMESTAMP.value) or self.connector.client.seconds() return fixed From 5915c722d2b29430b1775f5c8420c557d7799768 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Fri, 1 Sep 2023 16:43:15 +0200 Subject: [PATCH 05/14] [Staggered] always ensure orders quantity --- .../staggered_orders_trading.py | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 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 6c9d869a2..a31d3429a 100644 --- a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py +++ b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py @@ -296,13 +296,15 @@ async def create_order(self, order_data, current_price, symbol_market): available = trading_api.get_portfolio_currency(self.exchange_manager, currency).available if available < order_quantity: self.logger.error( - f"Skipping {order_data.side.value} order creation at {float(order_data.price)}: " + f"Skipping {order_data.symbol} {order_data.side.value} order creation of " + f"{order_quantity} at {float(order_price)}: " f"not enough {currency}: available: {available}, required: {order_quantity}" ) return [] elif market_available < order_quantity * order_price: self.logger.error( - f"Skipping {order_data.side.value} order creation at {float(order_data.price)}: " + f"Skipping {order_data.symbol} {order_data.side.value} order creation of " + f"{order_quantity} at {float(order_price)}: " f"not enough {market}: available: {market_available}, required: {order_quantity * order_price}" ) return [] @@ -866,7 +868,7 @@ async def _pack_and_balance_missing_orders(self, trades_with_missing_mirror_orde order_amount = trading_personal_data.decimal_adapt_quantity(symbol_market, abs(to_create_order_quantity)) if order_amount == trading_constants.ZERO: self.logger.error( - f"No enough computed funds to recreate packed missing mirror order balancing order on {self.symbol}" + f"No enough computed funds to recreate packed missed mirror order balancing order on {self.symbol}" ) return currency_available, market_available, market_quantity = \ @@ -874,7 +876,7 @@ async def _pack_and_balance_missing_orders(self, trades_with_missing_mirror_orde limiting_amount = currency_available if order_type is trading_enums.TraderOrderType.SELL_MARKET \ else market_quantity if order_amount > limiting_amount: - self.logger.error(f"No enough available funds to packed missing mirror order balancing " + self.logger.error(f"No enough available funds to create missed mirror order balancing " f"order on {self.symbol}. " f"Required {float(order_amount)}, available {float(limiting_amount)}") return @@ -1114,35 +1116,38 @@ def _get_spread_missing_order_quantity( order_limiting_currency_available_amount, recent_trades, sorted_orders, current_price ): + quantity = None if sorted_orders: - if quantity := self._get_quantity_from_existing_orders( + quantity = self._get_quantity_from_existing_orders( price, sorted_orders, selling - ): - return quantity - if quantity := self._get_quantity_from_recent_trades( - price, limiting_amount_from_this_order, recent_trades, current_price, selling - ): - return quantity - try: - quantity = self._get_quantity_from_iteration( - average_order_quantity, self.mode, side, i, orders_count, price, price ) - except trading_errors.NotSupported: - if quantity := self._get_quantity_from_existing_boundary_orders( - price, sorted_orders, selling - ): - self.logger.info( - f"Using boundary orders to compute restored order quantity for {'sell' if selling else 'buy'} " - f"order at {price}: no equivalent order for in recent trades (recent trades: {recent_trades})." - ) - return quantity - self.logger.error( - f"Error when computing restored order quantity for {'sell' if selling else 'buy'} order at " - f"price: {price}: recent trades or active orders are required." + if not quantity: + quantity = self._get_quantity_from_recent_trades( + price, limiting_amount_from_this_order, recent_trades, current_price, selling ) - return None + if not quantity: + try: + quantity = self._get_quantity_from_iteration( + average_order_quantity, self.mode, side, i, orders_count, price, price + ) + except trading_errors.NotSupported: + quantity = self._get_quantity_from_existing_boundary_orders( + price, sorted_orders, selling + ) + if quantity: + self.logger.info( + f"Using boundary orders to compute restored order quantity for {'sell' if selling else 'buy'} " + f"order at {price}: no equivalent order for in recent trades (recent trades: {recent_trades})." + ) + else: + self.logger.error( + f"Error when computing restored order quantity for {'sell' if selling else 'buy'} order at " + f"price: {price}: recent trades or active orders are required." + ) + return None if quantity is None: return None + # always ensure ideal quantity is available limiting_currency_quantity = quantity if limiting_currency_quantity > limiting_amount_from_this_order or \ limiting_currency_quantity > order_limiting_currency_available_amount: From 6069263041bc4d8b68e63151202b35758f64c51b Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Sun, 3 Sep 2023 12:39:56 +0200 Subject: [PATCH 06/14] [GridTrading] log missing orders --- Trading/Mode/grid_trading_mode/grid_trading.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Trading/Mode/grid_trading_mode/grid_trading.py b/Trading/Mode/grid_trading_mode/grid_trading.py index 1be43028a..1bc46b935 100644 --- a/Trading/Mode/grid_trading_mode/grid_trading.py +++ b/Trading/Mode/grid_trading_mode/grid_trading.py @@ -413,6 +413,10 @@ async def _generate_staggered_orders(self, current_price, ignore_available_funds missing_orders, state, _ = self._analyse_current_orders_situation( sorted_orders, recently_closed_trades, lowest_buy, highest_sell, current_price ) + if missing_orders: + self.logger.info( + f"{len(missing_orders)} missing {self.symbol} orders on {self.exchange_name}: {missing_orders}" + ) await self._handle_missed_mirror_orders_fills(recently_closed_trades, missing_orders, current_price) try: buy_orders = self._create_orders(lowest_buy, highest_buy, From a7703c04fabe0e99343602f2270a72e805aa65dc Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 5 Sep 2023 22:06:52 +0200 Subject: [PATCH 07/14] [StaggeredOrders] fix log --- .../staggered_orders_trading_mode/staggered_orders_trading.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 a31d3429a..9518f4e06 100644 --- a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py +++ b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py @@ -1137,7 +1137,8 @@ def _get_spread_missing_order_quantity( if quantity: self.logger.info( f"Using boundary orders to compute restored order quantity for {'sell' if selling else 'buy'} " - f"order at {price}: no equivalent order for in recent trades (recent trades: {recent_trades})." + f"order at {price}: no equivalent order for in recent trades (recent trades: " + f"{[str(t) for t in recent_trades]})." ) else: self.logger.error( From 803fc2abb6828212be0d7b6095170c1184889079 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Fri, 8 Sep 2023 12:43:56 +0200 Subject: [PATCH 08/14] [StaggeredOrders] improve missed orders balance --- Services/Interfaces/web_interface/web.py | 2 +- .../staggered_orders_trading.py | 116 +++++++++++------- 2 files changed, 75 insertions(+), 43 deletions(-) diff --git a/Services/Interfaces/web_interface/web.py b/Services/Interfaces/web_interface/web.py index 6300c134a..4b1fbcac8 100644 --- a/Services/Interfaces/web_interface/web.py +++ b/Services/Interfaces/web_interface/web.py @@ -274,4 +274,4 @@ async def stop(self): self.websocket_instance.stop() self.logger.debug("Stopped web interface") except Exception as e: - self.logger.exception(f"Error when stopping web interface : {e}", False) + self.logger.exception(e, False, f"Error when stopping web interface : {e}") 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 9518f4e06..8d0b9b804 100644 --- a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py +++ b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py @@ -296,14 +296,16 @@ async def create_order(self, order_data, current_price, symbol_market): available = trading_api.get_portfolio_currency(self.exchange_manager, currency).available if available < order_quantity: self.logger.error( - f"Skipping {order_data.symbol} {order_data.side.value} order creation of " + f"Skipping {order_data.symbol} {order_data.side.value} " + f"[{self.exchange_manager.exchange_name}] order creation of " f"{order_quantity} at {float(order_price)}: " f"not enough {currency}: available: {available}, required: {order_quantity}" ) return [] elif market_available < order_quantity * order_price: self.logger.error( - f"Skipping {order_data.symbol} {order_data.side.value} order creation of " + f"Skipping {order_data.symbol} {order_data.side.value} " + f"[{self.exchange_manager.exchange_name}] order creation of " f"{order_quantity} at {float(order_price)}: " f"not enough {market}: available: {market_available}, required: {order_quantity * order_price}" ) @@ -854,61 +856,91 @@ async def _handle_missed_mirror_orders_fills(self, sorted_trades, missing_orders async def _pack_and_balance_missing_orders(self, trades_with_missing_mirror_order_fills, current_price): base, quote = symbol_util.parse_symbol(self.symbol).base_and_quote() + self.logger.info( + f"Packing {len(trades_with_missing_mirror_order_fills)} missed [{self.exchange_manager.exchange_name}] " + f"mirror orders, trades {[trade.to_dict() for trade in trades_with_missing_mirror_order_fills]}" + ) to_create_order_quantity = sum( (trade.executed_quantity - trading_personal_data.get_fees_for_currency(trade.fee, base)) * (-1 if trade.side is trading_enums.TradeOrderSide.BUY else 1) for trade in trades_with_missing_mirror_order_fills ) + self.logger.info( + f"Packed {len(trades_with_missing_mirror_order_fills)} missed [{self.exchange_manager.exchange_name}] " + f"balancing quantity into: {to_create_order_quantity} {base}" + ) if to_create_order_quantity == trading_constants.ZERO: return # create a market order to balance funds order_type = trading_enums.TraderOrderType.SELL_MARKET if to_create_order_quantity < trading_constants.ZERO \ else trading_enums.TraderOrderType.BUY_MARKET - symbol_market = self.exchange_manager.exchange.get_market_status(self.symbol, with_fixer=False) - order_amount = trading_personal_data.decimal_adapt_quantity(symbol_market, abs(to_create_order_quantity)) - if order_amount == trading_constants.ZERO: + target_amount = abs(to_create_order_quantity) + to_create_details = trading_personal_data.decimal_check_and_adapt_order_details_if_necessary( + target_amount, + current_price, + self.symbol_market + ) + if not to_create_details: self.logger.error( - f"No enough computed funds to recreate packed missed mirror order balancing order on {self.symbol}" + f"No enough computed funds to recreate packed missed [{self.exchange_manager.exchange_name}] " + f"mirror order balancing order on {self.symbol}: target_amount: {target_amount} is not enough " + f"for exchange minimal trading amounts rules" ) return - currency_available, market_available, market_quantity = \ - trading_personal_data.get_portfolio_amounts(self.exchange_manager, self.symbol, current_price) - limiting_amount = currency_available if order_type is trading_enums.TraderOrderType.SELL_MARKET \ - else market_quantity - if order_amount > limiting_amount: - self.logger.error(f"No enough available funds to create missed mirror order balancing " - f"order on {self.symbol}. " - f"Required {float(order_amount)}, available {float(limiting_amount)}") - return - self.logger.info( - f"{len(trades_with_missing_mirror_order_fills)} missed order fills on {self.symbol}, " - f"creating a {order_type.value} order of size {float(order_amount)} to compensate. " - f"Missed fills: {[trade.to_dict() for trade in trades_with_missing_mirror_order_fills]}" - ) + for order_amount, order_price in to_create_details: + currency_available, market_available, market_quantity = \ + trading_personal_data.get_portfolio_amounts(self.exchange_manager, self.symbol, order_price) + limiting_amount = currency_available if order_type is trading_enums.TraderOrderType.SELL_MARKET \ + else market_quantity + if order_amount > limiting_amount: + limiting_currency = base if order_type is trading_enums.TraderOrderType.SELL_MARKET \ + else quote + other_amount = currency_available if order_type is trading_enums.TraderOrderType.BUY_MARKET \ + else market_quantity + other_currency = base if order_type is trading_enums.TraderOrderType.BUY_MARKET \ + else quote + self.logger.error( + f"No enough available funds to create missed [{self.exchange_manager.exchange_name}] mirror " + f"order {order_type.value} balancing order on {self.symbol}. " + f"Required {float(order_amount)} {limiting_currency}, available {float(limiting_amount)} " + f"{limiting_currency} ({other_currency} available: {other_amount})" + ) + return + self.logger.info( + f"{len(trades_with_missing_mirror_order_fills)} missed [{self.exchange_manager.exchange_name}] order " + f"fills on {self.symbol}, creating a {order_type.value} order of {float(order_amount)} {base} " + f"to compensate." + ) - balancing_order = trading_personal_data.create_order_instance( - trader=self.exchange_manager.trader, - order_type=order_type, - symbol=self.symbol, - current_price=current_price, - quantity=order_amount, - price=current_price, - reduce_only=False, - ) - created_order = await self.trading_mode.create_order(balancing_order) - # wait for order to be filled - if created_order.is_open(): - if created_order.state is None: - self.logger.error(f"None state on created order, impossible to wait for fill, order: {created_order}") + balancing_order = trading_personal_data.create_order_instance( + trader=self.exchange_manager.trader, + order_type=order_type, + symbol=self.symbol, + current_price=order_price, + quantity=order_amount, + price=order_price, + reduce_only=False, + ) + created_order = await self.trading_mode.create_order(balancing_order) + # wait for order to be filled + if created_order.is_open(): + if created_order.state is None: + self.logger.error( + f"None state on created order, impossible to wait for fill, order: {created_order}" + ) + else: + try: + await created_order.state.wait_for_next_state( + self.MISSING_MIRROR_ORDERS_MARKET_REBALANCE_TIMEOUT + ) + except asyncio.TimeoutError: + self.logger.error( + f"Timeout while waiting for rebalance marker order fill, order {created_order}" + ) + if created_order.is_open(): + self.logger.error(f"Rebalance marker order is still open, order {created_order}") else: - try: - await created_order.state.wait_for_next_state(self.MISSING_MIRROR_ORDERS_MARKET_REBALANCE_TIMEOUT) - except asyncio.TimeoutError: - self.logger.error(f"Timeout while waiting for rebalance marker order fill, order {created_order}") - if created_order.is_open(): - self.logger.error(f"Rebalance marker order is still open, order {created_order}") - else: - self.logger.info("Successfully rebalanced funds after missed mirror orders.") + self.logger.info("Successfully rebalanced funds after missed mirror orders.") def _find_missing_mirror_order_fills(self, sorted_trades, missing_orders): trades_with_missing_mirror_order_fills = [] From 4c92b2f13fb13058e1ff827ad3ee73a8a897d553 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 12 Sep 2023 23:40:45 +0200 Subject: [PATCH 09/14] [StaggeredOrders] adapt missed mirror orders quantity to holdings --- .../staggered_orders_trading.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 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 8d0b9b804..53ad8e206 100644 --- a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py +++ b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py @@ -359,6 +359,7 @@ class StaggeredOrdersTradingModeProducer(trading_modes.AbstractTradingModeProduc GENERATE_ORDERS_LOCKS_BY_EXCHANGE_ID = {} FUNDS_INCREASE_RATIO_THRESHOLD = decimal.Decimal("0.5") # ratio bellow with funds will be reallocated: # used to track new funds and update orders accordingly + ALLOWED_MISSED_MIRRORED_ORDERS_ADAPT_DELTA_RATIO = decimal.Decimal("0.5") def __init__(self, channel, config, trading_mode, exchange_manager): super().__init__(channel, config, trading_mode, exchange_manager) @@ -875,6 +876,19 @@ async def _pack_and_balance_missing_orders(self, trades_with_missing_mirror_orde order_type = trading_enums.TraderOrderType.SELL_MARKET if to_create_order_quantity < trading_constants.ZERO \ else trading_enums.TraderOrderType.BUY_MARKET target_amount = abs(to_create_order_quantity) + currency_available, market_available, market_quantity = \ + trading_personal_data.get_portfolio_amounts(self.exchange_manager, self.symbol, current_price) + limiting_amount = currency_available if order_type is trading_enums.TraderOrderType.SELL_MARKET \ + else market_quantity + if target_amount > limiting_amount: + # use limiting_amount if delta from order_amount is bellow allowed threshold + delta = target_amount - limiting_amount + try: + if delta / target_amount < self.ALLOWED_MISSED_MIRRORED_ORDERS_ADAPT_DELTA_RATIO: + target_amount = limiting_amount + except (decimal.DivisionByZero, decimal.InvalidOperation): + # leave as is + pass to_create_details = trading_personal_data.decimal_check_and_adapt_order_details_if_necessary( target_amount, current_price, @@ -888,10 +902,6 @@ async def _pack_and_balance_missing_orders(self, trades_with_missing_mirror_orde ) return for order_amount, order_price in to_create_details: - currency_available, market_available, market_quantity = \ - trading_personal_data.get_portfolio_amounts(self.exchange_manager, self.symbol, order_price) - limiting_amount = currency_available if order_type is trading_enums.TraderOrderType.SELL_MARKET \ - else market_quantity if order_amount > limiting_amount: limiting_currency = base if order_type is trading_enums.TraderOrderType.SELL_MARKET \ else quote From 3e2cef2d646316078ef037b3255ecb40132236d0 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 12 Sep 2023 20:01:21 +0200 Subject: [PATCH 10/14] [TradingModes] update get_split_orders_count_and_increment call --- Trading/Mode/dca_trading_mode/dca_trading.py | 18 +++++++++--------- .../dip_analyser_trading.py | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Trading/Mode/dca_trading_mode/dca_trading.py b/Trading/Mode/dca_trading_mode/dca_trading.py index b516ee707..179e347a0 100644 --- a/Trading/Mode/dca_trading_mode/dca_trading.py +++ b/Trading/Mode/dca_trading_mode/dca_trading.py @@ -224,19 +224,19 @@ async def _create_entry_with_chained_exit_orders(self, entry_order, entry_price, ) stop_price = entry_price * ( trading_constants.ONE - ( - self.trading_mode.stop_loss_price_multiplier * exit_multiplier_side_flag - ) + self.trading_mode.stop_loss_price_multiplier * exit_multiplier_side_flag + ) ) first_sell_price = entry_price * ( trading_constants.ONE + ( - self.trading_mode.exit_limit_orders_price_multiplier * exit_multiplier_side_flag - ) + self.trading_mode.exit_limit_orders_price_multiplier * exit_multiplier_side_flag + ) ) last_sell_price = entry_price * ( trading_constants.ONE + ( - self.trading_mode.secondary_exit_orders_price_multiplier * - (1 + self.trading_mode.secondary_exit_orders_count) * exit_multiplier_side_flag - ) + self.trading_mode.secondary_exit_orders_price_multiplier * + (1 + self.trading_mode.secondary_exit_orders_count) * exit_multiplier_side_flag + ) ) # split entry into multiple exits if necessary (and possible) exit_quantities = self._split_entry_quantity( @@ -264,7 +264,7 @@ async def _create_entry_with_chained_exit_orders(self, entry_order, entry_price, if i == 1 else ( self.trading_mode.exit_limit_orders_price_multiplier + self.trading_mode.secondary_exit_orders_price_multiplier * i - ) + ) take_profit_price = trading_personal_data.decimal_adapt_price( symbol_market, entry_price * ( @@ -291,7 +291,7 @@ def _split_entry_quantity(quantity, target_exits_count, lowest_price, highest_pr if target_exits_count == 1: return [(1, quantity)] adapted_sell_orders_count, increment = trading_personal_data.get_split_orders_count_and_increment( - lowest_price, highest_price, quantity, target_exits_count, symbol_market + lowest_price, highest_price, quantity, target_exits_count, symbol_market, False ) if adapted_sell_orders_count: return [ diff --git a/Trading/Mode/dip_analyser_trading_mode/dip_analyser_trading.py b/Trading/Mode/dip_analyser_trading_mode/dip_analyser_trading.py index 35e40b471..6303a5688 100644 --- a/Trading/Mode/dip_analyser_trading_mode/dip_analyser_trading.py +++ b/Trading/Mode/dip_analyser_trading_mode/dip_analyser_trading.py @@ -417,7 +417,7 @@ def _generate_sell_orders(self, sell_orders_count, quantity, sell_weight, sell_b volume_with_price = [] sell_max = sell_base * self.PRICE_WEIGH_TO_PRICE_PERCENT[sell_weight] adapted_sell_orders_count, increment = trading_personal_data.get_split_orders_count_and_increment( - sell_base, sell_max, quantity, sell_orders_count, symbol_market + sell_base, sell_max, quantity, sell_orders_count, symbol_market, True ) if adapted_sell_orders_count: order_volume = quantity / adapted_sell_orders_count From 3a2a21d0dea28b168bac2ed0844daca47ae6ef11 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 12 Sep 2023 23:06:23 +0200 Subject: [PATCH 11/14] [Kucoin] fix get_account_id issue --- Trading/Exchange/kucoin/kucoin_exchange.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Trading/Exchange/kucoin/kucoin_exchange.py b/Trading/Exchange/kucoin/kucoin_exchange.py index b8d1d60e7..1a61dd11b 100644 --- a/Trading/Exchange/kucoin/kucoin_exchange.py +++ b/Trading/Exchange/kucoin/kucoin_exchange.py @@ -127,6 +127,7 @@ async def get_account_id(self, **kwargs: dict) -> str: account_id = None subaccount_id = None sub_accounts = await self.connector.client.private_get_sub_accounts() + has_subaccounts = bool(sub_accounts["data"]) for account in sub_accounts["data"]: if account["subUserId"]: subaccount_id = account["subName"] @@ -147,7 +148,7 @@ async def get_account_id(self, **kwargs: dict) -> str: f"sub_accounts={sub_accounts} subaccount_api_key_details={subaccount_api_key_details}" ) return constants.DEFAULT_ACCOUNT_ID - if account_id is None: + if has_subaccounts and account_id is None: self.logger.error( f"kucoin api changed: can't fetch master account account_id. " f"kucoin get_account_id has to be updated." @@ -155,14 +156,14 @@ async def get_account_id(self, **kwargs: dict) -> str: ) account_id = constants.DEFAULT_ACCOUNT_ID # we are on the master account - return account_id + return account_id or constants.DEFAULT_ACCOUNT_ID except ccxt.AuthenticationError: # when api key is wrong raise except ccxt.ExchangeError: # ExchangeError('kucoin This user is not a master user') # raised when calling this endpoint with a subaccount - return constants.DEFAULT_ACCOUNT_ID + return constants.DEFAULT_SUBACCOUNT_ID @_kucoin_retrier From 04e3e04ed87dc87569c8e8174ea0b78eee0dfc24 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Fri, 22 Sep 2023 18:08:19 +0200 Subject: [PATCH 12/14] [RemoteSignals] log error on missing order from signal --- .../remote_trading_signals_trading.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Trading/Mode/remote_trading_signals_trading_mode/remote_trading_signals_trading.py b/Trading/Mode/remote_trading_signals_trading_mode/remote_trading_signals_trading.py index 175059487..b143cd4c4 100644 --- a/Trading/Mode/remote_trading_signals_trading_mode/remote_trading_signals_trading.py +++ b/Trading/Mode/remote_trading_signals_trading_mode/remote_trading_signals_trading.py @@ -456,13 +456,20 @@ def get_open_order_from_description(self, order_descriptions, symbol): found_orders += accurate_orders continue # 2nd chance: use order type and price as these are kept between bot restarts (loaded from exchange) - found_orders += [ + orders = [ (order_description, order) for order in self.exchange_manager.exchange_personal_data.orders_manager.get_open_orders(symbol=symbol) if (order.origin_price == order_description[trading_enums.TradingSignalOrdersAttrs.STOP_PRICE.value] or order.origin_price == order_description[trading_enums.TradingSignalOrdersAttrs.LIMIT_PRICE.value]) and self._is_compatible_order_type(order, order_description) ] + if orders: + found_orders += orders + else: + self.logger.error( + f"Order not found on {self.exchange_manager.exchange_name} open orders, " + f"description: {order_description}" + ) return found_orders def _is_compatible_order_type(self, order, order_description): From ab6c21c8fe999ef600af6cf3a86ac65feb920925 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Fri, 22 Sep 2023 18:08:40 +0200 Subject: [PATCH 13/14] [StaggeredOrders] use warning to log missing funds --- .../staggered_orders_trading_mode/staggered_orders_trading.py | 4 ++-- 1 file changed, 2 insertions(+), 2 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 53ad8e206..6ab08278b 100644 --- a/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py +++ b/Trading/Mode/staggered_orders_trading_mode/staggered_orders_trading.py @@ -295,7 +295,7 @@ async def create_order(self, order_data, current_price, symbol_market): if selling: available = trading_api.get_portfolio_currency(self.exchange_manager, currency).available if available < order_quantity: - self.logger.error( + self.logger.warning( f"Skipping {order_data.symbol} {order_data.side.value} " f"[{self.exchange_manager.exchange_name}] order creation of " f"{order_quantity} at {float(order_price)}: " @@ -303,7 +303,7 @@ async def create_order(self, order_data, current_price, symbol_market): ) return [] elif market_available < order_quantity * order_price: - self.logger.error( + self.logger.warning( f"Skipping {order_data.symbol} {order_data.side.value} " f"[{self.exchange_manager.exchange_name}] order creation of " f"{order_quantity} at {float(order_price)}: " From 8dfadf0fc207d89b36f18bf7921dec46fb932ff4 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Sat, 23 Sep 2023 15:01:23 +0200 Subject: [PATCH 14/14] [WebInterface] update docs links --- .../Interfaces/web_interface/static/js/common/tutorial.js | 4 ++-- .../Interfaces/web_interface/templates/config_tentacle.html | 2 +- Services/Interfaces/web_interface/templates/profile.html | 5 ++--- Services/Services_bases/telegram_service/telegram.py | 2 +- Services/Services_bases/web_service/web.py | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Services/Interfaces/web_interface/static/js/common/tutorial.js b/Services/Interfaces/web_interface/static/js/common/tutorial.js index be463de82..b39e65f6a 100644 --- a/Services/Interfaces/web_interface/static/js/common/tutorial.js +++ b/Services/Interfaces/web_interface/static/js/common/tutorial.js @@ -119,7 +119,7 @@ _TUTORIALS = { }, { title: 'See also', - intro: `More details on ${getWebsiteLink("/guides/#trading_modes", "the OctoBot guide")}.` + intro: `More details on ${getDocsLink("/configuration/profiles", "the profiles guide")}.` }, ] } @@ -242,7 +242,7 @@ _TUTORIALS = { }, { title: 'See also', - intro: `More details the ${getWebsiteLink("/guides/#backtesting", "backtesting guide")}.` + intro: `More details the ${getDocsLink("/advanced_usage/backtesting-and-strategy-optimization", "backtesting guide")}.` }, ] } diff --git a/Services/Interfaces/web_interface/templates/config_tentacle.html b/Services/Interfaces/web_interface/templates/config_tentacle.html index 4f76dc318..298c98396 100644 --- a/Services/Interfaces/web_interface/templates/config_tentacle.html +++ b/Services/Interfaces/web_interface/templates/config_tentacle.html @@ -195,7 +195,7 @@

Test configuration Activation required {% endif %} - +   {% if activated_trading_mode %} diff --git a/Services/Interfaces/web_interface/templates/profile.html b/Services/Interfaces/web_interface/templates/profile.html index 7bea5d9d0..3740ef9a8 100644 --- a/Services/Interfaces/web_interface/templates/profile.html +++ b/Services/Interfaces/web_interface/templates/profile.html @@ -215,8 +215,7 @@

This trading mode doesn't need any strategy.

Profile strategy configuration read only - +

@@ -359,7 +358,7 @@

Strategies

Exchanges + href="{{EXCHANGES_DOCS_URL}}">

diff --git a/Services/Services_bases/telegram_service/telegram.py b/Services/Services_bases/telegram_service/telegram.py index cd39c33b6..68d9e227f 100644 --- a/Services/Services_bases/telegram_service/telegram.py +++ b/Services/Services_bases/telegram_service/telegram.py @@ -67,7 +67,7 @@ def get_read_only_info(self): @classmethod def get_help_page(cls) -> str: - return f"{constants.OCTOBOT_WEBSITE_URL}/guides/#telegram" + return f"{constants.OCTOBOT_DOCS_URL}/interfaces/telegram-interface" @staticmethod def is_setup_correctly(config): diff --git a/Services/Services_bases/web_service/web.py b/Services/Services_bases/web_service/web.py index 6541301c7..c1f866ff7 100644 --- a/Services/Services_bases/web_service/web.py +++ b/Services/Services_bases/web_service/web.py @@ -58,7 +58,7 @@ def get_required_config(self): @classmethod def get_help_page(cls) -> str: - return f"{constants.OCTOBOT_WEBSITE_URL}/#octobot" + return f"{constants.OCTOBOT_DOCS_URL}/interfaces/web-interface" @staticmethod def is_setup_correctly(config):