Skip to content

tradai-strategy

Strategy framework, validation, and metadata for the TradAI platform.

from tradai.strategy import TradAIStrategy, StrategyMetadata, StrategyValidator

Base Strategy

TradAIStrategy

Base class for all TradAI trading strategies, extending Freqtrade's IStrategy.

from tradai.strategy import TradAIStrategy, StrategyMetadata
import talib.abstract as ta


class MyStrategy(TradAIStrategy):
    """Simple trend-following strategy."""

    metadata = StrategyMetadata(
        name="MyStrategy",
        version="1.0.0",
        author="TradAI",
        category="trend_following",
        description="EMA crossover strategy",
    )

    # Strategy parameters
    ema_fast = 12
    ema_slow = 26

    # Freqtrade settings
    minimal_roi = {"0": 0.1}
    stoploss = -0.05
    timeframe = "1h"

    def populate_indicators(self, dataframe, metadata):
        """Calculate technical indicators."""
        dataframe['ema_fast'] = ta.EMA(dataframe, timeperiod=self.ema_fast)
        dataframe['ema_slow'] = ta.EMA(dataframe, timeperiod=self.ema_slow)
        return dataframe

    def populate_entry_trend(self, dataframe, metadata):
        """Define entry signals."""
        dataframe.loc[
            (dataframe['ema_fast'] > dataframe['ema_slow']) &
            (dataframe['ema_fast'].shift(1) <= dataframe['ema_slow'].shift(1)),
            'enter_long'
        ] = 1
        return dataframe

    def populate_exit_trend(self, dataframe, metadata):
        """Define exit signals."""
        dataframe.loc[
            (dataframe['ema_fast'] < dataframe['ema_slow']) &
            (dataframe['ema_fast'].shift(1) >= dataframe['ema_slow'].shift(1)),
            'exit_long'
        ] = 1
        return dataframe

Required Methods:

Method Description
populate_indicators() Calculate technical indicators
populate_entry_trend() Define entry signals
populate_exit_trend() Define exit signals

Optional Methods:

Method Description
custom_stake_amount() Dynamic position sizing
custom_stoploss() Dynamic stoploss
confirm_trade_entry() Additional entry validation
confirm_trade_exit() Additional exit validation

Metadata

StrategyMetadata

Strategy metadata for registry and discovery.

from tradai.strategy import StrategyMetadata, StrategyCategory, StrategyStatus

metadata = StrategyMetadata(
    name="TrendFollowingStrategy",
    version="1.0.0",
    description="EMA crossover with RSI filter",
    timeframe="1h",
    category=StrategyCategory.TREND_FOLLOWING,
    # Optional fields
    author="TradAI Team",
    tags=["trend", "ema", "rsi"],
    can_short=False,
    status=StrategyStatus.TESTING,
    exchange="binance",
    pairs=["BTC/USDT:USDT", "ETH/USDT:USDT"],
)

Required Fields:

Field Type Description
name str Strategy class name
version str Semantic version (X.Y.Z)
description str Brief description
timeframe str Primary timeframe (e.g., "1h")
category StrategyCategory Strategy category enum

Optional Fields:

Field Type Default Description
author str "TradAI Team" Author name
tags list[str] [] Searchable tags
can_short bool False Supports short positions
status StrategyStatus TESTING Lifecycle status
exchange str None Target exchange
pairs list[str] [] Trading pairs
trading_modes list[str] ["backtest"] Supported modes

Validation

StrategyValidator

Validate strategy implementation.

from tradai.strategy import StrategyValidator, ValidationLevel

# Validate strategy class
result = StrategyValidator.validate_strategy(
    strategy_class=MyStrategy,
    level=ValidationLevel.STRICT
)

if result["valid"]:
    print("Strategy passed validation")
else:
    for error in result["errors"]:
        print(f"Error: {error}")
    for warning in result["warnings"]:
        print(f"Warning: {warning}")

Validation Levels:

Level Description
BASIC Minimum checks (class structure)
STANDARD Standard checks (+ indicators, signals)
STRICT All checks (+ performance hints)

Checks Performed: - Required methods present - Metadata completeness - Indicator calculations valid - Signal generation correct - No common anti-patterns

validate_dataframe

Validate strategy output DataFrame.

from tradai.strategy import validate_dataframe

# Validate indicator DataFrame
errors = validate_dataframe(
    dataframe,
    required_columns=['ema_fast', 'ema_slow', 'rsi'],
    check_nan=True,
    check_inf=True
)

if errors:
    for error in errors:
        print(f"DataFrame issue: {error}")

CI/CD Integration

CIBacktestGate

Quality gate for CI/CD pipelines.

from tradai.strategy import CIBacktestGate, CIBacktestThresholds

# Define quality thresholds
thresholds = CIBacktestThresholds(
    min_sharpe_ratio=1.0,
    max_drawdown_pct=20.0,
    min_trade_count=50,
    min_win_rate=0.4,
    require_positive_returns=True,
)

# Create gate
gate = CIBacktestGate(thresholds=thresholds)

# Validate backtest results
result = gate.validate(backtest_result)

if result.passed:
    print("Strategy ready for deployment")
else:
    print(result.to_summary())
    for violation in result.violations:
        print(f"  {violation.rule_id}: {violation.message}")

CI Module Exports

For detailed CI analysis, import from the submodule:

from tradai.strategy.ci import (
    CIBacktestGate,
    CIBacktestThresholds,
    CIValidationResult,
    CIGateRuleId,      # Enum of gate rules
    CIViolation,       # Individual violation details
)

# Available gate rules
CIGateRuleId.SHARPE_TOO_LOW
CIGateRuleId.DRAWDOWN_TOO_HIGH
CIGateRuleId.INSUFFICIENT_TRADES
CIGateRuleId.NEGATIVE_RETURNS
CIGateRuleId.WIN_RATE_TOO_LOW

Usage in CI:

# .github/workflows/strategy-ci.yml
- name: Run Backtest Gate
  run: |
    tradai strategy gate MyStrategy \
      --min-sharpe 1.0 \
      --max-drawdown 20 \
      --min-trades 50

Preflight Checks

PreflightValidationService

Run pre-deployment validation checks.

from tradai.strategy.preflight import (
    PreflightValidationService,
    PreflightResult,
    CheckResult,
    CheckSeverity,
)
from datetime import datetime

service = PreflightValidationService()

# Run all checks
result: PreflightResult = await service.validate(
    strategy_name="MyStrategy",
    symbols=["BTC/USDT:USDT", "ETH/USDT:USDT"],
    start_date=datetime(2024, 1, 1),
    end_date=datetime(2024, 6, 1),
    timeframe="1h",
    strict=False
)

print(f"Valid: {result.valid}, Can proceed: {result.can_proceed}")

for check in result.checks:
    status = "PASS" if check.passed else "FAIL"
    print(f"[{status}] {check.check_name}: {check.message}")

Available Checks:

Check Purpose Validates Severity
MetadataCheck Validate get_metadata() Required fields, semver version ERROR/WARNING
StrategyClassCheck Validate TradAIStrategy Inheritance, instantiation, Freqtrade attrs ERROR
LintCheck Static analysis Look-ahead bias, warmup, empty methods ERROR/WARNING
DataAvailabilityCheck Data exists Symbols, date range coverage ERROR/WARNING
DataQualityCheck OHLCV quality NaN, inf, price relationships, gaps ERROR/WARNING
WarmupCheck Indicator warmup startup_candle_count sufficiency WARNING

MetadataCheck

Validates that strategy metadata is complete and properly structured.

Validations: - Strategy has get_metadata() method - Metadata returns StrategyMetadata type - Required fields present: name, version, description, timeframe, category - Version follows semver format (X.Y.Z or X.Y.Z-tag)

Result: - ERROR: Missing get_metadata() or wrong return type - WARNING: Missing/empty required fields, invalid semver

StrategyClassCheck

Validates strategy class structure using StrategyValidator.

Validations: - Strategy inherits from TradAIStrategy - Strategy can be instantiated - Required Freqtrade attributes present - Required methods implemented

Result: - ERROR: Class doesn't inherit correctly, instantiation fails, missing required methods - WARNING: Non-critical issues like missing optional attributes

LintCheck

Runs AST-based linting to detect common strategy issues.

Validations: - Look-ahead bias detection (shift(-N) patterns that use future data) - Missing startup_candle_count when indicators are used - Empty populate_indicators methods

Result: - ERROR: Look-ahead bias detected (critical - makes backtest results invalid) - WARNING: Other linting issues (treated as ERROR in strict mode)

DataAvailabilityCheck

Validates that data exists for requested symbols and date range.

Validations: - Data exists for all requested symbols - Data covers the requested date range - Data is not stale (latest date >= end date)

Result: - ERROR: Data not found for one or more symbols - WARNING: Data may be incomplete (stale or partial coverage)

Suggestion on failure: Run 'tradai data sync' to fetch missing data

DataQualityCheck

Validates OHLCV data quality for backtesting accuracy.

Validations: - Price relationships: High >= Low, High >= max(Open, Close), Low <= min(Open, Close) - Volume: No negative volumes, warning if >5% zero-volume candles - Timestamps: No duplicate timestamps, no gaps exceeding 2x expected interval

Thresholds:

Check Threshold Severity
Zero volume candles >5% WARNING
Timestamp gap >2x expected interval WARNING
Negative volume Any ERROR
Invalid OHLC relationship Any ERROR
Duplicate timestamps Any ERROR

WarmupCheck

Validates that startup_candle_count is sufficient for indicators used.

Validations: - Strategy has startup_candle_count attribute - Value is a positive integer - Value is sufficient for detected indicators

Recommended Warmup Periods:

Indicator Min Warmup Notes
RSI 14 Standard RSI period
MACD 35 slow(26) + signal(9)
EMA/SMA 200 Depends on period used
Bollinger Bands 20 Standard period
ATR 14 Standard period
ADX 28 14 × 2 for smoothing

Result: - ERROR: startup_candle_count is negative or wrong type - WARNING: startup_candle_count not set, is 0, or insufficient for detected indicators

Preflight Entities:

from tradai.strategy.preflight import (
    PreflightContext,          # Validation context
    DataAvailabilityResult,    # Data availability per symbol
)

Sanity Checks

SanityCheckService

Post-backtest sanity validation.

from tradai.strategy.sanity import (
    SanityCheckService,
    SanityCheckResult,
    SanityRuleId,
    SanityWarning,
)

service = SanityCheckService()

# Check backtest results for issues
result: SanityCheckResult = service.check(backtest_result)

if result.warnings:
    for warning in result.warnings:
        print(f"[{warning.rule_id}] {warning.message}")

Sanity Rules:

from tradai.strategy.sanity import SanityRuleId

# Available rules
SanityRuleId.WIN_RATE_TOO_HIGH      # Suspiciously high win rate
SanityRuleId.RETURNS_TOO_HIGH       # Unrealistic returns
SanityRuleId.NO_TRADES              # No trades executed
SanityRuleId.SHARPE_INCONSISTENT    # Inconsistent Sharpe ratio
SanityRuleId.PROFIT_FACTOR_EXTREME  # Extreme profit factor
SanityRuleId.MAX_DRAWDOWN_EXTREME   # Extreme max drawdown
SanityRuleId.TRADE_COUNT_LOW        # Insufficient trade count

Checks Performed: - Unrealistic returns detection - Overfitting indicators - Look-ahead bias detection - Insufficient trade count - Extreme drawdown patterns


Debug Utilities

StrategyDebugMixin

Mixin for strategy debugging during development.

from tradai.strategy import TradAIStrategy, StrategyDebugMixin


class MyStrategy(StrategyDebugMixin, TradAIStrategy):
    """Strategy with debug capabilities."""

    def populate_indicators(self, dataframe, metadata):
        dataframe['ema'] = ta.EMA(dataframe, timeperiod=20)

        # Debug: log indicator values
        self.debug_indicator('ema', dataframe['ema'].iloc[-1])

        return dataframe

    def populate_entry_trend(self, dataframe, metadata):
        # Debug: log signal generation
        self.debug_signal('entry_check', {
            'ema': dataframe['ema'].iloc[-1],
            'close': dataframe['close'].iloc[-1],
        })

        return dataframe

FreqAI Integration

TradAI provides two FreqAI prediction models with different use cases:

Model Purpose Use Case
TradAIPredictionModel Inference-only Live trading, uses pre-trained models
TrainingPredictionModel Walk-forward training Backtesting with actual ML training

TradAIPredictionModel

Inference-only model for live trading. Loads predictions from CSV or MLflow.

Prediction Sources:

Source Class Description
CSV CSVLoader Pre-computed predictions from CSV file
MLflow MLflowLoader Real-time inference from registered model

TrainingPredictionModel

Walk-forward training model for backtesting. Trains ML models during backtest runs.

Supported ML Frameworks:

Framework model_type Import
LightGBM lightgbm LGBMRegressor
CatBoost catboost CatBoostRegressor
XGBoost xgboost XGBRegressor

Walk-Forward Pattern:

Train: Days 1-30 → Predict: Days 31-37 → Retrain: Days 8-37 → ...

Configuration:

{
    "freqai": {
        "enabled": true,
        "model_type": "lightgbm",
        "train_period_days": 30,
        "backtest_period_days": 7,
        "model_training_parameters": {
            "n_estimators": 500,
            "learning_rate": 0.05,
            "max_depth": 7
        },
        "data_split_parameters": {
            "test_size": 0.1,
            "shuffle": false
        },
        "identifier": "walkforward_v1"
    }
}

CSVLoader

Load pre-computed predictions from CSV file.

from tradai.strategy.freqai import CSVLoader
from pathlib import Path

loader = CSVLoader(path=Path("predictions/my_predictions.csv"))
predictions, do_predict = loader.load(dataframe, data_kitchen)

CSV Format:

date,prediction
2024-01-01 00:00:00,0.65
2024-01-01 01:00:00,0.42

MLflowLoader

Load model from MLflow for real-time inference.

from tradai.strategy.freqai import MLflowLoader
from tradai.common import MLflowAdapter

adapter = MLflowAdapter(base_url="http://localhost:5000")
loader = MLflowLoader(
    adapter=adapter,
    model_uri="models:/MyStrategy/Production",  # or version: "models:/MyStrategy/3"
    validate_on_init=True  # Validates model exists and is READY
)

# Use in strategy
predictions, do_predict = loader.load(dataframe, data_kitchen)

Model URI Formats:

Format Example Description
Stage models:/ModelName/Production Latest version in stage
Version models:/ModelName/3 Specific version number

FreqAIConfigBuilder

Build FreqAI configuration programmatically.

from tradai.strategy.freqai import FreqAIConfigBuilder
from tradai.common import MLflowAdapter

mlflow_adapter = MLflowAdapter(base_url="http://localhost:5000")
builder = FreqAIConfigBuilder(mlflow_adapter=mlflow_adapter)

# Build inference config from registered model
config = builder.build_inference_config(
    base_config={"freqai": {"enabled": True}},
    model_uri="models:/MyModel/Production"
)

# Or build training config
training_config = builder.build_training_config(
    base_config=base_config,
    model_type="LightGBMClassifier",
    training_period=365,
    backtest_period=30
)

# Validate model exists before deployment
if builder.validate_model_exists("models:/MyModel/Production"):
    print("Model ready for deployment")

FreqAI Module Exports

from tradai.strategy.freqai import (
    FreqAIConfigBuilder,       # Config builder
    TradAIPredictionModel,     # Inference-only model
    TrainingPredictionModel,   # Walk-forward training model
    CSVLoader,                 # Load predictions from CSV
    MLflowLoader,              # Load predictions from MLflow
)

Feature Engineering

Define features using the standard FreqAI feature engineering methods:

from tradai.strategy import TradAIStrategy
import talib.abstract as ta


class MLStrategy(TradAIStrategy):
    """FreqAI-enabled ML strategy with MLflow predictions."""

    def populate_indicators(self, dataframe, metadata):
        # Add features for ML model
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
        dataframe['macd'] = ta.MACD(dataframe)['macd']
        dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
        return dataframe

    def feature_engineering_expand_all(self, dataframe, period, **kwargs):
        """Define features for ML model (called for each symbol).

        Features prefixed with '%-' are used as ML features.
        """
        dataframe['%-rsi'] = ta.RSI(dataframe, timeperiod=period)
        dataframe['%-macd'] = ta.MACD(dataframe, fastperiod=period)['macd']
        dataframe['%-atr'] = ta.ATR(dataframe, timeperiod=period)
        return dataframe

    def feature_engineering_expand_basic(self, dataframe, metadata, **kwargs):
        """Add non-indicator features (OHLCV transformations)."""
        dataframe['%-pct_change'] = dataframe['close'].pct_change()
        dataframe['%-volume_change'] = dataframe['volume'].pct_change()
        return dataframe

    def set_freqai_targets(self, dataframe, metadata, **kwargs):
        """Define prediction target.

        Target column must be named '&-<name>'.
        """
        # Predict next candle direction
        dataframe['&-target'] = (
            (dataframe['close'].shift(-1) > dataframe['close']).astype(int)
        )
        return dataframe

Feature Naming Conventions:

Prefix Usage Example
%- Input features for ML model %-rsi, %-macd
&- Target label for training &-target, &-returns

Model Persistence

Models are saved and loaded automatically by FreqAI:

Training Mode (TrainingPredictionModel): - Models saved to: user_data/models/{identifier}/ - Each walk-forward window creates a new model checkpoint - Metadata includes training metrics per window

Inference Mode (TradAIPredictionModel): - Models loaded from MLflow registry - Stage aliases (Production, Staging) resolved to specific versions - Version validation ensures model is in READY status


Filters

apply_filters

Apply common trade filters to refine entry signals. Filters based on volume and trend conditions.

Requirements: - DataFrame must have enter_long and/or enter_short columns with signals - For volume filter: volume column required - For trend filter: sma_50 column required

from tradai.strategy import apply_filters

def populate_entry_trend(self, dataframe, metadata):
    # First, generate entry signals
    dataframe.loc[
        (dataframe['ema_fast'] > dataframe['ema_slow']) &
        (dataframe['rsi'] < 70),
        'enter_long'
    ] = 1

    # Then, apply filters (operates on enter_long/enter_short columns)
    # - Removes signals when volume < 50% of 20-period average
    # - Removes long signals when price below SMA50
    # - Removes short signals when price above SMA50
    dataframe = apply_filters(dataframe)

    return dataframe

apply_volatility_filter

Filter signals by volatility (ATR). Removes signals in both low volatility (boring markets) and high volatility (too risky) conditions.

Requirements: - DataFrame must have atr column - DataFrame must have enter_long and/or enter_short columns

from tradai.strategy import apply_volatility_filter

# Filter out signals when ATR% is outside acceptable range
# Default: Keep signals when 0.5% < ATR/price < 3.0%
dataframe = apply_volatility_filter(
    dataframe,
    min_atr_multiplier=0.5,   # Minimum ATR as % of price (filter boring markets)
    max_atr_multiplier=3.0,   # Maximum ATR as % of price (filter risky markets)
)

Enums

SignalDirection

Trading signal direction.

from tradai.strategy import SignalDirection

direction = SignalDirection.LONG   # Long/buy signal
direction = SignalDirection.SHORT  # Short/sell signal

StrategyCategory

Strategy categorization.

from tradai.strategy import StrategyCategory

category = StrategyCategory.TREND_FOLLOWING  # Trend-following strategies
category = StrategyCategory.MEAN_REVERSION   # Mean reversion strategies
category = StrategyCategory.BREAKOUT         # Breakout strategies
category = StrategyCategory.MOMENTUM         # Momentum strategies
category = StrategyCategory.ARBITRAGE        # Arbitrage strategies

ValidationLevel

Validation strictness levels.

from tradai.strategy import ValidationLevel

level = ValidationLevel.BASIC     # Minimum checks
level = ValidationLevel.STANDARD  # Standard checks
level = ValidationLevel.STRICT    # All checks

Additional Enums

from tradai.strategy import (
    BacktestStatus,     # Status of backtest (PENDING, RUNNING, COMPLETED, FAILED)
    StrategyStatus,     # Strategy lifecycle status
    PredictionSource,   # Source of ML predictions (CSV, MLFLOW, LIVE)
)

Exceptions

StrategyError

Base exception for strategy errors.

from tradai.strategy import StrategyError, IndicatorCalculationError, InsufficientDataError

try:
    result = calculate_indicator(dataframe)
except IndicatorCalculationError as e:
    logger.error(f"Indicator failed: {e}")
    raise StrategyError("Strategy cannot proceed") from e

Exception Hierarchy:

StrategyError (base)
├── IndicatorCalculationError  # Indicator computation failed
└── InsufficientDataError      # Not enough data for warmup

Linting

StrategyLinter

Lint strategy code for common issues.

Internal API

Linting is primarily used internally by PreflightValidationService. For most use cases, use preflight validation instead of direct linting.

# Direct linting (advanced use)
from tradai.strategy.linting import StrategyLinter, lint_file

linter = StrategyLinter()
issues = linter.lint(strategy_class)

for issue in issues:
    print(f"Line {issue.line}: [{issue.severity}] {issue.message}")

# Or lint a file directly
issues = lint_file("strategies/my_strategy.py")

Recommended: Use Preflight Instead

from tradai.strategy.preflight import PreflightValidationService

service = PreflightValidationService()
result = await service.validate(strategy_class=MyStrategy, config=config)
# Linting is included as part of preflight checks

Detected Issues: - Hardcoded magic numbers - Missing docstrings - Unused imports - Look-ahead bias patterns - Non-vectorized operations


Testing Utilities

Fixtures

Pytest fixtures for strategy testing.

# conftest.py
from tradai.strategy.testing.fixtures import (
    sample_dataframe,
    sample_ohlcv,
    mock_strategy,
)

def test_strategy_indicators(sample_dataframe):
    strategy = MyStrategy({})
    result = strategy.populate_indicators(sample_dataframe, {})

    assert 'ema_fast' in result.columns
    assert 'ema_slow' in result.columns
    assert not result['ema_fast'].isna().all()

Top-Level Exports

All exports available directly from tradai.strategy:

from tradai.strategy import (
    # Base class
    TradAIStrategy,
    LoggerMixin,

    # CI Validation
    CIBacktestGate,
    CIBacktestThresholds,
    CIValidationResult,

    # Debug
    StrategyDebugMixin,
    debug,

    # Metadata
    StrategyMetadata,

    # Validation
    StrategyValidator,
    validate_dataframe,

    # Filters
    apply_filters,
    apply_volatility_filter,

    # Enums
    SignalDirection,
    StrategyStatus,
    StrategyCategory,
    BacktestStatus,
    ValidationLevel,
    PredictionSource,

    # Exceptions
    StrategyError,
    IndicatorCalculationError,
    InsufficientDataError,
)

Submodule Imports (for specialized features):

# Preflight validation
from tradai.strategy.preflight import PreflightValidationService, PreflightResult

# Sanity checks
from tradai.strategy.sanity import SanityCheckService, SanityCheckResult

# FreqAI integration
from tradai.strategy.freqai import FreqAIConfigBuilder, TradAIPredictionModel

# CI details
from tradai.strategy.ci import CIGateRuleId, CIViolation

# Linting (advanced)
from tradai.strategy.linting import StrategyLinter

Complete Examples

Complete Strategy Implementation

Full strategy with indicators, signals, and validation:

from datetime import datetime
from typing import ClassVar

import pandas as pd
from freqtrade.strategy import IStrategy

from tradai.strategy import (
    TradAIStrategy,
    StrategyMetadata,
    SignalDirection,
    StrategyCategory,
    apply_filters,
    validate_dataframe,
)


class MomentumCrossStrategy(TradAIStrategy, IStrategy):
    """EMA crossover strategy with volume and trend filters."""

    # Freqtrade configuration
    INTERFACE_VERSION: ClassVar[int] = 3
    timeframe = "1h"
    stoploss = -0.05
    trailing_stop = True
    trailing_stop_positive = 0.02
    startup_candle_count = 200

    # Strategy parameters
    ema_fast_period = 12
    ema_slow_period = 26
    rsi_period = 14
    rsi_overbought = 70
    rsi_oversold = 30

    @classmethod
    def get_metadata(cls) -> StrategyMetadata:
        """Strategy metadata for registry and validation."""
        return StrategyMetadata(
            name="MomentumCrossStrategy",
            version="1.2.0",
            author="TradAI Team",
            description="EMA crossover with RSI confirmation",
            category=StrategyCategory.MOMENTUM,
            pairs=["BTC/USDT:USDT", "ETH/USDT:USDT"],
            timeframe="1h",
            min_history_days=30,
            tags=["momentum", "crossover", "trend-following"],
        )

    def populate_indicators(
        self,
        dataframe: pd.DataFrame,
        metadata: dict,
    ) -> pd.DataFrame:
        """Calculate technical indicators."""
        # Validate input
        validate_dataframe(dataframe)

        # Moving averages
        dataframe["ema_fast"] = dataframe["close"].ewm(
            span=self.ema_fast_period, adjust=False
        ).mean()
        dataframe["ema_slow"] = dataframe["close"].ewm(
            span=self.ema_slow_period, adjust=False
        ).mean()

        # RSI
        delta = dataframe["close"].diff()
        gain = delta.where(delta > 0, 0).rolling(window=self.rsi_period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=self.rsi_period).mean()
        rs = gain / loss.replace(0, 1e-10)
        dataframe["rsi"] = 100 - (100 / (1 + rs))

        # Volume SMA for filter
        dataframe["volume_sma"] = dataframe["volume"].rolling(window=20).mean()

        return dataframe

    def populate_entry_trend(
        self,
        dataframe: pd.DataFrame,
        metadata: dict,
    ) -> pd.DataFrame:
        """Generate entry signals."""
        dataframe["enter_long"] = 0
        dataframe["enter_short"] = 0

        # Long: EMA cross up + RSI oversold recovery
        long_conditions = (
            (dataframe["ema_fast"] > dataframe["ema_slow"])
            & (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1))
            & (dataframe["rsi"] > self.rsi_oversold)
            & (dataframe["rsi"] < 50)
            & (dataframe["volume"] > dataframe["volume_sma"])
        )
        dataframe.loc[long_conditions, "enter_long"] = 1

        # Short: EMA cross down + RSI overbought decline
        short_conditions = (
            (dataframe["ema_fast"] < dataframe["ema_slow"])
            & (dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1))
            & (dataframe["rsi"] < self.rsi_overbought)
            & (dataframe["rsi"] > 50)
            & (dataframe["volume"] > dataframe["volume_sma"])
        )
        dataframe.loc[short_conditions, "enter_short"] = 1

        # Apply standard filters
        dataframe = apply_filters(dataframe)

        return dataframe

    def populate_exit_trend(
        self,
        dataframe: pd.DataFrame,
        metadata: dict,
    ) -> pd.DataFrame:
        """Generate exit signals."""
        dataframe["exit_long"] = 0
        dataframe["exit_short"] = 0

        # Exit long on RSI overbought
        dataframe.loc[dataframe["rsi"] > self.rsi_overbought, "exit_long"] = 1

        # Exit short on RSI oversold
        dataframe.loc[dataframe["rsi"] < self.rsi_oversold, "exit_short"] = 1

        return dataframe

Preflight Validation Workflow

Run comprehensive checks before deployment:

from datetime import datetime, timedelta

from tradai.strategy.preflight import PreflightValidationService, PreflightResult
from tradai.data.core.entities import SymbolList, Timeframe, DateRange
from tradai.data.infrastructure.repositories import CCXTRepository
from tradai.common import ExchangeConfig, TradingMode

# Import your strategy
from my_strategies import MomentumCrossStrategy


def validate_strategy_for_deployment() -> PreflightResult:
    """Run preflight checks before deploying strategy."""

    # 1. Create validation service
    service = PreflightValidationService()

    # 2. Get strategy metadata
    metadata = MomentumCrossStrategy.get_metadata()

    # 3. Configure validation parameters
    symbols = SymbolList.from_input(metadata.pairs)
    timeframe = Timeframe.parse(metadata.timeframe)
    date_range = DateRange(
        start=datetime.now() - timedelta(days=metadata.min_history_days),
        end=datetime.now(),
    )

    # 4. Optional: provide data repository for data availability checks
    config = ExchangeConfig(name="binance", trading_mode=TradingMode.FUTURES)
    data_repo = CCXTRepository(config=config)

    # 5. Run all preflight checks
    result = service.validate(
        strategy_class=MomentumCrossStrategy,
        symbols=symbols,
        timeframe=timeframe,
        date_range=date_range,
        data_repository=data_repo,
    )

    # 6. Process results
    print(f"Validation passed: {result.passed}")
    print(f"Total checks: {len(result.checks)}")

    for check in result.checks:
        status = "✅" if check.passed else "❌"
        print(f"  {status} {check.name}: {check.message}")

        if check.details:
            for key, value in check.details.items():
                print(f"      {key}: {value}")

    # 7. Return result for CI/CD integration
    return result


# Run validation
if __name__ == "__main__":
    result = validate_strategy_for_deployment()
    exit(0 if result.passed else 1)

FreqAI Strategy Configuration

Configure ML-powered strategy with FreqAI:

from typing import ClassVar

import pandas as pd
from freqtrade.strategy import IStrategy

from tradai.strategy import (
    TradAIStrategy,
    StrategyMetadata,
    StrategyCategory,
)
from tradai.strategy.freqai import (
    FreqAIConfigBuilder,
    TradAIPredictionModel,
    PredictionSource,
)


class MLMomentumStrategy(TradAIStrategy, IStrategy):
    """FreqAI-powered momentum strategy with LightGBM."""

    INTERFACE_VERSION: ClassVar[int] = 3
    timeframe = "1h"
    stoploss = -0.05
    startup_candle_count = 300  # Extra candles for ML training

    # FreqAI configuration
    freqai_config = FreqAIConfigBuilder(
        model_name="LightGBMRegressor",
        train_period_days=30,
        backtest_period_days=7,
        live_retrain_hours=24,
        identifier="ml_momentum_v1",
    ).with_feature_parameters(
        include_timeframes=["1h", "4h"],
        include_corr_pairlist=["BTC/USDT:USDT"],
        label_period_candles=24,
        include_shifted_candles=2,
    ).with_data_split(
        train_test_split=0.85,
        shuffle=False,
    ).build()

    @classmethod
    def get_metadata(cls) -> StrategyMetadata:
        return StrategyMetadata(
            name="MLMomentumStrategy",
            version="2.0.0",
            author="TradAI Team",
            description="LightGBM momentum prediction",
            category=StrategyCategory.ML_HYBRID,
            pairs=["BTC/USDT:USDT", "ETH/USDT:USDT"],
            timeframe="1h",
            min_history_days=60,
            freqai_enabled=True,
            tags=["ml", "lightgbm", "momentum"],
        )

    def feature_engineering_expand_all(
        self,
        dataframe: pd.DataFrame,
        period: int,
        metadata: dict,
        **kwargs,
    ) -> pd.DataFrame:
        """Generate features for all timeframes."""
        # RSI
        delta = dataframe["close"].diff()
        gain = delta.where(delta > 0, 0).rolling(window=period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
        rs = gain / loss.replace(0, 1e-10)
        dataframe[f"%-rsi-period_{period}"] = 100 - (100 / (1 + rs))

        # Bollinger Bands width
        sma = dataframe["close"].rolling(window=period).mean()
        std = dataframe["close"].rolling(window=period).std()
        dataframe[f"%-bb-width-period_{period}"] = (std * 2) / sma

        # Price momentum
        dataframe[f"%-momentum-period_{period}"] = (
            dataframe["close"] / dataframe["close"].shift(period) - 1
        )

        return dataframe

    def feature_engineering_expand_basic(
        self,
        dataframe: pd.DataFrame,
        metadata: dict,
        **kwargs,
    ) -> pd.DataFrame:
        """Generate base timeframe features only."""
        # MACD
        exp1 = dataframe["close"].ewm(span=12, adjust=False).mean()
        exp2 = dataframe["close"].ewm(span=26, adjust=False).mean()
        dataframe["%-macd"] = exp1 - exp2
        dataframe["%-macd-signal"] = dataframe["%-macd"].ewm(span=9, adjust=False).mean()

        # Volume ratio
        dataframe["%-volume-ratio"] = (
            dataframe["volume"] / dataframe["volume"].rolling(window=20).mean()
        )

        return dataframe

    def set_freqai_targets(
        self,
        dataframe: pd.DataFrame,
        metadata: dict,
        **kwargs,
    ) -> pd.DataFrame:
        """Define prediction targets."""
        # Predict future returns (regression)
        dataframe["&-target"] = (
            dataframe["close"].shift(-24) / dataframe["close"] - 1
        )
        return dataframe

    def populate_indicators(
        self,
        dataframe: pd.DataFrame,
        metadata: dict,
    ) -> pd.DataFrame:
        """Run FreqAI prediction."""
        # FreqAI handles feature engineering and prediction
        dataframe = self.freqai.start(dataframe, metadata, self)

        # Prediction confidence threshold
        dataframe["prediction_confidence"] = dataframe.get(
            "&-prediction_confidence", 0.5
        )

        return dataframe

    def populate_entry_trend(
        self,
        dataframe: pd.DataFrame,
        metadata: dict,
    ) -> pd.DataFrame:
        """Generate ML-based entry signals."""
        dataframe["enter_long"] = 0
        dataframe["enter_short"] = 0

        # Get prediction from FreqAI
        prediction = dataframe.get("&-prediction", 0)
        confidence = dataframe["prediction_confidence"]

        # Long: positive prediction with high confidence
        long_conditions = (prediction > 0.01) & (confidence > 0.6)
        dataframe.loc[long_conditions, "enter_long"] = 1

        # Short: negative prediction with high confidence
        short_conditions = (prediction < -0.01) & (confidence > 0.6)
        dataframe.loc[short_conditions, "enter_short"] = 1

        return dataframe

    def populate_exit_trend(
        self,
        dataframe: pd.DataFrame,
        metadata: dict,
    ) -> pd.DataFrame:
        """Generate ML-based exit signals."""
        dataframe["exit_long"] = 0
        dataframe["exit_short"] = 0

        prediction = dataframe.get("&-prediction", 0)

        # Exit when prediction reverses
        dataframe.loc[prediction < -0.005, "exit_long"] = 1
        dataframe.loc[prediction > 0.005, "exit_short"] = 1

        return dataframe

See Also

Related SDKs:

Services:

Architecture:

Lambdas:

Quickstarts:

CLI: