Skip to content

TradAI API Reference

Complete API reference for TradAI services. For interactive documentation, visit the Swagger UI at each service's /docs endpoint.

Services Overview

Service Port Description
Backend 8000 Central gateway for backtest orchestration, strategy management
Strategy Service 8003 Strategy configuration, MLflow integration, hyperopt, A/B testing
Data Collection 8002 Market data fetching and storage

Interactive API Documentation

Each service exposes Swagger UI for interactive exploration:

Service Swagger UI ReDoc
Backend API http://localhost:8000/docs http://localhost:8000/redoc
Data Collection http://localhost:8002/docs http://localhost:8002/redoc
Strategy Service http://localhost:8003/docs http://localhost:8003/redoc

Start services first

Run just up to start all services, then open the Swagger URLs in your browser.

Authentication

All services support optional JWT authentication via AWS Cognito.

# Include token in Authorization header
curl -H "Authorization: Bearer <token>" http://localhost:8000/api/v1/backtests

Configuration

Variable Description
{SERVICE}_COGNITO_USER_POOL_ID Cognito User Pool ID
{SERVICE}_COGNITO_CLIENT_ID Cognito App Client ID
{SERVICE}_COGNITO_REGION AWS region (default: eu-central-1)

Authentication is optional in development but enforced in production.

Response Headers

All responses include: - X-Correlation-ID: Unique request ID for tracing/debugging

Error Response Format

Validation Error (400)

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "config", "strategy"],
      "msg": "Field required",
      "input": null
    }
  ]
}

Not Found (404)

{
  "detail": "Backtest job 'abc123' not found"
}

Service Unavailable (503)

{
  "detail": "Strategy service unavailable: connection timeout"
}

Error Code Catalog

Standardized error codes for programmatic error handling.

Validation Errors (VAL)

Code Status Description Resolution
VAL001 400 Invalid date range Ensure start < end and dates are ISO 8601 format
VAL002 400 Invalid symbol format Use BASE/QUOTE:SETTLE format (e.g., BTC/USDT:USDT)
VAL003 400 Invalid timeframe Use valid timeframe: 1m, 5m, 15m, 1h, 4h, 1d
VAL004 400 Missing required field Check request body for required fields
VAL005 400 Value out of range Check min/max constraints in field documentation
VAL006 400 Invalid config format Validate JSON structure matches expected schema

Authentication Errors (AUTH)

Code Status Description Resolution
AUTH001 401 Token expired Refresh JWT token via Cognito
AUTH002 401 Invalid token Verify token signature and issuer
AUTH003 401 Missing token Include Authorization: Bearer <token> header
AUTH004 403 Insufficient permissions Request elevated permissions or use different credentials
AUTH005 403 Resource access denied Verify resource ownership or sharing permissions

Resource Errors (RES)

Code Status Description Resolution
RES001 404 Backtest job not found Verify job_id is correct and job exists
RES002 404 Strategy not found Check strategy name in /api/v1/strategies endpoint
RES003 404 Model version not found List versions via /api/v1/models/{name}/versions
RES004 404 Config not found Check available configs via /api/v1/strategies
RES005 409 Resource conflict Resource is in a conflicting state (e.g., already terminal)
RES006 410 Resource expired Resource has been archived or deleted

Service Errors (SVC)

Code Status Description Resolution
SVC001 500 Internal server error Check logs, report if persistent
SVC002 502 Bad gateway Upstream service returned invalid response
SVC003 503 Strategy service unavailable Wait and retry, check service health
SVC004 503 Data collection unavailable Wait and retry, check service health
SVC005 503 MLflow unavailable Wait and retry, check MLflow health
SVC006 504 Gateway timeout Increase timeout or retry later

Data Errors (DATA)

Code Status Description Resolution
DATA001 400 Insufficient historical data Extend date range or use different symbol
DATA002 400 Data gap detected Use /api/v1/data/freshness to check data availability
DATA003 404 Symbol not available Check /api/v1/data/symbols for available symbols
DATA004 503 Exchange API error Retry later, check exchange status
DATA005 503 Data storage unavailable Check ArcticDB health

Example Error Response

{
  "detail": {
    "code": "VAL001",
    "message": "Invalid date range: end_date must be >= start_date",
    "field": "end_date",
    "context": {
      "start_date": "2024-03-01",
      "end_date": "2024-01-01"
    }
  }
}

Error Handling Best Practices

import type { ErrorResponse } from './types/backend';

async function submitBacktest(config: BacktestConfig) {
  const { data, error } = await client.POST('/api/v1/backtests', { body: config });

  if (error) {
    const errorCode = error.detail?.code;

    switch (errorCode) {
      case 'VAL001':
        throw new Error('Please select a valid date range');
      case 'AUTH001':
        await refreshToken();
        return submitBacktest(config); // Retry
      case 'SVC003':
      case 'SVC004':
        await delay(5000);
        return submitBacktest(config); // Retry with backoff
      default:
        throw new Error(`API Error: ${error.detail?.message || 'Unknown error'}`);
    }
  }

  return data;
}

API Workflows

Backtest Lifecycle

stateDiagram-v2
    [*] --> PENDING: POST /backtests
    PENDING --> RUNNING: Executor picks up
    RUNNING --> COMPLETED: Success
    RUNNING --> FAILED: Error
    RUNNING --> CANCELLED: Cancel request
    PENDING --> CANCELLED: Cancel before start

Step-by-Step Flow:

  1. Submit - POST /api/v1/backtests creates job, returns job_id
  2. Monitor - Poll GET /api/v1/backtests/{job_id} until terminal state
  3. Results - On completion, GET /api/v1/backtests/{job_id}/equity for charts
  4. Report - GET /api/v1/backtests/{job_id}/report-data for full analysis

Strategy Promotion Pipeline

flowchart LR
    A[Backtest Validation] --> B[Register to MLflow]
    B --> C["Stage (Dry-Run)"]
    C --> D["A/B Test (Optional)"]
    D --> E[Promote to Production]
    E --> F["Deploy (Production)"]

API Sequence:

# 1. Run backtest
POST /api/v1/backtests  job_id

# 2. Wait for completion
GET /api/v1/backtests/{job_id}  status: "completed"

# 3. Register to MLflow
POST /api/v1/strategies/{name}/register
  --backtest_run_id <mlflow_run_id>
  --docker_image_uri <ecr_uri>

# 4. Stage for validation
POST /api/v1/strategies/{name}/stage --version 3

# 5. (Optional) A/B test
POST /api/v1/models/{name}/challenges
  --champion_version 2
  --challenger_version 3

# 6. Promote to production
POST /api/v1/strategies/{name}/promote --version 3

Hyperopt Workflow

flowchart LR
    A["Start Hyperopt\nPOST /hyperopt"] --> B["Monitor Trials\nGET /hyperopt/{id}"]
    B --> C["Get Results\nGET /hyperopt/{id}/result"]
    B --> D["Live Updates\n(WebSocket)"]
    C --> E["Best Params\nApply Config"]

Usage Example:

// 1. Start optimization
const { data: job } = await client.POST('/api/v1/hyperopt', {
  body: {
    strategy: 'TrendFollowingStrategy',
    n_trials: 100,
    objective_metric: 'sharpe_ratio'
  }
});

// 2. Poll for progress
let status = 'running';
while (status === 'running') {
  const { data } = await client.GET('/api/v1/hyperopt/{job_id}', {
    params: { path: { job_id: job.job_id } }
  });
  status = data.status;
  console.log(`Progress: ${data.current_trial}/${data.total_trials}`);
  await delay(5000);
}

// 3. Get best parameters
const { data: result } = await client.GET('/api/v1/hyperopt/{job_id}/result', {
  params: { path: { job_id: job.job_id } }
});
console.log('Best params:', result.best_params);

Champion/Challenger A/B Test

flowchart TD
    A["Create Challenge\nPOST /challenges"] --> B["Route Traffic\nCanary: 5/95"]
    B --> C["Log Trades\nPOST /trades"]
    C --> D["Stage 1: 5%"]
    D --> E["Stage 2: 10%"]
    E --> F["Stage 3: 25%"]
    F --> G["Stage 4: 50%"]
    G --> H{Evaluate}
    H --> I["PROMOTE\nChallenger Wins"]
    H --> J["ROLLBACK\nKeep Champion"]

Evaluation Criteria:

Metric Threshold Description
p_value < 0.05 Statistical significance
improvement_pct > 10% Meaningful improvement
min_trades >= 100 Sufficient sample size

Backend Service (Port 8000)

Base URL: http://localhost:8000/api/v1

Health Check

GET /health

Check service health status with dependency checks.

Response 200:

{
  "status": "healthy",
  "service": "backend",
  "version": "0.1.0",
  "dependencies": {
    "strategy_service": "healthy",
    "data_collection": "healthy",
    "mlflow": "healthy"
  }
}


Backtest Management

POST /api/v1/backtests

Submit a new backtest job.

Request:

{
  "config": {
    "strategy": "TrendFollowingStrategy",
    "symbols": ["BTC/USDT:USDT", "ETH/USDT:USDT"],
    "timeframe": "1h",
    "start_date": "2024-01-01",
    "end_date": "2024-03-01",
    "stake_amount": 100.0
  },
  "experiment_name": "trend-following-btc-eth-q1-2024",
  "priority": 5
}

Response 201:

{
  "job_id": "bt-abc123def456",
  "status": "pending"
}

Errors: 400 (validation), 503 (service unavailable)


GET /api/v1/backtests

List backtest jobs with pagination.

Query Parameters: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | limit | int | 20 | Max results per page (1-100) | | cursor | string | null | Pagination cursor | | status | string | null | Filter by status |

Response 200:

{
  "items": [
    {
      "job_id": "bt-abc123def456",
      "status": "completed",
      "result": { ... },
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-15T11:45:00Z"
    }
  ],
  "next_cursor": "eyJsYXN0X2lkIjogImJ0LXh5eiJ9"
}


GET /api/v1/backtests/{job_id}

Get status of a specific backtest job.

Response 200:

{
  "job_id": "bt-abc123def456",
  "status": "completed",
  "result": {
    "total_trades": 150,
    "winning_trades": 95,
    "losing_trades": 55,
    "total_profit": 2500.50,
    "total_profit_pct": 25.05,
    "sharpe_ratio": 1.85,
    "profit_factor": 1.72,
    "win_rate": 63.33,
    "max_drawdown_pct": 12.5
  },
  "error": null,
  "created_at": "2024-01-15T10:30:00Z",
  "updated_at": "2024-01-15T11:45:00Z"
}

Errors: 400 (invalid job_id), 404 (not found)


POST /api/v1/backtests/{job_id}/cancel

Cancel a running backtest job.

Response 200:

{
  "job_id": "bt-abc123def456",
  "status": "cancelled"
}

Errors: 400 (already terminal), 404 (not found)


GET /api/v1/backtests/{job_id}/equity

Get equity curve data for visualization.

Response 200:

{
  "dates": ["2024-01-01", "2024-01-02", ...],
  "equity": [10000.0, 10150.0, ...],
  "drawdown": [0.0, 0.0, -1.2, ...],
  "monthly_returns": {
    "2024-01": 5.2,
    "2024-02": 8.1
  }
}

Errors: 400 (not completed), 404 (not found)


GET /api/v1/backtests/{job_id}/report-data

Get complete report data for HTML report generation.

Response 200:

{
  "result": { ... },
  "equity_curve": { ... },
  "trades": [...],
  "pair_performance": { ... }
}


Strategy Management

GET /api/v1/strategies

List available strategy configurations.

Response 200:

{
  "configs": ["momentum-v1", "trend-following-v2", "mean-reversion"],
  "count": 3
}


GET /api/v1/strategies/{config_name}

Get a specific strategy configuration.

Response 200:

{
  "strategy": "TrendFollowingStrategy",
  "pairs": ["BTC/USDT:USDT"],
  "timeframe": "1h",
  "stake_amount": 100.0,
  "freqai": {
    "enabled": true,
    "model_type": "LightGBMClassifier"
  }
}


POST /api/v1/strategies/{name}/stage

Stage a strategy model version for validation testing.

Request:

{
  "version": "3"
}

Response 200:

{
  "success": true,
  "message": "Version 3 staged successfully",
  "previous_stage": "None",
  "new_stage": "Staging"
}


POST /api/v1/strategies/{name}/promote

Promote a strategy model version to Production.

Request:

{
  "version": "3",
  "archive_previous": true,
  "skip_validation": false
}

Response 200:

{
  "success": true,
  "message": "Version 3 promoted to Production",
  "archived_versions": ["2"],
  "previous_stage": "Staging",
  "new_stage": "Production"
}


Model Operations

GET /api/v1/models/{model_name}/versions

List all versions of a registered model.

Query Parameters: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | include_archived | bool | true | Include archived versions |

Response 200:

{
  "model_name": "TrendFollowingStrategy",
  "versions": [
    {
      "version": "3",
      "stage": "Production",
      "created_at": "2024-01-15T10:30:00Z"
    },
    {
      "version": "2",
      "stage": "Archived",
      "created_at": "2024-01-10T08:00:00Z"
    }
  ],
  "count": 2
}


POST /api/v1/models/{model_name}/rollback

Rollback a model to a previous version.

Request:

{
  "target_version": "2",
  "dry_run": false
}

Response 200:

{
  "success": true,
  "message": "Rolled back to version 2",
  "previous_production": "3",
  "new_production": "2"
}


Strategy Catalog

GET /api/v1/catalog/strategies

List strategies from the catalog with filtering.

Query Parameters: | Parameter | Type | Description | |-----------|------|-------------| | stage | string | Filter by stage (Production, Staging) | | exchange | string | Filter by exchange | | timeframe | string | Filter by timeframe | | search | string | Text search | | min_sharpe | float | Minimum Sharpe ratio | | max_drawdown | float | Maximum drawdown % | | sort_field | string | Sort by (name, sharpe, drawdown) | | sort_order | string | asc or desc | | limit | int | Page size (1-100) | | offset | int | Page offset |


GET /api/v1/catalog/strategies/{name}/compare

Compare two versions of a strategy.

Query Parameters: | Parameter | Type | Description | |-----------|------|-------------| | version_a | string | First version (champion) | | version_b | string | Second version (challenger) |

Response 200:

{
  "decision": "PROMOTE",
  "confidence": 0.85,
  "metrics": {
    "sharpe_ratio": {"a": 1.5, "b": 1.8, "improvement": 20.0},
    "profit_factor": {"a": 1.6, "b": 1.9, "improvement": 18.75}
  },
  "recommendation": "Challenger outperforms champion with high confidence"
}


Strategy Operations

POST /api/v1/strategies/{strategy_id}/run

Launch a strategy on ECS.

Request:

{
  "mode": "dry_run",
  "timeframe": "1h",
  "symbols": ["BTC/USDT:USDT"],
  "config_overrides": {}
}

Response 201:

{
  "deployment_id": "dep-abc123",
  "instance_id": "task-xyz789",
  "strategy_id": "pascal-v1",
  "mode": "dry_run",
  "status": "running"
}

Errors: 400 (invalid parameters), 503 (ECS not configured)


GET /api/v1/strategies/{strategy_id}/instances

List running instances for a strategy.

Response 200:

{
  "strategy_id": "pascal-v1",
  "instances": [
    {
      "instance_id": "task-xyz789",
      "status": "running",
      "mode": "dry_run",
      "started_at": "2024-01-15T10:30:00Z"
    }
  ],
  "total": 1
}


POST /api/v1/strategies/{strategy_id}/instances/{instance_id}/stop

Stop a running strategy instance.

Response 200:

{
  "instance_id": "task-xyz789",
  "strategy_id": "pascal-v1",
  "success": true,
  "message": "Instance stopped"
}

Errors: 404 (instance not found), 503 (stop failed)


GET /api/v1/strategies/{strategy_id}/instances/{instance_id}/logs

Get logs for a strategy instance from CloudWatch.

Query Parameters: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | lines | int | 100 | Number of log lines (1-1000) |

Response 200:

{
  "strategy_id": "pascal-v1",
  "instance_id": "task-xyz789",
  "entries": [
    {"timestamp": "2024-01-15T10:30:00Z", "message": "Strategy started"}
  ],
  "total": 1
}

Errors: 404 (log stream not found), 503 (CloudWatch query failed)


POST /api/v1/strategies/{strategy_id}/instances/{instance_id}/pause

Pause a running strategy instance. Updates trading state to PAUSED in DynamoDB. The trading container detects the change within 30s and pauses the Freqtrade engine (no new trades, existing positions managed gracefully).

Response 200:

{
  "success": true,
  "message": "Instance paused",
  "instance_id": "task-xyz789",
  "status": "paused"
}

Errors: 404 (instance not found), 503 (ECS/Freqtrade command failed)


POST /api/v1/strategies/{strategy_id}/instances/{instance_id}/resume

Resume a paused strategy instance. Updates trading state to RUNNING in DynamoDB. The trading container detects the change within 30s and resumes the Freqtrade engine.

Response 200:

{
  "success": true,
  "message": "Instance resumed",
  "instance_id": "task-xyz789",
  "status": "running"
}

Errors: 404 (instance not found), 503 (ECS/Freqtrade command failed)


GET /api/v1/trading/status

Get status of all active trading instances with summary statistics.

Response 200:

{
  "instances": [
    {
      "strategy_id": "pascal-v1",
      "instance_id": "task-xyz789",
      "status": "running",
      "mode": "dry_run",
      "started_at": "2024-01-15T10:30:00Z",
      "uptime_seconds": 3600
    }
  ],
  "summary": {
    "total_running": 2,
    "total_paused": 0
  }
}

Errors: 503 (ECS query failed)


GET /api/v1/strategies/{name}/routing-info

Get current A/B test routing configuration for a strategy.

Response 200:

{
  "active_test": true,
  "champion_version": "2",
  "challenger_version": "3",
  "traffic_split": [0.95, 0.05],
  "canary_stage": "stage_1"
}

Errors: 404 (strategy not found), 500 (routing info retrieval failed)


GET /api/v1/strategies/pnl

Get P&L metrics for strategies from the most recent heartbeat cycle.

Query Parameters: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | strategy_id | string | null | Filter by strategy ID |

Response 200:

{
  "strategies": [
    {
      "strategy_id": "pascal-v1",
      "total_profit_pct": 12.5,
      "open_trades": 3
    }
  ],
  "portfolio_summary": {
    "total_profit_pct": 12.5
  }
}

Errors: 503 (service unavailable)


GET /api/v1/strategies/{strategy_id}/trades

Get open trades for a strategy from the most recent heartbeat cycle.

Query Parameters: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | limit | int | 20 | Max trades to return (1-100) |

Response 200:

{
  "trades": [
    {
      "pair": "BTC/USDT:USDT",
      "profit_pct": 2.5,
      "open_date": "2024-01-15T10:30:00Z"
    }
  ],
  "total": 1
}

Errors: 404 (strategy not found), 503 (service unavailable)


WebSocket Ticket

POST /api/v1/ws/ticket

Exchange a valid JWT for a single-use WebSocket ticket. When auth is disabled (dev mode), generates an anonymous ticket so WebSocket flows still work.

Response 200:

{
  "ticket": "wst-abc123def456"
}

The ticket is valid for 30 seconds and can only be used once. Pass it as a query parameter when connecting to the WebSocket:

const ws = new WebSocket('ws://localhost:8000/ws/backtests/bt-abc123?ticket=wst-abc123def456');


Data Proxy

GET /api/v1/data/symbols

Get available trading symbols from exchange.

Query Parameters: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | exchange | string | binance_futures | Exchange key |

Response 200:

{
  "exchange": "binance_futures",
  "symbols": ["BTC/USDT:USDT", "ETH/USDT:USDT", ...],
  "count": 150
}


GET /api/v1/data/freshness

Check data freshness for symbols.

Query Parameters: | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | symbols | list | required | Symbols to check | | stale_threshold_hours | int | 24 | Stale threshold |


Strategy Service (Port 8003)

Base URL: http://localhost:8003/api/v1

Hyperparameter Optimization

POST /hyperopt

Start Optuna hyperparameter optimization.

Request:

{
  "strategy": "TrendFollowingStrategy",
  "pairs": ["BTC/USDT:USDT"],
  "timeframe": "1h",
  "n_trials": 100,
  "train_period_days": 180,
  "backtest_period_days": 30,
  "objective_metric": "sharpe_ratio",
  "direction": "maximize",
  "model_types": ["LightGBMClassifier", "XGBClassifier"],
  "n_estimators_range": [100, 1000],
  "learning_rate_range": [0.01, 0.3]
}

Response 200:

{
  "job_id": "abc12345",
  "status": "pending",
  "message": "Hyperopt job started with 100 trials",
  "n_trials": 100,
  "strategy": "TrendFollowingStrategy"
}


GET /hyperopt/{job_id}

Get hyperopt job status.

Response 200:

{
  "job_id": "abc12345",
  "status": "running",
  "started_at": "2024-01-15T10:30:00Z",
  "progress": 45,
  "current_trial": 45,
  "total_trials": 100,
  "best_value": 1.85
}


GET /hyperopt/{job_id}/result

Get hyperopt results when complete.

Response 200:

{
  "best_params": {
    "model_type": "LightGBMClassifier",
    "n_estimators": 500,
    "learning_rate": 0.05
  },
  "best_value": 2.1,
  "n_trials_completed": 100,
  "study_name": "abc12345"
}


Champion/Challenger A/B Testing

POST /api/v1/models/{model_name}/challenges

Create a champion/challenger A/B test.

Request:

{
  "champion_version": "2",
  "challenger_version": "3",
  "min_trades": 100,
  "max_duration_hours": 168,
  "enable_canary": true
}

Response 201:

{
  "challenge_id": "ch-abc123",
  "model_name": "TrendFollowingStrategy",
  "champion_version": "2",
  "challenger_version": "3",
  "status": "active",
  "canary_stage": "stage_1",
  "traffic_split": [0.95, 0.05]
}


GET /api/v1/models/{model_name}/challenges/active

Get active challenge for a model.

Response 200:

{
  "challenge_id": "ch-abc123",
  "model_name": "TrendFollowingStrategy",
  "champion_version": "2",
  "challenger_version": "3",
  "status": "active",
  "canary_stage": "stage_1",
  "traffic_split": [0.95, 0.05]
}

Returns null if no active challenge exists.


GET /api/v1/challenges/{challenge_id}

Get challenge by ID.

Response 200:

{
  "challenge_id": "ch-abc123",
  "model_name": "TrendFollowingStrategy",
  "champion_version": "2",
  "challenger_version": "3",
  "status": "active",
  "canary_stage": "stage_1",
  "traffic_split": [0.95, 0.05]
}

Errors: 404 (challenge not found)


POST /api/v1/challenges/{challenge_id}/trades

Add a trade result to a challenge.

Request:

{
  "variant": "challenger",
  "profit_pct": 2.5
}

Response 200: Updated ChallengeResponse with current state.

Errors: 400 (invalid variant), 404 (challenge not found)


GET /api/v1/challenges/{challenge_id}/evaluate

Evaluate challenge and get recommendation.

Response 200:

{
  "challenge_id": "ch-abc123",
  "status": "completed",
  "recommendation": "PROMOTE_CHALLENGER",
  "champion_mean": 1.2,
  "challenger_mean": 1.8,
  "improvement_pct": 50.0,
  "p_value": 0.02,
  "is_significant": true
}

Errors: 404 (challenge not found)


POST /api/v1/challenges/{challenge_id}/cancel

Cancel an ongoing challenge.

Response 200:

{
  "challenge_id": "ch-abc123",
  "status": "cancelled",
  "traffic_split": [1.0, 0.0]
}

Errors: 404 (challenge not found)


Shadow Tests

POST /api/v1/models/{model_name}/shadow-tests

Create a shadow test for a model. Runs the candidate version in parallel with production, comparing trade signals without actual execution.

Request:

{
  "production_version": "2",
  "candidate_version": "3",
  "duration_days": 30,
  "metrics_to_compare": ["profit_pct", "win_rate"]
}

Response 201:

{
  "test_id": "st-abc123",
  "model_name": "TrendFollowingStrategy",
  "production_version": "2",
  "candidate_version": "3",
  "status": "active",
  "duration_days": 30
}

Errors: 400 (invalid parameters or active test exists), 404 (model version not found), 503 (MLflow unavailable)


GET /api/v1/models/{model_name}/shadow-tests/active

Get active shadow test for a model.

Response 200:

{
  "test_id": "st-abc123",
  "model_name": "TrendFollowingStrategy",
  "production_version": "2",
  "candidate_version": "3",
  "status": "active",
  "duration_days": 30
}

Returns null if no active shadow test exists.


GET /api/v1/shadow-tests/{test_id}

Get shadow test by ID.

Response 200:

{
  "test_id": "st-abc123",
  "model_name": "TrendFollowingStrategy",
  "production_version": "2",
  "candidate_version": "3",
  "status": "active",
  "duration_days": 30
}

Errors: 404 (shadow test not found)


POST /api/v1/shadow-tests/{test_id}/trades

Add a trade result to a shadow test.

Request:

{
  "variant": "candidate",
  "profit_pct": 1.5
}

Response 200: Updated ShadowTestResponse with current state.

Errors: 400 (invalid variant), 404 (shadow test not found)


GET /api/v1/shadow-tests/{test_id}/metrics

Get metrics for a shadow test including trade counts, mean profit, and win rates for both variants.

Response 200:

{
  "test_id": "st-abc123",
  "production_trades": 85,
  "candidate_trades": 85,
  "production_mean": 1.2,
  "candidate_mean": 1.6,
  "production_win_rate": 58.0,
  "candidate_win_rate": 62.0,
  "improvement_pct": 33.3,
  "evaluated_at": "2024-02-15T10:30:00Z"
}

Errors: 404 (shadow test not found)


Strategy Registration

POST /api/v1/strategies/{name}/register

Register a strategy version after successful backtest.

Request:

{
  "backtest_run_id": "run-abc123",
  "docker_image_uri": "123456789.dkr.ecr.eu-central-1.amazonaws.com/strategy:v1.0.0",
  "description": "Improved trend following with ML enhancements",
  "skip_validation": false,
  "thresholds": {
    "min_sharpe": 1.0,
    "min_profit_factor": 1.2,
    "max_drawdown": 20.0
  }
}

Response 201:

{
  "success": true,
  "message": "Strategy registered successfully",
  "version": "4",
  "model_name": "TrendFollowingStrategy"
}


TypeScript Integration

Generate Types

# Start services first
just up

# Generate TypeScript types from running services
npm run generate-types

Client Setup

import createClient from 'openapi-fetch';
import type { paths } from './types/backend';

// Create client with default configuration
const client = createClient<paths>({
  baseUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
  headers: {
    'Content-Type': 'application/json',
  },
});

// Add authentication interceptor
client.use({
  async onRequest({ request }) {
    const token = await getAuthToken();
    if (token) {
      request.headers.set('Authorization', `Bearer ${token}`);
    }
    return request;
  },
  async onResponse({ response }) {
    // Extract correlation ID for debugging
    const correlationId = response.headers.get('X-Correlation-ID');
    if (correlationId) {
      console.debug(`Request ${correlationId}`);
    }
    return response;
  },
});

Basic Usage

// Type-safe API calls
const { data, error } = await client.POST('/api/v1/backtests', {
  body: {
    config: {
      strategy: 'TrendFollowingStrategy',
      symbols: ['BTC/USDT:USDT'],
      timeframe: '1h',
      start_date: '2024-01-01',
      end_date: '2024-03-01',
      stake_amount: 100.0,
    },
    experiment_name: 'my-backtest',
    priority: 5,
  },
});

if (error) {
  console.error('Failed:', error.detail);
  return;
}

console.log('Job ID:', data.job_id);

Error Handling

import type { ErrorDetail } from './types/backend';

// Retry configuration
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000;

async function withRetry<T>(
  fn: () => Promise<{ data?: T; error?: { detail: ErrorDetail } }>,
  retries = MAX_RETRIES
): Promise<T> {
  const { data, error } = await fn();

  if (error) {
    const code = error.detail?.code;

    // Non-retryable errors
    if (code?.startsWith('VAL') || code?.startsWith('AUTH')) {
      throw new ApiError(error.detail);
    }

    // Retryable service errors
    if (code?.startsWith('SVC') && retries > 0) {
      await delay(RETRY_DELAY * (MAX_RETRIES - retries + 1));
      return withRetry(fn, retries - 1);
    }

    throw new ApiError(error.detail);
  }

  return data!;
}

// Usage
try {
  const result = await withRetry(() =>
    client.POST('/api/v1/backtests', { body: config })
  );
  console.log('Created:', result.job_id);
} catch (e) {
  if (e instanceof ApiError) {
    handleApiError(e);
  }
}

Pagination

// Generic paginated fetcher
async function* fetchPaginated<T>(
  endpoint: string,
  params?: Record<string, unknown>
): AsyncGenerator<T, void, unknown> {
  let cursor: string | null = null;

  do {
    const { data, error } = await client.GET(endpoint, {
      params: {
        query: { ...params, cursor, limit: 50 },
      },
    });

    if (error) throw new ApiError(error.detail);

    for (const item of data.items) {
      yield item as T;
    }

    cursor = data.next_cursor;
  } while (cursor);
}

// Usage: Iterate all backtests
for await (const backtest of fetchPaginated('/api/v1/backtests')) {
  console.log(backtest.job_id, backtest.status);
}

// Or collect all into array
async function getAllBacktests(status?: string) {
  const results = [];
  for await (const bt of fetchPaginated('/api/v1/backtests', { status })) {
    results.push(bt);
  }
  return results;
}

Polling for Job Completion

interface PollOptions {
  interval?: number;
  timeout?: number;
  onProgress?: (status: string) => void;
}

async function pollBacktest(
  jobId: string,
  options: PollOptions = {}
): Promise<BacktestResult> {
  const { interval = 5000, timeout = 600000, onProgress } = options;
  const startTime = Date.now();

  while (Date.now() - startTime < timeout) {
    const { data, error } = await client.GET('/api/v1/backtests/{job_id}', {
      params: { path: { job_id: jobId } },
    });

    if (error) throw new ApiError(error.detail);

    onProgress?.(data.status);

    if (data.status === 'completed') {
      return data.result;
    }

    if (data.status === 'failed') {
      throw new Error(`Backtest failed: ${data.error}`);
    }

    if (data.status === 'cancelled') {
      throw new Error('Backtest was cancelled');
    }

    await delay(interval);
  }

  throw new Error(`Polling timeout after ${timeout}ms`);
}

// Usage
const result = await pollBacktest(jobId, {
  interval: 3000,
  timeout: 300000,
  onProgress: (status) => console.log(`Status: ${status}`),
});

Complete Example: Submit and Monitor Backtest

import createClient from 'openapi-fetch';
import type { paths, components } from './types/backend';

type BacktestConfig = components['schemas']['BacktestConfig'];
type BacktestResult = components['schemas']['BacktestResult'];

const client = createClient<paths>({
  baseUrl: 'http://localhost:8000',
});

async function runBacktest(config: BacktestConfig): Promise<BacktestResult> {
  // 1. Submit backtest
  const { data: job, error: submitError } = await client.POST('/api/v1/backtests', {
    body: {
      config,
      experiment_name: `backtest-${Date.now()}`,
      priority: 5,
    },
  });

  if (submitError) {
    throw new Error(`Submit failed: ${submitError.detail}`);
  }

  console.log(`Job submitted: ${job.job_id}`);

  // 2. Poll until complete
  const result = await pollBacktest(job.job_id, {
    onProgress: (status) => console.log(`[${job.job_id}] ${status}`),
  });

  // 3. Get equity curve
  const { data: equity } = await client.GET('/api/v1/backtests/{job_id}/equity', {
    params: { path: { job_id: job.job_id } },
  });

  return { ...result, equity };
}

// Run it
const result = await runBacktest({
  strategy: 'TrendFollowingStrategy',
  symbols: ['BTC/USDT:USDT', 'ETH/USDT:USDT'],
  timeframe: '1h',
  start_date: '2024-01-01',
  end_date: '2024-06-01',
  stake_amount: 100,
});

console.log(`Sharpe: ${result.sharpe_ratio}, Profit: ${result.total_profit_pct}%`);

Rate Limiting

Rate limiting is not currently implemented. Consider implementing for production: - 100 requests/minute for authenticated users - 20 requests/minute for unauthenticated users


Versioning

API versioning is done via URL path (/api/v1/). Breaking changes will increment the version number.


Support

  • Documentation: /docs on each service
  • Health Check: /api/v1/health on each service
  • Issues: https://github.com/tradai-bot/tradai/issues