API Endpoint Audit — Dev Environment¶
Date: 2026-04-23 ALB: http://tradai-dev-1942285475.eu-central-1.elb.amazonaws.com Region: eu-central-1, Account 600802701449 Tested from: macOS / Windows (curl)
Routing Architecture¶
Internet
|
ALB (port 80)
├── /mlflow, /mlflow/* ──► MLflow ECS Service (port 5000)
├── /api/v1/live* ──► Live-Trading ECS Service (port 8004)
├── /api/v1/dry-run* ──► Dry-Run-Trading ECS Service (port 8005)
└── /api/v1/* ──► Backend ECS Service (port 8000)
├── internal proxy ──► strategy-service:8003 (Service Discovery)
└── internal proxy ──► data-collection:8002 (Service Discovery)
Strategy-service and data-collection are not exposed via ALB — only reachable through the backend's internal HTTP proxy over ECS Service Discovery.
Summary¶
| Category | Count | Details |
|---|---|---|
| Working (200) | 14 | Production-ready |
| Validation (422/405) | 7 | Endpoint works, needs correct payload |
| Real bugs | 7 | Need code / infra fixes |
| Not found (by design) | 10+ | Route does not exist in code |
1. Working Endpoints (200)¶
ALB="http://tradai-dev-1942285475.eu-central-1.elb.amazonaws.com"
# Health
curl -s "$ALB/api/v1/health" | jq .
# → {"healthy":true,"status":"healthy","service":"Backend","version":"0.1.0",
# "checks":[dynamodb:ok, sqs:ok, data_collection:ok, strategy_service:ok]}
# Strategy list (proxied to strategy-service)
curl -s "$ALB/api/v1/strategies" | jq .
# → {"configs":["pascal-v2"],"count":1}
# Strategy detail (by config name, not strategy class name)
curl -s "$ALB/api/v1/strategies/pascal-v2" | jq .
# → {"name":"PascalStrategy","version":"2.0.0","timeframe":"1h","pairs":["BTC/USDT:USDT"],...}
# Catalog
curl -s "$ALB/api/v1/catalog/strategies" | jq .
# → {"strategies":[],"total":0,"limit":20,"offset":0}
curl -s "$ALB/api/v1/catalog/strategies/PascalStrategy" | jq .
# → 200 (individual lookup works even when list is empty)
curl -s "$ALB/api/v1/catalog/leaderboard" | jq .
# → {"entries":[],"total":0,"limit":10,"offset":0}
# Config versions
curl -s "$ALB/api/v1/configs/E2ETestStrategy" | jq .
# → {"strategy_name":"E2ETestStrategy","items":[],"total":0}
# Model versions (from MLflow Registry)
curl -s "$ALB/api/v1/models/E2ETestStrategy/versions" | jq .
# → {"model_name":"E2ETestStrategy","versions":[{"version":"34","status":"READY",...}]}
# Data
curl -s "$ALB/api/v1/data/symbols" | jq '.symbols[:3]'
# → ["0G/USDT:USDT","1000000BOB/USDT:USDT","1000000MOG/USDT:USDT"]
curl -s "$ALB/api/v1/data/freshness?symbols=BTC%2FUSDT%3AUSDT" | jq .
# → {"symbols":[{"symbol":"BTC/USDT:USDT","latest_date":"2026-04-22T00:00:00Z","is_stale":true}]}
# Trading status
curl -s "$ALB/api/v1/trading/status" | jq .
# → {"instances":[],"summary":{"running":0,"total":0,...}}
# A/B routing info
curl -s "$ALB/api/v1/strategies/E2ETestStrategy/routing-info" | jq .
# → {"active_test":false,"champion_version":null,...}
# WebSocket ticket
curl -s -X POST "$ALB/api/v1/ws/ticket" | jq .
# → 200
# MLflow UI
curl -s -o /dev/null -w "%{http_code}" "$ALB/mlflow/"
# → 200
2. Validation Errors (422 / 405) — Endpoints work, need correct payload¶
# POST /api/v1/backtests — missing: config, experiment_name
curl -s -X POST -H "Content-Type: application/json" \
-d '{"strategy":"E2ETestStrategy"}' "$ALB/api/v1/backtests" | jq .
# POST /api/v1/configs — missing: config_data
curl -s -X POST -H "Content-Type: application/json" \
-d '{"strategy_name":"X"}' "$ALB/api/v1/configs" | jq .
# POST /api/v1/strategies/{name}/stage — missing: version
curl -s -X POST -H "Content-Type: application/json" \
-d '{}' "$ALB/api/v1/strategies/E2ETestStrategy/stage" | jq .
# POST /api/v1/strategies/{name}/promote — missing required body
curl -s -X POST "$ALB/api/v1/strategies/E2ETestStrategy/promote" | jq .
# GET /api/v1/catalog/strategies/{name}/compare — missing: version_a, version_b
curl -s "$ALB/api/v1/catalog/strategies/E2ETestStrategy/compare" | jq .
# GET /api/v1/data/freshness (without symbols param)
curl -s "$ALB/api/v1/data/freshness" | jq .
# GET /api/v1/configs → 405 (only POST allowed — create new config version)
curl -s "$ALB/api/v1/configs" | jq .
3. Real Bugs¶
B1 — GET /api/v1/backtests → 503 (Pydantic deserialization)¶
Impact: High — blocks the dashboard backtest list page.
curl -s "$ALB/api/v1/backtests" | jq .
# → {"detail":"Unexpected error during list_all: 1 validation error for BacktestJobStatus
# result
# Input should be a valid dictionary or instance of BacktestResult
# [type=model_type, input_value='{\"total_trades\":66,...}', input_type=str]"}
Root cause: The result field in the DynamoDB tradai-workflow-state-dev table is stored as a JSON string (DynamoDB S type), but the BacktestJobStatus Pydantic model expects a dict (BacktestResult). Deserialization fails because Pydantic receives a raw string instead of a parsed object.
Fix: Add a @field_validator('result', mode='before') that calls json.loads() when the input is a string, or fix the serialization path that writes result as a string instead of a Map.
B2 — GET /api/v1/strategies/{id}/instances → 503 (IAM)¶
Impact: High — blocks live strategy instance management in dashboard.
curl -s "$ALB/api/v1/strategies/E2ETestStrategy/instances" | jq .
# → {"detail":"Access denied to ECS cluster: arn:aws:ecs:eu-central-1:600802701449:cluster/tradai-dev"}
Root cause: The backend ECS task role is missing ecs:ListTasks and ecs:DescribeTasks permissions on the tradai-dev cluster.
Fix: Add ECS read permissions to the backend task role in infra/compute/modules/ecs_services.py or the IAM policy module.
B3 — POST /api/v1/strategies/{id}/run → 503 (IAM)¶
Impact: High — cannot launch strategy instances from the dashboard.
curl -s -X POST "$ALB/api/v1/strategies/E2ETestStrategy/run" | jq .
# → {"detail":"Access denied to ECS cluster: arn:aws:ecs:eu-central-1:600802701449:cluster/tradai-dev"}
Root cause: Same as B2 — additionally needs ecs:RunTask permission.
B4 — GET /api/v1/strategies/pnl → 503 (proxy 404)¶
Impact: Medium — P&L dashboard panel broken.
Root cause: The P&L handler calls the strategy config lookup internally. If the strategy config is not found in strategy-service, it surfaces as 503. Backend wraps all non-2xx proxy responses as 503 (ExternalServiceError).
B5 — POST /api/v1/models/{name}/rollback → 503 (MLflow error)¶
Impact: Medium — model rollback not functional.
curl -s -X POST -H "Content-Type: application/json" \
-d '{}' "$ALB/api/v1/models/E2ETestStrategy/rollback" | jq .
# → {"detail":"Rollback model failed: 400"}
Root cause: Downstream MLflow model registry returns 400. Likely missing required fields in the rollback request or no previous version to roll back to.
B6 — GET /api/v1/strategies/{id}/trades → 400 (DynamoDB schema)¶
Impact: Medium — open trades panel broken.
curl -s "$ALB/api/v1/strategies/E2ETestStrategy/trades" | jq .
# → {"detail":"Invalid parameter for get: The provided key element does not match the schema"}
Root cause: The code queries DynamoDB with a key that doesn't match the table's key schema — likely using strategy_id (String) when the table expects a different partition key name or type.
B7 — GET /mlflow/api/2.0/mlflow/* → 404 (all MLflow REST API)¶
Impact: High — MLflow API not accessible through ALB; only the UI works.
curl -s "$ALB/mlflow/api/2.0/mlflow/experiments/list"
# → <html>404 Not Found</html>
curl -s -X POST -H "Content-Type: application/json" \
-d '{"experiment_ids":["10"]}' "$ALB/mlflow/api/2.0/mlflow/runs/search"
# → <html>404 Not Found</html>
Root cause: MLflow is started with --static-prefix=/mlflow, which only configures the UI (static asset paths). The Flask REST API still registers routes at /api/2.0/mlflow/.... When ALB forwards a request to /mlflow/api/2.0/mlflow/experiments/list, Flask sees the full path (including the /mlflow prefix) and finds no matching route.
Fix options: 1. Set SCRIPT_NAME=/mlflow in Gunicorn/WSGI config so Flask mounts all routes under /mlflow 2. Configure ALB target group with path rewrite (strip /mlflow prefix) 3. Route MLflow API calls through the backend proxy (strategy-service already has /api/v1/mlflow/models/{name} and /api/v1/mlflow/experiments/{id} routes, but these are not ALB-exposed)
4. Routes That Don't Exist (404 by design)¶
These URLs return 404 because no route is registered for them:
| Tested URL | Why 404 | Correct alternative |
|---|---|---|
GET /docs | Swagger disabled in dev | N/A |
GET /api/v1/trading/positions | Route never implemented | GET /api/v1/trading/status |
GET /api/v1/trading/balance | Route never implemented | — |
GET /api/v1/trading/orders | Route never implemented | — |
GET /api/v1/models | No list-all route | GET /api/v1/models/{name}/versions |
GET /api/v1/models/{name} | No single-model route | GET /api/v1/models/{name}/versions |
GET /api/v1/strategies/{name}/status | Route never implemented | GET /api/v1/strategies/{name}/instances |
GET /api/v1/strategies/{name}/metrics | Route never implemented | — |
GET /api/v1/catalog/strategies/{name}/performance | Route never implemented | GET /api/v1/catalog/leaderboard |
GET /api/v1/mlflow/models/{name} | Strategy-service route, not ALB-exposed | — |
5. Full Endpoint Inventory¶
Backend Service (14 routes via ALB)¶
| Method | Path | Status |
|---|---|---|
| GET | /api/v1/health | 200 |
| POST | /api/v1/backtests | 422 (works) |
| GET | /api/v1/backtests | 503 (B1) |
| GET | /api/v1/backtests/{job_id} | 200* |
| POST | /api/v1/backtests/{job_id}/cancel | 200* |
| GET | /api/v1/backtests/{job_id}/equity | 200* |
| GET | /api/v1/backtests/{job_id}/report-data | 200* |
| POST | /api/v1/ws/ticket | 200 |
| POST | /api/v1/configs | 422 (works) |
| GET | /api/v1/configs/{strategy_name} | 200 |
| GET | /api/v1/configs/{strategy_name}/{config_id} | 200* |
| POST | /api/v1/configs/{strategy_name}/{config_id}/activate | 200* |
| POST | /api/v1/configs/{strategy_name}/{config_id}/deprecate | 200* |
| GET | /api/v1/strategies | 200 (proxy) |
| GET | /api/v1/strategies/{config_name} | 200 (proxy) |
| POST | /api/v1/strategies/{id}/run | 503 (B3) |
| GET | /api/v1/strategies/{id}/instances | 503 (B2) |
| POST | /api/v1/strategies/{id}/instances/{iid}/stop | 503 (B2)* |
| GET | /api/v1/strategies/{id}/instances/{iid}/logs | 503 (B2)* |
| POST | /api/v1/strategies/{id}/instances/{iid}/pause | 503 (B2)* |
| POST | /api/v1/strategies/{id}/instances/{iid}/resume | 503 (B2)* |
| GET | /api/v1/trading/status | 200 |
| GET | /api/v1/strategies/{name}/routing-info | 200 |
| GET | /api/v1/strategies/pnl | 503 (B4) |
| GET | /api/v1/strategies/{id}/trades | 400 (B6) |
| POST | /api/v1/strategies/{name}/stage | 422 (works) |
| POST | /api/v1/strategies/{name}/promote | 422 (works) |
| GET | /api/v1/models/{name}/versions | 200 |
| POST | /api/v1/models/{name}/rollback | 503 (B5) |
| GET | /api/v1/data/symbols | 200 (proxy) |
| GET | /api/v1/data/freshness | 200 (proxy, needs symbols param) |
| GET | /api/v1/catalog/strategies | 200 |
| GET | /api/v1/catalog/strategies/{name} | 200 |
| GET | /api/v1/catalog/leaderboard | 200 |
| GET | /api/v1/catalog/strategies/{name}/compare | 422 (works) |
* = untestable without valid job_id / config_id, expected to work.
MLflow (via ALB)¶
| Method | Path | Status |
|---|---|---|
| GET | /mlflow/ | 200 (UI only) |
| * | /mlflow/api/2.0/mlflow/* | 404 (B7) |
Strategy-Service (9 route groups, NOT ALB-exposed)¶
Accessible only through backend proxy or Service Discovery. See services/strategy-service/src/ for full route definitions.
Data-Collection (5 routes, NOT ALB-exposed)¶
Accessible only through backend proxy or Service Discovery. See services/data-collection/src/ for full route definitions.