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]),