Repository navigation
API Reference
Django REST Framework API mounted at /api/ by config/urls.py (routes in core/urls.py). Paths
are unversioned, payloads are JSON, and the browsable HTML renderer exists only when
DJANGO_DEBUG=true (Configuration). Verified against core/urls.py, core/views.py,
core/serializers.py, core/exceptions.py and core/tests/.
| Item | Value |
|---|---|
| Scheme | DRF TokenAuthentication, then SessionAuthentication (browsable API / /admin/) |
| Header |
Authorization: Token <key> — the literal keyword is Token, not Bearer
|
| Anonymous result |
401 with WWW-Authenticate; default permission is IsAuthenticated
|
| Token lifetime | No protocol expiry; a nightly task purges token rows older than 30 days |
POST /api/auth/token/ exchanges credentials for a token. Repeat logins return the same row
(Token.objects.get_or_create) — there is no rotation on re-login.
API=http://127.0.0.1:8000
TOKEN=$(curl -sS -X POST "$API/api/auth/token/" -H 'Content-Type: application/json' \
-d '{"username":"demo","password":"<your-password>"}' | jq -r .token)
curl -sS "$API/api/portfolio/" -H "Authorization: Token $TOKEN"An unauthenticated call returns {"error": true, "status_code": 401, "detail": "Authentication credentials were not provided.", "errors": {...}}.
-
Ownership is enforced on every endpoint. Querysets are filtered by
request.user; no endpoint accepts a user or portfolio id from the client. Touching another user's recommendation returns404, never403(RecommendationDecisionViewfilterspkandportfoliotogether). - All endpoints require
Authorization: Token <key>exceptPOST /api/auth/token/(public),POST /api/telegram/webhook/(shared secret),GET /healthz/(public) and/admin/(session auth + staff user). - A
Portfoliois auto-provisioned on first access with $10,000.00 cash,mediumrisk,max_trade_allocation_pct=5.00anddaily_loss_limit_usd=500.00. -
tasks.purge_expired_auth_tokens_task(daily 03:30 UTC) deletes tokens whosecreatedtimestamp is older than 30 days — creation, not last use, despite the docstring. Session auth needs a CSRF token for unsafe methods.
Error envelope. core.exceptions.alphaagent_exception_handler rewrites every DRF error as
{"error": true, "status_code": <int>, "detail": "<message>", "errors": <field errors|null>}.
Unhandled exceptions become 500 {"detail": "Internal server error."} — no stack trace is leaked.
Pagination. PageNumberPagination, PAGE_SIZE = 50, on every list endpoint except snapshots.
?page=<n> selects the page (page=last for the final one); page_size is not enabled.
Envelope: {"count": 128, "next": ".../logs/?page=3", "previous": null, "results": []}.
GET /api/portfolio/snapshots/ is deliberately unpaginated ({"count", "results"} only) so a chart
client receives the whole series in one response.
Rate limiting. No DRF throttling class is configured; HTTP endpoints are not rate limited, and
ALPHA_TICKER_COOLDOWN_SECONDS / ALPHA_SWEEP_DEBOUNCE_SECONDS gate the agent sweep — see Configuration.
Portfolio with nested assets and computed metrics. Live prices resolve once per request into a shared
price_map; if market data is unavailable the API degrades to the stored cost basis instead of failing.
{"id": 1, "username": "demo", "balance_usd": "10000.00", "risk_profile": "medium",
"is_autonomous": false, "max_trade_allocation_pct": "5.00", "max_trade_budget_usd": "500.00",
"daily_loss_limit_usd": "500.00", "metrics": {"cash_balance_usd": "10000.00", "positions_value_usd": "1500.00",
"total_equity_usd": "11500.00", "unrealised_pnl_usd": "500.00", "is_autonomy_blocked": false,
"block_reason": null, "unpriced_tickers": []},
"assets": [{"id": 7, "ticker": "AAPL", "amount": "10.00000000", "avg_purchase_price": "100.00",
"market_price": "150.00", "price_source": "yfinance", "market_value_usd": "1500.00"}]}metrics also carries invested_cost_usd, unrealised_pnl_pct, realised_pnl_today_usd,
realised_pnl_total_usd, daily_loss_limit_usd, daily_loss_used_usd and
daily_loss_remaining_usd; each asset also carries cost_basis_usd, unrealised_pnl_usd,
unrealised_pnl_pct and allocation_pct. is_autonomy_blocked is true when the daily loss limit
is exhausted or buying power is zero; unpriced_tickers lists positions valued at cost.
Oldest-first equity series for the dashboard chart (pagination_class = None). Each row: id,
captured_at, total_equity_usd, cash_balance_usd, positions_value_usd, unrealised_pnl_usd,
realised_pnl_today_usd.
| Query | Type | Default | Notes |
|---|---|---|---|
hours |
int | — | Keep points newer than now - hours; unparseable values are ignored |
limit |
int | 2000 |
Capped at 10000; a non-integer value surfaces as 500
|
The only way to hand trading control to the AI. Enabling requires confirm=true and a portfolio
with positive cash and a non-zero allocation ceiling.
| Body | Behaviour |
|---|---|
{"is_autonomous": true, "confirm": true} |
Enable autonomy |
{"is_autonomous": false} |
Disable autonomy (no confirmation needed) |
{} |
Flip the current state; a flip to true still requires confirm
|
curl -sS -X POST "$API/api/portfolio/toggle-autonomy/" \
-H "Authorization: Token $TOKEN" -H 'Content-Type: application/json' \
-d '{"is_autonomous": true, "confirm": true}'
# 200 {"portfolio_id": 1, "is_autonomous": true, "changed": true, "max_trade_budget_usd": "500.00",
# "daily_loss_limit_usd": "500.00", "message": "Autonomous trading enabled."}400 means confirm is missing, the balance is $0.00, or the allocation ceiling is 0%; a no-op
returns "changed": false with 200.
Immutable ledger, newest first, paginated (50 per page), scoped to the caller's portfolio. Row shape:
id, ticker, tx_type (BUY/SELL), amount, price, gross_value_usd, executed_by, timestamp.
| Query | Values | Notes |
|---|---|---|
ticker |
e.g. AAPL
|
Upper-cased before filtering |
tx_type |
BUY, SELL
|
Upper-cased; an unknown value returns an empty page, not an error |
Enqueues tasks.autonomous_market_monitoring_task and returns immediately with
{"status": "queued", "task_id": "b0c1...", "portfolio_id": 1}. 409 means the caller's portfolio
has is_autonomous=false ({"error": true, "status_code": 409, "detail": "Autonomous trading is disabled for this portfolio.", "errors": null}).
Two caveats: the task is the global fan-out used by Celery Beat, so it sweeps every autonomous
portfolio (held tickers + ALPHA_WATCHLIST), not just the caller's; the 60s debounce can no-op it.
AI decision audit trail (chain-of-thought, debate cases, token spend), newest first, paginated.
Row shape: id, action_taken, market_sentiment, ticker, transaction_id, tokens_used,
api_cost_usd, reasoning, bull_case, bear_case, created_at.
| Query | Values | Notes |
|---|---|---|
sentiment |
BULLISH, BEARISH, NEUTRAL
|
Upper-cased; unknown values yield an empty page |
ticker |
e.g. AAPL
|
Matches the linked transaction; HOLD rows have ticker: null
|
executed |
1/true/yes
|
Truthy set only; any other value selects rows without a transaction |
since |
ISO-8601 |
created_at >= since; an unparseable value returns 400
|
Runs the AI in advisory mode: proposals are queued for approval, nothing executes, and autonomy
is not required. Returns {"status": "queued", "task_id": "...", "mode": "advisory"}.
Proposals awaiting (or having received) a human decision, newest first, paginated. Optional
?status= accepts PENDING, APPROVED, REJECTED, EXPIRED, EXECUTED, BLOCKED. Row shape:
id, ticker, action (BUY/SELL), amount, price, notional_usd, sentiment, reasoning,
status, decided_via (WEB/TELEGRAM/API/AUTO), decided_at, expires_at, created_at,
is_actionable, transaction_id; proposals expire after RECOMMENDATION_TTL_MINUTES (default 60).
Approving is not a bypass of the Execution-Guard: the trade is re-validated against current
prices, budget and the loss limit. A stale proposal is marked BLOCKED, still with HTTP 200.
curl -sS -X POST "$API/api/portfolio/recommendations/12/approve/" -H "Authorization: Token $TOKEN"
# 200 executed: {"id": 12, "status": "EXECUTED", "executed": true, "transaction_id": 44,
# "message": "Executed BUY 2 AAPL @ $100.00", "guard_reason": ""}
# 200 blocked: {"status": "BLOCKED", "executed": false, "transaction_id": null, "guard_reason": "..."}| Status | Condition |
|---|---|
200 |
Decision applied (executed, or blocked with status: "BLOCKED" and guard_reason) |
404 |
Unknown id or the recommendation belongs to another user |
409 |
Already decided, or past expires_at (the message reads "already pending") |
500 |
Unexpected failure while applying the decision |
401 |
Missing or invalid token |
Marks the proposal REJECTED (decided_via: "WEB"), writes a matching audit row and starts no
trade. The body has "executed": false and message: "Recommendation rejected.".
RSS headlines scored by the same lexicon the bull/bear agents argue from, split into
positive/negative/neutral lists. ticker is required (trimmed, upper-cased); limit defaults to
10, is clamped to 1..30, and 400 means ticker was missing or blank.
{"ticker": "AAPL", "headline_count": 5, "sources_used": ["Google News"], "degraded": false,
"fetched_at": "2025-09-30T12:00:00Z",
"sentiment": {"label": "BULLISH", "score": 0.42, "confidence": 0.61, "positive_hits": ["beat"],
"negative_hits": [], "articles_scored": 5},
"positive": [{"title": "...", "source": "...", "url": "...", "published_at": "...", "summary": "...",
"polarity": 0.42, "sentiment": "BULLISH"}], "negative": [], "neutral": []}degraded is true when no article could be fetched; the internal articles key is not part of
the serializer output — use the three split lists.
The integration is optional and degrades cleanly when TELEGRAM_BOT_TOKEN is empty. See
Telegram-Bot for the command surface.
Returns {"linked": false}, or {"linked": true, ...} with chat_id, telegram_username,
is_active, notify_trades, notify_recommendations, linked_at, last_seen_at — only the
caller's own binding is ever returned.
Mints a pairing code (16 hex, upper-cased) sent to the bot as /start <code>; 503 means no bot
token is configured. Until pairing, the row keeps a negative synthetic chat_id.
curl -sS -X POST "$API/api/telegram/link/" -H "Authorization: Token $TOKEN"
# 200 {"link_code": "A1B2C3D4E5F60718", "expires_at": "2025-09-30T12:15:00Z", "instructions":
# "Open Telegram, start a chat with your AlphaAgent bot and send /start A1B2C3D4E5F60718"}expires_at is the advertised 15-minute validity window; the bot matches any non-empty stored code.
Called by Telegram, not by clients. Unauthenticated by design (Telegram cannot send a DRF token) and
CSRF-exempt, but verified with the X-Telegram-Bot-Api-Secret-Token header when
TELEGRAM_WEBHOOK_SECRET is set — an empty value disables the check entirely.
| Status | Body | Condition |
|---|---|---|
200 |
{"ok": true} |
Handled, integration disabled, or handler raised (always acked so Telegram stops retrying) |
403 |
{"ok": false} |
Secret configured and the header does not match |
Public liveness probe: no auth, no database access; used by the Compose healthcheck. Returns
{"status": "ok", "service": "AlphaAgent"}.
When frontend/dist/index.html exists (SPA_DIST_DIR), Django also serves the compiled dashboard;
these routes are registered after the API, admin and health endpoints, so they cannot shadow
/api/, /admin/, /healthz/ or /ws/.
| Method | Path | Notes |
|---|---|---|
GET |
/assets/<path>, /favicon.ico, /robots.txt, /manifest.webmanifest, /vite.svg
|
Build assets; path traversal returns 404
|
GET |
/<any client route> |
SPA shell (index.html) with Cache-Control: no-cache
|
Unknown API routes return 404 — they are never rewritten to the SPA shell. The live dashboard streams
over ws://<host>/ws/portfolio/?token=<key> (token in the query string — browsers cannot set WebSocket
headers); see Real-Time-Layer and Quality-and-Testing.
Start here
The system
Interfaces
Running it
Background