Files
Celes Renata f468e30af0
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled
feat: implement dual-pipeline signal engine service
New service at services/signal_engine/ implementing concurrent heuristic
(deterministic scoring) and probabilistic (Bayesian inference) pipelines
that evaluate technical signals across 6 timeframes (M30-M) and produce
independent BUY/WATCH/SKIP verdicts per ticker per evaluation tick.

Components:
- Input Normalizer: multi-source data assembly with sentinel fallbacks
- Signal Library: Fibonacci, MA Stack, RSI, Cup & Handle, Elliott Wave
- Multi-Timeframe Confluence Engine: weighted scoring with D/W/M anchors
- Hard Filter Engine: macro_bias, valuation, earnings proximity gating
- Heuristic Pipeline: S_total scoring with confidence-gated verdicts
- Probabilistic Pipeline: Bayesian log-odds with regime priors, entropy
  gating, EV_R calculation, and signal correlation penalty
- Exit Engine: stop-loss, targets, trailing ATR-based stops
- Delta Analyzer: pipeline agreement tracking with rolling Redis metrics
- Output Formatter: SignalOutput contract + Recommendation schema mapping
- Worker orchestrator: concurrent pipelines with failure isolation
- Main entry point: queue polling with fail-safe config loading

Infrastructure:
- Migration 039: signal_engine_outputs table with 3 indexes
- Helm chart: signalEngine service entry (processing tier)
- Redis key: QUEUE_SIGNAL_ENGINE constant

Tests: 390 tests (unit + property-based) covering all components
Config: dual_pipeline_enabled=false by default (safe rollout)
2026-05-02 07:32:26 +00:00

1308 lines
52 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.01.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.01.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` | 1168 | 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` | 1168 | 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` | 1168 | 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` | 1168 | 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` | 1168 | 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.500.60, 0.600.70, 0.700.80, 0.800.90, 0.901.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 | — | 110 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.01.0 | Export dependency |
| `source` | string | `"manual"` | `manual`, `inferred` | Data source |
| `confidence` | float | `1.0` | 0.01.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` | 01 | 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 (01). 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 | — | 110 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