Skip to content

Configuration & Model Versioning

How strategy configurations and ML models are versioned, promoted, and managed across environments.

graph LR
    subgraph Config["Strategy Config"]
        C1["tradai.yaml"] --> C2["S3 Upload"]
        C2 --> C3["DRAFT"]
        C3 --> C4["ACTIVE"]
        C4 --> C5["DEPRECATED"]
    end

    subgraph Model["ML Model + Release"]
        M1["Training"] --> M2["MLflow Registry - (artifact/run store)"]
        M2 --> M3["register() - composes Release - (ENV#REL#...)"]
        M3 --> M4["ACTIVE pointer - (ENV#ACTIVE, epoch)"]
        M4 -->|promote / rollback| M4
    end

    C4 -.->|resolved into the Release| M3

Strategy Configuration

tradai.yaml

Every strategy has a tradai.yaml at its root that defines how TradAI services interact with it:

strategy:
  name: "MyStrategy"
  version: "1.0.0"
  entry_point: "mystrategy.strategy:MyStrategy"
  category: "momentum"
  timeframe: "1h"

strategy_service:
  source:
    KIND: Binance
  adapter:
    KIND: AWS
    bucket_name: tradai-data
    library: ohlcv
  defaults:
    timerange: "20240101-20241201"
    symbols:
      - "BTC/USDT:USDT"
      - "ETH/USDT:USDT"
    stake_amount: 1000
    max_open_trades: 3

mlflow:
  tracking_uri: ${MLFLOW_TRACKING_URI:-http://localhost:5001}
  experiment_name: "strategies/mystrategy"
  auto_log_params: true

optimization:
  defaults:
    epochs: 100
    loss_function: sharpe
    spaces: [buy, sell]
  presets:
    quick:
      epochs: 50
      spaces: [buy]
    standard:
      epochs: 200
      spaces: [buy, sell]
    production:
      epochs: 1000
      spaces: [buy, sell, roi, stoploss, trailing]
      walk_forward: true

deployment:
  ecs:
    cpu: 512
    memory: 1024

Config Storage

Configurations are stored in S3 and versioned in DynamoDB:

graph TD
    YAML["tradai.yaml"] -->|upload| S3["S3 Bucket - tradai-configs"]
    S3 -->|version| DDB["DynamoDB - config-versions"]
    DDB -->|ACTIVE version| ECS["ECS Task"]

    ENV[".env / Secrets Manager"] -->|merge| Merge["ConfigMergeService"]
    S3 -->|base config| Merge
    Merge --> ECS
Component Location Purpose
tradai.yaml Strategy repo root Source of truth for strategy config
S3 bucket tradai-configs-{env} Persisted config versions
DynamoDB table tradai-config-versions-{env} Version registry with lifecycle tracking
Secrets Manager AWS Secrets Manager Exchange credentials, API keys

Config Version Lifecycle

Each config version follows a strict lifecycle:

stateDiagram-v2
    [*] --> DRAFT : create_version()
    DRAFT --> ACTIVE : activate()
    ACTIVE --> DEPRECATED : new version activated
    DEPRECATED --> [*] : TTL auto-cleanup (90 days)

    note right of DRAFT : Not yet validated
    note right of ACTIVE : Currently deployed (one per strategy)
    note right of DEPRECATED : Superseded, auto-deleted after 90d

Key rules:

  • Only one ACTIVE version per strategy at any time
  • Activating a new version automatically deprecates the previous one
  • Deprecated versions have a 90-day TTL for auto-cleanup in DynamoDB
  • Versions are content-addressable (SHA256 hash) -- duplicate configs are detected

Config Version Entity

Each version is tracked with these fields:

Field Type Description
strategy_name string Partition key (e.g., "PascalStrategy")
config_id string Sort key: v{version:05d}-{hash[:8]} (5-digit zero-padded so lex sort matches numeric order)
config_hash string SHA256 of normalized config content
config_data dict Frozen config content
status enum DRAFT, ACTIVE, or DEPRECATED
version_number int Sequential version per strategy
created_at datetime When created
deployed_at datetime When activated (null if DRAFT)
superseded_by string config_id of newer version (if deprecated)

Managing Config Versions

CLI

# Create a new config version (starts as DRAFT)
tradai config create MyStrategy --data '{"timeframe":"1h","stoploss":-0.03}'
tradai config create MyStrategy --file config.json

# List versions for a strategy
tradai config list MyStrategy
tradai config list MyStrategy --status active

# Show a specific version
tradai config show MyStrategy v00001-a3b2c1d4

# Activate a version (auto-deprecates previous ACTIVE)
tradai config activate MyStrategy v00001-a3b2c1d4

# Deprecate a version
tradai config deprecate MyStrategy v00001-a3b2c1d4

# Submit backtest with a config version
tradai backtest quick MyStrategy --config-version v00001-a3b2c1d4
tradai backtest quick MyStrategy --config-version ACTIVE

API

Method Endpoint Description
POST /api/v1/configs Create config version
GET /api/v1/configs/{strategy} List versions
GET /api/v1/configs/{strategy}/{config_id} Get version
POST /api/v1/configs/{strategy}/{config_id}/activate Activate
POST /api/v1/configs/{strategy}/{config_id}/deprecate Deprecate

Python SDK

from tradai.common.config.service import ConfigVersionService

service = ConfigVersionService(table_name="tradai-config-versions-dev")

# Create a new version (starts as DRAFT)
version = service.create_version(
    strategy_name="MyStrategy",
    config_data={"timeframe": "1h", "symbols": ["BTC/USDT:USDT"]},
    description="Updated symbols list",
)

# Activate it (auto-deprecates previous ACTIVE version)
active = service.activate("MyStrategy", version.config_id)

# List all versions for a strategy
versions = service.list_versions("MyStrategy")

# Get the currently active version
current = service.get_active("MyStrategy")

Config Data Merge

When a backtest is submitted with config_version_id, the active config's config_data is merged as defaults into the backtest parameters. Explicit parameters always win:

config_data: {"stoploss": -0.03, "stake_amount": 150}
backtest request: {"stake_amount": 1000}
result: {"stoploss": -0.03, "stake_amount": 1000}  # explicit wins

Protected fields (strategy, task_definition) cannot be overridden by config_data.

Config Version Propagation

When a backtest runs with a config version, the config_version_id is tracked across 5 stores:

# Store How
1 Step Functions input Backend passes in SF execution input
2 ECS container env SF sets RUN_CONFIG; config_version_id rides inside it (#754)
3 DynamoDB job record Extracted from result to top-level field
4 S3 result.json Written by strategy container to tradai-results-{env}
5 MLflow run tag BacktestMLflowLogger sets config_version_id tag

See Config Versioning Verification Guide for step-by-step verification commands.

Config Loading at Runtime

When a strategy container starts, configs are loaded and merged from multiple sources:

graph TD
    S3["S3 Config - (base)"] --> Loader["StrategyConfigLoader"]
    MLflow["MLflow Tags - (model params)"] --> Loader
    ENV["Environment Vars - (overrides)"] --> Loader
    Loader --> Merge["ConfigMergeService"]
    Merge --> Validate["Validation"]
    Validate -->|pass| Config["StrategyConfig - (runtime)"]
    Validate -->|fail| Error["Startup Error"]

Priority order (highest wins):

  1. Environment variables
  2. MLflow model tags
  3. S3 stored config
  4. tradai.yaml defaults

Model Versioning (MLflow)

v4: MLflow is the artifact/run store — it records model versions, params, metrics, and tags. It is no longer the deployment lifecycle. What trades is decided by the Release / ACTIVE-pointer plane (below + the v4 Cutover & Promotion Runbook), not by MLflow None/Staging/Production/Archived stages. The strategy-as-model alias plane was retired in v4 P6.

Deployment lifecycle (authoritative: Release / ACTIVE pointer)

stateDiagram-v2
    [*] --> Registered : register() composes a Release (ENV#REL#...)
    Registered --> Active : first ACTIVE pointer set (first write)
    Active --> Active : promote() — gate + epoch-guarded swap to a challenger Release
    Active --> Active : rollback() — repoint ACTIVE to a prior Release (no gate/cooldown)

    note right of Registered : Immutable Release: image digest + resolved-config hash + slot bindings + gate snapshot
    note right of Active : ACTIVE pointer (ENV#ACTIVE, epoch) names the one Release that trades per (strategy, env)
Concept Description Who moves it
Release (ENV#REL#<ulid>) Immutable composition: image digest + content-addressed resolved config + per-slot model bindings + gate snapshot register (composes; first write sets ACTIVE)
ACTIVE pointer (ENV#ACTIVE, epoch) Names the one Release that trades for a (strategy, env) tradai strategy promote (gated swap) / rollback / repoint-active

The MLflow ModelStage enum (None/Staging/Production/Archived) still exists as model- version metadata, but it no longer governs what trades and there is no stage-transition promotion path.

Model Registration

After a backtest or training run, models are automatically registered:

sequenceDiagram
    participant ECS as ECS Task - (Freqtrade)
    participant MLflow as MLflow - Registry
    participant DDB as DynamoDB - State

    ECS->>MLflow: Log metrics + params
    ECS->>MLflow: Log model artifacts
    ECS->>MLflow: Register model version
    MLflow-->>ECS: Version number
    ECS->>DDB: Update job status
    ECS->>MLflow: Tag with git_commit, strategy_name

The ModelRegistrar handles this automatically:

from tradai.common.entrypoint.training.model_registrar import ModelRegistrar

registrar = ModelRegistrar(mlflow_adapter=adapter)
result = registrar.register(config=training_config, result=training_result)
# result.model_version is now set

Promotion gate (v4)

v4: Promotion is no longer an MLflow stage transition (champion/staging/Production aliases are retired). A challenger is an immutable Release; promotion runs a gate and, on pass, performs an epoch-guarded swap of the single ACTIVE pointer. See v4 Cutover & Promotion Runbook.

graph TD
    Challenger["Challenger Release - (gate_snapshot)"] --> Gate["Promotion gate"]
    Baseline["Current ACTIVE Release - (baseline gate_snapshot)"] --> Gate
    Gate --> Decision{"Pass?"}
    Decision -->|"yes"| Swap["Epoch-guarded pointer swap - (+ atomic audit)"]
    Decision -->|"no"| Reject["Rejection audit - (ACTIVE unchanged)"]

The gate (PromotionPolicy) applies floors (absolute), regression vs the current ACTIVE (from the 2nd release per (strategy, env)), and a cooldown. Every threshold is overridable via its TRADAI_GATE_* env var (see below). Signed-off launch defaults:

Check Default TRADAI_GATE_* override
sharpe >= MIN_SHARPE 1.0 TRADAI_GATE_MIN_SHARPE
max_drawdown_pct <= MAX_DD 20.0 TRADAI_GATE_MAX_DD
total_trades >= MIN_TRADES 50 TRADAI_GATE_MIN_TRADES
out-of-sample required true TRADAI_GATE_REQUIRE_OOS
profit_factor >= baseline - ε ε=0.1 TRADAI_GATE_PF_EPSILON
max_drawdown_pct <= baseline + δ δ=2.0pp TRADAI_GATE_MAX_DD_DELTA
cooldown since last promotion 24h TRADAI_GATE_COOLDOWN_HOURS

TRADAI_GATE_REQUIRE_OOS footgun: only the literal "false" disables the out-of-sample requirement. Any other value (e.g. "0", "no") still requires OOS.

v4 activation flags

Flag Default Effect
LOCAL_DEV_OVERRIDE unset true → runtime uses the legacy S3-by-name config load (dev only). Unset → resolves config from the ACTIVE Release (authoritative, fail-closed).
RELEASE_STRICT_GATE true Registration fail-closes Release composition (requires mlflow_run_id + a contract-bearing version + a resolvable immutable image digest). Registration scope only — not the runtime resolver or the promotion gate.
REQUIRE_IMAGE_DIGEST false true → runtime refuses to trade if RUNNING_IMAGE_DIGEST is absent. A mismatch always fails closed regardless.
RELEASE_REQUIRE_LINEAGE false true → registration rejects a Release whose contract-bearing slots lack training-data data_hash. Independent of RELEASE_STRICT_GATE.

CLI Commands

# Verify the ACTIVE pointer names a cutover-ready Release (gate before the runtime flip)
tradai strategy assert-cutover-ready MyStrategy --env dev

# Promote a challenger Release: gate + epoch-guarded swap (exit 2 + rejection audit on fail)
tradai strategy promote MyStrategy "DEV#REL#01ARZ..." --env dev

# Roll ACTIVE back to a prior Release (bypasses gate + cooldown; still audited)
tradai strategy rollback MyStrategy "DEV#REL#01PRIOR..." --env dev

# Audited manual repoint onto a cutover-ready Release (admin op)
tradai strategy repoint-active MyStrategy "DEV#REL#01ARZ..." --env dev

API Endpoints

The Backend exposes the pointer-plane promotion APIs:

Method Path Description
POST /api/v1/strategies/{name}/promote Gate + epoch-guarded swap of the ACTIVE pointer
POST /api/v1/strategies/{name}/rollback Repoint ACTIVE to a prior Release (incident)
POST /api/v1/catalog/rebuild Rebuild the catalog-index cache from the pointer plane

Automated Retraining Pipeline

The retraining workflow is orchestrated by Step Functions:

graph TD
    Trigger["Trigger - Schedule / Drift / Manual"] --> Check["Check Retraining - Needed?"]
    Check -->|"yes"| Train["Train Model - (ECS + FreqAI)"]
    Check -->|"no"| Skip["Skip"]
    Train --> Register["Register Release - (compose + ACTIVE pointer)"]
    Register --> Promote["Promote - (gate + epoch-guarded swap)"]
    Promote --> Notify["Notify - (SNS + Slack)"]

v4: there is no compare-models/promote-model/model-rollback stage-transition path anymore; the challenger is registered as a Release and promoted via the gate. The drift-monitor and retraining-scheduler Lambdas remain (trigger side).


Environment-Specific Configuration

Settings Hierarchy

Each service uses Pydantic settings with environment variable prefixes:

Service Prefix Key Settings
Backend BACKEND_ BACKEND_EXECUTOR_MODE, BACKEND_BACKTEST_QUEUE_URL
Strategy Service STRATEGY_SERVICE_ STRATEGY_SERVICE_MLFLOW_TRACKING_URI, STRATEGY_SERVICE_STRATEGY_PATH
Data Collection DATA_COLLECTION_ DATA_COLLECTION_EXCHANGES, DATA_COLLECTION_ARCTIC_S3_BUCKET

Settings Mixins

Common settings are shared via mixins:

# MLflow settings (shared by Strategy Service and Backend)
class MLflowSettingsMixin:
    mlflow_tracking_uri: str   # MLFLOW_TRACKING_URI
    mlflow_username: str       # MLFLOW_USERNAME
    mlflow_password: str       # MLFLOW_PASSWORD

# ArcticDB settings (shared by Data Collection and Strategy Service)
class ArcticSettingsMixin:
    arctic_s3_bucket: str      # ARCTIC_S3_BUCKET
    arctic_library_name: str   # ARCTIC_LIBRARY_NAME (default: "ohlcv")
    arctic_s3_endpoint: str    # ARCTIC_S3_ENDPOINT

Per-Environment Differences

Setting Dev Staging Prod
Executor mode local sqs stepfunctions
RDS instance db.t4g.micro db.t4g.micro db.t4g.small
ECS launch type EC2 (consolidated) EC2 (consolidated) Fargate
Log retention 30 days 30 days 90 days
Deletion protection Off Off On
MLflow URL http://localhost:5001 Service Discovery Service Discovery

Traceability

Every operation is traceable across the entire system:

graph LR
    TraceID["trace_id"] --> Backend["Backend API"]
    TraceID --> SQS["SQS Message"]
    TraceID --> SF["Step Functions"]
    TraceID --> ECS["ECS Task"]
    TraceID --> MLflow["MLflow Run"]
    TraceID --> DDB["DynamoDB - job record"]

    JobID["job_id"] --> DDB
    JobID --> S3["S3 Results"]

    RunID["mlflow_run_id"] --> MLflow
    RunID --> DDB

    GitSHA["git_commit"] --> MLflow
    GitSHA --> DDB
Field Description Where stored
trace_id End-to-end correlation ID DynamoDB, Step Functions input, ECS env
job_id DynamoDB job identifier DynamoDB, S3 result paths
mlflow_run_id MLflow experiment run DynamoDB, BacktestResult, MLflow
git_commit Code version SHA BacktestResult, MLflow tags

Quick Reference

Config Version Commands

Command Description
tradai config create STRATEGY --data JSON Create config version from JSON
tradai config create STRATEGY --file FILE Create config version from file
tradai config list STRATEGY List versions for strategy
tradai config show STRATEGY CONFIG_ID Show version details
tradai config activate STRATEGY CONFIG_ID Activate version
tradai config deprecate STRATEGY CONFIG_ID Deprecate version
tradai backtest quick STRATEGY --config-version ID Backtest with config version

Release / promotion commands (v4)

Command Description
tradai strategy list List registered strategies
tradai strategy register ... Compose an immutable Release (first write sets ACTIVE)
tradai strategy assert-cutover-ready NAME -e ENV Verify the ACTIVE pointer names a cutover-ready Release
tradai strategy repoint-active NAME "ENV#REL#..." -e ENV Audited manual repoint of ACTIVE onto a cutover-ready Release
tradai strategy promote NAME "ENV#REL#..." -e ENV Gate a challenger Release + epoch-guarded ACTIVE swap
tradai strategy rollback NAME "ENV#REL#..." -e ENV Repoint ACTIVE to a prior Release (no gate/cooldown; audited)
tradai strategy promote-cross-env NAME measurement.json --source-env E --target-env E Re-measure + gate + swap a source-env Release into the target env
tradai deploy strategy ./path --env dev Deploy strategy task to ECS
tradai deploy rollback NAME --env dev Roll back the ECS deployment (task/image) — not the ACTIVE Release pointer

Key Source Files

Component Path
ConfigVersion entity libs/tradai-common/src/tradai/common/entities/config_version.py
ConfigVersionService libs/tradai-common/src/tradai/common/config/service.py
S3ConfigRepository libs/tradai-common/src/tradai/common/aws/s3_config_repository.py
ConfigMergeService libs/tradai-common/src/tradai/common/config/merge.py
StrategyConfigLoader libs/tradai-common/src/tradai/common/config/loader.py
ModelStage enum libs/tradai-common/src/tradai/common/entities/mlflow.py
MLflowAdapter libs/tradai-common/src/tradai/common/mlflow/adapter.py
ModelComparator libs/tradai-common/src/tradai/common/model_comparison/comparator.py
ModelRegistrar libs/tradai-common/src/tradai/common/entrypoint/training/model_registrar.py
Promotion routes services/strategy-service/src/tradai/strategy_service/api/promotion_routes.py
Config routes services/strategy-service/src/tradai/strategy_service/api/config_routes.py

See Also