Skip to content

tradai.common.entities

Domain entities using Pydantic for validation and immutability.

TradAI common entities.

This module provides all shared entity classes used across the TradAI platform. All entities are Pydantic models with frozen=True for immutability.

Submodules: - aws: AWS-related entities (S3Path, AWSConfig, JobStatus) - backtest: Backtesting entities (BacktestConfig, BacktestResult, BacktestJobStatus) - config_version: Config versioning entities (ConfigVersion, ConfigVersionStatus) - deployment: Deployment entities (DeploymentRecord, DeploymentStatus) - exchange: Exchange entities (ExchangeConfig, TradingMode, AuthType, OperatingMode) - hyperopt: Hyperparameter optimization entities (OptunaHyperoptConfig, TrialResult) - mlflow: MLflow entities (ModelVersion, ExperimentRun, RegisteredModel) - retraining: Retraining workflow entities (RetrainingState, RetrainingTrigger) - strategy: Strategy entity (Strategy) - trading_state: Trading state entities (TradingState, TradingStatus, StrategyPnL)

Example

from tradai.common.entities import BacktestConfig, ExchangeConfig config = BacktestConfig(strategy_name="PascalStrategy", ...)

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/exchange.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
        """
        from tradai.common.aws.secrets_manager import get_secret

        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:
            from tradai.common._optional import MissingDependencyError

            raise MissingDependencyError(
                "ccxt", "ccxt", "ExchangeConfig.validate_credentials"
            ) 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/exchange.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
    """
    from tradai.common.aws.secrets_manager import get_secret

    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/exchange.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/exchange.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:
        from tradai.common._optional import MissingDependencyError

        raise MissingDependencyError(
            "ccxt", "ccxt", "ExchangeConfig.validate_credentials"
        ) 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/exchange.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/exchange.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. Validates symbols use proper trading pair format and date range is valid.

Attributes:

Name Type Description
strategy str

Strategy name to backtest

timeframe str

Trading timeframe (e.g., "1h", "4h", "1d")

start_date str

Start date in YYYY-MM-DD format

end_date str

End date in YYYY-MM-DD format

symbols list[str]

List of trading pairs

stoploss float | None

Optional stoploss percentage (-1.0 to 0.0)

stake_amount float

Position size in stake currency

exchange str

Exchange key (e.g., "binance_futures")

strategy_version str

MLflow version specifier ("latest", stage name, or number)

config_version_id str | None

Optional ConfigVersion ID for reproducibility

Example

config = BacktestConfig( ... strategy="PascalStrategy", ... timeframe="1h", ... start_date="2024-01-01", ... end_date="2024-06-30", ... symbols=["BTC/USDT:USDT", "ETH/USDT:USDT"], ... exchange="binance_futures", ... strategy_version="Production", ... )

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

    Defines all parameters needed to run a backtest.
    Validates symbols use proper trading pair format and date range is valid.

    Attributes:
        strategy: Strategy name to backtest
        timeframe: Trading timeframe (e.g., "1h", "4h", "1d")
        start_date: Start date in YYYY-MM-DD format
        end_date: End date in YYYY-MM-DD format
        symbols: List of trading pairs
        stoploss: Optional stoploss percentage (-1.0 to 0.0)
        stake_amount: Position size in stake currency
        exchange: Exchange key (e.g., "binance_futures")
        strategy_version: MLflow version specifier ("latest", stage name, or number)
        config_version_id: Optional ConfigVersion ID for reproducibility

    Example:
        >>> config = BacktestConfig(
        ...     strategy="PascalStrategy",
        ...     timeframe="1h",
        ...     start_date="2024-01-01",
        ...     end_date="2024-06-30",
        ...     symbols=["BTC/USDT:USDT", "ETH/USDT:USDT"],
        ...     exchange="binance_futures",
        ...     strategy_version="Production",
        ... )
    """

    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)

    # Exchange configuration (default matches existing hardcoded value)
    exchange: str = Field(
        default="binance_futures",
        # Pattern derived from TradingMode enum values (see _EXCHANGE_PATTERN above)
        pattern=_EXCHANGE_PATTERN,
        description="Exchange key: '{name}_{trading_mode}' (e.g., 'binance_futures')",
    )

    # Strategy version for MLflow resolution (default matches existing hardcoded value)
    strategy_version: str = Field(
        default="latest",
        description="'latest', 'Production', 'Staging', or specific version number",
    )

    # Config version reference for reproducibility (opt-in)
    config_version_id: str | None = Field(
        default=None,
        description="ConfigVersion ID for reproducibility tracking",
    )

    # ECS task definition override (bypass unified convention)
    task_definition: str | None = Field(
        default=None,
        min_length=1,
        description=(
            "ECS task definition family name override. If None, computed from "
            "strategy name + environment via get_strategy_task_definition(). "
            "WARNING: Setting this bypasses the unified naming convention. "
            "Use only for emergency overrides or migration."
        ),
    )

    model_config = {"frozen": True}

    @field_validator("start_date", "end_date")
    @classmethod
    def validate_date_is_real(cls, v: str) -> str:
        """Validate date string represents an actual calendar date (C6).

        The pattern regex only checks format (YYYY-MM-DD), not validity.
        This validator catches impossible dates like 2024-02-30 or 2024-13-01.
        """
        try:
            datetime.strptime(v, "%Y-%m-%d")
        except ValueError as e:
            raise ValueError(f"Invalid date '{v}': {e}") from e
        return v

    @field_validator("strategy_version")
    @classmethod
    def validate_strategy_version(cls, v: str) -> str:
        """Validate strategy_version format.

        Valid formats:
        - "latest" - resolve to highest version number
        - "Production", "Staging", "Archived", "None" - resolve by MLflow stage
        - Numeric string like "1", "2", "3" - specific version number
        """
        valid_specifiers = {"latest", "production", "staging", "archived", "none"}
        if v.lower() in valid_specifiers or v.isdigit():
            return v
        raise ValueError(
            f"Invalid strategy_version '{v}'. Use 'latest', stage name, or version number"
        )

    @field_validator("symbols")
    @classmethod
    def validate_symbols_format(cls, v: list[str]) -> list[str]:
        """Validate all symbols use proper trading pair format.

        Uses validate_trading_pair to ensure format like BTC/USDT or BTC/USDT:USDT.
        Prevents command injection and normalizes to uppercase.
        """
        return [validate_trading_pair(s) for s in v]

    @model_validator(mode="after")
    def validate_date_range(self) -> BacktestConfig:
        """Ensure end_date is not before start_date."""
        start = datetime.strptime(self.start_date, "%Y-%m-%d")
        end = datetime.strptime(self.end_date, "%Y-%m-%d")
        if end < start:
            raise ValueError(
                f"end_date ({self.end_date}) must be >= start_date ({self.start_date})"
            )
        return self

validate_date_is_real(v: str) -> str classmethod

Validate date string represents an actual calendar date (C6).

The pattern regex only checks format (YYYY-MM-DD), not validity. This validator catches impossible dates like 2024-02-30 or 2024-13-01.

Source code in libs/tradai-common/src/tradai/common/entities/backtest.py
@field_validator("start_date", "end_date")
@classmethod
def validate_date_is_real(cls, v: str) -> str:
    """Validate date string represents an actual calendar date (C6).

    The pattern regex only checks format (YYYY-MM-DD), not validity.
    This validator catches impossible dates like 2024-02-30 or 2024-13-01.
    """
    try:
        datetime.strptime(v, "%Y-%m-%d")
    except ValueError as e:
        raise ValueError(f"Invalid date '{v}': {e}") from e
    return v

validate_strategy_version(v: str) -> str classmethod

Validate strategy_version format.

Valid formats: - "latest" - resolve to highest version number - "Production", "Staging", "Archived", "None" - resolve by MLflow stage - Numeric string like "1", "2", "3" - specific version number

Source code in libs/tradai-common/src/tradai/common/entities/backtest.py
@field_validator("strategy_version")
@classmethod
def validate_strategy_version(cls, v: str) -> str:
    """Validate strategy_version format.

    Valid formats:
    - "latest" - resolve to highest version number
    - "Production", "Staging", "Archived", "None" - resolve by MLflow stage
    - Numeric string like "1", "2", "3" - specific version number
    """
    valid_specifiers = {"latest", "production", "staging", "archived", "none"}
    if v.lower() in valid_specifiers or v.isdigit():
        return v
    raise ValueError(
        f"Invalid strategy_version '{v}'. Use 'latest', stage name, or version number"
    )

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

Validate all symbols use proper trading pair format.

Uses validate_trading_pair to ensure format like BTC/USDT or BTC/USDT:USDT. Prevents command injection and normalizes to uppercase.

Source code in libs/tradai-common/src/tradai/common/entities/backtest.py
@field_validator("symbols")
@classmethod
def validate_symbols_format(cls, v: list[str]) -> list[str]:
    """Validate all symbols use proper trading pair format.

    Uses validate_trading_pair to ensure format like BTC/USDT or BTC/USDT:USDT.
    Prevents command injection and normalizes to uppercase.
    """
    return [validate_trading_pair(s) for s in v]

validate_date_range() -> BacktestConfig

Ensure end_date is not before start_date.

Source code in libs/tradai-common/src/tradai/common/entities/backtest.py
@model_validator(mode="after")
def validate_date_range(self) -> BacktestConfig:
    """Ensure end_date is not before start_date."""
    start = datetime.strptime(self.start_date, "%Y-%m-%d")
    end = datetime.strptime(self.end_date, "%Y-%m-%d")
    if end < start:
        raise ValueError(
            f"end_date ({self.end_date}) must be >= start_date ({self.start_date})"
        )
    return self

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.

Attributes:

Name Type Description
total_trades int

Total number of trades executed

winning_trades int

Number of profitable trades

losing_trades int

Number of losing trades

draw_trades int

Number of break-even trades

total_profit float

Total profit in stake currency

total_profit_pct float

Total profit as percentage

profit_factor float | None

Ratio of gross profit to gross loss

sharpe_ratio float | None

Risk-adjusted return metric

sortino_ratio float | None

Downside risk-adjusted return

calmar_ratio float | None

Return over max drawdown

sqn float | None

System Quality Number

cagr float | None

Compound Annual Growth Rate

max_drawdown float | None

Maximum drawdown in stake currency

max_drawdown_pct float | None

Maximum drawdown as percentage

win_rate float | None

Percentage of winning trades

metrics dict[str, float]

Additional custom metrics

trades list[dict[str, Any]]

Raw trade data (limited)

raw_stats dict[str, Any]

Full raw stats from Freqtrade

mlflow_run_id str | None

MLflow run ID for traceability

config_artifact_uri str | None

URI of config artifact in MLflow

results_artifact_uri str | None

S3 URI of results file

job_id str | None

DynamoDB job ID for reverse lookup

git_commit str | None

Git commit SHA for reproducibility

resolved_strategy_version str | None

Resolved MLflow version number

config_version_id str | None

ConfigVersion ID used for this backtest

Source code in libs/tradai-common/src/tradai/common/entities/backtest.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.

    Attributes:
        total_trades: Total number of trades executed
        winning_trades: Number of profitable trades
        losing_trades: Number of losing trades
        draw_trades: Number of break-even trades
        total_profit: Total profit in stake currency
        total_profit_pct: Total profit as percentage
        profit_factor: Ratio of gross profit to gross loss
        sharpe_ratio: Risk-adjusted return metric
        sortino_ratio: Downside risk-adjusted return
        calmar_ratio: Return over max drawdown
        sqn: System Quality Number
        cagr: Compound Annual Growth Rate
        max_drawdown: Maximum drawdown in stake currency
        max_drawdown_pct: Maximum drawdown as percentage
        win_rate: Percentage of winning trades
        metrics: Additional custom metrics
        trades: Raw trade data (limited)
        raw_stats: Full raw stats from Freqtrade
        mlflow_run_id: MLflow run ID for traceability
        config_artifact_uri: URI of config artifact in MLflow
        results_artifact_uri: S3 URI of results file
        job_id: DynamoDB job ID for reverse lookup
        git_commit: Git commit SHA for reproducibility
        resolved_strategy_version: Resolved MLflow version number
        config_version_id: ConfigVersion ID used for this backtest
    """

    # 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)
    # Expected keys: pair, profit_ratio, profit_abs, open_date, close_date,
    # trade_duration, is_open, is_short, open_rate, close_rate, leverage
    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")

    # E2E traceability fields (P1.2/P1.3)
    job_id: str | None = Field(default=None, description="DynamoDB job ID for reverse lookup")
    git_commit: str | None = Field(default=None, description="Git commit SHA for reproducibility")

    # Internal run correlation (S3 paths and MLflow tags)
    tradai_run_id: str | None = Field(
        default=None, description="Internal run ID used for S3 paths and MLflow tags"
    )

    # Strategy versioning traceability (V-003)
    resolved_strategy_version: str | None = Field(
        default=None, description="Resolved MLflow version number (e.g., '3')"
    )
    config_version_id: str | None = Field(
        default=None, description="ConfigVersion ID used for this backtest"
    )

    # Exchange traceability
    exchange: str | None = Field(default=None, description="Exchange key (e.g., 'binance_futures')")

    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

    @field_validator("git_commit")
    @classmethod
    def validate_git_commit(cls, v: str | None) -> str | None:
        """Validate git commit SHA format (P1.3).

        Accepts:
        - None (no git info available)
        - "unknown" (not in a git repository)
        - 7-char short SHA (e.g., "abc1234")
        - 40-char full SHA (e.g., "abc1234567890...")
        """
        if v is None or v == "unknown":
            return v
        if len(v) not in (7, 40):
            raise ValueError(f"Invalid git commit SHA length: {len(v)} (expected 7 or 40)")
        if not all(c in "0123456789abcdef" for c in v.lower()):
            raise ValueError(f"Invalid git commit SHA (not hex): {v}")
        return v

    @classmethod
    def from_mlflow_metrics(
        cls,
        metrics: dict[str, float],
        mlflow_run_id: str,
        config_artifact_uri: str | None = None,
    ) -> BacktestResult:
        """Create BacktestResult from MLflow run metrics dict.

        Extracts standard metric keys from MLflow's flat metrics dictionary.

        Args:
            metrics: MLflow run metrics (e.g., run.data.metrics)
            mlflow_run_id: MLflow run ID for traceability
            config_artifact_uri: Optional URI of config artifact

        Returns:
            BacktestResult with metrics populated from the dict

        Example:
            >>> run = mlflow_client.get_run(run_id)
            >>> result = BacktestResult.from_mlflow_metrics(
            ...     run.data.metrics, run_id
            ... )
        """
        return cls(
            total_trades=int(metrics.get("total_trades", 0)),
            winning_trades=int(metrics.get("winning_trades", 0)),
            losing_trades=int(metrics.get("losing_trades", 0)),
            total_profit=metrics.get("total_profit", 0.0),
            total_profit_pct=metrics.get("total_profit_pct", 0.0),
            sharpe_ratio=metrics.get("sharpe_ratio"),
            profit_factor=metrics.get("profit_factor"),
            win_rate=metrics.get("win_rate"),
            max_drawdown_pct=metrics.get("max_drawdown"),
            mlflow_run_id=mlflow_run_id,
            config_artifact_uri=config_artifact_uri,
        )

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

Ensure winning trades <= total trades.

Source code in libs/tradai-common/src/tradai/common/entities/backtest.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

validate_git_commit(v: str | None) -> str | None classmethod

Validate git commit SHA format (P1.3).

Accepts: - None (no git info available) - "unknown" (not in a git repository) - 7-char short SHA (e.g., "abc1234") - 40-char full SHA (e.g., "abc1234567890...")

Source code in libs/tradai-common/src/tradai/common/entities/backtest.py
@field_validator("git_commit")
@classmethod
def validate_git_commit(cls, v: str | None) -> str | None:
    """Validate git commit SHA format (P1.3).

    Accepts:
    - None (no git info available)
    - "unknown" (not in a git repository)
    - 7-char short SHA (e.g., "abc1234")
    - 40-char full SHA (e.g., "abc1234567890...")
    """
    if v is None or v == "unknown":
        return v
    if len(v) not in (7, 40):
        raise ValueError(f"Invalid git commit SHA length: {len(v)} (expected 7 or 40)")
    if not all(c in "0123456789abcdef" for c in v.lower()):
        raise ValueError(f"Invalid git commit SHA (not hex): {v}")
    return v

from_mlflow_metrics(metrics: dict[str, float], mlflow_run_id: str, config_artifact_uri: str | None = None) -> BacktestResult classmethod

Create BacktestResult from MLflow run metrics dict.

Extracts standard metric keys from MLflow's flat metrics dictionary.

Parameters:

Name Type Description Default
metrics dict[str, float]

MLflow run metrics (e.g., run.data.metrics)

required
mlflow_run_id str

MLflow run ID for traceability

required
config_artifact_uri str | None

Optional URI of config artifact

None

Returns:

Type Description
BacktestResult

BacktestResult with metrics populated from the dict

Example

run = mlflow_client.get_run(run_id) result = BacktestResult.from_mlflow_metrics( ... run.data.metrics, run_id ... )

Source code in libs/tradai-common/src/tradai/common/entities/backtest.py
@classmethod
def from_mlflow_metrics(
    cls,
    metrics: dict[str, float],
    mlflow_run_id: str,
    config_artifact_uri: str | None = None,
) -> BacktestResult:
    """Create BacktestResult from MLflow run metrics dict.

    Extracts standard metric keys from MLflow's flat metrics dictionary.

    Args:
        metrics: MLflow run metrics (e.g., run.data.metrics)
        mlflow_run_id: MLflow run ID for traceability
        config_artifact_uri: Optional URI of config artifact

    Returns:
        BacktestResult with metrics populated from the dict

    Example:
        >>> run = mlflow_client.get_run(run_id)
        >>> result = BacktestResult.from_mlflow_metrics(
        ...     run.data.metrics, run_id
        ... )
    """
    return cls(
        total_trades=int(metrics.get("total_trades", 0)),
        winning_trades=int(metrics.get("winning_trades", 0)),
        losing_trades=int(metrics.get("losing_trades", 0)),
        total_profit=metrics.get("total_profit", 0.0),
        total_profit_pct=metrics.get("total_profit_pct", 0.0),
        sharpe_ratio=metrics.get("sharpe_ratio"),
        profit_factor=metrics.get("profit_factor"),
        win_rate=metrics.get("win_rate"),
        max_drawdown_pct=metrics.get("max_drawdown"),
        mlflow_run_id=mlflow_run_id,
        config_artifact_uri=config_artifact_uri,
    )

AWSConfig

Bases: BaseModel

AWS infrastructure configuration.

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

Attributes:

Name Type Description
region str

AWS region (e.g., "us-east-1")

account_id str

12-digit AWS account ID

execution_role_arn str

IAM role ARN for task execution

task_role_arn str

IAM role ARN for task

ecs_cluster str

ECS cluster name

subnets list[str]

List of subnet IDs

security_groups list[str]

List of security group IDs

Example

config = AWSConfig( ... region="us-east-1", ... account_id="123456789012", ... execution_role_arn="arn:aws:iam::123456789012:role/ecsTaskExecutionRole", ... task_role_arn="arn:aws:iam::123456789012:role/ecsTaskRole", ... ecs_cluster="tradai-cluster", ... subnets=["subnet-abc123"], ... security_groups=["sg-xyz789"], ... )

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

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

    Attributes:
        region: AWS region (e.g., "us-east-1")
        account_id: 12-digit AWS account ID
        execution_role_arn: IAM role ARN for task execution
        task_role_arn: IAM role ARN for task
        ecs_cluster: ECS cluster name
        subnets: List of subnet IDs
        security_groups: List of security group IDs

    Example:
        >>> config = AWSConfig(
        ...     region="us-east-1",
        ...     account_id="123456789012",
        ...     execution_role_arn="arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
        ...     task_role_arn="arn:aws:iam::123456789012:role/ecsTaskRole",
        ...     ecs_cluster="tradai-cluster",
        ...     subnets=["subnet-abc123"],
        ...     security_groups=["sg-xyz789"],
        ... )
    """

    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/aws.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/aws.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.

Attributes:

Name Type Description
bucket str

S3 bucket name (3-63 characters)

key str

S3 object key

Example

path = S3Path.parse("s3://my-bucket/path/to/file.json") path.bucket 'my-bucket' path.key 'path/to/file.json' str(path) 's3://my-bucket/path/to/file.json'

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

    Immutable representation of an S3 URI.

    Attributes:
        bucket: S3 bucket name (3-63 characters)
        key: S3 object key

    Example:
        >>> path = S3Path.parse("s3://my-bucket/path/to/file.json")
        >>> path.bucket
        'my-bucket'
        >>> path.key
        'path/to/file.json'
        >>> str(path)
        's3://my-bucket/path/to/file.json'
    """

    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/aws.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/aws.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/exchange.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/exchange.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/exchange.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/exchange.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/exchange.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]}"
    )