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)¶
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:
2. Add to All Authenticated Requests¶
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 (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.tradesis the capped (≤100) list. For the complete trade list useGET /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
openCloudWatch page can returnnext_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/sharedECS_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:
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:
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[] (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"
}
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:
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 (somewscatbuilds). 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 thepersistentstack Cognito output)alg == RS256, signature valid against the user pool's JWKSexpnot yet passedtoken_use == "access"— ID tokens are explicitly rejected- Either
client_idORaudis in the allowed list (so both M2Mclient_credentialsand userauthorization_codeflows 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 closure1006— 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¶
(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: ISO-8601 UTC withZsuffix. Sourced from the container's structured log; falls back to the CloudWatch event timestamp if absent.level:DEBUG|INFO|WARNING|ERROR|CRITICAL. Defaults toINFOif 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:
LOG_FORMAT=json— emits each record as a single-line JSON object. Injected by the backtest Step Function's ECSContainerOverrides.- A
job_idon every record.tradai-common's JSON formatter stamps this automatically from theJOB_IDenv var (also injected by the Step Function), so no per-call boilerplate is needed. A process handling multiple jobs can override per-context withset_job_id(job_id)fromtradai.common. - A root
INFOhandler. Backtest progress logs atINFO; without an INFO-capable root handler those records fall through to Python'slastResort(WARNING-only) and never leave the container. Usingtradai-common'sLoggerMixinguarantees this; a plainloggingsetup must calllogging.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