Skip to content

Strategy Development Lifecycle

Complete guide for developing, testing, and deploying trading strategies with TradAI.

Lifecycle Overview

graph LR
    Create --> Develop --> Test --> Backtest --> Register
    Register --> Stage --> ABTest["A/B Test"] --> Promote --> Deploy

Phase 1: Create Strategy

See Creating a Trading Strategy for full setup instructions (wizard, CLI options, cookiecutter, generated file structure, and implementation guide).

Key decision: Choose where to create your strategy:

Repository Purpose When to Use
tradai-uv Platform development Testing framework, infrastructure
tradai-strategies Production strategies Real trading strategies

For tradai-strategies, run just codeartifact-login first (12-hour token), then just new StrategyName.


Phase 2: Develop Strategy

Required Implementation

Every strategy must extend TradAIStrategy and implement these methods:

from tradai.strategy import TradAIStrategy, StrategyMetadata
from tradai.strategy.enums import StrategyCategory

class MomentumStrategy(TradAIStrategy):
    """Momentum-based trading strategy."""

    # Freqtrade required attributes
    timeframe = '1h'
    stoploss = -0.08
    trailing_stop = True
    trailing_stop_positive = 0.01
    trailing_stop_positive_offset = 0.02

    # TradAI REQUIRED: Metadata
    def get_metadata(self) -> StrategyMetadata:
        return StrategyMetadata(
            name="MomentumStrategy",
            version="1.0.0",
            description="RSI-based momentum strategy",
            timeframe=self.timeframe,
            category=StrategyCategory.MOMENTUM,
            tags=["rsi", "momentum", "btc"],
        )

    # Freqtrade REQUIRED: Calculate indicators
    def populate_indicators(self, dataframe, metadata):
        dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14)
        dataframe['ema_fast'] = ta.EMA(dataframe['close'], timeperiod=12)
        dataframe['ema_slow'] = ta.EMA(dataframe['close'], timeperiod=26)
        return dataframe

    # Freqtrade REQUIRED: Define entry signals
    def populate_entry_trend(self, dataframe, metadata):
        # Long entry
        dataframe.loc[
            (dataframe['rsi'] < 30) &
            (dataframe['ema_fast'] > dataframe['ema_slow']),
            'enter_long'
        ] = 1

        # Short entry (if enabled)
        dataframe.loc[
            (dataframe['rsi'] > 70) &
            (dataframe['ema_fast'] < dataframe['ema_slow']),
            'enter_short'
        ] = 1

        return dataframe

    # Freqtrade REQUIRED: Define exit signals
    def populate_exit_trend(self, dataframe, metadata):
        dataframe.loc[
            (dataframe['rsi'] > 70),
            'exit_long'
        ] = 1

        dataframe.loc[
            (dataframe['rsi'] < 30),
            'exit_short'
        ] = 1

        return dataframe

Strategy Categories

Category Use Case Typical Timeframe
TREND_FOLLOWING Riding directional moves 1h, 4h, 1d
MOMENTUM Quick momentum captures 15m, 1h
MEAN_REVERSION Fading extreme moves 1h, 4h
BREAKOUT Range expansion 15m, 1h
ARBITRAGE Price discrepancies 1m

Note: Categories map to StrategyCategory enum in tradai.strategy.enums.

ML-Enabled Strategies (FreqAI)

For ML strategies, implement additional methods:

class TrendMLStrategy(TradAIStrategy):
    """ML-enhanced trend following strategy."""

    def feature_engineering_expand_all(self, dataframe, **kwargs):
        """Create features for ML model."""
        dataframe['rsi'] = ta.RSI(dataframe['close'])
        dataframe['macd'] = ta.MACD(dataframe['close'])['macd']
        # Add more features...
        return dataframe

    def feature_engineering_expand_basic(self, dataframe, **kwargs):
        """Basic features using %change."""
        dataframe['pct_change'] = dataframe['close'].pct_change()
        return dataframe

Phase 3: Test Strategy

Unit Tests

# In tradai-strategies repo:
just test-strategy momentum-strategy

# Or run specific tests:
cd strategies/momentum-strategy && uv run pytest tests/ -k "test_metadata"

Test Structure

# tests/test_strategy.py
import pytest
from momentum_strategy.strategy import MomentumStrategy

def test_metadata():
    """Test strategy metadata is valid."""
    strategy = MomentumStrategy({})
    metadata = strategy.get_metadata()

    assert metadata.name == "MomentumStrategy"
    assert metadata.version == "1.0.0"
    assert metadata.timeframe == "1h"

def test_indicators(sample_dataframe):
    """Test indicators are populated."""
    strategy = MomentumStrategy({})
    result = strategy.populate_indicators(sample_dataframe, {})

    assert 'rsi' in result.columns
    assert 'ema_fast' in result.columns
    assert 'ema_slow' in result.columns

def test_entry_signals(sample_dataframe_with_indicators):
    """Test entry signals are generated."""
    strategy = MomentumStrategy({})
    result = strategy.populate_entry_trend(sample_dataframe_with_indicators, {})

    assert 'enter_long' in result.columns
    assert result['enter_long'].sum() > 0  # At least some signals

Lint and Type Check

# Lint entire project
just lint

# Lint specific strategy
tradai strategy lint ./momentum-strategy/

Phase 4: Backtest

Running Backtests

# Quick smoke backtest (30 days)
tradai backtest quick MomentumStrategy --period 30d --local

# Full backtest (custom date range)
tradai backtest quick MomentumStrategy --timerange 20240101-20241201 --local

Via API

# Submit backtest
curl -X POST http://localhost:8000/api/v1/backtests \
  -H "Content-Type: application/json" \
  -d '{
    "config": {
      "strategy": "MomentumStrategy",
      "pairs": ["BTC/USDT:USDT"],
      "timeframe": "1h",
      "date_range": {
        "start": "2024-01-01T00:00:00Z",
        "end": "2024-12-01T00:00:00Z"
      },
      "stake_amount": 100.0,
      "dry_run_wallet": 10000.0
    },
    "experiment_name": "momentum-v1-btc",
    "priority": 5
  }'

# Check status
curl http://localhost:8000/api/v1/backtests/${JOB_ID} | jq

Quality Thresholds

Metric Minimum Target
Sharpe Ratio > 1.0 > 1.5
Profit Factor > 1.2 > 1.5
Max Drawdown < 20% < 15%
Win Rate > 40% > 50%
Total Trades > 50 > 100

Phase 5: Register

Automatic Registration (CI/CD)

When you tag a release, CI automatically: 1. Runs full backtest 2. Builds Docker image 3. Pushes to ECR 4. Registers to MLflow Model Registry

# Tag release
git tag momentum-strategy-v1.0.0
git push --tags

Manual Registration

# Via CLI — `--oos` marks the backtest result as out-of-sample so the strict
# PerformanceThresholds gate (Issue #460) accepts it.
tradai strategy register ./momentum-strategy --env dev --count 1

strategy-service register-strategy MomentumStrategy \
  --backtest-run abc123 \
  --docker-image 123456789.dkr.ecr.eu-central-1.amazonaws.com/momentum:v1 \
  --oos \
  --oos-period "2024-07-01..2024-12-31"

# Via API
curl -X POST http://localhost:8003/api/v1/strategies/MomentumStrategy/register \
  -H "Content-Type: application/json" \
  -d '{
    "backtest_run_id": "mlflow_run_id_here",
    "docker_image_uri": "ecr_image_uri_here",
    "description": "Initial release",
    "thresholds": {
      "min_sharpe": 1.0,
      "min_profit_factor": 1.2,
      "max_drawdown": 20.0
    }
  }'

Strict out-of-sample gate (Issue #460)

PerformanceThresholds.require_out_of_sample defaults to True. Registration and promotion will reject any BacktestResult whose is_out_of_sample is not True with:

Result is not marked out-of-sample (is_out_of_sample=None); refusing promotion of in-sample metrics

Recovery paths:

  1. Re-run the backtest with BacktestConfig(is_out_of_sample=True, out_of_sample_period="...") so BacktestMLflowLogger tags the run.
  2. Pass --oos / --oos-period on the CLI register-strategy command (the API route reads the same MLflow tags).
  3. Construct PerformanceThresholds(require_out_of_sample=False) with explicit justification — NOT recommended for production promotion.

The v4 promotion gate (tradai.common.promotion.promote) enforces this at promote time: the challenger Release's measured BacktestResult must carry is_out_of_sample=True (the TRADAI_GATE_REQUIRE_OOS floor, on by default); an in-sample or untagged result is rejected with a loud gate failure, not a silent false claim.


Phase 6: Cutover-readiness

v4: there is no MLflow Staging stage transition. Registration (Phase 5) composes an immutable Release and sets the ACTIVE pointer on first write; "staging" is just a separate environment with its own ACTIVE pointer. Before running a Release in dry-run, assert its (strategy, env) ACTIVE pointer names a cutover-ready Release. See the v4 Cutover & Promotion Runbook.

# Confirm the ACTIVE pointer names a cutover-ready Release (non-zero exit otherwise)
tradai strategy assert-cutover-ready MomentumStrategy --env dev --json

# Repoint ACTIVE onto a specific cutover-ready Release if needed (audited admin op)
tradai strategy repoint-active MomentumStrategy "DEV#REL#01ARZ..." --env dev

The legacy tradai strategy stage command and the /api/v1/strategies/{name}/stage endpoint (the MLflow Staging alias) are removed in v4 — what trades is the ACTIVE Release, not an MLflow stage.

Staging Validation

Run the strategy in dry-run mode for 7 days: - Monitor for errors - Validate signal generation - Check resource usage


Phase 7: A/B Test (Optional)

v4: A/B testing is an offline comparison that informs which Release you promote — it does not promote a model alias. There is no champion/challenger MLflow-alias swap; promotion is always the epoch-guarded ACTIVE-pointer swap in Phase 8. The A/B endpoints below compare two registered model versions to pick the challenger Release worth gating.

Compare a new model version against the currently-active one.

# Create A/B test
curl -X POST http://localhost:8003/api/v1/models/MomentumStrategy/challenges \
  -H "Content-Type: application/json" \
  -d '{
    "champion_version": "1",
    "challenger_version": "2",
    "min_trades": 100,
    "max_duration_hours": 168,
    "enable_canary": true
  }'

# Check results
curl http://localhost:8003/api/v1/challenges/${CHALLENGE_ID}/evaluate | jq

Traffic Split (Canary)

Stage Champion Challenger
1 95% 5%
2 90% 10%
3 75% 25%
Final 50% 50%

Phase 8: Promote

v4: promotion is an epoch-guarded swap of the ACTIVE pointer onto a challenger Release, gated by PromotionPolicy — not an MLflow stage transition. Stage aliases (champion/staging/Production) are retired. See the v4 Cutover & Promotion Runbook.

# Via CLI: gate (floors + regression + cooldown) + epoch-guarded swap, with an atomic audit.
# Exit 2 + a rejection audit if the gate rejects the challenger.
tradai strategy promote MomentumStrategy "DEV#REL#01ARZ..." --env dev

# Via API (Backend pointer-plane route)
curl -X POST http://localhost:8000/api/v1/strategies/MomentumStrategy/promote \
  -H "Content-Type: application/json" \
  -d '{ "release_deployment_id": "DEV#REL#01ARZ...", "environment": "dev" }'

Promotion Requirements

  • Staging validation passed (7 days)
  • Challenger registered as a cutover-ready Release (assert-cutover-ready)
  • Promotion gate floors + regression pass (or thresholds adjusted via TRADAI_GATE_*)
  • No critical errors in logs
  • Manual approval (promotion is manual by default — PIN advance policy)

Phase 9: Deploy

Deploy the strategy to live trading.

# Deploy to ECS
tradai deploy strategy momentum-strategy \
  --env prod \
  --version v1.0.0 \
  --mode dry-run  # or 'live'

# Via API
curl -X POST http://localhost:8000/api/v1/strategies/MomentumStrategy/run \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "dry-run",
    "timeframe": "1h",
    "symbols": ["BTC/USDT:USDT"],
    "config_overrides": {
      "stake_amount": 100
    }
  }'

Monitoring

# List running instances
curl http://localhost:8000/api/v1/strategies/MomentumStrategy/instances | jq

# View logs
curl http://localhost:8000/api/v1/strategies/MomentumStrategy/instances/${ID}/logs | jq

# Stop instance
curl -X POST http://localhost:8000/api/v1/strategies/MomentumStrategy/instances/${ID}/stop

Version Management

Semantic Versioning

<major>.<minor>.<patch>

1.0.0 - Initial release
1.1.0 - New feature (backward compatible)
1.1.1 - Bug fix
2.0.0 - Breaking change

Tagging Convention

# Format: <strategy-slug>-v<version>
git tag momentum-strategy-v1.0.0
git tag trend-ml-v2.1.3

Release plane (v4 — replaces MLflow stages)

There are no None/Staging/Production/Archived MLflow stages in v4. What trades is decided by the ACTIVE pointer per (strategy, environment):

Concept Description
Release (ENV#REL#<ulid>) Immutable composition: image digest + content-addressed resolved config + per-slot model bindings + gate snapshot. Many per strategy.
ACTIVE pointer (ENV#ACTIVE, epoch) Names the one Release that trades for a (strategy, env). Moved only by an epoch-guarded swap.
Stage (dashboard) Derived from whether a staging/prod ACTIVE pointer exists — not stored on the model.
Rollback Repoint ACTIVE to a prior Release (tradai strategy rollback).

Rollback

v4: rollback repoints the ACTIVE pointer to a prior Release — bypassing the gate and cooldown (incident response), still epoch-guarded and audited. It is not an MLflow stage/registry transition. See the v4 Cutover & Promotion Runbook.

If issues are detected in production:

# Via CLI: repoint ACTIVE to a known-good prior Release (no gate, no cooldown)
tradai strategy rollback MomentumStrategy "DEV#REL#01PRIOR..." --env dev

# Via API (Backend pointer-plane route)
curl -X POST http://localhost:8000/api/v1/strategies/MomentumStrategy/rollback \
  -H "Content-Type: application/json" \
  -d '{ "release_deployment_id": "DEV#REL#01PRIOR...", "environment": "dev" }'

An ECS deployment (task/image) rollback is a separate concern handled by tradai deploy rollback <name> --env <env>; it does not move the ACTIVE Release pointer.


Strategy Development:

Testing & Validation:

Deployment & Operations:

Reference: