From 5c228e2dc5e0225b5dd738d4e23a79e9a650a40f Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Fri, 29 Dec 2023 19:40:08 +0100 Subject: [PATCH 01/15] [Exchanges] add get_account_id tests --- .../abstract_authenticated_exchange_tester.py | 14 ++++++++++++++ additional_tests/exchanges_tests/test_ascendex.py | 4 ++++ additional_tests/exchanges_tests/test_binance.py | 3 +++ .../exchanges_tests/test_binance_futures.py | 4 ++++ additional_tests/exchanges_tests/test_bingx.py | 4 ++++ additional_tests/exchanges_tests/test_bitget.py | 4 ++++ additional_tests/exchanges_tests/test_bybit.py | 4 ++++ .../exchanges_tests/test_bybit_futures.py | 4 ++++ additional_tests/exchanges_tests/test_coinbase.py | 4 ++++ additional_tests/exchanges_tests/test_cryptocom.py | 4 ++++ additional_tests/exchanges_tests/test_hollaex.py | 4 ++++ additional_tests/exchanges_tests/test_huobi.py | 4 ++++ additional_tests/exchanges_tests/test_kucoin.py | 3 +++ .../exchanges_tests/test_kucoin_futures.py | 3 +++ additional_tests/exchanges_tests/test_mexc.py | 4 ++++ additional_tests/exchanges_tests/test_okx.py | 3 +++ .../exchanges_tests/test_okx_futures.py | 3 +++ additional_tests/exchanges_tests/test_phemex.py | 4 ++++ 18 files changed, 77 insertions(+) diff --git a/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py b/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py index d688b41e5..bd8563025 100644 --- a/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py +++ b/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py @@ -69,6 +69,7 @@ class AbstractAuthenticatedExchangeTester: IGNORE_EXCHANGE_TRADE_ID = False # set True when trade.exchange_trade_id can't be set MAX_TRADE_USD_VALUE = decimal.Decimal(8000) MIN_TRADE_USD_VALUE = decimal.Decimal("0.1") + IS_ACCOUNT_ID_AVAILABLE = True # set False when get_account_id is not available and should be checked # Implement all "test_[name]" methods, call super() to run the test, pass to ignore it. # Override the "inner_test_[name]" method to override a test content. @@ -109,6 +110,19 @@ def check_portfolio_content(self, portfolio): at_least_one_value = True assert at_least_one_value + async def test_get_account_id(self): + async with self.local_exchange_manager(): + await self.inner_test_get_account_id() + + async def inner_test_get_account_id(self): + if self.IS_ACCOUNT_ID_AVAILABLE: + account_id = await self.exchange_manager.exchange.get_account_id() + assert account_id + assert isinstance(account_id, str) + else: + with pytest.raises(NotImplementedError): + await self.exchange_manager.exchange.get_account_id() + async def test_create_and_cancel_limit_orders(self): async with self.local_exchange_manager(): await self.inner_test_create_and_cancel_limit_orders() diff --git a/additional_tests/exchanges_tests/test_ascendex.py b/additional_tests/exchanges_tests/test_ascendex.py index a684fead7..4b2cf6fb1 100644 --- a/additional_tests/exchanges_tests/test_ascendex.py +++ b/additional_tests/exchanges_tests/test_ascendex.py @@ -39,6 +39,10 @@ async def test_get_portfolio_with_market_filter(self): # pass if not implemented pass + async def test_get_account_id(self): + # pass if not implemented + pass + async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() diff --git a/additional_tests/exchanges_tests/test_binance.py b/additional_tests/exchanges_tests/test_binance.py index 9d5fdcff2..c5d2cf152 100644 --- a/additional_tests/exchanges_tests/test_binance.py +++ b/additional_tests/exchanges_tests/test_binance.py @@ -38,6 +38,9 @@ async def test_get_portfolio(self): async def test_get_portfolio_with_market_filter(self): await super().test_get_portfolio_with_market_filter() + async def test_get_account_id(self): + await super().test_get_account_id() + async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() diff --git a/additional_tests/exchanges_tests/test_binance_futures.py b/additional_tests/exchanges_tests/test_binance_futures.py index 9828f4a33..c108e140d 100644 --- a/additional_tests/exchanges_tests/test_binance_futures.py +++ b/additional_tests/exchanges_tests/test_binance_futures.py @@ -34,6 +34,7 @@ class TestBinanceFuturesAuthenticatedExchange( INVERSE_SYMBOL = f"{ORDER_CURRENCY}/USD:{ORDER_CURRENCY}" ORDER_SIZE = 10 # % of portfolio to include in test orders DUPLICATE_TRADES_RATIO = 0.1 # allow 10% duplicate in trades (due to trade id set to order id) + IS_ACCOUNT_ID_AVAILABLE = False # set False when get_account_id is not available and should be checked async def _set_account_types(self, account_types): # todo remove this and use both types when exchange-side multi portfolio is enabled @@ -46,6 +47,9 @@ async def test_get_portfolio_with_market_filter(self): # pass if not implemented pass + async def test_get_account_id(self): + await super().test_get_account_id() + async def test_get_empty_linear_and_inverse_positions(self): await super().test_get_empty_linear_and_inverse_positions() diff --git a/additional_tests/exchanges_tests/test_bingx.py b/additional_tests/exchanges_tests/test_bingx.py index 01c5dea2a..3d74fec20 100644 --- a/additional_tests/exchanges_tests/test_bingx.py +++ b/additional_tests/exchanges_tests/test_bingx.py @@ -39,6 +39,10 @@ async def test_get_portfolio(self): async def test_get_portfolio_with_market_filter(self): await super().test_get_portfolio_with_market_filter() + async def test_get_account_id(self): + # pass if not implemented + pass + async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() diff --git a/additional_tests/exchanges_tests/test_bitget.py b/additional_tests/exchanges_tests/test_bitget.py index 218239c05..c65d0e0e2 100644 --- a/additional_tests/exchanges_tests/test_bitget.py +++ b/additional_tests/exchanges_tests/test_bitget.py @@ -41,6 +41,10 @@ async def test_get_portfolio_with_market_filter(self): # pass if not implemented pass + async def test_get_account_id(self): + # pass if not implemented + pass + async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() diff --git a/additional_tests/exchanges_tests/test_bybit.py b/additional_tests/exchanges_tests/test_bybit.py index 9d720e348..3698f9863 100644 --- a/additional_tests/exchanges_tests/test_bybit.py +++ b/additional_tests/exchanges_tests/test_bybit.py @@ -43,6 +43,10 @@ async def test_get_portfolio_with_market_filter(self): # pass if not implemented pass + async def test_get_account_id(self): + # pass if not implemented + pass + async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() diff --git a/additional_tests/exchanges_tests/test_bybit_futures.py b/additional_tests/exchanges_tests/test_bybit_futures.py index e017f34f6..df4bddfa1 100644 --- a/additional_tests/exchanges_tests/test_bybit_futures.py +++ b/additional_tests/exchanges_tests/test_bybit_futures.py @@ -44,6 +44,10 @@ async def test_get_portfolio_with_market_filter(self): # pass if not implemented pass + async def test_get_account_id(self): + # pass if not implemented + pass + async def test_get_empty_linear_and_inverse_positions(self): await super().test_get_empty_linear_and_inverse_positions() diff --git a/additional_tests/exchanges_tests/test_coinbase.py b/additional_tests/exchanges_tests/test_coinbase.py index 802754c97..6cb7ed0ee 100644 --- a/additional_tests/exchanges_tests/test_coinbase.py +++ b/additional_tests/exchanges_tests/test_coinbase.py @@ -39,6 +39,10 @@ async def test_get_portfolio_with_market_filter(self): # pass if not implemented pass + async def test_get_account_id(self): + # pass if not implemented + pass + async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() diff --git a/additional_tests/exchanges_tests/test_cryptocom.py b/additional_tests/exchanges_tests/test_cryptocom.py index a62a97822..5815b0993 100644 --- a/additional_tests/exchanges_tests/test_cryptocom.py +++ b/additional_tests/exchanges_tests/test_cryptocom.py @@ -39,6 +39,10 @@ async def test_get_portfolio_with_market_filter(self): # pass if not implemented pass + async def test_get_account_id(self): + # pass if not implemented + pass + async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() diff --git a/additional_tests/exchanges_tests/test_hollaex.py b/additional_tests/exchanges_tests/test_hollaex.py index 60406dafe..63719dd54 100644 --- a/additional_tests/exchanges_tests/test_hollaex.py +++ b/additional_tests/exchanges_tests/test_hollaex.py @@ -42,6 +42,10 @@ async def test_get_portfolio_with_market_filter(self): # pass if not implemented pass + async def test_get_account_id(self): + # pass if not implemented + pass + async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() diff --git a/additional_tests/exchanges_tests/test_huobi.py b/additional_tests/exchanges_tests/test_huobi.py index 231c1fa9f..42381f081 100644 --- a/additional_tests/exchanges_tests/test_huobi.py +++ b/additional_tests/exchanges_tests/test_huobi.py @@ -41,6 +41,10 @@ async def test_get_portfolio_with_market_filter(self): # pass if not implemented pass + async def test_get_account_id(self): + # pass if not implemented + pass + async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() diff --git a/additional_tests/exchanges_tests/test_kucoin.py b/additional_tests/exchanges_tests/test_kucoin.py index ffc79eab2..63725ec16 100644 --- a/additional_tests/exchanges_tests/test_kucoin.py +++ b/additional_tests/exchanges_tests/test_kucoin.py @@ -40,6 +40,9 @@ async def test_get_portfolio_with_market_filter(self): async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() + async def test_get_account_id(self): + await super().test_get_account_id() + async def test_create_and_fill_market_orders(self): await super().test_create_and_fill_market_orders() diff --git a/additional_tests/exchanges_tests/test_kucoin_futures.py b/additional_tests/exchanges_tests/test_kucoin_futures.py index 598590678..83aca21d3 100644 --- a/additional_tests/exchanges_tests/test_kucoin_futures.py +++ b/additional_tests/exchanges_tests/test_kucoin_futures.py @@ -42,6 +42,9 @@ async def test_get_portfolio_with_market_filter(self): # pass if not implemented pass + async def test_get_account_id(self): + await super().test_get_account_id() + async def test_get_empty_linear_and_inverse_positions(self): await super().test_get_empty_linear_and_inverse_positions() diff --git a/additional_tests/exchanges_tests/test_mexc.py b/additional_tests/exchanges_tests/test_mexc.py index 27da287cf..39bda1bdf 100644 --- a/additional_tests/exchanges_tests/test_mexc.py +++ b/additional_tests/exchanges_tests/test_mexc.py @@ -41,6 +41,10 @@ async def test_get_portfolio_with_market_filter(self): # pass if not implemented pass + async def test_get_account_id(self): + # pass if not implemented + pass + async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() diff --git a/additional_tests/exchanges_tests/test_okx.py b/additional_tests/exchanges_tests/test_okx.py index a387cf294..24ff343b5 100644 --- a/additional_tests/exchanges_tests/test_okx.py +++ b/additional_tests/exchanges_tests/test_okx.py @@ -40,6 +40,9 @@ async def test_get_portfolio_with_market_filter(self): async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() + async def test_get_account_id(self): + await super().test_get_account_id() + async def test_create_and_fill_market_orders(self): await super().test_create_and_fill_market_orders() diff --git a/additional_tests/exchanges_tests/test_okx_futures.py b/additional_tests/exchanges_tests/test_okx_futures.py index c95152535..56256dc89 100644 --- a/additional_tests/exchanges_tests/test_okx_futures.py +++ b/additional_tests/exchanges_tests/test_okx_futures.py @@ -41,6 +41,9 @@ async def test_get_portfolio_with_market_filter(self): # pass if not implemented pass + async def test_get_account_id(self): + await super().test_get_account_id() + async def test_get_empty_linear_and_inverse_positions(self): await super().test_get_empty_linear_and_inverse_positions() diff --git a/additional_tests/exchanges_tests/test_phemex.py b/additional_tests/exchanges_tests/test_phemex.py index a6c1be6be..763540e34 100644 --- a/additional_tests/exchanges_tests/test_phemex.py +++ b/additional_tests/exchanges_tests/test_phemex.py @@ -40,6 +40,10 @@ async def test_get_portfolio_with_market_filter(self): # pass if not implemented pass + async def test_get_account_id(self): + # pass if not implemented + pass + async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() From a9e512767240cd833f1e9406968de45c929a57d4 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Thu, 4 Jan 2024 00:21:58 +0100 Subject: [PATCH 02/15] [Constants] update guides links --- octobot/constants.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/octobot/constants.py b/octobot/constants.py index 2181deb5f..b93de1964 100644 --- a/octobot/constants.py +++ b/octobot/constants.py @@ -36,9 +36,9 @@ # OctoBot urls OCTOBOT_WEBSITE_URL = os.getenv("OCTOBOT_ONLINE_URL", "https://www.octobot.cloud") -OCTOBOT_DOCS_URL = os.getenv("DOCS_OCTOBOT_ONLINE_URL", "https://www.octobot.cloud/guides") -EXCHANGES_DOCS_URL = os.getenv("DOCS_OCTOBOT_ONLINE_URL", "https://www.octobot.cloud/guides/exchanges/") -DEVELOPER_DOCS_URL = os.getenv("DOCS_OCTOBOT_ONLINE_URL", "https://www.octobot.cloud/guides/developers/") +OCTOBOT_DOCS_URL = os.getenv("DOCS_OCTOBOT_ONLINE_URL", "https://www.octobot.cloud/en/guides") +EXCHANGES_DOCS_URL = os.getenv("DOCS_OCTOBOT_ONLINE_URL", "https://www.octobot.cloud/en/guides/exchanges/") +DEVELOPER_DOCS_URL = os.getenv("DOCS_OCTOBOT_ONLINE_URL", "https://www.octobot.cloud/en/guides/developers/") OCTOBOT_ONLINE = os.getenv("TENTACLES_OCTOBOT_ONLINE_URL", "octobot.online") OCTOBOT_FEEDBACK = os.getenv("FEEDBACK_OCTOBOT_ONLINE_URL", "https://feedback.octobot.cloud/") TENTACLES_REPOSITORY = "tentacles" From b4755d53a957b21a8b5d1bd9bda44d27f6aa8f18 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 10:28:23 +0100 Subject: [PATCH 03/15] [CCXT] update exchange tests for ccxt 4.2.10 --- .../abstract_authenticated_exchange_tester.py | 2 +- additional_tests/exchanges_tests/test_bitget.py | 2 ++ .../exchanges_tests/{test_huobi.py => test_htx.py} | 6 ++---- additional_tests/exchanges_tests/test_kucoin_futures.py | 2 +- additional_tests/exchanges_tests/test_phemex.py | 3 --- 5 files changed, 6 insertions(+), 9 deletions(-) rename additional_tests/exchanges_tests/{test_huobi.py => test_htx.py} (94%) diff --git a/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py b/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py index bd8563025..fbb31fc2b 100644 --- a/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py +++ b/additional_tests/exchanges_tests/abstract_authenticated_exchange_tester.py @@ -500,7 +500,7 @@ def check_raw_closed_orders(self, closed_orders): assert len(closed_orders) - len(incomplete_fees_orders) >= 2 def check_parsed_closed_order( - self, order: personal_data.Order, incomplete_fee_orders: list, allow_incomplete_fees: bool + self, order: personal_data.Order, incomplete_fee_orders: list, allow_incomplete_fees: bool ): assert order.symbol assert order.timestamp diff --git a/additional_tests/exchanges_tests/test_bitget.py b/additional_tests/exchanges_tests/test_bitget.py index c65d0e0e2..ffb860bc5 100644 --- a/additional_tests/exchanges_tests/test_bitget.py +++ b/additional_tests/exchanges_tests/test_bitget.py @@ -46,9 +46,11 @@ async def test_get_account_id(self): pass async def test_create_and_cancel_limit_orders(self): + # KYC needed 8th jan 2024 ccxt 4.2.10 await super().test_create_and_cancel_limit_orders() async def test_create_and_fill_market_orders(self): + # KYC needed 8th jan 2024 ccxt 4.2.10 await super().test_create_and_fill_market_orders() async def test_get_my_recent_trades(self): diff --git a/additional_tests/exchanges_tests/test_huobi.py b/additional_tests/exchanges_tests/test_htx.py similarity index 94% rename from additional_tests/exchanges_tests/test_huobi.py rename to additional_tests/exchanges_tests/test_htx.py index 42381f081..8ff2cecf7 100644 --- a/additional_tests/exchanges_tests/test_huobi.py +++ b/additional_tests/exchanges_tests/test_htx.py @@ -21,13 +21,11 @@ pytestmark = pytest.mark.asyncio -# 17/08/23 (untested with ccxt 4.0.65: -class _TestHuobiAuthenticatedExchange( +class TestHTXAuthenticatedExchange( abstract_authenticated_exchange_tester.AbstractAuthenticatedExchangeTester ): - # PASSED the 6th of aug 2023 # enter exchange name as a class variable here - EXCHANGE_NAME = "huobi" + EXCHANGE_NAME = "htx" ORDER_CURRENCY = "BTC" SETTLEMENT_CURRENCY = "USDT" SYMBOL = f"{ORDER_CURRENCY}/{SETTLEMENT_CURRENCY}" diff --git a/additional_tests/exchanges_tests/test_kucoin_futures.py b/additional_tests/exchanges_tests/test_kucoin_futures.py index 83aca21d3..a50e6c085 100644 --- a/additional_tests/exchanges_tests/test_kucoin_futures.py +++ b/additional_tests/exchanges_tests/test_kucoin_futures.py @@ -31,7 +31,7 @@ class TestKucoinFuturesAuthenticatedExchange( SETTLEMENT_CURRENCY = "USDT" SYMBOL = f"{ORDER_CURRENCY}/{SETTLEMENT_CURRENCY}:{SETTLEMENT_CURRENCY}" INVERSE_SYMBOL = f"{ORDER_CURRENCY}/USD:{ORDER_CURRENCY}" - ORDER_SIZE = 60 # % of portfolio to include in test orders + ORDER_SIZE = 90 # % of portfolio to include in test orders SUPPORTS_GET_LEVERAGE = False SUPPORTS_SET_LEVERAGE = False diff --git a/additional_tests/exchanges_tests/test_phemex.py b/additional_tests/exchanges_tests/test_phemex.py index 763540e34..47e694958 100644 --- a/additional_tests/exchanges_tests/test_phemex.py +++ b/additional_tests/exchanges_tests/test_phemex.py @@ -21,8 +21,6 @@ pytestmark = pytest.mark.asyncio -# 17/08/23 (untested with ccxt 4.0.65: -# ccxt.base.errors.ExchangeError: phemex {"msg":"Please try again later","code":10500}? class TestPemexAuthenticatedExchange( abstract_authenticated_exchange_tester.AbstractAuthenticatedExchangeTester ): @@ -48,7 +46,6 @@ async def test_create_and_cancel_limit_orders(self): await super().test_create_and_cancel_limit_orders() async def test_create_and_fill_market_orders(self): - # 08 dec 2022: did not run so far, testnet is bugged: order are apparently accepted but never show up await super().test_create_and_fill_market_orders() async def test_get_my_recent_trades(self): From dec5d7653bc5d0d2a8bf4e22482e899570f99969 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 00:19:31 +0100 Subject: [PATCH 04/15] [Requirements] bump --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index b2c9babb0..3406b5ca7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,12 +1,12 @@ # Drakkar-Software requirements OctoBot-Commons==1.9.35 -OctoBot-Trading==2.4.48 +OctoBot-Trading==2.4.49 OctoBot-Evaluators==1.9.3 OctoBot-Tentacles-Manager==2.9.6 OctoBot-Services==1.6.6 OctoBot-Backtesting==1.9.7 Async-Channel==2.2.1 -trading-backend==1.2.11 +trading-backend==1.2.12 ## Others colorlog==4.7.2 From fd892c2ff4b9e4410db87ce28d9b1beeb9a6c370 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 10:28:19 +0100 Subject: [PATCH 05/15] [Community] remove mqtt feed --- octobot/community/__init__.py | 2 - octobot/community/feeds/__init__.py | 5 - .../community/feeds/community_mqtt_feed.py | 355 ------------------ octobot/community/feeds/feed_factory.py | 6 +- requirements.txt | 1 - .../community/test_community_mqtt_feed.py | 160 -------- 6 files changed, 3 insertions(+), 526 deletions(-) delete mode 100644 octobot/community/feeds/community_mqtt_feed.py delete mode 100644 tests/unit_tests/community/test_community_mqtt_feed.py diff --git a/octobot/community/__init__.py b/octobot/community/__init__.py index 9a5df7627..e91abdd76 100644 --- a/octobot/community/__init__.py +++ b/octobot/community/__init__.py @@ -74,7 +74,6 @@ from octobot.community.feeds import ( AbstractFeed, CommunityWSFeed, - CommunityMQTTFeed, community_feed_factory, ) from octobot.community.errors_upload import ( @@ -126,6 +125,5 @@ "upsert_historical_bot_portfolio_query", "AbstractFeed", "CommunityWSFeed", - "CommunityMQTTFeed", "community_feed_factory", ] diff --git a/octobot/community/feeds/__init__.py b/octobot/community/feeds/__init__.py index e1bd2c4f8..599c6b2fe 100644 --- a/octobot/community/feeds/__init__.py +++ b/octobot/community/feeds/__init__.py @@ -22,10 +22,6 @@ from octobot.community.feeds.community_ws_feed import ( CommunityWSFeed, ) -from octobot.community.feeds import community_mqtt_feed -from octobot.community.feeds.community_mqtt_feed import ( - CommunityMQTTFeed, -) from octobot.community.feeds import community_supabase_feed from octobot.community.feeds.community_supabase_feed import ( CommunitySupabaseFeed, @@ -38,7 +34,6 @@ __all__ = [ "AbstractFeed", "CommunityWSFeed", - "CommunityMQTTFeed", "CommunitySupabaseFeed", "community_feed_factory", ] diff --git a/octobot/community/feeds/community_mqtt_feed.py b/octobot/community/feeds/community_mqtt_feed.py deleted file mode 100644 index 4b440565d..000000000 --- a/octobot/community/feeds/community_mqtt_feed.py +++ /dev/null @@ -1,355 +0,0 @@ -# This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot) -# Copyright (c) 2023 Drakkar-Software, All rights reserved. -# -# OctoBot is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either -# version 3.0 of the License, or (at your option) any later version. -# -# OctoBot is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public -# License along with OctoBot. If not, see . -import uuid -import gmqtt -import json -import zlib -import asyncio -import packaging.version as packaging_version - -import octobot_commons.enums as commons_enums -import octobot_commons.errors as commons_errors -import octobot_commons.constants as commons_constants -import octobot.community.errors as errors -import octobot.community.feeds.abstract_feed as abstract_feed -import octobot.constants as constants - - -class CommunityMQTTFeed(abstract_feed.AbstractFeed): - MQTT_VERSION = gmqtt.constants.MQTTv311 - MQTT_BROKER_PORT = 1883 - RECONNECT_DELAY = 15 - RECONNECT_ENSURE_DELAY = 1 - MAX_MESSAGE_ID_CACHE_SIZE = 100 - MAX_SUBSCRIPTION_ATTEMPTS = 5 - DISABLE_RECONNECT_VALUE = -2 - DEVICE_CREATE_TIMEOUT = 5 * commons_constants.MINUTE_TO_SECONDS - DEVICE_CREATION_REFRESH_DELAY = 2 - CONNECTION_TIMEOUT = 10 - - # Quality of Service level determines the reliability of the data flow between a client and a message broker. - # The message may be sent in three ways: - # QoS 0: the message will be received at most once (also known as “fire and forget”). - # QoS 1: the message will be received at least once. - # QoS 2: the message will be received exactly once. - # from https://www.scaleway.com/en/docs/iot/iot-hub/concepts/#quality-of-service-levels-(qos) - default_QOS = 1 - - def __init__(self, feed_url, authenticator): - super().__init__(feed_url, authenticator) - self.mqtt_version = self.MQTT_VERSION - self.mqtt_broker_port = self.MQTT_BROKER_PORT - self.default_QOS = self.default_QOS - self.subscribed = False - - self._mqtt_client: gmqtt.Client = None - self._valid_auth = True - self._disconnected = True - self._device_uuid: str = None - self._subscription_attempts = 0 - self._subscription_topics = set() - self._reconnect_task = None - self._connect_task = None - self._connected_at_least_once = False - self._processed_messages = [] - - async def start(self): - self.should_stop = False - self._device_uuid = self.authenticator.user_account.get_selected_bot_device_uuid() - try: - await self._connect() - if self.is_connected(): - self.logger.info("Successful connection request to mqtt device") - else: - self.logger.info("Failed to connect to mqtt device") - except asyncio.TimeoutError as err: - self.logger.exception(err, True, f"Timeout when connecting to mqtt device: {err}") - except Exception as err: - self.logger.exception(err, True, f"Unexpected error when connecting to mqtt device: {err}") - - async def stop(self): - self.logger.debug("Stopping ...") - self.should_stop = True - await self._stop_mqtt_client() - if self._reconnect_task is not None and not self._reconnect_task.done(): - self._reconnect_task.cancel() - if self._connect_task is not None and not self._connect_task.done(): - self._connect_task.cancel() - self._reset() - self.logger.debug("Stopped") - - async def restart(self): - try: - if not self.should_stop: - await self.stop() - await self.start() - except Exception as err: - self.logger.exception(err, True, f"{err}") - - def is_up_to_date_with_account(self, user_account): - try: - return self._device_uuid == user_account.get_selected_bot_device_uuid() - except errors.NoBotDeviceError: - return False - - def _reset(self): - self._connected_at_least_once = False - self._subscription_attempts = 0 - self._connect_task = None - self._valid_auth = True - self._disconnected = True - - async def _stop_mqtt_client(self): - if self.is_connected(): - await self._mqtt_client.disconnect() - - def is_connected(self): - return self._mqtt_client is not None and self._mqtt_client.is_connected and not self._disconnected - - def is_connected_to_remote_feed(self): - return self.subscribed - - def can_connect(self): - return self._valid_auth - - async def register_feed_callback(self, channel_type, callback, identifier=None): - self.is_signal_receiver = True - topic = self._build_topic(channel_type, identifier) - try: - self.feed_callbacks[topic].append(callback) - except KeyError: - self.feed_callbacks[topic] = [callback] - if topic not in self._subscription_topics: - self._subscription_topics.add(topic) - if self._valid_auth: - self._subscribe((topic, )) - else: - self.logger.error(f"Can't subscribe to {channel_type.name} feed, invalid authentication") - - def remove_device_details(self): - self._device_uuid = None - - @staticmethod - def _build_topic(channel_type, identifier): - return f"{channel_type.value}/{identifier}" - - async def _on_message(self, client, topic, payload, qos, properties): - try: - uncompressed_payload = zlib.decompress(payload).decode() - self.logger.debug(f"Received message, client_id: {client._client_id}, topic: {topic}, " - f"uncompressed payload: {uncompressed_payload}, QOS: {qos}, properties: {properties}") - parsed_message = json.loads(uncompressed_payload) - except Exception as err: - self.logger.exception(err, True, f"Unexpected error when reading message: {err}") - return - await self._process_message(topic, parsed_message) - - async def _process_message(self, topic, parsed_message): - try: - self._ensure_supported(parsed_message) - if self._should_process(parsed_message): - self.update_last_message_time() - for callback in self._get_callbacks(topic): - await callback(parsed_message) - except commons_errors.UnsupportedError as err: - self.logger.error(f"Unsupported message: {err}") - except Exception as err: - self.logger.exception(err, True, f"Unexpected error when processing message: {err}") - - def _should_process(self, parsed_message): - if parsed_message[commons_enums.CommunityFeedAttrs.ID.value] in self._processed_messages: - self.logger.debug(f"Ignored already processed message with id: " - f"{parsed_message[commons_enums.CommunityFeedAttrs.ID.value]}") - return False - self._processed_messages.append(parsed_message[commons_enums.CommunityFeedAttrs.ID.value]) - if len(self._processed_messages) > self.MAX_MESSAGE_ID_CACHE_SIZE: - self._processed_messages = [ - message_id - for message_id in self._processed_messages[self.MAX_MESSAGE_ID_CACHE_SIZE // 2:] - ] - return True - - async def send(self, message, channel_type, identifier, **kwargs): - self.is_signal_emitter = True - if not self._valid_auth: - self.logger.warning(f"Can't send {channel_type.name}, invalid feed authentication.") - return - topic = self._build_topic(channel_type, identifier) - self.logger.info(f"Sending message on topic: {topic}, message: {message}") - self._mqtt_client.publish( - self._build_topic(channel_type, identifier), - self._build_message(channel_type, message), - qos=self.default_QOS - ) - - def _get_callbacks(self, topic): - for callback in self.feed_callbacks.get(topic, ()): - yield callback - - def _get_channel_type(self, message): - return commons_enums.CommunityChannelTypes(message[commons_enums.CommunityFeedAttrs.CHANNEL_TYPE.value]) - - def _build_message(self, channel_type, message): - if message: - return zlib.compress( - json.dumps({ - commons_enums.CommunityFeedAttrs.CHANNEL_TYPE.value: channel_type.value, - commons_enums.CommunityFeedAttrs.VERSION.value: constants.COMMUNITY_FEED_CURRENT_MINIMUM_VERSION, - commons_enums.CommunityFeedAttrs.VALUE.value: message, - commons_enums.CommunityFeedAttrs.ID.value: str(uuid.uuid4()), # assign unique id to each message - }).encode() - ) - return {} - - def _ensure_supported(self, parsed_message): - if packaging_version.Version(parsed_message[commons_enums.CommunityFeedAttrs.VERSION.value]) \ - < packaging_version.Version(constants.COMMUNITY_FEED_CURRENT_MINIMUM_VERSION): - raise commons_errors.UnsupportedError( - f"Minimum version: {constants.COMMUNITY_FEED_CURRENT_MINIMUM_VERSION}" - ) - - def _on_connect(self, client, flags, rc, properties): - self._disconnected = False - self.logger.info(f"Connected, client_id: {client._client_id}") - # There are no subscription when we just connected - self.subscribed = False - # Auto subscribe to known topics (mainly used in case of reconnection) - self._subscribe(self._subscription_topics) - - def _try_reconnect_if_necessary(self, client): - if self._reconnect_task is None or self._reconnect_task.done(): - self.logger.debug("Trying to reconnect") - self._reconnect_task = asyncio.create_task(self._reconnect(client)) - else: - self.logger.debug("A reconnect task is already running") - - async def _reconnect(self, client): - try: - try: - await self._stop_mqtt_client() - except Exception as e: - self.logger.debug(f"Ignored error while stopping client: {e}.") - attempt = 1 - while not self.should_stop: - delay = 0 if attempt == 1 else self.RECONNECT_DELAY - error = None - try: - self.logger.info(f"Reconnecting, client_id: {client._client_id} (attempt {attempt})") - await self._connect() - await asyncio.sleep(self.RECONNECT_ENSURE_DELAY) - if self.is_connected(): - self.logger.info(f"Reconnected, client_id: {client._client_id}") - return - error = "failed to connect" - except Exception as err: - error = f"{err}" - finally: - self.logger.debug(f"Reconnect attempt {attempt} {'succeeded' if error is None else 'failed'}.") - attempt += 1 - self.logger.debug(f"Error while reconnecting: {error}. Trying again in {delay} seconds.") - await asyncio.sleep(delay) - finally: - self.logger.debug("Reconnect task complete") - - def _on_disconnect(self, client, packet, exc=None): - self._disconnected = True - self.subscribed = False - if self.should_stop: - self.logger.info(f"Disconnected after stop call") - else: - if self._connected_at_least_once: - self.logger.info(f"Disconnected, client_id: {client._client_id}") - self._try_reconnect_if_necessary(client) - else: - if self._connect_task is not None and not self._connect_task.done(): - self._connect_task.cancel() - - def _on_subscribe(self, client, mid, qos, properties): - # from https://github.com/wialon/gmqtt/blob/master/examples/resubscription.py#L28 - # in order to check if all the subscriptions were successful, we should first get all subscriptions with this - # particular mid (from one subscription request) - subscriptions = client.get_subscriptions_by_mid(mid) - for subscription, granted_qos in zip(subscriptions, qos): - # in case of bad suback code, we can resend subscription - if granted_qos >= gmqtt.constants.SubAckReasonCode.UNSPECIFIED_ERROR.value: - self.logger.warning(f"Retrying subscribe to {[s.topic for s in subscriptions]}, " - f"client_id: {client._client_id}, mid: {mid}, " - f"reason code: {granted_qos}, properties {properties}") - if self._subscription_attempts < self.MAX_SUBSCRIPTION_ATTEMPTS * len(subscriptions): - self._subscription_attempts += 1 - client.resubscribe(subscription) - else: - self.logger.error(f"Max subscription attempts reached, stopping subscription " - f"to {[s.topic for s in subscriptions]}. Are you subscribing to this " - f"strategy on your OctoBot account ?") - self.subscribed = False - return - else: - self._subscription_attempts = 0 - self.subscribed = True - self.logger.info(f"Subscribed, client_id: {client._client_id}, mid {mid}, QOS: {granted_qos}, " - f"properties {properties}") - - def _register_callbacks(self, client): - client.on_connect = self._on_connect - client.on_message = self._on_message - client.on_disconnect = self._on_disconnect - client.on_subscribe = self._on_subscribe - - def _update_client_config(self, client): - default_config = gmqtt.constants.DEFAULT_CONFIG - # prevent default auto-reconnect as it loop infinitely on windows long disconnections - default_config.update({ - 'reconnect_retries': self.DISABLE_RECONNECT_VALUE, - }) - client.set_config(default_config) - - async def _connect(self): - if self._device_uuid is None: - self._valid_auth = False - raise errors.BotError("mqtt device uuid is None, impossible to connect client") - self._mqtt_client = gmqtt.Client(self.__class__.__name__) - self._update_client_config(self._mqtt_client) - self._register_callbacks(self._mqtt_client) - self._mqtt_client.set_auth_credentials(self._device_uuid, None) - self.logger.debug(f"Connecting client using device " - f"'{self.authenticator.user_account.get_selected_bot_device_name()}'") - self._connect_task = asyncio.create_task( - self._mqtt_client.connect(self.feed_url, self.mqtt_broker_port, version=self.MQTT_VERSION) - ) - try: - await asyncio.wait_for(self._connect_task, self.CONNECTION_TIMEOUT) - self._connected_at_least_once = True - except asyncio.CancelledError: - # got cancelled by on_disconnect, can't connect - self.logger.error(f"Can't connect to server, make sure that your device uuid is valid. " - f"Current mqtt uuid is: {self._device_uuid}") - self._valid_auth = False - except asyncio.TimeoutError as err: - message = "Timeout error when trying to connect to mqtt device" - self.logger.debug(message) - raise asyncio.TimeoutError(message) from err - - def _subscribe(self, topics): - if not topics: - self.logger.debug("No topic to subscribe to, skipping subscribe for now") - return - subscriptions = [ - gmqtt.Subscription(topic, qos=self.default_QOS) - for topic in topics - ] - self.logger.debug(f"Subscribing to {', '.join(topics)}") - self._mqtt_client.subscribe(subscriptions) diff --git a/octobot/community/feeds/feed_factory.py b/octobot/community/feeds/feed_factory.py index 1a6ba0398..033000ba1 100644 --- a/octobot/community/feeds/feed_factory.py +++ b/octobot/community/feeds/feed_factory.py @@ -15,15 +15,15 @@ # License along with OctoBot. If not, see . import octobot.enums import octobot.community.feeds.community_ws_feed as community_ws_feed -import octobot.community.feeds.community_mqtt_feed as community_mqtt_feed import octobot.community.feeds.community_supabase_feed as community_supabase_feed def community_feed_factory(feed_url: str, authenticator, feed_type: octobot.enums.CommunityFeedType): if feed_type is octobot.enums.CommunityFeedType.WebsocketFeed: return community_ws_feed.CommunityWSFeed(feed_url, authenticator) - if feed_type is octobot.enums.CommunityFeedType.MQTTFeed: - return community_mqtt_feed.CommunityMQTTFeed(feed_url, authenticator) + # disabled + # if feed_type is octobot.enums.CommunityFeedType.MQTTFeed: + # return community_mqtt_feed.CommunityMQTTFeed(feed_url, authenticator) if feed_type is octobot.enums.CommunityFeedType.SupabaseFeed: return community_supabase_feed.CommunitySupabaseFeed(feed_url, authenticator) raise NotImplementedError(f"Unsupported feed type: {feed_type}") diff --git a/requirements.txt b/requirements.txt index 3406b5ca7..e5aa6072a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,7 +19,6 @@ setuptools<65.6 # Added because the distutils.log.Log class was removed in setup # Community websockets -gmqtt==0.6.11 # Supabase ensure supabase_backend_tests keep passing when updating any of those supabase==1.0.4 # Supabase client diff --git a/tests/unit_tests/community/test_community_mqtt_feed.py b/tests/unit_tests/community/test_community_mqtt_feed.py deleted file mode 100644 index 914cd1018..000000000 --- a/tests/unit_tests/community/test_community_mqtt_feed.py +++ /dev/null @@ -1,160 +0,0 @@ -# This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot) -# Copyright (c) 2023 Drakkar-Software, All rights reserved. -# -# OctoBot is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either -# version 3.0 of the License, or (at your option) any later version. -# -# OctoBot is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public -# License along with OctoBot. If not, see . -import pytest -import pytest_asyncio -import mock -import uuid -import json -import zlib -import gmqtt - -import octobot.community as community -import octobot.constants as constants -import octobot_commons.enums as commons_enums - -# All test coroutines will be treated as marked. -pytestmark = pytest.mark.asyncio - -FEED_URL = "x.y.z" -TOKEN = "acb1" -NAME = "name_a" - - -def _build_message(value, identifier): - return { - commons_enums.CommunityFeedAttrs.CHANNEL_TYPE.value: commons_enums.CommunityChannelTypes.SIGNAL.value, - commons_enums.CommunityFeedAttrs.VERSION.value: constants.COMMUNITY_FEED_CURRENT_MINIMUM_VERSION, - commons_enums.CommunityFeedAttrs.VALUE.value: value, - commons_enums.CommunityFeedAttrs.ID.value: identifier, - } - - -def _zipped_message(value): - return zlib.compress( - json.dumps(value).encode() - ) - - -@pytest_asyncio.fixture -async def authenticator(): - community.IdentifiersProvider.use_production() - auth = community.CommunityAuthentication(None, None) - auth._auth_token = TOKEN - auth.refresh_token = TOKEN - auth._expire_at = 11 - return auth - - -@pytest_asyncio.fixture -async def connected_community_feed(authenticator): - feed = None - try: - feed = community.CommunityMQTTFeed(FEED_URL, authenticator) - feed.INIT_TIMEOUT = 1 - with mock.patch.object(authenticator.user_account, "get_selected_bot_device_uuid", mock.Mock(return_value=TOKEN)) \ - as get_selected_bot_device_uuid_mock, \ - mock.patch.object(authenticator.user_account, "get_selected_bot_device_name", mock.Mock(return_value=NAME) - ) as _get_selected_bot_device_uuid_mock, \ - mock.patch.object(feed, "_subscribe", mock.Mock()) as _subscribe_mock, \ - mock.patch.object(gmqtt.Client, "connect", mock.AsyncMock()) as _connect_mock: - await feed.register_feed_callback(commons_enums.CommunityChannelTypes.SIGNAL, mock.AsyncMock()) - _subscribe_mock.assert_called_once_with((f"{commons_enums.CommunityChannelTypes.SIGNAL.value}/None", )) - await feed.start() - get_selected_bot_device_uuid_mock.assert_called_once() - _connect_mock.assert_called_once_with(FEED_URL, feed.mqtt_broker_port, version=feed.MQTT_VERSION) - yield feed - finally: - if feed is not None: - await feed.stop() - if feed._mqtt_client is not None and not feed._mqtt_client._resend_task.done(): - feed._mqtt_client._resend_task.cancel() - - -async def test_start_and_connect(connected_community_feed): - # connected_community_feed already called _connect, check that everything has been set - assert isinstance(connected_community_feed._mqtt_client, gmqtt.Client) - assert connected_community_feed._mqtt_client._config["reconnect_retries"] == \ - connected_community_feed.DISABLE_RECONNECT_VALUE - assert connected_community_feed._mqtt_client.on_connect == connected_community_feed._on_connect - assert connected_community_feed._mqtt_client.on_message == connected_community_feed._on_message - assert connected_community_feed._mqtt_client.on_disconnect == connected_community_feed._on_disconnect - assert connected_community_feed._mqtt_client.on_subscribe == connected_community_feed._on_subscribe - assert connected_community_feed._mqtt_client._username == TOKEN.encode() - - -async def test_stop(connected_community_feed): - connected_community_feed._reconnect_task = None - with mock.patch.object(connected_community_feed, "_stop_mqtt_client", mock.AsyncMock()) as _stop_mqtt_client_mock: - await connected_community_feed.stop() - _stop_mqtt_client_mock.assert_called_once() - - _stop_mqtt_client_mock.reset_mock() - connected_community_feed._reconnect_task = mock.Mock(done=mock.Mock(return_value=True), cancel=mock.Mock()) - await connected_community_feed.stop() - _stop_mqtt_client_mock.assert_called_once() - connected_community_feed._reconnect_task.done.assert_called_once() - connected_community_feed._reconnect_task.cancel.assert_not_called() - - _stop_mqtt_client_mock.reset_mock() - connected_community_feed._reconnect_task = mock.Mock(done=mock.Mock(return_value=False), cancel=mock.Mock()) - await connected_community_feed.stop() - _stop_mqtt_client_mock.assert_called_once() - connected_community_feed._reconnect_task.done.assert_called_once() - connected_community_feed._reconnect_task.cancel.assert_called_once() - - -async def test_register_feed_callback(connected_community_feed): - # TODO - pass - - -async def test_on_message(connected_community_feed): - client = mock.Mock(client_id="1") - topic = "topic" - connected_community_feed.feed_callbacks["topic"] = [mock.AsyncMock(), mock.AsyncMock()] - - message = _build_message("hello", "1") - # from topic - await connected_community_feed._on_message(client, "other_topic", _zipped_message(message), 1, {}) - assert all(cb.assert_not_called() is None for cb in connected_community_feed.feed_callbacks["topic"]) - - message = _build_message("hello", "2") - # call callbacks - await connected_community_feed._on_message(client, topic, _zipped_message(message), 1, {}) - assert all(cb.assert_called_once_with(message) is None for cb in connected_community_feed.feed_callbacks["topic"]) - - # already processed message - connected_community_feed.feed_callbacks["topic"][0].reset_mock() - connected_community_feed.feed_callbacks["topic"][1].reset_mock() - await connected_community_feed._on_message(client, topic, _zipped_message(message), 1, {}) - assert all(cb.assert_not_called() is None for cb in connected_community_feed.feed_callbacks["topic"]) - - -async def test_send(connected_community_feed): - with mock.patch.object(connected_community_feed._mqtt_client, "publish", mock.Mock()) as publish_mock, \ - mock.patch.object(uuid, "uuid4", mock.Mock(return_value="uuid41")) as uuid4_mock: - await connected_community_feed.send("hello", commons_enums.CommunityChannelTypes.SIGNAL, "x") - publish_mock.assert_called_once_with( - f"{commons_enums.CommunityChannelTypes.SIGNAL.value}/x", - zlib.compress(json.dumps({ - commons_enums.CommunityFeedAttrs.CHANNEL_TYPE.value: commons_enums.CommunityChannelTypes.SIGNAL.value, - commons_enums.CommunityFeedAttrs.VERSION.value: constants.COMMUNITY_FEED_CURRENT_MINIMUM_VERSION, - commons_enums.CommunityFeedAttrs.VALUE.value: "hello", - commons_enums.CommunityFeedAttrs.ID.value: "uuid41", # assign unique id to each message - }).encode()), - qos=connected_community_feed.default_QOS - ) - uuid4_mock.assert_called_once() From f1c9b018d2d99d9335918b4f95dfb4c66e3ed0d1 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 10:28:43 +0100 Subject: [PATCH 06/15] [Config] update schema --- octobot/config/config_schema.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/octobot/config/config_schema.json b/octobot/config/config_schema.json index 62386ca59..47ac2df0b 100644 --- a/octobot/config/config_schema.json +++ b/octobot/config/config_schema.json @@ -83,6 +83,9 @@ "trades": { "type": "boolean" }, + "other": { + "type": "boolean" + }, "notification-type": { "type": "array", "items": { From 469accbd9a14aa623c8c5f7190741699ed052ae2 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 10:28:57 +0100 Subject: [PATCH 07/15] [Dependencies] update --- requirements.txt | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index e5aa6072a..dfa18de50 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,13 +9,11 @@ Async-Channel==2.2.1 trading-backend==1.2.12 ## Others -colorlog==4.7.2 -yarl==1.7.2 -idna<2.9,>=2.5 -requests==2.25.1 -packaging==21.3 -python-dotenv==0.21.0 -setuptools<65.6 # Added because the distutils.log.Log class was removed in setuptools >= 65.6. Should be remove when bumping numpy. +colorlog==6.8.0 +requests==2.31.0 +packaging==23.2 +python-dotenv==1.0.0 +setuptools==69.0.3 # Community websockets From 694d8f88c81e336a17504b85a49e92a109fda889 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 12:38:24 +0100 Subject: [PATCH 08/15] [Requirements] bump --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index dfa18de50..64885d9c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ # Drakkar-Software requirements -OctoBot-Commons==1.9.35 +OctoBot-Commons==1.9.36 OctoBot-Trading==2.4.49 -OctoBot-Evaluators==1.9.3 -OctoBot-Tentacles-Manager==2.9.6 -OctoBot-Services==1.6.6 +OctoBot-Evaluators==1.9.4 +OctoBot-Tentacles-Manager==2.9.7 +OctoBot-Services==1.6.7 OctoBot-Backtesting==1.9.7 Async-Channel==2.2.1 trading-backend==1.2.12 From 5b9488b14a1213d2a8a9511c2d876f35d0dd01ed Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 16:08:10 +0100 Subject: [PATCH 09/15] [Readme] update guides link --- CHANGELOG.md | 2 +- README.md | 40 ++++++++++++++++++++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2914babd6..d61cab5fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,7 +47,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [Webhook] Support Ngrok custom domains ### Updated - [ChatGPT] Default GPT Trading profile now uses the DCA trading mode -- [TradingView] Revamped docs on https://www.octobot.cloud/guides/octobot-interfaces/tradingview +- [TradingView] Revamped docs on https://www.octobot.cloud/en/guides/octobot-interfaces/tradingview - [DCATrading] Improved error messages - [WebInterface] Do not select duplicated profiles by default - [DataCollector] Make errors clearer diff --git a/README.md b/README.md index 8cea016b9..73500c764 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ We are looking forward to receiving your feedback on our new OctoBot based syste ## What is Octobot ?

- + Follow your profits using OctoBot directly from its web interface      - + Follow each trade and profits of your OctoBot and send it commands from telegram

@@ -44,22 +44,22 @@ We are looking forward to receiving your feedback on our new OctoBot based syste [Octobot](https://www.octobot.cloud/?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=readme_what_is_octobot) is a powerful open-source cryptocurrency trading robot. OctoBot is highly customizable using its configuration and tentacles system. -You can build your own bot using the infinite [configuration](https://www.octobot.cloud/guides/octobot-configuration/profile-configuration?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=configuration) possibilities such as **technical analysis**, **social media processing** or even **external statistics management** like google trends. +You can build your own bot using the infinite [configuration](https://www.octobot.cloud/en/guides/octobot-configuration/profile-configuration?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=configuration) possibilities such as **technical analysis**, **social media processing** or even **external statistics management** like google trends. OctoBot is **AI ready**: Python being the main language for OctoBot, it's easy to integrate machine-learning libraries such as [Tensorflow](https://github.com/tensorflow/tensorflow) or any other libraries and take advantage of all the available data and create a very powerful trading strategy. Octobot's main feature is **evolution**, you can : - Share your configurations with other octobot users. -- [Install](https://www.octobot.cloud/guides/octobot-advanced-usage/tentacle-manager?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=install_tentacles), [modify](https://www.octobot.cloud/guides/octobot-tentacles-development/create-a-tentacle?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=modify_tentacles) and even [create](https://www.octobot.cloud/guides/octobot-tentacles-development/create-a-tentacle?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=create_tentacles) new tentacles to build your ideal cryptocurrency trading robot. -- [Contribute](https://www.octobot.cloud/guides/octobot-developers-environment/setup-your-environment?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=contribute) to improve OctoBot core repositories and tentacles. +- [Install](https://www.octobot.cloud/en/guides/octobot-advanced-usage/tentacle-manager?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=install_tentacles), [modify](https://www.octobot.cloud/en/guides/octobot-tentacles-development/create-a-tentacle?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=modify_tentacles) and even [create](https://www.octobot.cloud/en/guides/octobot-tentacles-development/create-a-tentacle?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=create_tentacles) new tentacles to build your ideal cryptocurrency trading robot. +- [Contribute](https://www.octobot.cloud/en/guides/octobot-developers-environment/setup-your-environment?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=contribute) to improve OctoBot core repositories and tentacles. -Looking for more info ? Check out our Octobot guides at [octobot.cloud/guides](https://www.octobot.cloud/guides/?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=checkout_guides) +Looking for more info ? Check out our Octobot guides at [octobot.cloud/guides](https://www.octobot.cloud/en/guides/?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=checkout_guides) ## Installation OctoBot's installation is **very simple**, you can either: - [Deploy your OctoBot on OctoBot Cloud](https://octobot.cloud/?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=readme_deploy_on_cloud). With OctoBot cloud, experience hassle-free installation, updates, and maintenance - leave it all to us! Your robot will also benefit from cloud only features. -- [Download and install](https://www.octobot.cloud/guides/octobot-installation/local?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=readme_local_installation) OctoBot on your computer or server and enjoy all features for free. -- Install OctoBot [using docker](https://www.octobot.cloud/guides/octobot-installation/with-docker?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=readme_docker_installation). +- [Download and install](https://www.octobot.cloud/en/guides/octobot-installation/local?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=readme_local_installation) OctoBot on your computer or server and enjoy all features for free. +- Install OctoBot [using docker](https://www.octobot.cloud/en/guides/octobot-installation/with-docker?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=readme_docker_installation). Docker install in one line summary: ``` @@ -68,18 +68,18 @@ OctoBot's installation is **very simple**, you can either: Your OctoBot will be accessible on [http://localhost](http://localhost). ## Exchanges -[![Binance supported exchange partnership](../assets/binance-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/binance?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=binance) -[![Okx supported exchange partnership](../assets/okex-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/okx?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=okx) -[![Kucoin supported exchange partnership](../assets/kucoin-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/kucoin?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=kucoin) -[![Crypto.com supported exchange partnership](../assets/cryptocom-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/crypto-com?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=crypto-com) -[![Huobi supported exchange partnership](../assets/huobi-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/huobi?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=huobi) -[![Hollaex supported exchange partnership](../assets/hollaex-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/hollaex?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=hollaex) -[![Coinbase supported exchange](../assets/coinbasepro-logo.png)](https://www.octobot.cloud/guides/octobot-supported-exchanges/coinbase?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=coinbase) -[![GateIO supported exchange partnership](../assets/gateio-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/gateio?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=gateio) -[![Ascendex supported exchange partnership](../assets/ascendex-logo.png)](https://www.octobot.cloud/guides/octobot-partner-exchanges/ascendex?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=ascendex) +[![Binance supported exchange partnership](../assets/binance-logo.png)](https://www.octobot.cloud/en/guides/octobot-partner-exchanges/binance?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=binance) +[![Okx supported exchange partnership](../assets/okex-logo.png)](https://www.octobot.cloud/en/guides/octobot-partner-exchanges/okx?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=okx) +[![Kucoin supported exchange partnership](../assets/kucoin-logo.png)](https://www.octobot.cloud/en/guides/octobot-partner-exchanges/kucoin?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=kucoin) +[![Crypto.com supported exchange partnership](../assets/cryptocom-logo.png)](https://www.octobot.cloud/en/guides/octobot-partner-exchanges/crypto-com?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=crypto-com) +[![Huobi supported exchange partnership](../assets/huobi-logo.png)](https://www.octobot.cloud/en/guides/octobot-partner-exchanges/huobi?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=huobi) +[![Hollaex supported exchange partnership](../assets/hollaex-logo.png)](https://www.octobot.cloud/en/guides/octobot-partner-exchanges/hollaex?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=hollaex) +[![Coinbase supported exchange](../assets/coinbasepro-logo.png)](https://www.octobot.cloud/en/guides/octobot-supported-exchanges/coinbase?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=coinbase) +[![GateIO supported exchange partnership](../assets/gateio-logo.png)](https://www.octobot.cloud/en/guides/octobot-partner-exchanges/gateio?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=gateio) +[![Ascendex supported exchange partnership](../assets/ascendex-logo.png)](https://www.octobot.cloud/en/guides/octobot-partner-exchanges/ascendex?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=ascendex) -Octobot supports many [exchanges](https://www.octobot.cloud/guides/exchanges?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=exchanges) thanks to the [ccxt library](https://github.com/ccxt/ccxt). -To activate trading on an exchange, just configure OctoBot with your API keys as described [on the exchange setup guides](https://www.octobot.cloud/guides/octobot-configuration/exchanges?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=exchanges_setup_guides). +Octobot supports many [exchanges](https://www.octobot.cloud/en/guides/exchanges?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=exchanges) thanks to the [ccxt library](https://github.com/ccxt/ccxt). +To activate trading on an exchange, just configure OctoBot with your API keys as described [on the exchange setup guides](https://www.octobot.cloud/en/guides/octobot-configuration/exchanges?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=exchanges_setup_guides). ### Paper trading @@ -88,7 +88,7 @@ To trade on any exchange, just enable the exchange in your OctoBot. This you to No exchange credential is required. ### Real trading -To use your real exchange account with OctoBot, enter your exchange API keys as described [on the exchange guides](https://www.octobot.cloud/guides/exchanges?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=exchanges_guides). +To use your real exchange account with OctoBot, enter your exchange API keys as described [on the exchange guides](https://www.octobot.cloud/en/guides/exchanges?utm_source=github&utm_medium=dk&utm_campaign=regular_open_source_content&utm_content=exchanges_guides). ## Testing trading strategies From 9a695d9077defc37313e5eeb81aa6e7c48500912 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 16:22:45 +0100 Subject: [PATCH 10/15] [Version] v1.0.6 --- CHANGELOG.md | 16 ++++++++++++++++ README.md | 2 +- octobot/__init__.py | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d61cab5fc..8c5c10912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 *It is strongly advised to perform an update of your tentacles after updating OctoBot. (start.py tentacles --install --all)* +## [1.0.6] - 2024-01-09 +### Added +- [TradingModes] Improved documentation and added links to full guides +- [InstantMAEvaluator] Add trigger threshold to avoid triggering at each price update +### Updated +- [CCXT] update to ccxt 4.2.10 +- [ChatGPT] update to openai 1.7.0 +- [DailyTradingMode] Enable futures position increase: add warning +### Fixed +- [DailyTradingMode] handle invalid MAX_CURRENCY_RATIO +- [TradingView] Fix SIGNAL=CANCEL docs typo +- [Exchanges] MEXC orders synchronization issues +- [Exchanges] HTX renamed Huobi into HTX +### Removed +- [Exchanges] Bittrex + ## [1.0.5] - 2023-12-19 ### Added - [GPTEvaluator] Settings to limit used tokens and disable re-evaluation diff --git a/README.md b/README.md index 73500c764..69b34c931 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# OctoBot [1.0.5](https://octobot.click/gh-changelog) +# OctoBot [1.0.6](https://octobot.click/gh-changelog) [![PyPI](https://img.shields.io/pypi/v/OctoBot.svg?logo=pypi)](https://octobot.click/gh-pypi) [![Downloads](https://pepy.tech/badge/octobot/month)](https://pepy.tech/project/octobot) [![Dockerhub](https://img.shields.io/docker/pulls/drakkarsoftware/octobot.svg?logo=docker)](https://octobot.click/gh-dockerhub) diff --git a/octobot/__init__.py b/octobot/__init__.py index 181734bb6..c25b9213e 100644 --- a/octobot/__init__.py +++ b/octobot/__init__.py @@ -16,5 +16,5 @@ PROJECT_NAME = "OctoBot" AUTHOR = "Drakkar-Software" -VERSION = "1.0.5" # major.minor.revision +VERSION = "1.0.6" # major.minor.revision LONG_VERSION = f"{VERSION}" From 0b00726bc6143d3f90e7541053e091077e7a704c Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Tue, 9 Jan 2024 22:38:00 +0100 Subject: [PATCH 11/15] [Backtesting] skip openai proxies in memory check --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 64885d9c1..9ebfaafc7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ OctoBot-Commons==1.9.36 OctoBot-Trading==2.4.49 OctoBot-Evaluators==1.9.4 OctoBot-Tentacles-Manager==2.9.7 -OctoBot-Services==1.6.7 +OctoBot-Services==1.6.9 OctoBot-Backtesting==1.9.7 Async-Channel==2.2.1 trading-backend==1.2.12 From 574aac5380947f6e2781c3372dc43b5408ef56ef Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Wed, 10 Jan 2024 13:10:03 +0100 Subject: [PATCH 12/15] [Community] add !inner check --- octobot/community/supabase_backend/community_supabase_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octobot/community/supabase_backend/community_supabase_client.py b/octobot/community/supabase_backend/community_supabase_client.py index bedcdba23..0db75b30b 100644 --- a/octobot/community/supabase_backend/community_supabase_client.py +++ b/octobot/community/supabase_backend/community_supabase_client.py @@ -154,7 +154,7 @@ async def fetch_bot(self, bot_id) -> dict: raise errors.BotNotFoundError(f"Can't find bot with id: {bot_id}") async def fetch_bots(self) -> list: - return (await self.table("bots").select("*,bot_deployment:bot_deployments!bots_current_deployment_id_fkey(*)").execute()).data + return (await self.table("bots").select("*,bot_deployment:bot_deployments!bots_current_deployment_id_fkey!inner(*)").execute()).data async def create_bot(self, deployment_type: enums.DeploymentTypes) -> dict: created_bot = (await self.table("bots").insert({ From 53eca65b8fa7ba3a96f1e8bcd40459d57c3a9cab Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Wed, 10 Jan 2024 13:10:34 +0100 Subject: [PATCH 13/15] [Requirements] bump services --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9ebfaafc7..d9a0c9b25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ OctoBot-Commons==1.9.36 OctoBot-Trading==2.4.49 OctoBot-Evaluators==1.9.4 OctoBot-Tentacles-Manager==2.9.7 -OctoBot-Services==1.6.9 +OctoBot-Services==1.6.10 OctoBot-Backtesting==1.9.7 Async-Channel==2.2.1 trading-backend==1.2.12 From fc36e88753215bffefd4f65ac742d508da4a319c Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Wed, 10 Jan 2024 14:23:14 +0100 Subject: [PATCH 14/15] [Requirements] bump --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index d9a0c9b25..304ca11e7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ # Drakkar-Software requirements -OctoBot-Commons==1.9.36 +OctoBot-Commons==1.9.37 OctoBot-Trading==2.4.49 OctoBot-Evaluators==1.9.4 -OctoBot-Tentacles-Manager==2.9.7 +OctoBot-Tentacles-Manager==2.9.8 OctoBot-Services==1.6.10 OctoBot-Backtesting==1.9.7 Async-Channel==2.2.1 From c238ca1c486db57ab87a4a5aedd9ca995c63f8f9 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Wed, 10 Jan 2024 22:30:30 +0100 Subject: [PATCH 15/15] [Tests] remove unused import --- additional_tests/exchanges_tests/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/additional_tests/exchanges_tests/__init__.py b/additional_tests/exchanges_tests/__init__.py index 4a4ced08e..65ad3e142 100644 --- a/additional_tests/exchanges_tests/__init__.py +++ b/additional_tests/exchanges_tests/__init__.py @@ -26,7 +26,6 @@ import octobot_trading.api as trading_api import octobot_trading.exchanges as exchanges import octobot_trading.constants as trading_constants -import octobot_trading.enums as enums import octobot_trading.errors as errors import octobot_trading.exchange_channel as exchange_channel import octobot_trading.personal_data as personal_data