Skip to content

Frontend API Integration Guide

Overview

This document provides the complete API reference for Frontend (Amplify) integration with TradAI platform. All endpoints are exposed through AWS API Gateway with JWT authentication via Cognito.


Quick Start

1. Get AWS Endpoints

AWS_PROFILE=tradai aws cloudformation describe-stacks \
  --stack-name tradai-edge-dev \
  --query 'Stacks[0].Outputs' \
  --region eu-central-1

2. Configure Amplify

// amplify.config.ts
import { Amplify } from 'aws-amplify';

Amplify.configure({
  Auth: {
    Cognito: {
      region: 'eu-central-1',
      userPoolId: 'eu-central-1_xxxxx',
      userPoolClientId: 'xxxxx',
      identityPoolId: 'eu-central-1:xxxxx',
    },
  },
  API: {
    REST: {
      endpoint: 'https://api.example.com', // or API Gateway endpoint
      region: 'eu-central-1',
    },
  },
});

3. Authenticate & Call API

import { signIn, fetchAuthSession } from 'aws-amplify/auth';
import { get } from 'aws-amplify/api';

// Login
const { isSignedIn, userId } = await signIn({ username, password });

// Get JWT Token
const session = await fetchAuthSession();
const token = session.tokens?.accessToken?.toString();

// Call API
const response = await get({
  apiName: 'tradai',
  path: '/api/v1/backtests',
  options: {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  },
}).response;

API Infrastructure

Network Architecture

  • API Gateway: HTTP API with VPC Link to ALB (Cognito JWT auth)
  • ALB: Proxies to ECS services via path-based routing
  • Service Discovery: Internal service communication via tradai-{env}.local
  • Authentication: Cognito User Pool JWT tokens

Deployment Stacks

Stack Components Outputs
persistent DynamoDB, S3, ECR, Cognito, Secrets Manager cognito_user_pool_id, cognito_user_pool_client_id, cognito_user_pool_endpoint
foundation VPC, Subnets, NAT, RDS, SQS, SNS vpc_id, private_subnet_ids, rds_endpoint
compute ALB, ECS, Lambda, Step Functions alb_dns_name, alb_https_listener_arn
edge API Gateway, WAF, CloudWatch api_gateway_endpoint, api_gateway_id, api_custom_domain

Service Endpoints (Internal)

Service Health Check Port Discovery Name
backend-api /api/v1/health 8000 backend-api.tradai-{env}.local
data-collection /api/v1/health 8002 data-collection.tradai-{env}.local
strategy-service /api/v1/health 8003 strategy-service.tradai-{env}.local
mlflow /mlflow/ 5000 mlflow.tradai-{env}.local

Base URLs

Development (Local)

HTTP:  http://localhost:8000
WS:    ws://localhost:8000

AWS (API Gateway)

HTTP:  https://{API_GATEWAY_ENDPOINT}  (auto-generated)
       https://{API_DOMAIN}             (custom domain if configured)
WS:    wss://{API_GATEWAY_ENDPOINT}
       wss://{API_DOMAIN}

Authentication Flow

1. Cognito OAuth2 Token Endpoint

Endpoint: https://{cognito-endpoint}/oauth2/token

Request (Client Credentials):

curl -X POST https://{cognito-endpoint}/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "client_id={CLIENT_ID}" \
  -d "client_secret={CLIENT_SECRET}" \
  -d "grant_type=client_credentials" \
  -d "scope=tradai-api/read"

Response:

{
  "access_token": "eyJhbGc...",
  "token_type": "Bearer",
  "expires_in": 3600
}

2. Add to All Authenticated Requests

Authorization: Bearer {access_token}

JWT Token Details

  • Issuer: https://{cognito-endpoint}
  • Audience: {cognito_user_pool_client_id} or {cognito_m2m_client_id}
  • Expiry: 3600 seconds (1 hour)
  • Algorithm: RS256

Complete API Routes

All routes require JWT authentication except /api/v1/health.

Rate Limiting

  • Default: 100 req/s, burst 200
  • POST /api/v1/backtests: 10 req/s, burst 20 (throttled)

Route Table

HTTP Path Auth Rate Limit Purpose
Health & Status
GET /api/v1/health Health check
Backtests
POST /api/v1/backtests 10/s Submit backtest job
GET /api/v1/backtests 100/s List backtests (paginated)
GET /api/v1/backtests/{job_id} 100/s Get backtest status
POST /api/v1/backtests/{job_id}/cancel 100/s Cancel backtest
GET /api/v1/backtests/{job_id}/equity 100/s Equity curve data
GET /api/v1/backtests/{job_id}/report-data 100/s Full report data
GET /api/v1/backtests/{job_id}/trades 100/s Full trade list (from S3 artifact)
GET /api/v1/backtests/{job_id}/logs 100/s Historical logs (from CloudWatch)
Strategies
GET /api/v1/strategies 100/s List strategies
GET /api/v1/strategies/{id} 100/s Get strategy details
POST /api/v1/strategies 100/s Create strategy
POST /api/v1/strategies/{name}/rollback 100/s Roll ACTIVE back to a prior Release
POST /api/v1/strategies/{name}/promote 100/s Promote to production
Catalog & Leaderboard
GET /api/v1/catalog/strategies 100/s Strategy leaderboard
GET /api/v1/catalog/strategies/{name} 100/s Catalog entry details
GET /api/v1/catalog/strategies/{name}/compare 100/s Compare versions
Data Management
GET /api/v1/data/symbols 100/s Available symbols
GET /api/v1/data/freshness 100/s Data freshness status
GET /api/v1/data/quality 100/s Data quality (gaps + anomalies + completeness)
POST /api/v1/data/sync 100/s Sync market data
Model Management
GET /api/v1/models/{name}/versions 100/s List model versions
POST /api/v1/models/{name}/rollback 100/s Rollback version
MLflow Proxy
ANY /mlflow/{proxy+} 100/s MLflow API (wildcard)

Endpoint Details

Backtests

POST /api/v1/backtests

Submit a new backtest job (async).

Request:

{
  "strategy_config": {
    "name": "MyStrategy",
    "version": "1.0.0",
    "parameters": {
      "ma_period": 20,
      "threshold": 0.02
    }
  },
  "date_range": {
    "start": "2024-01-01T00:00:00Z",
    "end": "2024-12-31T23:59:59Z"
  },
  "exchange": "binance_futures",
  "timeframe": "1h",
  "capital": 10000,
  "leverage": 1.0
}

Response (201):

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "QUEUED",
  "created_at": "2024-01-01T00:00:00Z"
}

Errors: - 400: Validation error (invalid strategy, date range) - 422: Unprocessable entity (invalid capital/leverage) - 429: Rate limited (>10 req/s per IP)


GET /api/v1/backtests

List backtest jobs with pagination.

Query Parameters:

?limit=20&cursor=abc123&status_param=COMPLETED
- limit (int, 1-100, default=20): Results per page - cursor (string, optional): Pagination cursor - status_param (enum, optional): QUEUED | RUNNING | COMPLETED | FAILED | CANCELLED

Response:

{
  "jobs": [
    {
      "job_id": "550e8400-e29b-41d4-a716-446655440000",
      "status": "COMPLETED",
      "strategy_name": "MyStrategy",
      "strategy_version": "1.0.0",
      "created_at": "2024-01-01T00:00:00Z",
      "completed_at": "2024-01-02T12:30:45Z",
      "result": {
        "total_return": 0.1234,
        "sharpe_ratio": 1.45,
        "max_drawdown": -0.082,
        "trades_count": 42,
        "win_rate": 0.619,
        "profit_factor": 1.89
      }
    }
  ],
  "cursor": "next_cursor_token",
  "total": 156
}


GET /api/v1/backtests/{job_id}

Get specific backtest status and progress.

Path Parameters: - job_id (uuid): Backtest job ID

Response:

{
  "job_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "RUNNING",
  "strategy_name": "MyStrategy",
  "strategy_version": "1.0.0",
  "created_at": "2024-01-01T00:00:00Z",
  "progress": {
    "current": 75,
    "total": 100,
    "percentage": 75
  },
  "message": "Processing 2024-10 data..."
}

Statuses: - QUEUED: Waiting to start - RUNNING: Currently executing - COMPLETED: Finished successfully - FAILED: Execution error - CANCELLED: User cancelled


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

Get equity curve data for visualization.

Response:

{
  "equity_curve": [
    {
      "timestamp": "2024-01-01T00:00:00Z",
      "portfolio_value": 10000,
      "drawdown_percent": 0
    },
    {
      "timestamp": "2024-01-01T01:00:00Z",
      "portfolio_value": 10150,
      "drawdown_percent": 0
    }
  ]
}


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

Get HTML-ready report data (for PDF/HTML generation).

Response:

{
  "summary": {
    "strategy": "MyStrategy",
    "period": "2024-01-01 to 2024-12-31"
  },
  "metrics": {
    "total_return": 0.1234,
    "sharpe_ratio": 1.45,
    "max_drawdown": -0.082,
    "trades": 42,
    "win_rate": 0.619
  },
  "trades": [
    {
      "entry_time": "2024-01-15T10:30:00Z",
      "exit_time": "2024-01-15T14:00:00Z",
      "symbol": "BTC/USDT:USDT",
      "side": "long",
      "entry_price": 42000,
      "exit_price": 42500,
      "pnl": 500
    }
  ],
  "charts": {
    "equity_curve_url": "...",
    "drawdown_url": "..."
  }
}

Note: report-data.trades is the capped (≤100) list. For the complete trade list use GET /api/v1/backtests/{job_id}/trades.


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

Get the complete trade list. raw_stats caps embedded trades for storage, so the full set is served from the S3 results artifact; falls back to the capped list (with truncated: true) when the artifact is unavailable.

Response:

{
  "job_id": "bt-a1b2c3d4",
  "trades": [ { "pair": "BTC/USDT:USDT", "profit_ratio": 0.012 } ],
  "total": 142,
  "source": "s3_artifact",
  "truncated": false
}

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

Historical log lines for a past (or running) backtest. Served from the durable job-log ledger while authoritative, else CloudWatch (see "Source selection & stream_state" below). Each frame carries a source discriminator — ledger frames add seq/cursor; cloudwatch frames do not. Unknown jobs return 404; a malformed next_token returns 400.

Query params:

Param Type Default Notes
limit int (1-1000) 200 Max frames per page
next_token string Opaque, source-tagged pagination token from a previous response
level string Minimum severity (e.g. WARNING keeps WARNING+ERROR)

Response:

{
  "job_id": "bt-a1b2c3d4",
  "entries": [
    { "source": "ledger", "ts": "2026-06-26T10:00:00.123Z", "level": "INFO", "msg": "Backtest started", "seq": 0, "cursor": "F#000000#000000000" },
    { "source": "cloudwatch", "ts": "2026-06-26T10:00:05.900Z", "level": "ERROR", "msg": "boom" }
  ],
  "next_token": null,
  "stream_state": "complete"
}

Page through history by passing the response's next_token back as the next_token query param.

Source selection & stream_state (#785): logs are served from the durable job-log ledger while a job's manifest is authoritative, else from CloudWatch (legacy/pre-producer jobs, an incomplete session, or a fail-open ledger error). Each response carries a stream_state that tells the client what to do next, orthogonal to next_token:

stream_state meaning client action
open producer live, this page has data keep polling with next_token
caught_up producer live, drained for now (ledger only) back off, then resume with next_token (non-null)
complete terminal — no more data will arrive drain any remaining next_token, then stop

So: stop polling when stream_state is complete and next_token is null. A caught_up page returns a non-null token even with no new lines — do not treat that as "done". Each frame carries a source discriminator (ledger frames also carry seq/cursor; cloudwatch frames do not).

CloudWatch caveat: the ledger path always hands back a resumable token while a job is running. The CloudWatch source (the default until the producer ships, and the fallback) has no resume cursor when it is momentarily caught up, so an open CloudWatch page can return next_token: null. Keep polling (a running job still produces logs) but re-request from the start; frames may repeat — dedupe by (ts, msg). A resumable CloudWatch high-water cursor is tracked for the dashboard drain work (#789).

Retention caveat: CloudWatch Logs retention is finite — 14 days in dev, 30 days in prod (infra/shared ECS_LOG_RETENTION_DAYS). Logs for backtests older than the retention window are gone. Archiving older history to S3 is a possible future phase.


Strategies

GET /api/v1/strategies

List all strategies with filtering and sorting.

Query Parameters:

?stage=prod&exchange=binance_futures&timeframe=1h&search=trend&min_sharpe=1.0&sort_by=sharpe&sort_order=desc&limit=20&offset=0
- stage (enum): dev | staging | prod - exchange (string): binance_futures, etc. - timeframe (string): 1m, 5m, 1h, 4h, 1d - search (string): Keyword search - min_sharpe (float): Minimum Sharpe ratio filter - max_drawdown (float): Maximum drawdown % - sort_by (enum): name | sharpe | return | trades - sort_order (enum): asc | desc - limit (int, 1-100, default=20) - offset (int, pagination)

Response:

{
  "strategies": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "TrendFollower",
      "version": "1.0.0",
      "stage": "prod",
      "description": "SMA crossover trend following strategy",
      "metrics": {
        "sharpe_ratio": 1.67,
        "total_return": 0.2345,
        "max_drawdown": -0.095,
        "trades": 156,
        "win_rate": 0.641
      },
      "created_at": "2023-06-15T10:00:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    }
  ],
  "total": 42,
  "limit": 20,
  "offset": 0
}


GET /api/v1/catalog/strategies

Strategy leaderboard (composite scoring).

Same parameters and response structure as /api/v1/strategies, but with additional leaderboard ranking.


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

Gate a challenger Release and, if it passes, atomically swap the ACTIVE pointer onto it (v4 pointer plane). promoted: false means the gate rejected it (audited, no swap); a lost epoch race returns 409.

Path Parameters: - name (string): Strategy name

Request:

{
  "environment": "prod",
  "release_deployment_id": "PROD#REL#01ARZ3NDEKTSV4RRFFQ69G5FAV"
}

Response:

{
  "promoted": true,
  "new_epoch": 6,
  "release_deployment_id": "PROD#REL#01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "decision": { "passed": true, "violations": [] }
}


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

Roll ACTIVE back onto a prior Release (incident; bypasses the gate, still epoch-guarded).

Path Parameters: - name (string): Strategy name

Request:

{
  "environment": "prod",
  "release_deployment_id": "PROD#REL#01ARZ3NDEKTSV4RRFFQ69G5FAU"
}

Response: the updated ActivePointerRecord (new epoch, release_deployment_id).


Data Management

GET /api/v1/data/symbols?exchange=binance_futures

Get available trading symbols.

Query Parameters: - exchange (string, default="binance_futures"): Exchange identifier. Ignored when source="stored" — storage is shared across exchanges and returns all symbols in the library. The value is still echoed in the response. - source (string, default="exchange"): "exchange" lists every symbol the exchange advertises via CCXT; "stored" lists only symbols that already have OHLCV rows in ArcticDB.

Response:

{
  "symbols": [
    "BTC/USDT:USDT",
    "ETH/USDT:USDT",
    "SOL/USDT:USDT",
    "ADA/USDT:USDT"
  ],
  "exchange": "binance_futures",
  "count": 250,
  "last_updated": "2024-01-01T12:00:00Z"
}


GET /api/v1/data/freshness

Check data freshness for symbols.

Query Parameters:

?symbols[]=BTC/USDT:USDT&symbols[]=ETH/USDT:USDT&stale_threshold_hours=24
- symbols[] (array): Symbols to check - stale_threshold_hours (int, default=24): Freshness threshold

Response:

{
  "freshness": {
    "BTC/USDT:USDT": {
      "last_update": "2024-01-01T23:00:00Z",
      "hours_old": 5,
      "is_stale": false
    },
    "ETH/USDT:USDT": {
      "last_update": "2024-01-01T10:00:00Z",
      "hours_old": 18,
      "is_stale": false
    }
  },
  "threshold_hours": 24,
  "checked_at": "2024-01-02T04:00:00Z"
}


GET /api/v1/data/quality

On-the-fly data quality report (issue #627 Phase 1). Wraps the platform DataQualityService — no persisted lifecycle yet (active-vs-resolved, MTTD timeline arrive in Phase 2).

Query Parameters:

?symbols=BTC/USDT:USDT&symbols=ETH/USDT:USDT&timeframes=1h&timeframes=4h&start_date=2024-01-01&end_date=2024-01-31
- symbols (array, required): symbols to check (comma-separated or repeated). - timeframes (array, optional): defaults to ["1h"]. Comma-separated or repeated. - start_date / end_date (string, required, YYYY-MM-DD): inclusive range to analyse.

Response:

{
  "symbols": [
    {
      "symbol": "BTC/USDT:USDT",
      "timeframe": "1h",
      "completeness_pct": 99.8,
      "freshness_hours": 1.3,
      "total_candles": 743,
      "expected_candles": 744,
      "latest_date": "2024-01-31T23:00:00Z",
      "gaps": [
        {
          "start": "2024-01-15T03:00:00Z",
          "end": "2024-01-15T04:00:00Z",
          "expected_candles": 1,
          "missing_candles": 1,
          "duration_hours": 1.0
        }
      ],
      "anomalies": [
        {
          "date": "2024-01-20T12:00:00Z",
          "anomaly_type": "price_spike",
          "field": "close",
          "value": 99000.0,
          "expected_range": [40000.0, 60000.0],
          "message": "close diverges from rolling band"
        }
      ]
    }
  ],
  "checked_at": "2024-02-01T00:00:00Z"
}
Rows are flat — one entry per (symbol, timeframe) pair — so the shape is identical whether one or many timeframes are requested.

Anomaly types: price_spike, zero_volume, price_zero, ohlc_invalid, timestamp_duplicate, timestamp_out_of_order.

Errors: - 422: invalid date format / unknown timeframe / no symbols - 503: Data Collection Service unavailable


POST /api/v1/data/sync

Sync market data from exchange.

Request:

{
  "symbols": ["BTC/USDT:USDT", "ETH/USDT:USDT"],
  "exchange": "binance_futures",
  "force_refresh": false
}

Response:

{
  "status": "completed",
  "symbols_synced": 2,
  "new_rows": 480,
  "sync_duration_ms": 2340,
  "completed_at": "2024-01-02T04:15:30Z"
}


Model Management

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

List all versions of a model.

Query Parameters: - include_archived (bool, default=true): Include archived versions

Response:

{
  "model_name": "price-predictor",
  "versions": [
    {
      "version": "1.2.0",
      "stage": "prod",
      "created_at": "2024-01-01T00:00:00Z",
      "metrics": {
        "accuracy": 0.924,
        "auc": 0.881,
        "f1_score": 0.867
      },
      "archived": false
    },
    {
      "version": "1.1.0",
      "stage": "archived",
      "created_at": "2023-12-15T00:00:00Z",
      "metrics": {
        "accuracy": 0.912,
        "auc": 0.868,
        "f1_score": 0.854
      },
      "archived": true
    }
  ]
}


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

Rollback to a previous model version.

Path Parameters: - model_name (string): Model name

Request:

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

Response:

{
  "model_name": "price-predictor",
  "previous_version": "1.2.0",
  "rolled_back_to": "1.1.0",
  "status": "completed",
  "timestamp": "2024-01-02T04:30:00Z"
}


WebSocket Real-Time Updates

Use this: wss://realtime.{env}.tradai-system.com/{feed}

Direct Cognito-auth WebSocket on a dedicated custom domain. The same Cognito access token you already use for the HTTP API works unchanged. No ticket exchange, no backend round-trip.

Env URL
dev wss://realtime.dev.tradai-system.com/backtests
staging/prod not yet enabled (api_gateway:enable_websocket_api gate is off)

Future feeds (/positions, /orders, …) plug in as additional path keys under the same domain — same ACM cert, same Route 53 record, same authorizer.

Auth — Cognito JWT in Sec-WebSocket-Protocol (preferred for browsers)

The server echoes back Sec-WebSocket-Protocol: tradai-jwt on the upgrade response, so browsers don't close 1006 per RFC 6455 §4.1. The JWT never lands in the URL, browser history, Referer header, or APIGW access logs.

import { fetchAuthSession } from 'aws-amplify/auth'

// Never hard-code the dev host in shipped code. staging/prod are gated off
// today (see the Env table above); point this at the right host per
// environment once they're enabled.
const WS_BASE = process.env.NEXT_PUBLIC_WS_BASE_URL ?? 'wss://realtime.dev.tradai-system.com'
const jobId = 'ee975747-5429-4017-93fd-0e2fc736a6e9' // example job UUID from POST /api/v1/backtests

const session = await fetchAuthSession()
const accessToken = session.tokens?.accessToken?.toString()
if (!accessToken) throw new Error('not signed in')

const url = `${WS_BASE}/backtests`
          + `?job_id=${encodeURIComponent(jobId)}`

// Two-token form. First token MUST be the literal "tradai-jwt" (the marker
// the server echoes back); second is the access token (verified, never echoed).
const ws = new WebSocket(url, ['tradai-jwt', accessToken])

ws.onopen    = ()  => console.log('connected')
ws.onmessage = (e) => handleProgress(JSON.parse(e.data))
ws.onerror   = (e) => console.error('ws error', e)
ws.onclose   = (e) => console.log('closed', e.code, e.reason)

Single-token form (new WebSocket(url, [accessToken])) does NOT work in browsers. The authorizer accepts it, but the server can't safely echo the JWT as a subprotocol so no echo is sent, and the browser closes 1006. Single-token form only works for permissive non-browser clients (some wscat builds). Always use the two-token form from the FE.

Auth — query-string fallback (debug/tooling only)

# WS_BASE is the dev host here; swap it per environment (staging/prod gated off today).
WS_BASE="wss://realtime.dev.tradai-system.com"
ACCESS_TOKEN=$(curl -sX POST https://tradai-dev.auth.eu-central-1.amazoncognito.com/oauth2/token \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  -d "grant_type=client_credentials&scope=tradai-api/read" | jq -r .access_token)

wscat -c "$WS_BASE/backtests?job_id=smoke&Authorization=$ACCESS_TOKEN"

APIGW access logs are disabled on the WS stage today (AccessLogSettings: null), so the token isn't being captured by AWS-side logging. It is still in the URL though — visible in browser devtools, possibly cached by intermediary proxies. Never use this from a production browser app.

What the authorizer accepts

  • iss == https://cognito-idp.eu-central-1.amazonaws.com/{user-pool-id} (e.g. eu-central-1_XXXXXXXXX; the live value comes from the persistent stack Cognito output)
  • alg == RS256, signature valid against the user pool's JWKS
  • exp not yet passed
  • token_use == "access" — ID tokens are explicitly rejected
  • Either client_id OR aud is in the allowed list (so both M2M client_credentials and user authorization_code flows work)

principalId = sub (or client_id for M2M tokens). Persisted on the connection row as user_id so future per-user filtering works.

Path & query parameters

Param Where Required Purpose
{feed} path yes backtests today; future feeds get their own keys
job_id query yes for /backtests The UUID returned by POST /api/v1/backtests. Only this connection receives that job's frames.
Authorization query yes (fallback only) Cognito access token. Use the subprotocol form in browsers instead.

For future feeds the query-param name will be feed-specific (?user_id=… for /positions, ?account_id=… for /orders).

Server-pushed messages — payload shape

Frames arrive when the job state changes — typically running then completed/failed/cancelled. status is always lowercase.

Progress frame:

{
  "job_id": "ee975747-5429-4017-93fd-0e2fc736a6e9",
  "status": "running",
  "error": null,
  "created_at": "2026-06-24T19:32:14.812Z",
  "updated_at": "2026-06-24T19:33:47.110Z"
}

Terminal frame (completed):

{
  "job_id": "ee975747-5429-4017-93fd-0e2fc736a6e9",
  "status": "completed",
  "error": null,
  "created_at": "2026-06-24T19:32:14.812Z",
  "updated_at": "2026-06-24T19:36:01.555Z",
  "final": true,
  "message": "Backtest completed",
  "result": {
    "total_trades": 14,
    "total_profit_pct": 2.13,
    "sharpe_ratio": 1.45,
    "win_rate": 0.571
  }
}

Terminal frame (failed):

{
  "job_id": "...",
  "status": "failed",
  "error": "Symbol BTC/USDT:USDT has no data for 2026-06-17..2026-06-18",
  "created_at": "...",
  "updated_at": "...",
  "final": true,
  "message": "Backtest failed"
}

Terminal frame (cancelled):

{
  "job_id": "...",
  "status": "cancelled",
  "error": null,
  "created_at": "...",
  "updated_at": "...",
  "final": true,
  "message": "Backtest cancelled"
}

error carries a human-readable reason only on status: "failed"; it is null on running, completed, and cancelled. message always mirrors the status verbatim — "Backtest completed" / "Backtest failed" / "Backtest cancelled". Branch on status, not on error being truthy, so a cancelled frame is not silently swallowed by a failed-only check:

if (frame.status === 'failed')      showError(frame.error)   // error is populated
else if (frame.status === 'cancelled') showCancelled()       // error is null
else if (frame.status === 'completed') showResult(frame.result)

final: true only appears on the terminal frame. Use it to know when to close the socket and stop listening.

Close codes

  • 1000 — normal closure
  • 1006 — abnormal closure. In the new world this should be rare; if you see it on the subprotocol path, double-check you pass exactly ['tradai-jwt', accessToken] and that the token is fresh.
  • 1008 — policy violation (auth failed at $connect). Re-mint the JWT and reconnect.
  • 1011 — server error

Reconnect & token lifetime

Cognito access tokens are valid for 1 hour. APIGW only authorizes once at $connect; an in-flight WS that outlives the token does not get re-checked. If you keep a socket open for hours, plan reconnect-with-fresh-token before expiry (e.g., every 50 minutes).

Feed: logs — live backtest/training logs

wss://realtime.{env}.tradai-system.com/logs?job_id={job_id}

(dev host today: wss://realtime.dev.tradai-system.com/logs — resolve per environment, same as WS_BASE above.)

Same auth as backtests (Cognito access token via Sec-WebSocket-Protocol two-token form). Subscribes to the running job's stdout/stderr, fanned out from the ECS task's CloudWatch log group through a subscription filter.

Frame schema (one frame per log line):

{
  "ts": "2026-06-26T10:00:00.123Z",
  "level": "INFO",
  "msg": "backtest step 10 complete"
}
  • ts: ISO-8601 UTC with Z suffix. Sourced from the container's structured log; falls back to the CloudWatch event timestamp if absent.
  • level: DEBUG | INFO | WARNING | ERROR | CRITICAL. Defaults to INFO if the container omits it.
  • msg: the log line message.

Latency: ~1–3 s (CloudWatch subscription filter batching). Fine for a "feels live" console view; not suitable for sub-second tailing.

No backfill: clients only see lines emitted AFTER the WS connection opens. This matches the historical dashboard behavior.

Drop policy: lines that are not JSON, or JSON without a job_id field, are dropped at the CloudWatch filter and never reach a connected client. This is by design — only structured backtest/training logs carry the routing key and should be streamed.

Producer requirements (why a line has a job_id): the emitting container must satisfy all three, or the line is dropped at the subscription filter and the feed is silent:

  1. LOG_FORMAT=json — emits each record as a single-line JSON object. Injected by the backtest Step Function's ECS ContainerOverrides.
  2. A job_id on every record. tradai-common's JSON formatter stamps this automatically from the JOB_ID env var (also injected by the Step Function), so no per-call boilerplate is needed. A process handling multiple jobs can override per-context with set_job_id(job_id) from tradai.common.
  3. A root INFO handler. Backtest progress logs at INFO; without an INFO-capable root handler those records fall through to Python's lastResort (WARNING-only) and never leave the container. Using tradai-common's LoggerMixin guarantees this; a plain logging setup must call logging.basicConfig(level=logging.INFO).

Setting LOG_FORMAT=json alone is not sufficient — all three conditions above must hold.

Deprecated paths (still working today, removed in a follow-up)

Path Status
POST /api/v1/ws/ticket 200, but emits Deprecation + Sunset: Thu, 24 Jul 2026 10:00:00 GMT response headers
wss://api-dev.tradai-system.com/ws/backtests/{job_id}?ticket=… Still routes; emits deprecation log per connection

Migrate to wss://realtime.{env}.tradai-system.com/{feed} before 2026-07-24.


Error Handling

HTTP Status Codes

Code Meaning Example
200 Success GET request completed
201 Created POST /backtests accepted
400 Bad Request Invalid parameters
401 Unauthorized Missing/invalid JWT
403 Forbidden Insufficient permissions
404 Not Found Job/strategy doesn't exist
422 Unprocessable Invalid data (semantic error)
429 Too Many Requests Rate limit exceeded
500 Server Error Internal error
503 Service Unavailable Service down

Error Response Format

{
  "detail": "Descriptive error message",
  "code": "ERROR_CODE",
  "timestamp": "2024-01-01T00:00:00Z"
}

CORS Configuration

Allowed Origins (configurable): - Dev: * (all) - Staging/Prod: Set via Pulumi config

Allowed Methods: GET, POST, PUT, DELETE, OPTIONS Allowed Headers: Authorization, Content-Type, X-Amz-Date Max Age: 86400 (24 hours)


Implementation Checklist

  • Retrieve AWS outputs (Cognito, API Gateway endpoints)
  • Configure Amplify with Cognito credentials
  • Implement JWT token acquisition
  • Add Authorization header to all authenticated requests
  • Test GET /api/v1/health (public endpoint)
  • Test authentication flow with token refresh
  • Implement pagination for list endpoints
  • Handle WebSocket connection with single-use tickets
  • Implement rate limiting retry logic (429 responses)
  • Test CORS preflight requests
  • Verify error handling for all status codes
  • Implement exponential backoff for retries
  • Test with mock data before live integration

Reference Files

  • Config: infra/shared/tradai_infra_shared/config.py
  • API Gateway: infra/edge/modules/api_gateway.py
  • Backend API: services/backend/src/tradai/backend/api/
  • Data Collection: services/data-collection/src/tradai/data_collection/api/
  • Strategy Service: services/strategy-service/src/tradai/strategy_service/api/

Last Updated: 2024-01-02 Document Version: 1.0