{"openapi":"3.1.0","info":{"title":"Vega API","version":"1.0.0"},"paths":{"/v1/auth/signup":{"post":{"summary":"Signup","description":"Create a new user account, open a session, and return the session cookie.\n\nValidation:\n  - email must be non-empty and well-formed (server-side, not just the UI)\n  - password must be 8–72 bytes (bcrypt limit)\n  - persona (optional) must be one of the known values if provided\nReturns 409 if the email is already registered.\n\nSide effect: a verification email is scheduled via BackgroundTasks (dev:\nlogged) — the account starts unverified but is NOT blocked from using the\nproduct. Scheduling it off the request path (IMPORTANT-1a) keeps signup's\nresponse time independent of mail-send cost.","operationId":"signup_v1_auth_signup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AuthBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/login":{"post":{"summary":"Login","description":"Authenticate with email + password; set a session cookie on success.\n\nError is intentionally generic (no email enumeration).","operationId":"login_v1_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_AuthBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/logout":{"post":{"summary":"Logout","description":"Revoke the current session and clear the session cookie.\nSafe to call even when not logged in (no-op on missing/invalid token).","operationId":"logout_v1_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/auth/me":{"get":{"summary":"Me","description":"Logged in: the user's id and email.\nLogged out: 200 {\"user\": None} — NOT a 401. The home page probes this on\nevery load; an error status spams the browser console for every visitor.","operationId":"me_v1_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/auth/email-link":{"post":{"summary":"Email Link Request","description":"Email a one-time sign-in link. ALWAYS 200 with the same body for a\nwell-formed address — known, unknown, suspended or not creatable alike.\n\nRATE-LIMITED twice, both keyed on facts the caller already holds (the IP,\nthe typed address), never on whether the address has an account — so a 429\nis enumeration-safe: per IP on the shared auth limiter (a flood across many\naddresses), and per ADDRESS on its own limiter (a mail bomb aimed at one\ninbox, from many IPs).","operationId":"email_link_request_v1_auth_email_link_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_EmailLinkBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/email-link/consume":{"post":{"summary":"Email Link Consume","description":"Redeem a sign-in link: single-use, unexpired, POST only.\n\nOn success: the token is spent (and any sibling login links with it), the\naddress is VERIFIED (clicking a link delivered to it proves it), a session\ncookie is set exactly as /login sets it, and — only on the FIRST sign-in of\nan account the email-link request created — the new-member hooks run once\n(desks, welcome bots, attribution, the welcome email). An existing member\nsigning in by link gets none of them.\n\nAn UNPROVEN account (unverified, holding a password) is treated like a\npassword reset (security audit F08/F19): whoever typed the address at signup\nis not necessarily who owns the inbox, so their password, sessions, keys and\npasskeys stop working before the new session is minted.","operationId":"email_link_consume_v1_auth_email_link_consume_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_EmailLinkConsumeBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/forgot":{"post":{"summary":"Forgot Password","description":"Start a password reset.  ALWAYS returns 200 {\"ok\": true} — byte-identical\nwhether or not the email is registered (no account enumeration).  When the\nemail DOES match a user, a single-use, 30-minute reset link is emailed\n(dev: logged).  The token is never returned in this response.\n\nIMPORTANT-1a: the token-INSERT + mail-send are scheduled via\nBackgroundTasks, not run inline — the response is written before that\nwork happens, so a caller timing the request can't distinguish \"email\nexists, token+mail scheduled\" from \"email unknown, nothing scheduled\" by\nresponse latency (the one lookup that DOES run inline, user_id_by_email,\nis a single indexed SELECT either way).\n\nRATE-LIMITED per IP (launch #7, 2026-08-12): without it a loop on a known\nmember's address is a MAIL BOMB — one real Resend send per request, burning\nquota and the member's inbox. Same auth_limiter + rightmost-XFF IP as\nlogin/signup, checked BEFORE the lookup so a flood consumes no DB work.\nThe 429 is still enumeration-safe: it's keyed on IP, never on whether the\nemail exists.","operationId":"forgot_password_v1_auth_forgot_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ForgotBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/reset":{"post":{"summary":"Reset Password","description":"Complete a password reset with a token + a new password (8–72 bytes).\n\nOn success: rotates the password, invalidates any OTHER unused reset\ntokens the user still holds (MINOR-2 — defense in depth against a stale\nbut still-valid link from an earlier \"forgot\" click), REVOKES ALL of the\nuser's sessions (every existing cookie dies), consumes the presented token\n(single-use), and returns {\"ok\": true}.  A missing/expired/already-used\ntoken → 400 (generic).","operationId":"reset_password_v1_auth_reset_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ResetBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/change-password":{"post":{"summary":"Change Password","description":"Change the logged-in user's password.  Session-auth required.\n\nVerifies current_password against the stored hash with the SAME bcrypt +\ntiming discipline as /login (db.verify_user_password — bcrypt.checkpw, dummy\nhash on the miss path), enforces the signup length rule on new_password, and\nrejects a no-op (new == current).\n\nOn success: rotates the password and REVOKES ALL OTHER sessions — every\nother device/cookie is logged out so a stolen session dies with the old\npassword — while KEEPING the requesting session alive (the user who just\nchanged their password stays logged in HERE).  Returns {\"ok\": true}.\n\nFailures:\n  - no/invalid session   → 401 (require_session_user)\n  - wrong current        → 403 \"Current password is incorrect.\"  (deliberately\n    NOT 401: a bad guess must not make the browser treat the caller as logged\n    out and tear down their session state client-side)\n  - invalid / no-op new  → 422 (the rule, in plain words)\n  - too many attempts    → 429 (per-user brute-force guard on current_password)","operationId":"change_password_v1_auth_change_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_ChangePwBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/verify":{"get":{"summary":"Verify Email","description":"Verify an email via the link in the verification message.  Idempotent: a\nfresh token verifies the account; clicking the SAME link again (already-used\ntoken, user already verified) still lands on the success page rather than an\nerror.  Redirects to the web app with ?verified=1 (ok) or ?verified=0\n(expired/unknown link — the app shows a resend prompt).\n\nSide effect: on the FIRST successful verify, the post-verification welcome\nemail is scheduled via BackgroundTasks (best-effort, off the request path).\nThe send is gated by a compare-and-set on welcomed_at (_enqueue_welcome_once\n-> db.claim_welcome), so a double-click / re-verify never sends a second\nwelcome, and a mail failure never breaks verification.","operationId":"verify_email_v1_auth_verify_get","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/resend-verification":{"post":{"summary":"Resend Verification","description":"Re-send the verification email to the logged-in user.  Requires a session.\nReturns {\"ok\": true} whether or not the user is already verified (no state\nleak); a no-op token/email is skipped when already verified.  The send is\nscheduled via BackgroundTasks (IMPORTANT-1a), consistent with signup/forgot.","operationId":"resend_verification_v1_auth_resend_verification_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/auth/tag-available":{"get":{"summary":"Tag Available","description":"Availability probe for the signup form.  200 always (except 429):\n{\"available\": true} or {\"available\": false, \"reason\": <plain words>}.\nThe truth source for the race is still the DB unique index at write time.","operationId":"tag_available_v1_auth_tag_available_get","parameters":[{"name":"tag","in":"query","required":false,"schema":{"type":"string","default":"","title":"Tag"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/tag-suggestions":{"get":{"summary":"Tag Suggestions","description":"Three availability-checked suggestions from the trading word banks.","operationId":"tag_suggestions_v1_auth_tag_suggestions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/trader-tag":{"patch":{"summary":"Change Trader Tag","description":"Claim (first time — no cooldown; this IS the /claim-tag backend for\nGoogle-created and legacy users) or change (30-day cooldown) the tag.\n\n  400  invalid / reserved / same tag you already have\n  401  not logged in\n  409  taken (pre-check OR the DB unique index on a race)\n  429  cooldown not over — detail says how many days remain","operationId":"change_trader_tag_v1_me_trader_tag_patch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_TagBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/google/start":{"get":{"summary":"Google Start","description":"302 to Google's consent screen with fresh state+nonce, both pinned in a\nsigned, httpOnly, 10-minute cookie that only /v1/auth/google/* receives.\n\n``anon`` (optional) is the browser's own analytics id, parked in a second\nshort-lived cookie so the callback can run the anon→member claim for a NEW\nmember — see _ANON_COOKIE. It is never read as identity.","operationId":"google_start_v1_auth_google_start_get","parameters":[{"name":"anon","in":"query","required":false,"schema":{"type":"string","default":"","title":"Anon"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/google/callback":{"get":{"summary":"Google Callback","description":"Verify state -> exchange code -> verify id_token (sig/aud/iss/exp via\ngoogle-auth, nonce + email_verified here) -> session cookie -> redirect.\n\n  303  Google itself refused BEFORE issuing a code (?error=...): the user\n       cancelled, or (far more common) the OAuth consent screen blocked the\n       account — app still in \"Testing\" and the address isn't a test user,\n       or the app is pending verification. Nothing our code can fix; we bounce\n       the browser back to /login with a readable banner instead of a raw 400.\n  400  missing/forged/expired state or cookie (CSRF / replayed callback)\n  401  code exchange refused, or the id_token failed verification\n  403  Google says the email is NOT verified (no account is created/linked)\n  503  not configured","operationId":"google_callback_v1_auth_google_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"type":"string","default":"","title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"type":"string","default":"","title":"State"}},{"name":"error","in":"query","required":false,"schema":{"type":"string","default":"","title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/passkeys/register-options":{"post":{"summary":"Register Options","operationId":"register_options_v1_me_passkeys_register_options_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/passkeys/register-verify":{"post":{"summary":"Register Verify","operationId":"register_verify_v1_me_passkeys_register_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_RegisterVerifyBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/passkeys":{"get":{"summary":"List Passkeys","operationId":"list_passkeys_v1_me_passkeys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/passkeys/{passkey_id}":{"delete":{"summary":"Delete Passkey","operationId":"delete_passkey_v1_me_passkeys__passkey_id__delete","parameters":[{"name":"passkey_id","in":"path","required":true,"schema":{"type":"integer","title":"Passkey Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/auth/passkey/login-options":{"post":{"summary":"Login Options","operationId":"login_options_v1_auth_passkey_login_options_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/auth/passkey/login-verify":{"post":{"summary":"Login Verify","operationId":"login_verify_v1_auth_passkey_login_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_LoginVerifyBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/backtests/{run_id}/explain":{"get":{"summary":"Explain Backtest","description":"Plain-English story for one of the CALLER'S backtests. 404 cloak on a\nmissing/foreign run; 503 when AI is off or unconfigured; 429 over the rate cap.\n\nP1-F2: refused (403, friendly) for the SHARED guest key with no session — the\ndemo tenant is one row shared by every anonymous visitor, so \"the caller's\nbacktests\" would include strangers' runs (and spend the shared AI budget on\nthem).","operationId":"explain_backtest_v1_backtests__run_id__explain_get","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/strategies/copilot":{"post":{"summary":"Strategies Copilot","description":"Plain English → a DRAFT spec JSON, validated (never executed) and returned\nwith the validator's honest notes. Off-topic / non-JSON model output → 422.\n\nFIRST, the deterministic intake guard: a manipulation / illegal / reckless /\ndeception / injection / guaranteed-returns request is refused HERE (never\ndrafted), and a vague wish / bare question gets a clarify — before the LLM is\never called. The guard is the safety floor; the copilot's own prompt is a\nsecond line only.","operationId":"strategies_copilot_v1_strategies_copilot_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CopilotRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/backtests/{run_id}/suggest-fix":{"post":{"summary":"Suggest Fix","description":"ONE supported, in-grammar spec change aimed at a stored backtest's HONEST\nfailure signals (SHELF/inconclusive verdict, low confidence, optimism gap,\nstop-reliance, thin sample). Returns {suggestion_text, suggested_spec (validated\nor null), valid, issues, disclaimer, label}. 404 on a missing/foreign run;\n503 when AI is off/unconfigured; 429 over the rate cap. The suggestion never\npromises to work — re-test via the normal backtest/deploy path.","operationId":"suggest_fix_v1_backtests__run_id__suggest_fix_post","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/strategies/port-external":{"post":{"summary":"Port External","description":"Translate a pasted external strategy (Pine/Python/prose) into a Quantradin\nspec, then run the SAME validate + port-check every spec gets. Returns\n{spec, valid, issues, port_report, label}. 503 when AI is off/unconfigured.\nThe pasted text is treated strictly as DATA (prompt-injection fenced); nothing\nit says can change behavior — the translated spec is validated normally and\nhonest gaps show up in the port report.","operationId":"port_external_v1_strategies_port_external_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortExternalRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/strategies/import":{"post":{"summary":"Import Strategy","description":"Frictionless import: paste a strategy (Pine / Python / plain English) →\na runnable Quantradin spec. Returns {spec, defaults_applied, mapping_notes,\nvalidation, source_type, label}. 503 when AI is off/unconfigured; 429 over the\nrate cap; 422 on an unknown source_type.\n\nOrder is fixed: intake guard FIRST (a manipulation / illegal / reckless /\ninjection / guaranteed-returns request is refused here, before the LLM is\nasked to translate anything — the safety floor never depends on the model),\nTHEN a source-type-aware AI translation, THEN suggest_defaults fills the safe\nblanks, THEN validate_spec runs. The engine alone grades — this route only\ndrafts + validates the spec.\n\nGUEST-FRIENDLY: converting is the home funnel, so a logged-OUT visitor may call it\n(per-IP rate-capped); logged-in callers use the per-user gate. The intake safety\nguard runs first either way. Running/saving/deploying still needs signup.","operationId":"import_strategy_v1_strategies_import_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/strategies/build/phase":{"post":{"summary":"Build Phase","description":"ONE step of the guided build: the member's plain English + the spec so far\n→ a spec FRAGMENT, a PLAIN-ENGLISH readback of what we understood, and the\nvalidator's honest word on the spec that fragment makes.\n\nThe AI translates. The ENGINE validates. Nothing here grades, and nothing here\ncan refuse to let the member continue — `blocking` says only whether THIS\nstep's own fragment is what broke the spec, so the page can ask again in the\nconfirm voice instead of failing.","operationId":"build_phase_v1_strategies_build_phase_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BuildPhaseRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/strategies/reality-check":{"post":{"summary":"Reality Check","description":"How many times has this rule ACTUALLY happened, on real bars?\n\nCounted by the engine on the same feature frame a backtest builds — never\nestimated, never extrapolated. `measured:false` means we could not take the\ncount, and the page must show no number at all rather than a guess.\n\nIt is ADVISORY. Zero fires is an honest observation with a suggestion to\nloosen, not a refusal: the member decides.","operationId":"reality_check_v1_strategies_reality_check_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RealityCheckRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/backtests/{run_id}/tweaks":{"post":{"summary":"Backtest Tweaks","description":"Up to four concrete tweaks for a graded run, each with a plain-words why and\na FULL patched spec the member can apply with one click.\n\nAI proposes; NOTHING auto-applies; the engine re-grades on the member's click.\nA tweak whose spec does not validate keeps its explanation and loses its Apply\nbutton — we never hand back a spec that cannot run.\n\nSession-cookie only (an account key pair gains no AI access). 404 on a missing\nor foreign run — byte-identical either way, so probing another member's ids\ntells a stranger nothing.","operationId":"backtest_tweaks_v1_backtests__run_id__tweaks_post","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/public/house-tape":{"get":{"summary":"Public House Tape","operationId":"public_house_tape_v1_public_house_tape_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/public/house-bots":{"get":{"summary":"Public House Bots","operationId":"public_house_bots_v1_public_house_bots_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/discord/link-code":{"post":{"summary":"My Link Code","description":"One-time 8-character code for `/link` (session only). Minting again\nreplaces the old code; a code older than a day no longer links.","operationId":"my_link_code_v1_me_discord_link_code_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/discord/interactions":{"post":{"summary":"Interactions","operationId":"interactions_v1_discord_interactions_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"summary":"Well Known As","operationId":"well_known_as__well_known_oauth_authorization_server_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/oauth-protected-resource":{"get":{"summary":"Well Known Resource","operationId":"well_known_resource__well_known_oauth_protected_resource_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/register":{"post":{"summary":"Register","operationId":"register_oauth_register_post","responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/authorize":{"get":{"summary":"Authorize","operationId":"authorize_oauth_authorize_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/oauth/consent":{"get":{"summary":"Consent Info","description":"What the consent page shows: the app's name, the member's desks, the\npermissions the app asked for (or the default set).","operationId":"consent_info_v1_oauth_consent_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/oauth/approve":{"post":{"summary":"Approve","operationId":"approve_v1_oauth_approve_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/token":{"post":{"summary":"Token","operationId":"token_oauth_token_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/revoke":{"post":{"summary":"Revoke","operationId":"revoke_oauth_revoke_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/oauth/connections":{"get":{"summary":"My Connections","operationId":"my_connections_v1_me_oauth_connections_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/oauth/connections/{key_id}/desk":{"post":{"summary":"Switch Desk","description":"Move a live connection to another of the member's desks — the app keeps\nthe token it holds and simply reaches the new desk from the next call.","operationId":"switch_desk_v1_me_oauth_connections__key_id__desk_post","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/oauth/connections/{key_id}":{"delete":{"summary":"Disconnect","operationId":"disconnect_v1_me_oauth_connections__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/live-page/news":{"get":{"tags":["live-page"],"summary":"Live Page News","description":"Alpaca FREE news feed, proxied server-side (the key never reaches the\nbrowser). 60s in-process cache per (symbols, limit). Without credentials it\ndegrades to {\"configured\": false} — the admin.py uptime/backups pattern.","operationId":"live_page_news_v1_live_page_news_get","parameters":[{"name":"symbols","in":"query","required":false,"schema":{"type":"string","default":"","title":"Symbols"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":25,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/live-page/daily/{symbol}":{"get":{"tags":["live-page"],"summary":"Live Page Daily","description":"~400 calendar days of REAL daily bars + the symbol-context numbers the\nterminal's focus view needs (52w range, prev-session OHLC, 20d avg volume).\n60s in-process cache per symbol — chart timeframes 1M/3M/1Y all slice this\none payload client-side.","operationId":"live_page_daily_v1_live_page_daily__symbol__get","parameters":[{"name":"symbol","in":"path","required":true,"schema":{"type":"string","title":"Symbol"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/live-page/movers":{"get":{"tags":["live-page"],"summary":"Live Page Movers","description":"Today's top-5 gainers and losers across the tradeable universe, computed\nfrom REAL Alpaca snapshots (last vs prev close) ONCE per minute server-side —\nevery browser shares the same cached sweep.","operationId":"live_page_movers_v1_live_page_movers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/live-page/held":{"get":{"tags":["live-page"],"summary":"Live Page Held","description":"SYMBOL → open-position summary across every desk the CALLER owns (badge +\nchip data for the my-positions overlay). Multi-tenant: strictly user-scoped\nvia the session. Unrealized P&L is the STORED last-mark figure (DOLLARS) —\n`as_of` carries the newest mark date so staleness is visible, never hidden.","operationId":"live_page_held_v1_live_page_held_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/live-page/clock":{"get":{"tags":["live-page"],"summary":"Live Page Clock","description":"Is the US market open, and when does it next open / close? Straight from\nthe engine's market calendar — the SAME tables the live desk gates entries\nwith, so the terminal can never disagree with the bot. Never a 500.","operationId":"live_page_clock_v1_live_page_clock_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/live-page/tail":{"get":{"tags":["live-page"],"summary":"Live Page Tail","description":"The last few REAL bars of ONE symbol plus its latest REAL trade, cached 3s.\nEvery browser watching the same symbol collapses to one upstream call, so the\nchart can breathe at a few seconds without re-pulling days of history.","operationId":"live_page_tail_v1_live_page_tail_get","parameters":[{"name":"symbol","in":"query","required":false,"schema":{"type":"string","default":"","title":"Symbol"}},{"name":"tf","in":"query","required":false,"schema":{"type":"string","default":"1m","title":"Tf"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/live-page/names":{"get":{"tags":["live-page"],"summary":"Live Page Names","description":"SYMBOL → company name across the tradeable universe, so the terminal's\nsearch matches \"apple\" to AAPL. Cached 6h; the last good payload survives an\nupstream outage (stale beats blank — names change ~never).","operationId":"live_page_names_v1_live_page_names_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/live-page/fleet":{"get":{"tags":["live-page"],"summary":"Live Page Fleet","description":"Which of the CALLER's deployed bots watch this symbol, and every REAL fill\nthey (or the member's own hand) printed on it. Multi-tenant: strictly\nuser-scoped via the session. Nothing here is simulated — a marker on the\nchart is a row in the member's own book or it does not exist.","operationId":"live_page_fleet_v1_live_page_fleet_get","parameters":[{"name":"symbol","in":"query","required":false,"schema":{"type":"string","default":"","title":"Symbol"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/live-page/pulse":{"get":{"tags":["live-page"],"summary":"Live Page Pulse","description":"Latest real prices for stocks (+prev close) and option contracts\n(real OPRA bid/ask), union-fetched once per second across ALL viewers.","operationId":"live_page_pulse_v1_live_page_pulse_get","parameters":[{"name":"symbols","in":"query","required":false,"schema":{"type":"string","default":"","title":"Symbols"}},{"name":"contracts","in":"query","required":false,"schema":{"type":"string","default":"","title":"Contracts"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/live/events":{"get":{"tags":["live-events"],"summary":"Live Events","description":"The next ~14 days of scheduled market events for the /live Earnings\npanel: FOMC + CPI from the engine's published-schedule tables (always\nserved — they need no feed), plus the cached best-effort earnings calendar.\nNever a 500; a dead earnings feed degrades to the macro rows and an honest\nnote.\n\n`past_days` (0..420, default 0 — the panel's original contract) reaches\nBACK into the same published Fed/BLS tables so the /live chart can flag\nFOMC/CPI dates on the bars it already draws. Macro only: the earnings\ncache holds a forward window, and serving a partial past would look like\n\"no earnings happened\", so earnings stay upcoming-only here.","operationId":"live_events_v1_live_events_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":14,"title":"Days"}},{"name":"past_days","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Past Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/price-alerts":{"get":{"tags":["price-alerts"],"summary":"My Price Alerts","description":"My price alerts, newest first — armed AND fired. A fired row stays here\nuntil deleted; a vanished alert reads like a lost alert.","operationId":"my_price_alerts_v1_me_price_alerts_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["price-alerts"],"summary":"Create Price Alert","description":"Arm one price alert. The symbol is format-checked only — the chart only\noffers universe symbols, and an alert on a symbol no checker ever prices\nsimply never fires (it can be deleted like any other).","operationId":"create_price_alert_v1_me_price_alerts_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PriceAlertBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/price-alerts/{alert_id}":{"delete":{"tags":["price-alerts"],"summary":"Remove Price Alert","description":"Delete ONE of MINE. A foreign or missing id gets the same 404, so a\nstranger learns nothing (user-scoped in the SQL).","operationId":"remove_price_alert_v1_me_price_alerts__alert_id__delete","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"integer","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/push/config":{"get":{"tags":["push"],"summary":"Push Config","description":"The VAPID public key (public by design) + whether push is configured.","operationId":"push_config_v1_push_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/push-subscriptions":{"post":{"tags":["push"],"summary":"Subscribe","description":"Register THIS browser/device for the signed-in member. Session-only.","operationId":"subscribe_v1_me_push_subscriptions_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/push-subscriptions/remove":{"post":{"tags":["push"],"summary":"Unsubscribe","description":"Remove THIS device (by endpoint). Session-only; own rows only.","operationId":"unsubscribe_v1_me_push_subscriptions_remove_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Body"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/sessions":{"get":{"summary":"List My Sessions","description":"Every ACTIVE session for the caller, newest-first, with the row that made\nthis request flagged `current`. Never returns token material of any kind.","operationId":"list_my_sessions_v1_me_sessions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/sessions/{sid}":{"delete":{"summary":"Revoke My Session","description":"Revoke ONE of the caller's OTHER sessions (\"log out this device\").\n\nThe current session is refused with 400 — logging THIS device out is\n/v1/auth/logout's job (it also clears the cookie). A session id that does\nnot exist or belongs to someone else gets a uniform 404 (no enumeration).","operationId":"revoke_my_session_v1_me_sessions__sid__delete","parameters":[{"name":"sid","in":"path","required":true,"schema":{"type":"integer","title":"Sid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/prefs":{"get":{"summary":"Get My Prefs","description":"The caller's notification + AI prefs, defaults merged in.","operationId":"get_my_prefs_v1_me_prefs_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"patch":{"summary":"Patch My Prefs","description":"Partial update — only the fields present in the body change. Returns the\nfull merged prefs so the UI can render exactly what the server now holds.","operationId":"patch_my_prefs_v1_me_prefs_patch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_PrefsPatch"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/unsubscribe":{"post":{"summary":"One Click Unsubscribe","description":"RFC 8058 one-click. Idempotent: unsubscribing twice is still 'off'.\n\nA browser form (the button on the campaign page) asks for text/html and\ngets the plain confirmation; a mail client's one-click POST gets the JSON\nit always got.","operationId":"one_click_unsubscribe_v1_unsubscribe_post","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Unsubscribe Landing","description":"Land the clicker on the switch. NEVER changes anything (see above).\n\nA member-campaign token gets the API's own ask-then-confirm page (one\nbutton that POSTs); every digest token keeps the old redirect to the\ntoggle.","operationId":"unsubscribe_landing_v1_unsubscribe_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"type":"string","default":"","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/ai-usage":{"get":{"summary":"Get My Ai Usage","description":"How many ai_call events THIS user generated today (UTC day, matching the\nadmin metrics' UTC bucketing). Honest zero when the AI was never used.","operationId":"get_my_ai_usage_v1_me_ai_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/profile-stats":{"get":{"summary":"Get My Profile Stats","description":"Per-user aggregates for the /profile stats wall. Every number comes from\nthe authoritative tables (vega_backtests.verdict, desk equity math), never\nfrom client-fired events. total_sim_pnl is Σ(realized+unrealized) across\nthe caller's desks in DOLLARS — null only when they have no desks.","operationId":"get_my_profile_stats_v1_me_profile_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/timeline":{"get":{"summary":"Get My Timeline","description":"The caller's merged activity feed, newest-first, paged. `total` counts\nthe whole merged feed so the UI can page honestly.","operationId":"get_my_timeline_v1_me_timeline_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/agent-freeze":{"get":{"summary":"Get Agent Freeze","description":"Read the caller's agent freeze: {frozen, frozen_at, reason}. Readable by a\nsession cookie OR an account key pair / principal key (so an agent can see it\nis frozen and stop) — but NOT settable by a key (see the mutates below).","operationId":"get_agent_freeze_v1_me_agent_freeze_get","parameters":[{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Set Agent Freeze","description":"Freeze ALL agent activity for the caller (session cookie ONLY). Blocks\nevery further mutating coder/agent action and stops running deployments from\nopening NEW positions (marks/exits continue). Idempotent — freezing an already\nfrozen account keeps the original time + reason. Returns the freeze status.","operationId":"set_agent_freeze_v1_me_agent_freeze_post","requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/_FreezeBody"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Clear Agent Freeze","description":"Clear the agent freeze (session cookie ONLY) — agent actions resume and the\nnext tick reopens entries. Reversible; nothing was retired or deleted.\nIdempotent. Returns the (now cleared) freeze status.","operationId":"clear_agent_freeze_v1_me_agent_freeze_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/delete-account":{"post":{"summary":"Delete My Account","description":"Hard-delete the caller's OWN account and everything they own, reusing\nthe SAME single-transaction cascade the admin surface uses\n(db.admin_delete_user — FK-safe order, per-table counts, sessions included\nso every device is logged out by construction).\n\nConfirmation is deliberate friction: the TYPED email must match (400) and\nthe password must verify with login's bcrypt + timing discipline (403).\nAdmins and audit-authoring users are refused (400) — the append-only audit\nlog must never lose its author row (see db.user_authored_admin_audit).","operationId":"delete_my_account_v1_me_delete_account_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/_DeleteAccountBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/health":{"get":{"summary":"Health","description":"Health check — reports whether the data layer has symbols.\n\n`ok` is about THIS PROCESS and never touches the database. `data_fresh`\nis a fact about the catalog that changes at most daily, and the uptime\nmonitor asks every 15 minutes forever: reading it live meant the\ndatabase could never be idle for five minutes, which is what Neon needs\nbefore it suspends a compute (2026-09-10, cost). It is now served from\nthe data service's own short-lived catalog cache, so a probe costs\nnothing and the answer is at most that cache stale. The REAL freshness\ncheck — the one that pages — is the nightly data audit, unchanged.","operationId":"health_v1_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/hooks/{source}":{"post":{"summary":"Vendor Hook","operationId":"vendor_hook_v1_hooks__source__post","parameters":[{"name":"source","in":"path","required":true,"schema":{"type":"string","title":"Source"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/llms.txt":{"get":{"summary":"Llms Txt","description":"Public agent-onboarding page: what Vega is, the coder endpoints,\nauth headers, and a worked example that actually validates.","operationId":"llms_txt_llms_txt_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/universe":{"get":{"summary":"Universe","description":"Return the tradeable symbol catalog the engine backtests against.\n\nUNAUTHENTICATED by design — this is the same public list the engine\nwalks (DataService.list_universe — the LIVING catalog: the original\ncurated core plus every dynamically onboarded symbol), not user data.\nThe cockpit fetches it once to render the universe\npicker honestly: the \"whole market · N stocks\" count is derived from\nthis exact list, never a hardcoded guess.  Best-effort: a data-layer\nhiccup yields an empty list (the UI then falls back to its static\ncatalog) rather than a 500.","operationId":"universe_v1_universe_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/indicators":{"get":{"summary":"Indicators","description":"Return the engine's machine-readable VOCABULARY: every indicator type\n(with its multi-output selectors + primary params/defaults), the built-in\nfeature ids, the benchmark-return operands, and the minute-only features.\n\nUNAUTHENTICATED by design — this is public, static engine metadata (the\nsame vocabulary llms.txt documents), not user data. The no-code builder\nfetches it once so its operand/indicator pickers speak the CURRENT engine\nspec instead of a hand-curated list that silently drifts. Served from a\ndict frozen at import (build_registry) — no per-request work, cacheable.","operationId":"indicators_v1_indicators_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/public/pulse":{"get":{"summary":"Public Pulse","description":"PUBLIC, UNAUTHENTICATED proof-of-life for the front door.\n\nHonest aggregate counts for the CURRENT session — or, when the market is\nclosed, the LAST COMPLETED session (the exchange calendar decides, via\nthe same MarketClock the live walk uses). COUNTS ONLY: no ids, no\nsymbols, no names, no P&L — the k-anonymity discipline agent_safety\napplies to receipts. The home page HIDES its strip when every count is\nzero; this route never invents a number to fill it.","operationId":"public_pulse_v1_public_pulse_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/events":{"post":{"summary":"Record Event","description":"Record ONE first-party analytics event.  Write-only, fire-and-forget.\n\nSecurity surface:\n  - per-IP rate limit (IMPORTANT-2): over _EVENTS_RATE_LIMIT/min from one\n    IP → 429, checked FIRST and before any DB work, so a flood is cheap\n    to reject (this is the first unauthenticated write route — nothing\n    here should be able to exhaust the connect-per-request Postgres\n    slots).\n  - anon_id is capped at _EVENT_ANON_ID_MAX_LEN chars (MINOR-1) — it's a\n    client uuid, anything longer is garbage → 400.\n  - name must be in the server-side whitelist (_EVENT_NAMES) → else 400.\n  - props is size-capped (~1KB serialized) → oversized → 400.\n  - user_id is taken from the session cookie when present; NEVER trusted\n    from the body.  anon_id (a client localStorage uuid) attributes\n    logged-out visitors.  No PII beyond these ids is expected.\nThe client fires this best-effort and ignores the result — a rejection\nhere never blocks any UX.","operationId":"record_event_v1_events_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventBody"}}},"required":true},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/idea-abandoned":{"post":{"summary":"Record Idea Abandoned","description":"ONE `vega_events` row `idea_abandoned` when a member leaves the\ntalk-build card after exactly one test (no second test, no deploy).\n\nSent by `navigator.sendBeacon` from pagehide / visibilitychange, so the\nbody arrives as text/plain (a CORS-safelisted type — a beacon cannot\nmake a preflight) and is parsed here as JSON.\n\n  - SESSION ONLY (the cookie): an API key or account pair gets 401 —\n    this is the browser's own page lifecycle, nothing else sends it.\n  - user_id from the session, never the body.\n  - `idea_text` is cut to IDEA_TEXT_MAX chars; `verdict`/`surface` are\n    whitelisted; the whole body is size-capped.\n  - IDEMPOTENT per `page_session`: a second beacon for the same page\n    session (pagehide AND visibilitychange both fire on many browsers)\n    writes nothing.\n  - Server-written only — deliberately NOT in `_EVENT_NAMES`, so the\n    raw-text event cannot be forged anonymously through /v1/events.\nThe raw words stay FIRST-PARTY: the web sends PostHog only the verdict,\nthe length and a hash prefix.","operationId":"record_idea_abandoned_v1_me_idea_abandoned_post","responses":{"204":{"description":"Successful Response"}}}},"/v1/config":{"get":{"summary":"Public Config","description":"Public launch-posture surface — what the signup page + member shell\nneed to render honestly BEFORE anyone types anything: is signup open, does\nit need an invite code, plus (admin-30 wave 3) the active feature FLAGS for\nthis caller and any broadcast BANNER. NO auth by design (an anonymous\nvisitor is exactly who needs this); switch reads ride db.get_setting's ~15s\nTTL cache. Flags are evaluated with the SESSION user_id when a cookie is\npresent (so per-user overrides + percent buckets are stable per member),\nelse user_id=None. Best-effort: flags/banner degrade to {}/absent, never a\n500.","operationId":"public_config_v1_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/waitlist":{"post":{"summary":"Join Waitlist","description":"Join the waitlist. Public, unauthenticated, and ALWAYS {ok:true} for\na well-formed email — a duplicate is a silent no-op (INSERT … ON\nCONFLICT DO NOTHING), so nobody can probe who is already on the list.\n\nOrder (deliberate): per-IP rate limit FIRST (its own limiter, before any\nDB work — this is an anonymous write route, same posture as /v1/events),\nthen email shape validation (honest 400 — that's form feedback, not\nenumeration), then the best-effort insert.","operationId":"join_waitlist_v1_waitlist_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaitlistBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/feedback":{"post":{"summary":"Submit Feedback","description":"Tell us anything. Open to guests and members alike.\n\nOrder (deliberate, mirroring /v1/waitlist): per-IP rate limit FIRST (this\nis an anonymous write route), then shape, then the SAVE, then — best\neffort, after the row exists — the support email.\n\nThe save is the source of truth. A mail failure logs a warning and the\ncaller still gets `ok: true`, because the message is safely stored and\ntelling someone their feedback failed when it did not would be a lie.\n\nOver-long input is TRIMMED, never rejected: losing a long note is the\nrudest possible answer to \"tell us anything\".","operationId":"submit_feedback_v1_feedback_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/feedback/survey":{"get":{"summary":"Read Feedback Survey","description":"What this member has answered so far — the thanks page's state.\nPer-IP limit FIRST (an anonymous read keyed on a guessable-looking\nstring), then the token lookup; an unknown token is a plain 404.","operationId":"read_feedback_survey_v1_feedback_survey_get","parameters":[{"name":"t","in":"query","required":false,"schema":{"type":"string","default":"","title":"T"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Answer Feedback Survey","description":"Save one or more answers. Each button in the email is one call with\none field; the thanks page sends the comment the same way. Only the\nfields present are written, so a tap never blanks an earlier answer,\nand tapping again CHANGES the answer (the last tap wins).\n\nOrder (deliberate, mirroring /v1/feedback): per-IP rate limit FIRST,\nthen the token (404 in plain words), then each field validated against\nthe ONE table (400 in plain words), then the save.","operationId":"answer_feedback_survey_v1_feedback_survey_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SurveyAnswerBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/strategies":{"post":{"summary":"Create Strategy","description":"Save a validated strategy spec and return its id.\n\nAuth: session cookie wins, else X-Vega-Key (require_user_or_session).\nA logged-in member saving from the member /backtest cockpit carries only\nthe session cookie (no per-user API key) — the session path lets them\nsave; SDK/API-key callers are unaffected.\n\nAsk-first defaults (Ro, 2026-07-21): a validation failure ALSO carries\n`suggestions` (per defaultable missing field: the value we'd fill in\nand a plain-words why) and — when it would validate as-is — a ready\n`suggested_spec` to resubmit in one round-trip. NOTHING is auto-applied;\n`detail` stays the (complete + kind) error string for existing clients.","operationId":"create_strategy_v1_strategies_post","parameters":[{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StrategyRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StrategyCreated"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"List Strategies","description":"Return all strategies for the authenticated user (session or key).\n\nP1-F2: refused (403, friendly) for the SHARED guest key with no session —\nevery anonymous visitor resolves to the same demo tenant, so this list\nwould show one guest another guest's strategies. Saving/building a spec\nas a guest still works; reading one back by its id still works.","operationId":"list_strategies_v1_strategies_get","parameters":[{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/strategies/bulk":{"post":{"summary":"Bulk Strategies","description":"Fleet import in ONE call (2026-07-21, built from a real 8-hour incident:\n24 strategies took 24 serial save→backtest→deploy round-trips).\n\nBody: {\"items\": [{\"name\", \"strategy_spec\"}, ...], \"deploy_to\": <desk id>?}\n— at most 50 items. Per item: validate (validate_spec + the dynamic-\nuniverse check) → save → if deploy_to, create the FORWARD-ONLY deployment\n(same path as the single deploy route). Items that fail validation don't\nblock the others; every failure is named in that item's errors[].\n\nNO backtest runs in this request. Grading backtests for the new\ndeployments are QUEUED on the bounded worker lane and attach in the\nbackground — the whole call returns in seconds for a 50-item fleet.\n\nAuth: session, principal X-Vega-Key, or account key pair (the same\nrequire_user_or_session as POST /v1/strategies). deploy_to adds desk\nauthorization exactly like the single deploy route: a key PAIR must\nmatch that desk (403 otherwise); a session/principal caller must OWN it.","operationId":"bulk_strategies_v1_strategies_bulk_post","parameters":[{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkStrategiesRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkStrategiesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/strategies/{strategy_id}":{"get":{"summary":"Get Strategy","description":"Return a single strategy by id (user-scoped; 404 if not owned).\n\nRefused for the SHARED guest key with no session (security audit F23):\nevery anonymous visitor is one tenant, so serial ids let one visitor read\nanother's saved spec. A guest never needs to read one back — it already\nholds the spec it just sent.","operationId":"get_strategy_v1_strategies__strategy_id__get","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"integer","title":"Strategy Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"summary":"Patch Strategy","description":"Rename and/or replace the spec of ONE of the caller's saved strategies.\n\nBody: ``{\"name\"?: str, \"spec\"?: object}`` — at least one (400, friendly, if\nneither). Auth: session cookie wins, else X-Vega-Key, else an account key\npair (the same resolution as GET /v1/strategies/{id}); the shared guest key\nwith no session is refused, because on that one tenant an edit by id would\nrewrite another visitor's work. 404 \"Strategy not found\" for a stranger —\nbyte-identical to a missing id. Bumps `updated_at`, returns the updated row.\n\nA replaced spec is re-validated through the SAME path POST /v1/strategies\nuses (_normalize_and_validate_spec), so a refusal carries the engine's own\nteaching message plus `suggestions` / `suggested_spec`. A refused edit stores\nNOTHING — the previous spec stands.\n\nWHAT AN EDIT NEVER TOUCHES: a DEPLOYED bot keeps trading its own spec\nsnapshot (vega_paper_deployments.spec has no FK to this table), and a\nfinished backtest keeps its own `spec_snapshot`. Editing the library entry\nis editing the DRAFT, never the running bot and never the graded receipt —\nredeploy or re-run to put the new version to work.","operationId":"patch_strategy_v1_strategies__strategy_id__patch","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"integer","title":"Strategy Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StrategyPatchRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Strategy","description":"Delete ONE of the caller's saved strategies. Its BACKTESTS all survive.\n\nAuth + cloak: exactly as PATCH above (404 \"Strategy not found\" for a\nstranger, identical to a missing id; a second delete is that same plain 404,\nnever a 500). `?confirm=true` is REQUIRED once the strategy has at least one\ngraded run — the 400 says how many runs exist and that they are kept.\n\nA backtest is a self-contained RECEIPT (it carries its own `spec_snapshot`),\nso deleting the strategy NULLs `vega_backtests.strategy_id` and keeps every\nrow. Returns ``{\"deleted\": id, \"backtests_kept\": N}``.\n\nNothing here stops a DEPLOYED bot: the deployment holds its own spec copy,\nso a bot built from this strategy keeps running exactly as it was. Retire it\nfrom the desk if you want it stopped.","operationId":"delete_strategy_v1_strategies__strategy_id__delete","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"integer","title":"Strategy Id"}},{"name":"confirm","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Confirm"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/strategies/{strategy_id}/signal-history":{"post":{"summary":"Upload Signal History","description":"Catalog #10 — upload the timestamped SIGNAL HISTORY that makes a\nwebhook-driven strategy gradable.\n\nBody: {\"signals\": [{\"symbol\": \"NVDA\", \"ts\": \"2026-03-03T14:30:00Z\"},\n...]} (<= 5000 rows). Stored ON the strategy (its spec carries\n`signal_history`), so any backtest of the strategy — POST /v1/backtests\nwith strategy_id, or the per-alpha run — grades entries off it with the\nnormal no-look-ahead discipline (signal ts → next-bar fill). Replaces\nany previous upload. Only meaningful on a webhook spec (entry.webhook:\ntrue) — refused in plain words otherwise. Owner-scoped (404 on someone\nelse's id, indistinguishable from a missing one); audited.","operationId":"upload_signal_history_v1_strategies__strategy_id__signal_history_post","parameters":[{"name":"strategy_id","in":"path","required":true,"schema":{"type":"integer","title":"Strategy Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/strategies/build":{"post":{"summary":"Build Strategy","description":"Normalize loose params into a full canonical spec and validate it.\nReturns {spec, valid, errors[]} — never 400s on a bad spec; the errors\nlist IS the product (an agent iterates on it). Auth: identity-only\n(session OR user API key OR account key pair via the shared resolver);\nthe resolved uid is unused — no user-scoped data is touched.","operationId":"build_strategy_v1_strategies_build_post","parameters":[{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/strategies/port-check":{"post":{"summary":"Port Check","description":"\"Does my strategy fit this platform?\" — the machine-checkable half of the\nPort Report. Classify a draft spec and/or a list of concept TAGS as\nfits | partial | cant, instead of only accept/reject.\n\nRequest: {\"spec\": <draft spec dict>} and/or {\"concepts\": [\"ratchet\", ...]}.\nResponse: {verdict, issues: [{concept, detail, dropped, closest, kind,\nnote}], notes: [truth-box one-liners]}.\n  - fits    — validates clean / every concept already supported.\n  - partial — only mappable losses (take-profit, trailing/ratchet, chosen\n              stop, sizing) — each issue names what's dropped + the closest\n              supported thing (verbatim W1 guidance).\n  - cant    — a hard wall with no mapping (intraday/minute bars, news/social\n              feeds, empty universe, unknown indicator type, …).\n\nAuth: identity-only, mirroring POST /v1/strategies/build (session OR\nX-Vega-Key OR account key pair via require_user_or_session); the uid is\nunused — no user-scoped data is touched. Never 400s on a bad spec (the\nclassification IS the product); a 400 only fires when neither a spec nor\na concepts list is supplied.","operationId":"port_check_v1_strategies_port_check_post","parameters":[{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/backtests":{"post":{"summary":"Submit Backtest","description":"Enqueue a backtest.\n\nAccepts either:\n  - ``strategy_spec``: an inline strategy spec dict\n  - ``strategy_id``: id of a previously saved strategy (user-scoped)\n\nThe spec is validated synchronously; a background task runs the engine\nand writes results to the DB.\n\nAuth: the EFFECTIVE user is the session-cookie user when a valid\nsession exists, else the X-Vega-Key header user (require_backtest_submitter\n— see auth.py). This matters because the home cockpit always sends the\nshared demo key alongside a real user's session cookie; without\npreferring the session, a logged-in account holder would be treated as\nthe demo user and hit the free-tier cap below.","operationId":"submit_backtest_v1_backtests_post","parameters":[{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BacktestRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BacktestQueued"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"List Backtests","description":"Return a paginated list of the authenticated user's backtests.\n\nP1-F2: refused (403, friendly) for the SHARED guest key with no session —\nall anonymous visitors resolve to the one demo tenant, so this list would\nhand a visitor every other visitor's runs. Submitting a run and polling\nit by run_id still work.\n\nAuth: same effective-user resolution as POST /v1/backtests\n(require_backtest_submitter — session cookie wins when present, else\nX-Vega-Key). Keeps the list scoped to whichever principal actually\nowns the rows the caller expects to see.\n\nFix wave (Phase D, Fix 1): every row carries `kind` ('single' |\n'portfolio' | 'walkforward' | 'ensemble') so a list UI can route each\nrow to the right renderer without a per-row fetch (a portfolio /\nwalkforward / ensemble row has no `real` key). Optional `?kind=`\nfilters to one row type; public values are \"backtest\"\n(single-strategy, POST /v1/backtests), \"portfolio\" (POST\n/v1/portfolio/backtests), \"walkforward\" (#38, POST\n/v1/backtests/walkforward) and \"ensemble\" (#39, POST\n/v1/backtests/ensemble) — \"backtest\" maps internally to the db's\n'single'. Any other value is a 422, not silently ignored.\nDefault (omitted) = all kinds, unchanged from before this fix.","operationId":"list_backtests_v1_backtests_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}},{"name":"kind","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kind"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/portfolio/backtests":{"post":{"summary":"Submit Portfolio Backtest","description":"Phase D (D1): enqueue a PORTFOLIO backtest over 2..8 of the caller's OWN\nsaved strategies, combining them into one honest book view (combined\nequity/drawdown, correlation matrix, overlap, contributions, concentration).\n\nBody: {strategy_ids: [int, ...]} — bounds 2..8 and distinctness are enforced\nby the request model (422 otherwise). Ownership is enforced HERE per id:\n  * no such strategy id            → 404\n  * strategy owned by another user → 403 (never leaks its contents)\nAuth: session cookie wins, else X-Vega-Key (require_backtest_submitter),\nmatching the single-backtest surface. Reuses the job/poll machinery: a row\nis created (kind='portfolio'), a background task runs + combines, and the\nrun is polled via the SAME GET /v1/backtests/{run_id}.","operationId":"submit_portfolio_backtest_v1_portfolio_backtests_post","parameters":[{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioBacktestRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioBacktestQueued"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/backtests/walkforward":{"post":{"summary":"Submit Walkforward Backtest","description":"#38 WALK-FORWARD RE-OPTIMIZATION: enqueue a walk-forward run of ONE\nstrategy. Body = the spec (inline `strategy_spec` or a saved\n`strategy_id`) + `walkforward: {\"folds\": K (2..6), \"optimize\":\n[{\"path\": \"entry.when.<...>\", \"grid\": [...]}]}` (≤3 tuned paths, ≤125\ncombinations — bigger grids are refused in plain words).\n\nThe engine splits the backtest window into K sequential folds; each\nfold f is graded OUT-OF-SAMPLE with the grid combination that had the\nbest EV on folds 1..f-1 ONLY (fold 1 only ever trains). The result is\nthe stitched OOS equity curve, the per-fold table {fold, window,\nchosen_params, is_ev, oos_ev} and the honest headline \"walk-forward\nOOS EV\" — every sub-window graded by run_backtest itself, never a\nsecond scoring path. Advisory like every engine read; the single-run\nverdict machinery is untouched.\n\nAuth: same resolution as POST /v1/backtests (session cookie wins, else\nX-Vega-Key). Polled via the SAME GET /v1/backtests/{run_id}; the row's\nkind is 'walkforward'.","operationId":"submit_walkforward_backtest_v1_backtests_walkforward_post","parameters":[{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalkforwardBacktestRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalkforwardBacktestQueued"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/backtests/ensemble":{"post":{"summary":"Submit Ensemble Backtest","description":"#39 MULTI-STRATEGY ENSEMBLE: grade 2..8 strategies and blend them.\nBody = `members: [{\"strategy_id\": 5} | {\"strategy_spec\": {...}}, ...]`\nplus `weights: \"equal\" | [0.5, 0.3, 0.2]` (one positive number per\nmember, normalised for you).\n\nEvery member is graded by the SAME run_backtest a single run uses —\nthere is no second grading path — and each keeps its OWN honest\nverdict. What the ensemble adds is the blended equity curve on a common\ndate axis, the CORRELATION matrix between the members (measured on\nper-step CHANGES over the shared window, never on the cumulative\ncurves) and the diversification benefit: blended volatility against the\nweighted mean of the members' own. \"ENSEMBLE\" is the row label, like\n\"PORTFOLIO\" — never a judgement.\n\nAuth: same resolution as POST /v1/backtests. Polled via the SAME\nGET /v1/backtests/{run_id}; the row's kind is 'ensemble'.","operationId":"submit_ensemble_backtest_v1_backtests_ensemble_post","parameters":[{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnsembleBacktestRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnsembleBacktestQueued"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/backtests/compare":{"get":{"summary":"Compare Backtests","description":"Diff two of the CALLER'S OWN backtests (owner-scoped: both must belong\nto the resolved user, else 404). Headline = the optimism-gap delta —\nthe overfitting signal, not the raw EV delta. Auth: the shared\nresolver — session cookie wins, else X-Vega-Key, else a Build-10\naccount key pair resolved to the desk owner's user_id.\n\nP1-F2: refused (403, friendly) for the SHARED guest key with no session —\ntwo run_ids from the one demo tenant may belong to two different visitors,\nso \"your own runs\" isn't a truthful claim there.","operationId":"compare_backtests_v1_backtests_compare_get","parameters":[{"name":"a","in":"query","required":true,"schema":{"type":"string","title":"A"}},{"name":"b","in":"query","required":true,"schema":{"type":"string","title":"B"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/backtests/{run_id}":{"get":{"summary":"Get Backtest","description":"Return a single backtest by run_id (user-scoped; 404 if not owned).\n\nAuth: same effective-user resolution as POST /v1/backtests\n(require_backtest_submitter — session cookie wins when present, else\nX-Vega-Key). Without this, a session-submitted run (owned by the real\nuser_id) would 404 when polled with only the shared demo key header —\nthe poll must be able to see what the matching submit created.\nOwnership scoping is unaffected: db.get_backtest still filters on the\nresolved uid, so a different user's session (or a different key)\nstill gets 404 on someone else's run.","operationId":"get_backtest_v1_backtests__run_id__get","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Backtest","description":"Delete ONE of the caller's own runs, and revoke any public share of it.\n\nOwner-only, same session-wins-else-X-Vega-Key-else-pair resolution as the\npoll/export/share routes; the shared guest key with no session is refused\n(guest A must not delete guest B's run). A run you don't own answers the\nSAME 404 \"Backtest not found\" as a run that never existed, and a second\ndelete is that same plain 404 — never a 500.\n\nThe share is revoked FIRST and in the same request (reusing\ndb.revoke_shares_for_run, the helper behind DELETE\n/v1/backtests/{id}/share): a public link that outlived its run would 404 for\nthe reader while still looking like a live link to whoever holds it.\nReturns ``{\"deleted\": run_id, \"share_revoked\": bool}``.\n\nA DEPLOYED bot is unaffected — a grade attached to a deployment lives in\n`deployment.backtest_result`, its own copy, so deleting the run never blanks\na bot's report card. No confirm flag: a run is a receipt of a computation,\nnot money history, and the member can always run it again.","operationId":"delete_backtest_v1_backtests__run_id__delete","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/backtests/{run_id}/export.csv":{"get":{"summary":"Export Backtest Csv","description":"E8 — download this run's per-trade rows as an RFC4180 CSV (header row +\none row per trade).\n\nAuth: identical session-wins-else-X-Vega-Key resolution and owner-only\nscoping as the poll route (require_backtest_submitter + db.get_backtest on\nthe resolved uid) — a run you don't own 404s. 409 while the run has no\nresult yet (queued/running). A NO_DATA run exports a header-only CSV.","operationId":"export_backtest_csv_v1_backtests__run_id__export_csv_get","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/backtests/{run_id}/export.json":{"get":{"summary":"Export Backtest Json","description":"E8 — download this run's FULL engine result (the §5 contract: verdict,\nreal, robustness, walk_forward, trades, equity_curve, metrics,\nmetrics_notes) as a JSON attachment.\n\nSame owner-only session/key auth as the poll + CSV routes. 409 while the\nrun has no result yet.","operationId":"export_backtest_json_v1_backtests__run_id__export_json_get","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/export":{"get":{"summary":"Export My Data","description":"launch #81 (2026-08-13) — ONE download holding everything this member's\naccount contains: profile, strategies, every backtest (full result),\ndesks with their bots / positions / working orders, and alerts, as a zip\nof plain JSON files. The data-portability twin of delete-everywhere (P4).\n\nSESSION-ONLY, deliberately (mirrors the alpha hard-delete): a per-desk\nagent key is scoped to ONE desk and must never be able to exfiltrate the\nwhole account. Every row is fetched through the same user_id-scoped\naccessors the app itself reads with, so cross-tenant isolation is\ninherited, not re-implemented.","operationId":"export_my_data_v1_me_export_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/backtests/{run_id}/share":{"get":{"summary":"Create Backtest Share","description":"Mint (or re-use) a public share link for THIS owner's run.\n\nP1-F2: refused (403, friendly) for the SHARED guest key with no session —\nminting off the shared demo tenant lets one visitor publish another\nvisitor's result. Signing in makes it yours to share.\n\nOwner-only, identical session-wins-else-X-Vega-Key resolution + scoping as\nthe poll/export routes (require_backtest_submitter + db.get_backtest on the\nresolved uid) — a run you don't own 404s, revealing nothing. 409 while the\nrun has no result yet. Idempotent: re-calling returns the SAME token, not a\nnew one. Returns {share_id, share_path} — build the full URL client-side.","operationId":"create_backtest_share_v1_backtests__run_id__share_get","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Revoke Backtest Share","description":"Revoke every active public share of THIS owner's run. Owner-scoped\n(a run you don't own 404s). Idempotent — revoking an already-revoked or\nnever-shared run returns {revoked: 0}. After this the public link 404s.\nSHARED-GUEST gated like mint (F2): guest A must not revoke guest B's link.","operationId":"revoke_backtest_share_v1_backtests__run_id__share_delete","parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","title":"Run Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/share/{share_id}":{"get":{"summary":"Get Public Share","description":"PUBLIC (no auth) — resolve a share token to its run's HONESTY SUMMARY.\n\nServes ONLY _public_share_summary's whitelist (verdict / engine's read / EV\n/ win rate / fills / skip rate / equity curve + the honesty stamp + the\nplain-words SHAPE + the share date). NEVER the spec, universe, entry/exit\nrules, trades, user/account ids, or any PII. An unknown OR revoked token →\na uniform 404 (never reveal a token existed). A run whose result was\ncleared → 404.\n\nPer-IP rate limit FIRST, before any database work (item 20): this is now\nthe read behind an INDEXABLE page, so it is the most-reached\nunauthenticated route we have, and a token-walking loop must be cheap to\nrefuse. The 429 says nothing about whether the token exists.","operationId":"get_public_share_v1_share__share_id__get","parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/public/featured-shares":{"get":{"summary":"Public Featured Shares","description":"PUBLIC (no auth) — the CURATED list of shared results we point at.\n\nItem 20, and the honesty of it is the whole design:\n\n* It is a list somebody CHOSE, never a ranking. Sorting members' shared\n  results by return and publishing the top of that list is marketing by\n  survivorship — the winners of a lottery presented as evidence. We\n  refuse to build the mechanism, not just the page.\n* WHERE THE CHOICE IS STORED: the admin-curated `vega_featured_shares`\n  table when it has anything in it, and otherwise the older\n  `VEGA_FEATURED_SHARES` environment variable — which keeps every\n  existing deploy working unchanged. `public_share.resolve_featured_tokens`\n  is the one place that decides, and its docstring is the reasoning.\n* It is not an enumeration. This is the ONLY route that lists share\n  tokens, and it lists exactly the ones an operator named — nothing here\n  sweeps the table, so no stranger can walk it to discover somebody\n  else's link.\n* A token that has been REVOKED (or whose run was deleted) simply does\n  not resolve, so it drops out of this list and out of the next generated\n  sitemap with no cleanup step to forget. Curation cannot outlive consent\n  — which is why the resolution below goes through db.get_share for every\n  token, whichever source named it.\n\nReturns `{\"shares\": [{token, path, strategy_name, verdict, n_fills,\nshared_at}]}` — the card fields only, in the operator's order.","operationId":"public_featured_shares_v1_public_featured_shares_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/share/{share_id}/report":{"post":{"summary":"Report Public Share","description":"PUBLIC (no auth) — tell us this shared page is wrong.\n\nA share page carries a member's own words on our domain, readable by\nanyone holding the link. So the report door has to be open to anyone\nholding the link too: no account, no login, no email.\n\n**A report never removes anything.** It files a flag an admin reads and\ndecides on. Auto-hiding on report would hand every stranger a takedown\nbutton for someone else's honest receipt — the exact opposite of the\nproduct.\n\nOrder mirrors /v1/feedback and /v1/waitlist: per-IP rate limit FIRST\n(this is an anonymous write), then shape, then the save.\n\n**The answer is identical whether the token is real, unknown, revoked, or\nalready flagged.** `GET /v1/share/{id}` deliberately 404s the same way for\nan unknown and a revoked token so nobody can discover which links exist;\nan honest 404 here would undo that and turn the report route into a token\noracle. A report against a token we can't resolve is thanked and dropped.\n\nUnknown reason values become \"other\" and an over-long note is TRIMMED —\nnever a 400. Somebody flagging abuse for us is doing us a favour, and\narguing with their form is how you stop hearing about it.","operationId":"report_public_share_v1_share__share_id__report_post","parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareReportBody"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts":{"get":{"summary":"List My Accounts","description":"Return the paper accounts owned by the currently-authenticated user.\n\nAuth: the shared resolver — session cookie wins, else the X-Vega-Key\nheader user, else a Build-10 account key pair resolved to the desk\nowner (require_user_or_session, the SAME dependency B12-T1 wired onto\nthe coder deploy/backtest/strategy routes). This lets a key-holding\ncoder LIST their own desks (browser + SDK/CLI both work) — without it a\nvalid X-Vega-Key got 401 here and the coder flow could never discover a\ndesk id to deploy onto. Owner-scoped exactly as before: db.list_user_accounts\nfilters on the resolved uid, so a different principal only ever sees\ntheir OWN desks — never anyone else's. Suspension still fails closed\n(a suspended session/key 403s inside the resolver, never reaching here).","operationId":"list_my_accounts_v1_me_accounts_get","parameters":[{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Create My Account","description":"Mint a new paper desk for the caller. name 1..40 chars, starting_cash\n$1,000..$1,000,000 (422 otherwise). Desk count is a PLAN cap\n(plans.CAPS[...]['desks'] — 1 free / 3 paid / 10 pro / 5 in the founding\nera, which is the pre-plans ceiling) → a friendly 409 at the ceiling.\nReturns the new desk in the GET /v1/me/accounts row shape.","operationId":"create_my_account_v1_me_accounts_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeskCreateRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}":{"patch":{"summary":"Rename My Account","description":"Rename one of the caller's own desks (name 1..40 chars → 422). 404 if the\ndesk is not the caller's (cloak).","operationId":"rename_my_account_v1_me_accounts__account_id__patch","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeskRenameRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete My Account","description":"Delete one of the caller's own desks and cascade-remove its data. Guard:\nbody.confirm_name MUST equal the desk's CURRENT name (else 400) — a\ntyped-name confirmation. 404 if the desk is not the caller's (cloak).\n\nCascade (db.delete_account_cascade, scoped to this account_id): orders,\npositions, deployment transitions, deployments, per-account key pairs,\nwatchlists, alerts, then the desk row. Deleting the caller's last desk is\nallowed — there is no lower floor (the cap of 5 is the only hard limit).","operationId":"delete_my_account_v1_me_accounts__account_id__delete","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeskDeleteRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Get My Account","description":"Return the full state for one paper account.\n\n`closed_limit` / `closed_before` PAGE the closed-trade table, newest\nfirst, cursor by position id — one desk in the 2026-09-22 survey shipped\n26.4 MB of the 28.8 MB response as closed trades nobody had asked to see.\nOMITTING THEM IS THE DEFAULT AND RETURNS EVERYTHING, so every existing\nclient is unaffected. OPEN rows are never capped (they are the live book),\n`positions.closed_total` always reports the real count, and\n`positions.closed_next_cursor` is the id to ask the next page with.\n\nAuth (B10-T1): a valid vega_session cookie (own desk) OR an account key\npair (X-Vega-Account-Key-Id + X-Vega-Account-Secret) that resolves to THIS\naccount. 401 unauthenticated / bad key; 403 when the desk is not the\ncaller's (or the key belongs to a different account).","operationId":"get_my_account_v1_me_accounts__account_id__get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"closed_limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Closed Limit"}},{"name":"closed_before","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Closed Before"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/max-per-trade":{"post":{"summary":"Set Desk Max Per Trade","description":"MAX $/TRADE (2026-08-05) — set (or clear with null) the DESK-level\nper-trade dollar cap. EVERY bot on the desk obeys it unless the bot sets its\nown (per-bot wins). Session-owner scoped like the other desk mutations\n(rename/deposit); 404 if the desk is not the caller's (cloak).\n\nA LIVE-FILL knob: the walk sizes an order down to fit it, or skips a trade\nwhose smallest unit exceeds it — it never touches any verdict.","operationId":"set_desk_max_per_trade_v1_me_accounts__account_id__max_per_trade_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaxPerTradeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/max-open-usd":{"post":{"summary":"Set Desk Max Open Usd","description":"MAX OPEN $ (2026-08-06) — set (or clear with null) the DESK-level cap on how\nmuch money any ONE bot may have committed across all its open positions at\nonce. Every bot on the desk obeys it unless the bot sets its own (per-bot\nwins). Session-owner scoped like the other desk mutations; 404 if the desk is\nnot the caller's (cloak).\n\nIt is a PER-BOT ceiling applied desk-wide, NOT a desk total — the desk's own\nlimit is its cash, which the buying-power gate already enforces over the whole\nbook. A LIVE-FILL knob: the walk refuses a NEW entry that would take a bot over\nit, with its own distinct reason, and never touches any verdict. Nothing is\nreserved: the committed total is derived fresh every walk, so closing a\nposition frees the room on the next one.","operationId":"set_desk_max_open_usd_v1_me_accounts__account_id__max_open_usd_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaxOpenUsdRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/max-sector-frac":{"post":{"summary":"Set Desk Max Sector Frac","description":"SECTOR EXPOSURE CAP (#35) — set (or clear with null) the DESK-level cap on\nthe fraction of the desk's equity ONE sector's open positions may hold,\ncounted across the WHOLE DESK whatever bot opened them. Every bot on the\ndesk obeys it unless the bot sets its own (per-bot wins). Session-owner\nscoped like the other desk mutations; 404 if the desk is not the caller's\n(cloak).\n\nA LIVE-FILL knob: the walk refuses a NEW entry whose sector would go over,\nwith its own distinct reason (`skipped_sector_cap` + a note showing the\nmath), and never touches any verdict. Sector comes from the engine's ONE\nstatic map (an unknown symbol is its own \"other\" bucket); the committed\ntotals are derived fresh every walk through the ONE cost model — nothing\nis reserved, so closing a position frees its sector's room on the next\nwalk.","operationId":"set_desk_max_sector_frac_v1_me_accounts__account_id__max_sector_frac_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaxSectorFracRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/max-daily-loss":{"post":{"summary":"Set Desk Max Daily Loss","description":"DESK-WIDE DAILY LOSS LIMIT (Wave A) — set (or clear with null) the\ndollars this DESK may lose in one ET session before every bot on it\nstops opening. Session-owner scoped like the other desk mutations; 404\nif the desk is not the caller's (cloak).\n\nWHAT MAKES IT DIFFERENT from the per-bot `risk.daily_loss_usd` in a\nspec: this one counts the WHOLE DESK's realized P&L for the session, so\none bot's bad morning can stop every other bot on the account. That is\nthe point — every other limit we ship is a bot talking to itself, and\nfifty individually well-behaved bots can still lose the account.\n\nEach bot READS this state on its own walk and derives the desk's P&L\nfresh from the desk's own closed rows: nothing is written, nothing is\nreserved, and a new ET session starts clean with no resume step. It\npauses ENTRIES ONLY — marks, exits and settlement never consult it.","operationId":"set_desk_max_daily_loss_v1_me_accounts__account_id__max_daily_loss_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaxDailyLossRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/halt":{"post":{"summary":"Set Desk Trading Halt","description":"THE DESK KILL SWITCH (Wave A) — stop (or resume) every bot on one\ndesk. Session-owner scoped; 404 if the desk is not the caller's (cloak).\n\n`{\"halted\": true, \"reason\": \"...\"}` stops every bot on the desk from\nOPENING anything, whatever its own spec or verdict says.\n`{\"halted\": false}` resumes and CLEARS the reason (a stale reason left\nlying around reads as a current one).\n\nIT STOPS ENTRIES ONLY, and that is the design, not a softening. Open\npositions keep being marked and keep exiting on their stops. A kill\nswitch that also froze the exits would leave a member unable to get OUT\nof the thing that scared them, which is the opposite of a safety\ncontrol. Every bot reads the flag on its own next walk, so a halt takes\neffect within one tick and needs no per-bot write.","operationId":"set_desk_trading_halt_v1_me_accounts__account_id__halt_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TradingHaltRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/max-gross-exposure":{"post":{"summary":"Set Desk Max Gross Exposure","description":"GROSS EXPOSURE CAP — the most the desk's open positions may be worth,\nas a FRACTION of the desk's equity (1.5 = 150%), counted across every\nbot. Unlike `max_open_usd` it MOVES WITH THE ACCOUNT, which is the point.\nNull clears it. Refusals are `skipped_max_gross_exposure`.","operationId":"set_desk_max_gross_exposure_v1_me_accounts__account_id__max_gross_exposure_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConcentrationLimitRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/max-net-exposure":{"post":{"summary":"Set Desk Max Net Exposure","description":"NET EXPOSURE CAP — the most DIRECTIONAL tilt the desk may carry\n(|longs − shorts| ÷ equity), either way. Now that short equity exists\nthis is genuinely different from gross: a market-neutral desk can be 200%\ngross and 0% net, and refusing it on gross alone would be wrong. An order\nthat REDUCES the tilt is never refused. Null clears it.","operationId":"set_desk_max_net_exposure_v1_me_accounts__account_id__max_net_exposure_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConcentrationLimitRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/max-positions-per-symbol":{"post":{"summary":"Set Desk Max Positions Per Symbol","description":"POSITIONS PER SYMBOL — how many open positions the WHOLE DESK may hold\non one name, counted by UNDERLYING (three NVDA contracts are three bets\non NVDA). Five bots can all pile into the same name today and nothing\nnotices. Null clears it. Refusals are `skipped_symbol_concentration`.","operationId":"set_desk_max_positions_per_symbol_v1_me_accounts__account_id__max_positions_per_symbol_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConcentrationLimitRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/max-correlation":{"post":{"summary":"Set Desk Max Correlation","description":"CORRELATION CAP — refuse a new name whose DAILY-RETURN correlation\nwith something the desk already holds is at or over this. The other half\nof the sector cap: two names in two different sectors can still be one\nbet. Measured on returns (not price levels) over\n`correlation_window_sessions` sessions ending at the signal's own bar; a\npair without enough overlapping history is UNKNOWN and the limit stands\ndown for it. Null clears it.","operationId":"set_desk_max_correlation_v1_me_accounts__account_id__max_correlation_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConcentrationLimitRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/correlation-window":{"post":{"summary":"Set Desk Correlation Window","description":"The trailing window (in trading SESSIONS of daily bars) the\ncorrelation cap is measured over. Default 60 — about three months, long\nenough that one shock does not decide it and short enough to describe how\nthe names trade now. Null goes back to the default.","operationId":"set_desk_correlation_window_v1_me_accounts__account_id__correlation_window_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConcentrationLimitRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/desk-position-stop":{"post":{"summary":"Set Desk Position Stop","description":"THE DESK'S EMERGENCY STOP PER POSITION — \"cut anything down X%,\nwhatever its strategy says\". It applies to every open position on the\ndesk, whichever bot opened it, and it has NO per-bot override on purpose:\nit is the floor UNDER the strategies, so the bot whose own stop is wrong\nis exactly the one that must not opt out.\n\nIt never pre-empts a TIGHTER strategy stop (that one fires first, at its\nown earlier bar), it is an EXIT so it keeps running on walks where\nentries are refused, and a SHELVED bot is suppressed like every other\ndiscretionary exit — shelf means the bot does not trade. Null clears\nit.","operationId":"set_desk_position_stop_v1_me_accounts__account_id__desk_position_stop_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConcentrationLimitRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/deposit":{"post":{"summary":"Deposit My Account","description":"Add paper cash to one of the caller's own desks (\"buy more money\").\namount $1,000..$1,000,000 (422 otherwise); 404 if not the caller's desk.\n\nHONEST DEPOSIT: this adds capital, it does NOT rewrite P&L. See\ndb.deposit_to_account — starting_cash means total-capital-contributed, every\nposition's realized/unrealized P&L is untouched, and the desk_deposit event\nbelow is the auditable money trail.","operationId":"deposit_my_account_v1_me_accounts__account_id__deposit_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeskDepositRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/paper":{"get":{"summary":"My Paper","description":"The paper page's headline: the caller's dual-use desks (0..5) with their\nSIMULATED cash/equity/P&L, open-position + working-order counts, and the\nactive key-pair metadata for each.\n\nAuth: valid vega_session cookie. Seeds the caller's default 3 desks first IFF\nthey own none (never resurrects a deleted desk). READ-ONLY P&L: reuses the stored-mark\nequity path (db.account_equity / paper_manual.buying_power), NOT a live\nre-mark, so listing all 5 desks never storms the quote feed. `key_meta` is\nmetadata only (key_id/created/revoked) — never secret material.","operationId":"my_paper_v1_me_paper_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/accounts/summary":{"get":{"summary":"Get My Accounts Summary","description":"Return the full AccountState for EVERY desk the caller owns, in ONE call.\n\n`closed_limit` / `closed_before` page the closed-trade table exactly as\nGET /v1/me/accounts/{id} does, and the cap is PER DESK — a global one\nwould let the busiest desk eat every other desk's rows and render them\nempty. Omitted, every closed trade comes back, which is what every\nexisting client already reads.\n\nThis is the cockpit's batch read: the page needs each desk's headline,\ndeployments, and open/closed positions to draw its stats, equity curve,\nverdict mix and per-desk cards. Previously it listed desks then fetched\nGET /v1/me/accounts/{id} once PER desk (an N+1 that stormed the connection\npool — Sentry JAVASCRIPT-NEXTJS-4). Here every desk is assembled from a\nhandful of batched IN/GROUP BY queries (equity, deployments, open + closed\npositions), so the round-trips are constant, not O(desks).\n\nEach element is byte-identical to what GET /v1/me/accounts/{id} returns for\nthat desk. Owner-scoped exactly like /v1/me/accounts: db.list_user_accounts\nfilters on the resolved uid, so a principal only ever sees their OWN desks.\n\nAuth: the shared resolver (session cookie / X-Vega-Key / account key pair) —\n401 when unauthenticated, and a suspended caller fails closed in the resolver.\nNote: this route MUST stay registered before /v1/me/accounts/{account_id} so\nthe literal path 'summary' is not parsed as an int account id (422).","operationId":"get_my_accounts_summary_v1_me_accounts_summary_get","parameters":[{"name":"closed_limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Closed Limit"}},{"name":"closed_before","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Closed Before"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountState"},"title":"Response Get My Accounts Summary V1 Me Accounts Summary Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/accounts":{"get":{"summary":"List Accounts","description":"List the PUBLIC DEMO paper desks with their headline SIMULATED P&L +\nposition/deployment counts.\n\nAuth: UNAUTHENTICATED by design — this powers the logged-out account\npicker.  It returns NO key material (only the sha256 hash is ever\nstored) and NO position-level detail; the per-account view\n(GET /v1/accounts/{id}) is key-gated.\n\nP1-F1 SCOPE: only OWNERLESS demo desks (db.list_demo_accounts — user_id\nIS NULL).  It used to list EVERY desk on the platform, which handed an\nanonymous caller every member's desk name, equity, P&L and bot counts.\nA member's own desks come from the session-scoped /v1/me/accounts\nroutes, never from here.","operationId":"list_accounts_v1_accounts_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/AccountSummary"},"type":"array","title":"Response List Accounts V1 Accounts Get"}}}}}}},"/v1/accounts/{account_id}":{"get":{"summary":"Get Account","description":"Return the full state for one paper account: headline, deployments, and\nopen/closed position tables + the equity breakdown.\n\nAuth: the X-Vega-Key header must resolve to THIS account_id (a key for a\ndifferent paper account gets 403 — you can only read your own book).","operationId":"get_account_v1_accounts__account_id__get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/reset":{"post":{"summary":"Reset My Account","description":"Wipe one of the caller's OWN desks back to a clean slate and optionally\nset its starting cash ($10,000..$1,000,000; omitted → keep current).\n\nSESSION-auth own-account only — resets are a deliberate owner/UI action, so\na key pair (a bot) can NOT reset a desk (403 cross-user, 401 unauth). Deletes\nevery position + the full order ledger under the account row lock so a\nconcurrent matcher fill can't survive or corrupt the wipe (see\ndb.reset_account). starting_cash out of range → 422 (AccountResetRequest).","operationId":"reset_my_account_v1_me_accounts__account_id__reset_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountResetRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/chart":{"get":{"summary":"My Alpha Chart Route","description":"Return the price chart for one deployment owned by the logged-in user.\n\nAuth: requires a valid vega_session cookie. Returns 403 if the account\ndoes not belong to the authenticated user.","operationId":"my_alpha_chart_route_v1_me_accounts__account_id__alphas__deploy_id__chart_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}},{"name":"symbol","in":"query","required":true,"schema":{"type":"string","title":"Symbol"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/orders":{"post":{"summary":"Place Order","description":"Place a manual MARKET or LIMIT order on the desk — fills NOW at the honest\nlive quote (market) or rests + reserves buying power (limit), in ONE book.\n\nAuth (B10-T1): session cookie (own desk) OR an account key pair for THIS\naccount — see _authorize_desk.","operationId":"place_order_v1_me_accounts__account_id__orders_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ManualOrderRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"My Orders","description":"The desk's order ledger — honest market fills plus resting limit orders\n(working), their fills, and cancellations (audit trail of every honest fill).\n\nAuth (B10-T1): session cookie OR an account key pair for THIS account.","operationId":"my_orders_v1_me_accounts__account_id__orders_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/positions":{"get":{"summary":"My Positions","description":"Every position on the desk — manual + deployed-bot Alphas — with FRESH\nhonest live marks (long@bid / short@ask) and a cash snapshot.\n\n`closed_limit` (optional) caps how many CLOSED trades come back, newest\nfirst — a desk with thousands of settled trades otherwise ships megabytes\nto render a dozen. OMITTING IT IS THE DEFAULT AND RETURNS EVERYTHING, so\nevery existing client is unaffected. OPEN rows are never capped, and\n`closed_total` always reports the true count.\n\nAuth (B10-T1): session cookie OR an account key pair for THIS account.","operationId":"my_positions_v1_me_accounts__account_id__positions_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"closed_limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Closed Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/orders/{order_id}/cancel":{"post":{"summary":"Cancel Order","description":"Cancel a RESTING limit order on the desk, releasing its reserved buying\npower (B8-T2). 404 if not on this desk; 400 if not working.\n\nAuth (B10-T1): session cookie OR an account key pair for THIS account.","operationId":"cancel_order_v1_me_accounts__account_id__orders__order_id__cancel_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"order_id","in":"path","required":true,"schema":{"type":"integer","title":"Order Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/positions/{position_id}/close":{"post":{"summary":"Close Position","description":"Flatten one OPEN position at the honest live quote and realize P&L.\n\nAuth (B10-T1): session cookie OR an account key pair for THIS account.","operationId":"close_position_v1_me_accounts__account_id__positions__position_id__close_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"position_id","in":"path","required":true,"schema":{"type":"integer","title":"Position Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/positions/{position_id}/protect":{"post":{"summary":"Protect Position","description":"B13-T4 — attach protective resting orders to an OPEN position:\ntake_profit / stop_loss (both = a bracket, OCO-linked) and/or a percent\ntrailing stop. SIMULATED, reserve-free (selling what you hold), and\nhonest: a stop that gaps through fills at the real market bid, never at\nthe stop price you wished for.","operationId":"protect_position_v1_me_accounts__account_id__positions__position_id__protect_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"position_id","in":"path","required":true,"schema":{"type":"integer","title":"Position Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProtectRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/keys":{"post":{"summary":"Create Account Key Route","description":"Generate a fresh key pair for one of the caller's own desks and return\n{key_id, secret} — the SECRET is shown ONCE and is never retrievable again.\nRevokes any prior active pair for this account. 403 if not the caller's desk.\n\nOptional ``capabilities`` (agentic-hardening control 1): a preset name\n(``read_only`` | ``backtest_only`` | ``deploy_allowed``) or a list of preset\n/ atomic (read|backtest|deploy) tokens, scoping the new pair. Omitted / null\nmints an UNRESTRICTED (full-scope) pair — the legacy default, no breakage.","operationId":"create_account_key_route_v1_me_accounts__account_id__keys_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/AccountKeyCreateRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"List Account Keys Route","description":"List key METADATA for one of the caller's own desks — key_id, created,\nrevoked — NEVER the secret. 403 if not the caller's desk.","operationId":"list_account_keys_route_v1_me_accounts__account_id__keys_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/keys/revoke":{"post":{"summary":"Revoke Account Key Route","description":"Revoke the active key pair for one of the caller's own desks (idempotent —\n{revoked: 0} when nothing is active). 403 if not the caller's desk.","operationId":"revoke_account_key_route_v1_me_accounts__account_id__keys_revoke_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/agent-audit":{"get":{"summary":"My Agent Audit","description":"The caller's OWN agent/key audit trail (control 5), newest-first.\n\nMulti-tenant: scoped to the session user's actor_uid — a user only ever\nsees the mutations attributed to them (their keys / principal). Session-auth\nonly; the ledger itself is append-only (no write/delete here).","operationId":"my_agent_audit_v1_me_agent_audit_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/keys/status":{"get":{"summary":"Account Key Status Route","description":"The read side of the account key pair: {exists, created_at, last_used_at}\nfor the desk's ACTIVE pair — NEVER key material. exists=false (null\ntimestamps) when no pair has been minted. 404 if not the caller's desk.","operationId":"account_key_status_route_v1_me_accounts__account_id__keys_status_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/accounts/{account_id}/deploy":{"post":{"summary":"Deploy Bulb","description":"Deploy a validated bulb onto a paper account: create the deployment and\nkick a background forward-walk sync (positions appear once it completes).\n\nAccepts an inline ``strategy_spec`` OR a saved ``strategy_id`` (resolved by\nid — paper accounts are standalone principals, not users, so the saved\nstrategy is NOT user-scoped here).  The spec is validated synchronously —\n400 on a bad spec, before anything persists.\n\nAuth: the X-Vega-Key header must resolve to THIS account_id.","operationId":"deploy_bulb_v1_accounts__account_id__deploy_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployCreated"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/deploy":{"post":{"summary":"Deploy To My Account","description":"Deploy a validated bulb onto one of the caller's OWN paper desks.\n\nThe member twin of POST /v1/accounts/{id}/deploy: a member deploying from\nthe /backtest cockpit carries only the vega_session cookie (no per-desk\nAPI key), authorized by ownership. B12-T1: the coder flow's vega_deploy\ntool holds an account key pair instead, so authorize via _authorize_desk\n(session-owner OR pair matching THIS desk, per-key rate-limited) — the\npair deploys only to its own desk (cross-desk pair 403), sessions behave\nexactly as before. Same spec resolution/validation/forward-walk as the\nkey path (shared _do_deploy).","operationId":"deploy_to_my_account_v1_me_accounts__account_id__deploy_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployCreated"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/deploy/preview":{"post":{"summary":"Preview Deploy","description":"PREVIEW, THEN COMMIT (2026-10-14): what POST .../deploy with this SAME\nbody would do — computed on the deploy's own code path — with NOTHING\nwritten except one agent-audit row (action ``deploy_preview``).\n\nSame auth as the deploy: the session owner, or the desk's OWN account\nkey pair (a pair for another desk is 403, an unknown pair 401, a\nstranger's session the same 403 \"not your account\" a missing desk gets).\nA pair needs only the 'read' capability — a preview changes nothing —\nand rides the same per-key rate limit every pair read does.\n\nRuns the deploy's spec resolution + normalization (side aliases, the\nuniverse default, the name) and its checks (validate_spec(for_live=True),\nthe plan's clock, the walker's vehicles, survivorship) — the SAME helper\nfunctions, not a copy — so ``errors`` are the deploy's own words and\n``normalized_spec`` is exactly what a deploy would store. Returns the\nresolved caps (named member-set / platform default / unset), the\ndisclosures + defaults the deploy response would carry, a per-trade cost\nestimate where it is knowable, the desk's available buying power, a\nplain-words risk summary, and ``preview_id`` to pass to the deploy.\nADVISORY ONLY: nothing here can refuse a later deploy.","operationId":"preview_deploy_v1_me_accounts__account_id__deploy_preview_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployPreview"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/pause":{"post":{"summary":"Alpha Pause","operationId":"alpha_pause_v1_me_accounts__account_id__alphas__deploy_id__pause_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/resume":{"post":{"summary":"Alpha Resume","operationId":"alpha_resume_v1_me_accounts__account_id__alphas__deploy_id__resume_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/retire":{"post":{"summary":"Alpha Retire","operationId":"alpha_retire_v1_me_accounts__account_id__alphas__deploy_id__retire_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/approve":{"post":{"summary":"Alpha Approve","description":"Approve a STAGED deployment (control 3): arm it live.\n\nSESSION-OWNER ONLY, mirroring the admin-cloak discipline — the human gate.\nAn account key PAIR (the agent credential) can PROPOSE a deploy\n(requires_approval) but can NEVER approve its own: approval requires the\nowner's logged-in session cookie, never a key. On approval the deployment\nflips staged → running and only THEN starts the forward-walk + grading\n(nothing traded while staged). 404 if not the caller's desk / not found;\n409 if the deployment is not awaiting approval.","operationId":"alpha_approve_v1_me_accounts__account_id__alphas__deploy_id__approve_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}":{"delete":{"summary":"Alpha Delete","description":"Delete a deployed bot that NEVER traded. A bot that traded is RETIRED.\n\nSESSION-OWNER ONLY, mirroring /approve: destroying a bot is a human act, so\nan account key PAIR (the agent credential) can never do it — a pair-only\ncaller gets 401 from require_session_user. `?confirm=true` is REQUIRED (400\notherwise). 404 \"Deployment not found\" for a desk that isn't the caller's or\na bot that isn't on it — byte-identical to a bot that never existed.\n\nTHE HONEST-MONEY RULE. Hard delete is allowed ONLY when the deployment has\nZERO position history AND ZERO order history (db.deployment_history_counts:\npositions open or closed, and the orders tied to them). If either ever\nexisted the answer is 409 and the bot can only be RETIRED — the money ledger\nhas to stay auditable, and retiring keeps every trade on the record.\n\nOPEN POSITIONS CANNOT BE ORPHANED, by construction: an open position IS\nposition history, so such a bot is refused here and retired instead. What\nRETIRE actually does to an open position (verified in paper_sim, 2026-07-29;\nsettlement clause added 2026-08-19 — do not soften either half): the\nstrategy's own exit rule NEVER runs again on that position. No stop-loss, no\ntake-profit, no ratchet, no rule/hold exit, no time-stop or flatten, no desk\nstop. The position stays `status='open'` and honest — it is still re-marked\n(both by the walk and by every positions read, since\npaper_manual.positions_view marks every open row on the desk regardless of\nwhich bot opened it) and any protective order already resting on it still\nfills from the desk matcher — but nothing will close it on the strategy's\nterms. Closing it is the member's move (the manual close on the desk).\n\nTHE ONE EXIT THAT STILL RUNS IS THE MARKET'S OWN: contract-mandated\nSETTLEMENT — an option at expiry, a credit/covered structure at its expiry,\nan equity that delists. That is not the strategy trading; the contract ended\non its own. Without it a retired bot's expired contract could be closed by\nNOBODY: the walk skipped it and a dead contract has no live quote, so the\nmanual close refuses it too (NoLiveQuote). Measured on the live desk —\nposition 4416, an AMZN call that expired 2026-08-17 on a bot retired\n2026-08-03. So a retired bot now walks exactly the SHELF path: mark, never\ntrade, settle what the market has already ended.\n\nA retired bot's open position is MARKED and STILL TRADEABLE BY HAND; it is\nnot auto-managed by the strategy.\n\nA zero-history delete cascades `vega_deployment_transitions` then the\ndeployment row (one transaction). Returns\n``{\"deleted\": deploy_id, \"cascade\": {...}}``.","operationId":"alpha_delete_v1_me_accounts__account_id__alphas__deploy_id__delete","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}},{"name":"confirm","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Confirm"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/transitions":{"get":{"summary":"Alpha Transitions","description":"Lifecycle history for one alpha, newest first (owner-scoped).","operationId":"alpha_transitions_v1_me_accounts__account_id__alphas__deploy_id__transitions_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/report":{"get":{"summary":"Alpha Report","description":"The FULL backtest_result for one deployed alpha (owner-scoped).\n\nThe desk-state / summary reads deliberately carry only a COMPACT\nbacktest_result per deployment (see guardrails.compact_backtest_result);\nthe /alphas/[id] scorecard + widgets need the full blob (all trades, the\ndeep scorecard blocks), so they fetch it here on demand — one heavy\npayload for the ONE alpha being viewed, never every alpha inline.\n\nAuth: session-owner OR the desk's account key pair (via _owned_deployment\n→ _authorize_desk). 404 when the deployment isn't on this desk. Returns\nthe raw BacktestResult object, or {} when nothing is graded yet.","operationId":"alpha_report_v1_me_accounts__account_id__alphas__deploy_id__report_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/breaker":{"post":{"summary":"Alpha Breaker","description":"B13-T3 — set (or clear with null) this alpha's daily-loss circuit\nbreaker, in DOLLARS of same-walk-day P&L drop. Trip pauses the alpha and\nalerts; resume is always manual.","operationId":"alpha_breaker_v1_me_accounts__account_id__alphas__deploy_id__breaker_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BreakerRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/max-per-trade":{"post":{"summary":"Alpha Max Per Trade","description":"MAX $/TRADE (2026-08-05) — set (or clear with null) this bot's OWN\nper-trade dollar cap. It OVERRIDES the desk cap; cleared → the bot follows\nthe desk cap (or is uncapped if that is unset too).\n\nA LIVE-FILL knob (like sizing): the walk sizes an order down to fit it, or\nskips a trade whose smallest unit already exceeds it — it NEVER touches the\nverdict. Owner/desk-pair scoped (404 cross-desk, via _owned_deployment),\naudited like the other per-bot edits.","operationId":"alpha_max_per_trade_v1_me_accounts__account_id__alphas__deploy_id__max_per_trade_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaxPerTradeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/max-open-usd":{"post":{"summary":"Alpha Max Open Usd","description":"MAX OPEN $ (2026-08-06) — set (or clear with null) this bot's OWN cap on how\nmuch money it may have committed across all its open positions at once. It\nOVERRIDES the desk cap; cleared → the bot follows the desk cap (or is uncapped\nif that is unset too).\n\nComposes with the per-trade cap rather than replacing it: `max_usd_per_trade`\nbounds ONE order, this bounds the whole bot, and the desk's buying power bounds\neverything. A trade must pass all three. A LIVE-FILL knob — it never touches the\nverdict. Owner/desk-pair scoped (404 cross-desk, via _owned_deployment), audited\nlike the other per-bot edits.","operationId":"alpha_max_open_usd_v1_me_accounts__account_id__alphas__deploy_id__max_open_usd_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaxOpenUsdRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/regrade-due":{"post":{"summary":"Alpha Regrade Due","description":"RE-GRADE APPOINTMENT (R9, 2026-09-23) — move this bot's re-grade date,\nor clear it with null.\n\nA SHADOW/SHELF grade books one automatically (``jobs.run_alpha_backtest``\nis still the only path that writes a deployment's grade). This is how the\nmember says \"not then, then\" or \"stop reminding me\" — both are answers we\ntake. Setting a date marks the appointment as THEIRS, so a later grade\nrefreshes the REASON and leaves their date alone; clearing removes the\nwhole appointment, including the nudge.\n\nIt gates NOTHING. The bot trades identically with an appointment, past\none, or with none — the engine advises, it never gates. No new column:\nthe date rides ``config`` (jsonb). Owner/desk-pair scoped (404 cross-desk,\nvia _owned_deployment), audited like the other per-bot edits.","operationId":"alpha_regrade_due_v1_me_accounts__account_id__alphas__deploy_id__regrade_due_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegradeDueRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/max-sector-frac":{"post":{"summary":"Alpha Max Sector Frac","description":"SECTOR EXPOSURE CAP (#35) — set (or clear with null) this bot's OWN cap\non the fraction of desk equity one sector's open positions may hold. It\nOVERRIDES the desk cap; cleared → the bot follows the desk cap (or is\nuncapped if that is also unset).\n\nThe exposure it bounds is still DESK-WIDE (the whole point — five bots\neach buying a different chip name is one concentrated sector); the\noverride only changes which NUMBER this bot enforces before adding to it.\nA LIVE-FILL knob — it never touches the verdict. Owner/desk-pair scoped\n(404 cross-desk, via _owned_deployment), audited like the other per-bot\nedits.","operationId":"alpha_max_sector_frac_v1_me_accounts__account_id__alphas__deploy_id__max_sector_frac_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaxSectorFracRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/max-daily-loss":{"post":{"summary":"Alpha Max Daily Loss","description":"DESK-WIDE DAILY LOSS LIMIT, this bot's own NUMBER (Wave A) — set (or\nclear with null) the figure THIS bot enforces. Cleared → it follows the\ndesk's number (or nothing, if that is unset too).\n\nNote carefully what is per-bot and what is not: the LIMIT is this bot's,\nthe LOSS it is compared against is the whole DESK's realized P&L for the\nET session. So a conservative bot can hold a tighter line than its\nneighbours on the same desk, but every one of them is watching the same\nmoney. Owner/desk-pair scoped (404 cross-desk, via _owned_deployment),\naudited like the other per-bot edits. A LIVE-FILL knob — the grader\nnever reads it.","operationId":"alpha_max_daily_loss_v1_me_accounts__account_id__alphas__deploy_id__max_daily_loss_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MaxDailyLossRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/max-gross-exposure":{"post":{"summary":"Alpha Max Gross Exposure","description":"This bot's own gross-exposure limit (see the desk route).","operationId":"alpha_max_gross_exposure_v1_me_accounts__account_id__alphas__deploy_id__max_gross_exposure_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConcentrationLimitRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/max-net-exposure":{"post":{"summary":"Alpha Max Net Exposure","description":"This bot's own net-exposure limit (see the desk route).","operationId":"alpha_max_net_exposure_v1_me_accounts__account_id__alphas__deploy_id__max_net_exposure_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConcentrationLimitRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/max-positions-per-symbol":{"post":{"summary":"Alpha Max Positions Per Symbol","description":"This bot's own per-symbol position limit (see the desk route). The\nCOUNT is still the whole desk's — this bot just enforces its own\nnumber before adding to it.","operationId":"alpha_max_positions_per_symbol_v1_me_accounts__account_id__alphas__deploy_id__max_positions_per_symbol_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConcentrationLimitRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/max-correlation":{"post":{"summary":"Alpha Max Correlation","description":"This bot's own correlation limit (see the desk route).","operationId":"alpha_max_correlation_v1_me_accounts__account_id__alphas__deploy_id__max_correlation_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConcentrationLimitRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/correlation-window":{"post":{"summary":"Alpha Correlation Window","description":"This bot's own correlation window in sessions (default 60).","operationId":"alpha_correlation_window_v1_me_accounts__account_id__alphas__deploy_id__correlation_window_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConcentrationLimitRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/signal":{"post":{"summary":"Alpha Signal","description":"WEBHOOK SIGNALS IN (catalog #10) — queue one entry/exit signal for a\nwebhook-driven bot (spec entry.webhook: true).\n\nBody: {\"symbol\": \"NVDA\", \"action\": \"enter\"|\"exit\" (default enter),\n\"ts\": optional ISO timestamp (default now, UTC)}. Replay-safe: the SAME\nsignal posted twice is ONE queued row ({queued: false, duplicate: true}\non the second). The next walk maps the signal onto its bar and takes it\nthrough EVERY ordinary entry gate (market hours, staleness, caps,\nbuying power) at the same graded price a rule-fired entry gets; an\n{\"action\": \"exit\"} closes the open position(s) on that symbol at the\nwalk's real mark (close_reason='webhook_exit').\n\nAuth: session owner or the per-desk account key PAIR — the agent door,\nsame dependency every other alpha route uses (404 cross-desk). Audited\n(it mutates the desk's future).","operationId":"alpha_signal_v1_me_accounts__account_id__alphas__deploy_id__signal_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/trades.json":{"get":{"summary":"Export Alpha Trades Json","description":"Every trade this bot ever made, oldest first, full fidelity.\n\nThe JSON keeps the nested trails whole (`rolls`, `scale_outs`, `legs`)\n— it is the auditable copy. Owner session or the desk key pair; a\nstranger probing another member's id gets the byte-identical 404 a\nmissing id gets.","operationId":"export_alpha_trades_json_v1_me_accounts__account_id__alphas__deploy_id__trades_json_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/trades.csv":{"get":{"summary":"Export Alpha Trades Csv","description":"The same history as a spreadsheet. Nested trails become a count\ncolumn plus a raw-JSON column, so nothing is lost to the flattening.","operationId":"export_alpha_trades_csv_v1_me_accounts__account_id__alphas__deploy_id__trades_csv_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/export.json":{"get":{"summary":"Export Alpha Data Json","description":"ONE branded document: meta + bot + spec + derived stats + the whole\nledger (open rows carry their stored marks). Owner session or the desk\nkey pair — identical auth to every sibling alpha route.","operationId":"export_alpha_data_json_v1_me_accounts__account_id__alphas__deploy_id__export_json_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/export.csv":{"get":{"summary":"Export Alpha Data Csv","description":"The closed-trades ledger as a parser-clean RFC4180 CSV — header +\nrows only, no banner (branding lives in the FILENAME and the JSON\ntwin; a CSV a script can't read unmodified is not an export).","operationId":"export_alpha_data_csv_v1_me_accounts__account_id__alphas__deploy_id__export_csv_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/export.ipynb":{"get":{"summary":"Export Alpha Notebook","description":"A runnable analysis notebook with the bot's own trades embedded.\n\nIdentical auth to export.json/.csv — the same _owned_deployment gate,\nso a stranger's session, another desk's key pair and a ghost id all\nanswer exactly as they do there.","operationId":"export_alpha_notebook_v1_me_accounts__account_id__alphas__deploy_id__export_ipynb_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/mode":{"post":{"summary":"Alpha Mode","description":"ADVISORY-MODE (2026-07-22, CORE PRODUCT LAW) — set the USER-controlled\ntrade mode for one deployed alpha: live | shadow | shelf.\n\nThis is DELIBERATELY separate from the engine verdict and from the run\nstatus (running/paused/retired). The honest engine ADVISES (its verdict\nis shown BESIDE this choice); the USER decides the fate:\n  live   → normal forward-walk.\n  shadow → tracks signals but commits no new fills (existing positions\n           still mark/exit).\n  shelf  → not writing (the walk skips it).\nA SHELF-graded strategy is NEVER auto-shelved — only the user can. Owner/\ndesk-pair scoped (404 cross-desk); audited. Idempotent (changed=false on\na no-op).","operationId":"alpha_mode_v1_me_accounts__account_id__alphas__deploy_id__mode_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/backtest":{"post":{"summary":"Alpha Backtest","description":"Run a FRESH backtest for a deployed alpha's current spec, on demand.\n\nSaves the run to the caller's vega_backtests (so it lists under\nGET /v1/backtests) and returns the {run_id, status} handle to poll via\nGET /v1/backtests/{run_id} — the same async contract as POST /v1/backtests.\nThe verdict this produces is ADVISORY only; it never changes the\ndeployment's mode or status (the engine advises, the user decides).\n\nDeploy-first: a strategy deployed with NO 'backtest' window still grades —\nthe run fills the full honest data window (WINDOW_START..WINDOW_END), like\nthe deployment grading job. Owner/desk-pair scoped (404 cross-desk).","operationId":"alpha_backtest_v1_me_accounts__account_id__alphas__deploy_id__backtest_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BacktestQueued"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/attach-backtest":{"post":{"summary":"Alpha Attach Backtest","description":"Attach a run the member ALREADY finished to the bot it was a test of.\n\nWHY (2026-10-07, \"Build by talking\"): a member presses \"Test it first\",\nreads the result, then deploys the very same table. Without this the new\nbot reads NOT GRADED and the only way to fix it is to run the identical\nbacktest a second time. This runs NOTHING — it copies a finished grade.\n\nHonesty rules, each a refusal rather than a guess:\n  * the run must belong to the desk's owner (404 otherwise — the same\n    cloak a missing run gets);\n  * it must be a finished single-strategy run (409 while running, and for\n    walk-forward / ensemble rows, whose result is not a bot's grade);\n  * it must have graded EXACTLY this bot's spec — every key but the name\n    and the provenance notes (409 \"a different strategy\").\nStill ADVISORY: mode and status are never touched, exactly like\njobs.run_alpha_backtest, whose attach step this reuses.","operationId":"alpha_attach_backtest_v1_me_accounts__account_id__alphas__deploy_id__attach_backtest_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/health":{"get":{"summary":"Alpha Health Route","description":"B13-T6/T7 — heartbeat + live-vs-backtest divergence for one alpha.\n\nHeartbeat: fire/sync freshness vs the data horizon. Divergence: live\nclosed trades vs the walk-forward promise (PERCENT units both sides;\nscore null under 20 live trades — a thin sample proves nothing).","operationId":"alpha_health_route_v1_me_accounts__account_id__alphas__deploy_id__health_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/graduation":{"get":{"summary":"Alpha Graduation Route","description":"Where this deployed alpha sits on the graduation ladder (read-only):\nbacktested → paper-live → paper-proven → real-eligible, each naming its\nnext gate in plain words.\n\nRead this ONCE when you want to know progression — it is NOT a wait-loop.\nRead-only and derived: it READS the graded verdict + confidence + the live\npaper record (divergence) and decides progression — it NEVER changes a\nverdict, EV, or any engine number. real-eligible is a v2 LABEL only;\nreal-money deploy is not built. Owner/desk-pair scoped (404 cross-desk).","operationId":"alpha_graduation_route_v1_me_accounts__account_id__alphas__deploy_id__graduation_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/alphas/{deploy_id}/why":{"get":{"summary":"Alpha Why","description":"AI HELPERS F1 — \"Why did my bot trade / not trade on this date?\" Runs the\ndeterministic rule trace (engine's explain_signal, no AI) for this deployed\nalpha's spec/symbols on `date`, then an AI plain-English narration of that\nreal trace. AI off/unavailable → the trace still returns with a null\nnarration (still useful). Owner/desk-pair scoped (404 cross-desk); defaults\nto the data horizon when no date is given.","operationId":"alpha_why_v1_me_accounts__account_id__alphas__deploy_id__why_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}},{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/pause":{"post":{"summary":"Desk Pause","description":"Pause every running alpha on ONE desk (retired/error untouched).","operationId":"desk_pause_v1_me_accounts__account_id__pause_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/resume":{"post":{"summary":"Desk Resume","description":"Resume every paused alpha on ONE desk (retired stays retired).","operationId":"desk_resume_v1_me_accounts__account_id__resume_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/pause-all":{"post":{"summary":"Portfolio Pause","description":"The portfolio kill switch: pause every running alpha on EVERY desk\nthe member owns. Session-only — deliberately not reachable with a\nsingle desk's key pair.","operationId":"portfolio_pause_v1_me_pause_all_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/alphas/compare":{"get":{"summary":"Compare Alphas","description":"B13-T8 — are these two alphas really one bet? Fire-set Jaccard\n((symbol, entry_date) fingerprints) + Pearson correlation of daily\nrealized P&L over COMMON exit days (null under 8 common days).","operationId":"compare_alphas_v1_me_alphas_compare_get","parameters":[{"name":"a","in":"query","required":true,"schema":{"type":"string","title":"A"}},{"name":"b","in":"query","required":true,"schema":{"type":"string","title":"B"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/portfolio/correlation":{"get":{"summary":"Portfolio Correlation","description":"B13-T9 — pairwise daily-P&L correlation across my alphas (all desks).\nBounded to the top 25 by |live P&L| with an explicit truncated flag —\nno silent caps. None entries = not enough common days / zero variance.","operationId":"portfolio_correlation_v1_me_portfolio_correlation_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/alphas/leaderboard":{"get":{"summary":"Alphas Leaderboard","description":"B13-T10 — my alphas ranked. by ∈ wf_ev (backtest promise, PERCENT) |\nlive_pnl (DOLLARS realized) | divergence (score, higher = drifting more).\nNulls sort last — a missing number never outranks a real one.","operationId":"alphas_leaderboard_v1_me_alphas_leaderboard_get","parameters":[{"name":"by","in":"query","required":false,"schema":{"type":"string","default":"wf_ev","title":"By"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/accounts/{account_id}/headroom":{"get":{"summary":"Desk Headroom","description":"B13-T11 — open exposure vs equity for one desk. DOLLARS throughout.\nHonest note: sizing is a 1-contract/1-share sim with no margin model —\n'free' is equity minus at-risk cost basis, not broker buying power.","operationId":"desk_headroom_v1_me_accounts__account_id__headroom_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/headroom":{"get":{"summary":"Portfolio Headroom","description":"B13-T11 — combined headroom across all my desks (+ per-desk rows).","operationId":"portfolio_headroom_v1_me_headroom_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/accounts/{account_id}/watchlist":{"get":{"summary":"Get Watchlist","operationId":"get_watchlist_v1_me_accounts__account_id__watchlist_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"summary":"Put Watchlist","description":"Symbols are validated against the live universe; unknowns are\nREPORTED in `dropped`, never silently discarded. Deduped, order kept.","operationId":"put_watchlist_v1_me_accounts__account_id__watchlist_put","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WatchlistRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/plan":{"get":{"summary":"Get My Plan","description":"This member's plan + the ROOM it gives them.\n\n`plan` is what is STORED (billing truth — a founding member sees they are\non free and keeps their founder discount later); `effective_plan` + `caps`\nare what applies right now. UNLIMITED renders as null. `never_capped`\nlists the things no plan can ever take away — the UI should show it\nwherever a cap is mentioned, so \"upgrade\" never reads as \"buy honesty\".\nAlso reports live USAGE against the two counted caps so the UI can show\nheadroom without a second call.\n\nONE QUERY, NO CACHE (2026-08-03). This asks five questions about one\nmember — stored plan, is-this-the-demo-tenant, runs, desks, bots — and it\nused to ask them one at a time, each borrowing its own pooled connection\nand opening its own transaction. Against Neon that is five network round\ntrips on a page every signed-in member loads; at 50 concurrent readers\nthe pool ran out and everybody queued (measured p50 4.5s). It is now ONE\nstatement. The counters stay LIVE — a member who just spent a run must\nsee it spent on the very next read, so caching usage is off the table;\nthe fix is fewer round trips, not staler numbers. If the combined read\nfails we fall back to the old per-question path, which keeps each\ncounter's own fail-soft None instead of reporting a confident zero.","operationId":"get_my_plan_v1_me_plan_get","parameters":[{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/plans":{"get":{"summary":"List Plans","description":"PUBLIC plan catalog for the pricing page. `pro` is present but marked\n`hidden: true` — it is built, and surfaced the day someone asks for the\nroom. No auth by design: a visitor deciding is exactly who needs this.","operationId":"list_plans_v1_plans_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/desk-day":{"get":{"summary":"My Desk Day","description":"What the member's desks did on the LAST COMPLETED ET SESSION.\n\nThe \"Yesterday\" line on /alphas. Facts with units and nothing else: how\nmany fills, across how many bots, and the last one. No projection, no\n\"doing well\", no comparison to anything.\n\nTHE DAY IS THE EXCHANGE CALENDAR, never `date.today() - 1`\n(`market_calendar.prev_session`, which knows the holidays and the\nhalf-days). On a Monday \"yesterday\" is FRIDAY, and on the day after\nThanksgiving it is Wednesday — a calendar day would report an honest\nzero about a day the market never opened, and a zero is exactly what a\nbroken walk looks like. The session we measured is returned WITH the\nnumbers so the reader can see which day they describe.\n\nREAD-ONLY, session-scoped: a member sees their OWN desks and no others.\nA member with no desk gets zeros and a null `last` — that is a fact\nabout an empty book, not an error.","operationId":"my_desk_day_v1_me_desk_day_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/prefs/{key}":{"get":{"summary":"Get Pref","operationId":"get_pref_v1_me_prefs__key__get","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"summary":"Put Pref","description":"Saved layouts / widget toggles. 16KB cap per key — a pref store,\nnot a database.","operationId":"put_pref_v1_me_prefs__key__put","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrefRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Delete Pref","description":"Clear one saved pref (avatar pick, layout, widget toggle) — session-only.\n\nREMOVES the row rather than storing a null: \"no row\" means \"no choice made\",\nwhich is what the avatar store reads to fall back to the tag identicon; a\nstored null would keep overriding it. Idempotent and never a 404 — a pref\nthat was never set is not an error, so the answer says so:\n``{\"deleted\": key, \"existed\": false}``. A malformed key is still a 400,\nexactly like GET/PUT.","operationId":"delete_pref_v1_me_prefs__key__delete","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/api-key":{"post":{"summary":"Rotate Api Key","description":"Loose-end #18 — mint the member's user API key (SDK / X-Vega-Key auth).\nReturned RAW exactly once; only its hash is stored; rotating kills the\nprior key immediately. Session-only — a key can't mint keys.","operationId":"rotate_api_key_v1_me_api_key_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/me/alerts":{"get":{"summary":"My Alerts","description":"The member's alert feed, newest first. `?unread=1` filters to unread;\nbounded to 500 rows. Session-only (a desk pair sees trading, not the\nwhole account's alert stream).\n\nP4: DISMISSED alerts are EXCLUDED by default — dismissing is how a member\nclears the feed. `?dismissed=1` includes them again, so nothing a member\ndismissed is silently unrecoverable; only DELETE removes a row for good.\n\nTHIS FEED IS THE MEMBER'S OWN NEWS, NOT OURS (2026-08-20). Only\n`db.MEMBER_ALERT_KINDS` are returned — the daily brief, a desk running\nout of money, a bot's own rules firing, a corporate action. Application\nerrors, engine reconciles and operator digests were reaching real\nmember dashboards; they are internal and now stop here. The allowlist\nfails CLOSED: a kind nobody classified is hidden until it is added.\n\n`?internal=1` returns the unfiltered stream and is honoured for ADMINS\nONLY (the admin growth page reads its own `admin_digest` this way). A\nnon-admin passing it gets the member feed regardless — the flag is a\nrequest, never a grant.","operationId":"my_alerts_v1_me_alerts_get","parameters":[{"name":"unread","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Unread"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}},{"name":"dismissed","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Dismissed"}},{"name":"internal","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Internal"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/alerts/read":{"post":{"summary":"Read Alerts","description":"Mark my alerts read. Foreign ids are ignored, not an error — the\nresponse's `marked` count is the truth.","operationId":"read_alerts_v1_me_alerts_read_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertReadRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/alerts/dismiss":{"post":{"summary":"Dismiss Alerts","description":"Dismiss my alerts — they leave the feed but are still there under\n`GET /v1/me/alerts?dismissed=1`.\n\nSession-only, like the sibling read route, and the SAME forgiving contract:\nforeign ids are ignored rather than an error (the `dismissed` count is the\ntruth), and re-dismissing counts 0 instead of failing. Read and dismissed\nare different facts: \"I saw it\" vs \"I'm done with it\".","operationId":"dismiss_alerts_v1_me_alerts_dismiss_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertDismissRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/me/alerts/{alert_id}":{"delete":{"summary":"Delete Alert","description":"Hard-delete ONE of my alerts. Session-only.\n\nAn alert belonging to somebody else answers the SAME 404 \"Alert not found\"\nas an id that never existed, and a second delete is that same plain 404.\nFor a feed you just want quiet, prefer /dismiss — that one is reversible.","operationId":"delete_alert_v1_me_alerts__alert_id__delete","parameters":[{"name":"alert_id","in":"path","required":true,"schema":{"type":"integer","title":"Alert Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/accounts/{account_id}/alphas/{deploy_id}/chart":{"get":{"summary":"Alpha Chart Route","operationId":"alpha_chart_route_v1_accounts__account_id__alphas__deploy_id__chart_get","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"deploy_id","in":"path","required":true,"schema":{"type":"string","title":"Deploy Id"}},{"name":"symbol","in":"query","required":true,"schema":{"type":"string","title":"Symbol"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/accounts/{account_id}/sync":{"post":{"summary":"Sync Account","description":"Re-run the forward-walk for every RUNNING deployment on this account\n(advances marks / closes matured positions).  Runs in the background;\nreturns immediately with {status: 'syncing'}.\n\nLEGACY route: it authenticates with the single per-desk KEY (X-Vega-Key).\nCoders on the newer account KEY PAIR (X-Vega-Account-Key-Id +\nX-Vega-Account-Secret) reach for a \"sync\" step that doesn't exist for the\npair — deploying already kicks the first forward-walk. When we see the\npair here, say that in plain words instead of a bare 401 (auth behavior\nis unchanged — this path still refuses; only the message is kinder).","operationId":"sync_account_v1_accounts__account_id__sync_post","parameters":[{"name":"account_id","in":"path","required":true,"schema":{"type":"integer","title":"Account Id"}},{"name":"x-vega-key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Key"}},{"name":"x-vega-account-key-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Account-Key-Id"}},{"name":"x-vega-account-secret","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Vega-Account-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/live/quotes":{"get":{"summary":"Live Quotes","description":"Watchlist last price + day change for a comma-separated symbol list.","operationId":"live_quotes_v1_live_quotes_get","parameters":[{"name":"symbols","in":"query","required":true,"schema":{"type":"string","title":"Symbols"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/live/bars":{"get":{"summary":"Live Bars","description":"OHLCV bars for one symbol at a timeframe (1m/5m/15m/1H/1D).\n``before`` = the page ending before that RFC3339 stamp (scroll-back);\n``days`` = a range chip's window (calendar days incl. today).","operationId":"live_bars_v1_live_bars_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","title":"Symbol"}},{"name":"tf","in":"query","required":false,"schema":{"type":"string","default":"1D","title":"Tf"}},{"name":"before","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Before"}},{"name":"days","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Days"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/live/chain":{"get":{"summary":"Live Chain","description":"LIVE OPRA option chain (±6 strikes around ATM) for one expiry.","operationId":"live_chain_v1_live_chain_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","title":"Symbol"}},{"name":"expiry","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Expiry"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/live/read":{"get":{"summary":"Live Read","description":"Vega's read — engine-computed RSI-14, above/below 200-day, families.","operationId":"live_read_v1_live_read_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","title":"Symbol"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/live/indicators":{"get":{"summary":"Live Indicators","description":"Per-bar indicator series for chart overlays — engine-computed\n(build_features + attach_indicators), same math as the backtests.\nnames: comma list, e.g. rsi,macd,bollinger,sma (defaults if omitted).\ntf: the CHART timeframe the series is computed on — 1m/5m/15m/30m/1h/4h/1D\n(default 1D). The window is the member's own bars, so a 20-period average\non a 5m chart is twenty five-minute bars.","operationId":"live_indicators_v1_live_indicators_get","parameters":[{"name":"symbol","in":"query","required":true,"schema":{"type":"string","title":"Symbol"}},{"name":"names","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Names"}},{"name":"tf","in":"query","required":false,"schema":{"type":"string","default":"1D","title":"Tf"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AccountKeyCreateRequest":{"properties":{"capabilities":{"anyOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Capabilities"}},"type":"object","title":"AccountKeyCreateRequest","description":"POST /v1/me/accounts/{id}/keys — optional capability scope for a new pair.\n\n``capabilities`` may be a preset name (``read_only`` | ``backtest_only`` |\n``deploy_allowed``), a list of preset / atomic (read|backtest|deploy) tokens,\nor null (an UNRESTRICTED full-scope pair — the legacy default). Unknown tokens\nare a 400."},"AccountResetRequest":{"properties":{"starting_cash":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Starting Cash"}},"type":"object","title":"AccountResetRequest","description":"POST /v1/me/accounts/{id}/reset — wipe the desk and (optionally) set cash.\n\nstarting_cash: the new base cash, $10,000..$1,000,000 (int or float). Omitted /\nnull keeps the account's current starting_cash (a plain history wipe). Out of\nrange → 422 (raised here as a validation error)."},"AccountState":{"properties":{"account":{"$ref":"#/components/schemas/AccountSummary"},"deployments":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Deployments"},"positions":{"additionalProperties":true,"type":"object","title":"Positions"},"equity":{"additionalProperties":true,"type":"object","title":"Equity"}},"type":"object","required":["account","deployments","positions","equity"],"title":"AccountState","description":"GET /v1/accounts/{id} — the full account view for the authenticated principal:\nthe account headline, its deployments, and open/closed position tables."},"AccountSummary":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"starting_cash":{"type":"number","title":"Starting Cash"},"equity":{"type":"number","title":"Equity"},"realized":{"type":"number","title":"Realized"},"unrealized":{"type":"number","title":"Unrealized"},"n_open":{"type":"integer","title":"N Open"},"n_closed":{"type":"integer","title":"N Closed"},"n_deployments":{"type":"integer","title":"N Deployments"},"max_usd_per_trade":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Usd Per Trade"},"max_open_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Open Usd"},"max_sector_frac":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Sector Frac"},"max_daily_loss_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Daily Loss Usd"},"trading_halted":{"type":"boolean","title":"Trading Halted","default":false},"trading_halt_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trading Halt Reason"},"trading_halted_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trading Halted By"},"max_gross_exposure":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Gross Exposure"},"max_net_exposure":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Net Exposure"},"max_positions_per_symbol":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Positions Per Symbol"},"max_correlation":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Correlation"},"correlation_window_sessions":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Correlation Window Sessions"},"desk_position_stop_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Desk Position Stop Pct"}},"type":"object","required":["id","starting_cash","equity","realized","unrealized","n_open","n_closed","n_deployments"],"title":"AccountSummary","description":"One row in GET /v1/accounts — a paper account's headline P&L + counts.\n\nEvery figure is SIMULATED paper P&L at real quote prices (zero real money)."},"AlertDismissRequest":{"properties":{"ids":{"items":{"type":"integer"},"type":"array","title":"Ids"}},"type":"object","required":["ids"],"title":"AlertDismissRequest","description":"POST /v1/me/alerts/dismiss — clear these alerts out of the feed.\n\nSame shape and same forgiving contract as AlertReadRequest: foreign ids are\nignored, the returned count is the truth."},"AlertReadRequest":{"properties":{"ids":{"items":{"type":"integer"},"type":"array","title":"Ids"}},"type":"object","required":["ids"],"title":"AlertReadRequest"},"BacktestQueued":{"properties":{"run_id":{"type":"string","title":"Run Id"},"status":{"type":"string","title":"Status","default":"queued"},"defaults_applied":{"items":{"type":"string"},"type":"array","title":"Defaults Applied","default":[]}},"type":"object","required":["run_id"],"title":"BacktestQueued"},"BacktestRequest":{"properties":{"strategy_spec":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Strategy Spec"},"strategy_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Strategy Id"},"anon_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Anon Id"}},"type":"object","title":"BacktestRequest","description":"POST /v1/backtests — either inline spec or a saved strategy_id."},"BreakerRequest":{"properties":{"max_daily_loss":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Daily Loss"}},"type":"object","title":"BreakerRequest"},"BuildPhaseRequest":{"properties":{"phase":{"type":"string","maxLength":32,"minLength":1,"title":"Phase"},"text":{"type":"string","maxLength":2000,"minLength":1,"title":"Text"},"spec":{"additionalProperties":true,"type":"object","title":"Spec"}},"type":"object","required":["phase","text"],"title":"BuildPhaseRequest"},"BulkItemResult":{"properties":{"name":{"type":"string","title":"Name"},"valid":{"type":"boolean","title":"Valid"},"strategy_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Strategy Id"},"deploy_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deploy Id"},"errors":{"items":{"type":"string"},"type":"array","title":"Errors","default":[]},"suggestions":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Suggestions"},"suggested_spec":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Suggested Spec"}},"type":"object","required":["name","valid"],"title":"BulkItemResult","description":"Per-item outcome of a bulk call — errors are named, never silently dropped.\n\nAsk-first defaults (Ro, 2026-07-21): an invalid item may carry\n`suggestions` ([{field, suggested_value, why}] in plain words) and a ready\n`suggested_spec` that validates as-is — resubmit it explicitly to accept\n(nothing is ever auto-applied)."},"BulkStrategiesRequest":{"properties":{"items":{"items":{"$ref":"#/components/schemas/BulkStrategyItem"},"type":"array","title":"Items"},"deploy_to":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Deploy To"}},"type":"object","required":["items"],"title":"BulkStrategiesRequest","description":"POST /v1/strategies/bulk — land a whole fleet in ONE call. Up to\nBULK_MAX_ITEMS items; each item is validated + saved independently (a bad\nitem never blocks the others). With ``deploy_to`` set, every VALID item is\nalso deployed forward-only onto that desk and starts trading immediately.\n\nNO BACKTEST IS RUN OR QUEUED (2026-07-28, Ro). Deploying is not a request to\nbe graded: not every strategy CAN be honestly graded, and grading uninvited\nis the engine DECIDING instead of ADVISING. The bots land ungraded, the UI\nOFFERS a backtest, and the user chooses. ``queued_backtests`` is retained in\nthe response for API compatibility and is always 0."},"BulkStrategiesResponse":{"properties":{"results":{"items":{"$ref":"#/components/schemas/BulkItemResult"},"type":"array","title":"Results"},"saved":{"type":"integer","title":"Saved"},"deployed":{"type":"integer","title":"Deployed"},"queued_backtests":{"type":"integer","title":"Queued Backtests"}},"type":"object","required":["results","saved","deployed","queued_backtests"],"title":"BulkStrategiesResponse","description":"Item-ordered results + saved/deployed counts + queued grading jobs."},"BulkStrategyItem":{"properties":{"strategy_spec":{"additionalProperties":true,"type":"object","title":"Strategy Spec"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","required":["strategy_spec"],"title":"BulkStrategyItem","description":"One strategy in a POST /v1/strategies/bulk call."},"ConcentrationLimitRequest":{"properties":{"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"}},"type":"object","title":"ConcentrationLimitRequest"},"CopilotRequest":{"properties":{"description":{"type":"string","maxLength":2000,"minLength":1,"title":"Description"}},"type":"object","required":["description"],"title":"CopilotRequest"},"DeployCreated":{"properties":{"deploy_id":{"type":"string","title":"Deploy Id"},"status":{"type":"string","title":"Status","default":"running"},"warning":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Warning"},"disclosures":{"items":{"type":"string"},"type":"array","title":"Disclosures","default":[]},"defaults_applied":{"items":{"type":"string"},"type":"array","title":"Defaults Applied","default":[]},"fire_check":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Fire Check"},"preview_match":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Preview Match"}},"type":"object","required":["deploy_id"],"title":"DeployCreated","description":"Response to a deploy: the new deployment id + its lifecycle status.\n\n``warning`` carries an honest advisory (plain-words ``message`` + the verdict /\nconfidence label / provenance caveat) when the deployed strategy has a KNOWN\nprior backtest that graded SHELF or inconclusive — the deploy still succeeds\n(paper is for learning), the warning just makes the backtest's \"no\" loud."},"DeployPreview":{"properties":{"preview_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preview Id"},"account_id":{"type":"integer","title":"Account Id"},"read_only":{"type":"boolean","title":"Read Only","default":true},"valid":{"type":"boolean","title":"Valid"},"errors":{"items":{"type":"string"},"type":"array","title":"Errors","default":[]},"would_deploy":{"type":"boolean","title":"Would Deploy","default":false},"blocked_by":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Blocked By","default":[]},"normalized_spec":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Normalized Spec"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"mode":{"type":"string","title":"Mode","default":"live"},"status":{"type":"string","title":"Status","default":"running"},"caps":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Caps"},"disclosures":{"items":{"type":"string"},"type":"array","title":"Disclosures","default":[]},"defaults_applied":{"items":{"type":"string"},"type":"array","title":"Defaults Applied","default":[]},"estimated_trade_cost":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Estimated Trade Cost"},"desk":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Desk"},"risk_summary":{"items":{"type":"string"},"type":"array","title":"Risk Summary","default":[]},"next_step":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Step"}},"type":"object","required":["account_id","valid"],"title":"DeployPreview","description":"Response to POST /v1/me/accounts/{id}/deploy/preview — what a deploy of the\nSAME body would do, computed on the deploy's own code path, with NOTHING\nwritten (beyond one agent-audit row).\n\n``valid``/``errors`` mirror exactly what the deploy would refuse with (same\nplain words). ``blocked_by`` lists the desk/account gates (maintenance,\nemail verification, agent freeze, the key's capability, the desk's deploy\ncap, the plan's bot cap) that would refuse the deploy right now — each with\nthe status and words the deploy would answer. ``would_deploy`` is true only\nwhen the spec is valid AND nothing blocks. ``preview_id`` is\nsha256(account_id + canonical JSON of ``normalized_spec``)[:16]; pass it to\nthe deploy to get ``preview_match`` back. Money: ``estimated_trade_cost``\n(a number only where the sizing makes one knowable before a fill, else\n\"depends on the fill\") and ``desk`` (equity + available buying power)."},"DeployRequest":{"properties":{"strategy_spec":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Strategy Spec"},"strategy_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Strategy Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"acknowledge_weak":{"type":"boolean","title":"Acknowledge Weak","default":false},"requires_approval":{"type":"boolean","title":"Requires Approval","default":false},"preview_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Preview Id"}},"type":"object","title":"DeployRequest","description":"POST /v1/accounts/{id}/deploy — deploy a validated bulb onto a paper account.\n\nProvide exactly ONE source for the spec:\n  - ``strategy_spec``: an inline strategy spec dict (validated server-side), or\n  - ``strategy_id``: id of a strategy previously saved by the SAME principal.\n\n``name`` is an optional human label for the deployment (e.g. \"oversold-bounce\").\n\nDeploys are FORWARD-ONLY (founder rule, 2026-07-17): the desk starts trading at\nthe moment of deployment and only ever shows trades that happened while it was\ndeployed.  Historical performance lives in the strategy's BACKTEST report (stored\nper deployment, shown separately) — it is never replayed onto the desk as if the\ndesk had traded it.  The old ``backfill_days`` field (default 60!) silently\npre-filled two months of simulated trades on every deploy and was removed for\nexactly that reason; unknown fields sent by old clients are ignored by pydantic."},"DeskCreateRequest":{"properties":{"name":{"type":"string","title":"Name"},"starting_cash":{"type":"number","title":"Starting Cash","default":100000}},"type":"object","required":["name"],"title":"DeskCreateRequest","description":"POST /v1/me/accounts — mint a new paper desk (name + opening cash)."},"DeskDeleteRequest":{"properties":{"confirm_name":{"type":"string","title":"Confirm Name"}},"type":"object","required":["confirm_name"],"title":"DeskDeleteRequest","description":"DELETE /v1/me/accounts/{id} — typed-name confirmation guard."},"DeskDepositRequest":{"properties":{"amount":{"type":"number","title":"Amount"}},"type":"object","required":["amount"],"title":"DeskDepositRequest","description":"POST /v1/me/accounts/{id}/deposit — add cash to a desk (\"buy more money\")."},"DeskRenameRequest":{"properties":{"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"DeskRenameRequest","description":"PATCH /v1/me/accounts/{id} — rename a desk."},"EnsembleBacktestQueued":{"properties":{"run_id":{"type":"string","title":"Run Id"},"status":{"type":"string","title":"Status","default":"queued"},"kind":{"type":"string","title":"Kind","default":"ensemble"}},"type":"object","required":["run_id"],"title":"EnsembleBacktestQueued"},"EnsembleBacktestRequest":{"properties":{"members":{"items":{"$ref":"#/components/schemas/EnsembleMember"},"type":"array","title":"Members"},"weights":{"anyOf":[{"items":{"type":"number"},"type":"array"},{"type":"string"},{"type":"null"}],"title":"Weights"}},"type":"object","required":["members"],"title":"EnsembleBacktestRequest","description":"POST /v1/backtests/ensemble (#39) — 2..8 strategies plus how to weight\nthem. Every member is graded by the SAME run_backtest as a single run;\nwhat the ensemble adds is the blended curve, the correlation matrix and\nthe diversification benefit. Bounds are enforced in plain words by\nvega_engine.validate.validate_ensemble."},"EnsembleMember":{"properties":{"strategy_spec":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Strategy Spec"},"strategy_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Strategy Id"}},"type":"object","title":"EnsembleMember","description":"One member of an ensemble: EXACTLY ONE of a saved strategy_id or an\ninline strategy_spec (the same either/or every backtest route uses)."},"EventBody":{"properties":{"name":{"type":"string","title":"Name"},"props":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Props"},"anon_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Anon Id"}},"type":"object","required":["name"],"title":"EventBody"},"FeedbackBody":{"properties":{"message":{"type":"string","title":"Message"},"page":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Page"},"context":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Context"},"screenshot":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screenshot"}},"type":"object","required":["message"],"title":"FeedbackBody"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ImportRequest":{"properties":{"source_type":{"type":"string","minLength":1,"title":"Source Type"},"source":{"type":"string","maxLength":12000,"minLength":1,"title":"Source"},"fill_gaps":{"type":"boolean","title":"Fill Gaps","default":false}},"type":"object","required":["source_type","source"],"title":"ImportRequest"},"ManualLeg":{"properties":{"contract":{"type":"string","title":"Contract"},"side":{"type":"string","title":"Side"},"qty":{"type":"integer","title":"Qty","default":1}},"type":"object","required":["contract","side"],"title":"ManualLeg","description":"One leg of a manual option order."},"ManualOrderRequest":{"properties":{"kind":{"type":"string","title":"Kind"},"side":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Side"},"symbol":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Symbol"},"legs":{"anyOf":[{"items":{"$ref":"#/components/schemas/ManualLeg"},"type":"array"},{"type":"null"}],"title":"Legs"},"qty":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Qty"},"order_type":{"type":"string","title":"Order Type","default":"market"},"limit_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Limit Price"}},"type":"object","required":["kind"],"title":"ManualOrderRequest","description":"POST /v1/me/accounts/{id}/orders — a manual MARKET or LIMIT order.\n\nShares:  {kind:'shares', side:'buy'|'sell', symbol, qty}\nOption:  {kind:'option', legs:[{contract, side, qty}, ...]}   (1 leg = single, 2+ = combo)\n\norder_type='limit' + limit_price → the order RESTS and reserves buying power\n(B8-T2); the matcher fills it limit-or-better when the live quote crosses. A\nlimit order supports shares (buy/sell) and a SINGLE option leg."},"MaxDailyLossRequest":{"properties":{"max_daily_loss_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Daily Loss Usd"}},"type":"object","title":"MaxDailyLossRequest"},"MaxOpenUsdRequest":{"properties":{"max_open_usd":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Open Usd"}},"type":"object","title":"MaxOpenUsdRequest"},"MaxPerTradeRequest":{"properties":{"max_usd_per_trade":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Usd Per Trade"}},"type":"object","title":"MaxPerTradeRequest"},"MaxSectorFracRequest":{"properties":{"max_sector_frac":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Sector Frac"}},"type":"object","title":"MaxSectorFracRequest"},"ModeRequest":{"properties":{"mode":{"type":"string","title":"Mode"}},"type":"object","required":["mode"],"title":"ModeRequest"},"PortExternalRequest":{"properties":{"source_text":{"type":"string","maxLength":12000,"minLength":1,"title":"Source Text"},"source_kind":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Kind"}},"type":"object","required":["source_text"],"title":"PortExternalRequest"},"PortfolioBacktestQueued":{"properties":{"run_id":{"type":"string","title":"Run Id"},"status":{"type":"string","title":"Status","default":"queued"},"kind":{"type":"string","title":"Kind","default":"portfolio"}},"type":"object","required":["run_id"],"title":"PortfolioBacktestQueued"},"PortfolioBacktestRequest":{"properties":{"strategy_ids":{"items":{"type":"integer"},"type":"array","title":"Strategy Ids"}},"type":"object","required":["strategy_ids"],"title":"PortfolioBacktestRequest","description":"POST /v1/portfolio/backtests — combine 2..8 of the caller's saved strategies.\n\nstrategy_ids must be a list of DISTINCT integers, length in [2, 8]. Bounds and\ninteger-ness are enforced here (422 on violation) before any DB/ownership work;\nownership (own-strategies-only, 403/404) is enforced in the route."},"PrefRequest":{"properties":{"value":{"title":"Value"}},"type":"object","title":"PrefRequest"},"PriceAlertBody":{"properties":{"symbol":{"type":"string","maxLength":10,"minLength":1,"title":"Symbol"},"price":{"type":"number","title":"Price"},"direction":{"type":"string","title":"Direction"}},"type":"object","required":["symbol","price","direction"],"title":"PriceAlertBody"},"ProtectRequest":{"properties":{"take_profit":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Take Profit"},"stop_loss":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Stop Loss"},"trail_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Trail Pct"}},"type":"object","title":"ProtectRequest"},"RealityCheckRequest":{"properties":{"spec":{"additionalProperties":true,"type":"object","title":"Spec"},"part":{"type":"string","maxLength":16,"title":"Part","default":"entry"},"leaf":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Leaf"}},"type":"object","required":["spec"],"title":"RealityCheckRequest"},"RegradeDueRequest":{"properties":{"due":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Due"}},"type":"object","title":"RegradeDueRequest","description":"THE APPOINTMENT IS THE MEMBER'S (2026-09-23). ``due`` is an ISO\n``YYYY-MM-DD``; null CLEARS the appointment entirely. It is a date to come\nback on, never a gate: nothing about the bot changes when it passes, and a\nbot with no appointment trades exactly like one that has kept it."},"ShareReportBody":{"properties":{"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","title":"ShareReportBody","description":"POST /v1/share/{share_id}/report — both fields optional.\n\nAn empty body is a valid report (\"this page is wrong\") — demanding a category\nfrom someone doing us a favour is how you get no reports at all."},"StrategyCreated":{"properties":{"strategy_id":{"type":"integer","title":"Strategy Id"}},"type":"object","required":["strategy_id"],"title":"StrategyCreated"},"StrategyPatchRequest":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"spec":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Spec"}},"type":"object","title":"StrategyPatchRequest","description":"PATCH /v1/strategies/{id} — rename and/or replace the spec.\n\nBoth fields optional, at least one required (400 otherwise). A replaced `spec`\ngoes through the SAME validation POST /v1/strategies uses."},"StrategyRequest":{"properties":{"strategy_spec":{"additionalProperties":true,"type":"object","title":"Strategy Spec"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","required":["strategy_spec"],"title":"StrategyRequest","description":"POST /v1/strategies — spec + optional human name."},"SurveyAnswerBody":{"properties":{"t":{"type":"string","title":"T"},"comfort":{"anyOf":[{"type":"string"},{"type":"integer"},{"type":"null"}],"title":"Comfort"},"ai":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ai"},"improve":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Improve"},"pay":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pay"},"comment":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Comment"}},"type":"object","required":["t"],"title":"SurveyAnswerBody"},"TradingHaltRequest":{"properties":{"halted":{"type":"boolean","title":"Halted"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","required":["halted"],"title":"TradingHaltRequest"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WaitlistBody":{"properties":{"email":{"type":"string","title":"Email"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"}},"type":"object","required":["email"],"title":"WaitlistBody"},"WalkforwardBacktestQueued":{"properties":{"run_id":{"type":"string","title":"Run Id"},"status":{"type":"string","title":"Status","default":"queued"},"kind":{"type":"string","title":"Kind","default":"walkforward"}},"type":"object","required":["run_id"],"title":"WalkforwardBacktestQueued"},"WalkforwardBacktestRequest":{"properties":{"strategy_spec":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Strategy Spec"},"strategy_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Strategy Id"},"walkforward":{"additionalProperties":true,"type":"object","title":"Walkforward"}},"type":"object","required":["walkforward"],"title":"WalkforwardBacktestRequest","description":"POST /v1/backtests/walkforward (#38) — the spec (inline or a saved\nstrategy_id) plus the walkforward config {\"folds\": 2..6, \"optimize\":\n[{\"path\", \"grid\"}]}. The config's bounds/paths are validated in\nvega_engine.validate.validate_walkforward (plain-words refusals)."},"WatchlistRequest":{"properties":{"symbols":{"items":{"type":"string"},"type":"array","title":"Symbols"}},"type":"object","required":["symbols"],"title":"WatchlistRequest"},"_AuthBody":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"persona":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Persona"},"trader_tag":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trader Tag"},"invite_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Invite Code"},"anon_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Anon Id"}},"type":"object","required":["email","password"],"title":"_AuthBody"},"_ChangePwBody":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","title":"New Password"}},"type":"object","required":["current_password","new_password"],"title":"_ChangePwBody"},"_DeleteAccountBody":{"properties":{"confirm_email":{"type":"string","title":"Confirm Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["confirm_email","password"],"title":"_DeleteAccountBody"},"_EmailLinkBody":{"properties":{"email":{"type":"string","title":"Email"},"next":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next"}},"type":"object","required":["email"],"title":"_EmailLinkBody"},"_EmailLinkConsumeBody":{"properties":{"token":{"type":"string","title":"Token"},"anon_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Anon Id"}},"type":"object","required":["token"],"title":"_EmailLinkConsumeBody"},"_ForgotBody":{"properties":{"email":{"type":"string","title":"Email"}},"type":"object","required":["email"],"title":"_ForgotBody"},"_FreezeBody":{"properties":{"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","title":"_FreezeBody"},"_LoginVerifyBody":{"properties":{"credential":{"additionalProperties":true,"type":"object","title":"Credential"}},"type":"object","required":["credential"],"title":"_LoginVerifyBody"},"_PrefsPatch":{"properties":{"ai_daily_brief":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Ai Daily Brief"},"divergence_alerts":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Divergence Alerts"},"deploy_events":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Deploy Events"},"personal_ai":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Personal Ai"},"bot_traded_email":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Bot Traded Email"},"lesson_nudge_email":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Lesson Nudge Email"},"weekly_digest_email":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Weekly Digest Email"},"regrade_due_email":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Regrade Due Email"},"marketing_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Marketing Email"}},"type":"object","title":"_PrefsPatch"},"_RegisterVerifyBody":{"properties":{"credential":{"additionalProperties":true,"type":"object","title":"Credential"},"nickname":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nickname"}},"type":"object","required":["credential"],"title":"_RegisterVerifyBody"},"_ResetBody":{"properties":{"token":{"type":"string","title":"Token"},"password":{"type":"string","title":"Password"}},"type":"object","required":["token","password"],"title":"_ResetBody"},"_TagBody":{"properties":{"trader_tag":{"type":"string","title":"Trader Tag"}},"type":"object","required":["trader_tag"],"title":"_TagBody"}}}}