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
13 changes: 13 additions & 0 deletions packages/commons/octobot_commons/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# License along with this library.
import os
import octobot_commons.enums as enums
import octobot_protocol.models.trading_type as protocol_trading_type


def parse_boolean_str(value: str) -> bool:
Expand Down Expand Up @@ -126,6 +127,18 @@ def parse_boolean_environment_var(env_key: str, default_value: str) -> bool:
CONFIG_EXCHANGE_MARGIN = "margin"
CONFIG_EXCHANGE_OPTION = "option"
CONFIG_EXCHANGE_SPOT = "spot"
TRADING_TYPE_TO_EXCHANGE_TYPE: dict[protocol_trading_type.TradingType, str] = {
protocol_trading_type.TradingType.SPOT: CONFIG_EXCHANGE_SPOT,
protocol_trading_type.TradingType.FUTURES: CONFIG_EXCHANGE_FUTURE,
protocol_trading_type.TradingType.OPTIONS: CONFIG_EXCHANGE_OPTION,
protocol_trading_type.TradingType.MARGIN: CONFIG_EXCHANGE_MARGIN,
}
EXCHANGE_TYPE_TO_TRADING_TYPE: dict[str, protocol_trading_type.TradingType] = {
CONFIG_EXCHANGE_SPOT: protocol_trading_type.TradingType.SPOT,
CONFIG_EXCHANGE_FUTURE: protocol_trading_type.TradingType.FUTURES,
CONFIG_EXCHANGE_OPTION: protocol_trading_type.TradingType.OPTIONS,
CONFIG_EXCHANGE_MARGIN: protocol_trading_type.TradingType.MARGIN,
}
CONFIG_EXCHANGE_REST_ONLY = "rest_only"
CONFIG_EXCHANGE_WEB_SOCKET = "web-socket"
CONFIG_EXCHANGE_SUB_ACCOUNT = "sub_account"
Expand Down
6 changes: 6 additions & 0 deletions packages/commons/octobot_commons/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,9 @@ class MaxAttemptsExceededError(ErrorStatementEncountered):
"""
Raised when a max attempts is exceeded when executing a script
"""


class AmbiguousTradedSymbolsTradingTypeError(ValueError):
"""
Raised when traded symbols map to more than one exchange trading type.
"""
4 changes: 4 additions & 0 deletions packages/commons/octobot_commons/symbols/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
is_symbol,
is_usd_like_coin,
get_most_common_usd_like_symbol,
trading_type_from_symbol,
trading_type_from_traded_symbols,
)

from octobot_commons.symbols import symbol
Expand All @@ -41,5 +43,7 @@
"is_symbol",
"is_usd_like_coin",
"get_most_common_usd_like_symbol",
"trading_type_from_symbol",
"trading_type_from_traded_symbols",
"Symbol",
]
36 changes: 36 additions & 0 deletions packages/commons/octobot_commons/symbols/symbol_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import octobot_commons
import octobot_commons.constants as constants
import octobot_commons.errors as commons_errors
import octobot_commons.symbols.symbol


Expand Down Expand Up @@ -147,3 +148,38 @@ def get_most_common_usd_like_symbol(pairs: list[str]) -> str:
if is_usd_like_coin(symbol):
return symbol
raise ValueError("Pairs cannot be empty")


def trading_type_from_symbol(symbol_str: str) -> str:
"""
Infer exchange trading type from a symbol string.
:param symbol_str: the symbol to parse
:return: CONFIG_EXCHANGE_* constant (spot, future, or option)
"""
parsed_symbol = parse_symbol(symbol_str)
if parsed_symbol.is_option():
return constants.CONFIG_EXCHANGE_OPTION
if parsed_symbol.is_spot():
return constants.CONFIG_EXCHANGE_SPOT
if parsed_symbol.is_future() or parsed_symbol.is_perpetual_future():
return constants.CONFIG_EXCHANGE_FUTURE
raise ValueError(f"Unable to infer trading type from symbol {symbol_str!r}.")


def trading_type_from_traded_symbols(symbols: list[str]) -> str:
"""
Infer a single exchange trading type from a list of traded symbols.
:param symbols: traded symbol strings
:return: CONFIG_EXCHANGE_* constant shared by all symbols
"""
if not symbols:
raise ValueError("Traded symbols cannot be empty.")
inferred_trading_types: set[str] = set()
for symbol_str in symbols:
inferred_trading_types.add(trading_type_from_symbol(symbol_str))
if len(inferred_trading_types) > 1:
trading_type_names = sorted(inferred_trading_types)
raise commons_errors.AmbiguousTradedSymbolsTradingTypeError(
f"Traded symbols map to multiple trading types: {', '.join(trading_type_names)}."
)
return next(iter(inferred_trading_types))
8 changes: 8 additions & 0 deletions packages/commons/octobot_commons/timestamp_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,11 @@ def create_datetime_from_string(
return datetime.strptime(date_time_str, date_time_format).replace(
tzinfo=LOCAL_TIMEZONE if local_timezone else timezone.utc
)


def utc_now_datetime() -> datetime:
"""
Get the current UTC time
:return: the current UTC time
"""
return datetime.now(tz=timezone.utc)
46 changes: 46 additions & 0 deletions packages/commons/tests/symbols/test_symbol_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,49 @@ def test_is_symbol():
assert octobot_commons.symbols.is_symbol("", separator="/") is False
assert octobot_commons.symbols.is_symbol("/", separator="/") is True
assert octobot_commons.symbols.is_symbol("BTC/USDT/ETH", separator="/") is True


import pytest

import octobot_commons.constants as commons_constants


class TestTradingTypeFromSymbol:
def test_spot_symbol_returns_spot(self):
assert octobot_commons.symbols.trading_type_from_symbol("BTC/USDT") == commons_constants.CONFIG_EXCHANGE_SPOT

def test_futures_symbol_returns_future(self):
assert (
octobot_commons.symbols.trading_type_from_symbol("BTC/USDT:USDT")
== commons_constants.CONFIG_EXCHANGE_FUTURE
)

def test_option_symbol_returns_option(self):
assert (
octobot_commons.symbols.trading_type_from_symbol("BTC/USDT:USDT-211225-60000-P")
== commons_constants.CONFIG_EXCHANGE_OPTION
)

def test_unknown_symbol_raises_value_error(self):
with pytest.raises(ValueError):
octobot_commons.symbols.trading_type_from_symbol("BTC")


class TestTradingTypeFromTradedSymbols:
def test_all_spot_symbols_return_spot(self):
assert octobot_commons.symbols.trading_type_from_traded_symbols(
["BTC/USDT", "ETH/USDT"]
) == commons_constants.CONFIG_EXCHANGE_SPOT

def test_all_futures_symbols_return_future(self):
assert octobot_commons.symbols.trading_type_from_traded_symbols(
["BTC/USDT:USDT", "ETH/USDT:USDT"]
) == commons_constants.CONFIG_EXCHANGE_FUTURE

def test_empty_symbols_raises_value_error(self):
with pytest.raises(ValueError):
octobot_commons.symbols.trading_type_from_traded_symbols([])

def test_mixed_symbol_types_raise_ambiguous_error(self):
with pytest.raises(octobot_commons.errors.AmbiguousTradedSymbolsTradingTypeError):
octobot_commons.symbols.trading_type_from_traded_symbols(["BTC/USDT", "BTC/USDT:USDT"])
7 changes: 0 additions & 7 deletions packages/node/octobot_node/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,4 @@

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"
16 changes: 12 additions & 4 deletions packages/node/octobot_node/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,6 @@ class UnsupportedAutomationConfigurationTypeError(UserActionError):
"""Raised when an automation configuration type is not supported by the node."""


class AccountContextMissingError(UserActionError):
"""Raised when required account context identifiers are missing (public key, wallet key, account id)."""


class AccountNotFoundError(UserActionError):
"""Raised when fetching an account via AccountProvider fails."""

Expand All @@ -66,6 +62,10 @@ class AccountAuthenticationNotFoundError(UserActionError):
"""Raised when fetching account authentication via AccountAuthenticationProvider fails."""


class AmbiguousExchangeConfigError(UserActionError):
"""Raised when multiple exchange configs are found for an exchange account."""


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

Expand All @@ -84,3 +84,11 @@ class ActiveAutomationWorkflowNotFoundError(UserActionError):

class AmbiguousActiveAutomationWorkflowError(UserActionError):
"""Raised when more than one active automation workflow matches the stop request (parent id / wallet filter)."""


class UnknownTradingTypeError(UserActionError):
"""Raised when the trading type is unknown."""


class AmbiguousTradingTypeError(UserActionError):
"""Raised when multiple trading types are found for an account."""
8 changes: 5 additions & 3 deletions packages/node/octobot_node/protocol/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# with OctoBot. If not, see <https://www.gnu.org/licenses/>.

import octobot_protocol.models as protocol_models
import octobot_node.constants as node_constants
import octobot_sync.constants as sync_constants
import octobot_sync.sync.collection_providers.user_account_provider as account_provider
import octobot_sync.sync.collection_backend.errors as collection_errors

Expand All @@ -27,7 +27,9 @@ def get_accounts_state_encrypted(address: str) -> dict[str, str] | None:
return None

def get_accounts_state(address: str) -> protocol_models.AccountsState:
provider = account_provider.AccountProvider.instance()
return protocol_models.AccountsState(
version=node_constants.EXCHANGE_ACCOUNTS_STATE_VERSION,
accounts=account_provider.AccountProvider.instance().list_items(address)
version=sync_constants.EXCHANGE_ACCOUNTS_STATE_VERSION,
accounts=provider.list_items(address),
exchange_configs=provider.list_exchange_configs(address),
)
4 changes: 2 additions & 2 deletions packages/node/octobot_node/protocol/user_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
# with OctoBot. If not, see <https://www.gnu.org/licenses/>.

import octobot_protocol.models as protocol_models
import octobot_node.constants as node_constants
import octobot_sync.constants as sync_constants
import octobot_node.scheduler.api as scheduler_api


async def get_user_data_state(wallet_address: str) -> protocol_models.UserDataState:
automations = await scheduler_api.get_automation_states(wallet_address)
user_actions = await scheduler_api.list_user_actions(wallet_address)
return protocol_models.UserDataState(
version=node_constants.USER_DATA_STATE_VERSION,
version=sync_constants.USER_DATA_STATE_VERSION,
automations=automations,
user_actions=user_actions,
)
16 changes: 6 additions & 10 deletions packages/node/octobot_node/scheduler/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@
# You should have received a copy of the GNU General Public
# License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.

import asyncio
import contextlib
import datetime
import dbos
import json
import logging
Expand All @@ -34,10 +32,10 @@
import octobot_node.constants
import octobot_node.scheduler.workflows_util as workflows_util
import octobot_node.scheduler.workflows.params as workflow_params
import octobot_node.scheduler.user_actions.user_action_util as user_action_util
import octobot_node.scheduler.encryption as encryption
import octobot_node.scheduler.task_context as task_context
import octobot_node.protocol.automations as automations_protocol
import octobot_node.protocol.accounts as accounts_protocol

try:
from octobot import VERSION
Expand Down Expand Up @@ -570,13 +568,11 @@ def _failed_user_action_when_output_missing(
updated_at = timestamp_util.utc_datetime_from_timestamp(
(workflow_status.created_at or 0) / 1000
)
user_action.result = protocol_models.UserActionResult(
actual_instance=protocol_models.AccountActionResult(
updated_at=updated_at,
result_type=protocol_models.UserActionResultType.ACCOUNT,
error_message=protocol_models.AccountActionResultErrorMessage.INTERNAL_ERROR,
error_details=error_text[:octobot_node.constants.FAILURE_ERROR_DETAILS_MAX_LENGTH],
),
result_type = user_action_util.resolve_user_action_result_type(user_action)
user_action.result = user_action_util.build_synthesized_failure_user_action_result(
result_type=result_type,
updated_at=updated_at,
error_details=error_text[:octobot_node.constants.FAILURE_ERROR_DETAILS_MAX_LENGTH],
)
user_action.updated_at = updated_at
return user_action
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 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 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_protocol.models as protocol_models


def resolve_user_action_result_type(
user_action: protocol_models.UserAction,
) -> protocol_models.UserActionResultType:
configuration = user_action.configuration
if configuration is None or configuration.actual_instance is None:
return protocol_models.UserActionResultType.ACCOUNT
action_type = configuration.actual_instance.action_type
match action_type:
case (
protocol_models.UserActionType.AUTOMATION_CREATE
| protocol_models.UserActionType.AUTOMATION_EDIT
| protocol_models.UserActionType.AUTOMATION_STOP
):
return protocol_models.UserActionResultType.AUTOMATION
case (
protocol_models.UserActionType.EXCHANGE_CONFIG_CREATE
| protocol_models.UserActionType.EXCHANGE_CONFIG_EDIT
| protocol_models.UserActionType.EXCHANGE_CONFIG_DELETE
):
return protocol_models.UserActionResultType.EXCHANGE_CONFIG
case _:
return protocol_models.UserActionResultType.ACCOUNT


def build_synthesized_failure_user_action_result(
*,
result_type: protocol_models.UserActionResultType,
updated_at: datetime.datetime,
error_details: str,
) -> protocol_models.UserActionResult:
match result_type:
case protocol_models.UserActionResultType.AUTOMATION:
return protocol_models.UserActionResult(
actual_instance=protocol_models.AutomationActionResult(
updated_at=updated_at,
result_type=result_type,
error_message=protocol_models.AutomationActionResultErrorMessage.INTERNAL_ERROR,
error_details=error_details,
)
)
case protocol_models.UserActionResultType.EXCHANGE_CONFIG:
return protocol_models.UserActionResult(
actual_instance=protocol_models.ExchangeConfigActionResult(
updated_at=updated_at,
result_type=result_type,
error_message=protocol_models.ExchangeConfigActionResultErrorMessage.INTERNAL_ERROR,
error_details=error_details,
)
)
case _:
return protocol_models.UserActionResult(
actual_instance=protocol_models.AccountActionResult(
updated_at=updated_at,
result_type=protocol_models.UserActionResultType.ACCOUNT,
error_message=protocol_models.AccountActionResultErrorMessage.INTERNAL_ERROR,
error_details=error_details,
)
)
Loading
Loading