Skip to content

tradai.common.entities

Domain entities using Pydantic for validation and immutability.

Domain entities using Pydantic for validation and immutability.

All entities use frozen=True to ensure immutability and thread safety.

ExchangeConfig

Bases: BaseModel

Configuration for a CCXT exchange connection.

Supports both CEX (API key/secret) and DEX (private key) authentication. Uses TradingMode enum for market type. Provides ccxt_types property for CCXT market filtering.

Example

CEX (Binance)

config = ExchangeConfig(name="binance", trading_mode=TradingMode.FUTURES) config.key 'binance_futures'

DEX (Hyperliquid)

config = ExchangeConfig( ... name="hyperliquid", ... auth_type=AuthType.PRIVATE_KEY, ... private_key=SecretStr("0x..."), ... )

From Secrets Manager

config = ExchangeConfig.from_secret("tradai/prod/exchange/binance")

Source code in libs/tradai-common/src/tradai/common/entities.py
class ExchangeConfig(BaseModel):
    """Configuration for a CCXT exchange connection.

    Supports both CEX (API key/secret) and DEX (private key) authentication.
    Uses TradingMode enum for market type.
    Provides ccxt_types property for CCXT market filtering.

    Example:
        >>> # CEX (Binance)
        >>> config = ExchangeConfig(name="binance", trading_mode=TradingMode.FUTURES)
        >>> config.key
        'binance_futures'

        >>> # DEX (Hyperliquid)
        >>> config = ExchangeConfig(
        ...     name="hyperliquid",
        ...     auth_type=AuthType.PRIVATE_KEY,
        ...     private_key=SecretStr("0x..."),
        ... )

        >>> # From Secrets Manager
        >>> config = ExchangeConfig.from_secret("tradai/prod/exchange/binance")
    """

    model_config = {"frozen": True}

    name: str = Field(..., description="CCXT exchange name (e.g., 'binance', 'hyperliquid')")
    trading_mode: TradingMode = Field(default=TradingMode.FUTURES)
    auth_type: AuthType = Field(default=AuthType.API_KEY)

    # CEX fields (API key/secret pattern)
    api_key: SecretStr = Field(default=SecretStr(""), description="Exchange API key (CEX)")
    api_secret: SecretStr = Field(default=SecretStr(""), description="Exchange API secret (CEX)")
    passphrase: SecretStr = Field(
        default=SecretStr(""), description="Exchange passphrase (Coinbase, Kucoin)"
    )

    # DEX fields (private key pattern)
    private_key: SecretStr = Field(default=SecretStr(""), description="Private key (DEX)")
    wallet_address: str = Field(default="", description="Wallet address (DEX, optional)")

    @classmethod
    def from_secret(
        cls,
        secret_name: str,
        name: str | None = None,
        trading_mode: TradingMode = TradingMode.FUTURES,
        client: Any = None,
    ) -> ExchangeConfig:
        """Load credentials from AWS Secrets Manager.

        Expected secret format for CEX:
        ```json
        {
            "auth_type": "api_key",
            "api_key": "xxx",
            "api_secret": "yyy",
            "passphrase": "zzz"
        }
        ```

        Expected secret format for DEX:
        ```json
        {
            "auth_type": "private_key",
            "private_key": "0x...",
            "wallet_address": "0x..."
        }
        ```

        Args:
            secret_name: Name or ARN of the secret in Secrets Manager
            name: Exchange name (optional, can be in secret as "exchange_name")
            trading_mode: Trading mode (default: FUTURES)
            client: Optional Secrets Manager client for testing

        Returns:
            ExchangeConfig instance

        Raises:
            ExternalServiceError: If secret retrieval fails
            ValidationError: If secret format is invalid
        """
        secret = get_secret(secret_name, client=client)
        auth_type = AuthType(secret.get("auth_type", AuthType.API_KEY.value))
        exchange_name = name or secret.get("exchange_name", "")

        if not exchange_name:
            raise ValueError("Exchange name must be provided or included in secret")

        if auth_type == AuthType.PRIVATE_KEY:
            return cls(
                name=exchange_name,
                trading_mode=trading_mode,
                auth_type=auth_type,
                private_key=SecretStr(secret["private_key"]),
                wallet_address=secret.get("wallet_address", ""),
            )

        return cls(
            name=exchange_name,
            trading_mode=trading_mode,
            auth_type=auth_type,
            api_key=SecretStr(secret["api_key"]),
            api_secret=SecretStr(secret["api_secret"]),
            passphrase=SecretStr(secret.get("passphrase", "")),
        )

    @property
    def key(self) -> str:
        """Return dict key for this exchange (e.g., 'binance_futures')."""
        return f"{self.name}_{self.trading_mode.value}"

    @property
    def ccxt_types(self) -> frozenset[str]:
        """Get CCXT market types for filtering."""
        return self.trading_mode.ccxt_types

    @property
    def is_authenticated(self) -> bool:
        """Check if credentials are present for authentication.

        Returns:
            True if required credentials are set.
        """
        if self.auth_type == AuthType.PRIVATE_KEY:
            return bool(self.private_key.get_secret_value())
        return bool(self.api_key.get_secret_value() and self.api_secret.get_secret_value())

    def to_ccxt_config(self) -> dict[str, str]:
        """Convert credentials to CCXT configuration format.

        Returns:
            Dictionary suitable for CCXT exchange initialization.

        Example:
            >>> config = ExchangeConfig(name="binance", api_key=SecretStr("key"), ...)
            >>> ccxt_config = config.to_ccxt_config()
            >>> exchange = ccxt.binance(ccxt_config)
        """
        result: dict[str, str] = {}

        if self.auth_type == AuthType.API_KEY:
            if self.api_key.get_secret_value():
                result["apiKey"] = self.api_key.get_secret_value()
            if self.api_secret.get_secret_value():
                result["secret"] = self.api_secret.get_secret_value()
            if self.passphrase.get_secret_value():
                result["password"] = self.passphrase.get_secret_value()
        elif self.auth_type == AuthType.PRIVATE_KEY:
            if self.private_key.get_secret_value():
                result["privateKey"] = self.private_key.get_secret_value()
            if self.wallet_address:
                result["walletAddress"] = self.wallet_address

        return result

    def validate_credentials(self, timeout: int = 10) -> None:
        """Validate exchange credentials by making an authenticated API call.

        Uses fetch_balance() as the validation method - it's lightweight and
        requires authentication on all exchanges.

        Args:
            timeout: Request timeout in seconds

        Raises:
            AuthenticationError: If credentials are invalid or missing
            ExternalServiceError: If exchange API call fails for other reasons

        Example:
            >>> config = ExchangeConfig.from_secret("tradai/prod/binance")
            >>> config.validate_credentials()  # Raises if invalid
        """
        if not self.is_authenticated:
            raise AuthenticationError(f"No credentials configured for {self.name}")

        exchange = None
        try:
            import ccxt

            # Create exchange client
            exchange_id = self.name.lower()
            if not hasattr(ccxt, exchange_id):
                raise ExternalServiceError(f"Unknown exchange: {exchange_id}")

            exchange_class = getattr(ccxt, exchange_id)
            # Build config with proper types (to_ccxt_config returns dict[str, str])
            ccxt_config: dict[str, Any] = {
                **self.to_ccxt_config(),
                "enableRateLimit": True,
                "timeout": timeout * 1000,  # ccxt uses milliseconds
            }

            exchange = exchange_class(ccxt_config)

            # Validate by calling an authenticated endpoint
            exchange.fetch_balance()

        except ccxt.AuthenticationError as e:
            raise AuthenticationError(f"Invalid credentials for {self.name}: {e}") from e
        except ccxt.ExchangeError as e:
            raise ExternalServiceError(
                f"Exchange error validating credentials for {self.name}: {e}"
            ) from e
        except ImportError as e:
            raise ExternalServiceError(f"CCXT not installed: {e}") from e
        finally:
            # Clean up exchange client to prevent resource leak
            if exchange is not None and hasattr(exchange, "close"):
                try:
                    exchange.close()
                except Exception as e:
                    _logger.debug("Exchange cleanup failed: %s", e)

    @field_validator("name")
    @classmethod
    def validate_name(cls, v: str) -> str:
        """Normalize exchange name to lowercase."""
        if not v:
            raise ValueError("Exchange name cannot be empty")
        return v.lower()

    @field_validator("trading_mode", mode="before")
    @classmethod
    def parse_trading_mode(cls, v: str | TradingMode) -> TradingMode:
        """Parse trading mode from string or TradingMode enum."""
        if isinstance(v, TradingMode):
            return v
        if isinstance(v, str):
            return TradingMode.from_string(v)
        raise ValueError(f"trading_mode must be str or TradingMode, got {type(v)}")

key: str property

Return dict key for this exchange (e.g., 'binance_futures').

ccxt_types: frozenset[str] property

Get CCXT market types for filtering.

is_authenticated: bool property

Check if credentials are present for authentication.

Returns:

Type Description
bool

True if required credentials are set.

from_secret(secret_name: str, name: str | None = None, trading_mode: TradingMode = TradingMode.FUTURES, client: Any = None) -> ExchangeConfig classmethod

Load credentials from AWS Secrets Manager.

Expected secret format for CEX:

{
    "auth_type": "api_key",
    "api_key": "xxx",
    "api_secret": "yyy",
    "passphrase": "zzz"
}

Expected secret format for DEX:

{
    "auth_type": "private_key",
    "private_key": "0x...",
    "wallet_address": "0x..."
}

Parameters:

Name Type Description Default
secret_name str

Name or ARN of the secret in Secrets Manager

required
name str | None

Exchange name (optional, can be in secret as "exchange_name")

None
trading_mode TradingMode

Trading mode (default: FUTURES)

FUTURES
client Any

Optional Secrets Manager client for testing

None

Returns:

Type Description
ExchangeConfig

ExchangeConfig instance

Raises:

Type Description
ExternalServiceError

If secret retrieval fails

ValidationError

If secret format is invalid

Source code in libs/tradai-common/src/tradai/common/entities.py
@classmethod
def from_secret(
    cls,
    secret_name: str,
    name: str | None = None,
    trading_mode: TradingMode = TradingMode.FUTURES,
    client: Any = None,
) -> ExchangeConfig:
    """Load credentials from AWS Secrets Manager.

    Expected secret format for CEX:
    ```json
    {
        "auth_type": "api_key",
        "api_key": "xxx",
        "api_secret": "yyy",
        "passphrase": "zzz"
    }
    ```

    Expected secret format for DEX:
    ```json
    {
        "auth_type": "private_key",
        "private_key": "0x...",
        "wallet_address": "0x..."
    }
    ```

    Args:
        secret_name: Name or ARN of the secret in Secrets Manager
        name: Exchange name (optional, can be in secret as "exchange_name")
        trading_mode: Trading mode (default: FUTURES)
        client: Optional Secrets Manager client for testing

    Returns:
        ExchangeConfig instance

    Raises:
        ExternalServiceError: If secret retrieval fails
        ValidationError: If secret format is invalid
    """
    secret = get_secret(secret_name, client=client)
    auth_type = AuthType(secret.get("auth_type", AuthType.API_KEY.value))
    exchange_name = name or secret.get("exchange_name", "")

    if not exchange_name:
        raise ValueError("Exchange name must be provided or included in secret")

    if auth_type == AuthType.PRIVATE_KEY:
        return cls(
            name=exchange_name,
            trading_mode=trading_mode,
            auth_type=auth_type,
            private_key=SecretStr(secret["private_key"]),
            wallet_address=secret.get("wallet_address", ""),
        )

    return cls(
        name=exchange_name,
        trading_mode=trading_mode,
        auth_type=auth_type,
        api_key=SecretStr(secret["api_key"]),
        api_secret=SecretStr(secret["api_secret"]),
        passphrase=SecretStr(secret.get("passphrase", "")),
    )

to_ccxt_config() -> dict[str, str]

Convert credentials to CCXT configuration format.

Returns:

Type Description
dict[str, str]

Dictionary suitable for CCXT exchange initialization.

Example

config = ExchangeConfig(name="binance", api_key=SecretStr("key"), ...) ccxt_config = config.to_ccxt_config() exchange = ccxt.binance(ccxt_config)

Source code in libs/tradai-common/src/tradai/common/entities.py
def to_ccxt_config(self) -> dict[str, str]:
    """Convert credentials to CCXT configuration format.

    Returns:
        Dictionary suitable for CCXT exchange initialization.

    Example:
        >>> config = ExchangeConfig(name="binance", api_key=SecretStr("key"), ...)
        >>> ccxt_config = config.to_ccxt_config()
        >>> exchange = ccxt.binance(ccxt_config)
    """
    result: dict[str, str] = {}

    if self.auth_type == AuthType.API_KEY:
        if self.api_key.get_secret_value():
            result["apiKey"] = self.api_key.get_secret_value()
        if self.api_secret.get_secret_value():
            result["secret"] = self.api_secret.get_secret_value()
        if self.passphrase.get_secret_value():
            result["password"] = self.passphrase.get_secret_value()
    elif self.auth_type == AuthType.PRIVATE_KEY:
        if self.private_key.get_secret_value():
            result["privateKey"] = self.private_key.get_secret_value()
        if self.wallet_address:
            result["walletAddress"] = self.wallet_address

    return result

validate_credentials(timeout: int = 10) -> None

Validate exchange credentials by making an authenticated API call.

Uses fetch_balance() as the validation method - it's lightweight and requires authentication on all exchanges.

Parameters:

Name Type Description Default
timeout int

Request timeout in seconds

10

Raises:

Type Description
AuthenticationError

If credentials are invalid or missing

ExternalServiceError

If exchange API call fails for other reasons

Example

config = ExchangeConfig.from_secret("tradai/prod/binance") config.validate_credentials() # Raises if invalid

Source code in libs/tradai-common/src/tradai/common/entities.py
def validate_credentials(self, timeout: int = 10) -> None:
    """Validate exchange credentials by making an authenticated API call.

    Uses fetch_balance() as the validation method - it's lightweight and
    requires authentication on all exchanges.

    Args:
        timeout: Request timeout in seconds

    Raises:
        AuthenticationError: If credentials are invalid or missing
        ExternalServiceError: If exchange API call fails for other reasons

    Example:
        >>> config = ExchangeConfig.from_secret("tradai/prod/binance")
        >>> config.validate_credentials()  # Raises if invalid
    """
    if not self.is_authenticated:
        raise AuthenticationError(f"No credentials configured for {self.name}")

    exchange = None
    try:
        import ccxt

        # Create exchange client
        exchange_id = self.name.lower()
        if not hasattr(ccxt, exchange_id):
            raise ExternalServiceError(f"Unknown exchange: {exchange_id}")

        exchange_class = getattr(ccxt, exchange_id)
        # Build config with proper types (to_ccxt_config returns dict[str, str])
        ccxt_config: dict[str, Any] = {
            **self.to_ccxt_config(),
            "enableRateLimit": True,
            "timeout": timeout * 1000,  # ccxt uses milliseconds
        }

        exchange = exchange_class(ccxt_config)

        # Validate by calling an authenticated endpoint
        exchange.fetch_balance()

    except ccxt.AuthenticationError as e:
        raise AuthenticationError(f"Invalid credentials for {self.name}: {e}") from e
    except ccxt.ExchangeError as e:
        raise ExternalServiceError(
            f"Exchange error validating credentials for {self.name}: {e}"
        ) from e
    except ImportError as e:
        raise ExternalServiceError(f"CCXT not installed: {e}") from e
    finally:
        # Clean up exchange client to prevent resource leak
        if exchange is not None and hasattr(exchange, "close"):
            try:
                exchange.close()
            except Exception as e:
                _logger.debug("Exchange cleanup failed: %s", e)

validate_name(v: str) -> str classmethod

Normalize exchange name to lowercase.

Source code in libs/tradai-common/src/tradai/common/entities.py
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
    """Normalize exchange name to lowercase."""
    if not v:
        raise ValueError("Exchange name cannot be empty")
    return v.lower()

parse_trading_mode(v: str | TradingMode) -> TradingMode classmethod

Parse trading mode from string or TradingMode enum.

Source code in libs/tradai-common/src/tradai/common/entities.py
@field_validator("trading_mode", mode="before")
@classmethod
def parse_trading_mode(cls, v: str | TradingMode) -> TradingMode:
    """Parse trading mode from string or TradingMode enum."""
    if isinstance(v, TradingMode):
        return v
    if isinstance(v, str):
        return TradingMode.from_string(v)
    raise ValueError(f"trading_mode must be str or TradingMode, got {type(v)}")

BacktestConfig

Bases: BaseModel

Backtest configuration with validation.

Defines all parameters needed to run a backtest.

Source code in libs/tradai-common/src/tradai/common/entities.py
class BacktestConfig(BaseModel):
    """
    Backtest configuration with validation.

    Defines all parameters needed to run a backtest.
    """

    strategy: str = Field(..., min_length=1)
    timeframe: str = Field(..., pattern=r"^\d+[mhd]$")  # e.g., "1h", "4h", "1d"
    start_date: str = Field(..., pattern=r"^\d{4}-\d{2}-\d{2}$")
    end_date: str = Field(..., pattern=r"^\d{4}-\d{2}-\d{2}$")
    symbols: list[str] = Field(..., min_length=1)
    stoploss: float | None = Field(default=None, ge=-1.0, le=0.0)
    stake_amount: float = Field(default=1000.0, gt=0)

    model_config = {"frozen": True}

    @field_validator("symbols")
    @classmethod
    def validate_symbols(cls, v: list[str]) -> list[str]:
        """Ensure all symbols are non-empty."""
        if not all(s.strip() for s in v):
            raise ValueError("All symbols must be non-empty")
        return v

validate_symbols(v: list[str]) -> list[str] classmethod

Ensure all symbols are non-empty.

Source code in libs/tradai-common/src/tradai/common/entities.py
@field_validator("symbols")
@classmethod
def validate_symbols(cls, v: list[str]) -> list[str]:
    """Ensure all symbols are non-empty."""
    if not all(s.strip() for s in v):
        raise ValueError("All symbols must be non-empty")
    return v

BacktestResult

Bases: BaseModel

Backtest execution result.

Contains complete metrics and trades from a Freqtrade backtest. Includes all key ratios: Sharpe, Sortino, Calmar, SQN, etc. Additional metrics stored in the metrics dict for MLflow logging.

Source code in libs/tradai-common/src/tradai/common/entities.py
class BacktestResult(BaseModel):
    """
    Backtest execution result.

    Contains complete metrics and trades from a Freqtrade backtest.
    Includes all key ratios: Sharpe, Sortino, Calmar, SQN, etc.
    Additional metrics stored in the metrics dict for MLflow logging.
    """

    # Trade summary
    total_trades: int = Field(ge=0)
    winning_trades: int = Field(ge=0)
    losing_trades: int = Field(default=0, ge=0)
    draw_trades: int = Field(default=0, ge=0)

    # Profit metrics
    total_profit: float = Field(default=0.0)
    total_profit_pct: float = Field(default=0.0)
    profit_factor: float | None = Field(default=None, ge=0)
    expectancy: float | None = Field(default=None)
    expectancy_ratio: float | None = Field(default=None)

    # Risk ratios (Freqtrade 2023.1+)
    sharpe_ratio: float | None = Field(default=None)
    sortino_ratio: float | None = Field(default=None)
    calmar_ratio: float | None = Field(default=None)
    sqn: float | None = Field(default=None)  # System Quality Number
    cagr: float | None = Field(default=None)  # Compound Annual Growth Rate

    # Drawdown metrics
    max_drawdown: float | None = Field(default=None)
    max_drawdown_pct: float | None = Field(default=None)
    drawdown_start: str | None = Field(default=None)
    drawdown_end: str | None = Field(default=None)

    # Trade durations
    avg_trade_duration: str | None = Field(default=None)
    winning_avg_duration: str | None = Field(default=None)
    losing_avg_duration: str | None = Field(default=None)

    # Win rate
    win_rate: float | None = Field(default=None, ge=0.0, le=1.0)

    # Best/worst performance
    best_pair: str | None = Field(default=None)
    worst_pair: str | None = Field(default=None)
    best_trade_profit: float | None = Field(default=None)
    worst_trade_profit: float | None = Field(default=None)

    # Additional metrics dict (for MLflow logging)
    metrics: dict[str, float] = Field(default_factory=dict)

    # Raw trades (limited to prevent memory issues)
    trades: list[dict[str, Any]] = Field(default_factory=list)

    # Full raw stats from Freqtrade (for complete logging)
    raw_stats: dict[str, Any] = Field(default_factory=dict)

    # MLflow traceability fields (SS010/BE010)
    mlflow_run_id: str | None = Field(default=None, description="MLflow run ID for traceability")
    config_artifact_uri: str | None = Field(
        default=None, description="URI of uploaded config artifact in MLflow"
    )
    results_artifact_uri: str | None = Field(default=None, description="S3 URI of results file")

    model_config = {"frozen": True}

    @field_validator("winning_trades")
    @classmethod
    def validate_winning_trades(cls, v: int, info: Any) -> int:
        """Ensure winning trades <= total trades."""
        if "total_trades" in info.data and v > info.data["total_trades"]:
            raise ValueError("winning_trades cannot exceed total_trades")
        return v

validate_winning_trades(v: int, info: Any) -> int classmethod

Ensure winning trades <= total trades.

Source code in libs/tradai-common/src/tradai/common/entities.py
@field_validator("winning_trades")
@classmethod
def validate_winning_trades(cls, v: int, info: Any) -> int:
    """Ensure winning trades <= total trades."""
    if "total_trades" in info.data and v > info.data["total_trades"]:
        raise ValueError("winning_trades cannot exceed total_trades")
    return v

AWSConfig

Bases: BaseModel

AWS infrastructure configuration.

All values loaded from environment variables - no hardcoded infrastructure IDs.

Source code in libs/tradai-common/src/tradai/common/entities.py
class AWSConfig(BaseModel):
    """
    AWS infrastructure configuration.

    All values loaded from environment variables - no hardcoded infrastructure IDs.
    """

    region: str = Field(..., min_length=1)
    account_id: str = Field(..., pattern=r"^\d{12}$")
    execution_role_arn: str = Field(..., pattern=r"^arn:aws:iam::\d{12}:role/.+$")
    task_role_arn: str = Field(..., pattern=r"^arn:aws:iam::\d{12}:role/.+$")
    ecs_cluster: str = Field(..., min_length=1)
    subnets: list[str] = Field(..., min_length=1)
    security_groups: list[str] = Field(..., min_length=1)

    model_config = {"frozen": True}

    @field_validator("subnets")
    @classmethod
    def validate_subnets(cls, v: list[str]) -> list[str]:
        """Validate subnet IDs format."""
        for subnet in v:
            if not subnet.startswith("subnet-"):
                raise ValueError(f"Invalid subnet ID: {subnet}")
        return v

    @field_validator("security_groups")
    @classmethod
    def validate_security_groups(cls, v: list[str]) -> list[str]:
        """Validate security group IDs format."""
        for sg in v:
            if not sg.startswith("sg-"):
                raise ValueError(f"Invalid security group ID: {sg}")
        return v

validate_subnets(v: list[str]) -> list[str] classmethod

Validate subnet IDs format.

Source code in libs/tradai-common/src/tradai/common/entities.py
@field_validator("subnets")
@classmethod
def validate_subnets(cls, v: list[str]) -> list[str]:
    """Validate subnet IDs format."""
    for subnet in v:
        if not subnet.startswith("subnet-"):
            raise ValueError(f"Invalid subnet ID: {subnet}")
    return v

validate_security_groups(v: list[str]) -> list[str] classmethod

Validate security group IDs format.

Source code in libs/tradai-common/src/tradai/common/entities.py
@field_validator("security_groups")
@classmethod
def validate_security_groups(cls, v: list[str]) -> list[str]:
    """Validate security group IDs format."""
    for sg in v:
        if not sg.startswith("sg-"):
            raise ValueError(f"Invalid security group ID: {sg}")
    return v

S3Path

Bases: BaseModel

Parsed S3 path with validation.

Immutable representation of an S3 URI.

Source code in libs/tradai-common/src/tradai/common/entities.py
class S3Path(BaseModel):
    """
    Parsed S3 path with validation.

    Immutable representation of an S3 URI.
    """

    bucket: str = Field(..., min_length=3, max_length=63)
    key: str = Field(..., min_length=1)

    model_config = {"frozen": True}

    @classmethod
    def parse(cls, uri: str) -> S3Path:
        """
        Parse S3 URI into bucket and key.

        Args:
            uri: S3 URI (e.g., "s3://bucket/key/path")

        Returns:
            S3Path instance

        Raises:
            ValueError: If URI format is invalid
        """
        if not uri.startswith("s3://"):
            raise ValueError(f"Invalid S3 URI: {uri} (must start with 's3://')")

        parts = uri[5:].split("/", 1)
        if len(parts) != 2 or not parts[0] or not parts[1]:
            raise ValueError(f"Invalid S3 URI: {uri} (must be s3://bucket/key)")

        return cls(bucket=parts[0], key=parts[1])

    def to_uri(self) -> str:
        """Convert back to S3 URI string."""
        return f"s3://{self.bucket}/{self.key}"

    def __str__(self) -> str:
        """String representation."""
        return self.to_uri()

parse(uri: str) -> S3Path classmethod

Parse S3 URI into bucket and key.

Parameters:

Name Type Description Default
uri str

S3 URI (e.g., "s3://bucket/key/path")

required

Returns:

Type Description
S3Path

S3Path instance

Raises:

Type Description
ValueError

If URI format is invalid

Source code in libs/tradai-common/src/tradai/common/entities.py
@classmethod
def parse(cls, uri: str) -> S3Path:
    """
    Parse S3 URI into bucket and key.

    Args:
        uri: S3 URI (e.g., "s3://bucket/key/path")

    Returns:
        S3Path instance

    Raises:
        ValueError: If URI format is invalid
    """
    if not uri.startswith("s3://"):
        raise ValueError(f"Invalid S3 URI: {uri} (must start with 's3://')")

    parts = uri[5:].split("/", 1)
    if len(parts) != 2 or not parts[0] or not parts[1]:
        raise ValueError(f"Invalid S3 URI: {uri} (must be s3://bucket/key)")

    return cls(bucket=parts[0], key=parts[1])

to_uri() -> str

Convert back to S3 URI string.

Source code in libs/tradai-common/src/tradai/common/entities.py
def to_uri(self) -> str:
    """Convert back to S3 URI string."""
    return f"s3://{self.bucket}/{self.key}"

TradingMode

Bases: str, Enum

Trading mode enum compatible with Freqtrade's TradingMode.

Defines market types for exchange connections: - SPOT: Spot trading markets - MARGIN: Margin trading markets - FUTURES: Futures markets (perpetual swaps and delivery)

Maps to CCXT market types via ccxt_types property.

Example

mode = TradingMode.FUTURES mode.value 'futures' mode.ccxt_types frozenset({'swap', 'future'})

Source code in libs/tradai-common/src/tradai/common/entities.py
class TradingMode(str, Enum):
    """Trading mode enum compatible with Freqtrade's TradingMode.

    Defines market types for exchange connections:
    - SPOT: Spot trading markets
    - MARGIN: Margin trading markets
    - FUTURES: Futures markets (perpetual swaps and delivery)

    Maps to CCXT market types via ccxt_types property.

    Example:
        >>> mode = TradingMode.FUTURES
        >>> mode.value
        'futures'
        >>> mode.ccxt_types
        frozenset({'swap', 'future'})
    """

    SPOT = "spot"
    MARGIN = "margin"
    FUTURES = "futures"

    @property
    def ccxt_types(self) -> frozenset[str]:
        """Get CCXT market types for filtering.

        Returns:
            Set of CCXT type strings for this trading mode
        """
        ccxt_map: dict[str, frozenset[str]] = {
            "spot": frozenset({"spot"}),
            "margin": frozenset({"margin"}),
            "futures": frozenset({"swap", "future"}),  # swap=perpetual, future=delivery
        }
        return ccxt_map[self.value]

    @classmethod
    def from_string(cls, value: str) -> TradingMode:
        """Parse trading mode from string (case-insensitive).

        Args:
            value: Trading mode string ('spot', 'margin', 'futures')

        Returns:
            TradingMode enum value

        Raises:
            ValueError: If value is not a valid trading mode
        """
        normalized = value.lower().strip()
        for member in cls:
            if member.value == normalized:
                return member
        valid = [m.value for m in cls]
        raise ValueError(f"Invalid trading mode '{value}'. Valid: {valid}")

ccxt_types: frozenset[str] property

Get CCXT market types for filtering.

Returns:

Type Description
frozenset[str]

Set of CCXT type strings for this trading mode

from_string(value: str) -> TradingMode classmethod

Parse trading mode from string (case-insensitive).

Parameters:

Name Type Description Default
value str

Trading mode string ('spot', 'margin', 'futures')

required

Returns:

Type Description
TradingMode

TradingMode enum value

Raises:

Type Description
ValueError

If value is not a valid trading mode

Source code in libs/tradai-common/src/tradai/common/entities.py
@classmethod
def from_string(cls, value: str) -> TradingMode:
    """Parse trading mode from string (case-insensitive).

    Args:
        value: Trading mode string ('spot', 'margin', 'futures')

    Returns:
        TradingMode enum value

    Raises:
        ValueError: If value is not a valid trading mode
    """
    normalized = value.lower().strip()
    for member in cls:
        if member.value == normalized:
            return member
    valid = [m.value for m in cls]
    raise ValueError(f"Invalid trading mode '{value}'. Valid: {valid}")

AuthType

Bases: str, Enum

Authentication type for exchange connections.

CEX (Centralized Exchange): Uses API key/secret authentication. DEX (Decentralized Exchange): Uses private key signing (EVM wallet).

Example

auth_type = AuthType.API_KEY # For Binance, Kraken auth_type = AuthType.PRIVATE_KEY # For Hyperliquid

Source code in libs/tradai-common/src/tradai/common/entities.py
class AuthType(str, Enum):
    """Authentication type for exchange connections.

    CEX (Centralized Exchange): Uses API key/secret authentication.
    DEX (Decentralized Exchange): Uses private key signing (EVM wallet).

    Example:
        >>> auth_type = AuthType.API_KEY  # For Binance, Kraken
        >>> auth_type = AuthType.PRIVATE_KEY  # For Hyperliquid
    """

    API_KEY = "api_key"  # CEX: Binance, Kraken, Coinbase
    PRIVATE_KEY = "private_key"  # DEX: Hyperliquid, dYdX

OperatingMode

Bases: str, Enum

Operating mode for strategy deployment.

Defines whether a deployed strategy runs with real or simulated trading: - LIVE: Real trading with actual funds - DRY_RUN: Paper trading simulation (no real orders)

Distinct from TradingMode (market type) and used for deployment tracking.

Example

mode = OperatingMode.DRY_RUN mode.value 'dry-run' mode.is_paper_trading True

Source code in libs/tradai-common/src/tradai/common/entities.py
class OperatingMode(str, Enum):
    """Operating mode for strategy deployment.

    Defines whether a deployed strategy runs with real or simulated trading:
    - LIVE: Real trading with actual funds
    - DRY_RUN: Paper trading simulation (no real orders)

    Distinct from TradingMode (market type) and used for deployment tracking.

    Example:
        >>> mode = OperatingMode.DRY_RUN
        >>> mode.value
        'dry-run'
        >>> mode.is_paper_trading
        True
    """

    LIVE = "live"
    DRY_RUN = "dry-run"

    @property
    def is_paper_trading(self) -> bool:
        """Check if this mode uses paper trading (no real orders)."""
        return self == OperatingMode.DRY_RUN

    @classmethod
    def from_string(cls, value: str) -> OperatingMode:
        """Parse operating mode from string (case-insensitive).

        Args:
            value: Operating mode string ('live', 'dry-run', 'dry_run')

        Returns:
            OperatingMode enum value

        Raises:
            ValueError: If value is not a valid operating mode
        """
        normalized = value.lower().replace("_", "-")
        for mode in cls:
            if mode.value == normalized:
                return mode
        raise ValueError(
            f"Invalid operating mode: {value}. Must be one of: {[m.value for m in cls]}"
        )

is_paper_trading: bool property

Check if this mode uses paper trading (no real orders).

from_string(value: str) -> OperatingMode classmethod

Parse operating mode from string (case-insensitive).

Parameters:

Name Type Description Default
value str

Operating mode string ('live', 'dry-run', 'dry_run')

required

Returns:

Type Description
OperatingMode

OperatingMode enum value

Raises:

Type Description
ValueError

If value is not a valid operating mode

Source code in libs/tradai-common/src/tradai/common/entities.py
@classmethod
def from_string(cls, value: str) -> OperatingMode:
    """Parse operating mode from string (case-insensitive).

    Args:
        value: Operating mode string ('live', 'dry-run', 'dry_run')

    Returns:
        OperatingMode enum value

    Raises:
        ValueError: If value is not a valid operating mode
    """
    normalized = value.lower().replace("_", "-")
    for mode in cls:
        if mode.value == normalized:
            return mode
    raise ValueError(
        f"Invalid operating mode: {value}. Must be one of: {[m.value for m in cls]}"
    )