From 5bc3525c4c73fae658719e3b74b3b6baf8003cf9 Mon Sep 17 00:00:00 2001 From: Guillaume De Saint Martin Date: Wed, 20 May 2026 12:02:40 +0200 Subject: [PATCH] [Sync] add auth, history and trading details collections --- packages/node/octobot_node/constants.py | 4 + packages/node/octobot_node/errors.py | 4 + .../protocol/accounts_authentication.py | 25 ++ .../octobot_node/protocol/accounts_trading.py | 31 +++ .../node/octobot_node/protocol/automations.py | 106 ++++---- .../account_user_action_executor.py | 2 + .../automation_user_action_executor.py | 2 + .../user_actions_executor/create_account.py | 5 +- .../create_automation.py | 4 + .../user_actions_executor/edit_account.py | 5 +- .../user_actions_executor/refresh_accounts.py | 2 +- .../util/account_authentication_resolver.py | 52 ++++ .../util/account_state_updater.py | 117 ++++++--- .../util/action_details_factory.py | 137 ++++++----- .../grid_workflow_simulator_test_util.py | 28 ++- .../test_accounts_CRUD_operations.py | 77 +++++- .../tests/protocol/test_accounts_trading.py | 60 +++++ .../node/tests/protocol/test_automations.py | 11 +- .../test_user_action_executor_factory.py | 4 +- .../account_executor_test_utils.py | 24 +- ..._user_action_executor_get_error_message.py | 17 ++ .../test_create_account.py | 37 +++ .../test_create_automation.py | 22 +- .../test_refresh_accounts.py | 38 +++ .../test_stop_automation.py | 4 +- .../util/test_account_state_updater.py | 197 +++++++++++---- packages/protocol/.openapi-generator/FILES | 36 ++- packages/protocol/docs/Account.md | 3 +- .../docs/AccountActionResultErrorMessage.md | 2 + .../protocol/docs/AccountAuthentication.md | 35 +++ .../docs/AccountAuthenticationDetails.md | 35 +++ packages/protocol/docs/AccountDetails.md | 10 - packages/protocol/docs/AccountSpecifics.md | 34 +++ packages/protocol/docs/AccountTrading.md | 33 +++ .../protocol/docs/AccountTradingDetails.md | 33 +++ .../docs/AccountTradingDetailsState.md | 31 +++ packages/protocol/docs/AccountTradingState.md | 31 +++ .../docs/AccountTrailingDetailsState.md | 32 +++ .../AccountsAuthenticationDetailsState.md | 31 +++ .../docs/AccountsAuthenticationState.md | 31 +++ .../docs/AuthenticationDetailsState.md | 31 +++ .../AutomationActionResultErrorMessage.md | 2 + packages/protocol/docs/AutomationState.md | 2 +- packages/protocol/docs/BlockchainAccount.md | 3 - packages/protocol/docs/CancelPolicy.md | 2 +- packages/protocol/docs/DetailedAsset.md | 32 +++ packages/protocol/docs/ExchangeAccount.md | 7 - packages/protocol/docs/GenericAccount.md | 1 - .../protocol/docs/HistoricalAssetValue.md | 32 +++ packages/protocol/docs/PortfolioContent.md | 32 +++ .../protocol/docs/PortfolioHistoricalValue.md | 32 +++ .../docs/PortfolioHistoricalValues.md | 31 +++ .../docs/PortfolioHistoricalValuesState.md | 31 +++ packages/protocol/docs/Strategy.md | 1 + packages/protocol/docs/TrailingProfile.md | 2 +- .../octobot_protocol/models/__init__.py | 12 +- .../octobot_protocol/models/account.py | 24 +- .../account_action_result_error_message.py | 1 + .../models/account_authentication.py | 98 ++++++++ ...ccount_details.py => account_specifics.py} | 16 +- .../models/account_trading.py | 119 +++++++++ .../models/account_trading_state.py | 98 ++++++++ .../models/accounts_authentication_state.py | 98 ++++++++ .../automation_action_result_error_message.py | 1 + .../models/automation_state.py | 6 +- .../models/blockchain_account.py | 10 +- .../octobot_protocol/models/cancel_policy.py | 6 +- .../models/{asset.py => detailed_asset.py} | 18 +- .../models/exchange_account.py | 52 +--- .../models/generic_account.py | 16 +- .../models/historical_asset_value.py | 92 +++++++ .../models/portfolio_historical_value.py | 101 ++++++++ .../models/portfolio_historical_values.py | 98 ++++++++ .../portfolio_historical_values_state.py | 94 +++++++ .../octobot_protocol/models/strategy.py | 4 +- .../models/trailing_profile.py | 6 +- packages/protocol/openapi.json | 231 ++++++++++++++---- packages/protocol/test/test_account.py | 8 +- .../test/test_account_authentication.py | 56 +++++ .../protocol/test/test_account_specifics.py | 61 +++++ ...unt_details.py => test_account_trading.py} | 57 ++--- .../test/test_account_trading_state.py | 204 ++++++++++++++++ .../test_accounts_authentication_state.py | 70 ++++++ packages/protocol/test/test_accounts_state.py | 8 +- .../protocol/test/test_automation_state.py | 6 +- .../protocol/test/test_blockchain_account.py | 5 +- packages/protocol/test/test_cancel_policy.py | 2 +- packages/protocol/test/test_copied_account.py | 8 +- .../test/test_create_account_configuration.py | 16 +- .../{test_asset.py => test_detailed_asset.py} | 26 +- .../test/test_edit_account_configuration.py | 8 +- .../protocol/test/test_exchange_account.py | 87 +------ .../protocol/test/test_generic_account.py | 10 +- .../test/test_historical_asset_value.py | 56 +++++ packages/protocol/test/test_order.py | 8 +- .../test/test_portfolio_historical_value.py | 60 +++++ .../test/test_portfolio_historical_values.py | 74 ++++++ .../test_portfolio_historical_values_state.py | 65 +++++ .../protocol/test/test_strategies_state.py | 2 + packages/protocol/test/test_strategy.py | 2 + .../protocol/test/test_trailing_profile.py | 2 +- .../test/test_user_action_configuration.py | 16 +- .../protocol/test/test_user_data_state.py | 6 +- packages/sync/octobot_sync/enums.py | 3 + packages/sync/octobot_sync/errors.py | 6 + packages/sync/octobot_sync/server.py | 19 ++ .../sync/collection_backend/__init__.py | 9 + .../abstract_local_collection_provider.py | 64 +++++ .../base_local_collection_provider.py | 31 +-- .../base_local_collection_storage.py | 37 +-- .../single_item_local_collection_provider.py | 88 +++++++ .../single_item_local_collection_storage.py | 51 ++++ .../sync/collection_providers/__init__.py | 8 + .../user_account_authentication_provider.py | 55 +++++ .../user_account_trading_provider.py | 38 +++ .../sync/octobot_sync/sync/collections.py | 24 ++ ...test_abstract_local_collection_provider.py | 96 ++++++++ ...t_single_item_local_collection_provider.py | 144 +++++++++++ ...st_single_item_local_collection_storage.py | 137 +++++++++++ .../test_account_authentication_backend.py | 66 +++++ .../test_account_trading_backend.py | 144 +++++++++++ .../test_strategy_backend.py | 1 + packages/sync/tests/sync/test_collections.py | 49 ++++ packages/sync/tests/test_server.py | 50 +++- packages/sync/tests/test_sync_collections.py | 8 +- .../personal_data/portfolios/protocol.py | 4 +- 126 files changed, 4226 insertions(+), 663 deletions(-) create mode 100644 packages/node/octobot_node/protocol/accounts_authentication.py create mode 100644 packages/node/octobot_node/protocol/accounts_trading.py create mode 100644 packages/node/octobot_node/scheduler/user_actions/user_actions_executor/util/account_authentication_resolver.py create mode 100644 packages/node/tests/protocol/test_accounts_trading.py create mode 100644 packages/protocol/docs/AccountAuthentication.md create mode 100644 packages/protocol/docs/AccountAuthenticationDetails.md create mode 100644 packages/protocol/docs/AccountSpecifics.md create mode 100644 packages/protocol/docs/AccountTrading.md create mode 100644 packages/protocol/docs/AccountTradingDetails.md create mode 100644 packages/protocol/docs/AccountTradingDetailsState.md create mode 100644 packages/protocol/docs/AccountTradingState.md create mode 100644 packages/protocol/docs/AccountTrailingDetailsState.md create mode 100644 packages/protocol/docs/AccountsAuthenticationDetailsState.md create mode 100644 packages/protocol/docs/AccountsAuthenticationState.md create mode 100644 packages/protocol/docs/AuthenticationDetailsState.md create mode 100644 packages/protocol/docs/DetailedAsset.md create mode 100644 packages/protocol/docs/HistoricalAssetValue.md create mode 100644 packages/protocol/docs/PortfolioContent.md create mode 100644 packages/protocol/docs/PortfolioHistoricalValue.md create mode 100644 packages/protocol/docs/PortfolioHistoricalValues.md create mode 100644 packages/protocol/docs/PortfolioHistoricalValuesState.md create mode 100644 packages/protocol/octobot_protocol/models/account_authentication.py rename packages/protocol/octobot_protocol/models/{account_details.py => account_specifics.py} (89%) create mode 100644 packages/protocol/octobot_protocol/models/account_trading.py create mode 100644 packages/protocol/octobot_protocol/models/account_trading_state.py create mode 100644 packages/protocol/octobot_protocol/models/accounts_authentication_state.py rename packages/protocol/octobot_protocol/models/{asset.py => detailed_asset.py} (84%) create mode 100644 packages/protocol/octobot_protocol/models/historical_asset_value.py create mode 100644 packages/protocol/octobot_protocol/models/portfolio_historical_value.py create mode 100644 packages/protocol/octobot_protocol/models/portfolio_historical_values.py create mode 100644 packages/protocol/octobot_protocol/models/portfolio_historical_values_state.py create mode 100644 packages/protocol/test/test_account_authentication.py create mode 100644 packages/protocol/test/test_account_specifics.py rename packages/protocol/test/{test_account_details.py => test_account_trading.py} (72%) create mode 100644 packages/protocol/test/test_account_trading_state.py create mode 100644 packages/protocol/test/test_accounts_authentication_state.py rename packages/protocol/test/{test_asset.py => test_detailed_asset.py} (67%) create mode 100644 packages/protocol/test/test_historical_asset_value.py create mode 100644 packages/protocol/test/test_portfolio_historical_value.py create mode 100644 packages/protocol/test/test_portfolio_historical_values.py create mode 100644 packages/protocol/test/test_portfolio_historical_values_state.py create mode 100644 packages/sync/octobot_sync/sync/collection_backend/abstract_local_collection_provider.py create mode 100644 packages/sync/octobot_sync/sync/collection_backend/single_item_local_collection_provider.py create mode 100644 packages/sync/octobot_sync/sync/collection_backend/single_item_local_collection_storage.py create mode 100644 packages/sync/octobot_sync/sync/collection_providers/user_account_authentication_provider.py create mode 100644 packages/sync/octobot_sync/sync/collection_providers/user_account_trading_provider.py create mode 100644 packages/sync/tests/sync/collection_backend/test_abstract_local_collection_provider.py create mode 100644 packages/sync/tests/sync/collection_backend/test_single_item_local_collection_provider.py create mode 100644 packages/sync/tests/sync/collection_backend/test_single_item_local_collection_storage.py create mode 100644 packages/sync/tests/sync/collection_providers/test_account_authentication_backend.py create mode 100644 packages/sync/tests/sync/collection_providers/test_account_trading_backend.py create mode 100644 packages/sync/tests/sync/test_collections.py diff --git a/packages/node/octobot_node/constants.py b/packages/node/octobot_node/constants.py index f920ff9de..5d7e1c89e 100644 --- a/packages/node/octobot_node/constants.py +++ b/packages/node/octobot_node/constants.py @@ -42,6 +42,10 @@ FAILURE_ERROR_DETAILS_MAX_LENGTH = 8_000 EXCHANGE_ACCOUNTS_STATE_VERSION = "1.0.0" +USER_ACCOUNTS_AUTH_STATE_VERSION = "1.0.0" +USER_ACCOUNTS_TRADING_STATE_VERSION = "1.0.0" USER_STRATEGIES_STATE_VERSION = "1.0.0" USER_DATA_STATE_VERSION = "1.0.0" USER_ACTIONS_STATE_VERSION = "1.0.0" + +DEFAULT_PORTFOLIO_VALUATION_UNIT = "USDT" diff --git a/packages/node/octobot_node/errors.py b/packages/node/octobot_node/errors.py index b5f376576..b8ec69c66 100644 --- a/packages/node/octobot_node/errors.py +++ b/packages/node/octobot_node/errors.py @@ -62,6 +62,10 @@ class AccountNotFoundError(UserActionError): """Raised when fetching an account via AccountProvider fails.""" +class AccountAuthenticationNotFoundError(UserActionError): + """Raised when fetching account authentication via AccountAuthenticationProvider fails.""" + + class AutomationStrategyNotFoundError(UserActionError): """Raised when the referenced strategy does not exist in StrategyProvider.""" diff --git a/packages/node/octobot_node/protocol/accounts_authentication.py b/packages/node/octobot_node/protocol/accounts_authentication.py new file mode 100644 index 000000000..78468483a --- /dev/null +++ b/packages/node/octobot_node/protocol/accounts_authentication.py @@ -0,0 +1,25 @@ +# This file is part of OctoBot Node (https://github.com/Drakkar-Software/OctoBot-Node) +# Copyright (c) 2025 Drakkar-Software, All rights reserved. +# +# OctoBot Node 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 octobot_sync.sync.collection_backend.errors as collection_errors +import octobot_sync.sync.collection_providers.user_account_authentication_provider as auth_provider + + +def get_accounts_authentication_state_encrypted(address: str) -> dict[str, str] | None: + try: + return auth_provider.AccountAuthenticationProvider.instance().list_items_encrypted(address) + except collection_errors.CollectionNoDataError: + return None diff --git a/packages/node/octobot_node/protocol/accounts_trading.py b/packages/node/octobot_node/protocol/accounts_trading.py new file mode 100644 index 000000000..aafd71721 --- /dev/null +++ b/packages/node/octobot_node/protocol/accounts_trading.py @@ -0,0 +1,31 @@ +# This file is part of OctoBot Node (https://github.com/Drakkar-Software/OctoBot-Node) +# Copyright (c) 2025 Drakkar-Software, All rights reserved. +# +# OctoBot Node 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 octobot_sync.sync.collection_backend.errors as collection_errors +import octobot_sync.sync.collection_providers.user_account_trading_provider as trading_provider + + +def get_account_trading_state_encrypted( + address: str, + account_id: str, +) -> dict[str, str] | None: + try: + return trading_provider.AccountTradingProvider.instance().load_state_encrypted( + address, + account_id, + ) + except collection_errors.CollectionNoDataError: + return None diff --git a/packages/node/octobot_node/protocol/automations.py b/packages/node/octobot_node/protocol/automations.py index 6b1315264..02c6be2b8 100644 --- a/packages/node/octobot_node/protocol/automations.py +++ b/packages/node/octobot_node/protocol/automations.py @@ -27,7 +27,6 @@ import octobot_trading.constants as octobot_trading_constants import octobot_trading.enums as octobot_trading_enums import octobot_trading.personal_data.portfolios.protocol as octobot_trading_portfolios_protocol -import octobot_trading.exchanges.util.exchange_data as exchange_data_import logger = octobot_commons_logging.get_logger("AutomationsProtocol") @@ -158,65 +157,6 @@ def _protocol_action_from_flow( ) -def _enrich_protocol_assets( - base_assets: list[protocol_models.Asset], - portfolio: exchange_data_import.PortfolioDetails, - unit: typing.Optional[str], -) -> list[protocol_models.Asset]: - enriched: list[protocol_models.Asset] = [] - for asset in base_assets: - update_fields: dict[str, typing.Any] = {} - if asset.symbol in portfolio.asset_values: - update_fields["value"] = float(portfolio.asset_values[asset.symbol]) - if unit: - update_fields["unit"] = unit - if update_fields: - enriched.append(asset.model_copy(update=update_fields)) - else: - enriched.append(asset) - return enriched - - -def _order_summaries_from_open_orders(open_orders: list[dict]) -> list[protocol_models.OrderSummary]: - order_columns = octobot_trading_enums.ExchangeConstantsOrderColumns - summaries: list[protocol_models.OrderSummary] = [] - for order in open_orders: - inner = order.get(octobot_trading_constants.STORAGE_ORIGIN_VALUE, order) - if not isinstance(inner, dict): - inner = order - order_id = inner.get(order_columns.EXCHANGE_ID.value) or inner.get(order_columns.ID.value) - symbol = inner.get(order_columns.SYMBOL.value) - if order_id is None or symbol is None: - continue - summaries.append(protocol_models.OrderSummary(id=str(order_id), symbol=str(symbol))) - return summaries - - -def _position_summaries(positions: list[typing.Any]) -> list[protocol_models.PositionSummary]: - position_columns = octobot_trading_enums.ExchangeConstantsPositionColumns - summaries: list[protocol_models.PositionSummary] = [] - for position_details in positions: - position_dict = position_details.position - position_id = position_dict.get(position_columns.ID.value) - symbol = position_dict.get(position_columns.SYMBOL.value) - if position_id is None or symbol is None: - continue - summaries.append(protocol_models.PositionSummary(id=str(position_id), symbol=str(symbol))) - return summaries - - -def _trade_summaries(trades: list[dict]) -> list[protocol_models.TradeSummary]: - order_columns = octobot_trading_enums.ExchangeConstantsOrderColumns - summaries: list[protocol_models.TradeSummary] = [] - for trade in trades: - trade_id = trade.get(order_columns.EXCHANGE_TRADE_ID.value) or trade.get(order_columns.EXCHANGE_ID.value) - symbol = trade.get(order_columns.SYMBOL.value) - if trade_id is None or symbol is None: - continue - summaries.append(protocol_models.TradeSummary(id=str(trade_id), symbol=str(symbol))) - return summaries - - def _fill_protocol_automation_state( protocol_automation_state: protocol_models.AutomationState, flow_automation_state: flow_entities.AutomationState, @@ -247,16 +187,14 @@ def _fill_protocol_automation_state( exchange_account_ids = [exchange_details.metadata.id] # Derive portfolio and trading summaries from automation exchange elements. exchange_elements = flow_automation_state.automation.exchange_account_elements - assets: typing.Optional[list[protocol_models.Asset]] = None + assets: typing.Optional[list[protocol_models.DetailedAsset]] = None orders: typing.Optional[list[protocol_models.OrderSummary]] = None trades: typing.Optional[list[protocol_models.TradeSummary]] = None positions: typing.Optional[list[protocol_models.PositionSummary]] = None if exchange_elements: portfolio = exchange_elements.portfolio if portfolio.content: - base_assets = octobot_trading_portfolios_protocol.to_protocol_assets(portfolio.content) - unit_for_assets = exchange_details.portfolio.unit if exchange_details else None - assets = _enrich_protocol_assets(base_assets, portfolio, unit_for_assets) + assets = octobot_trading_portfolios_protocol.to_protocol_assets(portfolio.content) orders = _order_summaries_from_open_orders(exchange_elements.orders.open_orders) or None positions = _position_summaries(exchange_elements.positions) or None trades = _trade_summaries(exchange_elements.trades) or None @@ -273,3 +211,43 @@ def _fill_protocol_automation_state( "positions": positions, } ) + + +def _order_summaries_from_open_orders(open_orders: list[dict]) -> list[protocol_models.OrderSummary]: + order_columns = octobot_trading_enums.ExchangeConstantsOrderColumns + summaries: list[protocol_models.OrderSummary] = [] + for order in open_orders: + inner = order.get(octobot_trading_constants.STORAGE_ORIGIN_VALUE, order) + if not isinstance(inner, dict): + inner = order + order_id = inner.get(order_columns.EXCHANGE_ID.value) or inner.get(order_columns.ID.value) + symbol = inner.get(order_columns.SYMBOL.value) + if order_id is None or symbol is None: + continue + summaries.append(protocol_models.OrderSummary(id=str(order_id), symbol=str(symbol))) + return summaries + + +def _position_summaries(positions: list[typing.Any]) -> list[protocol_models.PositionSummary]: + position_columns = octobot_trading_enums.ExchangeConstantsPositionColumns + summaries: list[protocol_models.PositionSummary] = [] + for position_details in positions: + position_dict = position_details.position + position_id = position_dict.get(position_columns.ID.value) + symbol = position_dict.get(position_columns.SYMBOL.value) + if position_id is None or symbol is None: + continue + summaries.append(protocol_models.PositionSummary(id=str(position_id), symbol=str(symbol))) + return summaries + + +def _trade_summaries(trades: list[dict]) -> list[protocol_models.TradeSummary]: + order_columns = octobot_trading_enums.ExchangeConstantsOrderColumns + summaries: list[protocol_models.TradeSummary] = [] + for trade in trades: + trade_id = trade.get(order_columns.EXCHANGE_TRADE_ID.value) or trade.get(order_columns.EXCHANGE_ID.value) + symbol = trade.get(order_columns.SYMBOL.value) + if trade_id is None or symbol is None: + continue + summaries.append(protocol_models.TradeSummary(id=str(trade_id), symbol=str(symbol))) + return summaries diff --git a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/account_user_action_executor.py b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/account_user_action_executor.py index 3ca47fc27..e6e78c50c 100644 --- a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/account_user_action_executor.py +++ b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/account_user_action_executor.py @@ -49,6 +49,8 @@ def _build_failure_user_action_result( def _get_error_message(self, exc: BaseException) -> protocol_models.AccountActionResultErrorMessage: if isinstance(exc, (node_errors.AccountNotFoundError, collection_errors.ItemNotFoundError)): return protocol_models.AccountActionResultErrorMessage.ACCOUNT_NOT_FOUND + if isinstance(exc, node_errors.AccountAuthenticationNotFoundError): + return protocol_models.AccountActionResultErrorMessage.ACCOUNT_AUTHENTICATION_DETAILS_NOT_FOUND if isinstance( exc, ( diff --git a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/automation_user_action_executor.py b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/automation_user_action_executor.py index 7c638dce6..f3c63e416 100644 --- a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/automation_user_action_executor.py +++ b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/automation_user_action_executor.py @@ -54,6 +54,8 @@ def _get_error_message(self, exc: BaseException) -> protocol_models.AutomationAc return protocol_models.AutomationActionResultErrorMessage.AUTOMATION_NOT_FOUND if isinstance(exc, node_errors.AccountNotFoundError): return protocol_models.AutomationActionResultErrorMessage.ACCOUNT_NOT_FOUND + if isinstance(exc, node_errors.AccountAuthenticationNotFoundError): + return protocol_models.AutomationActionResultErrorMessage.ACCOUNT_AUTHENTICATION_DETAILS_NOT_FOUND if isinstance(exc, node_errors.AutomationStrategyNotFoundError): return protocol_models.AutomationActionResultErrorMessage.STRATEGY_NOT_FOUND if isinstance(exc, node_errors.AutomationStrategyVersionMismatchError): diff --git a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/create_account.py b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/create_account.py index 04570c411..c23966dab 100644 --- a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/create_account.py +++ b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/create_account.py @@ -44,7 +44,10 @@ async def _do_execute( user_action: protocol_models.UserAction, ) -> None: create_payload = _get_create_account_payload(user_action) - checked_account = await account_state_updater.update_account_state(create_payload.configuration) + checked_account = await account_state_updater.update_account_state( + create_payload.configuration, + self._wallet_address, + ) collection_providers.AccountProvider.instance().create_item( self._wallet_address, checked_account, diff --git a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/create_automation.py b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/create_automation.py index e510fe4d9..c426ebb66 100644 --- a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/create_automation.py +++ b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/create_automation.py @@ -132,6 +132,8 @@ def _create_automation_actions(self, user_action: protocol_models.UserAction) -> automation_id=user_action.id, protocol_account=protocol_account, strategy_reference=automation_configuration.strategy, + wallet_address=self._wallet_address, + reference_market=stored_strategy.reference_market, ) match inner_configuration: @@ -162,6 +164,8 @@ def _create_automation_actions(self, user_action: protocol_models.UserAction) -> init_action, market_making_configuration, protocol_account, + self._wallet_address, + stored_strategy.reference_market, ), ] case _: diff --git a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/edit_account.py b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/edit_account.py index 68f4f4c88..32df8790b 100644 --- a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/edit_account.py +++ b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/edit_account.py @@ -52,7 +52,10 @@ async def _do_execute( raise node_errors.InvalidUserActionPayloadError( "EditAccountConfiguration.id must match configuration.id." ) - checked_account = await account_state_updater.update_account_state(edit_payload.configuration) + checked_account = await account_state_updater.update_account_state( + edit_payload.configuration, + self._wallet_address, + ) collection_providers.AccountProvider.instance().update_item( self._wallet_address, checked_account, diff --git a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/refresh_accounts.py b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/refresh_accounts.py index d3847e6da..1562e51a8 100644 --- a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/refresh_accounts.py +++ b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/refresh_accounts.py @@ -50,6 +50,6 @@ async def _do_execute( ] for account_id in account_ids_to_refresh: account = account_provider.get_item(self._wallet_address, account_id) - checked_account = await account_state_updater.update_account_state(account) + checked_account = await account_state_updater.update_account_state(account, self._wallet_address) account_provider.update_item(self._wallet_address, checked_account) self._mark_user_action_completed(user_action) diff --git a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/util/account_authentication_resolver.py b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/util/account_authentication_resolver.py new file mode 100644 index 000000000..902600800 --- /dev/null +++ b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/util/account_authentication_resolver.py @@ -0,0 +1,52 @@ +# This file is part of OctoBot Node (https://github.com/Drakkar-Software/OctoBot-Node) +# Copyright (c) 2025 Drakkar-Software, All rights reserved. +# +# OctoBot Node 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 octobot_protocol.models as protocol_models +import octobot_sync.sync.collection_backend.errors as collection_errors +import octobot_sync.sync.collection_providers as collection_providers + +import octobot_node.errors as node_errors + + +def get_exchange_authentication( + wallet_address: str, + account: protocol_models.Account, +) -> protocol_models.AccountAuthentication | None: + if account.is_simulated: + return None + account_specifics = account.specifics + if account_specifics is None or account_specifics.actual_instance is None: + raise node_errors.AccountAuthenticationNotFoundError( + f"Account {account.id!r} has no specifics for authentication lookup." + ) + if not isinstance(account_specifics.actual_instance, protocol_models.ExchangeAccount): + raise node_errors.AccountAuthenticationNotFoundError( + f"Account {account.id!r} is not an exchange account; cannot resolve authentication." + ) + try: + authentication = collection_providers.AccountAuthenticationProvider.instance().get_item( + wallet_address, + account.id, + ) + except collection_errors.ItemNotFoundError as err: + raise node_errors.AccountAuthenticationNotFoundError( + f"Authentication for account {account.id!r} not found for address {wallet_address!r}: {err}" + ) from err + if not authentication.api_key or not authentication.api_secret: + raise node_errors.AccountAuthenticationNotFoundError( + f"Authentication for account {account.id!r} is missing api_key or api_secret." + ) + return authentication diff --git a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/util/account_state_updater.py b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/util/account_state_updater.py index af352034b..c807a0e2e 100644 --- a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/util/account_state_updater.py +++ b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/util/account_state_updater.py @@ -1,4 +1,5 @@ import octobot_commons.configuration.fields_utils as fields_utils +import octobot_commons.constants as commons_constants import octobot_commons.profiles.profile_data as commons_profile_data import octobot_protocol.models as protocol_models import octobot_tentacles_manager.api as tentacles_manager_api @@ -8,6 +9,7 @@ import octobot_trading.exchanges.util.exchange_data as exchange_data_module import octobot_node.errors as node_errors +import octobot_node.scheduler.user_actions.user_actions_executor.util.account_authentication_resolver as account_authentication_resolver _TRADING_TYPE_TO_EXCHANGE_TYPE: dict[protocol_models.TradingType, trading_enums.ExchangeTypes] = { @@ -24,45 +26,56 @@ def _exchange_type_from_trading_type( return _TRADING_TYPE_TO_EXCHANGE_TYPE[trading_type].value -async def _request_exchange_to_ensure_authentication(exchange_manager) -> None: - await exchange_manager.exchange.request_exchange_to_ensure_authentication() - - async def _ensure_api_key_permissions(exchange_manager) -> None: await exchange_manager.exchange.ensure_api_key_permissions() async def update_account_state( account: protocol_models.Account, + wallet_address: str, ) -> protocol_models.Account: - account_details = account.details - if account_details is None or account_details.actual_instance is None: + account_specifics = account.specifics + if account_specifics is None or account_specifics.actual_instance is None: raise node_errors.InvalidUserActionPayloadError( - "Account.details.actual_instance is required for account checks." + "Account.specifics.actual_instance is required for account checks." ) - account_details_instance = account_details.actual_instance - if isinstance(account_details_instance, protocol_models.GenericAccount): + account_specifics_instance = account_specifics.actual_instance + if isinstance(account_specifics_instance, protocol_models.GenericAccount): return account - if isinstance(account_details_instance, protocol_models.BlockchainAccount): + if isinstance(account_specifics_instance, protocol_models.BlockchainAccount): raise node_errors.InvalidUserActionPayloadError("Blockchain accounts are not supported yet.") - if not isinstance(account_details_instance, protocol_models.ExchangeAccount): + if not isinstance(account_specifics_instance, protocol_models.ExchangeAccount): raise node_errors.InvalidUserActionPayloadError( - f"Unsupported account details type for checks: {type(account_details_instance).__name__}." + f"Unsupported account specifics type for checks: {type(account_specifics_instance).__name__}." ) - checked_state = await _check_exchange_account_state(account_details_instance) - return account.model_copy(update={"state": checked_state}) + checked_state, assets = await _check_exchange_account_state( + account_specifics_instance, + account, + wallet_address, + ) + account_updates: dict = {"state": checked_state} + if assets is not None: + account_updates["assets"] = assets + return account.model_copy(update=account_updates) def _encrypted_exchange_auth_details( exchange_account: protocol_models.ExchangeAccount, + authentication: protocol_models.AccountAuthentication | None, ) -> exchange_data_module.ExchangeAuthDetails: + if authentication is None: + return exchange_data_module.ExchangeAuthDetails( + exchange_type=_exchange_type_from_trading_type(exchange_account.trading_type), + sandboxed=False, + exchange_account_id=exchange_account.remote_account_id, + ) # Exchange manager expects Fernet-encrypted strings (see decrypt_element_if_possible on load). api_password = "" - if exchange_account.api_passphrase: - api_password = fields_utils.encrypt(exchange_account.api_passphrase).decode() + if authentication.api_passphrase: + api_password = fields_utils.encrypt(authentication.api_passphrase).decode() return exchange_data_module.ExchangeAuthDetails( - api_key=fields_utils.encrypt(exchange_account.api_key).decode(), - api_secret=fields_utils.encrypt(exchange_account.api_secret).decode(), + api_key=fields_utils.encrypt(authentication.api_key).decode(), + api_secret=fields_utils.encrypt(authentication.api_secret).decode(), api_password=api_password, exchange_type=_exchange_type_from_trading_type(exchange_account.trading_type), sandboxed=False, @@ -72,7 +85,13 @@ def _encrypted_exchange_auth_details( async def _check_exchange_account_state( exchange_account: protocol_models.ExchangeAccount, -) -> protocol_models.AccountState: + account: protocol_models.Account, + wallet_address: str, +) -> tuple[protocol_models.AccountState, list[protocol_models.DetailedAsset] | None]: + authentication = account_authentication_resolver.get_exchange_authentication( + wallet_address, + account, + ) profile_data = commons_profile_data.ProfileData( exchanges=[ commons_profile_data.ExchangeData( @@ -88,40 +107,74 @@ async def _check_exchange_account_state( exchange_internal_name=exchange_account.exchange, exchange_type=_exchange_type_from_trading_type(exchange_account.trading_type), sandboxed=False, - auth_details=_encrypted_exchange_auth_details(exchange_account), + auth_details=_encrypted_exchange_auth_details(exchange_account, authentication), ) tentacles_setup_config = tentacles_manager_api.get_full_tentacles_setup_config() - # todo change exchange_manager_from_exchange_data can raise async with trading_exchanges.exchange_manager_from_exchange_data( exchange_data, profile_data, tentacles_setup_config, price_fallback=None, ) as exchange_manager: - return await _check_exchange_manager_state(exchange_manager) + return await _check_exchange_manager_state(exchange_manager, account) -async def _check_exchange_manager_state(exchange_manager) -> protocol_models.AccountState: +async def _check_exchange_manager_state( + exchange_manager, + account: protocol_models.Account, +) -> tuple[protocol_models.AccountState, list[protocol_models.DetailedAsset] | None]: try: - await _request_exchange_to_ensure_authentication(exchange_manager) + balance = await exchange_manager.exchange.get_balance() await _ensure_api_key_permissions(exchange_manager) - return protocol_models.AccountState( - status=protocol_models.AccountStatus.VALID, - message=protocol_models.AccountStatusMessage.VALID, + assets = _assets_from_balance(balance) + return ( + protocol_models.AccountState( + status=protocol_models.AccountStatus.VALID, + message=protocol_models.AccountStatusMessage.VALID, + ), + assets, ) except trading_errors.RetriableFailedRequest: raise except trading_errors.InvalidAPIKeyIPWhitelistError: - return _invalid_state(protocol_models.AccountStatusMessage.INVALID_API_IP_WHITELIST) + return _invalid_state(protocol_models.AccountStatusMessage.INVALID_API_IP_WHITELIST), None except trading_errors.AuthenticationError as authentication_error: authentication_message = str(authentication_error).lower() if "withdrawal" in authentication_message: - return _invalid_state(protocol_models.AccountStatusMessage.REVOKE_API_WITHDRAWAL_RIGHTS) + return _invalid_state(protocol_models.AccountStatusMessage.REVOKE_API_WITHDRAWAL_RIGHTS), None if any(permission_keyword in authentication_message for permission_keyword in ("permission", "trading")): - return _invalid_state(protocol_models.AccountStatusMessage.MISSING_API_TRADING_RIGHTS) - return _invalid_state(protocol_models.AccountStatusMessage.INVALID_API_KEYS) + return _invalid_state(protocol_models.AccountStatusMessage.MISSING_API_TRADING_RIGHTS), None + return _invalid_state(protocol_models.AccountStatusMessage.INVALID_API_KEYS), None except Exception: - return _invalid_state(protocol_models.AccountStatusMessage.INTERNAL_SERVER_ERROR) + return _invalid_state(protocol_models.AccountStatusMessage.INTERNAL_SERVER_ERROR), None + + +def _balance_currency_holdings(balance: dict) -> list[tuple[str, float, float]]: + holdings: list[tuple[str, float, float]] = [] + for symbol, amounts in balance.items(): + if not isinstance(amounts, dict): + continue + total_amount = float(amounts.get(commons_constants.PORTFOLIO_TOTAL) or 0) + if total_amount == 0: + continue + available_amount = float( + amounts.get(commons_constants.PORTFOLIO_AVAILABLE) + or amounts.get("free") + or 0 + ) + holdings.append((str(symbol), total_amount, available_amount)) + return holdings + + +def _assets_from_balance(balance: dict) -> list[protocol_models.DetailedAsset]: + return [ + protocol_models.DetailedAsset( + symbol=holding_symbol, + total=total_amount, + available=available_amount, + ) + for holding_symbol, total_amount, available_amount in _balance_currency_holdings(balance) + ] def _invalid_state( diff --git a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/util/action_details_factory.py b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/util/action_details_factory.py index 44b73bf04..584e6a937 100644 --- a/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/util/action_details_factory.py +++ b/packages/node/octobot_node/scheduler/user_actions/user_actions_executor/util/action_details_factory.py @@ -12,6 +12,8 @@ import octobot_trading.exchanges.util.exchange_data as exchange_data_module import octobot_node.errors as node_errors +import octobot_node.scheduler.user_actions.user_actions_executor.util.account_authentication_resolver as account_authentication_resolver + _ACTION_ID_INIT = "action_init" _ACTION_ID_MAIN = "action_1" @@ -30,29 +32,28 @@ def init_action_factory( automation_id: str, protocol_account: protocol_models.Account, strategy_reference: protocol_models.StrategyReference, + wallet_address: str, + reference_market: str, ) -> flow_entities.AbstractActionDetails: """ Build an init APPLY_CONFIGURATION action like flow functional tests, but sourced from AccountProvider. """ - if protocol_account.details is None or protocol_account.details.actual_instance is None: + if protocol_account.specifics is None or protocol_account.specifics.actual_instance is None: raise node_errors.InvalidAutomationConfigurationError( - "Account.details.actual_instance is required to build init configuration." + "Account.specifics.actual_instance is required to build init configuration." ) - details_instance = protocol_account.details.actual_instance - if not isinstance(details_instance, protocol_models.ExchangeAccount): + specifics_instance = protocol_account.specifics.actual_instance + if not isinstance(specifics_instance, protocol_models.ExchangeAccount): raise node_errors.InvalidAutomationConfigurationError( - f"Only exchange accounts are supported for automations, got {type(details_instance).__name__}" + f"Only exchange accounts are supported for automations, got {type(specifics_instance).__name__}" ) - portfolio_content = _portfolio_content_from_assets(details_instance.assets or []) - reference_unit = _infer_reference_unit(details_instance.assets or []) - if reference_unit is None: - raise node_errors.InvalidAutomationConfigurationError( - "Unable to infer portfolio unit/reference market from account assets (no Asset.unit set)." - ) + portfolio_assets = _portfolio_assets_from_account(protocol_account) + portfolio_content = _portfolio_content_from_detailed_assets(portfolio_assets) base_exchange_config = exchange_protocol_account_to_apply_configuration_dict( protocol_account, - portfolio_unit=reference_unit, + wallet_address=wallet_address, + reference_market=reference_market, ) automation_metadata = flow_entities.AutomationMetadata( @@ -164,13 +165,19 @@ def market_making_action_factory( init_action: flow_entities.AbstractActionDetails, market_making_configuration: protocol_models.MarketMakingConfiguration, protocol_account: protocol_models.Account, + wallet_address: str, + reference_market: str, ) -> flow_entities.AbstractActionDetails: profile_data = market_making_profile_data_factory( protocol_account=protocol_account, market_making_configuration=market_making_configuration, + reference_market=reference_market, ) profile_data_dict = profile_data.to_dict(include_default_values=False) - exchange_auth_data = _exchange_auth_data_list_from_protocol_account(protocol_account) + exchange_auth_data = _exchange_auth_data_list_from_protocol_account( + protocol_account, + wallet_address, + ) exchange_auth_segment = dsl_interpreter.format_parameter_value(exchange_auth_data) run_dsl = ( "run_octobot_process(" @@ -188,24 +195,25 @@ def market_making_action_factory( def exchange_protocol_account_to_apply_configuration_dict( protocol_account: protocol_models.Account, *, + wallet_address: str, account_id_override: str | None = None, - portfolio_unit: str | None = None, + reference_market: str | None = None, ) -> dict: """ Build an AutomationState-shaped dict fragment for AutomationConfigurationUpdater: only ``exchange_account_details`` is populated from a protocol Account. """ - if protocol_account.details is None or protocol_account.details.actual_instance is None: + if protocol_account.specifics is None or protocol_account.specifics.actual_instance is None: raise node_errors.InvalidUserActionPayloadError( - "Account.details.actual_instance is required for exchange account user actions." + "Account.specifics.actual_instance is required for exchange account user actions." ) - details_instance = protocol_account.details.actual_instance - if not isinstance(details_instance, protocol_models.ExchangeAccount): + specifics_instance = protocol_account.specifics.actual_instance + if not isinstance(specifics_instance, protocol_models.ExchangeAccount): raise node_errors.InvalidUserActionPayloadError( - f"Only EXCHANGE accounts are supported for this translation; got {type(details_instance).__name__}." + f"Only EXCHANGE accounts are supported for this translation; got {type(specifics_instance).__name__}." ) - exchange_payload = details_instance + exchange_payload = specifics_instance account_identifier = account_id_override if account_id_override is not None else protocol_account.id exchange_details = commons_profile_data.ExchangeData(internal_name=exchange_payload.exchange) @@ -214,10 +222,14 @@ def exchange_protocol_account_to_apply_configuration_dict( if protocol_account.is_simulated: auth_details = exchange_data_module.ExchangeAuthDetails() else: + authentication = account_authentication_resolver.get_exchange_authentication( + wallet_address, + protocol_account, + ) auth_details = exchange_data_module.ExchangeAuthDetails( - api_key=exchange_payload.api_key, - api_secret=exchange_payload.api_secret, - api_password=exchange_payload.api_passphrase or "", + api_key=authentication.api_key, + api_secret=authentication.api_secret, + api_password=authentication.api_passphrase or "", ) exchange_account_details = flow_entities.ExchangeAccountDetails( @@ -228,7 +240,7 @@ def exchange_protocol_account_to_apply_configuration_dict( ), exchange_details=exchange_details, auth_details=auth_details, - portfolio=flow_entities.ExchangeAccountPortfolio(unit=portfolio_unit or ""), + portfolio=flow_entities.ExchangeAccountPortfolio(unit=reference_market or ""), ) return { @@ -240,15 +252,16 @@ def market_making_profile_data_factory( *, protocol_account: protocol_models.Account, market_making_configuration: protocol_models.MarketMakingConfiguration, + reference_market: str, ) -> commons_profile_data.ProfileData: - if protocol_account.details is None or protocol_account.details.actual_instance is None: + if protocol_account.specifics is None or protocol_account.specifics.actual_instance is None: raise node_errors.InvalidAutomationConfigurationError( - "Account.details.actual_instance is required to build market making profile data." + "Account.specifics.actual_instance is required to build market making profile data." ) - details_instance = protocol_account.details.actual_instance - if not isinstance(details_instance, protocol_models.ExchangeAccount): + specifics_instance = protocol_account.specifics.actual_instance + if not isinstance(specifics_instance, protocol_models.ExchangeAccount): raise node_errors.InvalidAutomationConfigurationError( - f"Market making requires an exchange account; got {type(details_instance).__name__}" + f"Market making requires an exchange account; got {type(specifics_instance).__name__}" ) symbols = [entry.trading_pair for entry in (market_making_configuration.pair_settings or [])] @@ -257,13 +270,10 @@ def market_making_profile_data_factory( "MarketMakingConfiguration.pair_settings must not be empty." ) - reference_market = _infer_reference_unit(details_instance.assets or []) - if reference_market is None: - raise node_errors.InvalidAutomationConfigurationError( - "Unable to infer reference market from account assets for market making." - ) + reference_market_value = reference_market - starting_portfolio = {asset.symbol: float(asset.total) for asset in (details_instance.assets or [])} + portfolio_assets = _portfolio_assets_from_account(protocol_account) + starting_portfolio = {asset.symbol: float(asset.total) for asset in portfolio_assets} profile_identifier = f"market_making_{protocol_account.id}" profile_details = commons_profile_data.ProfileDetailsData( @@ -276,7 +286,7 @@ def market_making_profile_data_factory( enabled=True, ) exchange_entry = commons_profile_data.ExchangeData( - internal_name=details_instance.exchange, + internal_name=specifics_instance.exchange, exchange_type=commons_constants.DEFAULT_EXCHANGE_TYPE, ) trader = commons_profile_data.TraderData(enabled=False) @@ -287,7 +297,7 @@ def market_making_profile_data_factory( taker_fees=0.0, ) trading = commons_profile_data.TradingData( - reference_market=reference_market, + reference_market=reference_market_value, risk=1.0, paused=False, ) @@ -329,7 +339,20 @@ def _translate_workflow_action( ) -def _portfolio_content_from_assets(assets: list[protocol_models.Asset]) -> dict[str, dict[str, float]]: +def _portfolio_assets_from_account( + protocol_account: protocol_models.Account, +) -> list[protocol_models.DetailedAsset]: + account_assets = protocol_account.assets + if not account_assets: + raise node_errors.InvalidAutomationConfigurationError( + "Account.assets is required to build automation configuration." + ) + return list(account_assets) + + +def _portfolio_content_from_detailed_assets( + assets: list[protocol_models.DetailedAsset], +) -> dict[str, dict[str, float]]: content: dict[str, dict[str, float]] = {} for asset in assets: content[str(asset.symbol)] = { @@ -339,40 +362,36 @@ def _portfolio_content_from_assets(assets: list[protocol_models.Asset]) -> dict[ return content -def _infer_reference_unit(assets: list[protocol_models.Asset]) -> str | None: - for asset in assets: - if asset.unit: - return str(asset.unit) - return None - - def _exchange_auth_data_list_from_protocol_account( protocol_account: protocol_models.Account, + wallet_address: str, ) -> list[dict] | None: """ - Build ``exchange_auth_data`` for ``run_octobot_process`` from the same protocol - ``Account`` payload that ``AccountProvider`` returned (no extra provider fetch). - Simulated accounts omit credentials (``None``). For non-simulated accounts, - ``Account.details`` must wrap an ``ExchangeAccount`` or this raises. + Build ``exchange_auth_data`` for ``run_octobot_process`` from AccountProvider data + and AccountAuthenticationProvider credentials. """ if protocol_account.is_simulated: return None - account_details = protocol_account.details - if account_details is None or account_details.actual_instance is None: + account_specifics = protocol_account.specifics + if account_specifics is None or account_specifics.actual_instance is None: raise node_errors.InvalidAutomationConfigurationError( - "Account.details.actual_instance is required to build exchange_auth_data for a live account." + "Account.specifics.actual_instance is required to build exchange_auth_data for a live account." ) - details_instance = account_details.actual_instance - if not isinstance(details_instance, protocol_models.ExchangeAccount): + specifics_instance = account_specifics.actual_instance + if not isinstance(specifics_instance, protocol_models.ExchangeAccount): raise node_errors.InvalidAutomationConfigurationError( - f"exchange_auth_data requires an exchange account; got {type(details_instance).__name__}." + f"exchange_auth_data requires an exchange account; got {type(specifics_instance).__name__}." ) + authentication = account_authentication_resolver.get_exchange_authentication( + wallet_address, + protocol_account, + ) return [ { - "internal_name": details_instance.exchange, - "api_key": details_instance.api_key, - "api_secret": details_instance.api_secret, - "api_password": details_instance.api_passphrase or "", + "internal_name": specifics_instance.exchange, + "api_key": authentication.api_key, + "api_secret": authentication.api_secret, + "api_password": authentication.api_passphrase or "", "exchange_type": commons_constants.DEFAULT_EXCHANGE_TYPE, "sandboxed": False, } diff --git a/packages/node/tests/functional_tests/grid_workflow_simulator_test_util.py b/packages/node/tests/functional_tests/grid_workflow_simulator_test_util.py index e54b6a3ce..1e5206296 100644 --- a/packages/node/tests/functional_tests/grid_workflow_simulator_test_util.py +++ b/packages/node/tests/functional_tests/grid_workflow_simulator_test_util.py @@ -96,16 +96,6 @@ def protocol_exchange_account_for_grid_functional(*, usdc_total: float) -> proto trading_type=protocol_models_module.TradingType.SPOT, exchange=exchange_internal_name(), remote_account_id="functional-test-account", - api_key="functional-key", - api_secret="functional-secret", - assets=[ - protocol_models_module.Asset( - symbol="USDC", - total=usdc_total, - available=usdc_total, - unit="USDC", - ) - ], ) def protocol_account_for_functional( @@ -120,7 +110,14 @@ def protocol_account_for_functional( is_simulated=True, created_at=_FUNCTIONAL_PROTOCOL_ACCOUNT_TS, updated_at=_FUNCTIONAL_PROTOCOL_ACCOUNT_TS, - details=protocol_models_module.AccountDetails( + assets=[ + protocol_models_module.DetailedAsset( + symbol="USDC", + total=usdc_total, + available=usdc_total, + ) + ], + specifics=protocol_models_module.AccountSpecifics( actual_instance=protocol_exchange_account_for_grid_functional(usdc_total=usdc_total), ), ) @@ -140,6 +137,7 @@ def seeded_grid_strategy_for_functional_wallet( id=stored_strategy_id, version=SIMULATOR_FUNCTIONAL_STRATEGY_VERSION, name="Simulator grid automation strategy", + reference_market="USDC", configuration=protocol_models_module.StrategyConfiguration( grid_configuration_matching_simulator_constants(), ), @@ -158,6 +156,7 @@ def seeded_copy_follower_strategy_for_functional_wallet( id=SIMULATOR_COPY_FOLLOWER_STORED_STRATEGY_ID, version=SIMULATOR_FUNCTIONAL_STRATEGY_VERSION, name="Simulator copy-follower automation strategy", + reference_market="USDC", configuration=protocol_models_module.StrategyConfiguration( protocol_models_module.CopyConfiguration( configuration_type=protocol_models_module.ActionConfigurationType.COPY, @@ -449,8 +448,11 @@ def assert_protocol_automation_matches_exchange_account_elements( f"unexpected OrderSummary.symbol {order_summary.symbol!r}; expected {expected_order_symbol!r}" ) content = _portfolio_content_from_exchange_elements(exchange_account_elements) - assets = protocol_automation.assets or [] - assets_by_symbol = {asset.symbol: asset for asset in assets} + protocol_assets = protocol_automation.assets + assert protocol_assets is not None, ( + "expected AutomationState.assets to be set" + ) + assets_by_symbol = {asset.symbol: asset for asset in protocol_assets} for symbol, row in content.items(): matching_asset = assets_by_symbol.get(symbol) assert matching_asset is not None, ( diff --git a/packages/node/tests/functional_tests/test_accounts_CRUD_operations.py b/packages/node/tests/functional_tests/test_accounts_CRUD_operations.py index 9eb8b407c..5f55a8d98 100644 --- a/packages/node/tests/functional_tests/test_accounts_CRUD_operations.py +++ b/packages/node/tests/functional_tests/test_accounts_CRUD_operations.py @@ -11,6 +11,7 @@ import octobot.community.authentication as community_authentication_module import octobot.community.local_authenticator as local_authenticator_module import octobot_commons.user_root_folder_provider as user_root_folder_provider_module +import octobot_commons.constants as commons_constants import octobot_node.constants as octobot_node_constants import octobot_node.errors as node_errors_module import octobot_node.scheduler @@ -29,6 +30,11 @@ _WORKFLOW_RESULT_TIMEOUT_SECONDS = 120.0 _USER_ACTION_LIST_POLL_TIMEOUT_SECONDS = 15.0 +_FUNCTIONAL_USDT_HOLDINGS = 1000.0 +_FUNCTIONAL_BTC_HOLDINGS = 0.5 +_FUNCTIONAL_ETH_HOLDINGS = 2.0 +_FUNCTIONAL_SOL_HOLDINGS = 10.0 + _REAL_UPDATE_ACCOUNT_STATE = account_state_updater_module.update_account_state @@ -60,8 +66,30 @@ async def _stub_load_symbol_markets_no_network(self, reload=False, market_filter """ self.client.markets = { "BTC/USDT": {"symbol": "BTC/USDT", "active": True, "spot": True}, + "SOL/BTC": {"symbol": "SOL/BTC", "active": True, "spot": True}, + } + self.client.symbols = ["BTC/USDT", "SOL/BTC"] + + +async def _stub_get_balance_no_network(self, **kwargs): + return { + "USDT": { + commons_constants.PORTFOLIO_TOTAL: _FUNCTIONAL_USDT_HOLDINGS, + commons_constants.PORTFOLIO_AVAILABLE: _FUNCTIONAL_USDT_HOLDINGS, + }, + "BTC": { + commons_constants.PORTFOLIO_TOTAL: _FUNCTIONAL_BTC_HOLDINGS, + commons_constants.PORTFOLIO_AVAILABLE: _FUNCTIONAL_BTC_HOLDINGS, + }, + "ETH": { + commons_constants.PORTFOLIO_TOTAL: _FUNCTIONAL_ETH_HOLDINGS, + commons_constants.PORTFOLIO_AVAILABLE: _FUNCTIONAL_ETH_HOLDINGS, + }, + "SOL": { + commons_constants.PORTFOLIO_TOTAL: _FUNCTIONAL_SOL_HOLDINGS, + commons_constants.PORTFOLIO_AVAILABLE: _FUNCTIONAL_SOL_HOLDINGS, + }, } - self.client.symbols = ["BTC/USDT"] async def _run_user_action_to_completion( @@ -81,10 +109,13 @@ async def _run_user_action_to_completion( return workflow_id -async def _update_account_state_fail_doomed_create(account: protocol_models.Account) -> protocol_models.Account: +async def _update_account_state_fail_doomed_create( + account: protocol_models.Account, + wallet_address: str, +) -> protocol_models.Account: if account.id == _DOOMED_ACCOUNT_ID: raise node_errors_module.WorkflowInputError("forced doomed create failure") - return await _REAL_UPDATE_ACCOUNT_STATE(account) + return await _REAL_UPDATE_ACCOUNT_STATE(account, wallet_address) def _assert_listed_user_actions_match_expected_id_status_pairs( @@ -96,6 +127,30 @@ def _assert_listed_user_actions_match_expected_id_status_pairs( assert actual_sorted == expected_sorted +def _assert_functional_assets( + assets: list[protocol_models.DetailedAsset] | None, +) -> None: + assert assets is not None + assets_by_symbol = {asset.symbol: asset for asset in assets} + assert set(assets_by_symbol) == {"USDT", "BTC", "ETH", "SOL"} + + usdt_asset = assets_by_symbol["USDT"] + assert usdt_asset.total == pytest.approx(_FUNCTIONAL_USDT_HOLDINGS) + assert usdt_asset.available == pytest.approx(_FUNCTIONAL_USDT_HOLDINGS) + + bitcoin_asset = assets_by_symbol["BTC"] + assert bitcoin_asset.total == pytest.approx(_FUNCTIONAL_BTC_HOLDINGS) + assert bitcoin_asset.available == pytest.approx(_FUNCTIONAL_BTC_HOLDINGS) + + ethereum_asset = assets_by_symbol["ETH"] + assert ethereum_asset.total == pytest.approx(_FUNCTIONAL_ETH_HOLDINGS) + assert ethereum_asset.available == pytest.approx(_FUNCTIONAL_ETH_HOLDINGS) + + sol_asset = assets_by_symbol["SOL"] + assert sol_asset.total == pytest.approx(_FUNCTIONAL_SOL_HOLDINGS) + assert sol_asset.available == pytest.approx(_FUNCTIONAL_SOL_HOLDINGS) + + @pytest.mark.asyncio class TestExecuteUserActionAccountCrud: async def test_create_edit_refresh_delete_accounts_on_temp_filesystem( @@ -122,8 +177,6 @@ def build_exchange_account() -> protocol_models.ExchangeAccount: trading_type=protocol_models.TradingType.SPOT, exchange="binanceus", remote_account_id="functional-test-remote-account", - api_key="functional-test-api-key", - api_secret="functional-test-api-secret", ) def build_account(*, account_id: str, account_name: str) -> protocol_models.Account: @@ -133,7 +186,7 @@ def build_account(*, account_id: str, account_name: str) -> protocol_models.Acco is_simulated=True, created_at=sample_timestamp, updated_at=sample_timestamp, - details=protocol_models.AccountDetails( + specifics=protocol_models.AccountSpecifics( actual_instance=build_exchange_account(), ), ) @@ -230,11 +283,6 @@ def build_delete_user_action(*, user_action_id: str, account_id: str) -> protoco "instance", return_value=account_provider, ), - mock.patch.object( - account_state_updater_module, - "_request_exchange_to_ensure_authentication", - new=mock.AsyncMock(return_value=None), - ), mock.patch.object( account_state_updater_module, "_ensure_api_key_permissions", @@ -250,6 +298,11 @@ def build_delete_user_action(*, user_action_id: str, account_id: str) -> protoco "load_symbol_markets", _stub_load_symbol_markets_no_network, ), + mock.patch.object( + ccxt_connector_module.CCXTConnector, + "get_balance", + _stub_get_balance_no_network, + ), ): # Step: Patches above strip network/CCXT and steer ``update_account_state`` / permissions for this scenario. @@ -323,6 +376,7 @@ def build_delete_user_action(*, user_action_id: str, account_id: str) -> protoco assert persisted_created_account.state.status == protocol_models.AccountStatus.VALID assert persisted_created_account.state.message == protocol_models.AccountStatusMessage.VALID assert len(account_provider.list_items(wallet_address)) == 1 + _assert_functional_assets(persisted_created_account.assets) # Step 3 — Edit: enqueue workflow only first; poll listings mid retry before awaiting terminal output. edited_account = build_account( @@ -422,6 +476,7 @@ def build_delete_user_action(*, user_action_id: str, account_id: str) -> protoco assert persisted_refreshed_account.state is not None assert persisted_refreshed_account.state.status == protocol_models.AccountStatus.VALID assert persisted_refreshed_account.state.message == protocol_models.AccountStatusMessage.VALID + _assert_functional_assets(persisted_refreshed_account.assets) # Step 5 — Delete: remove account from provider; listing gains COMPLETED delete row. delete_action = build_delete_user_action( diff --git a/packages/node/tests/protocol/test_accounts_trading.py b/packages/node/tests/protocol/test_accounts_trading.py new file mode 100644 index 000000000..4fdbf45ee --- /dev/null +++ b/packages/node/tests/protocol/test_accounts_trading.py @@ -0,0 +1,60 @@ +# Drakkar-Software OctoBot-Node +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License version 3.0 of the License, or (at your option) any later version. + +import mock + +import octobot_sync.constants as sync_constants +import octobot_sync.sync.collection_backend.errors as collection_errors + +import octobot_node.protocol.accounts_trading as accounts_trading_module + +_TEST_WALLET_ADDRESS = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb" +_TEST_ACCOUNT_ID = "acc-trading-1" +_SAMPLE_ENCRYPTED_BLOB = { + sync_constants.BLOB_IV_KEY: "sample-iv", + sync_constants.BLOB_DATA_KEY: "sample-data", +} + + +class TestGetAccountTradingStateEncrypted: + """Checks :func:`octobot_node.protocol.accounts_trading.get_account_trading_state_encrypted`.""" + + def test_passes_address_and_account_id_to_provider_and_returns_blob(self): + provider_stub = mock.Mock() + provider_stub.load_state_encrypted = mock.Mock(return_value=_SAMPLE_ENCRYPTED_BLOB) + with mock.patch.object( + accounts_trading_module.trading_provider.AccountTradingProvider, + "instance", + return_value=provider_stub, + ): + encrypted_state = accounts_trading_module.get_account_trading_state_encrypted( + _TEST_WALLET_ADDRESS, + _TEST_ACCOUNT_ID, + ) + + provider_stub.load_state_encrypted.assert_called_once_with( + _TEST_WALLET_ADDRESS, + _TEST_ACCOUNT_ID, + ) + assert encrypted_state == _SAMPLE_ENCRYPTED_BLOB + + def test_returns_none_when_provider_raises_collection_no_data_error(self): + provider_stub = mock.Mock() + provider_stub.load_state_encrypted = mock.Mock( + side_effect=collection_errors.CollectionNoDataError("missing trading state"), + ) + with mock.patch.object( + accounts_trading_module.trading_provider.AccountTradingProvider, + "instance", + return_value=provider_stub, + ): + encrypted_state = accounts_trading_module.get_account_trading_state_encrypted( + _TEST_WALLET_ADDRESS, + _TEST_ACCOUNT_ID, + ) + + assert encrypted_state is None diff --git a/packages/node/tests/protocol/test_automations.py b/packages/node/tests/protocol/test_automations.py index 4609a1ecd..a0bc2895a 100644 --- a/packages/node/tests/protocol/test_automations.py +++ b/packages/node/tests/protocol/test_automations.py @@ -241,7 +241,6 @@ def test_assets_orders_positions_trades_mapping(self): } elements = flow_entities.ExchangeAccountElements() elements.portfolio.content = portfolio_content - elements.portfolio.asset_values = {"BTC": 100.5} elements.orders.open_orders = [open_order] elements.orders.missing_orders = [missing_order] elements.positions = [position_details] @@ -261,8 +260,8 @@ def test_assets_orders_positions_trades_mapping(self): assert filled.assets is not None bitcoin_asset = filled.assets[0] assert bitcoin_asset.symbol == "BTC" - assert bitcoin_asset.value == 100.5 - assert bitcoin_asset.unit == "USDT" + assert bitcoin_asset.total == 2.0 + assert bitcoin_asset.available == 1.0 assert filled.orders is not None assert len(filled.orders) == 1 assert filled.orders[0].id == "oid-1" @@ -271,7 +270,7 @@ def test_assets_orders_positions_trades_mapping(self): assert filled.trades is not None assert filled.trades[0].id == "t1" - def test_unit_unset_without_exchange_account_details(self): + def test_assets_without_enrichment(self): portfolio_content = { "BTC": { octobot_commons_constants.PORTFOLIO_AVAILABLE: 1.0, @@ -285,7 +284,9 @@ def test_unit_unset_without_exchange_account_details(self): flow_state = flow_entities.AutomationState(automation=automation) filled = automations_protocol._fill_protocol_automation_state(_minimal_protocol_base(), flow_state) assert filled.assets is not None - assert filled.assets[0].unit is None + assert filled.assets[0].symbol == "BTC" + assert filled.assets[0].total == 2.0 + assert filled.assets[0].available == 1.0 class TestFillProtocolAutomationStateEmpties: diff --git a/packages/node/tests/scheduler/test_user_action_executor_factory.py b/packages/node/tests/scheduler/test_user_action_executor_factory.py index c08d3aa49..63f309224 100644 --- a/packages/node/tests/scheduler/test_user_action_executor_factory.py +++ b/packages/node/tests/scheduler/test_user_action_executor_factory.py @@ -30,8 +30,6 @@ def _exchange_account_payload() -> protocol_models.ExchangeAccount: trading_type=protocol_models.TradingType.SPOT, exchange="binanceus", remote_account_id="remote-1", - api_key="k", - api_secret="s", ) @classmethod @@ -42,7 +40,7 @@ def _minimal_exchange_account(cls, *, account_identifier: str) -> protocol_model is_simulated=True, created_at=datetime.datetime(2026, 2, 2, 10, 0, 0, tzinfo=datetime.UTC), updated_at=datetime.datetime(2026, 2, 3, 15, 30, 0, tzinfo=datetime.UTC), - details=protocol_models.AccountDetails( + specifics=protocol_models.AccountSpecifics( actual_instance=cls._exchange_account_payload(), ), ) diff --git a/packages/node/tests/scheduler/user_actions/user_actions_executor/account_executor_test_utils.py b/packages/node/tests/scheduler/user_actions/user_actions_executor/account_executor_test_utils.py index c8d0d5dba..550b407c2 100644 --- a/packages/node/tests/scheduler/user_actions/user_actions_executor/account_executor_test_utils.py +++ b/packages/node/tests/scheduler/user_actions/user_actions_executor/account_executor_test_utils.py @@ -30,19 +30,35 @@ def exchange_account_payload() -> protocol_models.ExchangeAccount: trading_type=protocol_models.TradingType.SPOT, exchange="binanceus", remote_account_id="remote-1", + ) + + +def assets_payload() -> list[protocol_models.DetailedAsset]: + return [ + protocol_models.DetailedAsset( + symbol="USDT", + total=1000.0, + available=1000.0, + ) + ] + + +def authentication_payload() -> protocol_models.AccountAuthentication: + return protocol_models.AccountAuthentication( api_key="k", api_secret="s", ) -def minimal_exchange_account(*, account_id: str) -> protocol_models.Account: +def minimal_exchange_account(*, account_id: str, is_simulated: bool = True) -> protocol_models.Account: return protocol_models.Account( id=account_id, name="Test account", - is_simulated=True, + is_simulated=is_simulated, created_at=_ACCOUNT_TS, updated_at=_ACCOUNT_TS, - details=protocol_models.AccountDetails( + assets=assets_payload(), + specifics=protocol_models.AccountSpecifics( actual_instance=exchange_account_payload(), ), ) @@ -55,7 +71,7 @@ def minimal_blockchain_account(*, account_id: str) -> protocol_models.Account: is_simulated=False, created_at=_ACCOUNT_TS, updated_at=_ACCOUNT_TS_OUT, - details=protocol_models.AccountDetails( + specifics=protocol_models.AccountSpecifics( actual_instance=protocol_models.BlockchainAccount( account_type=protocol_models.AccountType.BLOCKCHAIN, blockchain="ethereum", diff --git a/packages/node/tests/scheduler/user_actions/user_actions_executor/test_channel_user_action_executor_get_error_message.py b/packages/node/tests/scheduler/user_actions/user_actions_executor/test_channel_user_action_executor_get_error_message.py index 8f5351b4a..5aa98832d 100644 --- a/packages/node/tests/scheduler/user_actions/user_actions_executor/test_channel_user_action_executor_get_error_message.py +++ b/packages/node/tests/scheduler/user_actions/user_actions_executor/test_channel_user_action_executor_get_error_message.py @@ -46,6 +46,13 @@ def test_account_not_found(self): resolved = executor._get_error_message(node_errors.AccountNotFoundError("missing")) assert resolved == protocol_models.AutomationActionResultErrorMessage.ACCOUNT_NOT_FOUND + def test_account_authentication_details_not_found(self): + executor = create_automation_executor_module.CreateAutomationActionExecutor(_WALLET) + resolved = executor._get_error_message( + node_errors.AccountAuthenticationNotFoundError("missing auth") + ) + assert resolved == protocol_models.AutomationActionResultErrorMessage.ACCOUNT_AUTHENTICATION_DETAILS_NOT_FOUND + def test_unsupported_automation_configuration_type(self): executor = stop_automation_executor.StopAutomationActionExecutor(_WALLET) resolved = executor._get_error_message( @@ -82,6 +89,16 @@ def test_account_not_found(self): resolved = executor._get_error_message(node_errors.AccountNotFoundError("lookup failed")) assert resolved == protocol_models.AccountActionResultErrorMessage.ACCOUNT_NOT_FOUND + def test_account_authentication_details_not_found(self): + executor = create_account_executor.CreateAccountActionExecutor(_WALLET) + resolved = executor._get_error_message( + node_errors.AccountAuthenticationNotFoundError("missing auth") + ) + assert ( + resolved + == protocol_models.AccountActionResultErrorMessage.ACCOUNT_AUTHENTICATION_DETAILS_NOT_FOUND + ) + def test_invalid_user_action_payload(self): executor = create_account_executor.CreateAccountActionExecutor(_WALLET) resolved = executor._get_error_message(node_errors.InvalidUserActionPayloadError("bad")) diff --git a/packages/node/tests/scheduler/user_actions/user_actions_executor/test_create_account.py b/packages/node/tests/scheduler/user_actions/user_actions_executor/test_create_account.py index 85686ff63..b5a725d41 100644 --- a/packages/node/tests/scheduler/user_actions/user_actions_executor/test_create_account.py +++ b/packages/node/tests/scheduler/user_actions/user_actions_executor/test_create_account.py @@ -141,3 +141,40 @@ async def test_raises_for_unsupported_blockchain_account(self): expect_error_details=True, expected_error_message=protocol_models.AccountActionResultErrorMessage.INVALID_CONFIGURATION, ) + + @pytest.mark.asyncio + async def test_fails_when_authentication_details_missing_for_live_account(self): + account_model = account_executor_test_utils.minimal_exchange_account( + account_id="live-acc", + is_simulated=False, + ) + inner = protocol_models.CreateAccountConfiguration( + action_type=protocol_models.UserActionType.ACCOUNT_CREATE, + configuration=account_model, + ) + user_action = protocol_models.UserAction(id="ua-create-no-auth", configuration=account_executor_test_utils.wrap_configuration(inner)) + provider_mock = mock.Mock() + with ( + mock.patch( + "octobot_sync.sync.collection_providers.AccountProvider.instance", + return_value=provider_mock, + ), + mock.patch.object( + account_state_updater_module, + "update_account_state", + new=mock.AsyncMock( + side_effect=node_errors.AccountAuthenticationNotFoundError("missing auth"), + ), + ), + ): + executor = create_account_executor.CreateAccountActionExecutor(account_executor_test_utils.WALLET_ADDRESS) + with pytest.raises(node_errors.AccountAuthenticationNotFoundError): + await executor.execute(user_action) + provider_mock.create_item.assert_not_called() + provider_assertions.assert_user_action_terminal_state( + user_action=user_action, + expected_status=protocol_models.UserActionStatus.FAILED, + result_channel="account", + expect_error_details=True, + expected_error_message=protocol_models.AccountActionResultErrorMessage.ACCOUNT_AUTHENTICATION_DETAILS_NOT_FOUND, + ) diff --git a/packages/node/tests/scheduler/user_actions/user_actions_executor/test_create_automation.py b/packages/node/tests/scheduler/user_actions/user_actions_executor/test_create_automation.py index 515f67d82..b9eebb07f 100644 --- a/packages/node/tests/scheduler/user_actions/user_actions_executor/test_create_automation.py +++ b/packages/node/tests/scheduler/user_actions/user_actions_executor/test_create_automation.py @@ -73,6 +73,7 @@ def _stored_strategy_matching_reference( id=strategy_reference.id, version=strategy_reference.version, name="Seeded automation strategy", + reference_market="USDT", configuration=protocol_models.StrategyConfiguration(configuration_instance), ) @@ -88,22 +89,19 @@ def _minimal_exchange_account(*, account_id: str) -> protocol_models.Account: is_simulated=True, created_at=_TEST_ACCOUNT_TS, updated_at=_TEST_ACCOUNT_TS, - details=protocol_models.AccountDetails( + assets=[ + protocol_models.DetailedAsset( + symbol="USDT", + total=1000.0, + available=1000.0, + ) + ], + specifics=protocol_models.AccountSpecifics( actual_instance=protocol_models.ExchangeAccount( account_type=protocol_models.AccountType.EXCHANGE, trading_type=protocol_models.TradingType.SPOT, exchange="binanceus", remote_account_id="remote-1", - api_key="k", - api_secret="s", - assets=[ - protocol_models.Asset( - symbol="USDT", - total=1000.0, - available=1000.0, - unit="USDT", - ) - ], ), ), ) @@ -500,6 +498,7 @@ def test_market_making_returns_init_and_run_octobot_process(self): expected_profile = action_details_factory.market_making_profile_data_factory( protocol_account=account, market_making_configuration=market_making_configuration, + reference_market=stored.reference_market, ) expected_profile_dict = expected_profile.to_dict(include_default_values=False) expected_exchange_auth_segment = dsl_interpreter.format_parameter_value(None) @@ -621,6 +620,7 @@ def test_strategy_version_mismatch_raises_mapped_user_action_error_async(self): id=strat_ref.id, version="different-version", name="Stale version", + reference_market="USDT", configuration=protocol_models.StrategyConfiguration(idx), ) diff --git a/packages/node/tests/scheduler/user_actions/user_actions_executor/test_refresh_accounts.py b/packages/node/tests/scheduler/user_actions/user_actions_executor/test_refresh_accounts.py index 3692facbc..6ba9d914e 100644 --- a/packages/node/tests/scheduler/user_actions/user_actions_executor/test_refresh_accounts.py +++ b/packages/node/tests/scheduler/user_actions/user_actions_executor/test_refresh_accounts.py @@ -167,3 +167,41 @@ async def test_raises_for_unsupported_blockchain_account(self): expect_error_details=True, expected_error_message=protocol_models.AccountActionResultErrorMessage.INVALID_CONFIGURATION, ) + + @pytest.mark.asyncio + async def test_fails_when_authentication_details_missing_for_live_account(self): + account_model = account_executor_test_utils.minimal_exchange_account( + account_id="live-acc", + is_simulated=False, + ) + refresh_inner = protocol_models.RefreshAccountsConfiguration( + action_type=protocol_models.UserActionType.ACCOUNTS_REFRESH, + account_ids=["live-acc"], + ) + user_action = protocol_models.UserAction(id="ua-refresh-no-auth", configuration=account_executor_test_utils.wrap_configuration(refresh_inner)) + provider_mock = mock.Mock() + provider_mock.get_item.return_value = account_model + with ( + mock.patch( + "octobot_sync.sync.collection_providers.AccountProvider.instance", + return_value=provider_mock, + ), + mock.patch.object( + account_state_updater_module, + "update_account_state", + new=mock.AsyncMock( + side_effect=node_errors.AccountAuthenticationNotFoundError("missing auth"), + ), + ), + ): + executor = refresh_accounts_executor.RefreshAccountsActionExecutor(account_executor_test_utils.WALLET_ADDRESS) + with pytest.raises(node_errors.AccountAuthenticationNotFoundError): + await executor.execute(user_action) + provider_mock.update_item.assert_not_called() + provider_assertions.assert_user_action_terminal_state( + user_action=user_action, + expected_status=protocol_models.UserActionStatus.FAILED, + result_channel="account", + expect_error_details=True, + expected_error_message=protocol_models.AccountActionResultErrorMessage.ACCOUNT_AUTHENTICATION_DETAILS_NOT_FOUND, + ) diff --git a/packages/node/tests/scheduler/user_actions/user_actions_executor/test_stop_automation.py b/packages/node/tests/scheduler/user_actions/user_actions_executor/test_stop_automation.py index c3c5a08af..e65361dd1 100644 --- a/packages/node/tests/scheduler/user_actions/user_actions_executor/test_stop_automation.py +++ b/packages/node/tests/scheduler/user_actions/user_actions_executor/test_stop_automation.py @@ -150,14 +150,12 @@ async def test_invalid_payload_raises_invalid_user_action_payload(self): is_simulated=True, created_at=datetime.datetime(2026, 6, 1, 12, 0, 0, tzinfo=datetime.UTC), updated_at=datetime.datetime(2026, 6, 1, 13, 0, 0, tzinfo=datetime.UTC), - details=protocol_models.AccountDetails( + specifics=protocol_models.AccountSpecifics( actual_instance=protocol_models.ExchangeAccount( account_type=protocol_models.AccountType.EXCHANGE, trading_type=protocol_models.TradingType.SPOT, exchange="binanceus", remote_account_id="r", - api_key="k", - api_secret="s", ) ), ), diff --git a/packages/node/tests/scheduler/user_actions/user_actions_executor/util/test_account_state_updater.py b/packages/node/tests/scheduler/user_actions/user_actions_executor/util/test_account_state_updater.py index a832d7339..a617948ce 100644 --- a/packages/node/tests/scheduler/user_actions/user_actions_executor/util/test_account_state_updater.py +++ b/packages/node/tests/scheduler/user_actions/user_actions_executor/util/test_account_state_updater.py @@ -1,15 +1,47 @@ import contextlib +import datetime import mock import pytest import types +import octobot_commons.constants as commons_constants import octobot_protocol.models as protocol_models -import octobot_trading.enums as trading_enums import octobot_trading.errors as trading_errors +import octobot_trading.enums as trading_enums import octobot_node.scheduler.user_actions.user_actions_executor.util.account_state_updater as account_state_updater_module +_WALLET_ADDRESS = "0xwallet" +_ACCOUNT_TS = datetime.datetime(2026, 3, 12, 9, 0, tzinfo=datetime.UTC) + + +def _sample_account(*, is_simulated: bool = False) -> protocol_models.Account: + return protocol_models.Account( + id="acc-1", + name="Test account", + is_simulated=is_simulated, + created_at=_ACCOUNT_TS, + updated_at=_ACCOUNT_TS, + assets=[], + specifics=protocol_models.AccountSpecifics( + actual_instance=protocol_models.ExchangeAccount( + account_type=protocol_models.AccountType.EXCHANGE, + trading_type=protocol_models.TradingType.SPOT, + exchange="binanceus", + remote_account_id="remote-1", + ), + ), + ) + + +def _authentication() -> protocol_models.AccountAuthentication: + return protocol_models.AccountAuthentication( + api_key="plain-key", + api_secret="plain-secret", + ) + + class TestAccountStateUpdaterCheckExchangeAccountState: @pytest.mark.asyncio async def test_encrypts_clear_credentials_before_exchange_manager_context(self): @@ -18,9 +50,9 @@ async def test_encrypts_clear_credentials_before_exchange_manager_context(self): trading_type=protocol_models.TradingType.SPOT, exchange="binanceus", remote_account_id="remote-1", - api_key="plain-key", - api_secret="plain-secret", ) + account = _sample_account() + authentication = _authentication() encrypt_mock = mock.Mock( side_effect=lambda plain_text: ("enc:" + plain_text).encode(), ) @@ -31,6 +63,11 @@ async def fake_exchange_manager_from_exchange_data(*args, **kwargs): yield dummy_exchange_manager with ( + mock.patch.object( + account_state_updater_module.account_authentication_resolver, + "get_exchange_authentication", + return_value=authentication, + ), mock.patch.object( account_state_updater_module.fields_utils, "encrypt", @@ -55,15 +92,20 @@ async def fake_exchange_manager_from_exchange_data(*args, **kwargs): account_state_updater_module, "_check_exchange_manager_state", new=mock.AsyncMock( - return_value=protocol_models.AccountState( - status=protocol_models.AccountStatus.VALID, - message=protocol_models.AccountStatusMessage.VALID, + return_value=( + protocol_models.AccountState( + status=protocol_models.AccountStatus.VALID, + message=protocol_models.AccountStatusMessage.VALID, + ), + None, ) ), ), ): - account_state = await account_state_updater_module._check_exchange_account_state( - exchange_account + account_state, _ = await account_state_updater_module._check_exchange_account_state( + exchange_account, + account, + _WALLET_ADDRESS, ) assert account_state.status == protocol_models.AccountStatus.VALID encrypt_mock.assert_any_call("plain-key") @@ -77,6 +119,9 @@ async def test_encrypts_passphrase_when_present(self): trading_type=protocol_models.TradingType.SPOT, exchange="binanceus", remote_account_id="remote-1", + ) + account = _sample_account() + authentication = protocol_models.AccountAuthentication( api_key="plain-key", api_secret="plain-secret", api_passphrase="plain-pass", @@ -91,6 +136,11 @@ async def fake_exchange_manager_from_exchange_data(*args, **kwargs): yield dummy_exchange_manager with ( + mock.patch.object( + account_state_updater_module.account_authentication_resolver, + "get_exchange_authentication", + return_value=authentication, + ), mock.patch.object( account_state_updater_module.fields_utils, "encrypt", @@ -115,14 +165,21 @@ async def fake_exchange_manager_from_exchange_data(*args, **kwargs): account_state_updater_module, "_check_exchange_manager_state", new=mock.AsyncMock( - return_value=protocol_models.AccountState( - status=protocol_models.AccountStatus.VALID, - message=protocol_models.AccountStatusMessage.VALID, + return_value=( + protocol_models.AccountState( + status=protocol_models.AccountStatus.VALID, + message=protocol_models.AccountStatusMessage.VALID, + ), + None, ) ), ), ): - await account_state_updater_module._check_exchange_account_state(exchange_account) + await account_state_updater_module._check_exchange_account_state( + exchange_account, + account, + _WALLET_ADDRESS, + ) encrypt_mock.assert_any_call("plain-key") encrypt_mock.assert_any_call("plain-secret") encrypt_mock.assert_any_call("plain-pass") @@ -135,9 +192,8 @@ async def test_passes_futures_trading_type_as_future_exchange_type(self): trading_type=protocol_models.TradingType.FUTURES, exchange="binanceus", remote_account_id="remote-1", - api_key="plain-key", - api_secret="plain-secret", ) + account = _sample_account() encrypt_mock = mock.Mock( side_effect=lambda plain_text: ("enc:" + plain_text).encode(), ) @@ -151,6 +207,11 @@ async def fake_exchange_manager_from_exchange_data(*args, **kwargs): yield dummy_exchange_manager with ( + mock.patch.object( + account_state_updater_module.account_authentication_resolver, + "get_exchange_authentication", + return_value=_authentication(), + ), mock.patch.object( account_state_updater_module.fields_utils, "encrypt", @@ -175,14 +236,21 @@ async def fake_exchange_manager_from_exchange_data(*args, **kwargs): account_state_updater_module, "_check_exchange_manager_state", new=mock.AsyncMock( - return_value=protocol_models.AccountState( - status=protocol_models.AccountStatus.VALID, - message=protocol_models.AccountStatusMessage.VALID, + return_value=( + protocol_models.AccountState( + status=protocol_models.AccountStatus.VALID, + message=protocol_models.AccountStatusMessage.VALID, + ), + None, ) ), ), ): - await account_state_updater_module._check_exchange_account_state(exchange_account) + await account_state_updater_module._check_exchange_account_state( + exchange_account, + account, + _WALLET_ADDRESS, + ) expected_exchange_type = trading_enums.ExchangeTypes.FUTURE.value exchange_data = captured_exchange_data_args["exchange_data"] assert exchange_data.auth_details.exchange_type == expected_exchange_type @@ -193,45 +261,84 @@ async def fake_exchange_manager_from_exchange_data(*args, **kwargs): class TestAccountStateUpdaterCheckExchangeManagerState: @pytest.mark.asyncio async def test_returns_valid_state_when_exchange_checks_succeed(self): - exchange_manager = types.SimpleNamespace(exchange=types.SimpleNamespace()) - with ( - mock.patch.object( - account_state_updater_module, - "_request_exchange_to_ensure_authentication", - new=mock.AsyncMock(return_value=None), + account = _sample_account() + exchange = types.SimpleNamespace( + get_balance=mock.AsyncMock( + return_value={ + "USDT": { + commons_constants.PORTFOLIO_TOTAL: 1000.0, + commons_constants.PORTFOLIO_AVAILABLE: 1000.0, + } + } ), - mock.patch.object( - account_state_updater_module, - "_ensure_api_key_permissions", - new=mock.AsyncMock(return_value=None), - ), - ): - account_state = await account_state_updater_module._check_exchange_manager_state(exchange_manager) + ensure_api_key_permissions=mock.AsyncMock(return_value=None), + ) + exchange_manager = types.SimpleNamespace(exchange=exchange) + account_state, assets = await account_state_updater_module._check_exchange_manager_state( + exchange_manager, + account, + ) assert account_state.status == protocol_models.AccountStatus.VALID assert account_state.message == protocol_models.AccountStatusMessage.VALID + assert assets is not None + assert len(assets) == 1 + assert assets[0].symbol == "USDT" + assert assets[0].total == 1000.0 + assert assets[0].available == 1000.0 + exchange.get_balance.assert_awaited_once() @pytest.mark.asyncio async def test_maps_ip_whitelist_error(self): - exchange_manager = types.SimpleNamespace(exchange=types.SimpleNamespace()) - with mock.patch.object( - account_state_updater_module, - "_request_exchange_to_ensure_authentication", - new=mock.AsyncMock(side_effect=trading_errors.InvalidAPIKeyIPWhitelistError("ip")), - ): - account_state = await account_state_updater_module._check_exchange_manager_state(exchange_manager) + account = _sample_account() + exchange = types.SimpleNamespace( + get_balance=mock.AsyncMock(side_effect=trading_errors.InvalidAPIKeyIPWhitelistError("ip")), + ) + exchange_manager = types.SimpleNamespace(exchange=exchange) + account_state, assets = await account_state_updater_module._check_exchange_manager_state( + exchange_manager, + account, + ) assert account_state.status == protocol_models.AccountStatus.INVALID assert account_state.message == protocol_models.AccountStatusMessage.INVALID_API_IP_WHITELIST + assert assets is None @pytest.mark.asyncio async def test_maps_withdrawal_permissions_error(self): - exchange_manager = types.SimpleNamespace(exchange=types.SimpleNamespace()) - with mock.patch.object( - account_state_updater_module, - "_request_exchange_to_ensure_authentication", - new=mock.AsyncMock( + account = _sample_account() + exchange = types.SimpleNamespace( + get_balance=mock.AsyncMock( side_effect=trading_errors.AuthenticationError("Missing withdrawal permission") ), - ): - account_state = await account_state_updater_module._check_exchange_manager_state(exchange_manager) + ) + exchange_manager = types.SimpleNamespace(exchange=exchange) + account_state, assets = await account_state_updater_module._check_exchange_manager_state( + exchange_manager, + account, + ) assert account_state.status == protocol_models.AccountStatus.INVALID assert account_state.message == protocol_models.AccountStatusMessage.REVOKE_API_WITHDRAWAL_RIGHTS + assert assets is None + + +class TestAccountStateUpdaterAssetsFromBalance: + def test_maps_non_zero_holdings_to_detailed_assets(self): + balance = { + "USDT": { + commons_constants.PORTFOLIO_TOTAL: 1000.0, + commons_constants.PORTFOLIO_AVAILABLE: 900.0, + }, + "BTC": { + commons_constants.PORTFOLIO_TOTAL: 0.5, + commons_constants.PORTFOLIO_AVAILABLE: 0.5, + }, + "ETH": { + commons_constants.PORTFOLIO_TOTAL: 0.0, + commons_constants.PORTFOLIO_AVAILABLE: 0.0, + }, + } + assets = account_state_updater_module._assets_from_balance(balance) + assets_by_symbol = {asset.symbol: asset for asset in assets} + assert set(assets_by_symbol) == {"USDT", "BTC"} + assert assets_by_symbol["USDT"].total == 1000.0 + assert assets_by_symbol["USDT"].available == 900.0 + assert assets_by_symbol["BTC"].total == 0.5 diff --git a/packages/protocol/.openapi-generator/FILES b/packages/protocol/.openapi-generator/FILES index c70efe28c..aeda372e2 100644 --- a/packages/protocol/.openapi-generator/FILES +++ b/packages/protocol/.openapi-generator/FILES @@ -1,18 +1,21 @@ docs/Account.md docs/AccountActionResult.md docs/AccountActionResultErrorMessage.md -docs/AccountDetails.md +docs/AccountAuthentication.md docs/AccountReference.md +docs/AccountSpecifics.md docs/AccountState.md docs/AccountStatus.md docs/AccountStatusMessage.md +docs/AccountTrading.md +docs/AccountTradingState.md docs/AccountType.md +docs/AccountsAuthenticationState.md docs/AccountsState.md docs/Action.md docs/ActionConfigurationType.md docs/ActiveOrderSwapStrategy.md docs/ActiveOrderSwapStrategyType.md -docs/Asset.md docs/AutomationActionResult.md docs/AutomationActionResultErrorMessage.md docs/AutomationConfiguration.md @@ -28,6 +31,7 @@ docs/CreateAccountConfiguration.md docs/CreateAutomationConfiguration.md docs/DCAConfiguration.md docs/DeleteAccountConfiguration.md +docs/DetailedAsset.md docs/EMAMomentumEvaluatorConfiguration.md docs/EditAccountConfiguration.md docs/EditAutomationConfiguration.md @@ -39,6 +43,7 @@ docs/GenericAccount.md docs/GenericProcessConfiguration.md docs/GenericWorkflowConfiguration.md docs/GridConfiguration.md +docs/HistoricalAssetValue.md docs/IndexCoin.md docs/IndexConfiguration.md docs/MarketMakingConfiguration.md @@ -56,6 +61,9 @@ docs/OrderGroupType.md docs/OrderStatus.md docs/OrderSummary.md docs/OrderType.md +docs/PortfolioHistoricalValue.md +docs/PortfolioHistoricalValues.md +docs/PortfolioHistoricalValuesState.md docs/Position.md docs/PositionStatus.md docs/PositionSummary.md @@ -88,18 +96,21 @@ octobot_protocol/models/__init__.py octobot_protocol/models/account.py octobot_protocol/models/account_action_result.py octobot_protocol/models/account_action_result_error_message.py -octobot_protocol/models/account_details.py +octobot_protocol/models/account_authentication.py octobot_protocol/models/account_reference.py +octobot_protocol/models/account_specifics.py octobot_protocol/models/account_state.py octobot_protocol/models/account_status.py octobot_protocol/models/account_status_message.py +octobot_protocol/models/account_trading.py +octobot_protocol/models/account_trading_state.py octobot_protocol/models/account_type.py +octobot_protocol/models/accounts_authentication_state.py octobot_protocol/models/accounts_state.py octobot_protocol/models/action.py octobot_protocol/models/action_configuration_type.py octobot_protocol/models/active_order_swap_strategy.py octobot_protocol/models/active_order_swap_strategy_type.py -octobot_protocol/models/asset.py octobot_protocol/models/automation_action_result.py octobot_protocol/models/automation_action_result_error_message.py octobot_protocol/models/automation_configuration.py @@ -115,6 +126,7 @@ octobot_protocol/models/create_account_configuration.py octobot_protocol/models/create_automation_configuration.py octobot_protocol/models/dca_configuration.py octobot_protocol/models/delete_account_configuration.py +octobot_protocol/models/detailed_asset.py octobot_protocol/models/edit_account_configuration.py octobot_protocol/models/edit_automation_configuration.py octobot_protocol/models/ema_momentum_evaluator_configuration.py @@ -126,6 +138,7 @@ octobot_protocol/models/generic_account.py octobot_protocol/models/generic_process_configuration.py octobot_protocol/models/generic_workflow_configuration.py octobot_protocol/models/grid_configuration.py +octobot_protocol/models/historical_asset_value.py octobot_protocol/models/index_coin.py octobot_protocol/models/index_configuration.py octobot_protocol/models/market_making_configuration.py @@ -143,6 +156,9 @@ octobot_protocol/models/order_group_type.py octobot_protocol/models/order_status.py octobot_protocol/models/order_summary.py octobot_protocol/models/order_type.py +octobot_protocol/models/portfolio_historical_value.py +octobot_protocol/models/portfolio_historical_values.py +octobot_protocol/models/portfolio_historical_values_state.py octobot_protocol/models/position.py octobot_protocol/models/position_status.py octobot_protocol/models/position_summary.py @@ -173,18 +189,21 @@ test/__init__.py test/test_account.py test/test_account_action_result.py test/test_account_action_result_error_message.py -test/test_account_details.py +test/test_account_authentication.py test/test_account_reference.py +test/test_account_specifics.py test/test_account_state.py test/test_account_status.py test/test_account_status_message.py +test/test_account_trading.py +test/test_account_trading_state.py test/test_account_type.py +test/test_accounts_authentication_state.py test/test_accounts_state.py test/test_action.py test/test_action_configuration_type.py test/test_active_order_swap_strategy.py test/test_active_order_swap_strategy_type.py -test/test_asset.py test/test_automation_action_result.py test/test_automation_action_result_error_message.py test/test_automation_configuration.py @@ -200,6 +219,7 @@ test/test_create_account_configuration.py test/test_create_automation_configuration.py test/test_dca_configuration.py test/test_delete_account_configuration.py +test/test_detailed_asset.py test/test_edit_account_configuration.py test/test_edit_automation_configuration.py test/test_ema_momentum_evaluator_configuration.py @@ -211,6 +231,7 @@ test/test_generic_account.py test/test_generic_process_configuration.py test/test_generic_workflow_configuration.py test/test_grid_configuration.py +test/test_historical_asset_value.py test/test_index_coin.py test/test_index_configuration.py test/test_market_making_configuration.py @@ -228,6 +249,9 @@ test/test_order_group_type.py test/test_order_status.py test/test_order_summary.py test/test_order_type.py +test/test_portfolio_historical_value.py +test/test_portfolio_historical_values.py +test/test_portfolio_historical_values_state.py test/test_position.py test/test_position_status.py test/test_position_summary.py diff --git a/packages/protocol/docs/Account.md b/packages/protocol/docs/Account.md index ddb4f2613..b9e3c4859 100644 --- a/packages/protocol/docs/Account.md +++ b/packages/protocol/docs/Account.md @@ -13,7 +13,8 @@ Name | Type | Description | Notes **state** | [**AccountState**](AccountState.md) | | [optional] **created_at** | **datetime** | | **updated_at** | **datetime** | | -**details** | [**AccountDetails**](AccountDetails.md) | | [optional] +**assets** | [**List[DetailedAsset]**](DetailedAsset.md) | | [optional] +**specifics** | [**AccountSpecifics**](AccountSpecifics.md) | | [optional] ## Example diff --git a/packages/protocol/docs/AccountActionResultErrorMessage.md b/packages/protocol/docs/AccountActionResultErrorMessage.md index 7376b03d2..f4797db88 100644 --- a/packages/protocol/docs/AccountActionResultErrorMessage.md +++ b/packages/protocol/docs/AccountActionResultErrorMessage.md @@ -8,6 +8,8 @@ AccountActionResultErrorMessage * `INVALID_CONFIGURATION` (value: `'invalid_configuration'`) +* `ACCOUNT_AUTHENTICATION_DETAILS_NOT_FOUND` (value: `'account_authentication_details_not_found'`) + * `INTERNAL_ERROR` (value: `'internal_error'`) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/packages/protocol/docs/AccountAuthentication.md b/packages/protocol/docs/AccountAuthentication.md new file mode 100644 index 000000000..0929e6b0f --- /dev/null +++ b/packages/protocol/docs/AccountAuthentication.md @@ -0,0 +1,35 @@ +# AccountAuthentication + +AccountAuthentication + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**api_key** | **str** | | [optional] +**api_secret** | **str** | | [optional] +**api_passphrase** | **str** | | [optional] +**public_key** | **str** | | [optional] +**private_key** | **str** | | [optional] +**seed_phrase** | **str** | | [optional] + +## Example + +```python +from octobot_protocol.models.account_authentication import AccountAuthentication + +# TODO update the JSON string below +json = "{}" +# create an instance of AccountAuthentication from a JSON string +account_authentication_instance = AccountAuthentication.from_json(json) +# print the JSON string representation of the object +print(AccountAuthentication.to_json()) + +# convert the object into a dict +account_authentication_dict = account_authentication_instance.to_dict() +# create an instance of AccountAuthentication from a dict +account_authentication_from_dict = AccountAuthentication.from_dict(account_authentication_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/AccountAuthenticationDetails.md b/packages/protocol/docs/AccountAuthenticationDetails.md new file mode 100644 index 000000000..a15fbfd80 --- /dev/null +++ b/packages/protocol/docs/AccountAuthenticationDetails.md @@ -0,0 +1,35 @@ +# AccountAuthenticationDetails + +AccountAuthenticationDetails + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**api_key** | **str** | | [optional] +**api_secret** | **str** | | [optional] +**api_passphrase** | **str** | | [optional] +**public_key** | **str** | | [optional] +**private_key** | **str** | | [optional] +**seed_phrase** | **str** | | [optional] + +## Example + +```python +from octobot_protocol.models.account_authentication_details import AccountAuthenticationDetails + +# TODO update the JSON string below +json = "{}" +# create an instance of AccountAuthenticationDetails from a JSON string +account_authentication_details_instance = AccountAuthenticationDetails.from_json(json) +# print the JSON string representation of the object +print(AccountAuthenticationDetails.to_json()) + +# convert the object into a dict +account_authentication_details_dict = account_authentication_details_instance.to_dict() +# create an instance of AccountAuthenticationDetails from a dict +account_authentication_details_from_dict = AccountAuthenticationDetails.from_dict(account_authentication_details_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/AccountDetails.md b/packages/protocol/docs/AccountDetails.md index db5cdf791..5dbd41db4 100644 --- a/packages/protocol/docs/AccountDetails.md +++ b/packages/protocol/docs/AccountDetails.md @@ -9,18 +9,8 @@ Name | Type | Description | Notes **trading_type** | [**TradingType**](TradingType.md) | | **exchange** | **str** | | **remote_account_id** | **str** | | -**api_key** | **str** | | -**api_secret** | **str** | | -**api_passphrase** | **str** | | [optional] -**assets** | [**List[Asset]**](Asset.md) | | [optional] -**orders** | [**List[Order]**](Order.md) | | [optional] -**trades** | [**List[Trade]**](Trade.md) | | [optional] -**positions** | [**List[Position]**](Position.md) | | [optional] **blockchain** | **str** | | **network** | **str** | | [optional] -**public_key** | **str** | | [optional] -**private_key** | **str** | | [optional] -**passphrase** | **str** | | [optional] ## Example diff --git a/packages/protocol/docs/AccountSpecifics.md b/packages/protocol/docs/AccountSpecifics.md new file mode 100644 index 000000000..1bb768a66 --- /dev/null +++ b/packages/protocol/docs/AccountSpecifics.md @@ -0,0 +1,34 @@ +# AccountSpecifics + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**account_type** | [**AccountType**](AccountType.md) | generic | +**trading_type** | [**TradingType**](TradingType.md) | | +**exchange** | **str** | | +**remote_account_id** | **str** | | +**blockchain** | **str** | | +**network** | **str** | | [optional] + +## Example + +```python +from octobot_protocol.models.account_specifics import AccountSpecifics + +# TODO update the JSON string below +json = "{}" +# create an instance of AccountSpecifics from a JSON string +account_specifics_instance = AccountSpecifics.from_json(json) +# print the JSON string representation of the object +print(AccountSpecifics.to_json()) + +# convert the object into a dict +account_specifics_dict = account_specifics_instance.to_dict() +# create an instance of AccountSpecifics from a dict +account_specifics_from_dict = AccountSpecifics.from_dict(account_specifics_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/AccountTrading.md b/packages/protocol/docs/AccountTrading.md new file mode 100644 index 000000000..44050729c --- /dev/null +++ b/packages/protocol/docs/AccountTrading.md @@ -0,0 +1,33 @@ +# AccountTrading + +AccountTrading + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**updated_at** | **datetime** | | +**orders** | [**List[Order]**](Order.md) | | [optional] +**trades** | [**List[Trade]**](Trade.md) | | [optional] +**positions** | [**List[Position]**](Position.md) | | [optional] + +## Example + +```python +from octobot_protocol.models.account_trading import AccountTrading + +# TODO update the JSON string below +json = "{}" +# create an instance of AccountTrading from a JSON string +account_trading_instance = AccountTrading.from_json(json) +# print the JSON string representation of the object +print(AccountTrading.to_json()) + +# convert the object into a dict +account_trading_dict = account_trading_instance.to_dict() +# create an instance of AccountTrading from a dict +account_trading_from_dict = AccountTrading.from_dict(account_trading_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/AccountTradingDetails.md b/packages/protocol/docs/AccountTradingDetails.md new file mode 100644 index 000000000..7a59e46de --- /dev/null +++ b/packages/protocol/docs/AccountTradingDetails.md @@ -0,0 +1,33 @@ +# AccountTradingDetails + +AccountTradingDetails + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**updated_at** | **datetime** | | +**orders** | [**List[Order]**](Order.md) | | [optional] +**trades** | [**List[Trade]**](Trade.md) | | [optional] +**positions** | [**List[Position]**](Position.md) | | [optional] + +## Example + +```python +from octobot_protocol.models.account_trading_details import AccountTradingDetails + +# TODO update the JSON string below +json = "{}" +# create an instance of AccountTradingDetails from a JSON string +account_trading_details_instance = AccountTradingDetails.from_json(json) +# print the JSON string representation of the object +print(AccountTradingDetails.to_json()) + +# convert the object into a dict +account_trading_details_dict = account_trading_details_instance.to_dict() +# create an instance of AccountTradingDetails from a dict +account_trading_details_from_dict = AccountTradingDetails.from_dict(account_trading_details_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/AccountTradingDetailsState.md b/packages/protocol/docs/AccountTradingDetailsState.md new file mode 100644 index 000000000..363b0746f --- /dev/null +++ b/packages/protocol/docs/AccountTradingDetailsState.md @@ -0,0 +1,31 @@ +# AccountTradingDetailsState + +AccountsTradingDetailsState + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**version** | **str** | | +**details** | [**List[AccountTradingDetails]**](AccountTradingDetails.md) | | + +## Example + +```python +from octobot_protocol.models.account_trading_details_state import AccountTradingDetailsState + +# TODO update the JSON string below +json = "{}" +# create an instance of AccountTradingDetailsState from a JSON string +account_trading_details_state_instance = AccountTradingDetailsState.from_json(json) +# print the JSON string representation of the object +print(AccountTradingDetailsState.to_json()) + +# convert the object into a dict +account_trading_details_state_dict = account_trading_details_state_instance.to_dict() +# create an instance of AccountTradingDetailsState from a dict +account_trading_details_state_from_dict = AccountTradingDetailsState.from_dict(account_trading_details_state_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/AccountTradingState.md b/packages/protocol/docs/AccountTradingState.md new file mode 100644 index 000000000..8be0b8bd3 --- /dev/null +++ b/packages/protocol/docs/AccountTradingState.md @@ -0,0 +1,31 @@ +# AccountTradingState + +AccountTradingState + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**version** | **str** | | +**account_trading** | [**List[AccountTrading]**](AccountTrading.md) | | + +## Example + +```python +from octobot_protocol.models.account_trading_state import AccountTradingState + +# TODO update the JSON string below +json = "{}" +# create an instance of AccountTradingState from a JSON string +account_trading_state_instance = AccountTradingState.from_json(json) +# print the JSON string representation of the object +print(AccountTradingState.to_json()) + +# convert the object into a dict +account_trading_state_dict = account_trading_state_instance.to_dict() +# create an instance of AccountTradingState from a dict +account_trading_state_from_dict = AccountTradingState.from_dict(account_trading_state_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/AccountTrailingDetailsState.md b/packages/protocol/docs/AccountTrailingDetailsState.md new file mode 100644 index 000000000..284a1f3c5 --- /dev/null +++ b/packages/protocol/docs/AccountTrailingDetailsState.md @@ -0,0 +1,32 @@ +# AccountTrailingDetailsState + +AccountTrailingDetailsState + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**version** | **str** | | +**account_id** | **str** | | +**details** | [**List[AccountTradingDetails]**](AccountTradingDetails.md) | | + +## Example + +```python +from octobot_protocol.models.account_trailing_details_state import AccountTrailingDetailsState + +# TODO update the JSON string below +json = "{}" +# create an instance of AccountTrailingDetailsState from a JSON string +account_trailing_details_state_instance = AccountTrailingDetailsState.from_json(json) +# print the JSON string representation of the object +print(AccountTrailingDetailsState.to_json()) + +# convert the object into a dict +account_trailing_details_state_dict = account_trailing_details_state_instance.to_dict() +# create an instance of AccountTrailingDetailsState from a dict +account_trailing_details_state_from_dict = AccountTrailingDetailsState.from_dict(account_trailing_details_state_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/AccountsAuthenticationDetailsState.md b/packages/protocol/docs/AccountsAuthenticationDetailsState.md new file mode 100644 index 000000000..eb01ed164 --- /dev/null +++ b/packages/protocol/docs/AccountsAuthenticationDetailsState.md @@ -0,0 +1,31 @@ +# AccountsAuthenticationDetailsState + +AccountsAuthenticationDetailsState + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**version** | **str** | | +**details** | [**List[AccountAuthenticationDetails]**](AccountAuthenticationDetails.md) | | + +## Example + +```python +from octobot_protocol.models.accounts_authentication_details_state import AccountsAuthenticationDetailsState + +# TODO update the JSON string below +json = "{}" +# create an instance of AccountsAuthenticationDetailsState from a JSON string +accounts_authentication_details_state_instance = AccountsAuthenticationDetailsState.from_json(json) +# print the JSON string representation of the object +print(AccountsAuthenticationDetailsState.to_json()) + +# convert the object into a dict +accounts_authentication_details_state_dict = accounts_authentication_details_state_instance.to_dict() +# create an instance of AccountsAuthenticationDetailsState from a dict +accounts_authentication_details_state_from_dict = AccountsAuthenticationDetailsState.from_dict(accounts_authentication_details_state_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/AccountsAuthenticationState.md b/packages/protocol/docs/AccountsAuthenticationState.md new file mode 100644 index 000000000..b41954517 --- /dev/null +++ b/packages/protocol/docs/AccountsAuthenticationState.md @@ -0,0 +1,31 @@ +# AccountsAuthenticationState + +AccountsAuthenticationState + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**version** | **str** | | +**account_authentication** | [**List[AccountAuthentication]**](AccountAuthentication.md) | | + +## Example + +```python +from octobot_protocol.models.accounts_authentication_state import AccountsAuthenticationState + +# TODO update the JSON string below +json = "{}" +# create an instance of AccountsAuthenticationState from a JSON string +accounts_authentication_state_instance = AccountsAuthenticationState.from_json(json) +# print the JSON string representation of the object +print(AccountsAuthenticationState.to_json()) + +# convert the object into a dict +accounts_authentication_state_dict = accounts_authentication_state_instance.to_dict() +# create an instance of AccountsAuthenticationState from a dict +accounts_authentication_state_from_dict = AccountsAuthenticationState.from_dict(accounts_authentication_state_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/AuthenticationDetailsState.md b/packages/protocol/docs/AuthenticationDetailsState.md new file mode 100644 index 000000000..fe3ce4d46 --- /dev/null +++ b/packages/protocol/docs/AuthenticationDetailsState.md @@ -0,0 +1,31 @@ +# AuthenticationDetailsState + +AuthenticationDetailsState + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**version** | **str** | | +**details** | [**List[AccountAuthenticationDetails]**](AccountAuthenticationDetails.md) | | + +## Example + +```python +from octobot_protocol.models.authentication_details_state import AuthenticationDetailsState + +# TODO update the JSON string below +json = "{}" +# create an instance of AuthenticationDetailsState from a JSON string +authentication_details_state_instance = AuthenticationDetailsState.from_json(json) +# print the JSON string representation of the object +print(AuthenticationDetailsState.to_json()) + +# convert the object into a dict +authentication_details_state_dict = authentication_details_state_instance.to_dict() +# create an instance of AuthenticationDetailsState from a dict +authentication_details_state_from_dict = AuthenticationDetailsState.from_dict(authentication_details_state_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/AutomationActionResultErrorMessage.md b/packages/protocol/docs/AutomationActionResultErrorMessage.md index da03a44ab..ecb08c4eb 100644 --- a/packages/protocol/docs/AutomationActionResultErrorMessage.md +++ b/packages/protocol/docs/AutomationActionResultErrorMessage.md @@ -14,6 +14,8 @@ AutomationActionResultErrorMessage * `ACCOUNT_NOT_FOUND` (value: `'account_not_found'`) +* `ACCOUNT_AUTHENTICATION_DETAILS_NOT_FOUND` (value: `'account_authentication_details_not_found'`) + * `INTERNAL_ERROR` (value: `'internal_error'`) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/packages/protocol/docs/AutomationState.md b/packages/protocol/docs/AutomationState.md index 34a6e014f..6e9a28f94 100644 --- a/packages/protocol/docs/AutomationState.md +++ b/packages/protocol/docs/AutomationState.md @@ -13,7 +13,7 @@ Name | Type | Description | Notes **priority_actions** | [**List[Action]**](Action.md) | | [optional] **exchanges** | **List[str]** | | [optional] **exchange_account_ids** | **List[str]** | | [optional] -**assets** | [**List[Asset]**](Asset.md) | | [optional] +**assets** | [**List[DetailedAsset]**](DetailedAsset.md) | | [optional] **orders** | [**List[OrderSummary]**](OrderSummary.md) | | [optional] **trades** | [**List[TradeSummary]**](TradeSummary.md) | | [optional] **positions** | [**List[PositionSummary]**](PositionSummary.md) | | [optional] diff --git a/packages/protocol/docs/BlockchainAccount.md b/packages/protocol/docs/BlockchainAccount.md index cb8a713a2..182b73b0e 100644 --- a/packages/protocol/docs/BlockchainAccount.md +++ b/packages/protocol/docs/BlockchainAccount.md @@ -9,9 +9,6 @@ Name | Type | Description | Notes **account_type** | [**AccountType**](AccountType.md) | blockchain | **blockchain** | **str** | | **network** | **str** | | [optional] -**public_key** | **str** | | [optional] -**private_key** | **str** | | [optional] -**passphrase** | **str** | | [optional] ## Example diff --git a/packages/protocol/docs/CancelPolicy.md b/packages/protocol/docs/CancelPolicy.md index 793803ffe..576e6714a 100644 --- a/packages/protocol/docs/CancelPolicy.md +++ b/packages/protocol/docs/CancelPolicy.md @@ -7,7 +7,7 @@ CancelPolicy Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **type** | [**CancelPolicyType**](CancelPolicyType.md) | | -**details** | **object** | | [optional] +**specifics** | **object** | | [optional] ## Example diff --git a/packages/protocol/docs/DetailedAsset.md b/packages/protocol/docs/DetailedAsset.md new file mode 100644 index 000000000..f38b2ead3 --- /dev/null +++ b/packages/protocol/docs/DetailedAsset.md @@ -0,0 +1,32 @@ +# DetailedAsset + +DetailedAsset + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**symbol** | **str** | | +**total** | **float** | | +**available** | **float** | | + +## Example + +```python +from octobot_protocol.models.detailed_asset import DetailedAsset + +# TODO update the JSON string below +json = "{}" +# create an instance of DetailedAsset from a JSON string +detailed_asset_instance = DetailedAsset.from_json(json) +# print the JSON string representation of the object +print(DetailedAsset.to_json()) + +# convert the object into a dict +detailed_asset_dict = detailed_asset_instance.to_dict() +# create an instance of DetailedAsset from a dict +detailed_asset_from_dict = DetailedAsset.from_dict(detailed_asset_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/ExchangeAccount.md b/packages/protocol/docs/ExchangeAccount.md index 2720c872c..9565e200c 100644 --- a/packages/protocol/docs/ExchangeAccount.md +++ b/packages/protocol/docs/ExchangeAccount.md @@ -10,13 +10,6 @@ Name | Type | Description | Notes **trading_type** | [**TradingType**](TradingType.md) | | **exchange** | **str** | | **remote_account_id** | **str** | | -**api_key** | **str** | | -**api_secret** | **str** | | -**api_passphrase** | **str** | | [optional] -**assets** | [**List[Asset]**](Asset.md) | | [optional] -**orders** | [**List[Order]**](Order.md) | | [optional] -**trades** | [**List[Trade]**](Trade.md) | | [optional] -**positions** | [**List[Position]**](Position.md) | | [optional] ## Example diff --git a/packages/protocol/docs/GenericAccount.md b/packages/protocol/docs/GenericAccount.md index f46003d97..58d829025 100644 --- a/packages/protocol/docs/GenericAccount.md +++ b/packages/protocol/docs/GenericAccount.md @@ -7,7 +7,6 @@ GenericAccount Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **account_type** | [**AccountType**](AccountType.md) | generic | -**assets** | [**List[Asset]**](Asset.md) | | [optional] ## Example diff --git a/packages/protocol/docs/HistoricalAssetValue.md b/packages/protocol/docs/HistoricalAssetValue.md new file mode 100644 index 000000000..690867e49 --- /dev/null +++ b/packages/protocol/docs/HistoricalAssetValue.md @@ -0,0 +1,32 @@ +# HistoricalAssetValue + +HistoricalAssetValue + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**symbol** | **str** | | +**holdings** | **float** | | +**value** | **float** | | + +## Example + +```python +from octobot_protocol.models.historical_asset_value import HistoricalAssetValue + +# TODO update the JSON string below +json = "{}" +# create an instance of HistoricalAssetValue from a JSON string +historical_asset_value_instance = HistoricalAssetValue.from_json(json) +# print the JSON string representation of the object +print(HistoricalAssetValue.to_json()) + +# convert the object into a dict +historical_asset_value_dict = historical_asset_value_instance.to_dict() +# create an instance of HistoricalAssetValue from a dict +historical_asset_value_from_dict = HistoricalAssetValue.from_dict(historical_asset_value_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/PortfolioContent.md b/packages/protocol/docs/PortfolioContent.md new file mode 100644 index 000000000..f6b5bdd59 --- /dev/null +++ b/packages/protocol/docs/PortfolioContent.md @@ -0,0 +1,32 @@ +# PortfolioContent + +PortfolioContent + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**total** | **float** | | +**unit** | **str** | | +**assets** | [**List[DetailedAsset]**](DetailedAsset.md) | | + +## Example + +```python +from octobot_protocol.models.portfolio_content import PortfolioContent + +# TODO update the JSON string below +json = "{}" +# create an instance of PortfolioContent from a JSON string +portfolio_content_instance = PortfolioContent.from_json(json) +# print the JSON string representation of the object +print(PortfolioContent.to_json()) + +# convert the object into a dict +portfolio_content_dict = portfolio_content_instance.to_dict() +# create an instance of PortfolioContent from a dict +portfolio_content_from_dict = PortfolioContent.from_dict(portfolio_content_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/PortfolioHistoricalValue.md b/packages/protocol/docs/PortfolioHistoricalValue.md new file mode 100644 index 000000000..9afa657ba --- /dev/null +++ b/packages/protocol/docs/PortfolioHistoricalValue.md @@ -0,0 +1,32 @@ +# PortfolioHistoricalValue + +PortfolioHistoricalValue + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**timestamp** | **datetime** | | +**total** | **float** | | +**assets** | [**List[HistoricalAssetValue]**](HistoricalAssetValue.md) | | [optional] + +## Example + +```python +from octobot_protocol.models.portfolio_historical_value import PortfolioHistoricalValue + +# TODO update the JSON string below +json = "{}" +# create an instance of PortfolioHistoricalValue from a JSON string +portfolio_historical_value_instance = PortfolioHistoricalValue.from_json(json) +# print the JSON string representation of the object +print(PortfolioHistoricalValue.to_json()) + +# convert the object into a dict +portfolio_historical_value_dict = portfolio_historical_value_instance.to_dict() +# create an instance of PortfolioHistoricalValue from a dict +portfolio_historical_value_from_dict = PortfolioHistoricalValue.from_dict(portfolio_historical_value_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/PortfolioHistoricalValues.md b/packages/protocol/docs/PortfolioHistoricalValues.md new file mode 100644 index 000000000..0f495f7b5 --- /dev/null +++ b/packages/protocol/docs/PortfolioHistoricalValues.md @@ -0,0 +1,31 @@ +# PortfolioHistoricalValues + +PortfolioHistoricalValues + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**unit** | **str** | | +**values** | [**List[PortfolioHistoricalValue]**](PortfolioHistoricalValue.md) | | + +## Example + +```python +from octobot_protocol.models.portfolio_historical_values import PortfolioHistoricalValues + +# TODO update the JSON string below +json = "{}" +# create an instance of PortfolioHistoricalValues from a JSON string +portfolio_historical_values_instance = PortfolioHistoricalValues.from_json(json) +# print the JSON string representation of the object +print(PortfolioHistoricalValues.to_json()) + +# convert the object into a dict +portfolio_historical_values_dict = portfolio_historical_values_instance.to_dict() +# create an instance of PortfolioHistoricalValues from a dict +portfolio_historical_values_from_dict = PortfolioHistoricalValues.from_dict(portfolio_historical_values_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/PortfolioHistoricalValuesState.md b/packages/protocol/docs/PortfolioHistoricalValuesState.md new file mode 100644 index 000000000..6b68e16e8 --- /dev/null +++ b/packages/protocol/docs/PortfolioHistoricalValuesState.md @@ -0,0 +1,31 @@ +# PortfolioHistoricalValuesState + +PortfolioHistoricalValuesState + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**version** | **str** | | +**history** | [**PortfolioHistoricalValues**](PortfolioHistoricalValues.md) | | [optional] + +## Example + +```python +from octobot_protocol.models.portfolio_historical_values_state import PortfolioHistoricalValuesState + +# TODO update the JSON string below +json = "{}" +# create an instance of PortfolioHistoricalValuesState from a JSON string +portfolio_historical_values_state_instance = PortfolioHistoricalValuesState.from_json(json) +# print the JSON string representation of the object +print(PortfolioHistoricalValuesState.to_json()) + +# convert the object into a dict +portfolio_historical_values_state_dict = portfolio_historical_values_state_instance.to_dict() +# create an instance of PortfolioHistoricalValuesState from a dict +portfolio_historical_values_state_from_dict = PortfolioHistoricalValuesState.from_dict(portfolio_historical_values_state_dict) +``` +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/protocol/docs/Strategy.md b/packages/protocol/docs/Strategy.md index c74971939..3f0d04e85 100644 --- a/packages/protocol/docs/Strategy.md +++ b/packages/protocol/docs/Strategy.md @@ -12,6 +12,7 @@ Name | Type | Description | Notes **description** | **str** | | [optional] **created_at** | **datetime** | | [optional] **updated_at** | **datetime** | | [optional] +**reference_market** | **str** | | **configuration** | [**StrategyConfiguration**](StrategyConfiguration.md) | | ## Example diff --git a/packages/protocol/docs/TrailingProfile.md b/packages/protocol/docs/TrailingProfile.md index 68dd50a28..5ff80914e 100644 --- a/packages/protocol/docs/TrailingProfile.md +++ b/packages/protocol/docs/TrailingProfile.md @@ -7,7 +7,7 @@ TrailingProfile Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **type** | [**TrailingProfileType**](TrailingProfileType.md) | | -**details** | **object** | | [optional] +**specifics** | **object** | | [optional] ## Example diff --git a/packages/protocol/octobot_protocol/models/__init__.py b/packages/protocol/octobot_protocol/models/__init__.py index 22e15ebdd..58110e054 100644 --- a/packages/protocol/octobot_protocol/models/__init__.py +++ b/packages/protocol/octobot_protocol/models/__init__.py @@ -16,18 +16,21 @@ from octobot_protocol.models.account import Account from octobot_protocol.models.account_action_result import AccountActionResult from octobot_protocol.models.account_action_result_error_message import AccountActionResultErrorMessage -from octobot_protocol.models.account_details import AccountDetails +from octobot_protocol.models.account_authentication import AccountAuthentication from octobot_protocol.models.account_reference import AccountReference +from octobot_protocol.models.account_specifics import AccountSpecifics from octobot_protocol.models.account_state import AccountState from octobot_protocol.models.account_status import AccountStatus from octobot_protocol.models.account_status_message import AccountStatusMessage +from octobot_protocol.models.account_trading import AccountTrading +from octobot_protocol.models.account_trading_state import AccountTradingState from octobot_protocol.models.account_type import AccountType +from octobot_protocol.models.accounts_authentication_state import AccountsAuthenticationState from octobot_protocol.models.accounts_state import AccountsState from octobot_protocol.models.action import Action from octobot_protocol.models.action_configuration_type import ActionConfigurationType from octobot_protocol.models.active_order_swap_strategy import ActiveOrderSwapStrategy from octobot_protocol.models.active_order_swap_strategy_type import ActiveOrderSwapStrategyType -from octobot_protocol.models.asset import Asset from octobot_protocol.models.automation_action_result import AutomationActionResult from octobot_protocol.models.automation_action_result_error_message import AutomationActionResultErrorMessage from octobot_protocol.models.automation_configuration import AutomationConfiguration @@ -43,6 +46,7 @@ from octobot_protocol.models.create_automation_configuration import CreateAutomationConfiguration from octobot_protocol.models.dca_configuration import DCAConfiguration from octobot_protocol.models.delete_account_configuration import DeleteAccountConfiguration +from octobot_protocol.models.detailed_asset import DetailedAsset from octobot_protocol.models.ema_momentum_evaluator_configuration import EMAMomentumEvaluatorConfiguration from octobot_protocol.models.edit_account_configuration import EditAccountConfiguration from octobot_protocol.models.edit_automation_configuration import EditAutomationConfiguration @@ -54,6 +58,7 @@ from octobot_protocol.models.generic_process_configuration import GenericProcessConfiguration from octobot_protocol.models.generic_workflow_configuration import GenericWorkflowConfiguration from octobot_protocol.models.grid_configuration import GridConfiguration +from octobot_protocol.models.historical_asset_value import HistoricalAssetValue from octobot_protocol.models.index_coin import IndexCoin from octobot_protocol.models.index_configuration import IndexConfiguration from octobot_protocol.models.market_making_configuration import MarketMakingConfiguration @@ -71,6 +76,9 @@ from octobot_protocol.models.order_status import OrderStatus from octobot_protocol.models.order_summary import OrderSummary from octobot_protocol.models.order_type import OrderType +from octobot_protocol.models.portfolio_historical_value import PortfolioHistoricalValue +from octobot_protocol.models.portfolio_historical_values import PortfolioHistoricalValues +from octobot_protocol.models.portfolio_historical_values_state import PortfolioHistoricalValuesState from octobot_protocol.models.position import Position from octobot_protocol.models.position_status import PositionStatus from octobot_protocol.models.position_summary import PositionSummary diff --git a/packages/protocol/octobot_protocol/models/account.py b/packages/protocol/octobot_protocol/models/account.py index 893a6b748..0eb45c4af 100644 --- a/packages/protocol/octobot_protocol/models/account.py +++ b/packages/protocol/octobot_protocol/models/account.py @@ -20,8 +20,9 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict, StrictBool, StrictStr from typing import Any, ClassVar, Dict, List, Optional -from octobot_protocol.models.account_details import AccountDetails +from octobot_protocol.models.account_specifics import AccountSpecifics from octobot_protocol.models.account_state import AccountState +from octobot_protocol.models.detailed_asset import DetailedAsset from typing import Optional, Set from typing_extensions import Self from pydantic_core import to_jsonable_python @@ -37,8 +38,9 @@ class Account(BaseModel): state: Optional[AccountState] = None created_at: datetime updated_at: datetime - details: Optional[AccountDetails] = None - __properties: ClassVar[List[str]] = ["id", "name", "is_simulated", "description", "state", "created_at", "updated_at", "details"] + assets: Optional[List[DetailedAsset]] = None + specifics: Optional[AccountSpecifics] = None + __properties: ClassVar[List[str]] = ["id", "name", "is_simulated", "description", "state", "created_at", "updated_at", "assets", "specifics"] model_config = ConfigDict( validate_by_name=True, @@ -82,9 +84,16 @@ def to_dict(self) -> Dict[str, Any]: # override the default output from pydantic by calling `to_dict()` of state if self.state: _dict['state'] = self.state.to_dict() - # override the default output from pydantic by calling `to_dict()` of details - if self.details: - _dict['details'] = self.details.to_dict() + # override the default output from pydantic by calling `to_dict()` of each item in assets (list) + _items = [] + if self.assets: + for _item_assets in self.assets: + if _item_assets: + _items.append(_item_assets.to_dict()) + _dict['assets'] = _items + # override the default output from pydantic by calling `to_dict()` of specifics + if self.specifics: + _dict['specifics'] = self.specifics.to_dict() return _dict @classmethod @@ -104,7 +113,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "state": AccountState.from_dict(obj["state"]) if obj.get("state") is not None else None, "created_at": obj.get("created_at"), "updated_at": obj.get("updated_at"), - "details": AccountDetails.from_dict(obj["details"]) if obj.get("details") is not None else None + "assets": [DetailedAsset.from_dict(_item) for _item in obj["assets"]] if obj.get("assets") is not None else None, + "specifics": AccountSpecifics.from_dict(obj["specifics"]) if obj.get("specifics") is not None else None }) return _obj diff --git a/packages/protocol/octobot_protocol/models/account_action_result_error_message.py b/packages/protocol/octobot_protocol/models/account_action_result_error_message.py index 7fcad0ac7..c4040ea6e 100644 --- a/packages/protocol/octobot_protocol/models/account_action_result_error_message.py +++ b/packages/protocol/octobot_protocol/models/account_action_result_error_message.py @@ -28,6 +28,7 @@ class AccountActionResultErrorMessage(str, Enum): """ ACCOUNT_NOT_FOUND = 'account_not_found' INVALID_CONFIGURATION = 'invalid_configuration' + ACCOUNT_AUTHENTICATION_DETAILS_NOT_FOUND = 'account_authentication_details_not_found' INTERNAL_ERROR = 'internal_error' @classmethod diff --git a/packages/protocol/octobot_protocol/models/account_authentication.py b/packages/protocol/octobot_protocol/models/account_authentication.py new file mode 100644 index 000000000..2398ad7d2 --- /dev/null +++ b/packages/protocol/octobot_protocol/models/account_authentication.py @@ -0,0 +1,98 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class AccountAuthentication(BaseModel): + """ + AccountAuthentication + """ # noqa: E501 + api_key: Optional[StrictStr] = None + api_secret: Optional[StrictStr] = None + api_passphrase: Optional[StrictStr] = None + public_key: Optional[StrictStr] = None + private_key: Optional[StrictStr] = None + seed_phrase: Optional[StrictStr] = None + __properties: ClassVar[List[str]] = ["api_key", "api_secret", "api_passphrase", "public_key", "private_key", "seed_phrase"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AccountAuthentication from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AccountAuthentication from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "api_key": obj.get("api_key"), + "api_secret": obj.get("api_secret"), + "api_passphrase": obj.get("api_passphrase"), + "public_key": obj.get("public_key"), + "private_key": obj.get("private_key"), + "seed_phrase": obj.get("seed_phrase") + }) + return _obj + + diff --git a/packages/protocol/octobot_protocol/models/account_details.py b/packages/protocol/octobot_protocol/models/account_specifics.py similarity index 89% rename from packages/protocol/octobot_protocol/models/account_details.py rename to packages/protocol/octobot_protocol/models/account_specifics.py index 0ec3657bd..8f7e2429a 100644 --- a/packages/protocol/octobot_protocol/models/account_details.py +++ b/packages/protocol/octobot_protocol/models/account_specifics.py @@ -24,11 +24,11 @@ from typing import Union, List, Set, Optional, Dict from typing_extensions import Literal, Self -ACCOUNTDETAILS_ONE_OF_SCHEMAS = ["BlockchainAccount", "ExchangeAccount", "GenericAccount"] +ACCOUNTSPECIFICS_ONE_OF_SCHEMAS = ["BlockchainAccount", "ExchangeAccount", "GenericAccount"] -class AccountDetails(BaseModel): +class AccountSpecifics(BaseModel): """ - AccountDetails + AccountSpecifics """ # data type: ExchangeAccount oneof_schema_1_validator: Optional[ExchangeAccount] = None @@ -60,7 +60,7 @@ def __init__(self, *args, **kwargs) -> None: @field_validator('actual_instance') def actual_instance_must_validate_oneof(cls, v): - instance = AccountDetails.model_construct() + instance = AccountSpecifics.model_construct() error_messages = [] match = 0 # validate data type: ExchangeAccount @@ -80,10 +80,10 @@ def actual_instance_must_validate_oneof(cls, v): match += 1 if match > 1: # more than 1 match - raise ValueError("Multiple matches found when setting `actual_instance` in AccountDetails with oneOf schemas: BlockchainAccount, ExchangeAccount, GenericAccount. Details: " + ", ".join(error_messages)) + raise ValueError("Multiple matches found when setting `actual_instance` in AccountSpecifics with oneOf schemas: BlockchainAccount, ExchangeAccount, GenericAccount. Details: " + ", ".join(error_messages)) elif match == 0: # no match - raise ValueError("No match found when setting `actual_instance` in AccountDetails with oneOf schemas: BlockchainAccount, ExchangeAccount, GenericAccount. Details: " + ", ".join(error_messages)) + raise ValueError("No match found when setting `actual_instance` in AccountSpecifics with oneOf schemas: BlockchainAccount, ExchangeAccount, GenericAccount. Details: " + ", ".join(error_messages)) else: return v @@ -139,10 +139,10 @@ def from_json(cls, json_str: str) -> Self: if match > 1: # more than 1 match - raise ValueError("Multiple matches found when deserializing the JSON string into AccountDetails with oneOf schemas: BlockchainAccount, ExchangeAccount, GenericAccount. Details: " + ", ".join(error_messages)) + raise ValueError("Multiple matches found when deserializing the JSON string into AccountSpecifics with oneOf schemas: BlockchainAccount, ExchangeAccount, GenericAccount. Details: " + ", ".join(error_messages)) elif match == 0: # no match - raise ValueError("No match found when deserializing the JSON string into AccountDetails with oneOf schemas: BlockchainAccount, ExchangeAccount, GenericAccount. Details: " + ", ".join(error_messages)) + raise ValueError("No match found when deserializing the JSON string into AccountSpecifics with oneOf schemas: BlockchainAccount, ExchangeAccount, GenericAccount. Details: " + ", ".join(error_messages)) else: return instance diff --git a/packages/protocol/octobot_protocol/models/account_trading.py b/packages/protocol/octobot_protocol/models/account_trading.py new file mode 100644 index 000000000..ddc5ab225 --- /dev/null +++ b/packages/protocol/octobot_protocol/models/account_trading.py @@ -0,0 +1,119 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from datetime import datetime +from pydantic import BaseModel, ConfigDict +from typing import Any, ClassVar, Dict, List, Optional +from octobot_protocol.models.order import Order +from octobot_protocol.models.position import Position +from octobot_protocol.models.trade import Trade +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class AccountTrading(BaseModel): + """ + AccountTrading + """ # noqa: E501 + updated_at: datetime + orders: Optional[List[Order]] = None + trades: Optional[List[Trade]] = None + positions: Optional[List[Position]] = None + __properties: ClassVar[List[str]] = ["updated_at", "orders", "trades", "positions"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AccountTrading from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in orders (list) + _items = [] + if self.orders: + for _item_orders in self.orders: + if _item_orders: + _items.append(_item_orders.to_dict()) + _dict['orders'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in trades (list) + _items = [] + if self.trades: + for _item_trades in self.trades: + if _item_trades: + _items.append(_item_trades.to_dict()) + _dict['trades'] = _items + # override the default output from pydantic by calling `to_dict()` of each item in positions (list) + _items = [] + if self.positions: + for _item_positions in self.positions: + if _item_positions: + _items.append(_item_positions.to_dict()) + _dict['positions'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AccountTrading from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "updated_at": obj.get("updated_at"), + "orders": [Order.from_dict(_item) for _item in obj["orders"]] if obj.get("orders") is not None else None, + "trades": [Trade.from_dict(_item) for _item in obj["trades"]] if obj.get("trades") is not None else None, + "positions": [Position.from_dict(_item) for _item in obj["positions"]] if obj.get("positions") is not None else None + }) + return _obj + + diff --git a/packages/protocol/octobot_protocol/models/account_trading_state.py b/packages/protocol/octobot_protocol/models/account_trading_state.py new file mode 100644 index 000000000..f4166c2f1 --- /dev/null +++ b/packages/protocol/octobot_protocol/models/account_trading_state.py @@ -0,0 +1,98 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from octobot_protocol.models.account_trading import AccountTrading +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class AccountTradingState(BaseModel): + """ + AccountTradingState + """ # noqa: E501 + version: StrictStr + account_trading: List[AccountTrading] + __properties: ClassVar[List[str]] = ["version", "account_trading"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AccountTradingState from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in account_trading (list) + _items = [] + if self.account_trading: + for _item_account_trading in self.account_trading: + if _item_account_trading: + _items.append(_item_account_trading.to_dict()) + _dict['account_trading'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AccountTradingState from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "version": obj.get("version"), + "account_trading": [AccountTrading.from_dict(_item) for _item in obj["account_trading"]] if obj.get("account_trading") is not None else None + }) + return _obj + + diff --git a/packages/protocol/octobot_protocol/models/accounts_authentication_state.py b/packages/protocol/octobot_protocol/models/accounts_authentication_state.py new file mode 100644 index 000000000..2e69c5d24 --- /dev/null +++ b/packages/protocol/octobot_protocol/models/accounts_authentication_state.py @@ -0,0 +1,98 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from octobot_protocol.models.account_authentication import AccountAuthentication +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class AccountsAuthenticationState(BaseModel): + """ + AccountsAuthenticationState + """ # noqa: E501 + version: StrictStr + account_authentication: List[AccountAuthentication] + __properties: ClassVar[List[str]] = ["version", "account_authentication"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of AccountsAuthenticationState from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in account_authentication (list) + _items = [] + if self.account_authentication: + for _item_account_authentication in self.account_authentication: + if _item_account_authentication: + _items.append(_item_account_authentication.to_dict()) + _dict['account_authentication'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of AccountsAuthenticationState from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "version": obj.get("version"), + "account_authentication": [AccountAuthentication.from_dict(_item) for _item in obj["account_authentication"]] if obj.get("account_authentication") is not None else None + }) + return _obj + + diff --git a/packages/protocol/octobot_protocol/models/automation_action_result_error_message.py b/packages/protocol/octobot_protocol/models/automation_action_result_error_message.py index a7952e925..b65b18b87 100644 --- a/packages/protocol/octobot_protocol/models/automation_action_result_error_message.py +++ b/packages/protocol/octobot_protocol/models/automation_action_result_error_message.py @@ -31,6 +31,7 @@ class AutomationActionResultErrorMessage(str, Enum): STRATEGY_NOT_FOUND = 'strategy_not_found' STRATEGY_VERSION_NOT_FOUND = 'strategy_version_not_found' ACCOUNT_NOT_FOUND = 'account_not_found' + ACCOUNT_AUTHENTICATION_DETAILS_NOT_FOUND = 'account_authentication_details_not_found' INTERNAL_ERROR = 'internal_error' @classmethod diff --git a/packages/protocol/octobot_protocol/models/automation_state.py b/packages/protocol/octobot_protocol/models/automation_state.py index 6b27aa5ff..338121945 100644 --- a/packages/protocol/octobot_protocol/models/automation_state.py +++ b/packages/protocol/octobot_protocol/models/automation_state.py @@ -20,8 +20,8 @@ from pydantic import BaseModel, ConfigDict, StrictStr from typing import Any, ClassVar, Dict, List, Optional from octobot_protocol.models.action import Action -from octobot_protocol.models.asset import Asset from octobot_protocol.models.automation_metadata import AutomationMetadata +from octobot_protocol.models.detailed_asset import DetailedAsset from octobot_protocol.models.order_summary import OrderSummary from octobot_protocol.models.position_summary import PositionSummary from octobot_protocol.models.task_status import TaskStatus @@ -41,7 +41,7 @@ class AutomationState(BaseModel): priority_actions: Optional[List[Action]] = None exchanges: Optional[List[StrictStr]] = None exchange_account_ids: Optional[List[StrictStr]] = None - assets: Optional[List[Asset]] = None + assets: Optional[List[DetailedAsset]] = None orders: Optional[List[OrderSummary]] = None trades: Optional[List[TradeSummary]] = None positions: Optional[List[PositionSummary]] = None @@ -150,7 +150,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "priority_actions": [Action.from_dict(_item) for _item in obj["priority_actions"]] if obj.get("priority_actions") is not None else None, "exchanges": obj.get("exchanges"), "exchange_account_ids": obj.get("exchange_account_ids"), - "assets": [Asset.from_dict(_item) for _item in obj["assets"]] if obj.get("assets") is not None else None, + "assets": [DetailedAsset.from_dict(_item) for _item in obj["assets"]] if obj.get("assets") is not None else None, "orders": [OrderSummary.from_dict(_item) for _item in obj["orders"]] if obj.get("orders") is not None else None, "trades": [TradeSummary.from_dict(_item) for _item in obj["trades"]] if obj.get("trades") is not None else None, "positions": [PositionSummary.from_dict(_item) for _item in obj["positions"]] if obj.get("positions") is not None else None diff --git a/packages/protocol/octobot_protocol/models/blockchain_account.py b/packages/protocol/octobot_protocol/models/blockchain_account.py index 5f79f850f..219b27ede 100644 --- a/packages/protocol/octobot_protocol/models/blockchain_account.py +++ b/packages/protocol/octobot_protocol/models/blockchain_account.py @@ -31,10 +31,7 @@ class BlockchainAccount(BaseModel): account_type: AccountType = Field(description="blockchain") blockchain: StrictStr network: Optional[StrictStr] = None - public_key: Optional[StrictStr] = None - private_key: Optional[StrictStr] = None - passphrase: Optional[StrictStr] = None - __properties: ClassVar[List[str]] = ["account_type", "blockchain", "network", "public_key", "private_key", "passphrase"] + __properties: ClassVar[List[str]] = ["account_type", "blockchain", "network"] model_config = ConfigDict( validate_by_name=True, @@ -89,10 +86,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "account_type": obj.get("account_type"), "blockchain": obj.get("blockchain"), - "network": obj.get("network"), - "public_key": obj.get("public_key"), - "private_key": obj.get("private_key"), - "passphrase": obj.get("passphrase") + "network": obj.get("network") }) return _obj diff --git a/packages/protocol/octobot_protocol/models/cancel_policy.py b/packages/protocol/octobot_protocol/models/cancel_policy.py index 0ba454ee4..01370c29d 100644 --- a/packages/protocol/octobot_protocol/models/cancel_policy.py +++ b/packages/protocol/octobot_protocol/models/cancel_policy.py @@ -29,8 +29,8 @@ class CancelPolicy(BaseModel): CancelPolicy """ # noqa: E501 type: CancelPolicyType - details: Optional[Dict[str, Any]] = None - __properties: ClassVar[List[str]] = ["type", "details"] + specifics: Optional[Dict[str, Any]] = None + __properties: ClassVar[List[str]] = ["type", "specifics"] model_config = ConfigDict( validate_by_name=True, @@ -84,7 +84,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "type": obj.get("type"), - "details": obj.get("details") + "specifics": obj.get("specifics") }) return _obj diff --git a/packages/protocol/octobot_protocol/models/asset.py b/packages/protocol/octobot_protocol/models/detailed_asset.py similarity index 84% rename from packages/protocol/octobot_protocol/models/asset.py rename to packages/protocol/octobot_protocol/models/detailed_asset.py index dad392308..da07af81b 100644 --- a/packages/protocol/octobot_protocol/models/asset.py +++ b/packages/protocol/octobot_protocol/models/detailed_asset.py @@ -18,21 +18,19 @@ import json from pydantic import BaseModel, ConfigDict, StrictFloat, StrictInt, StrictStr -from typing import Any, ClassVar, Dict, List, Optional, Union +from typing import Any, ClassVar, Dict, List, Union from typing import Optional, Set from typing_extensions import Self from pydantic_core import to_jsonable_python -class Asset(BaseModel): +class DetailedAsset(BaseModel): """ - Asset + DetailedAsset """ # noqa: E501 symbol: StrictStr total: Union[StrictFloat, StrictInt] available: Union[StrictFloat, StrictInt] - value: Optional[Union[StrictFloat, StrictInt]] = None - unit: Optional[StrictStr] = None - __properties: ClassVar[List[str]] = ["symbol", "total", "available", "value", "unit"] + __properties: ClassVar[List[str]] = ["symbol", "total", "available"] model_config = ConfigDict( validate_by_name=True, @@ -52,7 +50,7 @@ def to_json(self) -> str: @classmethod def from_json(cls, json_str: str) -> Optional[Self]: - """Create an instance of Asset from a JSON string""" + """Create an instance of DetailedAsset from a JSON string""" return cls.from_dict(json.loads(json_str)) def to_dict(self) -> Dict[str, Any]: @@ -77,7 +75,7 @@ def to_dict(self) -> Dict[str, Any]: @classmethod def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: - """Create an instance of Asset from a dict""" + """Create an instance of DetailedAsset from a dict""" if obj is None: return None @@ -87,9 +85,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "symbol": obj.get("symbol"), "total": obj.get("total"), - "available": obj.get("available"), - "value": obj.get("value"), - "unit": obj.get("unit") + "available": obj.get("available") }) return _obj diff --git a/packages/protocol/octobot_protocol/models/exchange_account.py b/packages/protocol/octobot_protocol/models/exchange_account.py index 865cb37a5..97873504c 100644 --- a/packages/protocol/octobot_protocol/models/exchange_account.py +++ b/packages/protocol/octobot_protocol/models/exchange_account.py @@ -18,12 +18,8 @@ import json from pydantic import BaseModel, ConfigDict, Field, StrictStr -from typing import Any, ClassVar, Dict, List, Optional +from typing import Any, ClassVar, Dict, List from octobot_protocol.models.account_type import AccountType -from octobot_protocol.models.asset import Asset -from octobot_protocol.models.order import Order -from octobot_protocol.models.position import Position -from octobot_protocol.models.trade import Trade from octobot_protocol.models.trading_type import TradingType from typing import Optional, Set from typing_extensions import Self @@ -37,14 +33,7 @@ class ExchangeAccount(BaseModel): trading_type: TradingType exchange: StrictStr remote_account_id: StrictStr - api_key: StrictStr - api_secret: StrictStr - api_passphrase: Optional[StrictStr] = None - assets: Optional[List[Asset]] = None - orders: Optional[List[Order]] = None - trades: Optional[List[Trade]] = None - positions: Optional[List[Position]] = None - __properties: ClassVar[List[str]] = ["account_type", "trading_type", "exchange", "remote_account_id", "api_key", "api_secret", "api_passphrase", "assets", "orders", "trades", "positions"] + __properties: ClassVar[List[str]] = ["account_type", "trading_type", "exchange", "remote_account_id"] model_config = ConfigDict( validate_by_name=True, @@ -85,34 +74,6 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) - # override the default output from pydantic by calling `to_dict()` of each item in assets (list) - _items = [] - if self.assets: - for _item_assets in self.assets: - if _item_assets: - _items.append(_item_assets.to_dict()) - _dict['assets'] = _items - # override the default output from pydantic by calling `to_dict()` of each item in orders (list) - _items = [] - if self.orders: - for _item_orders in self.orders: - if _item_orders: - _items.append(_item_orders.to_dict()) - _dict['orders'] = _items - # override the default output from pydantic by calling `to_dict()` of each item in trades (list) - _items = [] - if self.trades: - for _item_trades in self.trades: - if _item_trades: - _items.append(_item_trades.to_dict()) - _dict['trades'] = _items - # override the default output from pydantic by calling `to_dict()` of each item in positions (list) - _items = [] - if self.positions: - for _item_positions in self.positions: - if _item_positions: - _items.append(_item_positions.to_dict()) - _dict['positions'] = _items return _dict @classmethod @@ -128,14 +89,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "account_type": obj.get("account_type"), "trading_type": obj.get("trading_type"), "exchange": obj.get("exchange"), - "remote_account_id": obj.get("remote_account_id"), - "api_key": obj.get("api_key"), - "api_secret": obj.get("api_secret"), - "api_passphrase": obj.get("api_passphrase"), - "assets": [Asset.from_dict(_item) for _item in obj["assets"]] if obj.get("assets") is not None else None, - "orders": [Order.from_dict(_item) for _item in obj["orders"]] if obj.get("orders") is not None else None, - "trades": [Trade.from_dict(_item) for _item in obj["trades"]] if obj.get("trades") is not None else None, - "positions": [Position.from_dict(_item) for _item in obj["positions"]] if obj.get("positions") is not None else None + "remote_account_id": obj.get("remote_account_id") }) return _obj diff --git a/packages/protocol/octobot_protocol/models/generic_account.py b/packages/protocol/octobot_protocol/models/generic_account.py index 73991976e..25b28f0b2 100644 --- a/packages/protocol/octobot_protocol/models/generic_account.py +++ b/packages/protocol/octobot_protocol/models/generic_account.py @@ -18,9 +18,8 @@ import json from pydantic import BaseModel, ConfigDict, Field -from typing import Any, ClassVar, Dict, List, Optional +from typing import Any, ClassVar, Dict, List from octobot_protocol.models.account_type import AccountType -from octobot_protocol.models.asset import Asset from typing import Optional, Set from typing_extensions import Self from pydantic_core import to_jsonable_python @@ -30,8 +29,7 @@ class GenericAccount(BaseModel): GenericAccount """ # noqa: E501 account_type: AccountType = Field(description="generic") - assets: Optional[List[Asset]] = None - __properties: ClassVar[List[str]] = ["account_type", "assets"] + __properties: ClassVar[List[str]] = ["account_type"] model_config = ConfigDict( validate_by_name=True, @@ -72,13 +70,6 @@ def to_dict(self) -> Dict[str, Any]: exclude=excluded_fields, exclude_none=True, ) - # override the default output from pydantic by calling `to_dict()` of each item in assets (list) - _items = [] - if self.assets: - for _item_assets in self.assets: - if _item_assets: - _items.append(_item_assets.to_dict()) - _dict['assets'] = _items return _dict @classmethod @@ -91,8 +82,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ - "account_type": obj.get("account_type"), - "assets": [Asset.from_dict(_item) for _item in obj["assets"]] if obj.get("assets") is not None else None + "account_type": obj.get("account_type") }) return _obj diff --git a/packages/protocol/octobot_protocol/models/historical_asset_value.py b/packages/protocol/octobot_protocol/models/historical_asset_value.py new file mode 100644 index 000000000..e06ad0232 --- /dev/null +++ b/packages/protocol/octobot_protocol/models/historical_asset_value.py @@ -0,0 +1,92 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictFloat, StrictInt, StrictStr +from typing import Any, ClassVar, Dict, List, Union +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class HistoricalAssetValue(BaseModel): + """ + HistoricalAssetValue + """ # noqa: E501 + symbol: StrictStr + holdings: Union[StrictFloat, StrictInt] + value: Union[StrictFloat, StrictInt] + __properties: ClassVar[List[str]] = ["symbol", "holdings", "value"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of HistoricalAssetValue from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of HistoricalAssetValue from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "symbol": obj.get("symbol"), + "holdings": obj.get("holdings"), + "value": obj.get("value") + }) + return _obj + + diff --git a/packages/protocol/octobot_protocol/models/portfolio_historical_value.py b/packages/protocol/octobot_protocol/models/portfolio_historical_value.py new file mode 100644 index 000000000..516190c1d --- /dev/null +++ b/packages/protocol/octobot_protocol/models/portfolio_historical_value.py @@ -0,0 +1,101 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from datetime import datetime +from pydantic import BaseModel, ConfigDict, StrictFloat, StrictInt +from typing import Any, ClassVar, Dict, List, Optional, Union +from octobot_protocol.models.historical_asset_value import HistoricalAssetValue +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class PortfolioHistoricalValue(BaseModel): + """ + PortfolioHistoricalValue + """ # noqa: E501 + timestamp: datetime + total: Union[StrictFloat, StrictInt] + assets: Optional[List[HistoricalAssetValue]] = None + __properties: ClassVar[List[str]] = ["timestamp", "total", "assets"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PortfolioHistoricalValue from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in assets (list) + _items = [] + if self.assets: + for _item_assets in self.assets: + if _item_assets: + _items.append(_item_assets.to_dict()) + _dict['assets'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PortfolioHistoricalValue from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "timestamp": obj.get("timestamp"), + "total": obj.get("total"), + "assets": [HistoricalAssetValue.from_dict(_item) for _item in obj["assets"]] if obj.get("assets") is not None else None + }) + return _obj + + diff --git a/packages/protocol/octobot_protocol/models/portfolio_historical_values.py b/packages/protocol/octobot_protocol/models/portfolio_historical_values.py new file mode 100644 index 000000000..c97daeee4 --- /dev/null +++ b/packages/protocol/octobot_protocol/models/portfolio_historical_values.py @@ -0,0 +1,98 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List +from octobot_protocol.models.portfolio_historical_value import PortfolioHistoricalValue +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class PortfolioHistoricalValues(BaseModel): + """ + PortfolioHistoricalValues + """ # noqa: E501 + unit: StrictStr + values: List[PortfolioHistoricalValue] + __properties: ClassVar[List[str]] = ["unit", "values"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PortfolioHistoricalValues from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of each item in values (list) + _items = [] + if self.values: + for _item_values in self.values: + if _item_values: + _items.append(_item_values.to_dict()) + _dict['values'] = _items + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PortfolioHistoricalValues from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "unit": obj.get("unit"), + "values": [PortfolioHistoricalValue.from_dict(_item) for _item in obj["values"]] if obj.get("values") is not None else None + }) + return _obj + + diff --git a/packages/protocol/octobot_protocol/models/portfolio_historical_values_state.py b/packages/protocol/octobot_protocol/models/portfolio_historical_values_state.py new file mode 100644 index 000000000..a9bb3655b --- /dev/null +++ b/packages/protocol/octobot_protocol/models/portfolio_historical_values_state.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +from __future__ import annotations +import pprint +import re # noqa: F401 +import json + +from pydantic import BaseModel, ConfigDict, StrictStr +from typing import Any, ClassVar, Dict, List, Optional +from octobot_protocol.models.portfolio_historical_values import PortfolioHistoricalValues +from typing import Optional, Set +from typing_extensions import Self +from pydantic_core import to_jsonable_python + +class PortfolioHistoricalValuesState(BaseModel): + """ + PortfolioHistoricalValuesState + """ # noqa: E501 + version: StrictStr + history: Optional[PortfolioHistoricalValues] = None + __properties: ClassVar[List[str]] = ["version", "history"] + + model_config = ConfigDict( + validate_by_name=True, + validate_by_alias=True, + validate_assignment=True, + protected_namespaces=(), + ) + + + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) + + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return json.dumps(to_jsonable_python(self.to_dict())) + + @classmethod + def from_json(cls, json_str: str) -> Optional[Self]: + """Create an instance of PortfolioHistoricalValuesState from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias. + + This has the following differences from calling pydantic's + `self.model_dump(by_alias=True)`: + + * `None` is only added to the output dict for nullable fields that + were set at model initialization. Other fields with value `None` + are ignored. + """ + excluded_fields: Set[str] = set([ + ]) + + _dict = self.model_dump( + by_alias=True, + exclude=excluded_fields, + exclude_none=True, + ) + # override the default output from pydantic by calling `to_dict()` of history + if self.history: + _dict['history'] = self.history.to_dict() + return _dict + + @classmethod + def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: + """Create an instance of PortfolioHistoricalValuesState from a dict""" + if obj is None: + return None + + if not isinstance(obj, dict): + return cls.model_validate(obj) + + _obj = cls.model_validate({ + "version": obj.get("version"), + "history": PortfolioHistoricalValues.from_dict(obj["history"]) if obj.get("history") is not None else None + }) + return _obj + + diff --git a/packages/protocol/octobot_protocol/models/strategy.py b/packages/protocol/octobot_protocol/models/strategy.py index c447b7f0b..146178e53 100644 --- a/packages/protocol/octobot_protocol/models/strategy.py +++ b/packages/protocol/octobot_protocol/models/strategy.py @@ -35,8 +35,9 @@ class Strategy(BaseModel): description: Optional[StrictStr] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None + reference_market: StrictStr configuration: StrategyConfiguration - __properties: ClassVar[List[str]] = ["id", "version", "name", "description", "created_at", "updated_at", "configuration"] + __properties: ClassVar[List[str]] = ["id", "version", "name", "description", "created_at", "updated_at", "reference_market", "configuration"] model_config = ConfigDict( validate_by_name=True, @@ -98,6 +99,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: "description": obj.get("description"), "created_at": obj.get("created_at"), "updated_at": obj.get("updated_at"), + "reference_market": obj.get("reference_market"), "configuration": StrategyConfiguration.from_dict(obj["configuration"]) if obj.get("configuration") is not None else None }) return _obj diff --git a/packages/protocol/octobot_protocol/models/trailing_profile.py b/packages/protocol/octobot_protocol/models/trailing_profile.py index 64c2e6791..17a7acd97 100644 --- a/packages/protocol/octobot_protocol/models/trailing_profile.py +++ b/packages/protocol/octobot_protocol/models/trailing_profile.py @@ -29,8 +29,8 @@ class TrailingProfile(BaseModel): TrailingProfile """ # noqa: E501 type: TrailingProfileType - details: Optional[Dict[str, Any]] = None - __properties: ClassVar[List[str]] = ["type", "details"] + specifics: Optional[Dict[str, Any]] = None + __properties: ClassVar[List[str]] = ["type", "specifics"] model_config = ConfigDict( validate_by_name=True, @@ -84,7 +84,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "type": obj.get("type"), - "details": obj.get("details") + "specifics": obj.get("specifics") }) return _obj diff --git a/packages/protocol/openapi.json b/packages/protocol/openapi.json index e1f0db671..bc01e40b9 100644 --- a/packages/protocol/openapi.json +++ b/packages/protocol/openapi.json @@ -98,7 +98,7 @@ "type": { "$ref": "#/components/schemas/TrailingProfileType" }, - "details": { + "specifics": { "type": "object" } } @@ -113,7 +113,7 @@ "type": { "$ref": "#/components/schemas/CancelPolicyType" }, - "details": { + "specifics": { "type": "object" } } @@ -416,8 +416,8 @@ } } }, - "Asset": { - "description": "Asset", + "DetailedAsset": { + "description": "DetailedAsset", "type": "object", "required": [ "symbol", @@ -433,12 +433,6 @@ }, "available": { "type": "number" - }, - "value": { - "type": "number" - }, - "unit": { - "type": "string" } } }, @@ -535,7 +529,7 @@ "assets": { "type": "array", "items": { - "$ref": "#/components/schemas/Asset" + "$ref": "#/components/schemas/DetailedAsset" } }, "orders": { @@ -577,45 +571,102 @@ "margin" ] }, - "ExchangeAccount": { - "description": "ExchangeAccount", + "HistoricalAssetValue": { + "description": "HistoricalAssetValue", "type": "object", "required": [ - "account_type", - "trading_type", - "exchange", - "remote_account_id", - "api_key", - "api_secret" + "symbol", + "holdings", + "value" ], "properties": { - "account_type": { - "$ref": "#/components/schemas/AccountType", - "description": "exchange" - }, - "trading_type": { - "$ref": "#/components/schemas/TradingType" - }, - "exchange": { + "symbol": { "type": "string" }, - "remote_account_id": { - "type": "string" + "holdings": { + "type": "number" }, - "api_key": { - "type": "string" + "value": { + "type": "number" + } + } + }, + "PortfolioHistoricalValue": { + "description": "PortfolioHistoricalValue", + "type": "object", + "required": [ + "timestamp", + "total" + ], + "properties": { + "timestamp": { + "type": "string", + "format": "date-time" }, - "api_secret": { - "type": "string" + "total": { + "type": "number" }, - "api_passphrase": { + "assets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HistoricalAssetValue" + } + } + } + }, + "PortfolioHistoricalValues": { + "description": "PortfolioHistoricalValues", + "type": "object", + "required": [ + "values", + "unit" + ], + "properties": { + "unit": { "type": "string" }, - "assets": { + "values": { "type": "array", "items": { - "$ref": "#/components/schemas/Asset" + "$ref": "#/components/schemas/PortfolioHistoricalValue" + } + } + } + }, + "AccountAuthentication": { + "description": "AccountAuthentication", + "type": "object", + "properties": { + "api_key": { + "type": "string" + }, + "api_secret": { + "type": "string" + }, + "api_passphrase": { + "type": "string" + }, + "public_key": { + "type": "string" + }, + "private_key": { + "type": "string" + }, + "seed_phrase": { + "type": "string" } + } + }, + "AccountTrading": { + "description": "AccountTrading", + "type": "object", + "required": [ + "updated_at" + ], + "properties": { + "updated_at": { + "type": "string", + "format": "date-time" }, "orders": { "type": "array", @@ -637,6 +688,31 @@ } } }, + "ExchangeAccount": { + "description": "ExchangeAccount", + "type": "object", + "required": [ + "account_type", + "trading_type", + "exchange", + "remote_account_id" + ], + "properties": { + "account_type": { + "$ref": "#/components/schemas/AccountType", + "description": "exchange" + }, + "trading_type": { + "$ref": "#/components/schemas/TradingType" + }, + "exchange": { + "type": "string" + }, + "remote_account_id": { + "type": "string" + } + } + }, "BlockchainAccount": { "description": "BlockchainAccount", "type": "object", @@ -654,15 +730,6 @@ }, "network": { "type": "string" - }, - "public_key": { - "type": "string" - }, - "private_key": { - "type": "string" - }, - "passphrase": { - "type": "string" } } }, @@ -676,12 +743,6 @@ "account_type": { "$ref": "#/components/schemas/AccountType", "description": "generic" - }, - "assets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Asset" - } } } }, @@ -758,7 +819,13 @@ "type": "string", "format": "date-time" }, - "details": { + "assets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DetailedAsset" + } + }, + "specifics": { "oneOf": [ { "$ref": "#/components/schemas/ExchangeAccount" @@ -1747,6 +1814,7 @@ "strategy_not_found", "strategy_version_not_found", "account_not_found", + "account_authentication_details_not_found", "internal_error" ] }, @@ -1782,6 +1850,7 @@ "enum": [ "account_not_found", "invalid_configuration", + "account_authentication_details_not_found", "internal_error" ] }, @@ -1891,6 +1960,7 @@ "required": [ "id", "version", + "reference_market", "configuration" ], "properties": { @@ -1914,6 +1984,9 @@ "type": "string", "format": "date-time" }, + "reference_market": { + "type": "string" + }, "configuration": { "oneOf": [ { @@ -1971,6 +2044,60 @@ } } } + }, + "AccountTradingState": { + "description": "AccountTradingState", + "type": "object", + "required": [ + "version", + "account_trading" + ], + "properties": { + "version": { + "type": "string" + }, + "account_trading": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AccountTrading" + } + } + } + }, + "PortfolioHistoricalValuesState": { + "description": "PortfolioHistoricalValuesState", + "type": "object", + "required": [ + "version", + "values" + ], + "properties": { + "version": { + "type": "string" + }, + "history": { + "$ref": "#/components/schemas/PortfolioHistoricalValues" + } + } + }, + "AccountsAuthenticationState": { + "description": "AccountsAuthenticationState", + "type": "object", + "required": [ + "version", + "account_authentication" + ], + "properties": { + "version": { + "type": "string" + }, + "account_authentication": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AccountAuthentication" + } + } + } } } } diff --git a/packages/protocol/test/test_account.py b/packages/protocol/test/test_account.py index 6ff9598e0..72560895f 100644 --- a/packages/protocol/test/test_account.py +++ b/packages/protocol/test/test_account.py @@ -44,7 +44,13 @@ def make_instance(self, include_optional) -> Account: message = 'pending_validation', ), created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - details = None + assets = [ + octobot_protocol.models.detailed_asset.DetailedAsset( + symbol = '', + total = 1.337, + available = 1.337, ) + ], + specifics = None ) else: return Account( diff --git a/packages/protocol/test/test_account_authentication.py b/packages/protocol/test/test_account_authentication.py new file mode 100644 index 000000000..a0d0e9235 --- /dev/null +++ b/packages/protocol/test/test_account_authentication.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from octobot_protocol.models.account_authentication import AccountAuthentication + +class TestAccountAuthentication(unittest.TestCase): + """AccountAuthentication unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AccountAuthentication: + """Test AccountAuthentication + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AccountAuthentication` + """ + model = AccountAuthentication() + if include_optional: + return AccountAuthentication( + api_key = '', + api_secret = '', + api_passphrase = '', + public_key = '', + private_key = '', + seed_phrase = '' + ) + else: + return AccountAuthentication( + ) + """ + + def testAccountAuthentication(self): + """Test AccountAuthentication""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/packages/protocol/test/test_account_specifics.py b/packages/protocol/test/test_account_specifics.py new file mode 100644 index 000000000..f6db7c8fd --- /dev/null +++ b/packages/protocol/test/test_account_specifics.py @@ -0,0 +1,61 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from octobot_protocol.models.account_specifics import AccountSpecifics + +class TestAccountSpecifics(unittest.TestCase): + """AccountSpecifics unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AccountSpecifics: + """Test AccountSpecifics + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AccountSpecifics` + """ + model = AccountSpecifics() + if include_optional: + return AccountSpecifics( + account_type = 'generic', + trading_type = 'spot', + exchange = '', + remote_account_id = '', + blockchain = '', + network = '' + ) + else: + return AccountSpecifics( + account_type = 'generic', + trading_type = 'spot', + exchange = '', + remote_account_id = '', + blockchain = '', + ) + """ + + def testAccountSpecifics(self): + """Test AccountSpecifics""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/packages/protocol/test/test_account_details.py b/packages/protocol/test/test_account_trading.py similarity index 72% rename from packages/protocol/test/test_account_details.py rename to packages/protocol/test/test_account_trading.py index 5d72996db..d1e022fd1 100644 --- a/packages/protocol/test/test_account_details.py +++ b/packages/protocol/test/test_account_trading.py @@ -14,10 +14,10 @@ import unittest -from octobot_protocol.models.account_details import AccountDetails +from octobot_protocol.models.account_trading import AccountTrading -class TestAccountDetails(unittest.TestCase): - """AccountDetails unit test stubs""" +class TestAccountTrading(unittest.TestCase): + """AccountTrading unit test stubs""" def setUp(self): pass @@ -25,31 +25,17 @@ def setUp(self): def tearDown(self): pass - def make_instance(self, include_optional) -> AccountDetails: - """Test AccountDetails + def make_instance(self, include_optional) -> AccountTrading: + """Test AccountTrading include_optional is a boolean, when False only required params are included, when True both required and optional params are included """ - # uncomment below to create an instance of `AccountDetails` + # uncomment below to create an instance of `AccountTrading` """ - model = AccountDetails() + model = AccountTrading() if include_optional: - return AccountDetails( - account_type = 'generic', - trading_type = 'spot', - exchange = '', - remote_account_id = '', - api_key = '', - api_secret = '', - api_passphrase = '', - assets = [ - octobot_protocol.models.asset.Asset( - symbol = '', - total = 1.337, - available = 1.337, - value = 1.337, - unit = '', ) - ], + return AccountTrading( + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), orders = [ octobot_protocol.models.order.Order( id = '', @@ -77,10 +63,10 @@ def make_instance(self, include_optional) -> AccountDetails: timeout = 1.337, ), ), trailing_profile = octobot_protocol.models.trailing_profile.TrailingProfile( type = 'filled_take_profit', - details = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), cancel_policy = octobot_protocol.models.cancel_policy.CancelPolicy( type = 'ExpirationTimeOrderCancelPolicy', - details = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), chained_orders = [ octobot_protocol.models.order.Order( id = '', @@ -121,27 +107,16 @@ def make_instance(self, include_optional) -> AccountDetails: mark_price = 1.337, liquidation_price = 1.337, status = 'open', ) - ], - blockchain = '', - network = '', - public_key = '', - private_key = '', - passphrase = '' + ] ) else: - return AccountDetails( - account_type = 'generic', - trading_type = 'spot', - exchange = '', - remote_account_id = '', - api_key = '', - api_secret = '', - blockchain = '', + return AccountTrading( + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ) """ - def testAccountDetails(self): - """Test AccountDetails""" + def testAccountTrading(self): + """Test AccountTrading""" # inst_req_only = self.make_instance(include_optional=False) # inst_req_and_optional = self.make_instance(include_optional=True) diff --git a/packages/protocol/test/test_account_trading_state.py b/packages/protocol/test/test_account_trading_state.py new file mode 100644 index 000000000..94b361a16 --- /dev/null +++ b/packages/protocol/test/test_account_trading_state.py @@ -0,0 +1,204 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from octobot_protocol.models.account_trading_state import AccountTradingState + +class TestAccountTradingState(unittest.TestCase): + """AccountTradingState unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AccountTradingState: + """Test AccountTradingState + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AccountTradingState` + """ + model = AccountTradingState() + if include_optional: + return AccountTradingState( + version = '', + account_trading = [ + octobot_protocol.models.account_trading.AccountTrading( + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + orders = [ + octobot_protocol.models.order.Order( + id = '', + symbol = '', + price = 1.337, + quantity = 1.337, + filled = 1.337, + exchange_id = '', + side = 'buy', + type = 'limit', + trigger_above = True, + reduce_only = True, + is_active = True, + status = 'pending_creation', + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + entries = [ + '' + ], + update_with_triggering_order_fees = True, + order_group = octobot_protocol.models.order_group.OrderGroup( + id = '', + active_order_swap_strategy = octobot_protocol.models.active_order_swap_strategy.ActiveOrderSwapStrategy( + type = 'StopFirstActiveOrderSwapStrategy', + trigger_price_configuration = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), + timeout = 1.337, ), ), + trailing_profile = octobot_protocol.models.trailing_profile.TrailingProfile( + type = 'filled_take_profit', + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + cancel_policy = octobot_protocol.models.cancel_policy.CancelPolicy( + type = 'ExpirationTimeOrderCancelPolicy', + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + chained_orders = [ + octobot_protocol.models.order.Order( + id = '', + symbol = '', + price = 1.337, + quantity = 1.337, + filled = 1.337, + exchange_id = '', + side = 'buy', + type = 'limit', + trigger_above = True, + reduce_only = True, + is_active = True, + status = 'pending_creation', + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + update_with_triggering_order_fees = True, ) + ], ) + ], + trades = [ + octobot_protocol.models.trade.Trade( + id = '', + trade_id = '', + type = , + symbol = '', + side = , + quantity = 1.337, + price = 1.337, + status = , + executed_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ) + ], + positions = [ + octobot_protocol.models.position.Position( + id = '', + symbol = '', + side = , + quantity = 1.337, + entry_price = 1.337, + mark_price = 1.337, + liquidation_price = 1.337, + status = 'open', ) + ], ) + ] + ) + else: + return AccountTradingState( + version = '', + account_trading = [ + octobot_protocol.models.account_trading.AccountTrading( + updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + orders = [ + octobot_protocol.models.order.Order( + id = '', + symbol = '', + price = 1.337, + quantity = 1.337, + filled = 1.337, + exchange_id = '', + side = 'buy', + type = 'limit', + trigger_above = True, + reduce_only = True, + is_active = True, + status = 'pending_creation', + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + entries = [ + '' + ], + update_with_triggering_order_fees = True, + order_group = octobot_protocol.models.order_group.OrderGroup( + id = '', + active_order_swap_strategy = octobot_protocol.models.active_order_swap_strategy.ActiveOrderSwapStrategy( + type = 'StopFirstActiveOrderSwapStrategy', + trigger_price_configuration = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), + timeout = 1.337, ), ), + trailing_profile = octobot_protocol.models.trailing_profile.TrailingProfile( + type = 'filled_take_profit', + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + cancel_policy = octobot_protocol.models.cancel_policy.CancelPolicy( + type = 'ExpirationTimeOrderCancelPolicy', + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + chained_orders = [ + octobot_protocol.models.order.Order( + id = '', + symbol = '', + price = 1.337, + quantity = 1.337, + filled = 1.337, + exchange_id = '', + side = 'buy', + type = 'limit', + trigger_above = True, + reduce_only = True, + is_active = True, + status = 'pending_creation', + created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + update_with_triggering_order_fees = True, ) + ], ) + ], + trades = [ + octobot_protocol.models.trade.Trade( + id = '', + trade_id = '', + type = , + symbol = '', + side = , + quantity = 1.337, + price = 1.337, + status = , + executed_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ) + ], + positions = [ + octobot_protocol.models.position.Position( + id = '', + symbol = '', + side = , + quantity = 1.337, + entry_price = 1.337, + mark_price = 1.337, + liquidation_price = 1.337, + status = 'open', ) + ], ) + ], + ) + """ + + def testAccountTradingState(self): + """Test AccountTradingState""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/packages/protocol/test/test_accounts_authentication_state.py b/packages/protocol/test/test_accounts_authentication_state.py new file mode 100644 index 000000000..63cc3319a --- /dev/null +++ b/packages/protocol/test/test_accounts_authentication_state.py @@ -0,0 +1,70 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from octobot_protocol.models.accounts_authentication_state import AccountsAuthenticationState + +class TestAccountsAuthenticationState(unittest.TestCase): + """AccountsAuthenticationState unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> AccountsAuthenticationState: + """Test AccountsAuthenticationState + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `AccountsAuthenticationState` + """ + model = AccountsAuthenticationState() + if include_optional: + return AccountsAuthenticationState( + version = '', + account_authentication = [ + octobot_protocol.models.account_authentication.AccountAuthentication( + api_key = '', + api_secret = '', + api_passphrase = '', + public_key = '', + private_key = '', + seed_phrase = '', ) + ] + ) + else: + return AccountsAuthenticationState( + version = '', + account_authentication = [ + octobot_protocol.models.account_authentication.AccountAuthentication( + api_key = '', + api_secret = '', + api_passphrase = '', + public_key = '', + private_key = '', + seed_phrase = '', ) + ], + ) + """ + + def testAccountsAuthenticationState(self): + """Test AccountsAuthenticationState""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/packages/protocol/test/test_accounts_state.py b/packages/protocol/test/test_accounts_state.py index 07cb7c33e..d50d2ea02 100644 --- a/packages/protocol/test/test_accounts_state.py +++ b/packages/protocol/test/test_accounts_state.py @@ -47,7 +47,13 @@ def make_instance(self, include_optional) -> AccountsState: message = 'pending_validation', ), created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - details = null, ) + assets = [ + octobot_protocol.models.detailed_asset.DetailedAsset( + symbol = '', + total = 1.337, + available = 1.337, ) + ], + specifics = null, ) ] ) else: diff --git a/packages/protocol/test/test_automation_state.py b/packages/protocol/test/test_automation_state.py index 05811dc53..ea8de426b 100644 --- a/packages/protocol/test/test_automation_state.py +++ b/packages/protocol/test/test_automation_state.py @@ -71,12 +71,10 @@ def make_instance(self, include_optional) -> AutomationState: '' ], assets = [ - octobot_protocol.models.asset.Asset( + octobot_protocol.models.detailed_asset.DetailedAsset( symbol = '', total = 1.337, - available = 1.337, - value = 1.337, - unit = '', ) + available = 1.337, ) ], orders = [ octobot_protocol.models.order_summary.OrderSummary( diff --git a/packages/protocol/test/test_blockchain_account.py b/packages/protocol/test/test_blockchain_account.py index fc57a8e54..94ef86be0 100644 --- a/packages/protocol/test/test_blockchain_account.py +++ b/packages/protocol/test/test_blockchain_account.py @@ -37,10 +37,7 @@ def make_instance(self, include_optional) -> BlockchainAccount: return BlockchainAccount( account_type = 'generic', blockchain = '', - network = '', - public_key = '', - private_key = '', - passphrase = '' + network = '' ) else: return BlockchainAccount( diff --git a/packages/protocol/test/test_cancel_policy.py b/packages/protocol/test/test_cancel_policy.py index 7aaafb1c1..3f93d60c8 100644 --- a/packages/protocol/test/test_cancel_policy.py +++ b/packages/protocol/test/test_cancel_policy.py @@ -36,7 +36,7 @@ def make_instance(self, include_optional) -> CancelPolicy: if include_optional: return CancelPolicy( type = 'ExpirationTimeOrderCancelPolicy', - details = None + specifics = None ) else: return CancelPolicy( diff --git a/packages/protocol/test/test_copied_account.py b/packages/protocol/test/test_copied_account.py index 340a7c3d8..d6199e326 100644 --- a/packages/protocol/test/test_copied_account.py +++ b/packages/protocol/test/test_copied_account.py @@ -71,10 +71,10 @@ def make_instance(self, include_optional) -> CopiedAccount: timeout = 1.337, ), ), trailing_profile = octobot_protocol.models.trailing_profile.TrailingProfile( type = 'filled_take_profit', - details = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), cancel_policy = octobot_protocol.models.cancel_policy.CancelPolicy( type = 'ExpirationTimeOrderCancelPolicy', - details = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), chained_orders = [ octobot_protocol.models.order.Order( id = '', @@ -142,10 +142,10 @@ def make_instance(self, include_optional) -> CopiedAccount: timeout = 1.337, ), ), trailing_profile = octobot_protocol.models.trailing_profile.TrailingProfile( type = 'filled_take_profit', - details = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), cancel_policy = octobot_protocol.models.cancel_policy.CancelPolicy( type = 'ExpirationTimeOrderCancelPolicy', - details = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), chained_orders = [ octobot_protocol.models.order.Order( id = '', diff --git a/packages/protocol/test/test_create_account_configuration.py b/packages/protocol/test/test_create_account_configuration.py index 2941c42f6..ab197dac4 100644 --- a/packages/protocol/test/test_create_account_configuration.py +++ b/packages/protocol/test/test_create_account_configuration.py @@ -46,7 +46,13 @@ def make_instance(self, include_optional) -> CreateAccountConfiguration: message = 'pending_validation', ), created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - details = null, ) + assets = [ + octobot_protocol.models.detailed_asset.DetailedAsset( + symbol = '', + total = 1.337, + available = 1.337, ) + ], + specifics = null, ) ) else: return CreateAccountConfiguration( @@ -61,7 +67,13 @@ def make_instance(self, include_optional) -> CreateAccountConfiguration: message = 'pending_validation', ), created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - details = null, ), + assets = [ + octobot_protocol.models.detailed_asset.DetailedAsset( + symbol = '', + total = 1.337, + available = 1.337, ) + ], + specifics = null, ), ) """ diff --git a/packages/protocol/test/test_asset.py b/packages/protocol/test/test_detailed_asset.py similarity index 67% rename from packages/protocol/test/test_asset.py rename to packages/protocol/test/test_detailed_asset.py index b9bf57613..9cf206779 100644 --- a/packages/protocol/test/test_asset.py +++ b/packages/protocol/test/test_detailed_asset.py @@ -14,10 +14,10 @@ import unittest -from octobot_protocol.models.asset import Asset +from octobot_protocol.models.detailed_asset import DetailedAsset -class TestAsset(unittest.TestCase): - """Asset unit test stubs""" +class TestDetailedAsset(unittest.TestCase): + """DetailedAsset unit test stubs""" def setUp(self): pass @@ -25,32 +25,30 @@ def setUp(self): def tearDown(self): pass - def make_instance(self, include_optional) -> Asset: - """Test Asset + def make_instance(self, include_optional) -> DetailedAsset: + """Test DetailedAsset include_optional is a boolean, when False only required params are included, when True both required and optional params are included """ - # uncomment below to create an instance of `Asset` + # uncomment below to create an instance of `DetailedAsset` """ - model = Asset() + model = DetailedAsset() if include_optional: - return Asset( + return DetailedAsset( symbol = '', total = 1.337, - available = 1.337, - value = 1.337, - unit = '' + available = 1.337 ) else: - return Asset( + return DetailedAsset( symbol = '', total = 1.337, available = 1.337, ) """ - def testAsset(self): - """Test Asset""" + def testDetailedAsset(self): + """Test DetailedAsset""" # inst_req_only = self.make_instance(include_optional=False) # inst_req_and_optional = self.make_instance(include_optional=True) diff --git a/packages/protocol/test/test_edit_account_configuration.py b/packages/protocol/test/test_edit_account_configuration.py index 15c7e80b1..57f61db09 100644 --- a/packages/protocol/test/test_edit_account_configuration.py +++ b/packages/protocol/test/test_edit_account_configuration.py @@ -47,7 +47,13 @@ def make_instance(self, include_optional) -> EditAccountConfiguration: message = 'pending_validation', ), created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - details = null, ) + assets = [ + octobot_protocol.models.detailed_asset.DetailedAsset( + symbol = '', + total = 1.337, + available = 1.337, ) + ], + specifics = null, ) ) else: return EditAccountConfiguration( diff --git a/packages/protocol/test/test_exchange_account.py b/packages/protocol/test/test_exchange_account.py index 2b4e2d382..7c00ac200 100644 --- a/packages/protocol/test/test_exchange_account.py +++ b/packages/protocol/test/test_exchange_account.py @@ -38,90 +38,7 @@ def make_instance(self, include_optional) -> ExchangeAccount: account_type = 'generic', trading_type = 'spot', exchange = '', - remote_account_id = '', - api_key = '', - api_secret = '', - api_passphrase = '', - assets = [ - octobot_protocol.models.asset.Asset( - symbol = '', - total = 1.337, - available = 1.337, - value = 1.337, - unit = '', ) - ], - orders = [ - octobot_protocol.models.order.Order( - id = '', - symbol = '', - price = 1.337, - quantity = 1.337, - filled = 1.337, - exchange_id = '', - side = 'buy', - type = 'limit', - trigger_above = True, - reduce_only = True, - is_active = True, - status = 'pending_creation', - created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - entries = [ - '' - ], - update_with_triggering_order_fees = True, - order_group = octobot_protocol.models.order_group.OrderGroup( - id = '', - active_order_swap_strategy = octobot_protocol.models.active_order_swap_strategy.ActiveOrderSwapStrategy( - type = 'StopFirstActiveOrderSwapStrategy', - trigger_price_configuration = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), - timeout = 1.337, ), ), - trailing_profile = octobot_protocol.models.trailing_profile.TrailingProfile( - type = 'filled_take_profit', - details = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), - cancel_policy = octobot_protocol.models.cancel_policy.CancelPolicy( - type = 'ExpirationTimeOrderCancelPolicy', - details = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), - chained_orders = [ - octobot_protocol.models.order.Order( - id = '', - symbol = '', - price = 1.337, - quantity = 1.337, - filled = 1.337, - exchange_id = '', - side = 'buy', - type = 'limit', - trigger_above = True, - reduce_only = True, - is_active = True, - status = 'pending_creation', - created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - update_with_triggering_order_fees = True, ) - ], ) - ], - trades = [ - octobot_protocol.models.trade.Trade( - id = '', - trade_id = '', - type = 'limit', - symbol = '', - side = 'buy', - quantity = 1.337, - price = 1.337, - status = 'pending_creation', - executed_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), ) - ], - positions = [ - octobot_protocol.models.position.Position( - id = '', - symbol = '', - side = 'buy', - quantity = 1.337, - entry_price = 1.337, - mark_price = 1.337, - liquidation_price = 1.337, - status = 'open', ) - ] + remote_account_id = '' ) else: return ExchangeAccount( @@ -129,8 +46,6 @@ def make_instance(self, include_optional) -> ExchangeAccount: trading_type = 'spot', exchange = '', remote_account_id = '', - api_key = '', - api_secret = '', ) """ diff --git a/packages/protocol/test/test_generic_account.py b/packages/protocol/test/test_generic_account.py index 13de16572..e3750b23b 100644 --- a/packages/protocol/test/test_generic_account.py +++ b/packages/protocol/test/test_generic_account.py @@ -35,15 +35,7 @@ def make_instance(self, include_optional) -> GenericAccount: model = GenericAccount() if include_optional: return GenericAccount( - account_type = 'generic', - assets = [ - octobot_protocol.models.asset.Asset( - symbol = '', - total = 1.337, - available = 1.337, - value = 1.337, - unit = '', ) - ] + account_type = 'generic' ) else: return GenericAccount( diff --git a/packages/protocol/test/test_historical_asset_value.py b/packages/protocol/test/test_historical_asset_value.py new file mode 100644 index 000000000..9825df0bb --- /dev/null +++ b/packages/protocol/test/test_historical_asset_value.py @@ -0,0 +1,56 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from octobot_protocol.models.historical_asset_value import HistoricalAssetValue + +class TestHistoricalAssetValue(unittest.TestCase): + """HistoricalAssetValue unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> HistoricalAssetValue: + """Test HistoricalAssetValue + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `HistoricalAssetValue` + """ + model = HistoricalAssetValue() + if include_optional: + return HistoricalAssetValue( + symbol = '', + holdings = 1.337, + value = 1.337 + ) + else: + return HistoricalAssetValue( + symbol = '', + holdings = 1.337, + value = 1.337, + ) + """ + + def testHistoricalAssetValue(self): + """Test HistoricalAssetValue""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/packages/protocol/test/test_order.py b/packages/protocol/test/test_order.py index 11866c9f4..2ce792bfc 100644 --- a/packages/protocol/test/test_order.py +++ b/packages/protocol/test/test_order.py @@ -61,10 +61,10 @@ def make_instance(self, include_optional) -> Order: timeout = 1.337, ), ), trailing_profile = octobot_protocol.models.trailing_profile.TrailingProfile( type = 'filled_take_profit', - details = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), cancel_policy = octobot_protocol.models.cancel_policy.CancelPolicy( type = 'ExpirationTimeOrderCancelPolicy', - details = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), chained_orders = [ octobot_protocol.models.order.Order( id = '', @@ -92,10 +92,10 @@ def make_instance(self, include_optional) -> Order: timeout = 1.337, ), ), trailing_profile = octobot_protocol.models.trailing_profile.TrailingProfile( type = 'filled_take_profit', - details = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), cancel_policy = octobot_protocol.models.cancel_policy.CancelPolicy( type = 'ExpirationTimeOrderCancelPolicy', - details = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), + specifics = octobot_protocol.models.trigger_price_configuration.trigger_price_configuration(), ), chained_orders = [ octobot_protocol.models.order.Order( id = '', diff --git a/packages/protocol/test/test_portfolio_historical_value.py b/packages/protocol/test/test_portfolio_historical_value.py new file mode 100644 index 000000000..39d155e40 --- /dev/null +++ b/packages/protocol/test/test_portfolio_historical_value.py @@ -0,0 +1,60 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from octobot_protocol.models.portfolio_historical_value import PortfolioHistoricalValue + +class TestPortfolioHistoricalValue(unittest.TestCase): + """PortfolioHistoricalValue unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PortfolioHistoricalValue: + """Test PortfolioHistoricalValue + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PortfolioHistoricalValue` + """ + model = PortfolioHistoricalValue() + if include_optional: + return PortfolioHistoricalValue( + timestamp = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + total = 1.337, + assets = [ + octobot_protocol.models.historical_asset_value.HistoricalAssetValue( + symbol = '', + holdings = 1.337, + value = 1.337, ) + ] + ) + else: + return PortfolioHistoricalValue( + timestamp = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + total = 1.337, + ) + """ + + def testPortfolioHistoricalValue(self): + """Test PortfolioHistoricalValue""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/packages/protocol/test/test_portfolio_historical_values.py b/packages/protocol/test/test_portfolio_historical_values.py new file mode 100644 index 000000000..3b5d996ac --- /dev/null +++ b/packages/protocol/test/test_portfolio_historical_values.py @@ -0,0 +1,74 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from octobot_protocol.models.portfolio_historical_values import PortfolioHistoricalValues + +class TestPortfolioHistoricalValues(unittest.TestCase): + """PortfolioHistoricalValues unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PortfolioHistoricalValues: + """Test PortfolioHistoricalValues + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PortfolioHistoricalValues` + """ + model = PortfolioHistoricalValues() + if include_optional: + return PortfolioHistoricalValues( + unit = '', + values = [ + octobot_protocol.models.portfolio_historical_value.PortfolioHistoricalValue( + timestamp = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + total = 1.337, + assets = [ + octobot_protocol.models.historical_asset_value.HistoricalAssetValue( + symbol = '', + holdings = 1.337, + value = 1.337, ) + ], ) + ] + ) + else: + return PortfolioHistoricalValues( + unit = '', + values = [ + octobot_protocol.models.portfolio_historical_value.PortfolioHistoricalValue( + timestamp = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + total = 1.337, + assets = [ + octobot_protocol.models.historical_asset_value.HistoricalAssetValue( + symbol = '', + holdings = 1.337, + value = 1.337, ) + ], ) + ], + ) + """ + + def testPortfolioHistoricalValues(self): + """Test PortfolioHistoricalValues""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/packages/protocol/test/test_portfolio_historical_values_state.py b/packages/protocol/test/test_portfolio_historical_values_state.py new file mode 100644 index 000000000..17eb1c605 --- /dev/null +++ b/packages/protocol/test/test_portfolio_historical_values_state.py @@ -0,0 +1,65 @@ +# coding: utf-8 + +""" + OctoBot protocol types + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + + The version of the OpenAPI document: 1.0.0 + Generated by OpenAPI Generator (https://openapi-generator.tech) + + Do not edit the class manually. +""" # noqa: E501 + + +import unittest + +from octobot_protocol.models.portfolio_historical_values_state import PortfolioHistoricalValuesState + +class TestPortfolioHistoricalValuesState(unittest.TestCase): + """PortfolioHistoricalValuesState unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional) -> PortfolioHistoricalValuesState: + """Test PortfolioHistoricalValuesState + include_optional is a boolean, when False only required + params are included, when True both required and + optional params are included """ + # uncomment below to create an instance of `PortfolioHistoricalValuesState` + """ + model = PortfolioHistoricalValuesState() + if include_optional: + return PortfolioHistoricalValuesState( + version = '', + history = octobot_protocol.models.portfolio_historical_values.PortfolioHistoricalValues( + unit = '', + values = [ + octobot_protocol.models.portfolio_historical_value.PortfolioHistoricalValue( + timestamp = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + total = 1.337, + assets = [ + octobot_protocol.models.historical_asset_value.HistoricalAssetValue( + symbol = '', + holdings = 1.337, + value = 1.337, ) + ], ) + ], ) + ) + else: + return PortfolioHistoricalValuesState( + version = '', + ) + """ + + def testPortfolioHistoricalValuesState(self): + """Test PortfolioHistoricalValuesState""" + # inst_req_only = self.make_instance(include_optional=False) + # inst_req_and_optional = self.make_instance(include_optional=True) + +if __name__ == '__main__': + unittest.main() diff --git a/packages/protocol/test/test_strategies_state.py b/packages/protocol/test/test_strategies_state.py index a82260862..aa3654610 100644 --- a/packages/protocol/test/test_strategies_state.py +++ b/packages/protocol/test/test_strategies_state.py @@ -44,6 +44,7 @@ def make_instance(self, include_optional) -> StrategiesState: description = '', created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + reference_market = '', configuration = null, ) ] ) @@ -58,6 +59,7 @@ def make_instance(self, include_optional) -> StrategiesState: description = '', created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + reference_market = '', configuration = null, ) ], ) diff --git a/packages/protocol/test/test_strategy.py b/packages/protocol/test/test_strategy.py index 3716f57ec..011d2c1fd 100644 --- a/packages/protocol/test/test_strategy.py +++ b/packages/protocol/test/test_strategy.py @@ -41,12 +41,14 @@ def make_instance(self, include_optional) -> Strategy: description = '', created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), + reference_market = '', configuration = None ) else: return Strategy( id = '', version = '', + reference_market = '', configuration = None, ) """ diff --git a/packages/protocol/test/test_trailing_profile.py b/packages/protocol/test/test_trailing_profile.py index 489030c56..68ec3ac05 100644 --- a/packages/protocol/test/test_trailing_profile.py +++ b/packages/protocol/test/test_trailing_profile.py @@ -36,7 +36,7 @@ def make_instance(self, include_optional) -> TrailingProfile: if include_optional: return TrailingProfile( type = 'filled_take_profit', - details = None + specifics = None ) else: return TrailingProfile( diff --git a/packages/protocol/test/test_user_action_configuration.py b/packages/protocol/test/test_user_action_configuration.py index 1bc2adf80..ad9203d32 100644 --- a/packages/protocol/test/test_user_action_configuration.py +++ b/packages/protocol/test/test_user_action_configuration.py @@ -46,7 +46,13 @@ def make_instance(self, include_optional) -> UserActionConfiguration: message = 'pending_validation', ), created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - details = null, ), + assets = [ + octobot_protocol.models.detailed_asset.DetailedAsset( + symbol = '', + total = 1.337, + available = 1.337, ) + ], + specifics = null, ), id = '', account_ids = [ '' @@ -65,7 +71,13 @@ def make_instance(self, include_optional) -> UserActionConfiguration: message = 'pending_validation', ), created_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), updated_at = datetime.datetime.strptime('2013-10-20 19:20:30.00', '%Y-%m-%d %H:%M:%S.%f'), - details = null, ), + assets = [ + octobot_protocol.models.detailed_asset.DetailedAsset( + symbol = '', + total = 1.337, + available = 1.337, ) + ], + specifics = null, ), id = '', ) """ diff --git a/packages/protocol/test/test_user_data_state.py b/packages/protocol/test/test_user_data_state.py index ea4ca3091..fa8e9332b 100644 --- a/packages/protocol/test/test_user_data_state.py +++ b/packages/protocol/test/test_user_data_state.py @@ -73,12 +73,10 @@ def make_instance(self, include_optional) -> UserDataState: '' ], assets = [ - octobot_protocol.models.asset.Asset( + octobot_protocol.models.detailed_asset.DetailedAsset( symbol = '', total = 1.337, - available = 1.337, - value = 1.337, - unit = '', ) + available = 1.337, ) ], orders = [ octobot_protocol.models.order_summary.OrderSummary( diff --git a/packages/sync/octobot_sync/enums.py b/packages/sync/octobot_sync/enums.py index d26bde468..037bc7495 100644 --- a/packages/sync/octobot_sync/enums.py +++ b/packages/sync/octobot_sync/enums.py @@ -20,6 +20,9 @@ class Collections(enum.StrEnum): USER_DATA = "user-data" USER_ACCOUNTS = "user-accounts" + USER_ACCOUNTS_AUTH = "user-accounts-auth" + USER_ACCOUNTS_TRADING = "user-accounts-trading" + USER_ACCOUNTS_HISTORY = "user-accounts-history" USER_SETTINGS = "user-settings" USER_STRATEGIES = "user-strategies" USER_ACTIONS = "user-actions" diff --git a/packages/sync/octobot_sync/errors.py b/packages/sync/octobot_sync/errors.py index 09431f5d5..8ed7f2aaf 100644 --- a/packages/sync/octobot_sync/errors.py +++ b/packages/sync/octobot_sync/errors.py @@ -51,3 +51,9 @@ class OctobotSyncWalletNotFoundError(OctobotSyncError): """Raised when no wallet exists for the requested address.""" pass + + +class OctobotSyncAccountIdMissingError(OctobotSyncError): + """Raised when account_id is missing from the store context.""" + + pass diff --git a/packages/sync/octobot_sync/server.py b/packages/sync/octobot_sync/server.py index e30a3348f..828611b0d 100644 --- a/packages/sync/octobot_sync/server.py +++ b/packages/sync/octobot_sync/server.py @@ -45,6 +45,8 @@ import octobot_node.protocol.user_actions as user_actions_protocol import octobot_node.protocol.user_data as user_data_protocol import octobot_node.protocol.accounts as accounts_protocol +import octobot_node.protocol.accounts_authentication as accounts_auth_protocol +import octobot_node.protocol.accounts_trading as accounts_trading_protocol _get_data: Callable[[str, StoreContext | None], Awaitable[str | None]] | None = None @@ -63,6 +65,12 @@ def _get_collection(context: StoreContext | None) -> str: raise errors.OctobotSyncCollectionMissingError("Collection is missing from the context") +def _get_account_id(context: StoreContext | None) -> str: + if context and context.params.get("account_id"): + return str(context.params["account_id"]) + raise errors.OctobotSyncAccountIdMissingError("account_id is missing from the context") + + def _get_wallet_private_key(address: str) -> str: try: wallet = community_authentication.CommunityAuthentication.instance().get_wallet(address) @@ -143,6 +151,17 @@ async def get_data(key: str, context: StoreContext | None = None) -> str | None: _get_address(context) ) already_encrypted_payload = json.dumps(encrypted_blob) + case enums.Collections.USER_ACCOUNTS_AUTH.value: + encrypted_blob = accounts_auth_protocol.get_accounts_authentication_state_encrypted( + _get_address(context) + ) + already_encrypted_payload = json.dumps(encrypted_blob) + case enums.Collections.USER_ACCOUNTS_TRADING.value: + encrypted_blob = accounts_trading_protocol.get_account_trading_state_encrypted( + _get_address(context), + _get_account_id(context), + ) + already_encrypted_payload = json.dumps(encrypted_blob) case enums.Collections.USER_ACTIONS.value: # reading user actions should always return an empty list actions_state = protocol_models.UserActionsState( diff --git a/packages/sync/octobot_sync/sync/collection_backend/__init__.py b/packages/sync/octobot_sync/sync/collection_backend/__init__.py index e8484d7a1..7d7f273cd 100644 --- a/packages/sync/octobot_sync/sync/collection_backend/__init__.py +++ b/packages/sync/octobot_sync/sync/collection_backend/__init__.py @@ -15,12 +15,18 @@ # License along with this library. +import octobot_sync.sync.collection_backend.abstract_local_collection_provider as abstract_local_collection_provider_module import octobot_sync.sync.collection_backend.base_local_collection_storage as base_local_collection_storage_module import octobot_sync.sync.collection_backend.base_local_collection_provider as base_local_collection_provider_module +import octobot_sync.sync.collection_backend.single_item_local_collection_storage as single_item_local_collection_storage_module +import octobot_sync.sync.collection_backend.single_item_local_collection_provider as single_item_local_collection_provider_module import octobot_sync.sync.collection_backend.errors as errors_module +AbstractLocalCollectionProvider = abstract_local_collection_provider_module.AbstractLocalCollectionProvider BaseLocalCollectionStorage = base_local_collection_storage_module.BaseLocalCollectionStorage BaseLocalCollectionProvider = base_local_collection_provider_module.BaseLocalCollectionProvider +SingleItemLocalCollectionStorage = single_item_local_collection_storage_module.SingleItemLocalCollectionStorage +SingleItemLocalCollectionProvider = single_item_local_collection_provider_module.SingleItemLocalCollectionProvider CollectionStorageError = errors_module.CollectionStorageError CollectionDecryptionError = errors_module.CollectionDecryptionError CollectionFileFormatError = errors_module.CollectionFileFormatError @@ -28,8 +34,11 @@ DuplicateItemError = errors_module.DuplicateItemError __all__ = [ + "AbstractLocalCollectionProvider", "BaseLocalCollectionStorage", "BaseLocalCollectionProvider", + "SingleItemLocalCollectionStorage", + "SingleItemLocalCollectionProvider", "CollectionStorageError", "CollectionDecryptionError", "CollectionFileFormatError", diff --git a/packages/sync/octobot_sync/sync/collection_backend/abstract_local_collection_provider.py b/packages/sync/octobot_sync/sync/collection_backend/abstract_local_collection_provider.py new file mode 100644 index 000000000..da756c437 --- /dev/null +++ b/packages/sync/octobot_sync/sync/collection_backend/abstract_local_collection_provider.py @@ -0,0 +1,64 @@ +# Drakkar-Software OctoBot-Sync +# Copyright (c) 2025 Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. + + +import abc +import typing + +import octobot.community.authentication as community_authentication +import octobot_sync.sync.collection_backend.base_local_collection_storage as base_storage +import octobot_sync.sync.collection_backend.state_model as state_model + + +S = typing.TypeVar("S", bound=state_model.StateModel) + + +class AbstractLocalCollectionProvider(typing.Generic[S], abc.ABC): + """ + Shared base for local collection providers. + + Subclasses configure ``COLLECTION``, ``STATE_VERSION``, ``STATE_CLASS``, + storage creation, and cache setup. + """ + COLLECTION: str = None # type: ignore + STATE_VERSION: str = None # type: ignore + STATE_CLASS: type[S] = None # type: ignore + + _CACHE_MAXSIZE = 1024 + _CACHE_TTL_SECONDS = 12 * 60 * 60 + + def __init__( + self, + base_folder: typing.Optional[str] = None, + ) -> None: + self._storage = self._create_storage(self.COLLECTION, base_folder) + self._setup_caches() + + @staticmethod + @abc.abstractmethod + def _create_storage( + collection: str, + base_folder: typing.Optional[str] = None, + ) -> base_storage.BaseLocalCollectionStorage: + """Return the storage backend for this provider.""" + + @abc.abstractmethod + def _setup_caches(self) -> None: + """Initialize provider-specific TTL caches.""" + + def _get_wallet_private_key(self, address: str) -> str: + wallet = community_authentication.CommunityAuthentication.instance().get_wallet(address) + return wallet.private_key diff --git a/packages/sync/octobot_sync/sync/collection_backend/base_local_collection_provider.py b/packages/sync/octobot_sync/sync/collection_backend/base_local_collection_provider.py index 3afe9fb5a..0cdb9314e 100644 --- a/packages/sync/octobot_sync/sync/collection_backend/base_local_collection_provider.py +++ b/packages/sync/octobot_sync/sync/collection_backend/base_local_collection_provider.py @@ -20,7 +20,7 @@ import cachetools -import octobot.community.authentication as community_authentication +import octobot_sync.sync.collection_backend.abstract_local_collection_provider as abstract_provider import octobot_sync.sync.collection_backend.base_local_collection_storage as base_storage import octobot_sync.sync.collection_backend.errors as collection_errors import octobot_sync.sync.collection_backend.state_model as state_model @@ -30,7 +30,11 @@ S = typing.TypeVar("S", bound=state_model.StateModel) -class BaseLocalCollectionProvider(typing.Generic[T, S], abc.ABC): +class BaseLocalCollectionProvider( + abstract_provider.AbstractLocalCollectionProvider[S], + typing.Generic[T, S], + abc.ABC, +): """ Generic provider exposing CRUD operations on typed pydantic model items ``T``. @@ -39,21 +43,8 @@ class BaseLocalCollectionProvider(typing.Generic[T, S], abc.ABC): ``ITEMS_KEY``). Subclasses must set the class variables and implement ``_get_item_id``. """ - COLLECTION: str = None # type: ignore - STATE_VERSION: str = None # type: ignore - STATE_CLASS: type[S] = None # type: ignore ITEMS_KEY: str = None # type: ignore - def __init__( - self, - base_folder: typing.Optional[str] = None, - ) -> None: - self._storage = self._create_storage(self.COLLECTION, base_folder) - self._cache: cachetools.TTLCache[str, list[T]] = cachetools.TTLCache( - maxsize=1024, - ttl=12 * 60 * 60, - ) - @staticmethod def _create_storage( collection: str, @@ -64,14 +55,16 @@ def _create_storage( base_folder=base_folder, ) + def _setup_caches(self) -> None: + self._cache: cachetools.TTLCache[str, list[T]] = cachetools.TTLCache( + maxsize=self._CACHE_MAXSIZE, + ttl=self._CACHE_TTL_SECONDS, + ) + @abc.abstractmethod def _get_item_id(self, item: T) -> str: """Return the unique identifier of a model instance.""" - def _get_wallet_private_key(self, address: str) -> str: - wallet = community_authentication.CommunityAuthentication.instance().get_wallet(address) - return wallet.private_key - def _get_cached_items(self, address: str) -> list[T] | None: cached = self._cache.get(address) if cached is None: diff --git a/packages/sync/octobot_sync/sync/collection_backend/base_local_collection_storage.py b/packages/sync/octobot_sync/sync/collection_backend/base_local_collection_storage.py index b3f7f659c..cc5fed37b 100644 --- a/packages/sync/octobot_sync/sync/collection_backend/base_local_collection_storage.py +++ b/packages/sync/octobot_sync/sync/collection_backend/base_local_collection_storage.py @@ -52,10 +52,15 @@ def _sanitize_address(self, address: str) -> str: sanitized = sanitized.replace("..", "_") return sanitized or "unknown" - def _file_path(self, address: str) -> pathlib.Path: - filename = f"{self._sanitize_address(address)}.json" + def _file_path(self, storage_key: str) -> pathlib.Path: + filename = f"{self._sanitize_address(storage_key)}.json" return self._root / filename + def _missing_data_error(self, storage_key: str) -> collection_errors.CollectionNoDataError: + return collection_errors.CollectionNoDataError( + f"{self.collection} file does not exist for address {storage_key}" + ) + def _payload_to_json_bytes(self, payload: state_model.StateModel) -> bytes: """Serialize a state dict to JSON bytes (handles datetime values from protocol models).""" @@ -112,16 +117,14 @@ def _decrypt( ) return decrypted_state - def _read_blob(self, address: str) -> dict[str, typing.Any]: + def _read_blob(self, storage_key: str) -> dict[str, typing.Any]: """Read the raw encrypted blob from disk. Raises ``CollectionNoDataError`` when the backing file does not exist. """ - path = self._file_path(address) + path = self._file_path(storage_key) if not path.exists(): - raise collection_errors.CollectionNoDataError( - f"{self.collection} file does not exist for address {address}" - ) + raise self._missing_data_error(storage_key) with self._lock: with open(path, "r", encoding="utf-8") as handle: raw = json.load(handle) @@ -132,37 +135,37 @@ def _read_blob(self, address: str) -> dict[str, typing.Any]: return raw def load_state( - self, address: str, wallet_private_key: str, state_model: type[state_model.StateModel], + self, storage_key: str, wallet_private_key: str, state_model: type[state_model.StateModel], ) -> state_model.StateModel: """ - Load and decrypt the state dict for a given wallet address. + Load and decrypt the state dict for a given storage key. Raises ``CollectionNoDataError`` when the backing file does not exist. """ - blob = self._read_blob(address) + blob = self._read_blob(storage_key) return self._decrypt(blob, wallet_private_key, state_model) - def load_items_encrypted(self, address: str) -> dict[str, str]: + def load_items_encrypted(self, storage_key: str) -> dict[str, str]: """ - Read the encrypted blob for *address* directly from disk. + Read the encrypted blob for *storage_key* directly from disk. Skips decryption entirely and returns the raw ``{"iv": ..., "data": ...}``-style dict as persisted. - Raises ``CollectionNoDataError`` when no file exists for the address. + Raises ``CollectionNoDataError`` when no file exists for the storage key. """ - return typing.cast(dict[str, str], self._read_blob(address)) + return typing.cast(dict[str, str], self._read_blob(storage_key)) def save_state( self, - address: str, + storage_key: str, wallet_private_key: str, state: state_model.StateModel, ) -> None: """ - Encrypt and atomically persist the state dict for a given wallet address. + Encrypt and atomically persist the state dict for a given storage key. """ - path = self._file_path(address) + path = self._file_path(storage_key) path.parent.mkdir(parents=True, exist_ok=True) blob = self._encrypt(state, wallet_private_key) tmp_path = path.with_suffix(".tmp") diff --git a/packages/sync/octobot_sync/sync/collection_backend/single_item_local_collection_provider.py b/packages/sync/octobot_sync/sync/collection_backend/single_item_local_collection_provider.py new file mode 100644 index 000000000..8e6f0db16 --- /dev/null +++ b/packages/sync/octobot_sync/sync/collection_backend/single_item_local_collection_provider.py @@ -0,0 +1,88 @@ +# Drakkar-Software OctoBot-Sync +# Copyright (c) 2025 Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. + + +import typing + +import cachetools + +import octobot_sync.sync.collection_backend.abstract_local_collection_provider as abstract_provider +import octobot_sync.sync.collection_backend.base_local_collection_storage as base_storage +import octobot_sync.sync.collection_backend.single_item_local_collection_storage as single_item_storage +import octobot_sync.sync.collection_backend.state_model as state_model + + +S = typing.TypeVar("S", bound=state_model.StateModel) + + +class SingleItemLocalCollectionProvider(abstract_provider.AbstractLocalCollectionProvider[S]): + """ + Provider for collections with one encrypted state file per identifier. + + Exposes whole-state load/save only (no list CRUD). Identifiers are typically + ``/`` under the collection root. + """ + + @staticmethod + def _create_storage( + collection: str, + base_folder: typing.Optional[str] = None, + ) -> base_storage.BaseLocalCollectionStorage: + return single_item_storage.SingleItemLocalCollectionStorage( + collection=collection, + base_folder=base_folder, + ) + + def _setup_caches(self) -> None: + self._state_cache: cachetools.TTLCache[tuple[str, str], S] = cachetools.TTLCache( + maxsize=self._CACHE_MAXSIZE, + ttl=self._CACHE_TTL_SECONDS, + ) + + def _build_identifier(self, address: str, account_id: str) -> str: + return ( + f"{self._storage._sanitize_address(address)}/{self._storage._sanitize_address(account_id)}" + ) + + def _get_cached_state(self, address: str, account_id: str) -> S | None: + return self._state_cache.get((address, account_id)) + + def _set_cached_state(self, address: str, account_id: str, state: S) -> None: + self._state_cache[(address, account_id)] = state + + def load_state(self, address: str, account_id: str) -> S: + cached_state = self._get_cached_state(address, account_id) + if cached_state is not None: + return cached_state + identifier = self._build_identifier(address, account_id) + wallet_private_key = self._get_wallet_private_key(address) + persisted_state = self._storage.load_state( + identifier, + wallet_private_key, + self.STATE_CLASS, + ) + self._set_cached_state(address, account_id, persisted_state) + return persisted_state + + def save_state(self, address: str, account_id: str, state: S) -> None: + identifier = self._build_identifier(address, account_id) + wallet_private_key = self._get_wallet_private_key(address) + self._storage.save_state(identifier, wallet_private_key, state) + self._set_cached_state(address, account_id, state) + + def load_state_encrypted(self, address: str, account_id: str) -> dict[str, str]: + identifier = self._build_identifier(address, account_id) + return self._storage.load_items_encrypted(identifier) diff --git a/packages/sync/octobot_sync/sync/collection_backend/single_item_local_collection_storage.py b/packages/sync/octobot_sync/sync/collection_backend/single_item_local_collection_storage.py new file mode 100644 index 000000000..97ba9d6e1 --- /dev/null +++ b/packages/sync/octobot_sync/sync/collection_backend/single_item_local_collection_storage.py @@ -0,0 +1,51 @@ +# Drakkar-Software OctoBot-Sync +# Copyright (c) 2025 Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. + + +import pathlib + +import octobot_sync.sync.collection_backend.base_local_collection_storage as base_local_collection_storage +import octobot_sync.sync.collection_backend.errors as collection_errors + + +class SingleItemLocalCollectionStorage(base_local_collection_storage.BaseLocalCollectionStorage): + """ + Encrypted collection storage keyed by a composite identifier. + + State is persisted at ``//.json`` where + *identifier* is typically ``/`` with each segment + sanitized separately (nested directories under the collection root). + """ + + def _file_path(self, identifier: str) -> pathlib.Path: + path_parts = [ + self._sanitize_address(part) + for part in identifier.split("/") + if part + ] + if not path_parts: + path_parts = ["unknown"] + directory_parts = path_parts[:-1] + filename = f"{path_parts[-1]}.json" + path = self._root + for directory_part in directory_parts: + path = path / directory_part + return path / filename + + def _missing_data_error(self, identifier: str) -> collection_errors.CollectionNoDataError: + return collection_errors.CollectionNoDataError( + f"{self.collection} file does not exist for identifier {identifier}" + ) diff --git a/packages/sync/octobot_sync/sync/collection_providers/__init__.py b/packages/sync/octobot_sync/sync/collection_providers/__init__.py index e59205c48..3a70e50ea 100644 --- a/packages/sync/octobot_sync/sync/collection_providers/__init__.py +++ b/packages/sync/octobot_sync/sync/collection_providers/__init__.py @@ -15,13 +15,21 @@ # License along with this library. +import octobot_sync.sync.collection_providers.user_account_authentication_provider as user_account_authentication_provider_module import octobot_sync.sync.collection_providers.user_account_provider as user_account_provider_module +import octobot_sync.sync.collection_providers.user_account_trading_provider as user_account_trading_provider_module import octobot_sync.sync.collection_providers.user_strategy_provider as user_strategy_provider_module AccountProvider = user_account_provider_module.AccountProvider +AccountAuthenticationProvider = ( + user_account_authentication_provider_module.AccountAuthenticationProvider +) +AccountTradingProvider = user_account_trading_provider_module.AccountTradingProvider StrategyProvider = user_strategy_provider_module.StrategyProvider __all__ = [ "AccountProvider", + "AccountAuthenticationProvider", + "AccountTradingProvider", "StrategyProvider", ] diff --git a/packages/sync/octobot_sync/sync/collection_providers/user_account_authentication_provider.py b/packages/sync/octobot_sync/sync/collection_providers/user_account_authentication_provider.py new file mode 100644 index 000000000..4dfa031b4 --- /dev/null +++ b/packages/sync/octobot_sync/sync/collection_providers/user_account_authentication_provider.py @@ -0,0 +1,55 @@ +# Drakkar-Software OctoBot-Sync +# Copyright (c) 2025 Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. + + +import octobot_commons.singleton.singleton_class as singleton_class +import octobot_node.constants as node_constants +import octobot_protocol.models as protocol_models +import octobot_sync.enums as sync_enums + +import octobot_sync.sync.collection_backend.base_local_collection_provider as base_provider +import octobot_sync.sync.collection_backend.errors as collection_errors + + +class AccountAuthenticationProvider( + base_provider.BaseLocalCollectionProvider[ + protocol_models.AccountAuthentication, + protocol_models.AccountsAuthenticationState, + ], + singleton_class.Singleton, +): + """ + Singleton provider exposing CRUD operations on AccountAuthentication models. + + Authentication credentials are grouped per wallet address and persisted as an encrypted + AccountsAuthenticationState envelope. All CRUD logic lives in the base class. + """ + COLLECTION = sync_enums.Collections.USER_ACCOUNTS_AUTH.value + STATE_VERSION = node_constants.USER_ACCOUNTS_AUTH_STATE_VERSION + STATE_CLASS = protocol_models.AccountsAuthenticationState + ITEMS_KEY = "account_authentication" + + def _get_item_id(self, item: protocol_models.AccountAuthentication) -> str: + # Wallet-scoped auth file holds a single credentials entry (no account_id on model). + return "wallet" + + def get_item(self, address: str, item_id: str) -> protocol_models.AccountAuthentication: + items = self.list_items(address) + if not items: + raise collection_errors.ItemNotFoundError( + f"Authentication not found for address {address}" + ) + return items[0] diff --git a/packages/sync/octobot_sync/sync/collection_providers/user_account_trading_provider.py b/packages/sync/octobot_sync/sync/collection_providers/user_account_trading_provider.py new file mode 100644 index 000000000..b70d8dbf4 --- /dev/null +++ b/packages/sync/octobot_sync/sync/collection_providers/user_account_trading_provider.py @@ -0,0 +1,38 @@ +# Drakkar-Software OctoBot-Sync +# Copyright (c) 2025 Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. + + +import octobot_commons.singleton.singleton_class as singleton_class +import octobot_node.constants as node_constants +import octobot_protocol.models as protocol_models +import octobot_sync.enums as sync_enums + +import octobot_sync.sync.collection_backend.single_item_local_collection_provider as single_item_provider + + +class AccountTradingProvider( + single_item_provider.SingleItemLocalCollectionProvider[protocol_models.AccountTradingState], + singleton_class.Singleton, +): + """ + Singleton provider for per-account trading state. + + Each account is stored in its own encrypted file under + ``//.json``. + """ + COLLECTION = sync_enums.Collections.USER_ACCOUNTS_TRADING.value + STATE_VERSION = node_constants.USER_ACCOUNTS_TRADING_STATE_VERSION + STATE_CLASS = protocol_models.AccountTradingState diff --git a/packages/sync/octobot_sync/sync/collections.py b/packages/sync/octobot_sync/sync/collections.py index 58bbb23e1..1b4ac573c 100644 --- a/packages/sync/octobot_sync/sync/collections.py +++ b/packages/sync/octobot_sync/sync/collections.py @@ -49,6 +49,30 @@ encryption="delegated", maxBodyBytes=constants.MAX_BODY_SIZE_PRIVATE, ), + CollectionConfig( + name=enums.Collections.USER_ACCOUNTS_AUTH.value, + storagePath="users/{identity}/accounts/auth", + readRoles=["self"], + writeRoles=["self"], + encryption="delegated", + maxBodyBytes=constants.MAX_BODY_SIZE_PRIVATE, + ), + CollectionConfig( + name=enums.Collections.USER_ACCOUNTS_TRADING.value, + storagePath="users/{identity}/accounts/{account_id}/trading", + readRoles=["self"], + writeRoles=["self"], + encryption="delegated", + maxBodyBytes=constants.MAX_BODY_SIZE_PRIVATE, + ), + CollectionConfig( + name=enums.Collections.USER_ACCOUNTS_HISTORY.value, + storagePath="users/{identity}/accounts/{account_id}/history", + readRoles=["self"], + writeRoles=["self"], + encryption="delegated", + maxBodyBytes=constants.MAX_BODY_SIZE_PRIVATE, + ), CollectionConfig( name=enums.Collections.USER_SETTINGS.value, storagePath="users/{identity}/settings", diff --git a/packages/sync/tests/sync/collection_backend/test_abstract_local_collection_provider.py b/packages/sync/tests/sync/collection_backend/test_abstract_local_collection_provider.py new file mode 100644 index 000000000..665bea8a6 --- /dev/null +++ b/packages/sync/tests/sync/collection_backend/test_abstract_local_collection_provider.py @@ -0,0 +1,96 @@ +# Drakkar-Software OctoBot-Sync +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. + +import typing + +import cachetools +import mock +import pydantic + +import octobot.community.authentication as community_authentication +import octobot_sync.sync.collection_backend.abstract_local_collection_provider as abstract_provider_module +import octobot_sync.sync.collection_backend.base_local_collection_storage as base_storage_module +import octobot_sync.sync.collection_backend.single_item_local_collection_storage as single_item_storage_module + +_TEST_ADDRESS = "0xaaabbbcccddd" +_TEST_PRIVATE_KEY = "private-key" + + +class _TestState(pydantic.BaseModel): + version: str + + +class _TestAbstractProvider(abstract_provider_module.AbstractLocalCollectionProvider[_TestState]): + COLLECTION = "test-abstract" + STATE_VERSION = "1.0.0" + STATE_CLASS = _TestState + + @staticmethod + def _create_storage( + collection: str, + base_folder: typing.Optional[str] = None, + ) -> base_storage_module.BaseLocalCollectionStorage: + return single_item_storage_module.SingleItemLocalCollectionStorage( + collection=collection, + base_folder=base_folder, + ) + + def _setup_caches(self) -> None: + self._state_cache: cachetools.TTLCache[tuple[str, str], _TestState] = cachetools.TTLCache( + maxsize=self._CACHE_MAXSIZE, + ttl=self._CACHE_TTL_SECONDS, + ) + + +def _make_provider(tmp_path): + return _TestAbstractProvider(base_folder=str(tmp_path)) + + +def _patch_wallet(private_key: str = _TEST_PRIVATE_KEY): + wallet = mock.Mock() + wallet.private_key = private_key + auth = mock.Mock() + auth.get_wallet.return_value = wallet + return mock.patch.object( + community_authentication.CommunityAuthentication, + "instance", + return_value=auth, + ) + + +class TestAbstractLocalCollectionProviderInit: + def test_creates_storage_via_create_storage(self, tmp_path): + provider = _make_provider(tmp_path) + + assert isinstance(provider._storage, single_item_storage_module.SingleItemLocalCollectionStorage) + assert provider._storage.collection == "test-abstract" + assert provider._storage._root == tmp_path / "test-abstract" + + def test_calls_setup_caches(self, tmp_path): + provider = _make_provider(tmp_path) + + assert hasattr(provider, "_state_cache") + assert isinstance(provider._state_cache, cachetools.TTLCache) + + +class TestAbstractLocalCollectionProviderGetWalletPrivateKey: + def test_returns_wallet_private_key_from_community_authentication(self, tmp_path): + provider = _make_provider(tmp_path) + + with _patch_wallet("expected-private-key"): + private_key = provider._get_wallet_private_key(_TEST_ADDRESS) + + assert private_key == "expected-private-key" diff --git a/packages/sync/tests/sync/collection_backend/test_single_item_local_collection_provider.py b/packages/sync/tests/sync/collection_backend/test_single_item_local_collection_provider.py new file mode 100644 index 000000000..8b17fb691 --- /dev/null +++ b/packages/sync/tests/sync/collection_backend/test_single_item_local_collection_provider.py @@ -0,0 +1,144 @@ +# Drakkar-Software OctoBot-Sync +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. + +import typing + +import mock +import pydantic + +import octobot_sync.constants as sync_constants +import octobot.community.authentication as community_authentication +import octobot_sync.sync.collection_backend.single_item_local_collection_provider as single_item_provider_module +import octobot_sync.sync.collection_backend.single_item_local_collection_storage as single_item_storage_module + +_TEST_ADDRESS = "0xaaabbbcccddd" +_TEST_ACCOUNT_ID = "acc-1" +_TEST_PRIVATE_KEY = "private-key" + + +class _TestItem(pydantic.BaseModel): + id: str + label: typing.Optional[str] = None + + +class _TestState(pydantic.BaseModel): + version: str + items: typing.Optional[list[_TestItem]] = None + + def to_json(self) -> str: + return self.model_dump_json() + + @classmethod + def from_json(cls, json_str: str) -> typing.Optional["_TestState"]: + return cls.model_validate_json(json_str) + + +class _TestSingleItemProvider( + single_item_provider_module.SingleItemLocalCollectionProvider[_TestState] +): + COLLECTION = "test-single-item" + STATE_VERSION = "1.0.0" + STATE_CLASS = _TestState + + +_SAMPLE_STATE = _TestState( + version="1.0.0", + items=[_TestItem(id="item-1", label="First")], +) + + +def _make_provider(tmp_path): + return _TestSingleItemProvider(base_folder=str(tmp_path)) + + +def _patch_wallet(private_key: str = _TEST_PRIVATE_KEY): + wallet = mock.Mock() + wallet.private_key = private_key + auth = mock.Mock() + auth.get_wallet.return_value = wallet + return mock.patch.object( + community_authentication.CommunityAuthentication, + "instance", + return_value=auth, + ) + + +class TestSingleItemLocalCollectionProviderCreateStorage: + def test_storage_is_single_item_local_collection_storage(self, tmp_path): + provider = _make_provider(tmp_path) + assert isinstance(provider._storage, single_item_storage_module.SingleItemLocalCollectionStorage) + + +class TestSingleItemLocalCollectionProviderBuildIdentifier: + def test_joins_sanitized_wallet_and_account_id(self, tmp_path): + provider = _make_provider(tmp_path) + unsafe_address = "../0xwallet" + unsafe_account_id = "..\\acc-1" + + identifier = provider._build_identifier(unsafe_address, unsafe_account_id) + + expected_identifier = ( + f"{provider._storage._sanitize_address(unsafe_address)}/" + f"{provider._storage._sanitize_address(unsafe_account_id)}" + ) + assert identifier == expected_identifier + assert ".." not in identifier + + +class TestSingleItemLocalCollectionProviderLoadState: + def test_returns_cached_state_without_second_disk_read(self, tmp_path): + provider = _make_provider(tmp_path) + with _patch_wallet(): + provider.save_state(_TEST_ADDRESS, _TEST_ACCOUNT_ID, _SAMPLE_STATE) + + with mock.patch.object( + provider._storage, + "load_state", + wraps=provider._storage.load_state, + ) as load_state_mock: + with _patch_wallet(): + first_load = provider.load_state(_TEST_ADDRESS, _TEST_ACCOUNT_ID) + second_load = provider.load_state(_TEST_ADDRESS, _TEST_ACCOUNT_ID) + + assert first_load == _SAMPLE_STATE + assert second_load == _SAMPLE_STATE + load_state_mock.assert_not_called() + + +class TestSingleItemLocalCollectionProviderSaveState: + def test_persists_and_updates_cache(self, tmp_path): + provider = _make_provider(tmp_path) + with _patch_wallet(): + provider.save_state(_TEST_ADDRESS, _TEST_ACCOUNT_ID, _SAMPLE_STATE) + + cached_state = provider._get_cached_state(_TEST_ADDRESS, _TEST_ACCOUNT_ID) + assert cached_state == _SAMPLE_STATE + + identifier = provider._build_identifier(_TEST_ADDRESS, _TEST_ACCOUNT_ID) + persisted_state = provider._storage.load_state(identifier, _TEST_PRIVATE_KEY, _TestState) + assert persisted_state == _SAMPLE_STATE + + +class TestSingleItemLocalCollectionProviderLoadStateEncrypted: + def test_reads_encrypted_blob_for_account_id(self, tmp_path): + provider = _make_provider(tmp_path) + with _patch_wallet(): + provider.save_state(_TEST_ADDRESS, _TEST_ACCOUNT_ID, _SAMPLE_STATE) + + blob = provider.load_state_encrypted(_TEST_ADDRESS, _TEST_ACCOUNT_ID) + + assert set(blob.keys()) == {sync_constants.BLOB_IV_KEY, sync_constants.BLOB_DATA_KEY} + assert "First" not in blob[sync_constants.BLOB_DATA_KEY] diff --git a/packages/sync/tests/sync/collection_backend/test_single_item_local_collection_storage.py b/packages/sync/tests/sync/collection_backend/test_single_item_local_collection_storage.py new file mode 100644 index 000000000..9c3f8a395 --- /dev/null +++ b/packages/sync/tests/sync/collection_backend/test_single_item_local_collection_storage.py @@ -0,0 +1,137 @@ +# Drakkar-Software OctoBot-Sync +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. + +import typing + +import pydantic +import pytest + +import octobot_sync.constants as sync_constants +import octobot_sync.sync.collection_backend.errors as collection_errors +import octobot_sync.sync.collection_backend.single_item_local_collection_storage as single_item_storage_module + +_TEST_WALLET = "0xaaabbbcccddd" +_TEST_ACCOUNT_ID = "acc-1" +_TEST_PRIVATE_KEY = "private-key" + + +class _TestItem(pydantic.BaseModel): + id: str + label: typing.Optional[str] = None + + +class _TestState(pydantic.BaseModel): + version: str + items: typing.Optional[list[_TestItem]] = None + + def to_json(self) -> str: + return self.model_dump_json() + + @classmethod + def from_json(cls, json_str: str) -> typing.Optional["_TestState"]: + return cls.model_validate_json(json_str) + + +_SAMPLE_STATE = _TestState( + version="1.0.0", + items=[_TestItem(id="item-1", label="First")], +) + + +def _make_storage(tmp_path, collection="test-single-item"): + return single_item_storage_module.SingleItemLocalCollectionStorage( + collection=collection, + base_folder=str(tmp_path), + ) + + +def _identifier(wallet: str = _TEST_WALLET, account_id: str = _TEST_ACCOUNT_ID) -> str: + return f"{wallet}/{account_id}" + + +class TestSingleItemLocalCollectionStorageFilePath: + def test_nested_path_for_wallet_and_account_id(self, tmp_path): + storage = _make_storage(tmp_path) + identifier = _identifier("0xwallet", "acc-1") + + path = storage._file_path(identifier) + + assert path == tmp_path / "test-single-item" / "0xwallet" / "acc-1.json" + + def test_sanitizes_path_segments(self, tmp_path): + storage = _make_storage(tmp_path) + unsafe_wallet = "../0xwallet" + unsafe_account_id = "..\\acc-1" + identifier = f"{unsafe_wallet}/{unsafe_account_id}" + + path = storage._file_path(identifier) + + sanitized_parts = [ + storage._sanitize_address(part) + for part in identifier.split("/") + if part + ] + expected_path = tmp_path / "test-single-item" + for directory_part in sanitized_parts[:-1]: + expected_path = expected_path / directory_part + expected_path = expected_path / f"{sanitized_parts[-1]}.json" + + assert path == expected_path + assert ".." not in path.as_posix() + + +class TestSingleItemLocalCollectionStorageLoadState: + def test_raises_no_data_when_file_absent(self, tmp_path): + storage = _make_storage(tmp_path) + identifier = _identifier() + + with pytest.raises(collection_errors.CollectionNoDataError, match="identifier"): + storage.load_state(identifier, _TEST_PRIVATE_KEY, _TestState) + + +class TestSingleItemLocalCollectionStorageSaveState: + def test_round_trip_encrypted_with_composite_identifier(self, tmp_path): + storage = _make_storage(tmp_path) + identifier = _identifier() + + storage.save_state(identifier, _TEST_PRIVATE_KEY, _SAMPLE_STATE) + loaded = storage.load_state(identifier, _TEST_PRIVATE_KEY, _TestState) + + assert loaded == _SAMPLE_STATE + + def test_separate_identifiers_use_separate_files(self, tmp_path): + storage = _make_storage(tmp_path) + first_identifier = _identifier(_TEST_WALLET, "acc-1") + second_identifier = _identifier(_TEST_WALLET, "acc-2") + first_state = _TestState(version="1.0.0", items=[_TestItem(id="first")]) + second_state = _TestState(version="1.0.0", items=[_TestItem(id="second")]) + + storage.save_state(first_identifier, _TEST_PRIVATE_KEY, first_state) + storage.save_state(second_identifier, _TEST_PRIVATE_KEY, second_state) + + assert storage.load_state(first_identifier, _TEST_PRIVATE_KEY, _TestState) == first_state + assert storage.load_state(second_identifier, _TEST_PRIVATE_KEY, _TestState) == second_state + + +class TestSingleItemLocalCollectionStorageLoadItemsEncrypted: + def test_returns_iv_and_data_keys_after_save(self, tmp_path): + storage = _make_storage(tmp_path) + identifier = _identifier() + + storage.save_state(identifier, _TEST_PRIVATE_KEY, _SAMPLE_STATE) + blob = storage.load_items_encrypted(identifier) + + assert set(blob.keys()) == {sync_constants.BLOB_IV_KEY, sync_constants.BLOB_DATA_KEY} diff --git a/packages/sync/tests/sync/collection_providers/test_account_authentication_backend.py b/packages/sync/tests/sync/collection_providers/test_account_authentication_backend.py new file mode 100644 index 000000000..47dddae46 --- /dev/null +++ b/packages/sync/tests/sync/collection_providers/test_account_authentication_backend.py @@ -0,0 +1,66 @@ +# Drakkar-Software OctoBot-Sync +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. + +import datetime + +import octobot_sync.sync.collection_backend.base_local_collection_storage as base_storage_module +import octobot_sync.sync.collection_providers.user_account_authentication_provider as auth_provider_module +import octobot_node.constants as node_constants +import octobot_protocol.models as protocol_models +import octobot_sync.enums as sync_enums + + +class TestAccountAuthenticationProviderCollection: + def test_collection_is_user_accounts_auth(self): + assert ( + auth_provider_module.AccountAuthenticationProvider.COLLECTION + == sync_enums.Collections.USER_ACCOUNTS_AUTH.value + ) + + def test_storage_collection_matches(self, tmp_path): + provider = auth_provider_module.AccountAuthenticationProvider(base_folder=str(tmp_path)) + assert provider._storage.collection == sync_enums.Collections.USER_ACCOUNTS_AUTH.value + + def test_storage_is_base_local_collection_storage(self, tmp_path): + provider = auth_provider_module.AccountAuthenticationProvider(base_folder=str(tmp_path)) + assert isinstance(provider._storage, base_storage_module.BaseLocalCollectionStorage) + + +class TestAccountAuthenticationProviderStateFormat: + def test_state_version_matches_constant(self): + assert ( + auth_provider_module.AccountAuthenticationProvider.STATE_VERSION + == node_constants.USER_ACCOUNTS_AUTH_STATE_VERSION + ) + + def test_state_class_is_accounts_authentication_state(self): + assert ( + auth_provider_module.AccountAuthenticationProvider.STATE_CLASS + is protocol_models.AccountsAuthenticationState + ) + + def test_items_key_is_account_authentication(self): + assert auth_provider_module.AccountAuthenticationProvider.ITEMS_KEY == "account_authentication" + + +class TestAccountAuthenticationProviderGetItemId: + def test_returns_wallet_scoped_item_id(self, tmp_path): + provider = auth_provider_module.AccountAuthenticationProvider(base_folder=str(tmp_path)) + authentication = protocol_models.AccountAuthentication( + api_key="key", + api_secret="secret", + ) + assert provider._get_item_id(authentication) == "wallet" diff --git a/packages/sync/tests/sync/collection_providers/test_account_trading_backend.py b/packages/sync/tests/sync/collection_providers/test_account_trading_backend.py new file mode 100644 index 000000000..17cb4bc9f --- /dev/null +++ b/packages/sync/tests/sync/collection_providers/test_account_trading_backend.py @@ -0,0 +1,144 @@ +# Drakkar-Software OctoBot-Sync +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. + +import datetime + +import mock +import octobot.community.authentication as community_authentication +import octobot_sync.sync.collection_backend.single_item_local_collection_storage as single_item_storage_module +import octobot_sync.sync.collection_providers.user_account_trading_provider as trading_provider_module +import octobot_node.constants as node_constants +import octobot_protocol.models as protocol_models +import octobot_sync.enums as sync_enums + +_TEST_ADDRESS = "0xaaabbbcccddd" +_TEST_ACCOUNT_ID = "acc-42" +_TEST_PRIVATE_KEY = "private-key" + + +def _patch_wallet(private_key: str = _TEST_PRIVATE_KEY): + wallet = mock.Mock() + wallet.private_key = private_key + auth = mock.Mock() + auth.get_wallet.return_value = wallet + return mock.patch.object( + community_authentication.CommunityAuthentication, + "instance", + return_value=auth, + ) + + +class TestAccountTradingProviderCollection: + def test_collection_is_USER_ACCOUNTS_TRADING(self): + assert ( + trading_provider_module.AccountTradingProvider.COLLECTION + == sync_enums.Collections.USER_ACCOUNTS_TRADING.value + ) + + def test_storage_collection_matches(self, tmp_path): + provider = trading_provider_module.AccountTradingProvider(base_folder=str(tmp_path)) + assert provider._storage.collection == sync_enums.Collections.USER_ACCOUNTS_TRADING.value + + def test_storage_is_single_item_local_collection_storage(self, tmp_path): + provider = trading_provider_module.AccountTradingProvider(base_folder=str(tmp_path)) + assert isinstance(provider._storage, single_item_storage_module.SingleItemLocalCollectionStorage) + + +class TestAccountTradingProviderStateFormat: + def test_state_version_matches_constant(self): + assert ( + trading_provider_module.AccountTradingProvider.STATE_VERSION + == node_constants.USER_ACCOUNTS_TRADING_STATE_VERSION + ) + + def test_state_class_is_account_trading_state(self): + assert ( + trading_provider_module.AccountTradingProvider.STATE_CLASS + is protocol_models.AccountTradingState + ) + + +class TestAccountTradingProviderLoadSaveState: + def test_save_and_load_state_per_account_id(self, tmp_path): + provider = trading_provider_module.AccountTradingProvider(base_folder=str(tmp_path)) + fixture_time = datetime.datetime(2026, 1, 15, tzinfo=datetime.UTC) + account_trading = protocol_models.AccountTrading( + updated_at=fixture_time, + ) + trading_state = protocol_models.AccountTradingState( + version=node_constants.USER_ACCOUNTS_TRADING_STATE_VERSION, + account_trading=[account_trading], + ) + updated_time = datetime.datetime(2026, 1, 16, tzinfo=datetime.UTC) + updated_account_trading = protocol_models.AccountTrading( + updated_at=updated_time, + ) + updated_state = protocol_models.AccountTradingState( + version=node_constants.USER_ACCOUNTS_TRADING_STATE_VERSION, + account_trading=[updated_account_trading], + ) + with _patch_wallet(): + provider.save_state(_TEST_ADDRESS, _TEST_ACCOUNT_ID, trading_state) + loaded_state = provider.load_state(_TEST_ADDRESS, _TEST_ACCOUNT_ID) + provider.save_state(_TEST_ADDRESS, _TEST_ACCOUNT_ID, updated_state) + reloaded_state = provider.load_state(_TEST_ADDRESS, _TEST_ACCOUNT_ID) + assert loaded_state.account_trading[0].updated_at == fixture_time + assert reloaded_state.account_trading[0].updated_at == updated_time + + def test_load_state_encrypted_reads_persisted_blob(self, tmp_path): + provider = trading_provider_module.AccountTradingProvider(base_folder=str(tmp_path)) + fixture_time = datetime.datetime(2026, 1, 15, tzinfo=datetime.UTC) + trading_state = protocol_models.AccountTradingState( + version=node_constants.USER_ACCOUNTS_TRADING_STATE_VERSION, + account_trading=[ + protocol_models.AccountTrading( + updated_at=fixture_time, + ) + ], + ) + with _patch_wallet(): + provider.save_state(_TEST_ADDRESS, _TEST_ACCOUNT_ID, trading_state) + encrypted_blob = provider.load_state_encrypted(_TEST_ADDRESS, _TEST_ACCOUNT_ID) + assert "iv" in encrypted_blob + assert "data" in encrypted_blob + + def test_accounts_use_separate_files(self, tmp_path): + provider = trading_provider_module.AccountTradingProvider(base_folder=str(tmp_path)) + first_time = datetime.datetime(2026, 1, 15, tzinfo=datetime.UTC) + second_time = datetime.datetime(2026, 1, 16, tzinfo=datetime.UTC) + first_state = protocol_models.AccountTradingState( + version=node_constants.USER_ACCOUNTS_TRADING_STATE_VERSION, + account_trading=[ + protocol_models.AccountTrading( + updated_at=first_time, + ) + ], + ) + second_state = protocol_models.AccountTradingState( + version=node_constants.USER_ACCOUNTS_TRADING_STATE_VERSION, + account_trading=[ + protocol_models.AccountTrading( + updated_at=second_time, + ) + ], + ) + with _patch_wallet(): + provider.save_state(_TEST_ADDRESS, "acc-1", first_state) + provider.save_state(_TEST_ADDRESS, "acc-2", second_state) + loaded_first = provider.load_state(_TEST_ADDRESS, "acc-1") + loaded_second = provider.load_state(_TEST_ADDRESS, "acc-2") + assert loaded_first == first_state + assert loaded_second == second_state diff --git a/packages/sync/tests/sync/collection_providers/test_strategy_backend.py b/packages/sync/tests/sync/collection_providers/test_strategy_backend.py index 35afbba77..f961f0a4a 100644 --- a/packages/sync/tests/sync/collection_providers/test_strategy_backend.py +++ b/packages/sync/tests/sync/collection_providers/test_strategy_backend.py @@ -59,6 +59,7 @@ def test_returns_strategy_id(self, tmp_path): id="strat-42", version="1.0.0", name="Test strategy", + reference_market="USDT", created_at=fixture_time, updated_at=fixture_time, configuration=protocol_models.StrategyConfiguration(configuration), diff --git a/packages/sync/tests/sync/test_collections.py b/packages/sync/tests/sync/test_collections.py new file mode 100644 index 000000000..bf8595ea2 --- /dev/null +++ b/packages/sync/tests/sync/test_collections.py @@ -0,0 +1,49 @@ +# Drakkar-Software OctoBot-Sync +# Copyright (c) Drakkar-Software, All rights reserved. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 3.0 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. + +import octobot_sync.constants as sync_constants +import octobot_sync.sync.collections as collections_module +import starfish_server.config.schema as schema_module + + +def _make_collection_config(storage_path: str) -> schema_module.CollectionConfig: + return schema_module.CollectionConfig( + name="test-collection", + storagePath=storage_path, + readRoles=["self"], + writeRoles=["self"], + encryption="delegated", + maxBodyBytes=sync_constants.MAX_BODY_SIZE_PRIVATE, + ) + + +class TestIsReplicableCollection: + def test_true_when_storage_path_has_no_placeholders(self): + collection_config = _make_collection_config("users/data") + + assert collections_module.is_replicable_collection(collection_config) is True + + def test_false_when_storage_path_contains_identity_placeholder(self): + collection_config = _make_collection_config("users/{identity}/data") + + assert collections_module.is_replicable_collection(collection_config) is False + + def test_false_when_storage_path_contains_account_id_placeholder(self): + collection_config = _make_collection_config( + "users/{identity}/accounts/{account_id}/trading" + ) + + assert collections_module.is_replicable_collection(collection_config) is False diff --git a/packages/sync/tests/test_server.py b/packages/sync/tests/test_server.py index 0a8a83b8f..6cd0e2fd8 100644 --- a/packages/sync/tests/test_server.py +++ b/packages/sync/tests/test_server.py @@ -23,10 +23,11 @@ def _make_context( identity: str | None = "0xabc", action: str = "pull", collection: str = "test", + params: dict | None = None, ) -> StoreContext: return StoreContext( collection=collection, - params={}, + params=params or {}, identity=identity, roles=(), action=action, @@ -125,6 +126,53 @@ async def test_user_accounts_collection(self): ).decode("utf-8") assert decrypted_plain == expected_plain + @pytest.mark.asyncio + async def test_user_accounts_auth_collection(self): + expected_plain = json.dumps({"version": "1", "account_authentication": []}) + encrypted_blob = sync_crypto.encrypt_bytes_to_blob_dict( + expected_plain.encode("utf-8"), + _TEST_WALLET_PRIVATE_KEY, + enums.Collections.USER_ACCOUNTS_AUTH.value, + ) + encrypted_blob_json = json.dumps(encrypted_blob) + context = _make_context( + identity="0xwallet", + collection=enums.Collections.USER_ACCOUNTS_AUTH.value, + ) + with mock.patch("octobot_sync.server.accounts_auth_protocol") as mock_proto: + mock_proto.get_accounts_authentication_state_encrypted = mock.Mock( + return_value=encrypted_blob + ) + result = await server.get_data("users/0xwallet/accounts/auth", context) + mock_proto.get_accounts_authentication_state_encrypted.assert_called_once_with("0xwallet") + wrapper = json.loads(result) + assert wrapper["hash"] == sync_crypto.sha256_hex(encrypted_blob_json) + assert wrapper["data"] == encrypted_blob_json + + @pytest.mark.asyncio + async def test_USER_ACCOUNTS_TRADING_collection(self): + expected_plain = json.dumps({"version": "1", "account_trading": []}) + encrypted_blob = sync_crypto.encrypt_bytes_to_blob_dict( + expected_plain.encode("utf-8"), + _TEST_WALLET_PRIVATE_KEY, + enums.Collections.USER_ACCOUNTS_TRADING.value, + ) + encrypted_blob_json = json.dumps(encrypted_blob) + context = _make_context( + identity="0xwallet", + collection=enums.Collections.USER_ACCOUNTS_TRADING.value, + params={"account_id": "acc-1"}, + ) + with mock.patch("octobot_sync.server.accounts_trading_protocol") as mock_proto: + mock_proto.get_account_trading_state_encrypted = mock.Mock( + return_value=encrypted_blob + ) + result = await server.get_data("users/0xwallet/accounts/acc-1/trading", context) + mock_proto.get_account_trading_state_encrypted.assert_called_once_with("0xwallet", "acc-1") + wrapper = json.loads(result) + assert wrapper["hash"] == sync_crypto.sha256_hex(encrypted_blob_json) + assert wrapper["data"] == encrypted_blob_json + @pytest.mark.asyncio async def test_unmatched_collection_reads_opaque_store(self): """Any collection without a protocol-bridge case falls through to diff --git a/packages/sync/tests/test_sync_collections.py b/packages/sync/tests/test_sync_collections.py index c8449eeb4..9817ec326 100644 --- a/packages/sync/tests/test_sync_collections.py +++ b/packages/sync/tests/test_sync_collections.py @@ -87,17 +87,23 @@ def test_fallback_to_default_config(): assert config.version == 1 assert config.namespaces is not None ns_collections = config.namespaces["octobot"].collections - assert len(ns_collections) == 5 + assert len(ns_collections) == 8 by_name = {c.name: c for c in ns_collections} assert set(by_name) == { "user-data", "user-accounts", + "user-accounts-auth", + "user-accounts-trading", + "user-accounts-history", "user-settings", "user-strategies", "user-actions", } assert by_name["user-data"].storage_path == "users/{identity}/data" assert by_name["user-accounts"].storage_path == "users/{identity}/accounts" + assert by_name["user-accounts-auth"].storage_path == "users/{identity}/accounts/auth" + assert by_name["user-accounts-trading"].storage_path == "users/{identity}/accounts/{account_id}/trading" + assert by_name["user-accounts-history"].storage_path == "users/{identity}/accounts/{account_id}/history" assert by_name["user-settings"].storage_path == "users/{identity}/settings" assert by_name["user-strategies"].storage_path == "users/{identity}/strategies" assert by_name["user-actions"].storage_path == "users/{identity}/actions" diff --git a/packages/trading/octobot_trading/personal_data/portfolios/protocol.py b/packages/trading/octobot_trading/personal_data/portfolios/protocol.py index f3306a0a7..1cb316f98 100644 --- a/packages/trading/octobot_trading/personal_data/portfolios/protocol.py +++ b/packages/trading/octobot_trading/personal_data/portfolios/protocol.py @@ -22,9 +22,9 @@ def to_protocol_assets( portfolio: dict[str, dict[str, typing.Union[float, decimal.Decimal]]] -) -> list[protocol_models.Asset]: +) -> list[protocol_models.DetailedAsset]: return [ - protocol_models.Asset( + protocol_models.DetailedAsset( symbol=symbol, available=float(symbol_balance[commons_constants.PORTFOLIO_AVAILABLE]), total=float(symbol_balance[commons_constants.PORTFOLIO_TOTAL]),