# QuantGPT Cloud API Guide Version: 0.5.0 | Base URL: http://www.quant-gpt.com OpenAPI: http://www.quant-gpt.com/openapi.json Public strategy verification platform — submit daily signals, automated platform tracking settlement, GitHub attestation, independent verification, and alpha factor library. --- ## Agent Quickstart (Minimum Path) ``` # 1. Register (creates anonymous account, no email needed) POST /api/v1/auth/invite { "invite_code": "YOUR_CODE" } # -> { "access_token": "eyJ...", "refresh_token": "...", "user": {...} } # 2. Create API Key (permanent — use this for all subsequent calls) POST /api/v1/billing/api-keys { "name": "my-agent" } Headers: Authorization: Bearer # -> { "api_key": "qgpt_..." } SAVE IT — shown only once # 3. Done. Use API Key for everything. Profile setup is OPTIONAL. # API Key does NOT depend on profile_complete. # You can skip profile entirely if you only use the API Key. ``` Profile setup (nickname/email/password) is only needed if you want: - A display name on public strategy pages - Email+password login as a fallback --- ## Authentication ``` Authorization: Bearer ``` | Type | Format | Lifetime | Use case | |------|--------|----------|----------| | Access Token | `eyJ...` (JWT) | 24 hours | Interactive sessions, creating API keys | | Refresh Token | `eyJ...` (JWT) | 7 days | Renew access token without re-login | | API Key | `qgpt_...` | Permanent until revoked | Agent automation (**recommended**) | ### Login Methods **Invite code (first-time registration):** ``` POST /api/v1/auth/invite Body: { "invite_code": "YOUR_CODE" } ``` ```json { "access_token": "eyJhbGciOiJIUzI1NiIs...", "refresh_token": "eyJhbGciOiJIUzI1NiIs...", "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "nickname": "user_a1b2c3", "email": "user_a1b2c3@quantgpt.local", "tier": "free", "profile_complete": false, "has_password": false } } ``` Each call creates a NEW anonymous account. Invite codes are reusable up to their max_uses limit. They are not tied to a specific tier — all new accounts start as `free`. **Email + password login (returning users):** ``` POST /api/v1/auth/login Body: { "email": "you@example.com", "password": "..." } # -> { "access_token", "refresh_token", "user": {...} } ``` Requires profile setup (email + password) first. ### Profile Setup (Optional for Agents) ``` PATCH /api/v1/auth/profile Headers: Authorization: Bearer Body: { "nickname": "my_username", "email": "me@example.com", "password": "mypassword" } # All fields optional, set any combination # -> { "id", "nickname", "email", "has_password", "profile_complete" } ``` - `@quantgpt.local` emails are rejected (reserved for anonymous accounts) - Nickname: 2-30 chars, letters/numbers/Chinese/underscore/hyphen, must be unique ### Token & Key Management ``` POST /api/v1/auth/refresh { "refresh_token": "..." } -> { "access_token": "..." } GET /api/v1/auth/me # Current user info GET /api/v1/billing/api-keys # List active API keys POST /api/v1/billing/api-keys { "name": "my-agent" } -> { "api_key": "qgpt_..." } DELETE /api/v1/billing/api-keys/{key_id} # Revoke key ``` --- ## Account Tiers | Tier | Max Strategies | How to get | |------|----------------|------------| | free | 3 | Default for all new accounts | | licensed | 20 | Admin-assigned after QuantOS (self-hosted edition) purchase | All universes (hs300, csi500, csi1000, csi2000, etf_all) are available to all tiers. You can also query dynamically: ``` GET /api/v1/billing/tier # -> { "tier": "free", "max_strategies": 3, # "strategy_universes": ["hs300","csi500","csi1000","csi2000","etf_all"] } GET /api/v1/billing/tiers # -> all tier definitions ``` --- ## Trading Calendar Before submitting signals, check if a date is a valid SSE trading day. No auth required. **Monthly query:** ``` GET /api/v1/calendar?month=2026-05 # -> { # "month": "2026-05", # "trading_days": ["2026-05-06", "2026-05-07", ...], # "count": 18, # "today": "2026-05-14", # "today_is_trading_day": true, # "next_trading_day": "2026-05-15" # } ``` **Single date query:** ``` GET /api/v1/calendar?date=2026-05-14 # -> { # "date": "2026-05-14", # "is_trading_day": true, # "next_trading_day": "2026-05-15", # "today": "2026-05-14" # } ``` Must provide either `month` or `date`. The calendar follows SSE (Shanghai Stock Exchange) rules: weekdays excluding Chinese public holidays. --- ## Strategy Management ### Create Strategy ``` POST /api/v1/strategies Headers: Authorization: Bearer qgpt_... ``` ```json { "name": "HS300 ML Alpha", "slug": "hs300-ml-alpha", "universe": "hs300", "execution_price": "open", "holding_period": 10, "initial_capital": 1000000, "commission_rate": 0.0003, "slippage_rate": 0.002 } ``` | Field | Type | Default | Constraint | |-------|------|---------|------------| | name | string | required | Display name | | slug | string | required | `^[a-z0-9][a-z0-9_-]{2,48}$`, globally unique, **immutable** (公开 URL 和 GitHub 存证路径依赖 slug,改了会断链) | | universe | enum | "hs300" | hs300 / csi500 / csi1000 / csi2000 / etf_all, **immutable** (benchmark 基准,中途切换导致 excess return 不可解释) | | execution_price | enum | "open" | "open" or "close", **immutable** | | holding_period | int | 10 | 1-60, **immutable** | | initial_capital | int | 1000000 | 100000-100000000, **immutable** | | commission_rate | float | 0.0003 | **immutable** | | slippage_rate | float | 0.002 | **immutable** | Response (201): ```json { "slug": "hs300-ml-alpha", "name": "HS300 ML Alpha", "description": null, "universe": "hs300", "execution_price": "open", "holding_period": 10, "initial_capital": 1000000, "commission_rate": 0.0003, "slippage_rate": 0.002, "status": "pending", "is_public": true, "activated_at": null, "retired_at": null, "created_at": "2026-05-14T07:15:41Z" } ``` ### List / Detail / Update / Retire ``` GET /api/v1/strategies # all statuses GET /api/v1/strategies?status=active # filter: pending/active/paused/retired GET /api/v1/strategies/{slug} # single strategy PATCH /api/v1/strategies/{slug} # update mutable fields DELETE /api/v1/strategies/{slug} # retire (soft delete) ``` GET response: ```json { "strategies": [ { "slug": "hs300-ml-alpha", "name": "HS300 ML Alpha", "description": null, "universe": "hs300", "execution_price": "open", "holding_period": 10, "initial_capital": 1000000, "commission_rate": 0.0003, "slippage_rate": 0.002, "status": "active", "is_public": true, "activated_at": "2026-05-14T07:20:00Z", "retired_at": null, "created_at": "2026-05-14T07:15:41Z" } ] } ``` Note: without `?status=`, the list includes ALL strategies including retired ones. Use `?status=active` to get only actionable strategies. **PATCH body:** `{ "name", "description", "status", "is_public" }` — only these 4 fields. Sending any other field (e.g. `universe`, `slug`) returns **422**. ### Strategy Status Machine ```json { "pending": ["retired"], "active": ["paused", "retired"], "paused": ["active", "retired"] } ``` - `pending` -> `active`: happens automatically on first signal submission - `retired` is terminal — no transitions out (公开 track record 需要完整生命周期,防止选择性隐藏亏损策略) - Invalid transitions return 400 --- ## Signal Submission ### Submit Signal ``` POST /api/v1/strategies/{slug}/signals Headers: Authorization: Bearer qgpt_... ``` ```json { "trade_date": "2026-05-14", "signals": [ {"symbol": "600519.SH", "weight": 0.05}, {"symbol": "000858.SZ", "weight": 0.04} ] } ``` - `weight`: each in (0, 1], sum <= 1.0 (余额为现金仓位,不允许杠杆) - Any A-share symbol accepted — `universe` is for benchmark only, not stock filtering - No duplicate symbols - `trade_date` must be a valid SSE trading day (check via `/api/v1/calendar`), today or future - Deadline depends on `execution_price` — **硬截止,无宽限期**: - `"open"` → trade_date 09:15 Asia/Shanghai (UTC+8) (开盘价撮合,开盘前必须提交) - `"close"` → trade_date 15:00 CST (UTC+8) (收盘价确定后锁定) - Deadline = server-received timestamp in Asia/Shanghai timezone. Network latency is your responsibility. - **截止前可覆盖**:同一日期重复提交会替换已有信号(未结算 + 未存证时)。已结算或已存证的信号不可覆盖。响应中 `replaced: true` 表示本次为覆盖提交。 - Strategy must be `pending` or `active` Response includes `attestation` field showing GitHub commit result: ```json { "id": "...", "signal_hash": "abc123...", "submitted_at": "...", "is_settled": false, "github_committed": true, "attestation": { "committed": true, "sha": "..." } } ``` If `attestation.committed` is false with `"reason": "repo_not_configured"`, it means the server's GitHub attestation repo is not set up. Signals are still recorded and will be settled — attestation is for independent verification, not a prerequisite. ### Signal History ``` GET /api/v1/strategies/{slug}/signals?limit=30&offset=0 ``` ```json { "signals": [ { "id": "a1b2c3d4-...", "trade_date": "2026-05-14", "signals": [{"symbol": "600519.SH", "weight": 0.05}, {"symbol": "000858.SZ", "weight": 0.04}], "signal_hash": "abc123def456...", "submitted_at": "2026-05-14T01:20:00+00:00", "is_settled": true, "github_committed": true } ] } ``` ### Update a Signal (Override) Before the deadline, simply re-POST with the same `trade_date` — the platform will replace the existing signal: ``` POST /api/v1/strategies/{slug}/signals (with updated data, same trade_date) → { "replaced": true, ... } ``` Override is only allowed when the existing signal is **not settled** and **not attested to GitHub**. After the deadline, the signal is locked. ### Delete Conditions ``` DELETE /api/v1/strategies/{slug}/signals/{trade_date} ``` | Condition | Required value | If violated | |-----------|---------------|-------------| | `is_settled` | `false` | 400 — settled signals are immutable | | `github_committed` | `false` | 400 — attested signals cannot be deleted | | current time vs deadline | before deadline | 400 — deadline passed, signal locked | All three conditions must be met. If any fails, the DELETE returns 400. --- ## Signal Lifecycle Timeline ``` YOU PLATFORM GITHUB | | | |-- POST signal (before | | | 09:15 or 15:00 CST | | | UTC+8) ----------------->| | | |-- compute signal_hash | | |-- git commit + push ------->| |<-- 201 { is_settled: false, | | | github_committed: true } | | | | | | [18:15 CST (UTC+8) — settlement] | | |-- fetch market prices | | |-- execute simulated trades | | |-- compute NAV | | |-- mark is_settled = true | | | | |-- How to check: | | | GET signals → is_settled | | | GET verify → full package | | | GET track-record → NAV | | ``` **Settlement schedule (all times CST = UTC+8, Asia/Shanghai):** - 09:00 — `portfolio_signal_generation()` auto-generates signals for active multi-factor portfolios - 09:10 — `attestation_retry()` retries any failed GitHub commits - 18:15 — `settle_all_active_strategies()` runs for all active strategies - 18:30 — `daily_evaluation()` recalculates strategy evaluation scores - 18:40 — `factor_os_tracking()` updates OS IC for active factors - 18:45 — `settlement_watchdog()` checks for missing NAV records **Timezone definitions:** - "CST" in this document = Asia/Shanghai, UTC+8 (NOT US Central Standard Time) - "today" = current date in Asia/Shanghai timezone (server clock) **Missed signal (no submission on a trading day):** treated as a hold day — existing positions are repriced at close, NAV is updated, no trades generated. --- ## Historical NAV Upload (Claim Data) Upload historical NAV curve for comparison with platform tracking. Upload = researcher's claim (历史业绩); platform tracking = platform's independent verification (平台跟踪). Tracking difference shows how well the claim holds up. **Best practice:** upload out-of-sample paper trading data, not in-sample backtest. Backtest curves can be overfit to look arbitrarily good; out-of-sample data cannot. When platform tracking begins, the tracking difference will expose overfit backtests (high tracking error) but validate genuine out-of-sample results (low tracking error, high correlation). The more realistic your uploaded data, the more credible your strategy. | Prerequisite | Required | If violated | |-------------|----------|-------------| | Strategy status | `active` or `paused` | 400 — submit at least one signal first to activate a `pending` strategy | | Strategy status | not `retired` | 400 — retired strategies are immutable | ### Upload / Replace ``` PUT /api/v1/strategies/{slug}/claimed Headers: Authorization: Bearer qgpt_... ``` ```json { "records": [ {"date": "2024-01-02", "nav": 1000000.00}, {"date": "2024-01-03", "nav": 1005200.50}, {"date": "2024-01-04", "nav": 1003100.25} ] } ``` | Constraint | Rule | |------------|------| | records | 2-5000 data points, chronological, no duplicates | | nav | positive, <= 1e10 | | dates | must be before strategy `activated_at` date, no future dates | Server normalizes NAV to 1.0 and computes: daily_return, cumulative_return, drawdown, sharpe, max_drawdown. If platform tracking overlap exists (>= 5 days), also computes tracking metrics. Response (201 first upload, 200 on replace): ```json { "strategy_slug": "hs300-ml-alpha", "data_points": 240, "start_date": "2024-01-02", "end_date": "2024-12-31", "total_return": 0.1523, "sharpe": 1.42, "max_drawdown": 0.0834, "uploaded_at": "2026-05-14T10:00:00Z" } ``` ### Query Claimed Performance ``` GET /api/v1/strategies/{slug}/claimed # -> { "meta": { start_date, end_date, data_points, total_return, sharpe, max_drawdown }, # "records": [{ date, nav, daily_return, cumulative_return }] } ``` ### Delete Claimed Performance ``` DELETE /api/v1/strategies/{slug}/claimed # -> { "deleted": true, "data_points_removed": 240 } ``` ### Public Claimed Performance (No Auth) ``` GET /api/v1/public/strategies/{slug}/claimed # -> same as authenticated GET, plus tracking metrics if available ``` The `track-record` endpoint also includes claimed performance data when available: ``` GET /api/v1/public/strategies/{slug}/track-record ``` ```json { "records": [ { "date": "2026-05-14", "nav": 1.0523, "daily_return": 0.0012, "cumulative_return": 0.0523, "benchmark_return": 0.0003 } ], "claimed": { "meta": { "start_date": "2024-01-02", "end_date": "2024-12-31", "data_points": 240, "total_return": 0.1523, "sharpe": 1.42, "max_drawdown": 0.0834, "uploaded_at": "2026-05-14T10:00:00+00:00" }, "records": [ { "date": "2024-01-02", "nav": 1.0, "cumulative_return": 0.0, "benchmark_return": 0.0 } ], "tracking": { "tracking_diff_annualized": 0.012, "tracking_error_annualized": 0.031, "correlation": 0.97, "overlap_days": 45 } } } ``` `claimed` and `claimed.tracking` are present only when data exists. > **Migration note:** the old `/backtest` endpoints (both auth and public) return 308/301 redirects to `/claimed`. Update your client when convenient. ### Tracking Metrics (auto-computed) When claimed and platform tracking overlap >= 5 days, the platform computes: - `tracking_diff_annualized` — mean daily return difference * 252 - `tracking_error_annualized` — std of daily return differences * sqrt(252) - `correlation` — Pearson correlation of daily returns - `overlap_days` — number of common trading days Verdict: `consistent` (TE<2%, corr>0.95), `moderate_divergence` (TE<5% or corr>0.85), `significant_divergence` (otherwise). --- ## Benchmarks | Value | Description | |-------|-------------| | `hs300` | CSI 300 (default) | | `csi500` | CSI 500 — mid-cap | | `csi1000` | CSI 1000 — small-cap | | `csi2000` | CSI 2000 — micro-cap | | `etf_all` | ETF composite | The `universe` field selects a benchmark for excess return and rolling Sharpe calculations. It does NOT restrict which stocks you can trade. --- ## Alpha Factor Library Submit factor cross-sectional values; the platform independently computes IC, turnover, and correlation using its own market data. Factors that pass all checks become `active` and enter out-of-sample tracking. ### Factor Lifecycle ``` draft → ready (auto when >= 120 trading days uploaded) → validating → active / rejected ``` - `draft`: factor created, uploading values - `ready`: enough data, can submit for validation - `validating`: platform computing IC/correlation/turnover (synchronous, < 5s) - `active`: passed all checks, entering OS tracking - `rejected`: failed one or more checks — upload more data or create a new factor ### Create Factor ``` POST /api/v1/factors Headers: Authorization: Bearer qgpt_... ``` ```json { "name": "momentum_20d", "universe": "hs300", "description": "20-day momentum factor", "expression": "rank(ts_mean(close, 20))", "claimed_ic_mean": 0.035, "claimed_ic_ir": 0.68 } ``` | Field | Type | Required | Constraint | |-------|------|----------|------------| | name | string | yes | 1-100 chars | | universe | enum | yes | hs300 / csi500 / csi1000 | | description | string | no | Free text | | expression | string | no | Factor formula (self-reported, not verified by platform) | | claimed_ic_mean | float | no | Self-reported IC mean (from local research) | | claimed_ic_ir | float | no | Self-reported IC IR | Response (201): ```json { "id": "uuid", "name": "momentum_20d", "universe": "hs300", "status": "draft", "expression": "rank(ts_mean(close, 20))", "is": { "ic_mean": null, "ic_ir": null, "turnover": null, "sharpe": null, "fitness": null, "ic_decay": null, "coverage": null, "data_days": null, "max_correlation": null }, "os": { "ic_mean_30d": null, "ic_mean_60d": null, "ic_mean_120d": null, "start_date": null }, "claimed": { "ic_mean": 0.035, "ic_ir": 0.68 }, "checks": null, "reject_reason": null, "yearly_stats": null, "submitted_at": null, "validated_at": null, "created_at": "..." } ``` ### List / Detail / Update / Delete ``` GET /api/v1/factors # list (supports ?status=, ?universe=, ?order=) GET /api/v1/factors/{factor_id} # detail (includes ic_series, ls_cumulative_returns) PATCH /api/v1/factors/{factor_id} # update name/description/expression (draft/rejected only) DELETE /api/v1/factors/{factor_id} # delete factor + values (draft/rejected only) ``` List supports: `?status=active`, `?universe=hs300`, `?order=-created_at|ic_mean|ic_ir|created_at`, `?limit=50&offset=0`. Detail response includes additional series data (not in list): - `ic_series`: `[{"date": "2025-01-02", "ic": 0.042}, ...]` — daily IC values - `ls_cumulative_returns`: `[{"date": "2025-01-02", "ls_return": 0.003, "cumulative": 1.003}, ...]` — long-short portfolio (top/bottom quintile) - `yearly_stats`: `[{"year": 2024, "ic_mean": 0.035, "ic_ir": 0.68, "returns": 0.12, "drawdown": 0.05, "turnover": 0.3, "days": 244}, ...]` ### Upload Factor Values ``` POST /api/v1/factors/{factor_id}/values Headers: Authorization: Bearer qgpt_... ``` ```json { "data": [ { "date": "2025-01-02", "values": { "600519.SH": 1.23, "600036.SH": -0.45 } }, { "date": "2025-01-03", "values": { "600519.SH": 0.87, "600036.SH": 0.12 } } ] } ``` | Constraint | Rule | |------------|------| | data | 1-500 entries per request | | date | must be SSE trading day (non-trading days silently skipped) | | values | non-empty dict of `symbol: float` | | factor status | must be draft, ready, or rejected | **Factor values format:** - Symbol format: SSE/SZSE with exchange suffix, e.g. `600519.SH`, `000001.SZ` - Values: cross-sectional factor scores (raw values, z-scores, or ranks — the platform ranks internally for IC computation) - Each date should cover as many symbols in the target universe as possible (coverage affects validation) Upsert behavior: same factor_id + date overwrites existing values. When total uploaded days >= 120, status auto-promotes from `draft` to `ready`. Uploading new data to a `rejected` factor resets it to `draft` or `ready` (clears checks/reject_reason). Response: ```json { "uploaded": 2, "total_days": 145, "status": "ready" } ``` ### Check Upload Progress ``` GET /api/v1/factors/{factor_id}/values/status # -> { "total_days": 145, "min_required": 120, "ready": true, # "start_date": "2024-06-03", "end_date": "2025-01-03" } ``` ### Submit for Validation ``` POST /api/v1/factors/{factor_id}/submit ``` Requires `status == "ready"`. The platform: 1. Fetches 1-day forward returns with delay=1 (factor_T predicts ret_{T+1→T+2}) 2. Computes Spearman rank IC (Pearson correlation on ranks, handles ties correctly) 3. Computes IC mean, IC IR (sample std), turnover, IC decay (1-5 day lags), coverage 4. L/S backtest: daily rebalance, quintile long-short on 1-day returns 5. Computes annualized Sharpe from L/S daily returns 6. Computes Fitness = Sharpe × √(|AnnualizedReturn| / max(Turnover, 0.02)) 7. Checks pairwise correlation against all user's `active` factors 8. Runs 5 checks — all must pass for `active` status **Key design choices (aligned with WQ BRAIN):** - **delay=1 (fixed)**: factor values computed at T close → trade at T+1 close. Prevents look-ahead bias. Not user-configurable. - **Daily rebalance**: all factors use 1-day forward returns for both IC and L/S backtest. No holding_period — ensures uniform evaluation scale. - **abs(IC) checks**: IC_MEAN and IC_IR use absolute values, so negative-IC factors (inverted direction) are accepted. Flip factor sign if IC is negative. - **Fitness**: WQ BRAIN's primary quality metric. Displayed as reference; not used as a pass/fail check. **Validation Checks:** | Check | Metric | Threshold | Fail means | |-------|--------|-----------|------------| | IC_MEAN | abs(ic_mean) | > 0.015 | Insufficient predictive power | | IC_IR | abs(ic_ir) | > 0.15 | Unstable signal | | HIGH_TURNOVER | turnover | < 0.35 | Suspected overfitting | | SELF_CORRELATION | max_correlation | < 0.7 | Too similar to existing factor | | DATA_COVERAGE | data_days | >= 120 | Insufficient data | Response: full factor object with `is` metrics (including `sharpe` and `fitness`), `checks` array, `status` (active/rejected), plus `ic_series`, `ls_cumulative_returns`, and `yearly_stats`. ### Poll Validation Status ``` GET /api/v1/factors/{factor_id}/submit # -> { "status": "active", "checks": [...], "validated_at": "..." } ``` ### View Correlations ``` GET /api/v1/factors/{factor_id}/correlations # -> { "correlations": [ # { "factor_id": "uuid", "name": "value_factor", "correlation": 0.423 } # ] } ``` Returns pairwise Spearman correlation with all user's `active` factors, sorted by |correlation| descending. ### IC Time Series ``` GET /api/v1/factors/{factor_id}/ic-series # -> { "records": [{ "date": "2025-01-02", "ic": 0.042 }, ...] } ``` ### Chart Data (all series in one call) ``` GET /api/v1/factors/{factor_id}/chart-data # -> { "ic_series": [...], "ls_cumulative_returns": [...], "yearly_stats": [...] } ``` Returns IC time series, long-short cumulative returns (quintile-based), and yearly performance breakdown. All data is pre-computed at validation time. ### Out-of-Sample Tracking Active factors are tracked daily (18:40 CST). As new factor values are uploaded post-activation, the platform computes rolling OS IC: - `os_ic_mean_30d` — last 30 trading days - `os_ic_mean_60d` — last 60 trading days - `os_ic_mean_120d` — last 120 trading days These appear in the factor detail response under the `os` key. --- ## Multi-Factor Portfolio Combine N active factors into a portfolio that auto-generates daily strategy signals. The platform z-score normalizes each factor, weights them (equal or IC_IR-based), ranks stocks by combined score, selects top_n, and submits as a DailySignal — feeding directly into the existing settlement pipeline. ### Portfolio Lifecycle ``` draft → active → paused → active (resume) ``` - `draft`: created, adding/removing member factors - `active`: auto-generates signals daily at 09:00 CST (before 09:15 open-price deadline) - `paused`: signal generation stopped, can resume or modify members ### Create Portfolio ``` POST /api/v1/portfolios { "name": "momentum_value", "universe": "hs300", "weighting": "equal", "top_n": 50 } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | name | string | yes | Portfolio name (1-100 chars) | | universe | string | yes | hs300 / csi500 / csi1000 | | weighting | string | no | `equal` (default) or `ic_weighted` | | top_n | integer | no | Number of stocks to select (default: 50, range: 5-500) | ### CRUD Endpoints ``` GET /api/v1/portfolios # list all portfolios GET /api/v1/portfolios/{portfolio_id} # detail (includes members + strategy info) DELETE /api/v1/portfolios/{portfolio_id} # delete (draft/paused only) ``` ### Member Management ``` POST /api/v1/portfolios/{portfolio_id}/members # add factor DELETE /api/v1/portfolios/{portfolio_id}/members/{factor_id} # remove factor ``` **Add member request:** ```json { "factor_id": "" } ``` Validations: - Factor must belong to the current user and be `active` - Factor universe must match portfolio universe - Cannot add duplicates - Cannot modify members while portfolio is `active` (pause first) ### Lifecycle Actions ``` POST /api/v1/portfolios/{portfolio_id}/activate # draft → active (creates Strategy, starts signals) POST /api/v1/portfolios/{portfolio_id}/pause # active → paused POST /api/v1/portfolios/{portfolio_id}/resume # paused → active ``` **Activate requirements:** at least 2 member factors. On activate, the platform creates a Strategy (slug: `mf--`, execution_price: "open") and binds it to the portfolio. If it's a trading day and before 09:15 CST, an immediate signal is generated. ### Signal History ``` GET /api/v1/portfolios/{portfolio_id}/signals?limit=30&offset=0 ``` Returns signals generated for this portfolio's strategy, including trade_date, stock_count, settlement status, and attestation status. ### Signal Generation Algorithm 1. Load each member factor's values for the target date 2. Z-score normalize each factor's cross-sectional values (sample std) 3. Weight factors: `equal` = 1/N each; `ic_weighted` = abs(IC_IR) / sum(abs(IC_IR)) 4. Combine: `score[symbol] = Σ(weight_i × zscore_i[symbol])` (intersection of all factors) 5. Rank by combined score descending, select top_n stocks 6. Assign equal weight: `1/top_n` per stock 7. Submit as DailySignal → settlement → NAV → attestation --- ## QuantOS License (Authenticated Users) QuantOS is the self-hosted private deployment edition of QuantGPT — runs backtesting and live trading infrastructure on the user's own servers. Licensed users get a machine-bound license file to activate their deployment. ### Platform Requirements - **Docker image architecture**: linux/amd64 + linux/arm64 (multi-platform) - **Linux x86_64**: recommended — native Docker, best performance - **Linux arm64**: supported — native Docker (e.g. AWS Graviton, Oracle Ampere) - **Windows x86_64**: supported via Docker Desktop (WSL2 backend) - **macOS Apple Silicon (M1–M4)**: supported via Docker Desktop (native arm64) - **macOS Intel**: supported via Docker Desktop ### Machine Fingerprint Generate the 64-char hex fingerprint on the target deployment server: - Linux / macOS: download and run `http://www.quant-gpt.com/deploy-kit/fingerprint.sh` - Windows: download and double-click `http://www.quant-gpt.com/deploy-kit/fingerprint.bat` ### Apply for a License ``` POST /api/v1/deploy/license-request Body: { "customer": "Acme Corp", "fingerprint": "<64-hex SHA256>", "note": "optional" } # -> 201 { "id", "customer", "fingerprint", "status": "pending", "created_at" } # -> 409 if a pending request already exists GET /api/v1/deploy/license-request # -> { "has_request": true, "id", "customer", "fingerprint", "status", "admin_reply", "created_at" } # -> { "has_request": false } ``` Status: `pending` → `approved` (license auto-issued, download available) or `rejected` (see `admin_reply`). If rejected, submit a new request after addressing the issue. ### View / Download License & Image ``` GET /api/v1/deploy/my-license # -> { "has_license": true, "customer", "fingerprint", "expires", "max_cores", "max_memory_gb", "created_at" } # -> { "has_license": false } GET /api/v1/deploy/my-license/download # -> license.json (application/json) GET /api/v1/deploy/quantos-image/download?arch=amd64 # arch: amd64 (default) or arm64 — auto-detected by install.sh # -> 302 redirect to download quantos-{arch}.tar.gz # Load with: docker load -i quantos-{arch}.tar.gz ``` ### Deployment Guide Full step-by-step guide (Chinese): `http://www.quant-gpt.com/guides/private-deploy` --- ## Public Verification (No Auth Required) All endpoints under `/api/v1/public/strategies/`. ### Strategy List ``` GET /api/v1/public/strategies?universe=hs300&limit=50&offset=0 ``` ```json { "strategies": [{ "slug": "hs300-ml-alpha", "name": "HS300 ML Alpha", "description": null, "universe": "hs300", "holding_period": 10, "execution_price": "open", "status": "active", "creator": "user_a1b2c3", "created_at": "2026-05-14T07:15:41+00:00", "activated_at": "2026-05-14T07:20:00+00:00", "trading_days": 45, "has_claimed": true, "latest_date": "2026-05-14", "nav": 1052300.50, "total_return": 0.0523, "daily_return": 0.0012 }] } ``` ### NAV History ``` GET /api/v1/public/strategies/{slug}/track-record # -> { "records": [{ "date", "nav", "daily_return", "cumulative_return", "benchmark_return" }] } ``` ### Performance Metrics ``` GET /api/v1/public/strategies/{slug}/performance # -> { # "metrics": { "total_return", "annual_return", "volatility", "sharpe", "max_drawdown", # "calmar", "excess_return", "excess_sharpe", "benchmark_return", "trading_days" }, # "rolling_excess_sharpe": [{ "date", "value" }] # } ``` ### Verification Data ``` GET /api/v1/public/strategies/{slug}/verify?date=2026-05-13 ``` Returns: `verification.nav`, `verification.positions`, `verification.signal_attestation`, `verification.trades`, `data_provenance`, `how_to_verify`. ### Verification Steps 1. Fetch verification data from the API 2. Fetch GitHub attestation from `github_raw_url`, compare `signal_hash` 3. Verify `symbols_hash`: `SHA256(comma-joined sorted symbols)[:16]` 4. Verify NAV: `sum(positions.market_value) + cash == reported_nav` 5. Check GitHub commit timestamp is before signal deadline: 09:15 CST (UTC+8) for open-price strategies, 15:00 CST (UTC+8) for close-price strategies (only valid for dates >= 2026-05-13) ### Audit Log ``` GET /api/v1/public/strategies/{slug}/audit?limit=50&offset=0 # -> { "audit_log": [{ "date", "type": "REBALANCE"|"HOLD", "detail" }], "total": int } ``` --- ## Pagination Endpoints that return lists accept: - `limit`: max items to return (default varies by endpoint, max 200) - `offset`: skip this many items (default 0) Specific defaults: signals limit=30, notifications limit=50, audit limit=50, public strategies limit=50, factors limit=50. --- ## Tickets / Feedback ``` POST /api/v1/tickets Body: { "title": "...", "description": "...", "category": "bug" } # category: bug | feature | feedback | data | other ``` Response (201): ```json { "id": "uuid", "user_id": "uuid", "title": "...", "description": "...", "category": "bug", "status": "open", "admin_reply": null, "resolved_at": null, "created_at": "2026-05-14T07:15:41+00:00" } ``` ``` GET /api/v1/tickets # List my tickets GET /api/v1/tickets?status=open # Filter by status GET /api/v1/tickets/{ticket_id} # Single ticket detail ``` --- ## Notifications ``` GET /api/v1/notifications # List all GET /api/v1/notifications?unread_only=true # Unread only GET /api/v1/notifications/unread-count # -> { "unread_count": 3 } PATCH /api/v1/notifications/{id}/read # Mark one as read POST /api/v1/notifications/read-all # Mark all as read ``` --- ## Error Response Format **Business errors (400/401/403/404/409):** ```json {"detail": "human-readable error message"} ``` The `detail` field is always a string. **Validation errors (422 — invalid request body):** ```json { "detail": [ {"type": "value_error", "loc": ["body", "field_name"], "msg": "error description"} ] } ``` The `detail` field is an array of error objects. Each has `type`, `loc` (path to the invalid field), and `msg`. **Server errors (500):** should not happen. If you receive one, report it via the tickets API. --- ## Recommended Workflow **One-time setup:** ``` POST /api/v1/auth/invite -> get access_token POST /api/v1/billing/api-keys -> get API key (use for all future calls) POST /api/v1/strategies -> create strategy with slug ``` **Agent daily loop (pseudocode):** ```python # run at ~09:00 CST (UTC+8) # "today" = current date in Asia/Shanghai timezone calendar = GET /api/v1/calendar?date={today} if not calendar.is_trading_day: exit() strategies = GET /api/v1/strategies?status=active for s in strategies: signals = compute_signals(s) POST /api/v1/strategies/{s.slug}/signals { trade_date: today, signals } # if 409: already submitted today, skip # if 400: deadline passed or not a trading day # run at ~18:45 CST (UTC+8) (settlement check) for s in strategies: resp = GET /api/v1/strategies/{s.slug}/signals?limit=1 if resp.signals[0].is_settled: nav = GET /api/v1/public/strategies/{s.slug}/verify?date={today} # verify nav, log results ``` **Factor submission workflow (pseudocode):** ```python # Step 1: Create factor factor = POST /api/v1/factors { "name": "my_alpha", "universe": "hs300", "expression": "rank(ts_mean(close, 20))", # optional, for record-keeping "claimed_ic_mean": 0.035 # optional, self-reported } factor_id = factor.id # Step 2: Upload factor values (batch, max 500 per request) for batch in chunked(factor_data, 500): POST /api/v1/factors/{factor_id}/values { "data": batch } # Step 3: Check readiness status = GET /api/v1/factors/{factor_id}/values/status assert status.ready # >= 120 trading days # Step 4: Submit for validation result = POST /api/v1/factors/{factor_id}/submit if result.status == "active": # factor passed — now in OS tracking elif result.status == "rejected": # check result.checks for which check failed # upload more/better data and resubmit ``` **First-time agent bootstrap (pseudocode):** ```python # Step 1: Register resp = POST /api/v1/auth/invite { "invite_code": INVITE_CODE } access_token = resp.access_token # Step 2: Create API key (use for all future calls) key_resp = POST /api/v1/billing/api-keys { "name": "my-agent" } api_key = key_resp.api_key # SAVE — shown only once # Step 3: Create strategy POST /api/v1/strategies { "name": "...", "slug": "...", "universe": "hs300" } # if 409: slug already taken, choose another # Step 4: Verify setup strategies = GET /api/v1/strategies assert len(strategies.strategies) > 0 # Setup complete — enter daily loop ``` --- ## Error Recovery ### Signal submission errors ``` 409 Conflict (already submitted today): existing = GET /api/v1/strategies/{slug}/signals?limit=1 if existing.signals[0].signals == my_signals: # identical content — skip, no action needed else: DELETE /api/v1/strategies/{slug}/signals/{today} POST /api/v1/strategies/{slug}/signals { new signals } 400 "not a trading day": cal = GET /api/v1/calendar?date={trade_date} # use cal.next_trading_day instead 400 "deadline passed": # no recovery — signal window is closed for this date # submit for next_trading_day instead 400 "strategy status is retired/paused": # retired: cannot recover, create a new strategy # paused: PATCH /api/v1/strategies/{slug} { "status": "active" } then retry 422 Validation error: # read detail[].loc to find which field is wrong # fix the specific field and retry — do NOT retry with same body ``` ### Authentication errors ``` 401 "Invalid token": if using API key (qgpt_...): # key may be revoked — GET /api/v1/billing/api-keys to check # if missing, create new: POST /api/v1/billing/api-keys { "name": "..." } if using access_token (eyJ...): # expired — POST /api/v1/auth/refresh { "refresh_token": "..." } # if refresh also fails: re-login with email+password 403 "Account disabled": # admin has deactivated this account — contact support POST /api/v1/tickets { "category": "other", "title": "account disabled" } ``` ### Settlement not appearing ``` # After 18:15 CST (UTC+8), check if today's signal was settled: resp = GET /api/v1/strategies/{slug}/signals?limit=1 if resp.signals[0].trade_date == today and resp.signals[0].is_settled == false: if current_time < 19:00 CST (UTC+8): # normal — settlement may still be running, wait 30 min else: # likely an issue — file a ticket POST /api/v1/tickets { "category": "data", "title": "settlement missing for {slug} on {today}" } ``` ### Network timeout recovery ``` # HTTP request timed out or connection reset: # 1. Do NOT blindly retry POST (non-idempotent) — the request may have succeeded # 2. Confirm current state first: # - Signal submit timeout -> GET /api/v1/strategies/{slug}/signals?limit=1 # If latest signal.trade_date == today: submission succeeded, skip retry # - Strategy create timeout -> GET /api/v1/strategies to check if slug exists # - API key create timeout -> GET /api/v1/billing/api-keys to check # 3. Only retry if the GET confirms the operation did NOT complete # 4. For idempotent operations (PUT, DELETE, PATCH): safe to retry directly ``` --- ## Idempotency & Retry Safety | Operation | Idempotent? | Confirm before retry | Notes | |-----------|-------------|----------------------|-------| | POST /api/v1/auth/invite | NO | — | Each call creates a NEW account | | POST /api/v1/billing/api-keys | NO | GET /api/v1/billing/api-keys | Each call creates a NEW key (even with same name) | | POST /api/v1/strategies | NO | GET /api/v1/strategies | 409 if slug already taken | | POST /api/v1/strategies/{slug}/signals | NO | GET /api/v1/strategies/{slug}/signals?limit=1 | 409 if trade_date already submitted | | PUT /api/v1/strategies/{slug}/claimed | YES | — | Always replaces existing data, safe to retry | | DELETE /api/v1/strategies/{slug}/signals/{date} | YES | — | 404 on second call (safe) | | POST /api/v1/notifications/read-all | YES | — | Safe to call repeatedly | | PATCH /api/v1/auth/profile | YES | — | Same data = same result | | POST /api/v1/factors | NO | GET /api/v1/factors | Each call creates a NEW factor | | POST /api/v1/factors/{id}/values | YES | — | Upsert — same date overwrites, safe to retry | | POST /api/v1/factors/{id}/submit | NO | GET /api/v1/factors/{id}/submit | Re-validates; check status before retry | | POST /api/v1/tickets | NO | GET /api/v1/tickets | Each call creates a NEW ticket | For non-idempotent POST operations: after a timeout, query the "Confirm before retry" endpoint to check if the operation already succeeded before retrying. --- ## Rate Limits No rate limits are currently enforced. You can poll settlement status, submit signals, and query public endpoints without throttling. This may change in the future — if rate limiting is introduced, responses will include `429 Too Many Requests` with a `Retry-After` header. --- ## Agent Security Rules 1. **Use API key, not access token** — API keys are permanent and do not require refresh logic. One key per agent instance. 2. **Never log or print tokens** — do not write `access_token`, `refresh_token`, or `api_key` values to stdout, log files, or error reports. Mask as `qgpt_...xxxx` (last 4 chars) if you must reference a key. 3. **Store credentials in environment variables or secrets manager** — never hardcode in source. 4. **Revoke compromised keys immediately** — `DELETE /api/v1/billing/api-keys/{key_id}` and create a new one. 5. **Do not share API keys across agent instances** — if one agent is compromised, revocation should not affect others. --- ## Health Check ``` GET /api/v1/health # -> { "status": "ok" } ```