Skip to content

Validating Strategies

Guide to pre-backtest validation and post-backtest sanity checks.

Overview

TradAI provides automatic validation at two stages:

  1. Preflight Validation - Before backtest starts (catches errors early)
  2. Sanity Checks - After backtest completes (warns about suspicious results)

Preflight Validation

Preflight validation runs automatically before every backtest (unless skipped).

What's Checked

Check Description
Strategy Class Valid TradAIStrategy subclass
Metadata Required fields present and valid
Look-Ahead Bias Detects shift(-N), global stats, future data access
Data Availability Data exists for requested symbols and period
Indicator Warmup startup_candle_count sufficient for indicators

Automatic Validation

Validation runs automatically with tradai backtest quick:

# Normal execution - validation runs automatically
tradai backtest quick MomentumV2

# Output:
# Preflight Validation
# PASS   Strategy Class    Valid TradAIStrategy
# PASS   Metadata          All required fields present
# WARN   Look-Ahead        strategies/momentum_v2/strategy.py:45: shift(-1) detected
# PASS   Data              BTC/USDT:USDT available
# PASS   Warmup            startup_candle_count=50 sufficient
#
# Preflight passed with 1 warning(s)

Validation Options

# Skip validation (not recommended)
tradai backtest quick MomentumV2 --skip-validation

# Strict mode - treat warnings as errors
tradai backtest quick MomentumV2 --strict

External Strategy Repos

When running strategies from a separate repo (e.g., tradai-strategies), the strategy class may not be importable from tradai-uv. In this case, preflight degrades gracefully:

tradai backtest quick StochRsiStrategy --strategy-dir ../tradai-strategies

# Output:
# Preflight Validation
# WARN   Strategy Class    Strategy 'StochRsiStrategy' found but could not be imported
# PASS   Metadata          Strategy class not available, skipping metadata check
# ...
#
# Preflight passed with 1 warning(s)

If the strategy is not found at all:

# FAIL   Strategy Class    Strategy 'Unknown' not found. Use --strategy-dir or set TRADAI_STRATEGY_DIR.

Understanding Results

Status Meaning Action
PASS Check passed Continue
WARN Potential issue Review, may continue
FAIL Critical error Must fix before backtest

Look-Ahead Bias Detection

Look-ahead bias is when your strategy "sees the future" during backtesting.

Detected Patterns

# BAD: shift(-N) accesses future data
df['signal'] = df['close'].shift(-1) > df['close']  # Uses tomorrow's price!

# BAD: Global statistics include future data
df['zscore'] = (df['close'] - df['close'].mean()) / df['close'].std()

# BAD: iloc[-1] comparison
df['signal'] = df['close'].iloc[-1] > df['close'].iloc[-2]

Correct Patterns

# GOOD: shift(1) uses past data
df['signal'] = df['close'] > df['close'].shift(1)  # Compare to yesterday

# GOOD: Rolling statistics (look-back only)
df['zscore'] = (df['close'] - df['close'].rolling(20).mean()) / df['close'].rolling(20).std()

# GOOD: Use expanding window
df['cum_mean'] = df['close'].expanding().mean()

Metadata Validation

Strategy metadata is validated for completeness:

class MyStrategy(TradAIStrategy):
    # Required metadata
    STRATEGY_NAME = "MyStrategy"  # Must match class name
    VERSION = "1.0.0"             # Semantic version
    CATEGORY = "momentum"          # Valid category
    TIMEFRAME = "1h"               # Valid timeframe

    # Optional but recommended
    AUTHOR = "Your Name"
    DESCRIPTION = "Strategy description"

Valid Categories

trend-following, momentum, mean-reversion, breakout, scalping, grid-trading, dca, arbitrage

Valid Timeframes

1m, 5m, 15m, 30m, 1h, 4h, 1d

Warmup Validation

Indicators need historical data to initialize. Warmup validation ensures you've set sufficient startup_candle_count.

Example

class MyStrategy(TradAIStrategy):
    # If you use EMA(50), you need at least 50 candles
    startup_candle_count = 50  # Validates this is sufficient

    def populate_indicators(self, dataframe, metadata):
        dataframe['ema50'] = ta.EMA(dataframe['close'], 50)
        return dataframe

What Happens Without Warmup

WARN   Warmup   startup_candle_count=0, but indicators need ~50
       → First candles may have NaN indicators

Post-Backtest Sanity Checks

After backtest completes, sanity checks warn about suspicious results.

Checks Performed

Check Warning Condition Meaning
Win Rate > 95% Likely look-ahead bias
Returns > 500% Unrealistic, check for bugs
Zero Trades 0 trades Strategy never triggered
Negative Sharpe Sharpe < 0 with positive returns High volatility
Profit Factor > 10 Small sample or overfitting
Drawdown < -50% Extremely risky

Example Output

Quick backtest: MomentumV2 on BTC/USDT:USDT (30d)
✓ Completed in 12.3s

Profit: +120.5% | Trades: 3 | Sharpe: 4.85

Sanity Check Warnings
WARN   Profit Factor (25.3) is extremely high - may indicate overfitting
       → Run with more data to verify stability
WARN   Only 3 trades in backtest period
       → Results may not be statistically significant

Strategy Checks

Run all validation checks on a strategy project with a single command:

# Run all 6 checks in sequence (stops on first failure)
tradai strategy check ./strategies/my-strategy/

# Continue past failures to see all results
tradai strategy check ./strategies/my-strategy/ --continue-on-error

The check aggregator runs these checks in order:

Step Command What it checks
1 check-files Required file structure (pyproject.toml, Dockerfile, tradai.yaml, src/, tests/, configs/)
2 check-config tradai.yaml schema (name, version, entry_point, category, timeframe)
3 lint Look-ahead bias, repainting indicators, missing warmup period
4 typecheck mypy type checking on src/
5 test pytest on tests/
6 check-params Hyperopt parameter search spaces (ranges, defaults, categories)

Individual Checks

Each check can also be run standalone:

tradai strategy check-files ./strategies/my-strategy/
tradai strategy check-config ./strategies/my-strategy/
tradai strategy typecheck ./strategies/my-strategy/
tradai strategy test ./strategies/my-strategy/
tradai strategy check-params ./strategies/my-strategy/

JSON output is available for check-files, check-config, and check-params:

tradai strategy check-files ./strategies/my-strategy/ --json

Strategy Linting

For detailed code analysis outside of backtesting:

tradai strategy lint MyStrategy

Checks for: - Look-ahead bias patterns - Missing startup_candle_count - Repainting indicators - Invalid parameter combinations - Unused imports

Example Output

Linting MyStrategy...

strategies/my_strategy/strategy.py:45: WARNING
  Pattern: df.shift(-1) - potential look-ahead bias

strategies/my_strategy/strategy.py:23: WARNING
  Missing startup_candle_count - indicators need warmup period

strategies/my_strategy/indicators.py:12: INFO
  Global .mean() detected - ensure this is intentional

Summary: 2 warnings, 1 info

Data Availability

Preflight checks verify data exists before backtesting:

# Check data manually
tradai data check-freshness BTC/USDT:USDT

# Sync if needed
tradai data sync BTC/USDT:USDT --start 2024-01-01 --end 2024-06-01

Validation Output

PASS   Data: BTC/USDT:USDT   Available from 2024-01-01 to 2024-12-22
WARN   Data: ETH/USDT:USDT   Partial data: 85% coverage
FAIL   Data: SOL/USDT:USDT   No data available
       → Run: tradai data sync SOL/USDT:USDT

Best Practices

1. Never Skip Validation

# DON'T do this unless debugging
tradai backtest quick MyStrategy --skip-validation  # ⚠️

2. Use Strict Mode for CI

# In CI pipelines, use strict to catch all issues
tradai backtest quick MyStrategy --strict

3. Lint Before Committing

# Add to pre-commit workflow
tradai strategy lint MyStrategy

4. Review All Warnings

Even if validation passes with warnings, review each one: - Look-ahead warnings → Could invalidate your backtest - Warmup warnings → First trades may use invalid indicators - Sanity warnings → Results may not be reliable

Quick Reference

Command Description
tradai backtest quick STRATEGY Auto-validates before backtest
tradai backtest quick --strict Treat warnings as errors
tradai backtest quick --skip-validation Skip validation (not recommended)
tradai strategy check PATH Run all validation checks
tradai strategy check-files PATH Validate file structure
tradai strategy check-config PATH Validate tradai.yaml
tradai strategy typecheck PATH Run mypy
tradai strategy test PATH Run pytest
tradai strategy check-params PATH Validate hyperopt params
tradai strategy lint NAME Detailed code analysis
tradai data check-freshness SYMBOL Check data availability

Validation Modules

For programmatic access:

# Unified validation entities
from tradai.strategy.validation_entities import (
    CheckSeverity,           # ERROR, WARNING, INFO
    ValidationIssue,         # Base issue class
    MetricValidationIssue,   # With actual_value, threshold, suggestion
)

# Preflight validation
from tradai.strategy.preflight import PreflightValidationService

service = PreflightValidationService()
result = service.validate(
    strategy_name="MomentumV2",
    symbols=["BTC/USDT:USDT"],
    start_date=datetime(2024, 1, 1),
    end_date=datetime(2024, 6, 1),
)

if not result.valid:
    for check in result.checks:
        if not check.passed:
            print(f"{check.severity}: {check.message}")

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

service = SanityCheckService()
result: SanityCheckResult = service.check(backtest_metrics)

# Use ValidationResultMixin properties
if result.has_errors:
    print(f"Blocking issues: {result.error_count}")
    for error in result.errors:
        print(f"  ERROR: {error.message}")

if result.has_warnings:
    print(f"Warnings: {result.warning_count}")
    for warning in result.warnings_only:
        print(f"  WARN: {warning.message}")
        if warning.suggestion:
            print(f"       → {warning.suggestion}")

# CI gate validation
from tradai.strategy.ci import CIBacktestGate, CIValidationResult

gate = CIBacktestGate(thresholds)
result: CIValidationResult = gate.validate(backtest_result)

# Same ValidationResultMixin properties available
print(f"Passed: {result.passed}")
print(f"Errors: {result.error_count}, Warnings: {result.warning_count}")

DataFrame Validation

Validate OHLCV dataframes during strategy development:

from tradai.strategy.dataframe_validators import (
    validate_dataframe,
    analyze_nan_indicators,
    DEFAULT_WARMUP_CANDLES,
)

# Validate required columns and minimum rows
validate_dataframe(
    dataframe=df,
    required_columns={"open", "high", "low", "close", "volume"},
    min_rows=200
)

# Analyze NaN in indicator columns (skipping warmup period)
analysis = analyze_nan_indicators(
    dataframe=df,
    warmup_candles=DEFAULT_WARMUP_CANDLES,  # 50 candles default
)

if analysis.has_nan_issues:
    print(f"Columns with NaN: {analysis.nan_column_names}")
    for col in analysis.get_columns_above_threshold(10.0):  # >10% NaN
        print(f"  {col.column}: {col.nan_percentage:.1f}% NaN")