# Stonks Oracle — API Reference

This document covers every HTTP endpoint exposed by the four FastAPI services in the Stonks Oracle platform. For each endpoint: HTTP method, path, query parameters (with type, default, and constraints), request body schema, response schema, and error codes.

**Live endpoints:**

| Service | Base URL | Source |
|---------|----------|--------|
| Query API | `https://stonks-api.celestium.life` | `services/api/app.py` |
| Symbol Registry | `https://stonks-registry.celestium.life` | `services/symbol_registry/app.py` |
| Trading Engine | `https://stonks-trading.celestium.life` | `services/trading/app.py` |
| Risk Engine | (cluster-internal) | `services/risk/app.py` |

**Common error format:** All services return errors as `{"detail": "error message"}` with the appropriate HTTP status code.

---

## Table of Contents

- [1. Query API](#1-query-api)
- [2. Symbol Registry API](#2-symbol-registry-api)
- [3. Trading Engine API](#3-trading-engine-api)
- [4. Risk Engine API](#4-risk-engine-api)

---

## 1. Query API

Source: `services/api/app.py`
Base path: `/` (most endpoints prefixed with `/api/`)

### 1.1 Health and Metrics

#### `GET /health`
Liveness probe. Verifies database connectivity.

- **Response:** `{"status": "ok"}`
- **Errors:** `503` — Database unavailable

#### `GET /metrics`
Prometheus metrics endpoint for scraping.

- **Response:** Prometheus text format (`text/plain`)

### 1.2 Companies

#### `GET /api/companies`
List tracked companies with optional filters.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `active` | bool | `true` | Filter by active status |
| `sector` | string | — | Filter by sector |
| `ticker` | string | — | Filter by ticker (auto-uppercased) |

- **Response:** Array of company objects with `id`, `ticker`, `legal_name`, `exchange`, `sector`, `industry`, `market_cap_bucket`, `active`, `created_at`, `updated_at`

#### `GET /api/companies/{company_id}`
Get a single company with aliases and active source count.

- **Path params:** `company_id` (UUID string)
- **Response:** Company object + `aliases[]` + `active_source_count`
- **Errors:** `404` — Company not found

#### `GET /api/companies/{company_id}/sources`
List sources configured for a company.

- **Path params:** `company_id` (UUID string)
- **Response:** Array of source objects with `id`, `source_type`, `source_name`, `config`, `credibility_score`, `retention_days`, `access_policy`, `active`

### 1.3 Documents

#### `GET /api/documents`
List documents with optional filters, ordered by `published_at` descending.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `ticker` | string | — | — | Filter by ticker |
| `company_id` | string | — | — | Filter by company UUID |
| `document_type` | string | — | — | Filter by type |
| `status` | string | — | — | Filter by processing status |
| `since` | string | — | ISO 8601 timestamp | Documents published after this time |
| `limit` | int | `50` | max `200` | Page size |
| `offset` | int | `0` | — | Pagination offset |

- **Response:** Array of document objects

#### `GET /api/documents/{document_id}`
Get a single document with intelligence extraction and company mentions.

- **Path params:** `document_id` (UUID string)
- **Response:** Document object + `company_mentions[]` + `intelligence` (with `company_impacts[]`)
- **Errors:** `404` — Document not found

### 1.4 Trends

#### `GET /api/trends`
List trend summaries with optional filters.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `ticker` | string | — | — | Filter by entity_id (ticker) |
| `entity_type` | string | `"company"` | — | Entity type filter |
| `window` | string | — | — | Time window filter |
| `limit` | int | `50` | max `200` | Page size |
| `offset` | int | `0` | — | Pagination offset |

- **Response:** Array of trend objects with JSONB fields parsed, plus `projection` sub-object from `trend_projections`

#### `GET /api/trends/history`
Historical trend snapshots for charting (time series from `trend_history` table).

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `ticker` | string | — | — | Filter by entity_id |
| `window` | string | — | — | Time window filter |
| `limit` | int | `200` | max `1000` | Max rows |

- **Response:** Array of trend history objects ordered by `generated_at` ascending

#### `GET /api/trends/{trend_id}`
Get a single trend summary by ID.

- **Path params:** `trend_id` (UUID string)
- **Response:** Trend object with parsed JSONB fields
- **Errors:** `404` — Trend not found

#### `GET /api/trends/{trend_id}/evidence`
Drill down from a trend window to contributing documents and raw artifacts. Full provenance chain.

- **Path params:** `trend_id` (UUID string)
- **Response:** `{ trend, evidence[] }` — each evidence item includes `intelligence` and `company_impacts[]`
- **Errors:** `404` — Trend not found

#### `GET /api/trends/{trend_id}/projection`
Trend projection for a specific trend window.

- **Path params:** `trend_id` (UUID string)
- **Response:** Projection object with `projected_direction`, `projected_strength`, `projected_confidence`, `projection_horizon`, `driving_factors`, `macro_contribution_pct`, `diverges_from_current`
- **Errors:** `404` — Trend not found

### 1.5 Market Prices

#### `GET /api/market/prices/{ticker}`
Historical OHLCV bars from `market_snapshots`, deduplicated by bar timestamp and ordered oldest-first. Also returns 90-day high/low range.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `limit` | int | `200` | max `500` | Max bars returned |

- **Path params:** `ticker` (auto-uppercased)
- **Response:** `{ bars: [{ ticker, close, open, high, low, volume, bar_timestamp, captured_at }], range_90d: { low, high } }`

#### `POST /api/market/backfill/{ticker}`
Backfill daily OHLCV bars from Polygon for the last N days. Deduplicates by bar timestamp.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `days` | int | `90` | max `365` | Number of days to backfill |

- **Path params:** `ticker` (auto-uppercased)
- **Response:** `{ ticker, inserted, total_bars, days }`
- **Errors:** `503` — No market data API key configured

#### `POST /api/market/backfill-all`
Backfill daily bars for all active companies from Polygon.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `days` | int | `90` | max `365` | Number of days to backfill |

- **Response:** `{ total_inserted, tickers, details[] }` — each detail has `{ ticker, inserted }` or `{ ticker, inserted: 0, error }`
- **Errors:** `503` — No market data API key configured

### 1.6 Recommendations

#### `GET /api/recommendations`
List recommendations with optional filters.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `ticker` | string | — | — | Filter by ticker |
| `action` | string | — | — | Filter by action (buy/sell/hold) |
| `mode` | string | — | — | Filter by mode |
| `since` | string | — | ISO 8601 | Generated after this time |
| `min_confidence` | float | — | 0.0–1.0 | Minimum confidence threshold |
| `limit` | int | `50` | max `200` | Page size |
| `offset` | int | `0` | — | Pagination offset |
| `latest` | bool | `true` | — | Return only latest per ticker |

- **Response:** Array of recommendation objects

#### `GET /api/recommendations/{recommendation_id}`
Get a single recommendation with evidence and risk evaluation.

- **Path params:** `recommendation_id` (UUID string)
- **Response:** Recommendation + `evidence[]` + `risk_evaluation`
- **Errors:** `404` — Recommendation not found

#### `GET /api/recommendations/{recommendation_id}/evidence`
Full evidence drill-down: provenance chain from recommendation to source documents and raw artifacts.

- **Path params:** `recommendation_id` (UUID string)
- **Response:** `{ recommendation, evidence[], trend_window }` — each evidence item includes `intelligence` and `company_impacts[]`
- **Errors:** `404` — Recommendation not found

### 1.7 Orders

#### `GET /api/orders`
List orders with optional filters.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `ticker` | string | — | — | Filter by ticker |
| `status` | string | — | — | Filter by order status |
| `side` | string | — | — | Filter by side (buy/sell) |
| `since` | string | — | ISO 8601 | Created after this time |
| `limit` | int | `50` | max `200` | Page size |
| `offset` | int | `0` | — | Pagination offset |

- **Response:** Array of order objects

#### `GET /api/orders/{order_id}`
Get a single order with events, decision trace, and full audit trail.

- **Path params:** `order_id` (UUID string)
- **Response:** Order object + `events[]` + `audit_trail`
- **Errors:** `404` — Order not found

### 1.8 Positions

#### `GET /api/positions`
List current positions.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `ticker` | string | — | Filter by ticker |

- **Response:** Array of position objects with `id`, `broker_account_id`, `ticker`, `quantity`, `avg_entry_price`, `current_price`, `unrealized_pnl`, `realized_pnl`, `updated_at`

### 1.9 Audit Trail

#### `GET /api/audit/{entity_type}/{entity_id}`
Get audit events for any entity type and ID.

- **Path params:** `entity_type` (string), `entity_id` (string)
- **Response:** Array of audit event objects

### 1.10 Admin: Source Health

#### `GET /api/admin/sources/health`
Source health overview with latest ingestion status and failure counts.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `source_type` | string | — | Filter by source type |
| `company_id` | string | — | Filter by company UUID |
| `active_only` | bool | `true` | Only show active sources |

- **Response:** Array of source health objects with `source_id`, `source_type`, `source_name`, `credibility_score`, `ticker`, `legal_name`, `last_run_status`, `last_run_at`, `last_error`, `last_items_fetched`, `last_items_new`, `total_runs_24h`, `failed_runs_24h`, `total_items_24h`

#### `GET /api/admin/sources/{source_id}/runs`
Recent ingestion runs for a specific source.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `limit` | int | `20` | max `100` | Page size |
| `offset` | int | `0` | — | Pagination offset |

#### `PUT /api/admin/sources/{source_id}/toggle`
Enable or disable a source.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `active` | bool | `true` | New active state |

- **Errors:** `404` — Source not found

#### `PUT /api/admin/sources/{source_id}/credibility`
Update a source's credibility score.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `credibility_score` | float | — | 0.0–1.0 | New credibility score |

- **Errors:** `404` — Source not found

### 1.11 Admin: Company Management

#### `PUT /api/admin/companies/{company_id}/toggle`
Enable or disable a tracked company.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `active` | bool | `true` | New active state |

- **Errors:** `404` — Company not found

#### `PUT /api/admin/companies/{company_id}/sector`
Update a company's sector and industry classification.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `sector` | string | — | **Required.** New sector |
| `industry` | string | — | Optional new industry |

- **Errors:** `404` — Company not found

#### `GET /api/admin/companies/coverage`
Source coverage overview per active company. Shows active source counts by type.

- **Response:** Array of objects with `company_id`, `ticker`, `legal_name`, `sector`, `active_sources`, `market_sources`, `news_sources`, `filings_sources`, `web_scrape_sources`, `broker_sources`

### 1.12 Admin: Trading Configuration

#### `GET /api/admin/trading/config`
Get the current active risk/trading configuration.

- **Response:** Risk config object with `id`, `name`, `trading_mode`, `config` (JSONB), `active`, timestamps

#### `PUT /api/admin/trading/mode`
Switch the active trading mode.

| Parameter | Type | Constraints | Description |
|-----------|------|-------------|-------------|
| `mode` | string | `paper`, `live`, or `disabled` | **Required.** New trading mode |

#### `PUT /api/admin/trading/config`
Update the active risk configuration JSON.

- **Body:** `dict[str, Any]` — partial or full risk config object
- **Response:** Updated config object

#### `GET /api/admin/trading/approvals`
List pending operator approval requests for live trading orders.

- **Response:** Array of approval objects with `order_job` (JSONB), `recommendation_id`, `ticker`, `side`, `quantity`, `estimated_value`, `status`, `expires_at`, etc.

#### `PUT /api/admin/trading/approvals/{approval_id}`
Approve or reject a pending operator approval request.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `approved` | bool | — | **Required.** Approve or reject |
| `reviewed_by` | string | `"operator"` | Reviewer identity |
| `review_note` | string | `""` | Optional note |

- **Errors:** `404` — Approval not found or no longer pending

#### `GET /api/admin/trading/lockouts`
List active symbol lockouts (news-shock, cooldown, manual).

- **Response:** Array of lockout objects

#### `POST /api/admin/trading/lockouts`
Create a manual symbol lockout.

- **Body:** `{ ticker: string, reason: string, duration_minutes: int, lockout_type?: string }`
- **Errors:** `400` — Missing or invalid fields

#### `DELETE /api/admin/trading/lockouts/{lockout_id}`
Delete a symbol lockout (early removal).

- **Errors:** `404` — Lockout not found

#### `GET /api/admin/trading/approval-config`
Get operator approval settings from the active risk config.

- **Response:** `{ auto_approve_paper, require_approval_for_live, approval_timeout_minutes }`

#### `PUT /api/admin/trading/approval-config`
Update operator approval settings.

- **Body:** `{ auto_approve_paper?: bool, require_approval_for_live?: bool, approval_timeout_minutes?: int }`
- **Response:** Updated approval settings

### 1.13 Operational Dashboard

#### `GET /api/ops/ingestion/throughput`
Ingestion throughput over time, bucketed by interval.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `hours` | int | `24` | 1–168 | Time window |
| `bucket` | string | `"1h"` | `15m`, `1h`, `6h`, `1d` | Bucket interval |

- **Response:** Array of bucketed throughput objects by source type

#### `GET /api/ops/ingestion/summary`
High-level ingestion summary for the operational dashboard.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `hours` | int | `24` | 1–168 | Time window |

- **Response:** `{ total_runs, completed, failed, pending, running, total_items_fetched, total_items_new, active_sources, active_companies, by_source_type[], hours }`

#### `GET /api/ops/model/failures`
Recent model extraction failures with error details.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `hours` | int | `24` | 1–168 | Time window |
| `limit` | int | `50` | max `200` | Max results |

#### `GET /api/ops/model/performance`
Aggregated model performance metrics.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `hours` | int | `24` | 1–168 | Time window |
| `model_name` | string | — | — | Filter by model |

#### `GET /api/ops/pipeline/health`
Pipeline stage health summary across ingestion, parsing, extraction, and aggregation.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `hours` | int | `24` | 1–168 | Time window |

- **Response:** `{ hours, pipeline_enabled, document_stages[], parsing, extraction, aggregation, queue_depths }`

#### `GET /api/ops/pipeline/stream`
Server-Sent Events stream of live pipeline status. Pushes queue depths and document stage counts every 3 seconds.

- **Response:** `text/event-stream` with JSON data payloads

#### `POST /api/ops/pipeline/retry-failed`
Re-enqueue documents stuck in `extraction_failed` for another attempt (up to 200).

- **Response:** `{ retried: int, message: string }`

#### `GET /api/ops/pipeline/toggle`
Get the current pipeline enabled/disabled state.

- **Response:** `{ pipeline_enabled: bool }`

#### `POST /api/ops/pipeline/toggle`
Toggle the pipeline on or off.

- **Body:** `{ enabled: bool }`
- **Response:** `{ pipeline_enabled: bool, message: string }`

#### `GET /api/ops/sources/coverage-gaps`
Identify symbols with missing or insufficient source coverage.

- **Response:** `{ missing_source_types[], stale_sources[] }`

### 1.14 System

#### `GET /api/system/rate-limits`
Current rate limit configuration and usage.

- **Response:** `{ polygon_global_limit, polygon_source_types, per_type_limits, cadences_seconds, market_api: { rate_per_minute, cadence_seconds, max_tickers_per_cycle, active_sources }, news_api: {...} }`

### 1.15 Analytics

#### `POST /api/analytics/query`
Proxy SQL to Trino with row limits.

- **Body:** `{ sql: string, limit?: int (default 1000, max 10000) }`
- **Response:** `{ columns[], rows[][], row_count, elapsed_ms }`
- **Errors:** `400` — Empty SQL; `502` — Trino connection error; `504` — Trino timeout

#### `GET /api/analytics/schema`
Trino catalog/schema/table/column metadata for the schema browser.

- **Response:** `{ catalog, schema, tables[{ name, columns[{ name, type }] }] }`

#### `GET /api/analytics/pg-schema`
PostgreSQL table/column metadata with primary keys, foreign keys, and row estimates.

- **Response:** `{ catalog: "postgresql", schema: "public", tables[{ name, row_estimate, columns[{ name, type, nullable, primary_key?, references?, has_default? }] }] }`

#### `POST /api/analytics/pg-query`
Run read-only SQL against PostgreSQL directly. Only SELECT statements allowed.

- **Body:** `{ sql: string, limit?: int (default 1000, max 10000) }`
- **Response:** `{ columns[], rows[][], row_count, elapsed_ms }`
- **Errors:** `400` — Non-SELECT query, syntax error, table not found, or query error

#### `GET /api/analytics/saved-queries`
List all saved queries.

- **Response:** Array of `{ id, name, description, sql_text, created_by, created_at, updated_at }`

#### `POST /api/analytics/saved-queries` (201)
Save a new query.

- **Body:** `{ name: string, description?: string, sql_text: string }`
- **Response:** `{ id, name, description, sql_text, created_by, created_at }`

#### `DELETE /api/analytics/saved-queries/{query_id}`
Delete a saved query.

- **Errors:** `404` — Query not found

### 1.16 Macro Signal Layer

#### `GET /api/admin/macro/status`
Return the current macro signal layer enabled/disabled state.

- **Response:** `{ macro_enabled: bool, source: "default" | "risk_configs" }`

#### `PUT /api/admin/macro/toggle`
Toggle the macro signal layer on or off. Records an audit event.

- **Body:** `{ enabled: bool, operator?: string (default "operator") }`
- **Response:** `{ macro_enabled, previous_enabled, toggled_by }`

### 1.17 Macro Events and Impacts

#### `GET /api/macro/events`
List recent global events with filtering.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `severity` | string | — | — | Filter by severity |
| `region` | string | — | — | Filter by affected region |
| `sector` | string | — | — | Filter by affected sector |
| `since` | string | — | ISO 8601 | Events after this time |
| `until` | string | — | ISO 8601 | Events before this time |
| `limit` | int | `50` | max `200` | Page size |
| `offset` | int | `0` | — | Pagination offset |

- **Response:** Array of global event objects with `id`, `event_types`, `severity`, `affected_regions`, `affected_sectors`, `affected_commodities`, `summary`, `key_facts`, `estimated_duration`, `confidence`, `source_document_id`, `created_at`

#### `GET /api/macro/events/{event_id}`
Event detail with affected companies and macro impact scores.

- **Path params:** `event_id` (UUID string)
- **Response:** Global event object + `impacts[]` (each with `company_id`, `ticker`, `macro_impact_score`, `impact_direction`, `contributing_factors`, `confidence`, `legal_name`, `sector`)
- **Errors:** `404` — Global event not found

#### `GET /api/macro/impacts/{ticker}`
Macro impacts and exposure profile for a specific company.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `since` | string | — | ISO 8601 | Impacts after this time |
| `limit` | int | `50` | max `200` | Page size |
| `offset` | int | `0` | — | Pagination offset |

- **Path params:** `ticker` (auto-uppercased)
- **Response:** `{ exposure_profile, impacts[] }` — each impact includes `event_summary`, `event_severity`, `event_types`, `affected_regions`

### 1.18 Competitive Signal Layer

#### `GET /api/admin/competitive/status`
Return the current competitive signal layer enabled/disabled state.

- **Response:** `{ competitive_enabled: bool, source: "default" | "risk_configs" }`

#### `PUT /api/admin/competitive/toggle`
Toggle the competitive signal layer on or off. Records an audit event.

- **Body:** `{ enabled: bool, operator?: string (default "operator") }`
- **Response:** `{ competitive_enabled, previous_enabled, toggled_by }`

### 1.19 Patterns and Competitive Signals

#### `GET /api/patterns/{ticker}`
Historical patterns for a company.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `catalyst_type` | string | — | Filter by catalyst type |
| `time_horizon` | string | — | Filter by time horizon |

- **Path params:** `ticker` (string)
- **Response:** `{ ticker, patterns[], count }`

#### `GET /api/patterns/{ticker}/competitors`
Cross-company patterns showing how this company's catalysts affected competitors.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `catalyst_type` | string | — | Filter by catalyst type |
| `time_horizon` | string | — | Filter by time horizon |

- **Response:** `{ ticker, cross_company_patterns[], count }`

#### `GET /api/patterns/{ticker}/competitive-signals`
Recent competitive signals targeting this company (limit 100).

- **Path params:** `ticker` (string)
- **Response:** `{ ticker, competitive_signals[], count }`

#### `GET /api/patterns/{ticker}/decisions`
Major corporate decision history with trend outcomes and pattern statistics.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `time_horizon` | string | — | Filter by time horizon |

- **Path params:** `ticker` (string)
- **Response:** `{ ticker, decisions[], count }` — each decision includes `pattern_statistics[]`

### 1.20 AI Agents

#### `GET /api/agents`
List all AI agent configurations.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `active_only` | bool | `false` | Only show active agents |

- **Response:** Array of agent objects with `id`, `name`, `slug`, `purpose`, `model_provider`, `model_name`, `system_prompt`, `user_prompt_template`, `prompt_version`, `schema_version`, `temperature`, `max_tokens`, `timeout_seconds`, `max_retries`, `active`, `source`, `created_at`, `updated_at`

#### `GET /api/agents/{agent_id}`
Get a single agent configuration.

- **Path params:** `agent_id` (UUID string)
- **Errors:** `404` — Agent not found

#### `POST /api/agents` (201)
Create a new user-defined agent.

- **Body:** `AgentCreateBody`

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | string | — | **Required** |
| `slug` | string | auto-generated | URL-safe identifier |
| `purpose` | string | `""` | Agent purpose |
| `model_provider` | string | `"ollama"` | LLM provider |
| `model_name` | string | `"llama3.1:8b"` | Model identifier |
| `system_prompt` | string | `""` | System prompt |
| `user_prompt_template` | string | `""` | User prompt template |
| `prompt_version` | string | `""` | Prompt version tag |
| `schema_version` | string | `"1.0.0"` | Output schema version |
| `temperature` | float | `0.0` | Sampling temperature |
| `max_tokens` | int | `32768` | Max output tokens |
| `timeout_seconds` | int | `120` | Request timeout |
| `max_retries` | int | `2` | Max retry attempts |

#### `PUT /api/agents/{agent_id}`
Update an agent configuration. Partial updates supported — only provided fields are changed.

- **Body:** `AgentUpdateBody` — all fields optional (same fields as create plus `active`)
- **Errors:** `400` — No fields to update; `404` — Agent not found

#### `DELETE /api/agents/{agent_id}`
Delete a user-created agent. System agents cannot be deleted.

- **Errors:** `403` — Cannot delete system agents; `404` — Agent not found

#### `GET /api/agents/{agent_id}/performance`
Aggregated performance metrics for an agent.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `hours` | int | `24` | max `720` | Time window |

- **Response:** `{ total_invocations, successes, failures, avg_duration_ms, p95_duration_ms, avg_confidence, avg_retries, total_input_tokens, total_output_tokens, success_rate }`

#### `GET /api/agents/{agent_id}/performance/history`
Hourly performance time-series for an agent.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `hours` | int | `24` | max `720` | Time window |

- **Response:** Array of `{ hour, invocations, successes, avg_duration_ms, avg_confidence }`

### 1.21 Agent Variants

#### `GET /api/agents/{agent_id}/variants`
List all variants for an agent, ordered by `created_at` ascending.

- **Response:** Array of variant objects with `id`, `agent_id`, `variant_name`, `variant_slug`, `description`, `model_provider`, `model_name`, `system_prompt`, `user_prompt_template`, `prompt_version`, `temperature`, `max_tokens`, `context_window`, `input_token_limit`, `token_budget`, `timeout_seconds`, `max_retries`, `is_active`, `created_at`, `updated_at`

#### `GET /api/agents/{agent_id}/variants/{variant_id}`
Get a single variant.

- **Errors:** `404` — Variant not found

#### `POST /api/agents/{agent_id}/variants` (201)
Create a new variant for an agent.

- **Body:** `VariantCreateBody`

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `variant_name` | string | — | **Required** |
| `variant_slug` | string | auto-generated | URL-safe identifier |
| `description` | string | `""` | Variant description |
| `model_provider` | string | `"ollama"` | LLM provider |
| `model_name` | string | — | **Required.** Model identifier |
| `system_prompt` | string | `""` | System prompt |
| `user_prompt_template` | string | `""` | User prompt template |
| `prompt_version` | string | `""` | Prompt version tag |
| `temperature` | float | `0.0` | Sampling temperature |
| `max_tokens` | int | `32768` | Max output tokens |
| `context_window` | int | `0` | Context window size |
| `input_token_limit` | int | `0` | Input token limit |
| `token_budget` | int | `0` | Token budget |
| `timeout_seconds` | int | `120` | Request timeout |
| `max_retries` | int | `2` | Max retry attempts |

- **Errors:** `409` — Duplicate variant slug

#### `PUT /api/agents/{agent_id}/variants/{variant_id}`
Partial update a variant.

- **Body:** `VariantUpdateBody` — all fields optional
- **Errors:** `400` — No fields to update; `404` — Variant not found

#### `DELETE /api/agents/{agent_id}/variants/{variant_id}`
Delete a variant. Cannot delete active variants.

- **Errors:** `400` — Cannot delete active variant; `404` — Variant not found

#### `POST /api/agents/{agent_id}/clone` (201)
Clone an agent's configuration as a new variant with optional overrides.

- **Body:** `VariantCloneBody { variant_name, variant_slug?, description?, model_provider?, model_name?, system_prompt?, user_prompt_template?, prompt_version?, temperature?, max_tokens?, context_window?, input_token_limit?, token_budget?, timeout_seconds?, max_retries? }`
- **Errors:** `404` — Agent not found; `409` — Duplicate slug

#### `POST /api/agents/{agent_id}/variants/{variant_id}/clone` (201)
Clone an existing variant as a new variant with optional overrides.

- **Body:** `VariantCloneBody` (same as above)
- **Errors:** `404` — Source variant not found; `409` — Duplicate slug

#### `POST /api/agents/{agent_id}/variants/{variant_id}/activate`
Set a variant as the active variant for its agent. Deactivates any currently active variant in a transaction.

- **Errors:** `404` — Variant not found

#### `POST /api/agents/{agent_id}/variants/deactivate`
Deactivate the currently active variant. Agent falls back to base configuration.

- **Response:** `{ deactivated: true }`

#### `GET /api/agents/{agent_id}/variants/{variant_id}/performance`
Aggregated performance metrics for a specific variant.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `hours` | int | `24` | max `720` | Time window |

- **Response:** Same shape as agent performance (invocations, successes, failures, durations, confidence, tokens, success_rate)

#### `GET /api/agents/{agent_id}/variants/{variant_id}/performance/history`
Hourly performance time-series for a specific variant.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `hours` | int | `24` | max `720` | Time window |

- **Response:** Array of `{ hour, invocations, successes, avg_duration_ms, avg_confidence }`

### 1.22 Model Validation

#### `GET /api/validation/summary`
Latest model metric snapshot plus quality gate status.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `lookback` | string | `"30d"` | `7d`, `30d`, `90d`, `all` | Lookback window |
| `horizon` | string | `"7d"` | `1h`, `6h`, `1d`, `7d`, `30d` | Prediction horizon |

- **Response:** `{ snapshot: { id, generated_at, lookback_window, horizon, prediction_count, win_rate, directional_accuracy, information_coefficient, rank_information_coefficient, avg_return, avg_excess_return_vs_spy, avg_excess_return_vs_sector, calibration_error, brier_score, buy_win_rate, sell_win_rate, hold_win_rate, metadata }, gate_status }`
- **Errors:** `400` — Invalid lookback or horizon value

#### `GET /api/validation/calibration`
Calibration table with confidence buckets showing predicted vs observed win rates.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `lookback` | string | `"30d"` | `7d`, `30d`, `90d`, `all` | Lookback window |
| `horizon` | string | `"7d"` | `1h`, `6h`, `1d`, `7d`, `30d` | Prediction horizon |

- **Response:** `{ buckets: [{ bucket_low, bucket_high, avg_confidence, observed_win_rate, prediction_count, miscalibrated }], lookback, horizon }`
- Buckets: 0.50–0.60, 0.60–0.70, 0.70–0.80, 0.80–0.90, 0.90–1.00
- `miscalibrated` is `true` when `|avg_confidence - observed_win_rate| > 0.15`
- **Errors:** `400` — Invalid lookback or horizon value

#### `GET /api/validation/ic-by-horizon`
Information Coefficient and Rank IC per prediction horizon.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `lookback` | string | `"30d"` | `7d`, `30d`, `90d`, `all` | Lookback window |

- **Response:** `{ horizons: [{ horizon, information_coefficient, rank_information_coefficient, prediction_count, generated_at }], lookback }`
- Horizons ordered: `1h`, `6h`, `1d`, `7d`, `30d`
- **Errors:** `400` — Invalid lookback value

#### `GET /api/validation/gate-status`
Quality gate evaluation detail from `risk_configs` where `name = 'model_quality_gate'`.

- **Response:** `{ gate_status, updated_at }` or `{ gate_status: null, message: "No gate evaluation found..." }`

### 1.23 Attribution

#### `GET /api/validation/attribution/sources`
Per-source performance metrics: win rate, IC, average return, duplicate rate.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `lookback` | string | `"30d"` | `7d`, `30d`, `90d`, `all` | Lookback window |
| `horizon` | string | `"7d"` | `1h`, `6h`, `1d`, `7d`, `30d` | Prediction horizon |

- **Response:** `{ sources[], lookback, horizon }`
- **Errors:** `400` — Invalid lookback or horizon; `500` — Computation failed

#### `GET /api/validation/attribution/catalysts`
Per-catalyst-type performance metrics: win rate, IC, average return.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `lookback` | string | `"30d"` | `7d`, `30d`, `90d`, `all` | Lookback window |
| `horizon` | string | `"7d"` | `1h`, `6h`, `1d`, `7d`, `30d` | Prediction horizon |

- **Response:** `{ catalysts[], lookback, horizon }`
- **Errors:** `400` — Invalid lookback or horizon; `500` — Computation failed

#### `GET /api/validation/attribution/layers`
Per-signal-layer (company, macro, competitive) performance metrics.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `lookback` | string | `"30d"` | `7d`, `30d`, `90d`, `all` | Lookback window |
| `horizon` | string | `"7d"` | `1h`, `6h`, `1d`, `7d`, `30d` | Prediction horizon |

- **Response:** `{ layers[], lookback, horizon }` — each layer has `avg_contribution_pct`, `dominant_win_rate`, `dominant_ic`
- **Errors:** `400` — Invalid lookback or horizon; `500` — Computation failed

### 1.24 Trading Reports

#### `GET /api/reports`
Paginated list of trading reports with optional filtering.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `report_type` | string | — | `daily` or `weekly` | Filter by report type |
| `start_date` | string | — | ISO date (YYYY-MM-DD) | Filter `period_start >= this` |
| `end_date` | string | — | ISO date (YYYY-MM-DD) | Filter `period_end <= this` |
| `limit` | int | `20` | max `100` | Page size |
| `offset` | int | `0` | min `0` | Pagination offset |

- **Response:** Array of `{ id, report_type, period_start, period_end, validation_status, generated_at }`
- **Errors:** `400` — Invalid `report_type` or date format

#### `GET /api/reports/{report_id}`
Fetch a single report including full `report_data` JSONB.

- **Path params:** `report_id` (UUID string)
- **Response:** `{ id, report_type, period_start, period_end, report_data, validation_status, generated_at, created_at }`
- **Errors:** `404` — Report not found

---

## 2. Symbol Registry API

Source: `services/symbol_registry/app.py` with routers from `exposure.py`, `competitors.py`, `competitor_inference.py`

### 2.1 Health

#### `GET /health`
Liveness probe. Verifies database connectivity.

- **Response:** `{"status": "ok"}`
- **Errors:** `503` — Database unavailable

### 2.2 Companies

#### `POST /companies` (201)
Create a new tracked company.

- **Body:** `CompanyCreate`

| Field | Type | Default | Constraints | Description |
|-------|------|---------|-------------|-------------|
| `ticker` | string | — | 1–10 uppercase letters | **Required.** Stock ticker |
| `legal_name` | string | — | — | **Required.** Company name |
| `exchange` | string | `null` | — | Stock exchange |
| `sector` | string | `null` | — | Sector classification |
| `industry` | string | `null` | — | Industry classification |
| `market_cap_bucket` | string | `null` | — | Market cap bucket |

- **Response:** `CompanyResponse { id, ticker, legal_name, exchange, sector, industry, market_cap_bucket, active }`
- **Errors:** `409` — Company already exists; `422` — Invalid ticker format

#### `GET /companies`
List tracked companies.

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `active` | bool | `true` | Filter by active status |

- **Response:** Array of `CompanyResponse`

#### `GET /companies/{company_id}`
Get a single company.

- **Path params:** `company_id` (UUID string)
- **Errors:** `404` — Company not found

#### `PUT /companies/{company_id}`
Update a company.

- **Body:** `CompanyCreate` (same as create)
- **Errors:** `404` — Company not found

### 2.3 Aliases

#### `POST /companies/{company_id}/aliases` (201)
Add an alias for a company.

- **Body:** `{ alias: string, alias_type?: string (default "brand") }`
- **Response:** `{ id, alias, alias_type }`

#### `GET /companies/{company_id}/aliases`
List aliases for a company.

- **Response:** Array of `{ id, alias, alias_type }`

### 2.4 Watchlists

#### `POST /watchlists` (201)
Create a new watchlist.

- **Body:** `{ name: string, description?: string }`
- **Response:** `{ id, name, description, active }`
- **Errors:** `409` — Watchlist name already exists

#### `GET /watchlists`
List all watchlists.

- **Response:** Array of `{ id, name, description, active }`

#### `POST /watchlists/{watchlist_id}/members/{company_id}` (201)
Add a company to a watchlist.

- **Response:** `{ status: "added" }`
- **Errors:** `409` — Already a member; `404` — Watchlist or company not found

#### `GET /watchlists/{watchlist_id}/members`
List companies in a watchlist.

- **Response:** Array of `CompanyResponse`

### 2.5 Sources

#### `POST /companies/{company_id}/sources` (201)
Add a data source for a company.

- **Body:** `SourceCreate`

| Field | Type | Default | Constraints | Description |
|-------|------|---------|-------------|-------------|
| `source_type` | string | — | `market_api`, `news_api`, `filings_api`, `web_scrape`, `broker` | **Required** |
| `source_name` | string | — | — | **Required** |
| `config` | dict | `{}` | URLs validated for `base_url` | Source configuration |
| `credibility_score` | float | `0.5` | — | Source credibility |
| `retention_days` | int | `365` | — | Data retention period |
| `access_policy` | string | `"internal"` | `internal`, `public`, `restricted` | Access policy |

- **Response:** `{ id, source_type, source_name, credibility_score, active }`
- **Errors:** `404` — Company not found; `422` — Invalid source_type or access_policy

#### `GET /companies/{company_id}/sources`
List sources for a company.

- **Response:** Array of `{ id, source_type, source_name, config, credibility_score, retention_days, access_policy, active }`

### 2.6 Exposure Profiles

#### `GET /companies/{company_id}/exposure`
Get the current active exposure profile for a company.

- **Response:** `ExposureProfileResponse { id, company_id, geographic_revenue_mix, supply_chain_regions, key_input_commodities, regulatory_jurisdictions, market_position_tier, export_dependency_pct, source, confidence, version, active, created_at, updated_at }`
- **Errors:** `404` — No active exposure profile found

#### `PUT /companies/{company_id}/exposure`
Create or update an exposure profile. Archives the previous active version.

- **Body:** `ExposureProfileCreate`

| Field | Type | Default | Constraints | Description |
|-------|------|---------|-------------|-------------|
| `geographic_revenue_mix` | dict[str, float] | `{}` | — | Region to revenue percentage |
| `supply_chain_regions` | list[str] | `[]` | — | Supply chain regions |
| `key_input_commodities` | list[str] | `[]` | — | Key input commodities |
| `regulatory_jurisdictions` | list[str] | `[]` | — | Regulatory jurisdictions |
| `market_position_tier` | string | `"regional"` | `global_leader`, `multinational`, `regional`, `domestic` | Market position |
| `export_dependency_pct` | float | `0.0` | 0.0–1.0 | Export dependency |
| `source` | string | `"manual"` | `manual`, `inferred` | Data source |
| `confidence` | float | `1.0` | 0.0–1.0 | Confidence score |

- **Errors:** `404` — Company not found

#### `GET /companies/{company_id}/exposure/history`
Get all exposure profile versions for a company, ordered by version descending.

- **Response:** Array of `ExposureProfileResponse`

### 2.7 Competitor Relationships

#### `POST /companies/{company_id}/competitors` (201)
Create a competitor relationship. Records an audit event.

- **Body:** `CompetitorRelationshipCreate`

| Field | Type | Default | Constraints | Description |
|-------|------|---------|-------------|-------------|
| `company_b_id` | string | — | UUID | **Required.** Other company |
| `relationship_type` | string | — | `direct_rival`, `same_sector`, `overlapping_products`, `supply_chain_adjacent` | **Required** |
| `strength` | float | `0.5` | 0–1 | Relationship strength |
| `bidirectional` | bool | `true` | — | Bidirectional relationship |
| `source` | string | `"manual"` | `manual`, `inferred` | Data source |

- **Response:** `CompetitorRelationship { id, company_a_id, company_b_id, relationship_type, strength, bidirectional, source, active, created_at, updated_at }`
- **Errors:** `400` — Self-reference; `404` — Company not found; `409` — Relationship already exists

#### `GET /companies/{company_id}/competitors`
List active competitor relationships, enriched with `ticker` and `legal_name` of the other company. Ordered by strength descending.

- **Errors:** `404` — Company not found

#### `PUT /companies/{company_id}/competitors/{relationship_id}`
Update a competitor relationship. Records an audit event with previous state.

- **Body:** `CompetitorRelationshipCreate`
- **Errors:** `404` — Relationship not found

#### `DELETE /companies/{company_id}/competitors/{relationship_id}`
Soft-delete a competitor relationship (sets `active=false`). Records an audit event.

- **Response:** `{ status: "deleted", id }`
- **Errors:** `404` — Active relationship not found

### 2.8 Competitor Inference

#### `POST /companies/{company_id}/competitors/infer`
Auto-infer competitor relationships based on sector/industry match and document co-mention frequency.

Strength formula: `0.3 * sector_match + 0.7 * normalized_co_mention_count`

Upserts relationships with `source='inferred'` and `relationship_type='same_sector'`.

- **Response:** Array of `CompetitorRelationship` objects sorted by strength descending
- **Errors:** `400` — Company missing sector or industry; `404` — Company not found

---

## 3. Trading Engine API

Source: `services/trading/app.py`

### 3.1 Health and Readiness

#### `GET /health`
Liveness probe.

- **Response:** `{"status": "ok"}`

#### `GET /ready`
Readiness probe — reports whether the engine is running.

- **Response:** `{"ready": bool}`

### 3.2 Debug

#### `GET /api/trading/debug`
Diagnostic endpoint showing engine internals for troubleshooting.

- **Response:** `{ running, has_pool, has_redis, config_enabled, polling_interval, last_poll, portfolio_state: { active_pool, reserve_pool, total_value, open_positions }, risk_tier, tasks, processed_rec_count }`

### 3.3 Engine Status and Control

#### `GET /api/trading/status`
Return current engine state.

- **Response:** `{ enabled, paused, risk_tier, circuit_breaker_status, active_pool, reserve_pool, portfolio_heat, open_positions, open_position_count, max_open_positions, absolute_position_cap, last_decision_at }`
- **Errors:** `503` — Engine not initialised

#### `PUT /api/trading/config`
Update trading engine configuration. Returns previous and new values for audit trail.

- **Body:** `ConfigUpdateRequest`

| Field | Type | Description |
|-------|------|-------------|
| `enabled` | bool | Enable/disable engine |
| `risk_tier` | string | Risk tier level |
| `reserve_siphon_pct` | float | Reserve pool siphon percentage |
| `polling_interval_seconds` | int | Polling interval |
| `absolute_position_cap` | float | Max position value |
| `active_pool_minimum` | float | Minimum active pool |
| `micro_trading_enabled` | bool | Enable micro trades |
| `max_open_positions` | int | Max concurrent positions |

All fields optional. Only provided fields are updated.

- **Response:** `{ previous, updated, change_source: "api", changed_at }`
- **Errors:** `503` — Engine not initialised

#### `POST /api/trading/pause`
Pause the trading engine.

- **Response:** `{"paused": true}`

#### `POST /api/trading/resume`
Resume the trading engine.

- **Response:** `{"paused": false}`

#### `POST /api/trading/reset`
Full paper trading reset: liquidate broker positions, cancel orders, clear trading state, reset capital.

- **Body:** `CapitalRequest`

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `initial_capital` | float | `0.0` | If 0, uses broker balance or defaults to 100,000 |
| `reserve_pct` | float | `null` | Reserve pool percentage (0–1). If null, uses engine config `reserve_siphon_pct` |

- **Response:** `{ reset: true, initial_capital, active_pool, reserve_pool, broker: { orders_cancelled, positions_closed, portfolio_value, cash, buying_power } }`
- **Errors:** `503` — Engine not initialised; `500` — Database reset failed

### 3.4 Decision Audit Trail

#### `GET /api/trading/decisions`
Return recent trading decisions from the database.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `ticker` | string | — | — | Filter by ticker |
| `decision` | string | — | — | Filter by decision type |
| `is_micro_trade` | bool | — | — | Filter micro trades |
| `limit` | int | `50` | max `200` | Page size |
| `offset` | int | `0` | — | Pagination offset |

- **Response:** Array of `{ id, recommendation_id, decision, skip_reason, ticker, computed_position_size, computed_share_quantity, risk_tier_at_decision, portfolio_heat_at_decision, active_pool_at_decision, reserve_pool_at_decision, circuit_breaker_status, is_micro_trade, created_at }`

### 3.5 Performance Metrics

#### `GET /api/trading/metrics`
Return current performance metrics.

- **Response:** `{ total_portfolio_value, active_pool, reserve_pool, unrealized_pnl, realized_pnl, daily_pnl, win_rate, profit_factor, sharpe_ratio, max_drawdown, portfolio_heat }`
- **Errors:** `503` — Engine not initialised

#### `GET /api/trading/metrics/history`
Return historical daily portfolio snapshots.

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `limit` | int | `30` | max `365` | Max snapshots |

- **Response:** Array of `{ id, snapshot_date, portfolio_value, active_pool, reserve_pool, daily_return, cumulative_return, unrealized_pnl, realized_pnl, win_count, loss_count, win_rate, sharpe_ratio, max_drawdown, current_drawdown_pct, portfolio_heat, risk_tier, created_at }`

### 3.6 Backtesting

#### `POST /api/trading/backtest`
Launch a backtest run asynchronously.

- **Body:** `BacktestRequest`

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `start_date` | string | — | **Required.** ISO date (YYYY-MM-DD) |
| `end_date` | string | — | **Required.** ISO date (YYYY-MM-DD) |
| `initial_capital` | float | `500.0` | Starting capital |
| `risk_tier` | string | `"moderate"` | Risk tier for backtest |

- **Response:** `{ id: string, status: "running" }`
- **Errors:** `503` — Engine not initialised

#### `GET /api/trading/backtest/{backtest_id}`
Retrieve backtest results.

- **Path params:** `backtest_id` (UUID string)
- **Response:** `{ id, start_date, end_date, initial_capital, risk_tier, config, total_return, sharpe_ratio, max_drawdown, win_rate, profit_factor, trade_count, equity_curve[], trades[], status, completed_at, created_at }`
- Status values: `running`, `completed`, `not_found`, `pending`

### 3.7 Notifications

#### `GET /api/trading/notifications/config`
Return current notification configuration.

- **Response:** `{ sms_enabled, email_enabled, phone_number, email_recipient }`
- **Errors:** `503` — Engine not initialised

#### `PUT /api/trading/notifications/config`
Update notification preferences.

- **Body:** `NotificationConfigRequest`

| Field | Type | Description |
|-------|------|-------------|
| `sms_enabled` | bool | Enable SMS notifications |
| `email_enabled` | bool | Enable email notifications |
| `phone_number` | string | SMS phone number |
| `email_recipient` | string | Email recipient address |

All fields optional.

- **Response:** `{ updated: { ...changed fields } }`
- **Errors:** `503` — Engine not initialised

#### `GET /api/trading/notifications/history`
Return recent notifications (placeholder — currently returns empty array).

| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `limit` | int | `50` | max `200` | Max results |

### 3.8 Override Orders

#### `POST /api/trading/override/order` (202)
Submit a manual override order to the broker queue. Auto-registers untracked tickers in the Symbol Registry.

- **Body:** `OverrideOrderRequest`

| Field | Type | Default | Constraints | Description |
|-------|------|---------|-------------|-------------|
| `ticker` | string | — | 1–10 alphabetic chars | **Required.** Stock ticker |
| `side` | string | — | `buy` or `sell` | **Required.** Order side |
| `quantity` | float | — | Must be positive | **Required.** Share quantity |
| `order_type` | string | `"market"` | `market`, `limit`, `stop`, `stop_limit` | Order type |
| `limit_price` | float | `null` | Required for `limit`/`stop_limit` | Limit price |
| `stop_price` | float | `null` | Required for `stop`/`stop_limit` | Stop price |

- **Response:** `OverrideOrderResponse { job_id, status: "queued", ticker, side, quantity, auto_registered }`
- **Errors:** `503` — Engine not initialised or broker queue unavailable; `422` — Validation errors

---

## 4. Risk Engine API

Source: `services/risk/app.py`

### 4.1 Health

#### `GET /health`
Liveness probe.

- **Response:** `{"status": "ok"}`

### 4.2 Order Evaluation

#### `POST /evaluate`
Evaluate a proposed order against risk rules.

- **Body:** `EvaluateRequest`

| Field | Type | Description |
|-------|------|-------------|
| `order` | ProposedOrder | **Required.** The order to evaluate |
| `config` | PortfolioRiskConfig | Optional. Risk config (uses defaults if null) |
| `state` | AccountRiskState | Optional. Current account state |

**ProposedOrder schema:**

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `recommendation_id` | string | `null` | Source recommendation |
| `ticker` | string | — | **Required.** Stock ticker |
| `sector` | string | `""` | Company sector |
| `action` | string | `"buy"` | `buy` or `sell` |
| `quantity` | float | `0.0` | Share quantity |
| `estimated_value` | float | `0.0` | Estimated order value |
| `confidence` | float | `0.0` | Recommendation confidence |

- **Response:** `RiskEvaluation { evaluation_id, recommendation_id, ticker, eligible, allowed_mode, checks[], rejection_reasons[], config_snapshot, state_snapshot, evaluated_at }`

### 4.3 Approvals

#### `GET /approvals/pending`
List pending approval requests.

- **Response:** Array of approval request objects (serialized via `to_dict()`)
- **Errors:** `503` — Database not ready

#### `GET /approvals/{approval_id}`
Get a single approval request.

- **Path params:** `approval_id` (UUID string)
- **Response:** Approval request object
- **Errors:** `404` — Approval not found; `503` — Database not ready

#### `POST /approvals/{approval_id}/review`
Approve or reject a pending approval request.

- **Body:** `ReviewRequest`

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `approved` | bool | — | **Required.** Approve or reject |
| `reviewed_by` | string | `"operator"` | Reviewer identity |
| `review_note` | string | `""` | Optional review note |

- **Response:** `{ approval_id, status }`
- **Errors:** `404` — Approval not found or no longer pending; `503` — Database not ready

### 4.4 Approval Expiration

#### `POST /approvals/expire`
Expire stale approvals that have passed their expiration time.

- **Response:** `{ expired: int, items: [] }`
- **Errors:** `503` — Database not ready
