BotRanks API v1
Report your strategy's orders, positions, and cash events over HTTPS. BotRanks persists the event log, derives positions and NAV, computes performance, and renders the dashboards your subscribers see.
Overview
The BotRanks API is REST over HTTPS. Requests and responses are JSON. All endpoints below live under a common base:
https://botranks.ai/api/v1
Source of truth. Your event log is the truth. BotRanks stores every order and cash event idempotently, then derives current positions, cash balance, and daily NAV history from that log plus cached Polygon prices. You never need to push a position or NAV snapshot separately — though the positions/snapshot endpoint is available for reconciliation.
Authentication
Every request must include an API key in the Authorization header:
Authorization: Bearer fq_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Generating a key
API keys are scoped to a single strategy. Sign in as the strategy's creator, then call:
POST https://botranks.ai/strategies/{strategy_id}/api-keys
Content-Type: application/json
Cookie: access_token=<your session cookie>
{ "name": "my-bot-prod" }
The response returns the plaintext key once; it's never retrievable again. Store it somewhere safe (1Password, Vault, environment variable).
{
"id": "3c9a…",
"name": "my-bot-prod",
"key_prefix": "fq_live_9Q",
"created_at": "2026-04-18T08:00:00Z",
"plaintext": "fq_live_9QxZw…43char-suffix",
"warning": "Store this key now; it cannot be retrieved later."
}
Managing keys
| Method | Path | Purpose |
|---|---|---|
| POST | /strategies/{id}/api-keys | Create a new key |
| GET | /strategies/{id}/api-keys | List key metadata (no plaintext) |
| POST | /strategies/{id}/api-keys/{kid}/revoke | Revoke (soft-delete, idempotent) |
Once revoked, a key will return 401 on every subsequent request. Rotate freely.
Quick Start
Post a filled buy order from your strategy:
curl
curl -X POST https://botranks.ai/api/v1/strategies/$STRATEGY_ID/orders \
-H "Authorization: Bearer $FQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"orders": [{
"symbol": "AAPL",
"side": "buy",
"quantity": 100,
"executed_at": "2026-04-18T14:31:22Z",
"external_order_id": "tb-20260418-00001"
}]
}'
Python
import os, requests
from datetime import datetime, timezone
BASE = "https://botranks.ai/api/v1"
STRATEGY = os.environ["FQ_STRATEGY_ID"]
HEADERS = {"Authorization": f"Bearer {os.environ['FQ_API_KEY']}"}
resp = requests.post(
f"{BASE}/strategies/{STRATEGY}/orders",
headers=HEADERS,
json={
"orders": [{
"symbol": "AAPL",
"side": "buy",
"quantity": 100,
"executed_at": datetime.now(timezone.utc).isoformat(),
"external_order_id": "tb-20260418-00001",
}]
},
timeout=10,
)
resp.raise_for_status()
print(resp.json())
Note: price is optional. When omitted, BotRanks resolves it from the close price on executed_at. Provide it if you have the actual fill price.
POST/strategies/{id}/orders
Submit one or more orders. Batch up to 500 per request.
Request body
{
"orders": [ Order, Order, … ]
}
Order fields
| Field | Type | Description | |
|---|---|---|---|
symbol | string | required | Ticker, e.g. AAPL, BTC-USD |
side | "buy" \| "sell" | required | Long buys and short sells both use buy/sell. Short is inferred from resulting position sign. |
quantity | number | required | Positive; units the order filled for |
executed_at | ISO-8601 | defaults to now | UTC fill time |
price | number | optional | Fill price. When omitted, resolved from the close on executed_at's date. |
fees | number | default 0 | Commission + slippage, in base currency |
order_type | enum | default "market" | market, limit, stop, stop_limit |
status | enum | default "filled" | pending, filled, partial, cancelled |
external_order_id | string | recommended | Your order identifier. Enables idempotency. |
details | object | optional | Free-form JSON for your own metadata |
Response
{
"accepted": [ Order, … ], // newly persisted
"duplicates": [ Order, … ], // matched an existing external_order_id (not re-applied)
"counts": { "accepted": 2, "duplicates": 0 }
}
After a successful ingest, BotRanks re-derives positions and rebuilds NAV history automatically. The next GET /state reflects the new truth.
POST/strategies/{id}/positions/snapshot
Replace the strategy's full position set with the snapshot you send. Every symbol present in the payload is upserted; any symbol not present is deleted. Optionally logs deltas against the prior state into reconciliation_log.
Use this to seed an existing strategy (first-time onboarding without a complete order history), or to reconcile periodically against the order-derived state.
Request body
{
"positions": [
{ "symbol": "AAPL", "quantity": 100, "cost_basis": 189.42 },
{ "symbol": "MSFT", "quantity": 30, "cost_basis": 420.10 }
],
"log_deltas": true
}
Response
{
"upserted": 2,
"removed": 0,
"reconciliation_entries": 1
}
POST/strategies/{id}/cash
Record a cash event — deposit, withdrawal, dividend received, margin interest, fee, or manual adjustment. Cash events participate in cash_balance and therefore in NAV.
Request body
| Field | Type | Description | |
|---|---|---|---|
event_type | enum | required | deposit, withdraw, dividend, interest, fee, adjustment |
amount | number | required | Signed: positive = cash in, negative = cash out |
occurred_at | ISO-8601 | defaults to now | |
external_id | string | recommended | Your event identifier; makes the call idempotent |
related_symbol | string | optional | For dividend / stock-related events |
note | string | optional | Human-readable context |
Response
{ "status": "created", "event": CashEvent }
// or on duplicate external_id:
{ "status": "duplicate", "event": CashEvent }
GET/strategies/{id}/state
Current derived state. Use this to verify your strategy agrees with BotRanks, or for reconciliation loops.
Response
{
"strategy_id": "…",
"positions": [
{ "symbol": "AAPL", "quantity": 100, "cost_basis": 189.42, "updated_at": "…" },
…
],
"nav": {
"total_equity": 1021183.79,
"cash_balance": 796110.50,
"market_value": 225073.29,
"captured_at": "2026-04-17T16:00:00Z"
},
"last_order_at": "2026-01-16T14:31:22Z"
}
GET/strategies/{id}/performance
Returns the metrics the dashboard renders: return, volatility, Sharpe/Sortino/Calmar, drawdown, monthly/yearly returns, and benchmark-relative stats.
Query parameters
| Param | Description | |
|---|---|---|
benchmark | default "SPY" | Symbol to compare against. Pass an empty string to skip. |
risk_free_rate | default 0 | Annualized, e.g. 0.045 for 4.5% |
Response (abridged)
{
"strategy_id": "…",
"inception_date": "2025-01-16",
"latest_date": "2026-04-17",
"days": 456,
"years": 1.25,
"starting_equity": 1000000.0,
"ending_equity": 1021183.79,
"total_return": 0.0212,
"cagr": 0.0169,
"volatility_annualized": 0.087,
"downside_volatility_annualized": 0.054,
"sharpe_ratio": 1.52,
"sortino_ratio": 2.10,
"calmar_ratio": 0.14,
"drawdown": {
"max_drawdown": -0.1194,
"max_drawdown_duration_days": 8,
"current_drawdown": -0.0031
},
"monthly_returns": { "2025-01": 0.012, "2025-02": -0.003, … },
"yearly_returns": { "2025": 0.094, "2026": 0.022 },
"benchmark": {
"symbol": "SPY",
"beta": 1.08,
"alpha_annualized": 0.014,
"tracking_error_annualized": 0.031,
"information_ratio": 0.45,
"benchmark_total_return": 0.087
}
}
Data Types
All timestamps are ISO-8601 UTC. All money amounts are in the strategy's base currency (USD by default). uuid refers to a 36-character UUIDv4 string.
Order
{
"id": "uuid",
"strategy_id": "uuid",
"symbol": "string",
"side": "buy" | "sell",
"quantity": number,
"price": number,
"fees": number,
"order_type": "market" | "limit" | "stop" | "stop_limit",
"status": "pending" | "filled" | "partial" | "cancelled",
"executed_at": "ISO-8601 UTC",
"created_at": "ISO-8601 UTC",
"external_order_id": "string | null",
"details": object | null
}
Position
{
"id": "uuid",
"strategy_id": "uuid",
"symbol": "string",
"quantity": number, // negative = short
"cost_basis": number, // weighted-average entry price, always positive
"updated_at": "ISO-8601 UTC"
}
CashEvent
{
"id": "uuid",
"strategy_id": "uuid",
"event_type": "deposit" | "withdraw" | "dividend" | "interest" | "fee" | "adjustment",
"amount": number, // signed
"occurred_at": "ISO-8601 UTC",
"external_id": "string | null",
"related_symbol": "string | null",
"note": "string | null",
"created_at": "ISO-8601 UTC"
}
Idempotency
Idempotency is per-strategy, per-identifier. When you send an order with an external_order_id that already exists for the strategy, BotRanks returns the existing row under duplicates and does not re-apply the effect. Same for cash_events.external_id.
This makes retries safe: send the same payload as many times as you like, you'll never double-count.
# First call — created
curl … -d '{"orders":[{"symbol":"AAPL","side":"buy","quantity":100,
"external_order_id":"tb-00001"}]}'
# → {"accepted":[…],"duplicates":[],"counts":{"accepted":1,"duplicates":0}}
# Same call — returns the existing row
curl … -d '{"orders":[{"symbol":"AAPL","side":"buy","quantity":100,
"external_order_id":"tb-00001"}]}'
# → {"accepted":[],"duplicates":[…],"counts":{"accepted":0,"duplicates":1}}
Always provide external_order_id / external_id. It's the cheapest insurance you'll ever buy.
Rate Limits
Each API key is limited to 60 requests per minute on a sliding window. Exceed it and you'll get:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{ "detail": "Rate limit exceeded: 60 requests per 60s" }
Keys are independent — rotate to a higher throughput by adding more keys, or batch up to 500 orders per call.
Errors
Errors return a consistent JSON body:
{ "detail": "human-readable message" }
| Status | When |
|---|---|
400 | Malformed request or unresolvable symbol |
401 | Missing or invalid Authorization header |
403 | API key doesn't grant access to this strategy |
404 | Strategy or resource not found |
422 | Validation failed (including price-resolution failure) |
429 | Rate limit exceeded |
5xx | Server error — retry with exponential backoff |
Handling 422 on omitted price
If you submit an order without price and BotRanks can't find a bar for that symbol on executed_at's date (e.g. brand-new ticker, crypto outside our universe), you get:
{ "detail": "No market data available for FOO at 2026-04-18T14:00:00Z" }
Resubmit with an explicit price in that case.
Questions or issues? Open one at github.com/microblue/FreeQuant/issues.
This document is for API v1. Breaking changes will ship under /api/v2.