Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/node/octobot_node/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
FAILURE_ERROR_DETAILS_MAX_LENGTH = 8_000

EXCHANGE_ACCOUNTS_STATE_VERSION = "1.0.0"
USER_ACCOUNTS_AUTH_DETAILS_STATE_VERSION = "1.0.0"
USER_ACCOUNTS_TRADING_DETAILS_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"
4 changes: 4 additions & 0 deletions packages/node/octobot_node/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ class AccountNotFoundError(UserActionError):
"""Raised when fetching an account via AccountProvider fails."""


class AccountAuthenticationDetailsNotFoundError(UserActionError):
"""Raised when fetching account authentication details via AccountAuthenticationDetailsProvider fails."""


class AutomationStrategyNotFoundError(UserActionError):
"""Raised when the referenced strategy does not exist in StrategyProvider."""

Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

import octobot_sync.sync.collection_backend.errors as collection_errors
import octobot_sync.sync.collection_providers.user_account_authentication_details_provider as auth_details_provider


def get_accounts_authentication_details_state_encrypted(address: str) -> dict[str, str] | None:
try:
return auth_details_provider.AccountAuthenticationDetailsProvider.instance().list_items_encrypted(address)
except collection_errors.CollectionNoDataError:
return None
31 changes: 31 additions & 0 deletions packages/node/octobot_node/protocol/accounts_trading_details.py
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

import octobot_sync.sync.collection_backend.errors as collection_errors
import octobot_sync.sync.collection_providers.user_account_trading_details_provider as trading_details_provider


def get_account_trading_details_state_encrypted(
address: str,
account_id: str,
) -> dict[str, str] | None:
try:
return trading_details_provider.AccountTradingDetailsProvider.instance().load_state_encrypted(
address,
account_id,
)
except collection_errors.CollectionNoDataError:
return None
106 changes: 42 additions & 64 deletions packages/node/octobot_node/protocol/automations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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.AccountAuthenticationDetailsNotFoundError):
return protocol_models.AccountActionResultErrorMessage.ACCOUNT_AUTHENTICATION_DETAILS_NOT_FOUND
if isinstance(
exc,
(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.AccountAuthenticationDetailsNotFoundError):
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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 _:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

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_details(
wallet_address: str,
account: protocol_models.Account,
) -> protocol_models.AccountAuthenticationDetails | None:
if account.is_simulated:
return None
account_details = account.details
if account_details is None or account_details.actual_instance is None:
raise node_errors.AccountAuthenticationDetailsNotFoundError(
f"Account {account.id!r} has no details for authentication lookup."
)
if not isinstance(account_details.actual_instance, protocol_models.ExchangeAccount):
raise node_errors.AccountAuthenticationDetailsNotFoundError(
f"Account {account.id!r} is not an exchange account; cannot resolve authentication details."
)
try:
authentication_details = collection_providers.AccountAuthenticationDetailsProvider.instance().get_item(
wallet_address,
account.id,
)
except collection_errors.ItemNotFoundError as err:
raise node_errors.AccountAuthenticationDetailsNotFoundError(
f"Authentication details for account {account.id!r} not found for address {wallet_address!r}: {err}"
) from err
if not authentication_details.api_key or not authentication_details.api_secret:
raise node_errors.AccountAuthenticationDetailsNotFoundError(
f"Authentication details for account {account.id!r} are missing api_key or api_secret."
)
return authentication_details
Loading
Loading