diff --git a/octobot/community/__init__.py b/octobot/community/__init__.py index 868d557f6..4c0fd2df7 100644 --- a/octobot/community/__init__.py +++ b/octobot/community/__init__.py @@ -31,6 +31,9 @@ CommunityDonation, StartupInfo, StrategyData, + get_exchange_type_from_availability, + to_bot_exchange_internal_name, + to_community_exchange_internal_name, ) from octobot.community.supabase_backend import ( SyncConfigurationStorage, @@ -105,6 +108,9 @@ "flush_tracker", "StartupInfo", "StrategyData", + "get_exchange_type_from_availability", + "to_bot_exchange_internal_name", + "to_community_exchange_internal_name", "SyncConfigurationStorage", "ASyncConfigurationStorage", "AuthenticatedAsyncSupabaseClient", diff --git a/octobot/community/models/__init__.py b/octobot/community/models/__init__.py index 5d2e3b81d..7f0057c4e 100644 --- a/octobot/community/models/__init__.py +++ b/octobot/community/models/__init__.py @@ -46,6 +46,9 @@ format_portfolio, format_portfolio_history, format_portfolio_with_profitability, + get_exchange_type_from_availability, + to_bot_exchange_internal_name, + to_community_exchange_internal_name, ) from octobot.community.models.community_public_data import ( CommunityPublicData @@ -66,6 +69,9 @@ "format_portfolio", "format_portfolio_history", "format_portfolio_with_profitability", + "get_exchange_type_from_availability", + "to_bot_exchange_internal_name", + "to_community_exchange_internal_name", "CommunityPublicData", "StrategyData", ] diff --git a/octobot/community/models/formatters.py b/octobot/community/models/formatters.py index 8f1b0e6fc..7aee66dba 100644 --- a/octobot/community/models/formatters.py +++ b/octobot/community/models/formatters.py @@ -16,11 +16,15 @@ import octobot.community.supabase_backend.enums as backend_enums import octobot.community.supabase_backend as supabase_backend import octobot_commons.constants as commons_constants +import octobot_commons.logging as commons_logging import octobot_trading.enums as trading_enums import octobot_trading.constants as trading_constants import octobot_trading.personal_data as trading_personal_data +FUTURES_INTERNAL_NAME_SUFFIX = "_futures" + + def format_trades(trades: list, exchange_name: str, bot_id: str) -> list: return [ _format_trade(trade, exchange_name, bot_id) @@ -139,6 +143,36 @@ def _get_order_type(order_or_trade): return order_type +def to_community_exchange_internal_name(bot_exchange_internal_name: str, exchange_type: str) -> str: + if exchange_type == commons_constants.CONFIG_EXCHANGE_FUTURE: + return f"{bot_exchange_internal_name}{FUTURES_INTERNAL_NAME_SUFFIX}" + return bot_exchange_internal_name + + +def to_bot_exchange_internal_name(community_exchange_internal_name: str) -> str: + if community_exchange_internal_name.endswith(FUTURES_INTERNAL_NAME_SUFFIX): + return community_exchange_internal_name[:-len(FUTURES_INTERNAL_NAME_SUFFIX)] + return community_exchange_internal_name + + +def get_exchange_type_from_availability(exchange_availability: dict) -> str: + if not exchange_availability: + # use spot by default + return commons_constants.CONFIG_EXCHANGE_SPOT + # 1. try futures + if exchange_availability.get("futures") == backend_enums.ExchangeSupportValues.SUPPORTED.value: + return commons_constants.CONFIG_EXCHANGE_FUTURE + # 2. try spot + if exchange_availability.get("spot") == backend_enums.ExchangeSupportValues.SUPPORTED.value: + return commons_constants.CONFIG_EXCHANGE_SPOT + # 3. something went wrong: select spot and log error + _get_logger().error( + f"Unknown exchange type from exchange availability: {exchange_availability}. " + f"Defaulting to {commons_constants.CONFIG_EXCHANGE_SPOT}" + ) + return commons_constants.CONFIG_EXCHANGE_SPOT + + def format_portfolio( current_value: dict, initial_value: dict, profitability: float, unit: str, content: dict, price_by_asset: dict, @@ -196,3 +230,7 @@ def get_adapted_portfolio(usd_like_asset, portfolio): currency = usd_like_asset formatted[currency] = asset[backend_enums.PortfolioAssetKeys.VALUE.value] return formatted + + +def _get_logger(): + return commons_logging.get_logger("CommunityFormatter") diff --git a/octobot/community/supabase_backend/community_supabase_client.py b/octobot/community/supabase_backend/community_supabase_client.py index f70734a13..3e15042f3 100644 --- a/octobot/community/supabase_backend/community_supabase_client.py +++ b/octobot/community/supabase_backend/community_supabase_client.py @@ -476,7 +476,10 @@ async def _fetch_full_exchange_configs( for config in (bot_config.get(enums.BotConfigKeys.EXCHANGES.value) or []) if ( config.get(enums.ExchangeKeys.EXCHANGE_ID.value, None) - and not config.get(enums.ExchangeKeys.INTERNAL_NAME.value, None) + and not ( + config.get(enums.ExchangeKeys.INTERNAL_NAME.value, None) + and config.get(enums.ExchangeKeys.AVAILABILITY.value, None) + ) ) } if incomplete_exchange_config_by_id: @@ -486,6 +489,7 @@ async def _fetch_full_exchange_configs( **incomplete_exchange_config_by_id[exchange[enums.ExchangeKeys.ID.value]], **{ enums.ExchangeKeys.INTERNAL_NAME.value: exchange[enums.ExchangeKeys.INTERNAL_NAME.value], + enums.ExchangeKeys.AVAILABILITY.value: exchange[enums.ExchangeKeys.AVAILABILITY.value], } } for exchange in fetched_exchanges @@ -509,6 +513,7 @@ async def _fetch_full_exchange_configs( **{ enums.ExchangeKeys.EXCHANGE_ID.value: exchange[enums.ExchangeKeys.ID.value], enums.ExchangeKeys.INTERNAL_NAME.value: exchange[enums.ExchangeKeys.INTERNAL_NAME.value], + enums.ExchangeKeys.AVAILABILITY.value: exchange[enums.ExchangeKeys.AVAILABILITY.value], } } for credentials_id, exchange in exchanges_by_credential_ids.items() @@ -528,30 +533,47 @@ async def _fetch_full_exchange_configs( { enums.ExchangeKeys.EXCHANGE_ID.value: exchange[enums.ExchangeKeys.ID.value], enums.ExchangeKeys.INTERNAL_NAME.value: exchange[enums.ExchangeKeys.INTERNAL_NAME.value], + enums.ExchangeKeys.AVAILABILITY.value: exchange[enums.ExchangeKeys.AVAILABILITY.value], } for exchange in fetched_exchanges + # no way to differentiate futures and spot exchanges using internal_names only: + # use spot exchange here by default + if ( + formatters.get_exchange_type_from_availability( + exchange.get(enums.ExchangeKeys.AVAILABILITY.value) + ) + == commons_constants.CONFIG_EXCHANGE_SPOT + ) ] else: commons_logging.get_logger(self.__class__.__name__).error( f"Impossible to fetch exchange details for profile with bot id: {profile_data.profile_details.id}" ) - # Register exchange_type if provided, otherwise will use default value. - # Multi exchange type configurations are not yet supported - exchange_type = ( - profile_data.exchanges[0].exchange_type if profile_data.exchanges - else commons_constants.DEFAULT_EXCHANGE_TYPE - ) + # Register exchange_type from exchange availability + exchange_type = commons_constants.CONFIG_EXCHANGE_SPOT + if exchanges_configs: + # Multi exchange type configurations are not yet supported + exchange_type = formatters.get_exchange_type_from_availability( + exchanges_configs[0].get( + enums.ExchangeKeys.AVAILABILITY.value + ) + ) for exchange_data in exchanges_configs: exchange_data[enums.ExchangeKeys.EXCHANGE_TYPE.value] = exchange_type + exchange_data[enums.ExchangeKeys.INTERNAL_NAME.value] = formatters.to_bot_exchange_internal_name( + exchange_data[enums.ExchangeKeys.INTERNAL_NAME.value] + ) return [ commons_profiles.ExchangeData.from_dict(exchange_data) for exchange_data in exchanges_configs ] async def fetch_exchanges(self, exchange_ids: list, internal_names: typing.Optional[list] = None) -> list: + # WARNING: setting internal_names can result in duplicate (futures and spot) exchanges select = self.table("exchanges").select( f"{enums.ExchangeKeys.ID.value}, " - f"{enums.ExchangeKeys.INTERNAL_NAME.value}" + f"{enums.ExchangeKeys.INTERNAL_NAME.value}, " + f"{enums.ExchangeKeys.AVAILABILITY.value}" ) if internal_names: return (await select.in_(enums.ExchangeKeys.INTERNAL_NAME.value, internal_names).execute()).data @@ -560,7 +582,7 @@ async def fetch_exchanges(self, exchange_ids: list, internal_names: typing.Optio async def fetch_exchanges_by_credential_ids(self, exchange_credential_ids: list) -> dict: exchanges = (await self.table("exchange_credentials").select( "id," - "exchange:exchanges(id, internal_name)" + f"exchange:exchanges(id, {enums.ExchangeKeys.INTERNAL_NAME.value}, {enums.ExchangeKeys.AVAILABILITY.value})" ).in_("id", exchange_credential_ids).execute()).data return { exchange["id"]: exchange["exchange"] diff --git a/octobot/community/supabase_backend/enums.py b/octobot/community/supabase_backend/enums.py index 9202c676b..b6bc56230 100644 --- a/octobot/community/supabase_backend/enums.py +++ b/octobot/community/supabase_backend/enums.py @@ -114,6 +114,12 @@ class ExchangeKeys(enum.Enum): EXCHANGE_ID = "exchange_id" EXCHANGE_TYPE = "exchange_type" SANDBOXED = "sandboxed" + AVAILABILITY = "availability" + + +class ExchangeSupportValues(enum.Enum): + SUPPORTED = "supported" + UNSUPPORTED = "unsupported" class SignalKeys(enum.Enum):