{"openapi":"3.1.0","info":{"title":"Flicker API","description":"Flicker Backend API - Technical Analysis Engine for cryptocurrency, stock, and FX (currency) markets. Provides real-time technical indicators, market zones, and AI-powered market insights.\n\n---\n\n## Changelog\n\n### v2.10.54\n- **Analysis:** `h1` stays free for mobile builds below `3.9.30`. Those builds send `timeframe=h1` on every analysis, signal and screener request — it is non-nullable in their request params and always serialized — so pricing `h1` as Pro would answer 402 to every free user of every build in the wild, on every asset page, alert template and screener list. The exemption is `h1` alone, mobile only (`x-platform` + `x-app-version`), fails open on an unknown version, and `/v2/config` publishes it: a client below the cutoff reads `needs_pro: false` for the chip it is served free. **Temporary** — it is deleted once those builds age out, and the `legacy_hourly_free` warn (throttled, one line per 5 minutes with a grant count) is how you know they have. `3.9.30` and not `3.9.29` because two mobile builds ship as `3.9.29+124`, one of which still hardcodes `h1`: the config-aware build must be released as `3.9.30` or later.\n- **Analysis:** The default timeframe is now **`d1`**, not `h1` — and with it the free tier, which is defined as the timeframe a request naming none is served. A request that sends no `timeframe` to `/v2/analysis`, `/v2/assets/signals*`, `/v2/positions/{id}/analysis`, or any MCP tool is answered on daily candles; `h1` is now a Pro timeframe like every other one. On-chain (`external`) assets are unaffected: every timeframe their source serves is still free. `GET /v2/config` carries the whole change — `analysis.default_timeframe` reads `d1` and each `timeframes[].needs_pro` moves with it — so a client that renders the config needs no code change; one that hardcodes `h1` as its free chip will offer a chip that answers 402.\n- **Analysis:** The warmer computes `d1` alongside `h1` and `m15`, so the signal screener (`/v2/assets/signals*`, MCP `get_trading_signals`) now accepts and serves `d1` instead of answering 400, and RSI / smart-zone alerts have three distinct evaluation windows rather than two (`alerts.kinds[].timeframes[].evaluates_as` in `/v2/config` reports them). The market-wide reads that had no timeframe parameter at all — the fear & greed leaderboard, the screener's breakout list, the MCP market overview — now read the daily analysis set rather than the hourly one, and regime-transition pushes are raised off `d1`.\n- **Analysis:** The two `402`s on `/v2/analysis`, `/v2/analysis/{id}/overview`, `/v2/analysis/{id}/signal` and `/v2/positions/{id}/analysis` are now told apart by their `code`: **1004** `AnalysisTimeframeRestricted` for a paid timeframe, **1005** `AnalysisHistoryDepthRestricted` for a window deeper than the free candle ceiling. Both answered `1` (`GeneralError`) before, so the only machine-readable part of the refusal was identical for two different paywalls and clients re-derived the cause by re-implementing `timeframe_needs_pro` against `/v2/config` — which answers nothing when that document never arrived. The messages are unchanged. A client that does not know the new values reads an unknown code, exactly as it already must for every other one.\n- Nothing changed shape. Every field keeps its type; `d1` and `h1` were both already valid values of the same enum.\n\n### v2.10.52\n- **Config:** New `GET /v2/config` — one cacheable, unauthenticated-safe read that answers what is free, what is paid, and which choices are real, so clients stop keeping their own copies of rules that live in Rust. Every field is derived from the predicate the request path itself runs, never from a second list: the free timeframe set from `timeframe_needs_pro` (the default alone), the free/Pro chart depths from the same boundary `wants_pro_candle_depth` gates on, the ten paid signal-list controls from `SignalCriteria`'s own filter list, with their accepted timeframes read off the resolver that 400s the rest. Those controls live under `signals` and belong to `/v2/assets/signals*` — **not** to `/v2/assets/screener`, which takes `asset_class` alone and ignores them. Send a session and `caller` reports your tier too; without one it reads `anonymous`. An unreadable subscription answers `caller.state: \"unavailable\"` with `is_pro: null` rather than failing the document or reporting free — a client must treat that as \"keep what you had\".\n- **Config:** Per alert kind, `alerts.kinds[].timeframes[].evaluates_as` names the window the alert is **actually** evaluated over. Two timeframes sharing that value produce the same alert: RSI and smart-zone alerts fold everything outside the two warmed analysis windows onto `h1` (so a `d1` chip charges Pro and behaves as `h1`), and percent-change alerts fold nine timeframes onto the three change columns the asset row carries (`1h`, `24h`, `7d`) — which is also what the push copy is captioned with, so a chip can say what it measures. A kind that reads no timeframe publishes an empty list.\n- Nothing on any existing response changed shape. The document is served `private` (it carries `caller`), so it is cached by the calling client and never by a shared cache. Clients that cannot reach this route should fall back to their current behaviour.\n\n### v2.10.50\n- **Positions:** Position sharing is removed. `GET /v2/positions/public/{position_id}` — the unauthenticated read that served a position marked shareable — is gone, and with it the `visible` write on `PATCH /v2/positions/{position_id}` and the MCP `edit_position` tool, and the `visible_only` filter on `GET /v2/positions` and the MCP `get_positions` tool. No shipped client called any of them; the endpoint also served the owner's full `PositionExtended`, `notes` included, to anyone holding an id. The MCP `get_portfolio_summary` tool now summarizes every position: it filtered on `visible` — a column nothing ever set to true — so it had been answering over an empty book. The `visible` field stays on the wire, reading `false` on every position, because shipped mobile builds decode it non-nullably; it and the column go once those builds age out.\n\n### v2.10.48\n- **Balances / Connections:** Hyperliquid spot holdings resolve to their assets again — `USDT`, `USDE`, `PURR` and every other spot-only symbol were served unpriced and counted out of the portfolio total. The venue answers `spotMetaAndAssetCtxs` with one price context per spot index ever issued (717) but lists only the live markets (326, first gap at index 71), and the ingest paired the two by position, so every market past a gap took a stranger's price: HYPE/USDC read $0.0999, UBTC/USDC $0.00004. Ticker classification admits a coin only within ±10% of its tracked price, so it rejected the whole feed and Hyperliquid wrote **zero** spot coin tickers — leaving nothing for a spot balance to bind to. Contexts are now addressed by the venue's own spot index. Perp feeds, including every builder dex, were never affected. Balances re-bind on the next connection sync; no client change.\n\n### v2.10.47\n- **Analysis:** `market_type` is now a **preference, not a filter**. An asset no venue lists on the named market is served from the book it actually trades on, and the response reports what was served — so a perp series is never captioned as spot. This is what makes FX and several stocks answerable at all: every Hyperliquid HIP-3 contract is a perpetual, and clients (the web, on every request) pin `market_type=spot`. Two consequences on the wire, both value changes, no field added or removed: `market_type` reads `\"derivatives\"` for a perp-only asset (JPY, gold, the stocks only Hyperliquid lists), and `xau` — which answered \"insufficient data\" because no venue lists it spot — now resolves.\n- **Analysis:** `GET /v2/analysis/{id}/markets` and the MCP `get_asset_markets` tool list a stock's or currency's **derivatives** venues alongside its spot ones. Both previously served stocks and FX as spot-only, and the MCP tool answered a `market_type=derivatives` request with \"stocks and FX are spot-only\" — untrue since tokenized-equity perps were indexed, and badly untrue now.\n- **Balances:** A holding whose venue currency is a tokenized equity (Bitget's `rAMZN`, Gate's `AMZNX`) or a currency resolves to its asset instead of being served unpriced. The binding existed in the stock and FX ticker tables; the lookup read only the coin ones, so the holding was stored untracked and left out of the portfolio total. Symbol matching is now case-normalized on both sides, which is what a venue spelling one listing `rAMZN` on its ticker feed and `RAMZN` on its balance feed needs.\n- **Connections:** Hyperliquid's HIP-3 dex markets are indexed as the **perpetuals they are** — into the futures ticker tables, carrying the open interest and funding rate the venue publishes — and across **every** builder dex, not only `xyz`. Filing them as spot rows is why a position on any of them (`xyz:AMZN`, `hyna:BTC`) synced untracked with no asset and no price: position sync resolves a venue pair through the futures tables only. The dex list is read live from the venue, which went from one builder dex to ten in three days. Markets on symbols Flicker does not track (`vntl:OPENAI`, `km:USBOND`) stay untracked, correctly. Migration 0078 clears the mis-filed rows; Hyperliquid's real spot listings are untouched.\n- **Stocks / FX:** Price and volume for a stock or currency fall back to its derivatives venues when no venue lists it spot, instead of freezing at the last spot figure it ever had.\n\n### v2.10.46\n- **Positions / Balances:** The inline-untracked cutoff moves from 3.9.24 to **3.9.26**. It was set to a version no mobile build had the nullable-`asset` decoder in, so 3.9.24 and 3.9.25 read as capable and threw inside `Position.fromJson` on the first unpriced holding — losing the whole portfolio screen instead of one card. Every build that exists now gets the pre-merge split shape again; 3.9.26 is the release the decoder change lands in.\n\n### v2.10.45\n- **Positions / Balances:** Every open position and every balance is now served in one list, priced or not. A position whose venue instrument maps to no asset Flicker tracks — and, new here, a balance whose venue currency does — is listed beside the rest with `asset: null` and `null` on every figure derived from a mark (`current_price*`, `market_value*`, `unrealized_pnl*`, `daily_pnl*`, `weekly_pnl*`, `margin_ratio`, `distance_to_liquidation`, `ticker_last_updated`; on a balance, `balance_usd`, `available_usd`, `frozen_usd` and `percentage_of_total`). **This is a breaking shape change**: those fields were non-nullable. Never `0.0` in their place — an unpriced holding is not a worthless one. The separate `untracked_positions` list introduced in v2.10.39 is gone. `open_positions_count` and its long/short siblings count the unpriced entries; the money totals still exclude them, because there is no honest figure to fold in. Mobile builds below 3.9.24 keep the previous shape (their decoders throw on a `null` asset), served out of one scoped module that is deleted once those builds age out.\n- **Balances:** A holding whose venue symbol resolves to no asset Flicker tracks is no longer dropped at sync — it was the user's own money, absent from their portfolio with no trace. It is stored and served unpriced, named by a new `symbol` field (the venue's currency code, uppercased) that is present on **every** balance, tracked or not. Balances are now keyed on `(connection, symbol, account)` rather than on the asset, so an untracked holding reconciles like any other; the `coins` foreign key is dropped, which also unblocks stock and FX balances that used to fail on insert and take a connection's whole balance sync down with them (migration 0076).\n- **Trades / Orders:** The same gap on fills. A trade whose pair resolves to no asset is stored rather than skipped, and `Trade.asset_id`, its deprecated `coin_id` alias and `TradeOrder.asset`/`asset_id` are **nullable** on `/v1/trades` and `/v1/orders` (breaking, same cutoff and same legacy handling as above — those builds see the tracked fills only, which is exactly what they saw before). Each connection's fill cursor is rewound once on deploy so the fills that were dropped come back: it had advanced past them, and no forward sync would have asked again. Re-reading a window cannot duplicate a fill — the table is unique on `(external_id, user_connection_id)` and the sync upserts on it — but each venue only re-serves about 30 days, so fills older than that were never recoverable and are untouched (migration 0077).\n- **MCP:** `get_positions`, `get_portfolio_summary`, `get_balances` and `get_orders` render an unpriced row as itself: the venue pair or symbol names it, its mark and PnL read `unavailable` rather than `$0.00`, and the portfolio header counts them under \"Unpriced Positions\". The tools see every row, priced or not, on every client.\n\n### v2.10.44\n- **Positions:** `PositionExtended` gains `funding_since_open` (additive, nullable): the cumulative funding the position has paid since it opened, in the settlement currency, as the venue reports it (negative = received). Hyperliquid is the only venue publishing a per-position figure (`clearinghouseState` → `cumFunding.sinceOpen`, refreshed every sync cycle); every other venue's positions carry `null`, which means \"not reported\", never \"zero funding\". Web zod should add it as `.nullable()`; mobile is unaffected until its decoder opts in (migration 0075).\n- **Connections:** BloFin positions now carry the stop-loss and take-profit the user holds at the venue (`orders-tpsl-pending`), synced every cycle alongside the position — the third venue after OKX and Hyperliquid. An unset leg (`\"\"`/`\"-1\"`) is `null`, never `0.0`; `size: \"-1\"` maps to a whole-position order; in one-way (net) mode the closing side names the protected direction (a resting sell protects the long). The read refuses a possibly-truncated full page rather than clearing stops it did not see.\n- **Positions:** `GET /v2/positions/{position_id}/analysis` (and the MCP `position_analysis` tool) now assesses far more of a position that has no stop-loss or take-profit — the common case, since most venues' protective-order sync has not shipped and many users never set one. All additive, all descriptive:\n  - `excursion` — real max favorable / max adverse excursion since entry (MFE/MAE), measured from served candles plus the live mark, in % of entry and in ATR units, with `bars_since_peak` and `current_over_peak` (the fraction of the best move still retained; negative when underwater, `null` when price was never in favor). `null` when the direction or entry time is unknown, or when the served candle window does not reach back to the entry — a truncated extreme would understate itself while reading as data. The bar containing the entry is excluded (its extremes straddle the fill).\n  - `plan_geometry` — the position measured on the matched zone's published geometry instead of the user's orders: `unrealized_r_at_plan_stop` and `remaining_rr_on_plan` (plan stop → plan TP1, from mark). Explicitly hypothetical — the field names carry the anchor, and clients must not render these as the position's own R. Present whenever a zone matched, `null` legs on degenerate geometry.\n  - `nearest_levels` — nearest published support below and resistance above the mark, each with signed % distance and ATR distance. Direction-free, so present even on a `direction_unknown` futures row.\n  - `liquidation_risk.abs_distance_atr` — the liquidation gap in ATR units (`null` during ATR warm-up), plus a new `liquidation_within_noise` warning when it is under 1 ATR — same bound as `stop_within_noise`.\n  - `published_at_entry` — **what we published when you entered**: the zone emission that was live for this asset, timeframe and direction at the position's entry time (from the `zone_emissions` history, matched with the same tolerance rule as live zones but in the ATR of that moment), with the entry diffed against it (`entry_vs_published_pct`, `entry_inside_published_zone`, `unrealized_r_at_published_stop` — hypothetical, anchored on the *published* stop) and how that plan has since resolved (`outcome`: `pending`/`entered`/`tp_hit`/`stopped_out`/`time_stopped`/`expired`, plus `plan_realized_r` — the plan's own simulated net R after the flat 0.20% round-trip cost, never the user's P&L). `null` when the direction or entry time is unknown or no recorded emission matches — absence of the record, not evidence the user traded off-plan. Migration 0074 adds the lookup index.\n\n### v2.10.41\n- **Connections:** Hyperliquid positions are now read from every perp dex, not just the default one. Hyperliquid is not one perp venue but eleven — each HIP-3 builder-deployed dex (`xyz`, `hyna`, `mkts`, …) has its own order books, margin engine and collateral, and `clearinghouseState` reads one of them at a time. Only the default dex was being read, so an account holding tokenized-stock, index or FX perps synced as completely flat; one public wallet checked on 2026-08-19 had zero default-dex positions and sixteen across three builder dexes. Positions on a dex whose markets Flicker does not index are kept as untracked (no `asset`, no price or PnL), the same shape introduced in v2.10.39. Venue stop-loss / take-profit orders follow the same per-dex read.\n- **Connections:** Hyperliquid `pair` values are now the venue's own instrument name rather than a synthesized one — `BTC` (not `BTC-USD`), `xyz:AAPL` (not `AAPL-USD`), `@142` (not `BTC/USDC`) — on ticker listings and on positions, trades and orders. **This changes the string, not the type or the field**, so decoding is unaffected, but anything displaying or matching on a Hyperliquid pair will see the new form, and a client pinning `?pair=AAPL-USD` on `/v2/analysis` must send `?pair=xyz:AAPL`. The old values addressed nothing at the venue, which is why every candle request had to rebuild the real name from the asset symbol.\n- **Analysis:** Hyperliquid **spot** candle requests returned perpetual candles. The stored spot pair was a canonicalized `{base}/{quote}` the venue does not accept, so the fetch fell back to the bare coin symbol — which on Hyperliquid names the perp. Spot series now come from the spot market they name. Affects `market_type=spot` on `/v2/analysis` with `connection=hyperliquid`; perpetual, stock and FX series were never affected.\n\n### v2.10.39\n- **Positions:** A synced position whose venue instrument maps to no asset Flicker tracks is no longer dropped. It is kept and served in a new, additive `untracked_positions` list beside `positions` on `/v2/positions` and `/v1/connections/user` (and in the MCP `get_positions` / `get_portfolio_summary` output): pair, side, size, entry, margin, leverage, liquidation, stop/take-profit, dates. It has no `asset` and no price or PnL (there is no ticker to mark it against), is deliberately not in the main list (shipped mobile decoders require an `asset` there), and is not included in the portfolio totals. `GET /v2/positions/{position_id}/analysis` returns `422` for such a position, since there is nothing to assess it against.\n- **Positions / Trades / Orders:** The polymorphic asset column is now named for what it holds. `positions.coin_id` and `trades.coin_id` are `asset_id` in the database and the code; on the wire `Trade` and `TradeOrder` gain `asset_id` and keep `coin_id` as a **deprecated** duplicate (identical value) until web and mobile read `asset_id`; `?asset_ids=` on `/v2/positions` and `?asset_id=` on `/v1/trades` and `/v1/orders` are the canonical filters, with `coin_ids` / `coin_id` accepted as deprecated aliases. The MCP `get_positions`, `get_trades` and `get_orders` tools take `asset_id` (`coin_id` still accepted). Clients should move to `asset_id`; the aliases will be removed once both have.\n- **Positions:** A synced position may now be any asset the venue lists a perp for — coins, tokenized stocks and FX alike (e.g. OKX `SNDK-USDT-SWAP` → `sndk`). Previously position and trade sync resolved venue pairs against coin tickers only, so a stock or FX perp was silently absent from `/v1/connections/user`, `/v2/positions` and the trades list. Such positions carry `\"asset_class\": \"stocks\"` / `\"fx\"` on their `asset` (the same discriminator every asset endpoint already emits), are priced off that class's perp ticker, and `GET /v2/positions/{position_id}/analysis` runs them against the matching candle universe. `?coin_ids=` on `/v2/positions` (and the MCP `coin_id` filter) accepts any asset id.\n- **Positions:** `PATCH /v2/positions/{position_id}` (and the MCP `edit_position` tool) no longer accept `stop_loss` / `take_profit` — the recording surface shipped in v2.10.37 is withdrawn before any client adopted it. A hand-typed stop beside a synced position is a second source of truth the venue can silently contradict; the fields stay on the positions response (nullable) and are populated by venue TP/SL order sync, rolling out per exchange. Until a venue's sync ships, its positions carry `null`, which the analysis endpoint reports as `unrealized_r: null` (never 0) plus a `no_stop_recorded` warning.\n- **Positions:** `GET /v2/positions/{position_id}/analysis` answers `422` (with the reason) when the position's mark or entry is not a positive price — e.g. a delisted instrument with no quote — instead of `500`; the MCP `position_analysis` tool names the same cause instead of \"try again later\". Prices in the MCP assessment text keep their leading digits on sub-$0.0001 instruments instead of printing `0.0000`.\n- **Connections:** OKX positions now carry the stop-loss and take-profit the user holds at the venue (`orders-algo-pending`, `conditional` + `oco`), synced every cycle alongside the position itself. Several resting orders on one position fold to one stop and one target — a whole-position order beats a partial, then the newest wins. A venue reporting no protective order writes `null` (the stop was cancelled, so it must not linger); a failed protective-orders read leaves the previous values untouched rather than clearing a stop it could not confirm. Other venues remain `null` until their own sync lands. The read pages `orders-algo-pending` and refuses a truncated page (levels left untouched rather than cleared), recognises a hedge-mode partial TP/SL by order side against position side, and ignores paused algo orders; trailing stops (`move_order_stop`) are deliberately not read yet.\n- **Positions:** `PositionExtended` gains `protective_levels_synced_at` (additive, nullable): when a venue protective-orders read last wrote the row's stop/target. `null` means no such read has ever succeeded for this position — the venue's sync is not shipped, or it has not yet returned — so a `null` stop there is not evidence of no stop. `GET /v2/positions/{position_id}/analysis` uses it: an unsynced row gets a `protective_levels_not_synced` warning instead of `no_stop_recorded`. Also on the analysis response (unreleased, no client break): the stop is now judged against the mark, so a stop trailed to breakeven or into profit reads as `stop_placement.breakeven_or_better` (with `unrealized_r: null` — planned risk is undefined) rather than `stop_wrong_side`; a recorded stop that is not a positive price gets `stop_unusable`; `timing.bars_held` is class-aware and nullable (`bars_held_at_least` carries the floor) and `horizon_exceeded` is nullable; `invalidation.distance_pct` is `signed_distance_pct`, `liquidation_risk.distance_pct` is `abs_distance_pct` (never null); `entry_vs_plan_pct` is nullable; `stop_placement` gains `inside_opposing_zone`; `remaining_rr` is `null` once the mark is past the target.\n- **Positions:** Position sync now takes the fresh asset resolution on every cycle (`asset_id` re-points when a pair resolves to a different asset; an unresolvable pair keeps the old binding), so an asset re-id heals on the next sync instead of failing the portfolio read for that user.\n- **Pilot:** Spectate surfaces carry more per row, all additive. `/pilot/spectate/track-record`: every trade, curve point, best/worst trade and `by_asset` segment gains an `asset` (id, symbol, name, icon; absent when the instrument is not in the coin universe); trades gain `entry_price`, `exit_price`, `stop_loss` and `take_profit` (the levels `planned_rr` was derived from); curve points gain `return_on_margin_pct` and `cumulative_return_on_margin_pct` — a second, toggle-able series that is gross and equal-margin-weighted (the running numerator of `avg_return_on_margin_pct`), absent from the first point that lacks a figure onward; and `?before_trade=<index>` pages the per-trade list backwards in this same shape (`trades_truncated` now means \"older trades exist before this window\"). `/pilot/spectate`: open positions gain live `mfe_r` / `mae_r` (sampled at the bot's poll interval, like the closed record's), and the overview gains `last_active_at` — when the bot last completed a healthy cycle, so \"flat\" and \"stalled\" no longer render alike. Absent when unknown; a client must not read absence as stalled.\n\n### v2.10.37\n- **Positions:** Added `GET /v2/positions/{position_id}/analysis` — a factual assessment of an open position against the published analysis zones: thesis status (intact / invalidated / unknown), unrealized R and remaining R:R, how the actual entry and stop diverge from the published plan, stop placement (noise distance, inside the entry zone, beyond invalidation, inside an opposing zone), liquidation-before-stop, obstacles on the path to each target, and bars held vs the resolution horizon. Descriptive only — no action, suggested stop, or thesis confidence. `timeframe=h1` is free; other timeframes require Pro. Session auth only, like every positions route.\n- **Positions:** `PATCH /v2/positions/{position_id}` (and the MCP `edit_position` tool) can now record a `stop_loss` and `take_profit` on a position. Both are nullable prices the user maintains — venue sync neither reads nor overwrites them; over HTTP an explicit `null` clears a recorded level. Without a recorded stop the analysis endpoint reports `unrealized_r: null` (never 0) and a `no_stop_recorded` warning. `PositionExtended` now carries both fields.\n- **MCP:** Added the `position_analysis` tool — the identical assessment over the same shared implementation, rendered as text.\n\n### v2.10.35\n- **Assets:** Added `/v2/assets/favorites` — `GET` lists the caller's favorites across coins, stocks and FX (filtered and sorted with the same parameters as `/v2/assets`), `POST` favorites an asset by id, and `DELETE /v2/assets/favorites/{asset_id}` removes one. The v1 surface (`/v1/assets/coins/favorite`) is unchanged and still supported.\n- **Assets:** Favoriting an asset the caller has already favorited now succeeds instead of returning `500`. The same holds for unfavoriting one they do not have. Applies to both API versions.\n\n### v2.10.31\n- **API keys:** `x-api-key` now authenticates. A key created under `/v1/users/api_keys` identifies its owner on the read-only market-data endpoints (`/v2/analysis/*`, `/v1/assets/*`, `/v2/assets/*`, `/v2/insights`, `/v1/exchanges/*`), so a Pro or Elite subscriber integrating over the API gets their paid entitlements instead of the free tier. Keys deliberately reach nothing that mutates or reads account data — orders, positions, connections, notifications and user routes still require `x-session-token`.\n- **API keys:** Calls made with a key are metered against a daily allowance that resets at 00:00 UTC: 100/day free, 1,000/day Pro, 5,000/day Elite. `GET /v1/users/api_keys/credits` reports the live figures; exceeding the allowance returns `429`. Previously no call was ever counted and no allowance ever reset.\n- **API keys:** Keys can now be given an optional lifetime (`expires_in_days`, 1–365) at creation, up to 10 active keys per account. Deleting a key revokes it immediately (the audit record is retained); the listing's `is_active` now reflects whether the key actually works, so an expired key reads as inactive.\n- **API keys:** Sending an `x-api-key` header no longer exempts a request from the supported-client check unless the key is valid. Previously any value did.\n- **MCP:** The `x-api-key` authentication described in v2.3.1 and v2.10.5 was removed in v2.10.9 when `/mcp` moved to OAuth for every tool. `/mcp` accepts OAuth bearer tokens only; each request's token is now also checked against the session it is used with.\n- **Docs:** The OpenAPI document no longer declares a global `x-api-key` requirement on every path. Authentication is declared per operation: `session`, plus `api_key` on the market-data endpoints that accept one.\n- **Screener:** `timeframe` values other than `h1` and `m15` are now rejected with `400` instead of being silently served as `h1` (and, for free callers, charged a `402` for a parameter that was being ignored). The MCP `get_trading_signals` tool rejects the same values, spelled `1h` and `15m` as everywhere else on that surface.\n\n### v2.10.9\n- **Assets:** Added a new `Fx` (foreign-exchange) asset class. Major FX pairs (EUR, GBP, JPY, KRW) are now first-class assets — browsable in the screener, searchable, favoritable, and alertable — alongside coins and stocks. Filter any asset endpoint to them with `asset_class=fx` (or `all`, which now includes FX). Rates refresh every minute from Hyperliquid's `xyz` dex, complementing the hourly openexchangerates.org pull that still drives internal USD quote conversion.\n- **MCP:** The `/mcp` endpoint now requires OAuth authentication for **every** tool, including the previously-anonymous market, analysis, discovery, and insight tools. Unauthenticated requests receive a `401` carrying an RFC 9728 `WWW-Authenticate: Bearer resource_metadata=\"...\"` challenge (with an RFC 6750 `error` code when a token is present but rejected), so MCP clients discover the authorization server and start the login flow instead of silently staying connected-but-anonymous. Protected Resource Metadata is now served at both the bare and `/mcp`-path-suffixed `.well-known` URLs.\n- **Connections:** Removed the FreeDX exchange integration (market data, account sync, and affiliate support). Existing FreeDX connections, positions, balances, trades, and ticker data have been purged.\n- **Pilot:** The Pilot-spectate 30-day affiliate volume gate now also counts BingX and Gate trading volume, in addition to Bybit, BloFin, and OKX.\n\n### v2.10.5\n- **MCP:** Added tools for watchlist management (`add_favorite_asset`, `remove_favorite_asset`), account info (`get_account_info`), rolled-up order history (`get_orders`), editing a position's notes/visibility (`edit_position`), alerts (`get_alerts`, `create_price_alert`, `delete_alert`), notification preferences (`get_notification_preferences`, `update_notification_preferences`), the exchanges catalog (`list_exchanges`, `get_exchange_detail`), and Pilot bot spectate (`get_pilot_spectate`, `get_pilot_spectate_history`).\n- **MCP:** `x-api-key` authentication is now header-only — the query-parameter form (`?x-api-key=...`) has been removed, in line with the MCP spec's prohibition on passing access tokens in the URL. Existing header-based integrations are unaffected.\n- **Notifications:** Added a `pilot_trade` category: push notification sent to Pilot-spectate-eligible users when the Pilot bot opens a new trade. Enabled by default for eligible users; toggle via `PATCH /v1/users/notification_preferences` (`pilot_trade_notification_disabled`) or the MCP `update_notification_preferences` tool.\n\n### v2.8.1\n- **Analysis:** Trading zones now return at most two take-profit targets. The third target was empirically never reached across 113 zones tested on BTC, ETH, and SOL, so emitting it (and allocating 20% of position size to it) was dragging realized expectancy down.\n- **Analysis:** Position sizing for two-target zones rebalanced to 70% close at TP1 / 30% at TP2 (previously 60/40). TP1 hit rate is roughly 3× TP2 in measured data; the heavier front-load locks in the more reliable target while keeping a runner for the occasional extension.\n\n### v2.7.0\n- **Insights:** Added `GET /v2/insights/fear_greed` endpoint that returns top crypto assets ranked by their Fear & Greed score. Supports filtering by sentiment `zone` (`extreme_fear`/`fear`/`neutral`/`greed`/`extreme_greed`), `min_volume`, and `exclude_stables` (default `true`). Sortable by `volume`, `score`, or `change_24h` (`sort_dir` = `asc`/`desc`). `limit` defaults to 50, capped at 100.\n\n### v2.6.19\n- **Connections:** `POST /v1/connections/user` now returns clear, exchange-specific error messages when credentials are rejected (e.g. \"Binance: Invalid API-key, IP, or permissions for action. (code -2015)\") instead of opaque parser errors. Applies to Binance, Bybit, OKX, Blofin, and Hyperliquid. Missing credential fields now return \"Invalid credentials: ...\" and Hyperliquid wallet addresses are validated for format before the API call.\n\n### v2.6.4\n- **Analysis:** `GET /v2/analysis/{asset_id}/markets` now returns all tickers (spot and derivatives combined) when `market_type` is omitted.\n\n### v2.6.0\n- **Analysis:** Added `GET /v2/analysis/{asset_id}/signal` endpoint that returns a Buy/Hold/Sell recommendation with confidence score and weighted factor breakdown (trend, momentum, sentiment, breakout, risk, cycle phase). Results are cached for 1 hour.\n\n### v2.5.0\n- **Analysis:** Improved trading zone detection with better support/resistance identification across the full price history.\n- **Analysis:** Added order block and fair value gap detection for more accurate entry zones.\n- **Analysis:** Zones now use adaptive distance filtering based on market volatility instead of a fixed percentage.\n- **Analysis:** Improved confidence scoring with graduated indicator signals and volume-aware weighting.\n- **Analysis:** Deeper historical context for volatility, volume, momentum, and market structure calculations.\n- **Analysis:** Fixed breakout scorer SAR alignment and volatility-adjusted risk calculations.\n- **Analysis:** Added corrupted candle data validation.\n\n### v2.4.0\n- **Connections:** Added Bybit exchange integration with support for positions, balances, and trade history syncing.\n- **Trades:** Added `GET /v1/trades` endpoint for querying trade execution history with filters (`user_connection_id`, `pair`, `side`) and pagination (`limit`, `offset`).\n- **Trades:** Trade history syncs automatically during connection sync. First sync fetches up to 1 month of history; subsequent syncs are incremental.\n- **Positions:** Deprecated manual position creation, editing, and deletion (`/v2/positions/manual/*`). These endpoints now return a deprecated response.\n\n### v2.3.15\n- **Analysis:** Pro users now receive deeper analysis powered by more historical data.\n- **Analysis Overview:** Added localization support. Pass `x-locale` header to receive the AI-generated overview in your preferred language.\n- **Connections:** Added `api_key_guide_url` field to connection responses.\n- **Positions:** `GET /v2/positions/public/{id}` now returns `404 Not Found` for missing positions instead of `500`.\n- **Favorites:** New users automatically receive Bitcoin, Ethereum, and Solana as default favorites on signup.\n\n### v2.3.8\n- **Analysis:** Non-1H timeframes now require a Pro subscription on both `/v2/analysis/{asset_id}` and `/v2/analysis/{asset_id}/overview`. Returns `402 Payment Required` for free users.\n\n### v2.3.5\n- **Assets:** Added `GET /v2/assets/popular` endpoint that returns the most popular assets based on community favorites.\n\n### v2.3.1\n- **MCP:** Added API key authentication. Pass your key via the `x-api-key` query parameter (e.g. `?x-api-key=flk_...`) or HTTP header. Public tools (prices, search, analysis, sentiment, top movers) remain accessible without authentication.\n- **Notifications:** Basic account notification limit now returns `402 Payment Required` instead of `400 Bad Request`.\n\n### v2.2.0\n- **Balances:** Added balance tracking for all connected exchanges with per-asset USD valuations, percentage breakdowns, and totals.\n- **Balances:** Supported exchanges: Blofin, Binance, OKX, and Hyperliquid.\n\n### v2.1.1\n- **Promotions:** Improved error handling for exchange verification. Error responses now return cleaner, user-friendly messages.\n\n### v2.1.0\n- **Positions:** Added `daily_pnl`, `daily_pnl_usd`, `weekly_pnl`, and `weekly_pnl_usd` fields to both individual position responses and portfolio summary.\n\n### v2.0.0\n- Initial public API release with technical analysis, market zones, and AI-powered market insights.\n","termsOfService":"https://flicker.finance/terms","contact":{"name":"Onik","url":"https://flicker.finance","email":"os@flicker.finance"},"license":{"name":""},"version":"2.10.55"},"servers":[{"url":"/","description":"Production"}],"paths":{"/v1/exchanges":{"get":{"tags":["Exchanges"],"operationId":"list_exchanges","responses":{"200":{"description":"All exchanges with aggregated volume, unique asset counts, promotions, and per-user connection status","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"type":"object","required":["id","name","icon","connected","volume"],"properties":{"affiliate_url":{"type":["string","null"]},"api_connection_exists":{"type":["boolean","null"]},"api_key_guide_url":{"type":["string","null"]},"connected":{"type":"boolean"},"icon":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"promotion":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AffiliatePromotion"}]},"volume":{"$ref":"#/components/schemas/ExchangeVolume"},"website_url":{"type":["string","null"]}}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v1/exchanges/{exchange_id}":{"get":{"tags":["Exchanges"],"operationId":"get_exchange","parameters":[{"name":"exchange_id","in":"path","description":"Exchange connection id, e.g. \"bybit\"","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Single exchange with spot and futures tickers, open interest, and stats","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"allOf":[{"$ref":"#/components/schemas/ExchangeResponse"},{"type":"object","required":["spot_pairs","futures_pairs","futures_open_interest_usd","spot_tickers","futures_tickers"],"properties":{"futures_open_interest_usd":{"type":"number","format":"double"},"futures_pairs":{"type":"integer","minimum":0},"futures_tickers":{"type":"array","items":{"$ref":"#/components/schemas/CoinFuturesTicker"}},"spot_pairs":{"type":"integer","minimum":0},"spot_tickers":{"type":"array","items":{"$ref":"#/components/schemas/CoinTicker"}}}}]},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"404":{"description":"Exchange not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v1/news/":{"get":{"tags":["News"],"operationId":"get_news","parameters":[{"name":"limit","in":"query","description":"Number of news items to return (1–100, default 5).","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"_asset_id","in":"query","description":"Reserved — asset-scoped filtering is not yet implemented.","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Latest crypto news articles, sorted by publish date","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"type":"object","required":["id","source","title","description","url","date"],"properties":{"date":{"type":"string","format":"date-time"},"description":{"type":"string"},"icon":{"type":["string","null"]},"id":{"type":"string"},"source":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"}}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"500":{"description":"Failed to fetch news","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/notifications":{"get":{"tags":["Notifications"],"operationId":"get_notifications","responses":{"200":{"description":"The user's configured notifications (price/zone/breakout/etc alerts), each resolved with its live target data","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"type":"object","required":["id","kind","user_id","created_at","status","body"],"properties":{"body":{"$ref":"#/components/schemas/NotificationResponseBody"},"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"kind":{"$ref":"#/components/schemas/Kind"},"status":{"$ref":"#/components/schemas/Status"},"user_id":{"type":"string","format":"uuid"}}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["Notifications"],"operationId":"add_notification","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payload"}}},"required":true},"responses":{"200":{"description":"Notification created"},"400":{"description":"Invalid payload, e.g. unknown/stablecoin asset or an out-of-range target","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Basic account notification limit reached, or a sub-1h timeframe requires a Pro subscription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"410":{"description":"No active device registered to receive the alert","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/v1/notifications/config":{"get":{"tags":["Notifications"],"operationId":"config","responses":{"200":{"description":"The user's notification count and limit, reflecting their subscription tier","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["current","max","unlimited"],"properties":{"current":{"type":"integer","format":"int64","description":"Notifications currently configured by the user."},"max":{"type":"integer","format":"int64","description":"Maximum allowed; `i64::MAX` when `unlimited` is true."},"unlimited":{"type":"boolean"}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/v1/notifications/{notification_id}":{"delete":{"tags":["Notifications"],"operationId":"delete_notification","parameters":[{"name":"notification_id","in":"path","description":"Notification id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Notification deleted"},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"patch":{"tags":["Notifications"],"operationId":"update_notification","parameters":[{"name":"notification_id","in":"path","description":"Notification id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Payload"}}},"required":true},"responses":{"200":{"description":"Notification updated"},"400":{"description":"Invalid payload, e.g. unknown/stablecoin asset or an out-of-range target","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"A sub-1h timeframe requires a Pro subscription","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/v1/orders":{"get":{"tags":["Orders"],"operationId":"get_orders","parameters":[{"name":"user_connection_id","in":"query","description":"Restrict to a single connected account.","required":false,"schema":{"type":["string","null"],"format":"uuid"}},{"name":"connection_id","in":"query","description":"Filter by exchange (connection) id, e.g. `blofin` — spans every account the user has on that exchange.","required":false,"schema":{"type":["string","null"]}},{"name":"pair","in":"query","description":"Filter by trading pair (exchange-native symbol, e.g. `BTC-USDT`).","required":false,"schema":{"type":["string","null"]}},{"name":"asset_id","in":"query","description":"Filter by internal asset id (e.g. `bitcoin`, `sndk`) — matches the asset across every exchange and pair. `coin_id` is accepted as a deprecated alias.","required":false,"schema":{"type":["string","null"]}},{"name":"side","in":"query","description":"Filter by side (`buy` / `sell`).","required":false,"schema":{"type":["string","null"]}},{"name":"start","in":"query","description":"Only include fills executed at or after this instant (RFC 3339, e.g. `2024-01-01T00:00:00Z`).","required":false,"schema":{"type":["string","null"],"format":"date-time"}},{"name":"end","in":"query","description":"Only include fills executed at or before this instant (RFC 3339, e.g. `2024-02-01T00:00:00Z`).","required":false,"schema":{"type":["string","null"],"format":"date-time"}},{"name":"limit","in":"query","description":"Page size. Defaults to 50, clamped to 0..=200. On `/v1/orders` this pages over orders.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"offset","in":"query","description":"Rows to skip for pagination (trades on `/v1/trades`, orders on `/v1/orders`).","required":false,"schema":{"type":["integer","null"],"format":"int64"}}],"responses":{"200":{"description":"Trade history rolled up into orders: each order carries its aggregate (side, size-weighted avg price, summed size/fee/pnl) plus its constituent fills. Derived from trades; `limit`/`offset` page over orders.","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["orders","total"],"properties":{"orders":{"type":"array","items":{"$ref":"#/components/schemas/OrderExtended"},"description":"The page of orders (grouped fills), newest order first."},"total":{"type":"integer","format":"int64","description":"Total orders matching the filter, across all pages."}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/v1/promotions/available":{"get":{"tags":["Promotions"],"operationId":"get_available_promotions","responses":{"200":{"description":"All affiliate promotions the user has not yet claimed","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"type":"object","required":["id","connection_id","partner_name","title","description","promotional_value","bonus_description","referral_url","icon","theme","steps","validations"],"properties":{"bonus_description":{"type":"string"},"connection_id":{"$ref":"#/components/schemas/PromotionConnection"},"description":{"type":"string"},"icon":{"type":"string"},"id":{"type":"string"},"partner_name":{"type":"string"},"promotional_value":{"type":"string"},"referral_url":{"type":"string"},"steps":{"type":"array","items":{"$ref":"#/components/schemas/PromotionStep"}},"theme":{"$ref":"#/components/schemas/PromotionTheme"},"title":{"type":"string"},"validations":{"type":"array","items":{"$ref":"#/components/schemas/ValidationConfig"}}}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"500":{"description":"Failed to load promotions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/promotions/campaigns":{"get":{"tags":["Promotions"],"operationId":"get_active_campaigns","responses":{"200":{"description":"Live promo-video campaigns in random order (empty when none are live)","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"type":"object","description":"A single affiliate promo-video campaign served to mobile/web.\n\nOnly structural/dynamic data lives here — all human-facing copy (eyebrow,\ntitle, tagline, perks, CTA, etc.) is resolved client-side from the app's\nlocalized bundles, keyed by [`PromoVideoCampaign::id`]. That keeps the seven\nsupported locales in the app rather than duplicated on the backend.\n\nThis is the wire contract, deliberately narrower than the `promo_campaigns`\nrow it is built from: scheduling/bookkeeping columns (`registration_open`,\n`is_enabled`, timestamps) stay server-side.","required":["id","url","video_url","web_banner_video_position","registration_close","exchange_icon_url","exchange_icon_alt"],"properties":{"exchange_icon_alt":{"type":"string"},"exchange_icon_url":{"type":"string","description":"Exchange logo shown in the eyebrow (CDN PNG) + its accessibility label."},"id":{"type":"string","description":"Stable campaign id (e.g. `race_to_1m`); also the analytics `campaign` property\nand the key the client uses to resolve localized copy."},"prize_amount":{"type":["string","null"]},"prize_banner_label":{"type":["string","null"],"description":"Headline prize, e.g. `1,000,000 USDT` (banner) and `1,000,000` (takeover).\nAbsent for non-prize campaigns (e.g. a score-difference trading activity);\nclients omit the prize block when these are missing."},"prize_icon_url":{"type":["string","null"]},"registration_close":{"type":"string","format":"date-time","description":"Registration close (ISO-8601 UTC). Drives the client countdown and the\nserver-side active filter — clients never receive an expired campaign."},"url":{"type":"string","description":"Affiliate landing page, channel/referral-tagged to Flicker."},"video_url":{"type":"string","description":"CDN-hosted cinematic MP4 (banner + takeover background)."},"web_banner_video_position":{"type":"string","description":"CSS `object-position` (e.g. `center 80%`) for cover-cropping the banner\nvideo on web. Web-layout-specific; mobile may ignore it."}}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"500":{"description":"Failed to load campaigns — distinct from an empty 200, which means nothing is live","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/promotions/random":{"get":{"tags":["Promotions"],"operationId":"get_random_promotion","responses":{"200":{"description":"A randomly-selected available affiliate promotion","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["id","connection_id","partner_name","title","description","promotional_value","bonus_description","referral_url","icon","theme","steps","validations"],"properties":{"bonus_description":{"type":"string"},"connection_id":{"$ref":"#/components/schemas/PromotionConnection"},"description":{"type":"string"},"icon":{"type":"string"},"id":{"type":"string"},"partner_name":{"type":"string"},"promotional_value":{"type":"string"},"referral_url":{"type":"string"},"steps":{"type":"array","items":{"$ref":"#/components/schemas/PromotionStep"}},"theme":{"$ref":"#/components/schemas/PromotionTheme"},"title":{"type":"string"},"validations":{"type":"array","items":{"$ref":"#/components/schemas/ValidationConfig"}}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"404":{"description":"No available promotions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Failed to select a promotion","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/promotions/volume":{"get":{"tags":["Promotions"],"operationId":"get_promotion_volume","responses":{"200":{"description":"Per-exchange + total trailing-30d affiliate volume, threshold, and current affiliate-earned grant status","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","description":"A user's affiliate-volume standing plus their current affiliate-earned (`Elite`) grant.","required":["per_exchange","total_30d_usd","threshold_usd","qualifies","granted_via_promotion"],"properties":{"granted_via_promotion":{"type":"boolean","description":"Whether the user currently holds a valid affiliate-earned `Elite` grant."},"per_exchange":{"type":"array","items":{"$ref":"#/components/schemas/PromotionVolumeExchange"}},"promotion_expires_at":{"type":["string","null"],"format":"date-time","description":"When that grant expires, if any."},"qualifies":{"type":"boolean","description":"`has an affiliate connection && total_30d_usd >= threshold_usd`."},"threshold_usd":{"type":"number","format":"double"},"total_30d_usd":{"type":"number","format":"double"}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"500":{"description":"Failed to compute affiliate volume","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/v1/promotions/{connection_id}":{"get":{"tags":["Promotions"],"operationId":"get_exact_promotion","parameters":[{"name":"connection_id","in":"path","description":"Exchange/connection identifier (e.g. okx, bingx)","required":true,"schema":{"$ref":"#/components/schemas/PromotionConnection"}}],"responses":{"200":{"description":"The affiliate promotion for this connection","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["id","connection_id","partner_name","title","description","promotional_value","bonus_description","referral_url","icon","theme","steps","validations"],"properties":{"bonus_description":{"type":"string"},"connection_id":{"$ref":"#/components/schemas/PromotionConnection"},"description":{"type":"string"},"icon":{"type":"string"},"id":{"type":"string"},"partner_name":{"type":"string"},"promotional_value":{"type":"string"},"referral_url":{"type":"string"},"steps":{"type":"array","items":{"$ref":"#/components/schemas/PromotionStep"}},"theme":{"$ref":"#/components/schemas/PromotionTheme"},"title":{"type":"string"},"validations":{"type":"array","items":{"$ref":"#/components/schemas/ValidationConfig"}}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"403":{"description":"User has already claimed this promotion","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No promotion exists for this connection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/promotions/{connection_id}/sign_up":{"get":{"tags":["Promotions"],"operationId":"get_promotion_sign_up_info","parameters":[{"name":"connection_id","in":"path","description":"Exchange/connection identifier (e.g. okx, bingx)","required":true,"schema":{"$ref":"#/components/schemas/PromotionConnection"}}],"responses":{"200":{"description":"Whether the sign-up form should be shown for this connection","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["show"],"properties":{"show":{"type":"boolean"}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"500":{"description":"Failed to load the user's promotion state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["Promotions"],"operationId":"check_promotion_sign_up","parameters":[{"name":"connection_id","in":"path","description":"Exchange/connection identifier (e.g. okx, bingx)","required":true,"schema":{"$ref":"#/components/schemas/PromotionConnection"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromotionSignUpWithUID"}}},"required":true},"responses":{"200":{"description":"Sign-up verified; 30 days of Pro granted"},"400":{"description":"Invalid payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Account not invited, already claimed, or UID already used","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/v1/trades":{"get":{"tags":["Trades"],"operationId":"get_trades","parameters":[{"name":"user_connection_id","in":"query","description":"Restrict to a single connected account.","required":false,"schema":{"type":["string","null"],"format":"uuid"}},{"name":"connection_id","in":"query","description":"Filter by exchange (connection) id, e.g. `blofin` — spans every account the user has on that exchange.","required":false,"schema":{"type":["string","null"]}},{"name":"pair","in":"query","description":"Filter by trading pair (exchange-native symbol, e.g. `BTC-USDT`).","required":false,"schema":{"type":["string","null"]}},{"name":"asset_id","in":"query","description":"Filter by internal asset id (e.g. `bitcoin`, `sndk`) — matches the asset across every exchange and pair. `coin_id` is accepted as a deprecated alias.","required":false,"schema":{"type":["string","null"]}},{"name":"side","in":"query","description":"Filter by side (`buy` / `sell`).","required":false,"schema":{"type":["string","null"]}},{"name":"start","in":"query","description":"Only include fills executed at or after this instant (RFC 3339, e.g. `2024-01-01T00:00:00Z`).","required":false,"schema":{"type":["string","null"],"format":"date-time"}},{"name":"end","in":"query","description":"Only include fills executed at or before this instant (RFC 3339, e.g. `2024-02-01T00:00:00Z`).","required":false,"schema":{"type":["string","null"],"format":"date-time"}},{"name":"limit","in":"query","description":"Page size. Defaults to 50, clamped to 0..=200. On `/v1/orders` this pages over orders.","required":false,"schema":{"type":["integer","null"],"format":"int64"}},{"name":"offset","in":"query","description":"Rows to skip for pagination (trades on `/v1/trades`, orders on `/v1/orders`).","required":false,"schema":{"type":["integer","null"],"format":"int64"}}],"responses":{"200":{"description":"Paginated trade (fill) history for the authenticated user, newest first","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["trades","total"],"properties":{"total":{"type":"integer","format":"int64","description":"Total trades matching the filter, across all pages."},"trades":{"type":"array","items":{"$ref":"#/components/schemas/Trade"},"description":"The page of trades (fills), newest first."}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Internal error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/v1/users/api_keys":{"get":{"tags":["API Keys"],"operationId":"list_api_keys","responses":{"200":{"description":"The account's active keys, newest first. `is_active` reports whether the key still authenticates (revoked and expired keys do not).","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"type":"object","required":["id","name","prefix","created_at","is_active"],"properties":{"created_at":{"type":"integer","format":"int64"},"expires_at":{"type":["integer","null"],"format":"int64"},"id":{"type":"string"},"is_active":{"type":"boolean"},"name":{"type":"string"},"prefix":{"type":"string"}}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["API Keys"],"operationId":"create_api_key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApiKeyRequest"}}},"required":true},"responses":{"200":{"description":"The new key. `api_key` is the only time the secret is returned — it is stored hashed and cannot be shown again.","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["api_key","id","name","prefix"],"properties":{"api_key":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"prefix":{"type":"string"}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"400":{"description":"Invalid name or lifetime, or the account already holds the maximum number of active keys","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/v1/users/api_keys/credits":{"get":{"tags":["API Keys"],"operationId":"get_credits","responses":{"200":{"description":"API calls used against today's allowance, and the allowance itself. The allowance follows the account's plan and resets at 00:00 UTC; calls over it are answered 429.","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["credits_used","credits_cap"],"properties":{"credits_cap":{"type":"integer","format":"int32"},"credits_used":{"type":"integer","format":"int32"}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"503":{"description":"The plan could not be read","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/v1/users/api_keys/{key_id}":{"delete":{"tags":["API Keys"],"operationId":"delete_api_key","parameters":[{"name":"key_id","in":"path","description":"The key's id, as returned by create/list — never the secret itself","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Revoked. The key stops authenticating immediately and leaves the listing; the record is retained for audit."},"401":{"description":"Missing or invalid session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No such key on this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/v2/analysis/{asset_id}":{"get":{"tags":["Analysis"],"operationId":"get_analysis","parameters":[{"name":"asset_id","in":"path","description":"Asset identifier (e.g., bitcoin, ethereum)","required":true,"schema":{"type":"string"}},{"name":"pair","in":"query","required":false,"schema":{"type":["string","null"]},"example":"USDT"},{"name":"connection","in":"query","required":false,"schema":{"type":["string","null"]},"example":"binance"},{"name":"market_type","in":"query","required":false,"schema":{"type":["string","null"]},"example":"spot"},{"name":"timeframe","in":"query","description":"Read through [`Self::timeframe`], never directly — otherwise the default lands in some call\npaths and not others.","required":false,"schema":{"type":["string","null"]},"example":"d1"},{"name":"skip_cache","in":"query","required":false,"schema":{"type":["boolean","null"]}},{"name":"candle_limit","in":"query","description":"How many candles of chart history to serve. Pro/Elite only — a free\ncaller is served the per-class default regardless (and the HTTP layer\nanswers 402 so the client can prompt an upgrade). Rounded up to the next\n500 and capped at [`crate::candles::main::CandlesModule::MAX_CANDLE_LIMIT`];\nvalues below the per-class analysis window are raised to it. Deepening the chart never\nchanges the analysis itself — zones, indexes and context are always\ncomputed from the same per-class window.\n\nNo `minimum`/`maximum` in the schema on purpose: out-of-range values are\nsilently floored/clamped by `resolve_candle_limit`, not rejected, and\ndocumenting a range that is never enforced tells a generated client the\nserver will 400 when it will quietly rewrite instead.","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0},"example":5000},{"name":"start_time","in":"query","description":"Start of the chart window, epoch **milliseconds**, inclusive. Pro/Elite,\nsame as `candle_limit` — and interchangeable with it: a `start_time`\nfurther back than the per-class window deepens the fetch exactly as a\n`candle_limit` would. When both are given the deeper one wins.","required":false,"schema":{"type":["integer","null"],"format":"int64"},"example":1735689600000},{"name":"end_time","in":"query","description":"End of the chart window, epoch **milliseconds**, inclusive. Trims the\nseries a caller is served and nothing else — see [`CandleWindow`]. Omit\nfor a series ending at the live edge, which is what a zoom-out wants; a\npast `end_time` is for panning back through history.","required":false,"schema":{"type":["integer","null"],"format":"int64"},"example":1738368000000}],"responses":{"200":{"description":"Technical analysis data. `candles` carries 500 bars for free callers; Pro/Elite get the per-class default (2000 coins, 1000 stocks/FX), or up to 5000 via `candle_limit` / a further-back `start_time`, trimmed to `[start_time, end_time]` when either is given. Zones, indexes, context and the signal always describe the live market: they are computed from the per-class trailing window and never vary with `candle_limit` or the chart window, so a panned chart still draws today's levels. The documented shape is what every client gets except `ios`/`android` builds older than `MIN_NULLABLE_SESSION_VERSION`, which receive `context.session_strength` / `context.time_based_bias` in their pre-2026-08-04 shape because their decoders throw on the nullable one — see `controllers::analysis::legacy_session`.","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["asset_id","timeframe","pair","market_type","candles","zones","context","indexes","breakout","fear_greed","overview","created_at","hit_from_cache"],"properties":{"asset_id":{"type":"string"},"breakout":{"$ref":"#/components/schemas/BreakoutScore"},"candles":{"type":"array","items":{"type":"object"},"description":"Shared with the candle store rather than copied: a cache hit clones a pointer, not the\nseries behind it, and one instrument's bars are one allocation however many analyses\nreference them. Serializes exactly as the `Vec` it replaced."},"connection":{"type":["object","null"]},"context":{"$ref":"#/components/schemas/MarketContext"},"created_at":{"type":"integer","format":"int64"},"fear_greed":{"$ref":"#/components/schemas/FearGreedIndex"},"hit_from_cache":{"type":"boolean"},"indexes":{"$ref":"#/components/schemas/Indexes"},"market_type":{"type":"string"},"overview":{"type":"string"},"pair":{"type":"string"},"timeframe":{"type":"string"},"zones":{"$ref":"#/components/schemas/AnalysisZones"}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"400":{"description":"Invalid request parameters — including an inverted/negative `start_time`/`end_time`, or a window this instrument has no candles in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Pro subscription required — for any timeframe other than the default (the one served when `timeframe` is omitted; the response echoes it), or for chart history deeper than the free 500 bars (via `candle_limit` or `start_time`). On-chain assets (`asset_class: external`) are free at every timeframe their source serves, so only the depth half applies to them. `code` names which of the two refused: 1004 (`AnalysisTimeframeRestricted`) or 1005 (`AnalysisHistoryDepthRestricted`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Asset not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/analysis/{asset_id}/markets":{"get":{"tags":["Analysis"],"operationId":"get_markets","parameters":[{"name":"asset_id","in":"path","description":"Asset identifier","required":true,"schema":{"type":"string"}},{"name":"market_type","in":"query","required":false,"schema":{"type":["string","null"]},"example":"spot"}],"responses":{"200":{"description":"Market tickers sorted by volume. Returns spot or derivatives based on market_type, or all tickers combined when market_type is omitted.","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/CoinTicker"},{"type":"object","required":["connection_id","connection_name","connection_icon"],"properties":{"connection_affiliate_url":{"type":["string","null"]},"connection_icon":{"type":"string"},"connection_id":{"type":"string"},"connection_name":{"type":"string"}}}]}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/analysis/{asset_id}/overview":{"get":{"tags":["Analysis"],"operationId":"get_overview","parameters":[{"name":"asset_id","in":"path","description":"Asset identifier","required":true,"schema":{"type":"string"}},{"name":"pair","in":"query","required":false,"schema":{"type":["string","null"]},"example":"USDT"},{"name":"connection","in":"query","required":false,"schema":{"type":["string","null"]},"example":"binance"},{"name":"market_type","in":"query","required":false,"schema":{"type":["string","null"]},"example":"spot"},{"name":"timeframe","in":"query","description":"Read through [`Self::timeframe`], never directly — otherwise the default lands in some call\npaths and not others.","required":false,"schema":{"type":["string","null"]},"example":"d1"},{"name":"skip_cache","in":"query","required":false,"schema":{"type":["boolean","null"]}},{"name":"candle_limit","in":"query","description":"How many candles of chart history to serve. Pro/Elite only — a free\ncaller is served the per-class default regardless (and the HTTP layer\nanswers 402 so the client can prompt an upgrade). Rounded up to the next\n500 and capped at [`crate::candles::main::CandlesModule::MAX_CANDLE_LIMIT`];\nvalues below the per-class analysis window are raised to it. Deepening the chart never\nchanges the analysis itself — zones, indexes and context are always\ncomputed from the same per-class window.\n\nNo `minimum`/`maximum` in the schema on purpose: out-of-range values are\nsilently floored/clamped by `resolve_candle_limit`, not rejected, and\ndocumenting a range that is never enforced tells a generated client the\nserver will 400 when it will quietly rewrite instead.","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0},"example":5000},{"name":"start_time","in":"query","description":"Start of the chart window, epoch **milliseconds**, inclusive. Pro/Elite,\nsame as `candle_limit` — and interchangeable with it: a `start_time`\nfurther back than the per-class window deepens the fetch exactly as a\n`candle_limit` would. When both are given the deeper one wins.","required":false,"schema":{"type":["integer","null"],"format":"int64"},"example":1735689600000},{"name":"end_time","in":"query","description":"End of the chart window, epoch **milliseconds**, inclusive. Trims the\nseries a caller is served and nothing else — see [`CandleWindow`]. Omit\nfor a series ending at the live edge, which is what a zoom-out wants; a\npast `end_time` is for panning back through history.","required":false,"schema":{"type":["integer","null"],"format":"int64"},"example":1738368000000}],"responses":{"200":{"description":"AI-generated market overview. The chart-window params (candle_limit, start_time, end_time) are NOT honoured on this route — the overview is written from the fixed per-class analysis window. They are still validated, so a malformed window is a 400.","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"string"},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Pro subscription required for any timeframe other than the default (the one served when `timeframe` is omitted). On-chain assets (`asset_class: external`) are free at every timeframe their source serves. `code` is 1004 (`AnalysisTimeframeRestricted`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Asset not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/analysis/{asset_id}/signal":{"get":{"tags":["Analysis"],"operationId":"get_signal","parameters":[{"name":"asset_id","in":"path","description":"Asset identifier (e.g., bitcoin, ethereum)","required":true,"schema":{"type":"string"}},{"name":"pair","in":"query","required":false,"schema":{"type":["string","null"]},"example":"USDT"},{"name":"connection","in":"query","required":false,"schema":{"type":["string","null"]},"example":"binance"},{"name":"market_type","in":"query","required":false,"schema":{"type":["string","null"]},"example":"spot"},{"name":"timeframe","in":"query","description":"Read through [`Self::timeframe`], never directly — otherwise the default lands in some call\npaths and not others.","required":false,"schema":{"type":["string","null"]},"example":"d1"},{"name":"skip_cache","in":"query","required":false,"schema":{"type":["boolean","null"]}},{"name":"candle_limit","in":"query","description":"How many candles of chart history to serve. Pro/Elite only — a free\ncaller is served the per-class default regardless (and the HTTP layer\nanswers 402 so the client can prompt an upgrade). Rounded up to the next\n500 and capped at [`crate::candles::main::CandlesModule::MAX_CANDLE_LIMIT`];\nvalues below the per-class analysis window are raised to it. Deepening the chart never\nchanges the analysis itself — zones, indexes and context are always\ncomputed from the same per-class window.\n\nNo `minimum`/`maximum` in the schema on purpose: out-of-range values are\nsilently floored/clamped by `resolve_candle_limit`, not rejected, and\ndocumenting a range that is never enforced tells a generated client the\nserver will 400 when it will quietly rewrite instead.","required":false,"schema":{"type":["integer","null"],"format":"int64","minimum":0},"example":5000},{"name":"start_time","in":"query","description":"Start of the chart window, epoch **milliseconds**, inclusive. Pro/Elite,\nsame as `candle_limit` — and interchangeable with it: a `start_time`\nfurther back than the per-class window deepens the fetch exactly as a\n`candle_limit` would. When both are given the deeper one wins.","required":false,"schema":{"type":["integer","null"],"format":"int64"},"example":1735689600000},{"name":"end_time","in":"query","description":"End of the chart window, epoch **milliseconds**, inclusive. Trims the\nseries a caller is served and nothing else — see [`CandleWindow`]. Omit\nfor a series ending at the live edge, which is what a zoom-out wants; a\npast `end_time` is for panning back through history.","required":false,"schema":{"type":["integer","null"],"format":"int64"},"example":1738368000000}],"responses":{"200":{"description":"Buy/Hold/Sell signal with weighted factors. `timeframe` echoes the window the signal was computed on — caption from that, not from the value sent, since a request omitting `timeframe` is served the backend's default. The chart-window params (candle_limit, start_time, end_time) are NOT honoured on this route — the signal is derived from the fixed per-class analysis window. They are still validated, so a malformed window is a 400.","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["signal","confidence","timeframe","factors"],"properties":{"confidence":{"type":"number","format":"double"},"factors":{"type":"array","items":{"$ref":"#/components/schemas/SignalFactor"}},"signal":{"$ref":"#/components/schemas/Signal"},"timeframe":{"type":"string","description":"The timeframe this signal was computed on, lowercase (`h1`, `m15`).\n\nServed so a client captions the reading with what produced it instead of a literal of its\nown: the timeframe a request that named none is served is the backend's to choose\n(`AnalysisParams::timeframe`), and a client-side `\"1H\"` beside it is a caption nothing can\ncorrect the day that choice moves."}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"400":{"description":"Invalid request parameters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Pro subscription required for any timeframe other than the default (the one served when `timeframe` is omitted). On-chain assets (`asset_class: external`) are free at every timeframe their source serves. `code` is 1004 (`AnalysisTimeframeRestricted`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Asset not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/assets/":{"get":{"tags":["Assets"],"operationId":"get_assets","parameters":[{"name":"search","in":"query","description":"Free-text search across name / symbol / id. A valid on-chain address triggers external-token lookup.","required":false,"schema":{"type":["string","null"]}},{"name":"limit","in":"query","description":"Page size.","required":false,"schema":{"type":["integer","null"],"minimum":0}},{"name":"skip","in":"query","description":"Pagination offset.","required":false,"schema":{"type":["integer","null"],"minimum":0}},{"name":"sort_by","in":"query","description":"Sort key (`price`, `change1h`, `change24h`, `change7d`, `name`, `symbol`, `volume`, `favorites`, `watched`).","required":false,"schema":{"type":["string","null"]},"example":"volume"},{"name":"sort_dir","in":"query","description":"Sort direction (`asc` or `desc`).","required":false,"schema":{"type":["string","null"]},"example":"desc"},{"name":"exclude_stables","in":"query","description":"Exclude stablecoins from results.","required":false,"schema":{"type":["boolean","null"]}},{"name":"asset_class","in":"query","description":"Restrict universe to `coins`, `stocks` or `fx`; `all` or absent is every class.","required":false,"schema":{"type":["string","null"]},"example":"coins"},{"name":"min_price","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_price","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_volume","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_volume","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_change_1h","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_change_1h","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_change_24h","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_change_24h","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_change_7d","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_change_7d","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_watch_count","in":"query","description":"Minimum watch count (asset-detail views since the last daily reset).","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_watch_count","in":"query","description":"Maximum watch count (asset-detail views since the last daily reset).","required":false,"schema":{"type":["number","null"],"format":"double"}}],"responses":{"200":{"description":"Filtered, sorted, paginated assets. When `search` is a valid on-chain address, returns matching external tokens.","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Asset"},{"type":"object","required":["asset_class","icon","dominant_color"],"properties":{"asset_class":{"$ref":"#/components/schemas/AssetClass","description":"Explicit class discriminator (`coins` / `stocks` / `fx` / `external`) — the\nflattened asset payload has no tag of its own, and clients shouldn't have to\ninfer the class from which fields happen to be present."},"dominant_color":{"type":"string"},"icon":{"type":"string"},"is_favorite":{"type":["boolean","null"]}}}]}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/assets/exact/{asset_id}":{"get":{"tags":["Assets"],"operationId":"get_exact_asset","parameters":[{"name":"asset_id","in":"path","description":"Asset identifier (e.g., `bitcoin`, `AAPL`, or an external-token id)","required":true,"schema":{"type":"string"}},{"name":"show_similar_assets","in":"query","description":"Include similar-priced assets in the response.","required":false,"schema":{"type":["boolean","null"]}}],"responses":{"200":{"description":"Asset detail, optionally with similar-priced assets","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["asset"],"properties":{"asset":{"$ref":"#/components/schemas/AssetExtended"},"similar_assets":{"type":["array","null"],"items":{"$ref":"#/components/schemas/AssetExtended"}}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"404":{"description":"Asset not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/assets/favorites":{"get":{"tags":["Assets"],"operationId":"get_favorites","parameters":[{"name":"search","in":"query","description":"Free-text search across name / symbol / id. A valid on-chain address triggers external-token lookup.","required":false,"schema":{"type":["string","null"]}},{"name":"limit","in":"query","description":"Page size.","required":false,"schema":{"type":["integer","null"],"minimum":0}},{"name":"skip","in":"query","description":"Pagination offset.","required":false,"schema":{"type":["integer","null"],"minimum":0}},{"name":"sort_by","in":"query","description":"Sort key (`price`, `change1h`, `change24h`, `change7d`, `name`, `symbol`, `volume`, `favorites`, `watched`).","required":false,"schema":{"type":["string","null"]},"example":"volume"},{"name":"sort_dir","in":"query","description":"Sort direction (`asc` or `desc`).","required":false,"schema":{"type":["string","null"]},"example":"desc"},{"name":"exclude_stables","in":"query","description":"Exclude stablecoins from results.","required":false,"schema":{"type":["boolean","null"]}},{"name":"asset_class","in":"query","description":"Restrict universe to `coins`, `stocks` or `fx`; `all` or absent is every class.","required":false,"schema":{"type":["string","null"]},"example":"coins"},{"name":"min_price","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_price","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_volume","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_volume","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_change_1h","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_change_1h","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_change_24h","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_change_24h","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_change_7d","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_change_7d","in":"query","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_watch_count","in":"query","description":"Minimum watch count (asset-detail views since the last daily reset).","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_watch_count","in":"query","description":"Maximum watch count (asset-detail views since the last daily reset).","required":false,"schema":{"type":["number","null"],"format":"double"}}],"responses":{"200":{"description":"The caller's favorites across every asset class, filtered and sorted like `/v2/assets`","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Asset"},{"type":"object","required":["asset_class","icon","dominant_color"],"properties":{"asset_class":{"$ref":"#/components/schemas/AssetClass","description":"Explicit class discriminator (`coins` / `stocks` / `fx` / `external`) — the\nflattened asset payload has no tag of its own, and clients shouldn't have to\ninfer the class from which fields happen to be present."},"dominant_color":{"type":"string"},"icon":{"type":"string"},"is_favorite":{"type":["boolean","null"]}}}]}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"401":{"description":"Session required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]},"post":{"tags":["Assets"],"operationId":"add_favorite","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FavoriteRequestPayload"}}},"required":true},"responses":{"200":{"description":"Favorited. Idempotent — re-favoriting an asset the caller already has succeeds"},"401":{"description":"Session required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Free-tier favorites limit reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"No coin, stock, or currency has this id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/v2/assets/favorites/{asset_id}":{"delete":{"tags":["Assets"],"operationId":"delete_favorite","parameters":[{"name":"asset_id","in":"path","description":"Asset identifier (e.g. `bitcoin`, `AAPL`, `eur`)","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Unfavorited. Idempotent — an asset the caller had not favorited succeeds"},"401":{"description":"Session required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}},"/v2/assets/popular":{"get":{"tags":["Assets"],"operationId":"get_popular","parameters":[{"name":"asset_class","in":"query","description":"Restrict to a single asset class (`coins`, `stocks`, `fx`; `all` or absent = every class).","required":false,"schema":{"type":["string","null"]},"example":"coins"}],"responses":{"200":{"description":"Popular assets by favorite count, with recently-added boosted to the top","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Asset"},{"type":"object","required":["asset_class","icon","dominant_color"],"properties":{"asset_class":{"$ref":"#/components/schemas/AssetClass","description":"Explicit class discriminator (`coins` / `stocks` / `fx` / `external`) — the\nflattened asset payload has no tag of its own, and clients shouldn't have to\ninfer the class from which fields happen to be present."},"dominant_color":{"type":"string"},"icon":{"type":"string"},"is_favorite":{"type":["boolean","null"]}}}]}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/assets/screener":{"get":{"tags":["Assets"],"operationId":"get_screener","parameters":[{"name":"asset_class","in":"query","description":"Restrict to a single asset class (`coins`, `stocks`, `fx`; `all` or absent = every class).","required":false,"schema":{"type":["string","null"]},"example":"coins"}],"responses":{"200":{"description":"Top 5 gainers, losers, and breakout candidates","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["gainers","losers","breakouts"],"properties":{"breakouts":{"type":"array","items":{"$ref":"#/components/schemas/AssetExtended"}},"gainers":{"type":"array","items":{"$ref":"#/components/schemas/AssetExtended"}},"losers":{"type":"array","items":{"$ref":"#/components/schemas/AssetExtended"}}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/assets/signals/":{"get":{"tags":["Assets"],"operationId":"get_in_zone","parameters":[{"name":"min_confidence","in":"query","description":"Minimum zone confidence (0.0–1.0). Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"exchanges","in":"query","description":"Comma-separated exchange ids (e.g. `binance,bybit`). Pro filter.","required":false,"schema":{"type":["string","null"]},"example":"binance,bybit"},{"name":"timeframe","in":"query","description":"Timeframe (`h1` default; `m15` available to Pro).","required":false,"schema":{"type":["string","null"]},"example":"d1"},{"name":"min_volume","in":"query","description":"Minimum 24h volume in USD. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_volume","in":"query","description":"Maximum 24h volume in USD. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_risk_reward","in":"query","description":"Minimum risk/reward ratio for the zone. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_distance","in":"query","description":"Maximum distance from current price to the zone, percent. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"zone_types","in":"query","description":"Comma-separated zone types (`buy`,`sell`,`accumulation`,`distribution`).","required":false,"schema":{"type":["string","null"]},"example":"buy,accumulation"},{"name":"direction","in":"query","description":"Breakout direction filter (`bullish`, `bearish`, `neutral`).","required":false,"schema":{"type":["string","null"]}},{"name":"min_breakout_score","in":"query","description":"Minimum breakout total score. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_risk","in":"query","description":"Maximum breakout risk level. Pro filter.","required":false,"schema":{"type":["string","null"]},"example":"Medium"},{"name":"asset_class","in":"query","description":"Restrict to a single asset class (`coins`, `stocks`, `fx`; `all` or absent = every class).\nFree, like `asset_class` on `/v2/assets` — it selects the universe rather than\nnarrowing it by a Pro signal attribute.","required":false,"schema":{"type":["string","null"]},"example":"coins"}],"responses":{"200":{"description":"Assets currently inside a buy/sell/accumulation/distribution zone, ordered by confidence then distance","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/AssetExtended"},{"type":"object","required":["zone_type","distance_percent"],"properties":{"distance_percent":{"type":"number","format":"double"},"zone_type":{"$ref":"#/components/schemas/ZoneType"}}}]}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"402":{"description":"Pro subscription required for advanced filters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/assets/signals/breakout":{"get":{"tags":["Assets"],"operationId":"breakout_signals","parameters":[{"name":"min_confidence","in":"query","description":"Minimum zone confidence (0.0–1.0). Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"exchanges","in":"query","description":"Comma-separated exchange ids (e.g. `binance,bybit`). Pro filter.","required":false,"schema":{"type":["string","null"]},"example":"binance,bybit"},{"name":"timeframe","in":"query","description":"Timeframe (`h1` default; `m15` available to Pro).","required":false,"schema":{"type":["string","null"]},"example":"d1"},{"name":"min_volume","in":"query","description":"Minimum 24h volume in USD. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_volume","in":"query","description":"Maximum 24h volume in USD. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_risk_reward","in":"query","description":"Minimum risk/reward ratio for the zone. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_distance","in":"query","description":"Maximum distance from current price to the zone, percent. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"zone_types","in":"query","description":"Comma-separated zone types (`buy`,`sell`,`accumulation`,`distribution`).","required":false,"schema":{"type":["string","null"]},"example":"buy,accumulation"},{"name":"direction","in":"query","description":"Breakout direction filter (`bullish`, `bearish`, `neutral`).","required":false,"schema":{"type":["string","null"]}},{"name":"min_breakout_score","in":"query","description":"Minimum breakout total score. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_risk","in":"query","description":"Maximum breakout risk level. Pro filter.","required":false,"schema":{"type":["string","null"]},"example":"Medium"},{"name":"asset_class","in":"query","description":"Restrict to a single asset class (`coins`, `stocks`, `fx`; `all` or absent = every class).\nFree, like `asset_class` on `/v2/assets` — it selects the universe rather than\nnarrowing it by a Pro signal attribute.","required":false,"schema":{"type":["string","null"]},"example":"coins"}],"responses":{"200":{"description":"Assets ranked by breakout total score, filtered by direction / risk / volume","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Asset"},{"type":"object","required":["asset_class","icon","dominant_color"],"properties":{"asset_class":{"$ref":"#/components/schemas/AssetClass","description":"Explicit class discriminator (`coins` / `stocks` / `fx` / `external`) — the\nflattened asset payload has no tag of its own, and clients shouldn't have to\ninfer the class from which fields happen to be present."},"dominant_color":{"type":"string"},"icon":{"type":"string"},"is_favorite":{"type":["boolean","null"]}}}]}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"402":{"description":"Pro subscription required for advanced filters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/assets/signals/buy":{"get":{"tags":["Assets"],"operationId":"buy_signals","parameters":[{"name":"min_confidence","in":"query","description":"Minimum zone confidence (0.0–1.0). Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"exchanges","in":"query","description":"Comma-separated exchange ids (e.g. `binance,bybit`). Pro filter.","required":false,"schema":{"type":["string","null"]},"example":"binance,bybit"},{"name":"timeframe","in":"query","description":"Timeframe (`h1` default; `m15` available to Pro).","required":false,"schema":{"type":["string","null"]},"example":"d1"},{"name":"min_volume","in":"query","description":"Minimum 24h volume in USD. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_volume","in":"query","description":"Maximum 24h volume in USD. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_risk_reward","in":"query","description":"Minimum risk/reward ratio for the zone. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_distance","in":"query","description":"Maximum distance from current price to the zone, percent. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"zone_types","in":"query","description":"Comma-separated zone types (`buy`,`sell`,`accumulation`,`distribution`).","required":false,"schema":{"type":["string","null"]},"example":"buy,accumulation"},{"name":"direction","in":"query","description":"Breakout direction filter (`bullish`, `bearish`, `neutral`).","required":false,"schema":{"type":["string","null"]}},{"name":"min_breakout_score","in":"query","description":"Minimum breakout total score. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_risk","in":"query","description":"Maximum breakout risk level. Pro filter.","required":false,"schema":{"type":["string","null"]},"example":"Medium"},{"name":"asset_class","in":"query","description":"Restrict to a single asset class (`coins`, `stocks`, `fx`; `all` or absent = every class).\nFree, like `asset_class` on `/v2/assets` — it selects the universe rather than\nnarrowing it by a Pro signal attribute.","required":false,"schema":{"type":["string","null"]},"example":"coins"}],"responses":{"200":{"description":"Assets with the highest-confidence buy zone matching filters","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Asset"},{"type":"object","required":["asset_class","icon","dominant_color"],"properties":{"asset_class":{"$ref":"#/components/schemas/AssetClass","description":"Explicit class discriminator (`coins` / `stocks` / `fx` / `external`) — the\nflattened asset payload has no tag of its own, and clients shouldn't have to\ninfer the class from which fields happen to be present."},"dominant_color":{"type":"string"},"icon":{"type":"string"},"is_favorite":{"type":["boolean","null"]}}}]}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"402":{"description":"Pro subscription required for advanced filters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/assets/signals/sell":{"get":{"tags":["Assets"],"operationId":"sell_signals","parameters":[{"name":"min_confidence","in":"query","description":"Minimum zone confidence (0.0–1.0). Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"exchanges","in":"query","description":"Comma-separated exchange ids (e.g. `binance,bybit`). Pro filter.","required":false,"schema":{"type":["string","null"]},"example":"binance,bybit"},{"name":"timeframe","in":"query","description":"Timeframe (`h1` default; `m15` available to Pro).","required":false,"schema":{"type":["string","null"]},"example":"d1"},{"name":"min_volume","in":"query","description":"Minimum 24h volume in USD. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_volume","in":"query","description":"Maximum 24h volume in USD. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"min_risk_reward","in":"query","description":"Minimum risk/reward ratio for the zone. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_distance","in":"query","description":"Maximum distance from current price to the zone, percent. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"zone_types","in":"query","description":"Comma-separated zone types (`buy`,`sell`,`accumulation`,`distribution`).","required":false,"schema":{"type":["string","null"]},"example":"buy,accumulation"},{"name":"direction","in":"query","description":"Breakout direction filter (`bullish`, `bearish`, `neutral`).","required":false,"schema":{"type":["string","null"]}},{"name":"min_breakout_score","in":"query","description":"Minimum breakout total score. Pro filter.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"max_risk","in":"query","description":"Maximum breakout risk level. Pro filter.","required":false,"schema":{"type":["string","null"]},"example":"Medium"},{"name":"asset_class","in":"query","description":"Restrict to a single asset class (`coins`, `stocks`, `fx`; `all` or absent = every class).\nFree, like `asset_class` on `/v2/assets` — it selects the universe rather than\nnarrowing it by a Pro signal attribute.","required":false,"schema":{"type":["string","null"]},"example":"coins"}],"responses":{"200":{"description":"Assets with the highest-confidence sell zone matching filters","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"array","items":{"allOf":[{"$ref":"#/components/schemas/Asset"},{"type":"object","required":["asset_class","icon","dominant_color"],"properties":{"asset_class":{"$ref":"#/components/schemas/AssetClass","description":"Explicit class discriminator (`coins` / `stocks` / `fx` / `external`) — the\nflattened asset payload has no tag of its own, and clients shouldn't have to\ninfer the class from which fields happen to be present."},"dominant_color":{"type":"string"},"icon":{"type":"string"},"is_favorite":{"type":["boolean","null"]}}}]}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"402":{"description":"Pro subscription required for advanced filters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/config":{"get":{"tags":["Config"],"operationId":"get_config","responses":{"200":{"description":"What is free, what is paid, and which choices are real — derived from the same predicates the request path runs, so a client never has to keep its own copy. Send a session to have `caller` answered too; without one it reports `anonymous`, and the rest of the document is identical. `signals` describes the controls on `/v2/assets/signals*` — **not** `/v2/assets/screener`, which takes `asset_class` alone. Cacheable by the calling client only: the response is `private` (it carries `caller`) and varies by session. A client that cannot reach this route should fall back to its built-in defaults: nothing here is required to render, it only removes guesswork.","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["analysis","signals","alerts","limits","insights","caller"],"properties":{"alerts":{"$ref":"#/components/schemas/AlertsConfig"},"analysis":{"$ref":"#/components/schemas/AnalysisConfig"},"caller":{"$ref":"#/components/schemas/CallerConfig"},"insights":{"$ref":"#/components/schemas/InsightsConfig"},"limits":{"type":"array","items":{"$ref":"#/components/schemas/EntitlementLimits"},"description":"Every tier, free first — the free row is the one a pricing page renders."},"signals":{"$ref":"#/components/schemas/SignalsConfig"}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}}},"security":[{"session":[]}]}},"/v2/insights/":{"get":{"tags":["Insights"],"operationId":"get_insights","responses":{"200":{"description":"Global market stats — caps, dominance, volumes, fear/greed, 7-day volume series","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["market_cap","market_cap_change_24h","volume_24h","volume_change_24h","btc_dominance","btc_dominance_change_24h","eth_dominance","eth_dominance_change_24h","defi_volume_24h","defi_volume_change_24h","stablecoin_volume_24h","stablecoin_volume_change_24h","derivatives_volume_24h","derivatives_volume_change_24h","fear_greed","market_insight","last_updated","volumes"],"properties":{"btc_dominance":{"type":"number","format":"double"},"btc_dominance_change_24h":{"type":"number","format":"double"},"defi_volume_24h":{"type":"integer","format":"int64"},"defi_volume_change_24h":{"type":"number","format":"double"},"derivatives_volume_24h":{"type":"integer","format":"int64"},"derivatives_volume_change_24h":{"type":"number","format":"double"},"eth_dominance":{"type":"number","format":"double"},"eth_dominance_change_24h":{"type":"number","format":"double"},"fear_greed":{"type":"number","format":"double"},"last_updated":{"type":"string","format":"date-time"},"market_cap":{"type":"integer","format":"int64"},"market_cap_change_24h":{"type":"number","format":"double"},"market_insight":{"type":"string"},"stablecoin_volume_24h":{"type":"integer","format":"int64"},"stablecoin_volume_change_24h":{"type":"number","format":"double"},"volume_24h":{"type":"integer","format":"int64"},"volume_change_24h":{"type":"number","format":"double"},"volumes":{"type":"array","items":{"$ref":"#/components/schemas/VolumeData"}}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"400":{"description":"Insights cache not yet populated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Failed to fetch insights","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/insights/fear_greed":{"get":{"tags":["Insights"],"operationId":"get_fear_greed_leaderboard","parameters":[{"name":"limit","in":"query","description":"Number of items to return (1–100, default 50).","required":false,"schema":{"type":["integer","null"],"minimum":0}},{"name":"sort_by","in":"query","description":"Sort key (`volume`, `score`, `change_24h`).","required":false,"schema":{"type":["string","null"]},"example":"volume"},{"name":"sort_dir","in":"query","description":"Sort direction (`asc` or `desc`).","required":false,"schema":{"type":["string","null"]},"example":"desc"},{"name":"exclude_stables","in":"query","description":"Exclude stablecoins from the leaderboard (default `true`).","required":false,"schema":{"type":["boolean","null"]}},{"name":"zone","in":"query","description":"Restrict to a single sentiment zone.","required":false,"schema":{"type":["string","null"]},"example":"extreme_fear"},{"name":"min_volume","in":"query","description":"Minimum 24h volume in USD.","required":false,"schema":{"type":["number","null"],"format":"double"}},{"name":"asset_class","in":"query","description":"Restrict to a single asset class (`coins`, `stocks`, `fx`; `all` or absent = no filter).","required":false,"schema":{"type":["string","null"]},"example":"coins"}],"responses":{"200":{"description":"Per-asset fear/greed leaderboard sorted by the requested key","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["items","computed_at"],"properties":{"computed_at":{"type":"string","format":"date-time"},"items":{"type":"array","items":{"$ref":"#/components/schemas/FearGreedLeaderboardItem"}}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/insights/funding_rates":{"get":{"tags":["Insights"],"operationId":"get_funding_rates","parameters":[{"name":"coin_id","in":"query","description":"Restrict funding rates to a specific coin id (e.g. `bitcoin`).","required":false,"schema":{"type":["string","null"]}}],"responses":{"200":{"description":"Funding rates grouped by asset across connected derivatives venues","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","required":["connections","pairs","assets"],"properties":{"assets":{"type":"object","description":"Asset metadata keyed by source asset id.","additionalProperties":{"$ref":"#/components/schemas/AssetExtended"},"propertyNames":{"type":"string"}},"connections":{"type":"object","description":"Connection metadata keyed by `connection_id`.","additionalProperties":{"$ref":"#/components/schemas/ConnectionBaseResponse"},"propertyNames":{"type":"string"}},"pairs":{"type":"object","description":"Futures tickers grouped by source asset id.","additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/CoinFuturesTicker"}},"propertyNames":{"type":"string"}}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}}},"security":[{"session":[]},{"api_key":[]}]}},"/v2/positions/{position_id}/analysis":{"get":{"tags":["Positions"],"operationId":"get_position_analysis","parameters":[{"name":"position_id","in":"path","description":"Position id (must belong to the authenticated user)","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"timeframe","in":"query","description":"Analysis timeframe. Omit it: the served default is the backend's to choose and the response echoes it back as `timeframe`. Any value other than that default requires Pro.","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Factual assessment of the open position against the published zones: thesis status, plan divergence, invalidation, stop placement, liquidation risk, path to targets, timing, and structured warnings. Descriptive only — no action, suggested stop, or thesis confidence. Blocks that cannot be computed from what was known are null (`unrealized_r` is null, never 0, without a usable stop); `thesis_status: \"unknown\"` means no published zone matched this entry and must be rendered as absence, not as all-clear. `timeframe` echoes the window every reading was computed on — caption from that, not from a client-side constant.","content":{"application/json":{"schema":{"type":"object","required":["code","success"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"data":{"type":"object","description":"The assessment. Every block is a checkable fact about geometry the user\nsupplied and zones we already published; `None` blocks mean \"not\nassessable from what was recorded\", never \"fine\". Nothing here tells the\nuser what to do — prescriptive fields (action, suggested stop, thesis\nconfidence) are gated on the conditional-outcome-distribution study.","required":["timeframe","thesis_status","nearest_levels","path_to_targets","regime_heuristic","warnings"],"properties":{"excursion":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Excursion","description":"`None` when the direction is unknown, the entry open time was never\nreported, or the served candle window does not reach back to the entry\n— a window-truncated extreme would read as data while understating it."}]},"invalidation":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/InvalidationStatus"}]},"liquidation_risk":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/LiquidationRisk"}]},"nearest_levels":{"$ref":"#/components/schemas/NearestLevels"},"path_to_targets":{"type":"array","items":{"$ref":"#/components/schemas/TargetPath"}},"plan_divergence":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PlanDivergence"}]},"plan_geometry":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PlanGeometry","description":"The same two ratios anchored on the matched zone's published stop and\nfirst target instead of the user's orders. `None` when no zone matched."}]},"published_at_entry":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PublishedAtEntry","description":"`None` when the direction or entry time is unknown, or when no\nemission recorded at entry time matches this entry — absence of the\nrecord, never evidence the user traded off-plan."}]},"regime_heuristic":{"$ref":"#/components/schemas/RegimeHeuristic"},"remaining_rr":{"type":["number","null"],"format":"double","description":"Remaining reward over remaining risk, both measured from mark to the\nuser's own take-profit and protective stop, as a price-move ratio gross\nof fees and funding. `None` when either leg is not positive: mark\nalready through the stop or past the target is not a ratio."},"stop_placement":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/StopPlacement"}]},"thesis_status":{"$ref":"#/components/schemas/ThesisStatus"},"timeframe":{"type":"string","description":"The timeframe every reading below was computed on, lowercase (`h1`, `m15`).\n\nZones, ATR, the resolution horizon and the bars-held count are all per-timeframe, so an\nassessment captioned from a client-side constant is captioned from something that never\nsaw the request: the timeframe a caller who named none is served is the backend's choice\n(`AnalysisParams::timeframe`), and it is the one this assessment actually ran on."},"timing":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Timing"}]},"unrealized_r":{"type":["number","null"],"format":"double","description":"Signed price move over planned risk against the current mark — the\nsame function the pilot books at close (`pilot_db::realized_r`), gross\nof fees and funding. Planned risk is the distance from entry to a stop\non the loss side of it, so this is `None` when no stop is recorded,\nwhen the recorded one is unusable, or when the stop has been trailed to\nbreakeven or better (`StopPlacement::breakeven_or_better`) — a\nfabricated 0 would read as \"flat\"."},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/Warning"}},"zone":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ZoneMatch"}]}}},"message":{"type":["string","null"]},"success":{"type":"boolean"}}}}}},"400":{"description":"Analysis could not be computed for this position's asset","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"402":{"description":"Pro subscription required for any timeframe other than the default (the one served when `timeframe` is omitted). `code` is 1004 (`AnalysisTimeframeRestricted`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Position not found (or not this user's)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Nothing to assess: the position's instrument is not an asset Flicker tracks (it is listed with `asset: null`), or its mark or entry is not a positive price (e.g. a delisted instrument with no quote); the message says which","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"session":[]}]}}},"components":{"schemas":{"ADXValues":{"type":"object","required":["adx","plus_di","minus_di"],"properties":{"adx":{"type":"number","format":"double"},"minus_di":{"type":"number","format":"double"},"plus_di":{"type":"number","format":"double"}}},"AffiliatePromotion":{"type":"object","required":["id","connection_id","partner_name","title","description","promotional_value","bonus_description","referral_url","icon","theme","steps","validations"],"properties":{"bonus_description":{"type":"string"},"connection_id":{"$ref":"#/components/schemas/PromotionConnection"},"description":{"type":"string"},"icon":{"type":"string"},"id":{"type":"string"},"partner_name":{"type":"string"},"promotional_value":{"type":"string"},"referral_url":{"type":"string"},"steps":{"type":"array","items":{"$ref":"#/components/schemas/PromotionStep"}},"theme":{"$ref":"#/components/schemas/PromotionTheme"},"title":{"type":"string"},"validations":{"type":"array","items":{"$ref":"#/components/schemas/ValidationConfig"}}}},"AlertKindConfig":{"type":"object","description":"One alert kind and the timeframe choices that are real for it.","required":["kind","timeframes"],"properties":{"kind":{"type":"string","example":"pricepercentchange"},"timeframes":{"type":"array","items":{"$ref":"#/components/schemas/AlertTimeframeOption"},"description":"Empty when the kind reads no timeframe at all — a client should render no timeframe\ncontrol for it, rather than one that changes nothing."}}},"AlertTimeframeOption":{"type":"object","description":"A timeframe a client may offer *for one alert kind*, and what the alert will actually be\nevaluated over.","required":["timeframe","needs_pro","evaluates_as"],"properties":{"evaluates_as":{"type":"string","description":"The window this choice is really evaluated over: an analysis timeframe (`h1`, `m15`) for\na kind served from cached analysis, or a percent-change column (`1h`, `24h`, `7d`) for one\nread off the asset row.\n\n**Two timeframes with the same value here produce the same alert.** It is also what the\npush copy is captioned with, so a chip can say what it measures.","example":"24h"},"needs_pro":{"type":"boolean","description":"Whether choosing it requires Pro. Read off the timeframe the user picked, not off\n`evaluates_as` — the paywall charges for the request, and the fold happens afterwards."},"timeframe":{"type":"string","example":"d1"}}},"AlertsConfig":{"type":"object","required":["kinds"],"properties":{"kinds":{"type":"array","items":{"$ref":"#/components/schemas/AlertKindConfig"}}}},"AnalysisConfig":{"type":"object","required":["default_timeframe","initial_timeframe","external_initial_timeframe","timeframes","external_timeframes","candles"],"properties":{"candles":{"$ref":"#/components/schemas/CandlesConfig"},"default_timeframe":{"type":"string","description":"The timeframe a request that names none is served — and, on a venue-listed asset, the only\nfree one. Read [`Self::timeframes`] for what this caller is charged rather than inferring\nit from here: a mobile build below\n[`MIN_CONFIG_TIMEFRAMES_VERSION`](utils::client_version::main::MIN_CONFIG_TIMEFRAMES_VERSION)\nis served `h1` free too, and the menu says so where this field cannot.","example":"d1"},"external_initial_timeframe":{"type":"string","description":"The chip to preselect on an on-chain asset, which is **not** [`Self::default_timeframe`]:\na pool listed this morning holds too few daily bars for analysis to run, so opening one on\nthe default answers a \"too new\" 400 while its faster series is fine. Free, like everything\non [`Self::external_timeframes`].","example":"m5"},"external_timeframes":{"type":"array","items":{"$ref":"#/components/schemas/TimeframeOption"},"description":"The menu for an on-chain asset (`asset_class: external`): its single source publishes no\n`m3`/`m30`/`w1`/`mn1` bar, and everything it does publish is free. Render this one whenever\nthe asset is `external` — [`Self::timeframes`] there offers four chips that can only 400."},"initial_timeframe":{"type":"string","description":"The chip to preselect on a **venue-listed** asset. Distinct from [`Self::default_timeframe`]\nby design, even where the two are equal today: that field is what the server serves a\nrequest naming no timeframe, this one is what a client should put in the request.","example":"d1"},"timeframes":{"type":"array","items":{"$ref":"#/components/schemas/TimeframeOption"},"description":"Every timeframe `/v2/analysis` accepts for a **venue-listed** asset — `asset_class` of\n`coins`, `stocks` or `fx`."}}},"AnalysisZone":{"type":"object","required":["entry_range","stop_loss","invalidation","take_profits","risk_reward_ratio","confidence","supporting_indicators","key_levels"],"properties":{"confidence":{"type":"number","format":"double","description":"\"Should I enter this trade?\" on a 0–100 scale, anchored so that 50 =\nbacktested breakeven: a calibrated win probability (H11/H22 logistic,\nrefit on rejection-rule outcomes) mapped through the empirically\nmeasured probability → net-R curve. Values track expected value —\nroughly one point per 0.01R of backtested edge — so honest scores\ncluster in the 40s–60s; nothing ever pretends to be a 95. Zone\nSELECTION (emission gate, dedup, ranking) intentionally does not use\nthis field — see `selection_score`. docs/zones-research-log.md (H22)."},"confidence_band":{"$ref":"#/components/schemas/ConfidenceBand","description":"Display band for `confidence`: Weak (<50, negative backtested EV),\nModerate (50–60), Strong (≥60). Decision bands beat raw probabilities\nfor end users (H21 evidence review)."},"entry_range":{"type":"array","items":false,"prefixItems":[{"type":"number","format":"double"},{"type":"number","format":"double"}]},"entry_rule":{"$ref":"#/components/schemas/ZoneEntryRule","description":"How the zone should be entered. `RejectionConfirmation` (enter on the\nbar after price closes back out of the zone in the trade direction) is\nthe default: it beat `FirstTouch` in 27/27 walk-forward cells across\nfour eras, and it is the only rule under which entering zones has\npositive expectancy at any confidence tier — see\ndocs/zones-research-log.md (H12, H21, H22)."},"invalidation":{"type":"number","format":"double"},"key_levels":{"type":"array","items":{"type":"number","format":"double"}},"risk_reward_ratio":{"type":"number","format":"double"},"stop_loss":{"type":"number","format":"double"},"supporting_indicators":{"type":"array","items":{"$ref":"#/components/schemas/Indicator"}},"take_profits":{"type":"array","items":{"$ref":"#/components/schemas/TakeProfit"}},"volume_profile":{"type":["number","null"],"format":"double"}}},"AnalysisZones":{"type":"object","required":["buy","sell","accumulation","distribution"],"properties":{"accumulation":{"type":"array","items":{"$ref":"#/components/schemas/AnalysisZone"}},"buy":{"type":"array","items":{"$ref":"#/components/schemas/AnalysisZone"}},"distribution":{"type":"array","items":{"$ref":"#/components/schemas/AnalysisZone"}},"sell":{"type":"array","items":{"$ref":"#/components/schemas/AnalysisZone"}}}},"AppCode":{"type":"string","enum":["Success","GeneralError","NotificationLimitExceeded","NotificationTimeframeRestricted","FavoriteLimitExceeded","AnalysisTimeframeRestricted","AnalysisHistoryDepthRestricted","UserPendingVerification","UserDeactivated","EmailPreferencesLinkInvalid"]},"Asset":{"type":"object","description":"Polymorphic asset payload — flattened into the parent object. Field shape matches exactly one of the Coin, Stock, Currency (FX), or ExternalToken schemas.","additionalProperties":true},"AssetAnalysis":{"type":"object","required":["asset_id","timeframe","pair","market_type","candles","zones","context","indexes","breakout","fear_greed","overview","created_at","hit_from_cache"],"properties":{"asset_id":{"type":"string"},"breakout":{"$ref":"#/components/schemas/BreakoutScore"},"candles":{"type":"array","items":{"type":"object"},"description":"Shared with the candle store rather than copied: a cache hit clones a pointer, not the\nseries behind it, and one instrument's bars are one allocation however many analyses\nreference them. Serializes exactly as the `Vec` it replaced."},"connection":{"type":["object","null"]},"context":{"$ref":"#/components/schemas/MarketContext"},"created_at":{"type":"integer","format":"int64"},"fear_greed":{"$ref":"#/components/schemas/FearGreedIndex"},"hit_from_cache":{"type":"boolean"},"indexes":{"$ref":"#/components/schemas/Indexes"},"market_type":{"type":"string"},"overview":{"type":"string"},"pair":{"type":"string"},"timeframe":{"type":"string"},"zones":{"$ref":"#/components/schemas/AnalysisZones"}}},"AssetClass":{"type":"string","description":"What kind of asset an id names — the one answer, workspace-wide.\n\nThere used to be three of these: a market class on `CandleRequest` (no `External`), a query\nfilter with an `All` variant no market has, and a response discriminator in `services`. Two\ntypes named `AssetClass` told apart only by import path is how a reader reaches for the wrong\none, and the conversions between them were where behaviour quietly diverged.\n\nThe class is decided exactly once, where the instrument is resolved from the asset id, and\nflows from there — the venue alone does not identify it, since Hyperliquid's `xyz` dex serves\nboth FX (`xyz:EUR`) and tokenized stocks (`xyz:AAPL`). Downstream consumers (zone config,\nmodel tags, outcome accounting, the timeframe paywall) must never re-derive it.\n\n`External` is an on-chain pool. It trades continuously like a coin, so the session-shaped\nmatches pair the two — but it is served by one source with no venue listing, which is what the\ntimeframe menu and its paywall turn on.","enum":["coins","stocks","fx","external"]},"AssetExtended":{"allOf":[{"$ref":"#/components/schemas/Asset"},{"type":"object","required":["asset_class","icon","dominant_color"],"properties":{"asset_class":{"$ref":"#/components/schemas/AssetClass","description":"Explicit class discriminator (`coins` / `stocks` / `fx` / `external`) — the\nflattened asset payload has no tag of its own, and clients shouldn't have to\ninfer the class from which fields happen to be present."},"dominant_color":{"type":"string"},"icon":{"type":"string"},"is_favorite":{"type":["boolean","null"]}}}]},"AssetIdentity":{"type":"object","description":"Minimal display data for an asset — id, symbol, name, icon. For endpoints\nthat reference an asset without needing live market data (price, volume,\nchange), unlike the fuller [`crate::assets::extended::AssetExtended`].","required":["id","symbol","name","icon"],"properties":{"icon":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"symbol":{"type":"string"}}},"AssetSignal":{"type":"object","required":["signal","confidence","timeframe","factors"],"properties":{"confidence":{"type":"number","format":"double"},"factors":{"type":"array","items":{"$ref":"#/components/schemas/SignalFactor"}},"signal":{"$ref":"#/components/schemas/Signal"},"timeframe":{"type":"string","description":"The timeframe this signal was computed on, lowercase (`h1`, `m15`).\n\nServed so a client captions the reading with what produced it instead of a literal of its\nown: the timeframe a request that named none is served is the backend's to choose\n(`AnalysisParams::timeframe`), and a client-side `\"1H\"` beside it is a caption nothing can\ncorrect the day that choice moves."}}},"Body":{"oneOf":[{"type":"object","required":["price"],"properties":{"price":{"$ref":"#/components/schemas/Price"}}},{"type":"object","required":["feargreed"],"properties":{"feargreed":{"$ref":"#/components/schemas/FearGreed"}}},{"type":"object","required":["smartzones"],"properties":{"smartzones":{"$ref":"#/components/schemas/SmartZones"}}},{"type":"object","required":["breakout"],"properties":{"breakout":{"$ref":"#/components/schemas/Breakout"}}},{"type":"object","required":["trendreversal"],"properties":{"trendreversal":{"$ref":"#/components/schemas/TrendReversal"}}},{"type":"object","required":["momentumdivergence"],"properties":{"momentumdivergence":{"$ref":"#/components/schemas/MomentumDivergence"}}},{"type":"object","required":["smartmoney"],"properties":{"smartmoney":{"$ref":"#/components/schemas/SmartMoney"}}},{"type":"object","required":["volumespike"],"properties":{"volumespike":{"$ref":"#/components/schemas/VolumeSpike"}}},{"type":"object","required":["pricepercentchange"],"properties":{"pricepercentchange":{"$ref":"#/components/schemas/PricePercentChange"}}},{"type":"object","required":["rsialert"],"properties":{"rsialert":{"$ref":"#/components/schemas/RsiAlert"}}}]},"BollingerValues":{"type":"object","required":["upper","middle","lower"],"properties":{"lower":{"type":"number","format":"double"},"middle":{"type":"number","format":"double"},"upper":{"type":"number","format":"double"}}},"Breakout":{"type":"object","required":["asset_id","min_probability","max_false_breakout_risk"],"properties":{"asset_id":{"type":"string"},"direction":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BreakoutDirection"}]},"max_false_breakout_risk":{"type":"number","format":"double"},"min_probability":{"type":"number","format":"double"}}},"BreakoutComponents":{"type":"object","required":["structure_score","momentum_score","volume_score","volatility_score","level_proximity_score","cycle_score","trend_alignment_score","liquidity_score"],"properties":{"cycle_score":{"type":"number","format":"double"},"level_proximity_score":{"type":"number","format":"double"},"liquidity_score":{"type":"number","format":"double"},"momentum_score":{"type":"number","format":"double"},"structure_score":{"type":"number","format":"double"},"trend_alignment_score":{"type":"number","format":"double"},"volatility_score":{"type":"number","format":"double"},"volume_score":{"type":"number","format":"double"}}},"BreakoutDirection":{"type":"string","enum":["Bullish","Bearish","Neutral"]},"BreakoutPattern":{"type":"string","enum":["TriangleBreakout","ChannelBreakout","RangeBreakout","FlagBreakout","VolumeBreakout","CompressionBreakout","Unknown"]},"BreakoutResponse":{"type":"object","required":["coin","min_probability","max_false_breakout_risk"],"properties":{"coin":{"$ref":"#/components/schemas/AssetExtended"},"direction":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BreakoutDirection"}]},"max_false_breakout_risk":{"type":"number","format":"double"},"min_probability":{"type":"number","format":"double"}}},"BreakoutRisk":{"type":"string","enum":["Low","Medium","High","Extreme"]},"BreakoutScore":{"type":"object","required":["total_score","breakout_probability","false_breakout_risk","direction_bias","breakout_pattern","time_horizon","risk_level","component_scores","confidence_bands","multi_timeframe_alignment","supporting_factors","warning_factors"],"properties":{"breakout_pattern":{"$ref":"#/components/schemas/BreakoutPattern"},"breakout_probability":{"type":"number","format":"double"},"component_scores":{"$ref":"#/components/schemas/BreakoutComponents"},"confidence_bands":{"$ref":"#/components/schemas/ConfidenceBands"},"direction_bias":{"$ref":"#/components/schemas/BreakoutDirection"},"false_breakout_risk":{"type":"number","format":"double"},"multi_timeframe_alignment":{"type":"number","format":"double"},"risk_level":{"$ref":"#/components/schemas/BreakoutRisk"},"supporting_factors":{"type":"array","items":{"type":"string"}},"time_horizon":{"$ref":"#/components/schemas/BreakoutTimeframe"},"total_score":{"type":"number","format":"double"},"warning_factors":{"type":"array","items":{"type":"string"}}}},"BreakoutTimeframe":{"type":"string","enum":["Immediate","Short","Medium","Long"]},"CallerConfig":{"type":"object","required":["state"],"properties":{"entitlement":{"type":["string","null"],"description":"The caller's effective tier; `null` unless `state` is `resolved` **and** they hold one.","example":"pro"},"is_pro":{"type":["boolean","null"],"description":"Whether the paid surfaces above are unlocked for this caller. `null` when `state` is not\n`resolved` — it is the absence of an answer, never a `false`."},"state":{"$ref":"#/components/schemas/CallerState"}}},"CallerState":{"type":"string","description":"Why `entitlement`/`is_pro` are what they are.\n\nExplicit rather than inferred from a null, because the three cases must not be conflated: an\nanonymous caller is genuinely free, and an unreadable subscription row is **not**. A client\nthat rendered `unavailable` as free would downgrade every paying user on one database blip,\nwhich is exactly the failure `controllers::entitlement` exists to prevent.","enum":["anonymous","resolved","unavailable"]},"CandlesConfig":{"type":"object","description":"The chart depth a caller is served, per tier.","required":["free_limit","max_limit","pro_defaults"],"properties":{"free_limit":{"type":"integer","format":"int64","description":"Candles served without a subscription, whatever was asked for.","example":500,"minimum":0},"max_limit":{"type":"integer","format":"int64","description":"The deepest series any caller may request, via `candle_limit` or a further-back\n`start_time`.","example":5000,"minimum":0},"pro_defaults":{"type":"array","items":{"$ref":"#/components/schemas/ClassCandleDepth"},"description":"What a Pro caller who names no depth is served, which varies by market class."}}},"Category":{"type":"string","enum":["buy","sell","accumulation","distribution"]},"ClassCandleDepth":{"type":"object","required":["asset_class","depth"],"properties":{"asset_class":{"type":"string","example":"coins"},"depth":{"type":"integer","format":"int64","example":2000,"minimum":0}}},"Coin":{"type":"object","required":["id","name","price","volume","symbol","change_1h","change_24h","change_7d","highest_24h","lowest_24h","stable","last_updated","inserted_at","watch_count"],"properties":{"change_1h":{"type":"number","format":"double"},"change_24h":{"type":"number","format":"double"},"change_7d":{"type":"number","format":"double"},"highest_24h":{"type":"number","format":"double"},"id":{"type":"string"},"inserted_at":{"type":"string","format":"date-time"},"last_updated":{"type":"string","format":"date-time"},"lowest_24h":{"type":"number","format":"double"},"name":{"type":"string"},"price":{"type":"number","format":"double"},"stable":{"type":"boolean"},"symbol":{"type":"string"},"volume":{"type":"number","format":"double"},"watch_count":{"type":"integer","format":"int64","description":"Asset-detail view count since the last daily reset (see `reset_watch_counts`)."}}},"CoinFuturesTicker":{"type":"object","required":["id","connection_id","pair","from","from_id","to","to_id","volume","volume_usd","last_price","last_price_usd","open_interest","open_interest_usd","funding_rate","next_funding_time","last_updated"],"properties":{"connection_id":{"type":"string"},"from":{"type":"string"},"from_id":{"type":"string"},"funding_rate":{"type":"number","format":"double"},"id":{"type":"string","format":"uuid"},"last_price":{"type":"number","format":"double"},"last_price_usd":{"type":"number","format":"double"},"last_updated":{"type":"string","format":"date-time"},"next_funding_time":{"type":"string","format":"date-time"},"open_interest":{"type":"number","format":"double"},"open_interest_usd":{"type":"number","format":"double"},"pair":{"type":"string"},"to":{"type":"string"},"to_id":{"type":"string"},"volume":{"type":"number","format":"double"},"volume_usd":{"type":"number","format":"double"}}},"CoinTicker":{"type":"object","required":["id","connection_id","pair","from","to","from_id","to_id","volume","volume_usd","last_price","last_price_usd","last_updated"],"properties":{"connection_id":{"type":"string"},"from":{"type":"string"},"from_id":{"type":"string"},"id":{"type":"string","format":"uuid"},"last_price":{"type":"number","format":"double"},"last_price_usd":{"type":"number","format":"double"},"last_updated":{"type":"string","format":"date-time"},"pair":{"type":"string"},"to":{"type":"string"},"to_id":{"type":"string"},"volume":{"type":"number","format":"double"},"volume_usd":{"type":"number","format":"double"}}},"ComponentScore":{"type":"object","required":["value","weight","contribution","description"],"properties":{"contribution":{"type":"number","format":"double"},"description":{"type":"string"},"value":{"type":"number","format":"double"},"weight":{"type":"number","format":"double"}}},"ConfidenceBand":{"type":"string","description":"Coarse action band for the H22 confidence score. Thresholds are in\nconfidence points: 50 is the backtested-breakeven anchor, 60 ≈ +0.10R of\nexpected edge per trade.","enum":["Weak","Moderate","Strong"]},"ConfidenceBands":{"type":"object","required":["base_score","lower_bound","upper_bound","confidence_level"],"properties":{"base_score":{"type":"number","format":"double"},"confidence_level":{"type":"number","format":"double"},"lower_bound":{"type":"number","format":"double"},"upper_bound":{"type":"number","format":"double"}}},"ConnectionBaseResponse":{"type":"object","required":["id","name","icon"],"properties":{"affiliate_url":{"type":["string","null"]},"api_key_guide_url":{"type":["string","null"]},"icon":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"}}},"CorrelationState":{"type":"string","enum":["Normal","Breaking","Broken"]},"CreateApiKeyRequest":{"type":"object","required":["name"],"properties":{"expires_in_days":{"type":["integer","null"],"format":"int64","description":"Optional lifetime. Absent means the key does not expire, which is what every key minted\nbefore this field existed does — so adding it changes nothing for an existing caller."},"name":{"type":"string"}}},"Currency":{"type":"object","description":"A tracked currency. The table is a curated set: rows are seeded explicitly\nand rate feeds only refresh existing rows (`update_rate`), never insert.\nEvery row carries `rate` (target-currency units per 1\nUSD — the sole convention every quote-conversion call site depends on,\nrefreshed hourly from openexchangerates.org) plus\nthe asset-facing fields (`price`, `volume`, `change_*`, `watch_count`, ...)\nthat let it stand in as `Asset::Fx` alongside `Coin`/`Stock`. The whole\ntable is the browsable FX set (like the coins/stocks tables); `price` is\nderived for every row by `update_currency_price_volume` — venue-weighted\nfrom `currency_tickers`, or `1/rate` when no venue currently lists it. `id`\nis the lowercase ISO 4217 code (e.g. \"eur\" — enforced by a CHECK since\nmigration 0039); `symbol` is its uppercase form (\"EUR\"), NOT NULL since\nmigration 0048 (backfilled and BEFORE-INSERT-triggered to `upper(id)` when\nleft unset). `name` is the display name, NOT NULL since migration 0049 (same\nbackfill/trigger rule as `symbol`). Both are hence required `String`s, so the\nAPI response marks them required. `status` is retained but no longer gates\nvisibility.","required":["id","rate","symbol","name","price","volume","change_1h","change_24h","change_7d","highest_24h","lowest_24h","status","last_updated","inserted_at","watch_count"],"properties":{"change_1h":{"type":"number","format":"double"},"change_24h":{"type":"number","format":"double"},"change_7d":{"type":"number","format":"double"},"highest_24h":{"type":"number","format":"double"},"id":{"type":"string"},"inserted_at":{"type":"string","format":"date-time"},"kind":{"type":["string","null"]},"last_updated":{"type":"string","format":"date-time"},"lowest_24h":{"type":"number","format":"double"},"name":{"type":"string"},"price":{"type":"number","format":"double"},"rate":{"type":"number","format":"double"},"status":{"type":"string"},"symbol":{"type":"string"},"volume":{"type":"number","format":"double"},"watch_count":{"type":"integer","format":"int64","description":"Asset-detail view count since the last daily reset (see `reset_watch_counts`)."}}},"CyclePhase":{"type":"string","enum":["Accumulation","Markup","Distribution","Markdown"]},"Direction":{"type":"string","enum":["above","below"]},"EMAValue":{"type":"object","required":["period","value"],"properties":{"period":{"type":"integer","format":"int32","minimum":0},"value":{"type":"number","format":"double"}}},"EmissionOutcome":{"type":"string","description":"Live resolution state of a published zone emission — the\n`zone_emissions.status` vocabulary, parsed at the DB edge so nothing\ndownstream matches on the string.","enum":["pending","entered","tp_hit","stopped_out","time_stopped","expired"]},"EntitlementLimits":{"type":"object","description":"What one tier may hold. `null` on a resource means that tier is uncapped, never zero.","properties":{"alerts":{"type":["integer","null"],"format":"int64","description":"Alerts — what `POST /v1/notifications` refuses past.","example":10},"connections":{"type":["integer","null"],"format":"int64","description":"Exchange connections — what `POST /v1/connections` refuses past.","example":4},"entitlement":{"type":["string","null"],"description":"The tier these caps apply to; `null` is the free tier. Spelled exactly as\n[`CallerConfig::entitlement`], so a client looks its own row up with the value the same\ndocument just handed it.","example":"pro"},"watchlist":{"type":["integer","null"],"format":"int64","description":"Favorites — what `POST /v1/favorites` refuses past.","example":15}}},"ErrorResponse":{"type":"object","required":["code","success","message"],"properties":{"code":{"$ref":"#/components/schemas/AppCode"},"message":{"type":"string"},"success":{"type":"boolean"}}},"ExactAssetResponse":{"type":"object","required":["asset"],"properties":{"asset":{"$ref":"#/components/schemas/AssetExtended"},"similar_assets":{"type":["array","null"],"items":{"$ref":"#/components/schemas/AssetExtended"}}}},"ExchangeDetailResponse":{"allOf":[{"$ref":"#/components/schemas/ExchangeResponse"},{"type":"object","required":["spot_pairs","futures_pairs","futures_open_interest_usd","spot_tickers","futures_tickers"],"properties":{"futures_open_interest_usd":{"type":"number","format":"double"},"futures_pairs":{"type":"integer","minimum":0},"futures_tickers":{"type":"array","items":{"$ref":"#/components/schemas/CoinFuturesTicker"}},"spot_pairs":{"type":"integer","minimum":0},"spot_tickers":{"type":"array","items":{"$ref":"#/components/schemas/CoinTicker"}}}}]},"ExchangeResponse":{"type":"object","required":["id","name","icon","connected","volume"],"properties":{"affiliate_url":{"type":["string","null"]},"api_connection_exists":{"type":["boolean","null"]},"api_key_guide_url":{"type":["string","null"]},"connected":{"type":"boolean"},"icon":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"promotion":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AffiliatePromotion"}]},"volume":{"$ref":"#/components/schemas/ExchangeVolume"},"website_url":{"type":["string","null"]}}},"ExchangeVolume":{"type":"object","required":["connection_id","spot_volume_usd","futures_volume_usd","stock_volume_usd","fx_volume_usd","total_volume_usd","unique_assets"],"properties":{"connection_id":{"type":"string"},"futures_volume_usd":{"type":"number","format":"double"},"fx_volume_usd":{"type":"number","format":"double"},"spot_volume_usd":{"type":"number","format":"double"},"stock_volume_usd":{"type":"number","format":"double"},"total_volume_usd":{"type":"number","format":"double"},"unique_assets":{"type":"integer","format":"int64"}}},"Excursion":{"type":"object","description":"Price extremes the position has actually seen since entry, measured from\nserved candles (plus the current mark). This is real MFE/MAE — unlike\n`context.max_adverse_excursion`, which is a volatility formula with no\ncandle series in scope and is deliberately not surfaced. Needs no stop, so\nit is the risk read that survives an unprotected position.","required":["max_favorable_pct","max_adverse_pct","bars_since_peak"],"properties":{"bars_since_peak":{"type":"integer","description":"Served bars between the favorable extreme and the last bar (0 = the\nextreme is the current bar or the live mark). The earliest bar printing\nthe extreme counts as the peak.","minimum":0},"current_over_peak":{"type":["number","null"],"format":"double","description":"Current move over the peak favorable move — the fraction of the best\nthe position has retained. Negative when underwater, `None` when the\npeak is 0 (a ratio over no favorable move is not a number)."},"max_adverse_atr":{"type":["number","null"],"format":"double"},"max_adverse_pct":{"type":"number","format":"double","description":"Worst move against the trade since entry, % of entry, as a magnitude\nfloored at 0."},"max_favorable_atr":{"type":["number","null"],"format":"double","description":"The same extremes in ATR units; `None` while the ATR is warming up."},"max_favorable_pct":{"type":"number","format":"double","description":"Best move in the trade's favor since entry, % of entry, floored at 0\n(price never in favor reads 0, not a negative \"best\")."}}},"ExternalToken":{"type":"object","required":["id","name","symbol","pool_name","pool_address","chain","price","volume","change_1h","change_24h","change_7d","highest_24h","lowest_24h","icon","liquidity","last_updated","inserted_at"],"properties":{"chain":{"type":"string"},"change_1h":{"type":"number","format":"double"},"change_24h":{"type":"number","format":"double"},"change_7d":{"type":"number","format":"double"},"highest_24h":{"type":"number","format":"double"},"icon":{"type":"string"},"id":{"type":"string"},"inserted_at":{"type":"string","format":"date-time"},"last_updated":{"type":"string","format":"date-time"},"liquidity":{"type":"number","format":"double"},"lowest_24h":{"type":"number","format":"double"},"name":{"type":"string"},"pool_address":{"type":"string"},"pool_name":{"type":"string"},"price":{"type":"number","format":"double"},"symbol":{"type":"string"},"volume":{"type":"number","format":"double"}}},"FavoriteRequestPayload":{"type":"object","description":"The asset a favorite request refers to — a coin, a stock, or an FX currency, all named by the\nsame id space (`bitcoin`, `AAPL`, `eur`). Shared with the v1 handlers, which take the same body.","required":["asset_id"],"properties":{"asset_id":{"type":"string","description":"Asset id, as served by `/v2/assets` (e.g. `bitcoin`, `AAPL`, `eur`).","example":"bitcoin"}}},"FearGreed":{"type":"object","required":["asset_id","target","direction"],"properties":{"asset_id":{"type":"string"},"direction":{"$ref":"#/components/schemas/Direction"},"target":{"type":"number","format":"double"}}},"FearGreedBand":{"type":"object","description":"One band of the fear & greed ladder, half-open `[min, max)`.\n\nMirrored from [`analysis::context::fear_greed::types::SentimentBand`] rather than re-exported:\na field added upstream would otherwise publish itself onto this wire with no compile error and\nnothing to review. `config/main/__test__.rs` pins the two together.","required":["sentiment","label"],"properties":{"label":{"type":"string","description":"The band's one user-facing spelling, shared with push notifications and the weekly digest.","example":"Fear"},"max":{"type":["number","null"],"format":"double","description":"**Exclusive** — a score equal to this belongs to the next band up.","example":45.0},"min":{"type":["number","null"],"format":"double","description":"Inclusive. `null` on the lowest band — the ladder is unbounded below, while the score the\nanalyzer publishes is clamped into 0–100, so a gauge supplies its own axis ends.","example":25.0},"sentiment":{"type":"string","description":"Spelled exactly as `fear_greed.sentiment` on the analysis response, so a client joins a\nreading to its band with no mapping table of its own.","example":"Fear"}}},"FearGreedComponents":{"type":"object","required":["market_extremes","momentum","volume","volatility","structure"],"properties":{"market_extremes":{"$ref":"#/components/schemas/ComponentScore"},"momentum":{"$ref":"#/components/schemas/ComponentScore"},"structure":{"$ref":"#/components/schemas/ComponentScore"},"volatility":{"$ref":"#/components/schemas/ComponentScore"},"volume":{"$ref":"#/components/schemas/ComponentScore"}}},"FearGreedIndex":{"type":"object","required":["score","sentiment","confidence","components","sentiment_trend","historical_percentile","reversal_probability","bullish_signals","bearish_signals","recommendation"],"properties":{"bearish_signals":{"type":"array","items":{"type":"string"}},"bullish_signals":{"type":"array","items":{"type":"string"}},"components":{"$ref":"#/components/schemas/FearGreedComponents"},"confidence":{"type":"number","format":"double"},"historical_percentile":{"type":"number","format":"double"},"recommendation":{"$ref":"#/components/schemas/SentimentRecommendation"},"reversal_probability":{"type":"number","format":"double"},"score":{"type":"number","format":"double"},"sentiment":{"$ref":"#/components/schemas/Sentiment"},"sentiment_trend":{"$ref":"#/components/schemas/SentimentTrend"}}},"FearGreedLeaderboard":{"type":"object","required":["items","computed_at"],"properties":{"computed_at":{"type":"string","format":"date-time"},"items":{"type":"array","items":{"$ref":"#/components/schemas/FearGreedLeaderboardItem"}}}},"FearGreedLeaderboardItem":{"allOf":[{"$ref":"#/components/schemas/AssetExtended"},{"type":"object","required":["score","sentiment","score_updated_at"],"properties":{"score":{"type":"integer","format":"int32"},"score_updated_at":{"type":"string","format":"date-time"},"sentiment":{"$ref":"#/components/schemas/FearGreedZone"}}}]},"FearGreedSortBy":{"type":"string","enum":["volume","score","change24h"]},"FearGreedZone":{"type":"string","enum":["extreme_fear","fear","neutral","greed","extreme_greed"]},"FundingRates":{"type":"object","required":["connections","pairs","assets"],"properties":{"assets":{"type":"object","description":"Asset metadata keyed by source asset id.","additionalProperties":{"$ref":"#/components/schemas/AssetExtended"},"propertyNames":{"type":"string"}},"connections":{"type":"object","description":"Connection metadata keyed by `connection_id`.","additionalProperties":{"$ref":"#/components/schemas/ConnectionBaseResponse"},"propertyNames":{"type":"string"}},"pairs":{"type":"object","description":"Futures tickers grouped by source asset id.","additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/CoinFuturesTicker"}},"propertyNames":{"type":"string"}}}},"FuturesTickerWithConnection":{"allOf":[{"$ref":"#/components/schemas/CoinFuturesTicker"},{"type":"object","required":["connection_id","connection_name","connection_icon"],"properties":{"connection_affiliate_url":{"type":["string","null"]},"connection_icon":{"type":"string"},"connection_id":{"type":"string"},"connection_name":{"type":"string"}}}]},"GradientColors":{"type":"object","required":["from","to"],"properties":{"from":{"type":"string"},"to":{"type":"string"},"via":{"type":["string","null"]}}},"InZoneAsset":{"allOf":[{"$ref":"#/components/schemas/AssetExtended"},{"type":"object","required":["zone_type","distance_percent"],"properties":{"distance_percent":{"type":"number","format":"double"},"zone_type":{"$ref":"#/components/schemas/ZoneType"}}}]},"Indexes":{"type":"object","required":["rsi","sma","ema","support_resistance"],"properties":{"adx":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ADXValues"}]},"atr":{"type":["number","null"],"format":"double"},"bollinger":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/BollingerValues"}]},"cci":{"type":["number","null"],"format":"double"},"ema":{"type":"array","items":{"$ref":"#/components/schemas/EMAValue"}},"macd":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/MACDValues"}]},"mfi":{"type":["number","null"],"format":"double"},"momentum":{"type":["number","null"],"format":"double"},"obv":{"type":["number","null"],"format":"double"},"parabolic_sar":{"type":["number","null"],"format":"double"},"roc":{"type":["number","null"],"format":"double"},"rsi":{"type":"number","format":"double"},"sma":{"type":"array","items":{"$ref":"#/components/schemas/SMAValue"}},"stochastic":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/StochasticValues"}]},"support_resistance":{"$ref":"#/components/schemas/SupportResistanceLevels"},"williams_r":{"type":["number","null"],"format":"double"}}},"Indicator":{"type":"object","required":["name","value","signal"],"properties":{"name":{"type":"string"},"signal":{"$ref":"#/components/schemas/Signal"},"value":{"type":"number","format":"double"}}},"InsightsConfig":{"type":"object","required":["fear_greed"],"properties":{"fear_greed":{"type":"array","items":{"$ref":"#/components/schemas/FearGreedBand"},"description":"The fear & greed ladder, ascending and contiguous: every band's `max` is the next band's\n`min`, and the ends are unbounded."}}},"InvalidationStatus":{"type":"object","description":"The level that kills the idea, and where price stands against it.","required":["level","crossed","signed_distance_pct"],"properties":{"crossed":{"type":"boolean"},"level":{"type":"number","format":"double"},"signed_distance_pct":{"type":"number","format":"double","description":"Signed % from mark to the level (negative = level is below mark)."}}},"Kind":{"type":"string","enum":["price","feargreed","smartzones","breakout","trendreversal","momentumdivergence","smartmoney","volumespike","pricepercentchange","rsialert","volume"]},"LevelDistance":{"type":"object","description":"One published S/R level and where it stands relative to mark.","required":["price","signed_distance_pct"],"properties":{"distance_atr":{"type":["number","null"],"format":"double","description":"Absolute gap in ATR units; `None` while the ATR is warming up."},"price":{"type":"number","format":"double"},"signed_distance_pct":{"type":"number","format":"double","description":"Signed % from mark to the level (negative = level is below mark)."}}},"LiquidationRisk":{"type":"object","required":["liquidation_price","abs_distance_pct"],"properties":{"abs_distance_atr":{"type":["number","null"],"format":"double","description":"The same gap in ATR units — a raw % says nothing about whether ordinary\nnoise can reach it. `None` while the ATR is warming up."},"abs_distance_pct":{"type":"number","format":"double","description":"Absolute % gap between mark and liquidation\n([`distance_to_liquidation_pct`], shared with the positions surface)."},"before_stop":{"type":["boolean","null"],"description":"Liquidation is hit before the protective stop. `None` without one."},"liquidation_price":{"type":"number","format":"double"}}},"LiquidityType":{"type":"string","enum":["StopLossCluster","TakeProfitCluster","OptionStrike","PsychologicalLevel","VolumeNode","FibonacciLevel"]},"LiquidityZone":{"type":"object","required":["price_level","strength","zone_type","touches"],"properties":{"last_test":{"type":["integer","null"],"minimum":0},"price_level":{"type":"number","format":"double"},"strength":{"type":"number","format":"double"},"touches":{"type":"integer","minimum":0},"zone_type":{"$ref":"#/components/schemas/LiquidityType"}}},"MACDValues":{"type":"object","required":["macd_line","signal_line","histogram"],"properties":{"histogram":{"type":"number","format":"double"},"macd_line":{"type":"number","format":"double"},"signal_line":{"type":"number","format":"double"}}},"MarketContext":{"type":"object","required":["timestamp","data_points","calculation_time_ms","confidence_score","trend_direction","trend_strength","trend_consistency","trend_age","trend_momentum_alignment","volatility_level","volatility_expansion","volatility_regime","volatility_percentile","volatility_zscore","momentum_direction","momentum_divergence","momentum_strength","hidden_divergence","momentum_quality","volume_trend","money_flow_state","volume_confirmation","smart_money_flow","volume_profile_skew","overbought_oversold","extreme_confidence","mean_reversion_probability","extreme_duration","cycle_phase","cycle_transition_probability","cycle_stability","sar_position","key_levels_proximity","risk_score","volatility_adjusted_risk","max_adverse_excursion","market_structure","liquidity_zones","fractal_dimension","session_strength","optimal_entry_window","hurst_exponent","entropy","correlation_breakdown"],"properties":{"calculation_time_ms":{"type":"integer","format":"int64","minimum":0},"confidence_score":{"type":"number","format":"double"},"correlation_breakdown":{"$ref":"#/components/schemas/CorrelationState"},"cycle_phase":{"$ref":"#/components/schemas/CyclePhase"},"cycle_stability":{"type":"number","format":"double"},"cycle_transition_probability":{"type":"number","format":"double"},"data_points":{"type":"integer","minimum":0},"entropy":{"type":"number","format":"double"},"extreme_confidence":{"type":"number","format":"double"},"extreme_duration":{"type":"integer","minimum":0},"fractal_dimension":{"type":"number","format":"double"},"hidden_divergence":{"type":"boolean"},"hurst_exponent":{"type":"number","format":"double"},"key_levels_proximity":{"type":"number","format":"double"},"liquidity_zones":{"type":"array","items":{"$ref":"#/components/schemas/LiquidityZone"}},"market_structure":{"$ref":"#/components/schemas/MarketStructure"},"max_adverse_excursion":{"type":"number","format":"double"},"mean_reversion_probability":{"type":"number","format":"double"},"momentum_direction":{"$ref":"#/components/schemas/MomentumState"},"momentum_divergence":{"type":"boolean"},"momentum_quality":{"$ref":"#/components/schemas/MomentumQuality"},"momentum_strength":{"type":"number","format":"double"},"money_flow_state":{"type":"number","format":"double"},"optimal_entry_window":{"type":"boolean"},"overbought_oversold":{"$ref":"#/components/schemas/MarketExtreme"},"risk_score":{"type":"number","format":"double"},"sar_position":{"$ref":"#/components/schemas/SARPosition"},"session_strength":{"$ref":"#/components/schemas/SessionStrength"},"smart_money_flow":{"$ref":"#/components/schemas/SmartMoneyFlow"},"structure_break_level":{"type":["number","null"],"format":"double"},"time_based_bias":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TimeBias","description":"`None` until a venue calendar exists here. Was `TimeBias::MidSession`\non every response, from a function that ignored its `candles`\nargument — a constant published as a measurement."}]},"timestamp":{"type":"integer","format":"int64","minimum":0},"trend_age":{"type":"integer","minimum":0},"trend_consistency":{"type":"number","format":"double"},"trend_direction":{"$ref":"#/components/schemas/TrendDirection"},"trend_momentum_alignment":{"type":"boolean"},"trend_strength":{"type":"number","format":"double"},"volatility_adjusted_risk":{"type":"number","format":"double"},"volatility_expansion":{"type":"boolean"},"volatility_level":{"type":"number","format":"double"},"volatility_percentile":{"type":"number","format":"double"},"volatility_regime":{"$ref":"#/components/schemas/VolatilityRegime"},"volatility_zscore":{"type":"number","format":"double"},"volume_confirmation":{"type":"boolean"},"volume_profile_skew":{"$ref":"#/components/schemas/VolumeSkew"},"volume_trend":{"$ref":"#/components/schemas/VolumeTrend"}}},"MarketExtreme":{"type":"string","enum":["StronglyOverbought","Overbought","Neutral","Oversold","StronglyOversold"]},"MarketStatsV2":{"type":"object","required":["market_cap","market_cap_change_24h","volume_24h","volume_change_24h","btc_dominance","btc_dominance_change_24h","eth_dominance","eth_dominance_change_24h","defi_volume_24h","defi_volume_change_24h","stablecoin_volume_24h","stablecoin_volume_change_24h","derivatives_volume_24h","derivatives_volume_change_24h","fear_greed","market_insight","last_updated","volumes"],"properties":{"btc_dominance":{"type":"number","format":"double"},"btc_dominance_change_24h":{"type":"number","format":"double"},"defi_volume_24h":{"type":"integer","format":"int64"},"defi_volume_change_24h":{"type":"number","format":"double"},"derivatives_volume_24h":{"type":"integer","format":"int64"},"derivatives_volume_change_24h":{"type":"number","format":"double"},"eth_dominance":{"type":"number","format":"double"},"eth_dominance_change_24h":{"type":"number","format":"double"},"fear_greed":{"type":"number","format":"double"},"last_updated":{"type":"string","format":"date-time"},"market_cap":{"type":"integer","format":"int64"},"market_cap_change_24h":{"type":"number","format":"double"},"market_insight":{"type":"string"},"stablecoin_volume_24h":{"type":"integer","format":"int64"},"stablecoin_volume_change_24h":{"type":"number","format":"double"},"volume_24h":{"type":"integer","format":"int64"},"volume_change_24h":{"type":"number","format":"double"},"volumes":{"type":"array","items":{"$ref":"#/components/schemas/VolumeData"}}}},"MarketStructure":{"type":"string","enum":["BullishImpulse","BullishCorrection","BearishImpulse","BearishCorrection","RangeHigh","RangeLow","Transitional","Choppy"]},"MomentumDivergence":{"type":"object","required":["asset_id","include_hidden"],"properties":{"asset_id":{"type":"string"},"include_hidden":{"type":"boolean"}}},"MomentumDivergenceResponse":{"type":"object","required":["coin","include_hidden"],"properties":{"coin":{"$ref":"#/components/schemas/AssetExtended"},"include_hidden":{"type":"boolean"}}},"MomentumQuality":{"type":"string","enum":["Strong","Moderate","Weak","Divergent"]},"MomentumState":{"type":"string","enum":["Accelerating","Decelerating","Negative"]},"NearestLevels":{"type":"object","description":"Nearest published S/R on each side of mark — the structural cushion and\nceiling. Direction-free, so it survives even a `DirectionUnknown` row.","properties":{"resistance_above":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/LevelDistance"}]},"support_below":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/LevelDistance"}]}}},"News":{"type":"object","required":["id","source","title","description","url","date"],"properties":{"date":{"type":"string","format":"date-time"},"description":{"type":"string"},"icon":{"type":["string","null"]},"id":{"type":"string"},"source":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"}}},"NotificationResponseBody":{"oneOf":[{"$ref":"#/components/schemas/TargetSampleResponse"},{"$ref":"#/components/schemas/TargetSampleResponse"},{"$ref":"#/components/schemas/SmartZonesResponse"},{"$ref":"#/components/schemas/BreakoutResponse"},{"$ref":"#/components/schemas/TrendReversalResponse"},{"$ref":"#/components/schemas/MomentumDivergenceResponse"},{"$ref":"#/components/schemas/SmartMoneyResponse"},{"$ref":"#/components/schemas/VolumeSpikeResponse"},{"$ref":"#/components/schemas/PricePercentChangeResponse"},{"$ref":"#/components/schemas/RsiAlertResponse"}]},"NotificationWithResponseBody":{"type":"object","required":["id","kind","user_id","created_at","status","body"],"properties":{"body":{"$ref":"#/components/schemas/NotificationResponseBody"},"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"kind":{"$ref":"#/components/schemas/Kind"},"status":{"$ref":"#/components/schemas/Status"},"user_id":{"type":"string","format":"uuid"}}},"NotificationsConfig":{"type":"object","required":["current","max","unlimited"],"properties":{"current":{"type":"integer","format":"int64","description":"Notifications currently configured by the user."},"max":{"type":"integer","format":"int64","description":"Maximum allowed; `i64::MAX` when `unlimited` is true."},"unlimited":{"type":"boolean"}}},"OrderExtended":{"type":"object","description":"[`TradeOrder`] with `asset_id` resolved into a display-ready [`AssetIdentity`]\n(id/symbol/name/icon) — looked up from the full coin catalog, so an order on\na coin the user has since fully exited still renders correctly. Deliberately\ncarries no live market data (price/volume/change): this is a historical\nrecord, and `avg_price` etc. below are the figures that actually happened.","required":["order_external_id","user_connection_id","connection_id","pair","side","total_size","total_fee","fill_count","first_executed_at","last_executed_at","trades"],"properties":{"asset":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AssetIdentity","description":"`null` when every fill in the order is on an instrument that maps to no asset we\ntrack (migration 0077). The order is the user's own — side, size, fees and\nrealized PnL are the venue's own figures — and [`Self::pair`] is what names it."}]},"avg_price":{"type":["number","null"],"format":"double","description":"Size-weighted average fill price, `None` only if every fill had zero size."},"connection_id":{"type":"string"},"fee_currency":{"type":["string","null"]},"fill_count":{"type":"integer","format":"int64"},"first_executed_at":{"type":"string","format":"date-time"},"last_executed_at":{"type":"string","format":"date-time"},"order_external_id":{"type":"string","description":"Group key: the exchange order id, or the fill's own `external_id` when the\nfill has no parent order."},"pair":{"type":"string"},"realized_pnl":{"type":["number","null"],"format":"double"},"side":{"type":"string"},"total_fee":{"type":"number","format":"double"},"total_size":{"type":"number","format":"double"},"trades":{"type":"array","items":{"$ref":"#/components/schemas/Trade"}},"user_connection_id":{"type":"string","format":"uuid"}}},"OrdersResponse":{"type":"object","required":["orders","total"],"properties":{"orders":{"type":"array","items":{"$ref":"#/components/schemas/OrderExtended"},"description":"The page of orders (grouped fills), newest order first."},"total":{"type":"integer","format":"int64","description":"Total orders matching the filter, across all pages."}}},"Payload":{"type":"object","required":["body"],"properties":{"body":{"$ref":"#/components/schemas/Body"},"status":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Status","description":"Defaults to `pending` when omitted."}]}}},"PlanDivergence":{"type":"object","description":"How the actual execution differs from the plan we emitted.","required":["entry_inside_zone","published_rr"],"properties":{"effective_rr":{"type":["number","null"],"format":"double","description":"Reward over planned risk from the actual entry and stop, as a price-move\nmultiple — gross of fees and funding. `None` without a stop on the loss\nside of entry."},"entry_inside_zone":{"type":"boolean"},"entry_vs_plan_pct":{"type":["number","null"],"format":"double","description":"Signed % the actual entry paid relative to the zone's conservative\nentry (positive = worse than plan for the trade direction). `None` when\nthe zone's entry bound is not a positive finite price — a garbled zone\nmust not serialize a NaN as `null` on a field declared non-null."},"published_rr":{"type":"number","format":"double","description":"The zone's published ratio, restated for the diff — the same gross\nprice-move basis as `effective_rr`."},"rr_basis":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/RrBasis","description":"What `effective_rr`'s reward leg was measured against."}]},"stop_tightness_vs_plan":{"type":["number","null"],"format":"double","description":"Plan risk over actual risk: 2.0 = your stop is 2× tighter than the\nplan's. `None` when no stop on the loss side of entry is recorded."}}},"PlanGeometry":{"type":"object","description":"The position measured on the published plan's geometry instead of the\nuser's own orders. Field names carry the anchor (\"at plan stop\", \"on\nplan\") so a client cannot present these as the position's own R — they are\nthe hypothetical framing that keeps R meaningful when no venue stop exists.","properties":{"remaining_rr_on_plan":{"type":["number","null"],"format":"double","description":"Remaining reward to the zone's first target over remaining risk to the\nplan stop, both from mark. `None` when either leg is not positive."},"unrealized_r_at_plan_stop":{"type":["number","null"],"format":"double","description":"Signed price move over the plan's entry-to-stop risk, against mark —\nsame `realized_r` definition as `unrealized_r`, gross of fees and\nfunding. `None` when the plan stop is not on the loss side of the\nactual entry."}}},"PositionAssessment":{"type":"object","description":"The assessment. Every block is a checkable fact about geometry the user\nsupplied and zones we already published; `None` blocks mean \"not\nassessable from what was recorded\", never \"fine\". Nothing here tells the\nuser what to do — prescriptive fields (action, suggested stop, thesis\nconfidence) are gated on the conditional-outcome-distribution study.","required":["timeframe","thesis_status","nearest_levels","path_to_targets","regime_heuristic","warnings"],"properties":{"excursion":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Excursion","description":"`None` when the direction is unknown, the entry open time was never\nreported, or the served candle window does not reach back to the entry\n— a window-truncated extreme would read as data while understating it."}]},"invalidation":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/InvalidationStatus"}]},"liquidation_risk":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/LiquidationRisk"}]},"nearest_levels":{"$ref":"#/components/schemas/NearestLevels"},"path_to_targets":{"type":"array","items":{"$ref":"#/components/schemas/TargetPath"}},"plan_divergence":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PlanDivergence"}]},"plan_geometry":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PlanGeometry","description":"The same two ratios anchored on the matched zone's published stop and\nfirst target instead of the user's orders. `None` when no zone matched."}]},"published_at_entry":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/PublishedAtEntry","description":"`None` when the direction or entry time is unknown, or when no\nemission recorded at entry time matches this entry — absence of the\nrecord, never evidence the user traded off-plan."}]},"regime_heuristic":{"$ref":"#/components/schemas/RegimeHeuristic"},"remaining_rr":{"type":["number","null"],"format":"double","description":"Remaining reward over remaining risk, both measured from mark to the\nuser's own take-profit and protective stop, as a price-move ratio gross\nof fees and funding. `None` when either leg is not positive: mark\nalready through the stop or past the target is not a ratio."},"stop_placement":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/StopPlacement"}]},"thesis_status":{"$ref":"#/components/schemas/ThesisStatus"},"timeframe":{"type":"string","description":"The timeframe every reading below was computed on, lowercase (`h1`, `m15`).\n\nZones, ATR, the resolution horizon and the bars-held count are all per-timeframe, so an\nassessment captioned from a client-side constant is captioned from something that never\nsaw the request: the timeframe a caller who named none is served is the backend's choice\n(`AnalysisParams::timeframe`), and it is the one this assessment actually ran on."},"timing":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Timing"}]},"unrealized_r":{"type":["number","null"],"format":"double","description":"Signed price move over planned risk against the current mark — the\nsame function the pilot books at close (`pilot_db::realized_r`), gross\nof fees and funding. Planned risk is the distance from entry to a stop\non the loss side of it, so this is `None` when no stop is recorded,\nwhen the recorded one is unusable, or when the stop has been trailed to\nbreakeven or better (`StopPlacement::breakeven_or_better`) — a\nfabricated 0 would read as \"flat\"."},"warnings":{"type":"array","items":{"$ref":"#/components/schemas/Warning"}},"zone":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ZoneMatch"}]}}},"PositionSide":{"type":"string","description":"Side of an already-open position. Distinct from the zone buckets: a\nposition knows its own direction (or doesn't — a futures row synced with a\nNULL direction maps to no side at all, never to an assumed Long, because an\nassumed Long inverts every structural read for a short).","enum":["long","short"]},"Price":{"type":"object","required":["asset_id","target","direction"],"properties":{"asset_id":{"type":"string"},"direction":{"$ref":"#/components/schemas/Direction"},"target":{"type":"number","format":"double"}}},"PricePercentChange":{"type":"object","required":["asset_id","target","direction"],"properties":{"asset_id":{"type":"string"},"direction":{"$ref":"#/components/schemas/Direction"},"target":{"type":"number","format":"double"},"timeframe":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Timeframe"}]}}},"PricePercentChangeResponse":{"type":"object","required":["coin","target","direction"],"properties":{"coin":{"$ref":"#/components/schemas/AssetExtended"},"direction":{"$ref":"#/components/schemas/Direction"},"target":{"type":"number","format":"double"},"timeframe":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Timeframe"}]}}},"PromoVideoCampaign":{"type":"object","description":"A single affiliate promo-video campaign served to mobile/web.\n\nOnly structural/dynamic data lives here — all human-facing copy (eyebrow,\ntitle, tagline, perks, CTA, etc.) is resolved client-side from the app's\nlocalized bundles, keyed by [`PromoVideoCampaign::id`]. That keeps the seven\nsupported locales in the app rather than duplicated on the backend.\n\nThis is the wire contract, deliberately narrower than the `promo_campaigns`\nrow it is built from: scheduling/bookkeeping columns (`registration_open`,\n`is_enabled`, timestamps) stay server-side.","required":["id","url","video_url","web_banner_video_position","registration_close","exchange_icon_url","exchange_icon_alt"],"properties":{"exchange_icon_alt":{"type":"string"},"exchange_icon_url":{"type":"string","description":"Exchange logo shown in the eyebrow (CDN PNG) + its accessibility label."},"id":{"type":"string","description":"Stable campaign id (e.g. `race_to_1m`); also the analytics `campaign` property\nand the key the client uses to resolve localized copy."},"prize_amount":{"type":["string","null"]},"prize_banner_label":{"type":["string","null"],"description":"Headline prize, e.g. `1,000,000 USDT` (banner) and `1,000,000` (takeover).\nAbsent for non-prize campaigns (e.g. a score-difference trading activity);\nclients omit the prize block when these are missing."},"prize_icon_url":{"type":["string","null"]},"registration_close":{"type":"string","format":"date-time","description":"Registration close (ISO-8601 UTC). Drives the client countdown and the\nserver-side active filter — clients never receive an expired campaign."},"url":{"type":"string","description":"Affiliate landing page, channel/referral-tagged to Flicker."},"video_url":{"type":"string","description":"CDN-hosted cinematic MP4 (banner + takeover background)."},"web_banner_video_position":{"type":"string","description":"CSS `object-position` (e.g. `center 80%`) for cover-cropping the banner\nvideo on web. Web-layout-specific; mobile may ignore it."}}},"PromotionConnection":{"type":"string","enum":["blofin","bingx","okx","lbank","htx","bybit","gate","hyperliquid","bitget"]},"PromotionSignUpInfo":{"type":"object","required":["show"],"properties":{"show":{"type":"boolean"}}},"PromotionSignUpWithUID":{"type":"object","required":["uid"],"properties":{"uid":{"type":"string"}}},"PromotionStep":{"type":"object","required":["step_number","description","is_external"],"properties":{"description":{"type":"string"},"details":{"type":["string","null"]},"is_external":{"type":"boolean"},"step_number":{"type":"integer","format":"int32","minimum":0}}},"PromotionTheme":{"type":"object","required":["gradient","text_color","accent_color","border_color"],"properties":{"accent_color":{"type":"string"},"border_color":{"type":"string"},"gradient":{"$ref":"#/components/schemas/GradientColors"},"text_color":{"type":"string"}}},"PromotionVolumeExchange":{"type":"object","description":"One exchange's trailing-30d affiliate volume, for the `/v1/promotions/volume` status view.","required":["connection","display_name"],"properties":{"connection":{"$ref":"#/components/schemas/PromotionConnection"},"display_name":{"type":"string"},"volume_30d_usd":{"type":["number","null"],"format":"double","description":"Trailing-30d USD volume, or `null` for an exchange that only exposes lifetime volume."}}},"PromotionVolumeStatus":{"type":"object","description":"A user's affiliate-volume standing plus their current affiliate-earned (`Elite`) grant.","required":["per_exchange","total_30d_usd","threshold_usd","qualifies","granted_via_promotion"],"properties":{"granted_via_promotion":{"type":"boolean","description":"Whether the user currently holds a valid affiliate-earned `Elite` grant."},"per_exchange":{"type":"array","items":{"$ref":"#/components/schemas/PromotionVolumeExchange"}},"promotion_expires_at":{"type":["string","null"],"format":"date-time","description":"When that grant expires, if any."},"qualifies":{"type":"boolean","description":"`has an affiliate connection && total_30d_usd >= threshold_usd`."},"threshold_usd":{"type":"number","format":"double"},"total_30d_usd":{"type":"number","format":"double"}}},"PublishedAtEntry":{"type":"object","description":"\"What we published when you entered\": the zone that was live and matched\nthe entry at entry time, and how the actual entry compares to it. The\ncurrent-analysis blocks answer \"is the idea still alive\"; this one answers\n\"what plan did you act on, and what did that plan do\".","required":["zone","entry_inside_published_zone"],"properties":{"entry_inside_published_zone":{"type":"boolean"},"entry_vs_published_pct":{"type":["number","null"],"format":"double","description":"Signed % the actual entry paid vs the published zone's conservative\nentry (positive = worse than plan for the trade direction). Same\ndefinition as `PlanDivergence::entry_vs_plan_pct`."},"unrealized_r_at_published_stop":{"type":["number","null"],"format":"double","description":"Hypothetical R against mark, anchored on the *published* stop — the\nentry-time analogue of `PlanGeometry::unrealized_r_at_plan_stop`.\n`None` when the published stop is not on the loss side of the actual\nentry."},"zone":{"$ref":"#/components/schemas/PublishedZone"}}},"PublishedZone":{"type":"object","description":"A zone as it was actually published during the entry window, resolved from\n`zone_emissions` by the caller (the analysis crate stays DB-free on its\nungated path). Also the wire shape of the matched one — self-contained, so\na client renders \"the plan you saw, and how it went\" without another call.","required":["zone_type","entry_range","stop_loss","take_profits","confidence","confidence_band","risk_reward_ratio","first_seen_at_ms","last_seen_at_ms","outcome"],"properties":{"confidence":{"type":"number","format":"double"},"confidence_band":{"$ref":"#/components/schemas/ConfidenceBand"},"entry_range":{"type":"array","items":false,"prefixItems":[{"type":"number","format":"double"},{"type":"number","format":"double"}]},"first_seen_at_ms":{"type":"integer","format":"int64","description":"The window the level was actually being published, epoch ms."},"last_seen_at_ms":{"type":"integer","format":"int64"},"outcome":{"$ref":"#/components/schemas/EmissionOutcome"},"plan_realized_r":{"type":["number","null"],"format":"double","description":"Net R of the published plan as simulated by the outcome loop —\nsize-weighted across TP legs, after the flat 0.20% round-trip cost,\non the zone's **conservative-entry basis**: the sim assumes a fill at\nthe range edge, which `outcome_sim` measures as ~+0.25R optimistic vs\na live-achievable fill. The *plan's* result, never the user's, and\nevery renderer must carry the basis with the number."},"resolved_at_ms":{"type":["integer","null"],"format":"int64"},"risk_reward_ratio":{"type":"number","format":"double"},"stop_loss":{"type":"number","format":"double"},"take_profits":{"type":"array","items":{"$ref":"#/components/schemas/TakeProfit"}},"zone_type":{"$ref":"#/components/schemas/ZoneType"}}},"RegimeHeuristic":{"type":"object","description":"Current volatility state. Named `_heuristic` on the response because that\nis what it is: the underlying context inputs were never fitted against a\nposition-holding outcome (H39) — advisory, not validated.","required":["volatility_expansion","volatility_zscore"],"properties":{"volatility_expansion":{"type":"boolean"},"volatility_zscore":{"type":"number","format":"double"}}},"RrBasis":{"type":"string","enum":["user_take_profit","zone_first_target"]},"RsiAlert":{"type":"object","required":["asset_id","target","direction"],"properties":{"asset_id":{"type":"string"},"direction":{"$ref":"#/components/schemas/Direction"},"target":{"type":"number","format":"double"},"timeframe":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Timeframe"}]}}},"RsiAlertResponse":{"type":"object","required":["coin","target","direction"],"properties":{"coin":{"$ref":"#/components/schemas/AssetExtended"},"direction":{"$ref":"#/components/schemas/Direction"},"target":{"type":"number","format":"double"},"timeframe":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Timeframe"}]}}},"SARPosition":{"type":"string","enum":["Above","Below"]},"SMAValue":{"type":"object","required":["period","value"],"properties":{"period":{"type":"integer","format":"int32","minimum":0},"value":{"type":"number","format":"double"}}},"ScreenerResponse":{"type":"object","required":["gainers","losers","breakouts"],"properties":{"breakouts":{"type":"array","items":{"$ref":"#/components/schemas/AssetExtended"}},"gainers":{"type":"array","items":{"$ref":"#/components/schemas/AssetExtended"}},"losers":{"type":"array","items":{"$ref":"#/components/schemas/AssetExtended"}}}},"Sentiment":{"type":"string","enum":["ExtremeFear","Fear","Neutral","Greed","ExtremeGreed"]},"SentimentRecommendation":{"oneOf":[{"type":"object","required":["ContraBuy"],"properties":{"ContraBuy":{"type":"object","required":["confidence","reasons"],"properties":{"confidence":{"type":"number","format":"double"},"reasons":{"type":"array","items":{"type":"string"}}}}}},{"type":"object","required":["CautiousBuy"],"properties":{"CautiousBuy":{"type":"object","required":["confidence","reasons"],"properties":{"confidence":{"type":"number","format":"double"},"reasons":{"type":"array","items":{"type":"string"}}}}}},{"type":"object","required":["FollowTrend"],"properties":{"FollowTrend":{"type":"object","required":["reasons"],"properties":{"reasons":{"type":"array","items":{"type":"string"}}}}}},{"type":"object","required":["CautiousSell"],"properties":{"CautiousSell":{"type":"object","required":["confidence","reasons"],"properties":{"confidence":{"type":"number","format":"double"},"reasons":{"type":"array","items":{"type":"string"}}}}}},{"type":"object","required":["ContraSell"],"properties":{"ContraSell":{"type":"object","required":["confidence","reasons"],"properties":{"confidence":{"type":"number","format":"double"},"reasons":{"type":"array","items":{"type":"string"}}}}}},{"type":"object","required":["Wait"],"properties":{"Wait":{"type":"object","required":["reasons"],"properties":{"reasons":{"type":"array","items":{"type":"string"}}}}}}]},"SentimentTrend":{"type":"string","enum":["RapidlyIncreasing","Increasing","Stable","Decreasing","RapidlyDecreasing"]},"SessionStrength":{"type":"object","description":"Intraday session context.\n\n**Only `recent_volume_ratio` is measured.** The session fields are `Option`\nand currently always `None`: nothing in this crate derives a trading session\n(candles carry a timestamp but no venue calendar), so publishing\n`Unknown`/`1.0`/`Sideways`/`0.0` as if they were readings put four fabricated\nconstants on the wire. `Option` says \"not computed\" instead — see the\nplaceholder rule in `.claude/rules/backend.md`.","required":["recent_volume_ratio"],"properties":{"current_session":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TradingSession"}]},"recent_volume_ratio":{"type":"number","format":"double","description":"Volume over the last 4 bars against the mean bar, over the compute\nwindow. Renamed 2026-08-04: it was `session_volume_ratio`, which named a\nwindow it was never computed from. It is load-bearing —\n`determine_optimal_entry` gates on `0.8 < ratio < 2.0`, the first `Wait`\nbranch of `get_trading_recommendation` — so the band wants fitting\nagainst what this actually measures."},"session_range_ratio":{"type":["number","null"],"format":"double"},"session_trend":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TrendDirection"}]},"session_volatility":{"type":["number","null"],"format":"double"}}},"Signal":{"type":"string","enum":["Buy","Hold","Sell"]},"SignalFactor":{"type":"object","required":["key","signal","score","weight","detail"],"properties":{"detail":{"type":"string"},"key":{"type":"string"},"score":{"type":"number","format":"double"},"signal":{"$ref":"#/components/schemas/Signal"},"weight":{"type":"integer","format":"int32","minimum":0}}},"SignalFilterConfig":{"type":"object","description":"One signal-list control and what it costs.","required":["param","needs_pro"],"properties":{"needs_pro":{"type":"boolean"},"param":{"type":"string","description":"The query parameter, exactly as the routes below accept it.","example":"min_confidence"}}},"SignalsConfig":{"type":"object","description":"The controls on the signal lists — `GET /v2/assets/signals`, `/signals/buy`, `/signals/sell`\nand `/signals/breakout`, which are what a screener UI is built from.\n\n**Not `/v2/assets/screener`**, which takes `asset_class` alone and ignores every parameter\nlisted here.","required":["filters","timeframes"],"properties":{"filters":{"type":"array","items":{"$ref":"#/components/schemas/SignalFilterConfig"}},"timeframes":{"type":"array","items":{"$ref":"#/components/schemas/TimeframeOption"},"description":"The timeframes these routes can actually run on. They read pre-computed analysis, so any\nother value is a 400 rather than a silent fold."}}},"SmartMoney":{"type":"object","required":["asset_id","flow_type"],"properties":{"asset_id":{"type":"string"},"flow_type":{"$ref":"#/components/schemas/SmartMoneyFlow"}}},"SmartMoneyFlow":{"type":"string","enum":["Accumulation","Distribution","Neutral","Divergent"]},"SmartMoneyResponse":{"type":"object","required":["coin","flow_type"],"properties":{"coin":{"$ref":"#/components/schemas/AssetExtended"},"flow_type":{"$ref":"#/components/schemas/SmartMoneyFlow"}}},"SmartZones":{"type":"object","required":["target_confidence","category"],"properties":{"asset_id":{"type":["string","null"]},"category":{"$ref":"#/components/schemas/Category"},"target_confidence":{"type":"number","format":"double"},"target_price":{"type":["number","null"],"format":"double"},"target_price_direction":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Direction"}]},"timeframe":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Timeframe"}]}}},"SmartZonesResponse":{"type":"object","required":["category","target_confidence"],"properties":{"category":{"$ref":"#/components/schemas/Category"},"coin":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/AssetExtended"}]},"target_confidence":{"type":"number","format":"double"},"target_price":{"type":["number","null"],"format":"double"},"target_price_direction":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Direction"}]},"timeframe":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/Timeframe"}]}}},"Status":{"type":"string","enum":["pending","sent","disabled"]},"StochasticValues":{"type":"object","required":["k","d"],"properties":{"d":{"type":"number","format":"double"},"k":{"type":"number","format":"double"}}},"Stock":{"type":"object","required":["id","symbol","name","price","volume","change_1h","change_24h","change_7d","highest_24h","lowest_24h","status","last_updated","inserted_at","watch_count"],"properties":{"change_1h":{"type":"number","format":"double"},"change_24h":{"type":"number","format":"double"},"change_7d":{"type":"number","format":"double"},"highest_24h":{"type":"number","format":"double"},"id":{"type":"string"},"inserted_at":{"type":"string","format":"date-time"},"kind":{"type":["string","null"]},"last_updated":{"type":"string","format":"date-time"},"lowest_24h":{"type":"number","format":"double"},"name":{"type":"string"},"price":{"type":"number","format":"double"},"status":{"type":"string"},"symbol":{"type":"string"},"volume":{"type":"number","format":"double"},"watch_count":{"type":"integer","format":"int64","description":"Asset-detail view count since the last daily reset (see `reset_watch_counts`)."}}},"StopPlacement":{"type":"object","description":"Structural reads on a stop that still protects the position (finite,\npositive, on the far side of mark). Every check that needs a zone or the\nATR is independently `Option`: `None` means \"not assessable\" (no zone\nmatched, or the ATR is still warming up) — an abstention, never a pass.","required":["inside_opposing_zone","breakeven_or_better"],"properties":{"beyond_invalidation":{"type":["boolean","null"]},"breakeven_or_better":{"type":"boolean","description":"The stop sits at entry or on the profit side of it — a managed winner\ntrailed to breakeven or better, not a broken plan. Planned risk is\nretired, so `unrealized_r` is `None`; what the stop now protects is a\ngross gain (at exactly entry the position still loses its fees).\nAlways known once this block exists, hence not `Option`."},"distance_from_mark_atr":{"type":["number","null"],"format":"double","description":"|mark − stop| in ATRs. `None` while the ATR is warming up: the 1%\nfallback the zones stack uses for geometry would fabricate a\nnoise-check verdict here, so this abstains instead."},"inside_entry_zone":{"type":["boolean","null"]},"inside_opposing_zone":{"type":"boolean","description":"The stop sits inside an opposite-direction zone's entry range. Always\ndecided (the opposing lists are the whole universe), hence not `Option`."},"within_one_atr":{"type":["boolean","null"]}}},"SupportResistanceLevels":{"type":"object","required":["support","resistance"],"properties":{"resistance":{"type":"array","items":{"type":"number","format":"double"}},"support":{"type":"array","items":{"type":"number","format":"double"}}}},"TakeProfit":{"type":"object","required":["price","percentage","size_percentage","rationale"],"properties":{"percentage":{"type":"number","format":"double"},"price":{"type":"number","format":"double"},"rationale":{"type":"string"},"size_percentage":{"type":"number","format":"double"}}},"TargetPath":{"type":"object","description":"One target and what stands between the current price and it.","required":["price","source","sr_levels_between","opposing_zones_between"],"properties":{"opposing_zones_between":{"type":"integer","description":"Opposite-direction zones whose entry range overlaps the path.","minimum":0},"price":{"type":"number","format":"double"},"source":{"$ref":"#/components/schemas/TargetSource"},"sr_levels_between":{"type":"integer","description":"S/R levels strictly between mark and the target (resistance for a\nlong, support for a short).","minimum":0}}},"TargetSampleResponse":{"type":"object","required":["coin","target","direction"],"properties":{"coin":{"$ref":"#/components/schemas/AssetExtended"},"direction":{"$ref":"#/components/schemas/Direction"},"target":{"type":"number","format":"double"}}},"TargetSource":{"oneOf":[{"type":"object","required":["kind"],"properties":{"kind":{"type":"string","enum":["user_take_profit"]}}},{"type":"object","required":["index","kind"],"properties":{"index":{"type":"integer","minimum":0},"kind":{"type":"string","enum":["zone_take_profit"]}}}]},"ThesisStatus":{"type":"string","description":"Is the idea behind this position still standing? `Unknown` means \"we have\nno zone for this entry\" and must render as absence — a position we cannot\nassess is not a position that looks fine.","enum":["intact","invalidated","unknown"]},"TickerWithConnection":{"allOf":[{"$ref":"#/components/schemas/CoinTicker"},{"type":"object","required":["connection_id","connection_name","connection_icon"],"properties":{"connection_affiliate_url":{"type":["string","null"]},"connection_icon":{"type":"string"},"connection_id":{"type":"string"},"connection_name":{"type":"string"}}}]},"TimeBias":{"type":"string","enum":["EarlySession","MidSession","LateSession","Transition"]},"Timeframe":{"type":"string","description":"The default is the timeframe analysis is always warmed for and the only one a free caller is\nserved without a paywall on a venue-listed asset, so it is the right answer for a request that\nnamed none. (An on-chain pool is free at every timeframe its source serves — see\n`analysis::timeframes::paywall::timeframe_needs_pro` — which does not move with this attribute,\nand neither does the temporary `h1` grant to pre-3.9.30 mobile builds documented there.)\n\n**Moving `#[default]` moves all three of those.** The paywall (`analysis::timeframes::main::\ntimeframe_needs_pro`), the cron's unwarmed-alert fallback (`handlers::analysis_timeframe`) and\nthe MCP tool parameter's own default all derive from this attribute rather than restating a\ntimeframe, so the one thing a move still has to satisfy is that the new default is warmed —\npinned by `cron-analysis`'s `the_default_timeframe_is_one_the_warmer_computes`.\n\n**`unwrap_or_default()` is not always the right resolution.** It is correct where the only\nquestion is \"what if the caller said nothing\" — `AnalysisParams::timeframe`. It is wrong\nwherever a *named* timeframe also has to be constrained: `analysis_timeframe` in the cron's\nnotification handlers clamps to the warmed timeframes, so `Some(H4)` must become the default too.\nDefaulting there instead of clamping reads a window analysis was never computed for and captions\nit with the one the user picked — a bug that has already happened once.","enum":["m3","m5","m15","m30","h1","h4","d1","w1","mn1"]},"TimeframeOption":{"type":"object","description":"A timeframe a client may offer, and what it costs.","required":["timeframe","needs_pro"],"properties":{"needs_pro":{"type":"boolean","description":"Whether choosing it requires a Pro entitlement."},"timeframe":{"type":"string","example":"d1"}}},"Timing":{"type":"object","required":["bars_held_at_least","horizon_bars"],"properties":{"bars_held":{"type":["integer","null"],"description":"Bars from the entry to the last analysed bar, on the sim's\n`bars_to_resolution` basis (bar-index offset: an entry on the last bar\nreads 0), so it compares with `horizon_bars` like with like. Coins are\ncomputed from the timeframe's bar length — 24/7 markets, exact at any\nage. Stocks and FX are counted over the analysed series, because\nsession gaps make wall-clock arithmetic over-count; `None` when the\nentry predates that series — the served window is tier-dependent, so\nits length would be a wrong number, not the count.","minimum":0},"bars_held_at_least":{"type":"integer","description":"A bound that always holds: `bars_held` when known, else the length of\nthe analysed series (every served bar is after the entry).","minimum":0},"horizon_bars":{"type":"integer","description":"Resolution horizon for this class and timeframe\n(`outcome_sim::horizon_bars_for`) — beyond it the backtest carries no\nevidence about trades like this one.","minimum":0},"horizon_exceeded":{"type":["boolean","null"],"description":"`bars_held > horizon_bars`. Decided from `bars_held_at_least` when the\nexact count is unknown; `None` when even that bound cannot decide it."}}},"Trade":{"type":"object","required":["id","external_id","user_id","user_connection_id","connection_id","pair","side","price","size","fee","executed_at","created_at"],"properties":{"asset_id":{"type":["string","null"],"description":"The asset this fill's pair resolved to, or `None` when it resolved to nothing we\ntrack (migration 0077). The fill is the user's own trade either way; only the\nasset is missing, and [`Self::pair`] is what names it."},"coin_id":{"type":["string","null"],"description":"Deprecated wire alias of `asset_id`: the shipped mobile `Trade.fromJson` reads\n`coin_id` non-nullably, which is why an untracked fill never reaches those builds\n(`controllers::legacy_portfolio::trades`). Not a column — filled by\n`SELECT *, asset_id AS coin_id_alias`."},"connection_id":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"executed_at":{"type":"string","format":"date-time"},"external_id":{"type":"string","description":"Exchange-native trade (fill) id."},"fee":{"type":"number","format":"double"},"fee_currency":{"type":["string","null"]},"id":{"type":"string","format":"uuid"},"order_external_id":{"type":["string","null"],"description":"Exchange-native id of the parent order this fill belongs to, when the\nexchange reports one. `None` for exchanges that only return bare fills."},"pair":{"type":"string"},"price":{"type":"number","format":"double"},"realized_pnl":{"type":["number","null"],"format":"double"},"role":{"type":["string","null"]},"side":{"type":"string"},"size":{"type":"number","format":"double"},"user_connection_id":{"type":"string","format":"uuid"},"user_id":{"type":"string","format":"uuid"}}},"TradesResponse":{"type":"object","required":["trades","total"],"properties":{"total":{"type":"integer","format":"int64","description":"Total trades matching the filter, across all pages."},"trades":{"type":"array","items":{"$ref":"#/components/schemas/Trade"},"description":"The page of trades (fills), newest first."}}},"TradingSession":{"type":"string","enum":["AsianSession","EuropeanSession","AmericanSession","Overlap","Weekend","Unknown"]},"TrendDirection":{"type":"string","enum":["Uptrend","Downtrend","Sideways"]},"TrendReversal":{"type":"object","required":["asset_id","min_strength"],"properties":{"asset_id":{"type":"string"},"from_direction":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TrendDirection"}]},"min_strength":{"type":"number","format":"double"}}},"TrendReversalResponse":{"type":"object","required":["coin","min_strength"],"properties":{"coin":{"$ref":"#/components/schemas/AssetExtended"},"from_direction":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/TrendDirection"}]},"min_strength":{"type":"number","format":"double"}}},"ValidationConfig":{"type":"object","required":["input_field","input_placeholder"],"properties":{"input_field":{"type":"string"},"input_placeholder":{"type":"string"}}},"VolatilityRegime":{"type":"string","enum":["Low","Normal","High","Extreme"]},"VolumeData":{"type":"object","required":["timestamp","volume","defi_volume","stablecoin_volume","derivatives_volume"],"properties":{"defi_volume":{"type":"integer","format":"int64"},"derivatives_volume":{"type":"integer","format":"int64"},"stablecoin_volume":{"type":"integer","format":"int64"},"timestamp":{"type":"string","format":"date-time"},"volume":{"type":"integer","format":"int64"}}},"VolumeSkew":{"type":"string","enum":["BullishHeavy","BearishHeavy","Balanced"]},"VolumeSpike":{"type":"object","required":["asset_id","target"],"properties":{"asset_id":{"type":"string"},"target":{"type":"number","format":"double"}}},"VolumeSpikeResponse":{"type":"object","required":["coin","target"],"properties":{"coin":{"$ref":"#/components/schemas/AssetExtended"},"target":{"type":"number","format":"double"}}},"VolumeTrend":{"type":"string","enum":["Increasing","Decreasing","Flat"]},"Warning":{"oneOf":[{"type":"object","description":"The venue was read and reported no stop.","required":["code"],"properties":{"code":{"type":"string","enum":["no_stop_recorded"]}}},{"type":"object","description":"No venue TP/SL read has been written for this position — a stop may\nexist at the venue; absence here is not evidence.","required":["code"],"properties":{"code":{"type":"string","enum":["protective_levels_not_synced"]}}},{"type":"object","required":["code"],"properties":{"code":{"type":"string","enum":["direction_unknown"]}}},{"type":"object","description":"A recorded stop that is not a positive finite price (`0.0`/NaN from a\ngarbled sync); a NaN serializes as `null` here. Every stop-based read\nis `None` because of it, and this is what says so.","required":["stop_loss","code"],"properties":{"code":{"type":"string","enum":["stop_unusable"]},"stop_loss":{"type":"number","format":"double"}}},{"type":"object","description":"The stop is at or through the current mark — it would trigger\ninstantly, so it protects nothing. Not the trailed-into-profit case,\nwhich is `StopPlacement::breakeven_or_better`.","required":["stop_loss","mark","code"],"properties":{"code":{"type":"string","enum":["stop_wrong_side"]},"mark":{"type":"number","format":"double"},"stop_loss":{"type":"number","format":"double"}}},{"type":"object","required":["distance_atr","code"],"properties":{"code":{"type":"string","enum":["stop_within_noise"]},"distance_atr":{"type":"number","format":"double"}}},{"type":"object","required":["entry_range","code"],"properties":{"code":{"type":"string","enum":["stop_inside_entry_zone"]},"entry_range":{"type":"array","items":false,"prefixItems":[{"type":"number","format":"double"},{"type":"number","format":"double"}]}}},{"type":"object","required":["entry_range","code"],"properties":{"code":{"type":"string","enum":["stop_inside_opposing_zone"]},"entry_range":{"type":"array","items":false,"prefixItems":[{"type":"number","format":"double"},{"type":"number","format":"double"}]}}},{"type":"object","required":["stop_loss","invalidation","code"],"properties":{"code":{"type":"string","enum":["stop_beyond_invalidation"]},"invalidation":{"type":"number","format":"double"},"stop_loss":{"type":"number","format":"double"}}},{"type":"object","required":["liquidation_price","stop_loss","code"],"properties":{"code":{"type":"string","enum":["liquidation_before_stop"]},"liquidation_price":{"type":"number","format":"double"},"stop_loss":{"type":"number","format":"double"}}},{"type":"object","description":"Liquidation sits within 1 ATR of mark — ordinary noise can reach it.\nSame 1-ATR bound as [`Warning::StopWithinNoise`]: one definition of\n\"inside the noise band\" for both protective levels.","required":["distance_atr","code"],"properties":{"code":{"type":"string","enum":["liquidation_within_noise"]},"distance_atr":{"type":"number","format":"double"}}},{"type":"object","required":["bars_held_at_least","horizon_bars","code"],"properties":{"bars_held_at_least":{"type":"integer","minimum":0},"code":{"type":"string","enum":["horizon_exceeded"]},"horizon_bars":{"type":"integer","minimum":0}}}],"description":"Structured warning codes carrying the numbers that triggered them."},"ZoneEntryRule":{"type":"string","enum":["FirstTouch","RejectionConfirmation"]},"ZoneMatch":{"type":"object","description":"The published zone this position was matched against — a snapshot of the\nfields the diff-against-plan factors read, so the response is\nself-contained without the client re-fetching `/v2/analysis`.","required":["zone_type","entry_range","stop_loss","invalidation","take_profits","risk_reward_ratio","confidence","confidence_band"],"properties":{"confidence":{"type":"number","format":"double"},"confidence_band":{"$ref":"#/components/schemas/ConfidenceBand"},"entry_range":{"type":"array","items":false,"prefixItems":[{"type":"number","format":"double"},{"type":"number","format":"double"}]},"invalidation":{"type":"number","format":"double"},"risk_reward_ratio":{"type":"number","format":"double"},"stop_loss":{"type":"number","format":"double"},"take_profits":{"type":"array","items":{"$ref":"#/components/schemas/TakeProfit"}},"zone_type":{"$ref":"#/components/schemas/ZoneType"}}},"ZoneType":{"type":"string","enum":["Buy","Sell","Accumulation","Distribution"]}},"securitySchemes":{"api_key":{"type":"apiKey","in":"header","name":"x-api-key"},"session":{"type":"apiKey","in":"header","name":"x-session-token"}}},"tags":[{"name":"Analysis","description":"Technical analysis endpoints"},{"name":"Assets","description":"Asset discovery, search, screener, and signal-based feeds"},{"name":"Insights","description":"Market-wide stats, funding rates, and fear/greed leaderboard"},{"name":"News","description":"Aggregated crypto news feed"},{"name":"Promotions","description":"Affiliate promotions and time-gated promo-video campaigns"},{"name":"Exchanges","description":"Exchange directory with volume, promotions, and connection status"},{"name":"Trades","description":"Executed trade (fill) history synced from connected exchange accounts"},{"name":"Orders","description":"Trade history rolled up into orders — fills grouped by their parent order with aggregates"},{"name":"Notifications","description":"Price/zone/breakout/etc alert configuration for the authenticated user"}]}