openapi: 3.1.0
info:
  title: Flowseeker — Skylit Public API
  version: "1.0.0"
  summary: Real-time and historical options-flow analytics as a public HTTP API.
  description: |
    Flowseeker exposes Skylit's real-time options-flow scoring stack — Flow
    Score, FlowBonus, VWF/SDF/FIR aggregates, sector rotation, and market
    breadth — as a versioned public HTTP API. Options data is licensed via
    OPRA and updates in real time during market hours.

    **Authentication.** Send your Skylit API key as a bearer token:

        Authorization: Bearer <key>

    A request without an `Authorization` header gets `401`; an invalid, revoked
    or expired key gets `403`.

    **Rate limits & quotas.** Per-minute limits are enforced by the Skylit
    gateway and surfaced on every response via `X-RateLimit-Limit`,
    `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. `429` includes
    `Retry-After`. When your credit balance runs out, requests return `402`
    `insufficient_credits`.

    **Credits & billing.** Every `/v1/*` data request debits a fixed number of
    **credits** from your account's shared Skylit balance (the same balance used
    across all Skylit public APIs). Each chargeable response carries
    `X-Credits-Remaining: <balance>`. The credit is charged before the request
    is served, so a `5xx` still bills. Cost is priced by server-side work and
    data volume returned:

    | Tier | Credits | Routes |
    |------|--------:|--------|
    | Light | 1 | single-key reads, ratios, scores, discovery/search lists, expirations, rvol, per-contract bull/bear, the flow feed |
    | Medium | 3 | charts, full chains, by-strike matrix, momentum/baseline/strikes, tide, aggregate, sweeps, market, sector, market-breadth, chain bull/bear, unusual-volume/oi screeners, dark-pool top-prints |
    | Heavy | 5 | trade feeds (`/trades`, dark-pool trades), history (`/history`, `historical-compare`), bulk endpoints |

    `/v1/openapi.json` is free. When you run out of credits the API returns
    `402` `insufficient_credits`; a suspended account returns `403`
    `account_suspended`; a transient billing error returns `503`
    `credit_check_failed` (safe to retry). These billing codes are lowercase
    (`insufficient_credits`, `account_suspended`, `credit_check_failed`) — the
    platform-wide convention for credit errors.

    **Response envelope.** All success responses share one shape:

        { "data": <payload>, "meta": { "timestamp": "...", "requestId": "..." } }

    All errors share one shape:

        { "error": { "code": "NOT_FOUND", "message": "..." } }

    Field names are camelCase throughout. Codes are stable, machine-readable
    `SCREAMING_SNAKE_CASE` strings — the message text may evolve.

    **Freshness.** Endpoints with a `timeframe` parameter (e.g. `/v1/flow/{ticker}`)
    return data through the last completed bucket. Intra-bucket fills land on the
    next request. The cumulative `/v1/flow/market-breadth` and aggregate scoring
    endpoints update at most once per second.

servers:
  - url: https://flow-api.skylit.ai
    description: Production

security:
  - bearerApiKey: []

tags:
  - name: Flow
    description: Per-ticker flow feed, aggregate scoring, baselines, and momentum.
  - name: Sweeps
    description: Aggregated multi-exchange sweeps and sweep-only feeds.
  - name: Sector
    description: Sector- and industry-level flow rollups.
  - name: Market
    description: Market-wide overview, breadth, advance/decline, sector rotation, and net-premium tide.
  - name: Analytics
    description: |
      Standalone analytics endpoints — Vol/OI accumulation, moneyness segmentation,
      and timeframe-aggregated sentiment scores.
  - name: Ratios
    description: |
      Bid/ask/mid distribution analyses at chain and contract granularity, plus
      call/put-aware bull/bear pressure breakdowns.
  - name: Scoring
    description: Per-trade scoring, sentiment, and intent classification.
  - name: Underlying
    description: |
      Ticker-level discovery and analytics — top tickers by flow,
      bulk stats, intraday chart bars, raw enriched trades, strike /
      expiration distributions, option chain, and historical rollups.
  - name: Contract
    description: |
      Per-contract discovery and analytics — top contracts by flow,
      unusual volume / OI scans, contract stats, intraday chart bars,
      raw enriched trades, and historical rollups.
  - name: Dark Pool
    description: |
      Off-exchange (TRF) prints — paginated dark-pool trades and the
      largest individual prints per ticker. No side / BBO / greeks.
  - name: Meta
    description: API metadata (this OpenAPI document, etc.).

paths:
  /v1/flow/{ticker}:
    get:
      summary: Raw flow feed for a ticker (Flow Score + FlowBonus per trade)
      operationId: getFlow
      tags: [Flow]
      description: |
        Returns the most recent options trades for `{ticker}` within the
        requested timeframe, each scored on Skylit's directional Flow Score
        (-100 → +100) and conviction-weighted FlowBonus. The response also
        includes timeframe-level VWF / SDF / FIR aggregates.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - $ref: "#/components/parameters/Timeframe"
        - name: limit
          in: query
          required: false
          description: Max trades returned. Server caps this at 500.
          schema: { type: integer, default: 100, minimum: 1, maximum: 500 }
        - name: min_premium
          in: query
          required: false
          description: Minimum total premium per trade (USD).
          schema: { type: number, format: double, example: 50000 }
        - name: option_type
          in: query
          required: false
          description: Filter to calls or puts. `all` returns both.
          schema: { type: string, enum: [call, put, all], default: all }
        - name: trade_type
          in: query
          required: false
          description: Filter by trade type. Comma-separated for multiple.
          schema: { type: string, enum: [sweep, multi_leg, all], default: all }
        - name: moneyness
          in: query
          required: false
          description: |
            Moneyness category filter. Comma-separated for multiple
            (e.g. `otm,deep_otm`). Unknown tokens are ignored.
          schema:
            type: string
            enum: [deep_itm, itm, atm, otm, deep_otm, all]
            default: all
        - name: start_time
          in: query
          required: false
          description: |
            Optional lower bound for the trade window. Accepts RFC 3339
            (`2026-05-27T13:30:00Z`) or Unix seconds. Omit to use the timeframe.
          schema: { type: string }
        - name: end_time
          in: query
          required: false
          description: Optional upper bound (RFC 3339 or Unix seconds).
          schema: { type: string }
        - name: max_premium
          in: query
          required: false
          description: Maximum total premium per trade (USD).
          schema: { type: number, format: double }
        - name: min_contracts
          in: query
          required: false
          description: Minimum contract size per trade.
          schema: { type: integer, minimum: 0 }
        - name: max_contracts
          in: query
          required: false
          description: Maximum contract size per trade.
          schema: { type: integer, minimum: 0 }
        - name: single_leg_only
          in: query
          required: false
          description: If `true`, exclude trades flagged as part of a multi-leg structure.
          schema: { type: boolean, default: false }
        - name: min_dte
          in: query
          required: false
          description: Minimum days to expiration.
          schema: { type: integer }
        - name: max_dte
          in: query
          required: false
          description: Maximum days to expiration.
          schema: { type: integer }
        - name: min_strike
          in: query
          required: false
          description: Minimum strike price (inclusive).
          schema: { type: number, format: double }
        - name: max_strike
          in: query
          required: false
          description: Maximum strike price (inclusive).
          schema: { type: number, format: double }
        - name: expiration
          in: query
          required: false
          description: Filter to a single expiration date (YYYY-MM-DD).
          schema: { type: string, format: date }
        - name: conviction_weights
          in: query
          required: false
          description: |
            Optional JSON object overriding the Flow Score conviction weights.
            Weights must be non-negative and sum to within 0.95–1.05, else 400.
          schema: { type: string }
        - name: min_flow_score
          in: query
          required: false
          description: Filter to trades with `flowScore` ≥ this value (-100..100).
          schema: { type: integer, minimum: -100, maximum: 100 }
        - name: min_flow_bonus
          in: query
          required: false
          description: Filter to trades with `flowBonus` ≥ this value.
          schema: { type: integer, minimum: 0 }
        - name: min_rvol
          in: query
          required: false
          description: Filter to trades with relative volume ≥ this multiple.
          schema: { type: number, format: double, minimum: 0, example: 2.0 }
        - name: include_clusters
          in: query
          required: false
          description: |
            If `true`, attach `cluster*` fields when a trade is part of a
            multi-leg cluster (sweep, condor, etc.).
          schema: { type: boolean, default: true }
        - name: date
          in: query
          required: false
          description: Trading date (YYYY-MM-DD). Defaults to current trading date.
          schema: { type: string, format: date, example: "2026-05-27" }
      responses:
        "200":
          description: Flow feed for `{ticker}`.
          headers:
            X-RateLimit-Limit: { schema: { type: integer } }
            X-RateLimit-Remaining: { schema: { type: integer } }
            X-RateLimit-Reset: { schema: { type: integer } }
            X-Credits-Remaining:
              description: Credit balance remaining after this request was charged.
              schema: { type: integer }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FlowSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/flow/{ticker}/aggregate:
    get:
      summary: Aggregate flow over an arbitrary [start, end] window
      operationId: getFlowAggregate
      tags: [Flow]
      description: |
        Server-side aggregation across an arbitrary `[startTime, endTime]`
        window — no row cap. Returns trade/sweep counts, VWF/SDF/FIR, and a
        bullish/bearish/neutral premium split with a one-line interpretation.
        Useful for arbitrary slicing without paging the full trade list.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - $ref: "#/components/parameters/StartTime"
        - $ref: "#/components/parameters/EndTime"
        - name: option_type
          in: query
          required: false
          schema: { type: string, enum: [call, put, all], default: all }
        - name: min_premium
          in: query
          required: false
          schema: { type: number, format: double }
        - name: exclude_multi_leg
          in: query
          required: false
          description: Exclude trades flagged as part of a multi-leg structure.
          schema: { type: boolean, default: false }
        - name: min_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: max_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: date
          in: query
          required: false
          schema: { type: string, format: date }
      responses:
        "200":
          description: Window-aggregated flow scores.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FlowAggregateSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/flow/{ticker}/tide:
    get:
      summary: Per-ticker net-premium time series ("flow tide")
      operationId: getFlowTide
      tags: [Flow]
      description: |
        Bucketed bullish vs bearish premium time series for a single ticker,
        with cumulative net premium and per-bucket VWF/SDF/FIR. The
        ticker-level analogue of `/v1/market/tide`.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - $ref: "#/components/parameters/StartTime"
        - $ref: "#/components/parameters/EndTime"
        - $ref: "#/components/parameters/Bucket"
        - name: option_type
          in: query
          required: false
          schema: { type: string, enum: [call, put, all], default: all }
        - name: min_premium
          in: query
          required: false
          schema: { type: number, format: double }
        - name: exclude_multi_leg
          in: query
          required: false
          schema: { type: boolean, default: false }
        - name: min_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: max_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: date
          in: query
          required: false
          schema: { type: string, format: date }
      responses:
        "200":
          description: Per-bucket flow tide bars.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FlowTideSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/flow/{ticker}/baseline:
    get:
      summary: Trailing per-time-of-day flow baseline (avg + stddev)
      operationId: getFlowBaseline
      tags: [Flow]
      description: |
        Time-of-day baseline buckets for `{ticker}` — average and standard
        deviation of trade count, premium, and FIR per intraday bucket over a
        configurable lookback window. The reference distribution behind
        `/v1/flow/{ticker}/momentum` z-scores.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - $ref: "#/components/parameters/Bucket"
        - name: lookback_days
          in: query
          required: false
          description: Trailing window size in trading days.
          schema: { type: integer, default: 20, minimum: 1, maximum: 30 }
        - name: start_time_of_day
          in: query
          required: false
          description: Lower bound of intraday window (HH:MM ET).
          schema: { type: string, default: "09:30", example: "09:30" }
        - name: end_time_of_day
          in: query
          required: false
          description: Upper bound of intraday window (HH:MM ET).
          schema: { type: string, default: "16:00", example: "16:00" }
        - name: min_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: max_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
      responses:
        "200":
          description: Per-bucket baseline statistics.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BaselineSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/flow/{ticker}/momentum:
    get:
      summary: Live momentum signal vs baseline (5m / 30m / 1h windows)
      operationId: getFlowMomentum
      tags: [Flow]
      description: |
        Compares the current 5-minute, 30-minute, and 1-hour flow against the
        trailing per-time-of-day baseline (`/v1/flow/{ticker}/baseline`).
        Returns z-scores for the 5-minute window and a one-token `trend`
        classification.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - name: as_of
          in: query
          required: false
          description: Replay anchor. Accepts RFC 3339 or Unix seconds. Defaults to "now".
          schema: { type: string }
        - name: lookback_days
          in: query
          required: false
          description: Trailing window size in trading days.
          schema: { type: integer, default: 20, minimum: 1, maximum: 30 }
        - name: min_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: max_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
      responses:
        "200":
          description: Live momentum metrics + signal.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MomentumSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/flow/{ticker}/strikes:
    get:
      summary: Strike-level flow concentration
      operationId: getFlowStrikes
      tags: [Flow]
      description: |
        Where the directional money is going for `{ticker}`. Top-N strikes
        by selected ordering (premium, net premium, volume, etc.) with
        bullish/bearish premium split, ask/bid mix, and OI context. Includes
        a top-3 concentration summary.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - $ref: "#/components/parameters/StartTime"
        - $ref: "#/components/parameters/EndTime"
        - name: top_n
          in: query
          required: false
          description: Number of strikes to return.
          schema: { type: integer, default: 20, minimum: 1, maximum: 100 }
        - name: right
          in: query
          required: false
          description: Restrict to calls or puts only.
          schema: { type: string, enum: [call, put] }
        - name: min_premium
          in: query
          required: false
          schema: { type: number, format: double }
        - name: order_by
          in: query
          required: false
          schema:
            type: string
            enum: [net_premium, total_premium, volume]
            default: net_premium
        - name: min_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: max_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
      responses:
        "200":
          description: Top-N strike rollup.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StrikesSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/flow/{ticker}/historical-compare:
    get:
      summary: Today's flow vs trailing average (with similar-days lookback)
      operationId: getFlowHistoricalCompare
      tags: [Flow]
      description: |
        Compares the current trading day's premium / volume / net premium /
        call-put ratio against the trailing 20-trading-day average for the
        same ticker. Returns absolute deltas, percentile rankings, and the
        five most-similar past trading days.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - name: date
          in: query
          required: false
          description: Date to evaluate (YYYY-MM-DD). Defaults to today.
          schema: { type: string, format: date }
      responses:
        "200":
          description: Today vs historical comparison.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/HistoricalCompareSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/flow/sector/{sector}:
    get:
      summary: Sector- or industry-level flow aggregation
      operationId: getSectorFlow
      tags: [Sector]
      description: |
        Aggregates options flow across all tickers in a GICS sector. The
        `{sector}` path parameter accepts either a sector ETF symbol
        (`XLK`, `XLF`, `XLE`, …) or a sector name (`Technology`,
        `Financials`, …). Returns sector-level metrics, top-contributor
        tickers, and an industry-level breakdown.
      parameters:
        - name: sector
          in: path
          required: true
          description: |
            Sector ETF symbol (`XLK`, `XLF`, `XLE`, `XLV`, `XLY`, `XLP`,
            `XLU`, `XLI`, `XLB`, `XLRE`, `XLC`) or full sector name
            (`Technology`, `Financials`, `Healthcare`, etc.).
          schema: { type: string, example: XLK }
        - name: date
          in: query
          required: false
          schema: { type: string, format: date }
        - name: top_n
          in: query
          required: false
          schema: { type: integer, default: 10, minimum: 1, maximum: 50 }
      responses:
        "200":
          description: Sector flow aggregation.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SectorFlowSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/flow/market-breadth:
    get:
      summary: Market-wide breadth, advance/decline, and sector rotation
      operationId: getMarketBreadth
      tags: [Market]
      description: |
        Combines SPY/QQQ/IWM aggregate sentiment with an advance/decline
        ratio (over directional FIR) and per-sector rotation signals. Ideal
        as a single "is the market risk-on or risk-off right now" probe.
      parameters:
        - name: date
          in: query
          required: false
          description: Trading date (YYYY-MM-DD). Defaults to today.
          schema: { type: string, format: date }
        - name: fir_threshold
          in: query
          required: false
          description: |
            Absolute FIR threshold (in %) used to classify a ticker as
            advancing or declining. Tickers with `|fir| < threshold` count
            as unchanged.
          schema: { type: number, format: double, default: 10.0 }
      responses:
        "200":
          description: Market breadth + advance/decline + sector rotation.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MarketBreadthSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/sweeps/{ticker}:
    get:
      summary: Aggregated multi-exchange sweep activity
      operationId: getSweepMonitor
      tags: [Sweeps]
      description: |
        Returns "logical sweeps" — multi-exchange splits of one large order
        grouped by contract within a one-second execution window. Each row
        carries the venue list, total contracts/premium, spread position,
        moneyness bucket, and Skylit Flow Score / FlowBonus. The summary
        block adds population-level Sweep Dominance Factor (SDF) and
        bullish/bearish counts extrapolated from the full-day total.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - name: timeframe
          in: query
          required: false
          description: |
            Trailing window. Currently only restricts the trading day;
            `5m`/`15m`/`1h`/`4h` reserved for future intraday filtering.
          schema:
            type: string
            enum: [5m, 15m, 1h, 4h, 1d]
            default: "1h"
        - name: min_premium
          in: query
          required: false
          schema: { type: number, format: double }
        - name: option_type
          in: query
          required: false
          schema: { type: string, enum: [call, put, all], default: all }
        - name: moneyness
          in: query
          required: false
          schema:
            type: string
            enum: [deep_itm, itm, atm, otm, deep_otm, all]
            default: all
        - name: min_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: max_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: min_strike
          in: query
          required: false
          schema: { type: number, format: double }
        - name: max_strike
          in: query
          required: false
          schema: { type: number, format: double }
        - name: expiration
          in: query
          required: false
          description: Restrict to a single expiration date (`YYYY-MM-DD`).
          schema: { type: string, format: date }
        - name: limit
          in: query
          required: false
          description: Max sweep rows returned (server caps at 500).
          schema: { type: integer, default: 100, minimum: 1, maximum: 500 }
        - name: date
          in: query
          required: false
          schema: { type: string, format: date }
      responses:
        "200":
          description: Sweep activity for `{ticker}`.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SweepSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/aggregate/{ticker}:
    get:
      summary: Aggregate sentiment scoring across timeframes (VWF / SDF / FIR / Composite)
      operationId: getAggregate
      tags: [Analytics]
      description: |
        Returns a Composite directional score plus its VWF / SDF / FIR
        components for one or more trailing timeframes (intraday or
        multi-day). Optional moneyness breakdown, optional time-decay
        weighting, and a comparative trend block contrasting short- vs
        long-horizon sentiment.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - name: timeframes
          in: query
          required: false
          description: |
            Comma-separated timeframes, or `all`. Supported atoms:
            `1h, 4h, 1d, 7d, 30d, 90d`. `all` expands to all six. Unknown
            atoms are treated as a single trading day.
          schema: { type: string, default: "1d", example: "1d,7d,30d" }
        - name: include_breakdown
          in: query
          required: false
          description: Attach the per-timeframe VWF/SDF/FIR component split.
          schema: { type: boolean, default: true }
        - name: include_moneyness
          in: query
          required: false
          description: Attach a `byMoneyness` array (deep_itm → deep_otm).
          schema: { type: boolean, default: false }
        - name: moneyness_filter
          in: query
          required: false
          schema:
            type: string
            enum: [deep_itm, itm, atm, otm, deep_otm, all]
            default: all
        - name: expiration_filter
          in: query
          required: false
          description: Restrict to one expiration bucket.
          schema:
            type: string
            enum: ["0dte", weekly, monthly, leaps, all]
            default: all
        - name: time_decay
          in: query
          required: false
          description: Apply exponential time decay to VWF / SDF / FIR components.
          schema: { type: boolean, default: false }
        - name: time_decay_half_life
          in: query
          required: false
          description: Half-life in minutes for the decay (only applied when `timeDecay=true`).
          schema: { type: integer, minimum: 1, default: 30 }
        - name: date
          in: query
          required: false
          schema: { type: string, format: date }
      responses:
        "200":
          description: Aggregate sentiment by timeframe.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AggregateSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/vol-oi/{ticker}:
    get:
      summary: Volume-vs-Open-Interest accumulation analysis
      operationId: getVolOi
      tags: [Analytics]
      description: |
        Distinguishes new position building (accumulation) from position
        closing (distribution) by bucketing Vol/OI ratios per option type
        and moneyness band. Returns an overall accumulation score (0–100),
        an estimate of the share of volume representing new positions, and
        a one-token signal (`strong_accumulation` → `low_activity`).
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - name: timeframe
          in: query
          required: false
          schema: { type: string, enum: [daily, weekly], default: daily }
        - name: option_type
          in: query
          required: false
          schema: { type: string, enum: [call, put, all], default: all }
        - name: moneyness
          in: query
          required: false
          schema:
            type: string
            enum: [otm_10plus, otm_5_10, otm_3_5, atm_itm, all]
            default: all
        - name: min_oi
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: date
          in: query
          required: false
          schema: { type: string, format: date }
      responses:
        "200":
          description: Vol/OI breakdown with accumulation score.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VolOiSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/moneyness/{ticker}:
    get:
      summary: Moneyness breakdown with pattern detection
      operationId: getMoneyness
      tags: [Analytics]
      description: |
        Splits calls and puts across `deep_itm / itm / atm / otm /
        deep_otm` buckets with premium, sentiment, percentage of total,
        and trade count. Surfaces detected patterns (e.g. heavy OTM call
        accumulation, ATM concentration, deep-OTM lottery tickets) and a
        directional `signal`/`dominantStrategy` interpretation.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - name: timeframe
          in: query
          required: false
          schema: { type: string, enum: [intraday, daily, 7d, 30d], default: daily }
        - name: date
          in: query
          required: false
          schema: { type: string, format: date }
        - name: min_premium
          in: query
          required: false
          schema: { type: number, format: double }
      responses:
        "200":
          description: Moneyness breakdown with patterns + interpretation.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MoneynessSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/score/{trade_id}:
    get:
      summary: Detailed scoring for a single trade
      operationId: getTradeScore
      tags: [Scoring]
      description: |
        Returns sentiment, urgency, and confidence scores for an individual
        trade plus a spread-level breakdown and full trade context (ticker,
        strike, expiration, sweep/block flags, moneyness). Used to drill
        into a single row from `/v1/flow/{ticker}`.

        The `trade_id` path parameter accepts three formats: the canonical
        `flow_{hex}_{idx}` id returned by the flow feed, a bare hex
        timestamp (`188afe42c3a77af2`), or a raw nanosecond integer.
      parameters:
        - name: trade_id
          in: path
          required: true
          description: |
            Trade id (`flow_{hex}_{idx}`, bare hex timestamp, or raw nanos).
          schema:
            type: string
            example: "flow_188afe42c3a77af2_0"
      responses:
        "200":
          description: Trade scoring breakdown.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TradeScoreSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/chain-ratio/{ticker}:
    get:
      summary: Chain-level bid/ask/mid distribution
      operationId: getChainRatio
      tags: [Ratios]
      description: |
        Aggregates a ticker's full option chain to surface buying vs
        selling pressure (`askRatio`, `bidRatio`, `midRatio`,
        `aggressionRatio`), call/put balance, and ATM/OTM concentration.
        Returns a `bias`, `aggression`, and `confidence` interpretation.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - name: date
          in: query
          required: false
          schema: { type: string, format: date }
        - name: timeframe
          in: query
          required: false
          schema:
            type: string
            enum: [5m, 15m, 1h, 4h, 1d]
            default: "1d"
        - name: option_type
          in: query
          required: false
          schema: { type: string, enum: [call, put, all], default: all }
        - name: min_premium
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: min_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: max_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
      responses:
        "200":
          description: Chain ratio analysis with interpretation.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ChainRatioSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/contract-ratio/{symbol}:
    get:
      summary: Per-contract bid/ask/mid distribution
      operationId: getContractRatio
      tags: [Ratios]
      description: |
        Single-contract counterpart to `/v1/chain-ratio/{ticker}`. Returns
        `askRatio`, `bidRatio`, `midRatio`, `aggressionRatio`, and a
        `bias`/`aggression`/`confidence` interpretation for one specific
        OPRA option symbol.
      parameters:
        - $ref: "#/components/parameters/OptionSymbol"
        - name: date
          in: query
          required: false
          schema: { type: string, format: date }
        - name: timeframe
          in: query
          required: false
          schema:
            type: string
            enum: [5m, 15m, 1h, 4h, 1d]
            default: "1d"
        - name: min_premium
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
      responses:
        "200":
          description: Contract ratio analysis with interpretation.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContractRatioSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/chain-bull-bear/{ticker}:
    get:
      summary: Chain-level call/put-aware bull/bear pressure
      operationId: getChainBullBear
      tags: [Ratios]
      description: |
        Folds option type into the bid/ask/mid signal: a call lifted at the
        ask is bullish, a put hit at the bid is also bullish (put selling),
        etc. Returns overall bull/bear/neutral percentages plus call-only
        and put-only bull breakdowns so callers can tell whether the
        directional pressure originates from call buying, put selling, or
        both.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - name: date
          in: query
          required: false
          schema: { type: string, format: date }
        - name: timeframe
          in: query
          required: false
          schema:
            type: string
            enum: [5m, 15m, 1h, 4h, 1d]
            default: "1d"
        - name: option_type
          in: query
          required: false
          schema: { type: string, enum: [call, put, all], default: all }
        - name: min_premium
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: min_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: max_dte
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
      responses:
        "200":
          description: Chain-level bull/bear analysis.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ChainBullBearSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/contract-bull-bear/{symbol}:
    get:
      summary: Per-contract call/put-aware bull/bear pressure
      operationId: getContractBullBear
      tags: [Ratios]
      description: |
        Single-contract counterpart to `/v1/chain-bull-bear/{ticker}`.
        Maps a contract's trades to bull/bear/neutral buckets using both
        side (bid/ask/mid) and option type, so a bullish put-seller and a
        bullish call-buyer both register as bullish pressure.
      parameters:
        - $ref: "#/components/parameters/OptionSymbol"
        - name: date
          in: query
          required: false
          schema: { type: string, format: date }
        - name: timeframe
          in: query
          required: false
          schema:
            type: string
            enum: [5m, 15m, 1h, 4h, 1d]
            default: "1d"
        - name: min_premium
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
      responses:
        "200":
          description: Per-contract bull/bear analysis.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContractBullBearSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/market/overview:
    get:
      summary: Market-wide flow overview for the current trading day
      operationId: getMarketOverview
      tags: [Market]
      description: |
        Returns market-wide call/put premium, total volume, directional
        bullish/bearish premium, FIR, premium relative volume vs the
        trailing 20-day baseline at the same time of day, and the top 10
        tickers by total premium. Optionally narrowed to a comma-separated
        ticker filter.
      parameters:
        - name: tickers
          in: query
          required: false
          description: |
            Comma-separated list of tickers (e.g. `AAPL,NVDA,SPY`). When
            omitted, returns true market-wide stats over every ticker.
          schema: { type: string, example: "AAPL,NVDA,SPY" }
      responses:
        "200":
          description: Market-wide overview snapshot.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MarketOverviewSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/market/tide:
    get:
      summary: Bucketed market-wide net call premium / net put premium time series
      operationId: getMarketTide
      tags: [Market]
      description: |
        Returns the market-wide intraday "tide" — bucketed Net Call
        Premium and Net Put Premium series with both per-bucket and
        cumulative values, plus an SPY price overlay for context. Two
        directional flavors are emitted per bar: the standard `ncp`/`npp`
        (call-buying minus call-selling, etc.) and a `manualNcp`/
        `manualNpp` variant with fewer exclusions applied, for callers
        that need raw flow.
      parameters:
        - name: interval
          in: query
          required: false
          description: |
            Trailing window length. Defaults to a single trading day
            (`1D`); multi-day intervals roll up history at the chosen
            bucket size.
          schema:
            type: string
            enum: [1D, 2D, 3D, 5D, 7D, 14D, 30D, 45D, 60D, 90D, 120D, 180D, 360D]
            default: "1D"
        - name: bucket
          in: query
          required: false
          description: Bucket size for the time series.
          schema:
            type: string
            enum: [1min, 5min, 15min, 30min, 1d, 1w]
            default: "5min"
        - name: date
          in: query
          required: false
          description: Trading date anchor (`YYYY-MM-DD`). Defaults to today.
          schema: { type: string, format: date }
        - name: exclude_multi_leg
          in: query
          required: false
          description: Exclude multi-leg / spread trades from the directional totals.
          schema: { type: boolean, default: false }
        - name: exclude_deep_itm
          in: query
          required: false
          description: |
            Exclude deep in-the-money trades (`moneyness_percent < -20`) from
            the directional totals.
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: Market tide bars.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MarketTideSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  # ── Underlying (ticker discovery & analytics) ──────────────────────────
  /v1/underlying:
    get:
      summary: List underlyings active on a date
      operationId: listUnderlyings
      tags: [Underlying]
      description: |
        Lists every ticker that traded options on the requested date,
        ordered by total premium (descending). Useful as a starting point
        for discovery or for repopulating the universe of tradable tickers.
      parameters:
        - in: query
          name: limit
          required: false
          description: Maximum rows to return. Server caps at 500.
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
        - $ref: "#/components/parameters/DateQuery"
        - in: query
          name: min_premium
          schema: { type: number, format: double, minimum: 0 }
          description: Minimum total premium (USD) for the day.
        - in: query
          name: min_volume
          schema: { type: integer, minimum: 0 }
          description: Minimum total option volume for the day.
      responses:
        "200":
          description: Tickers ranked by total premium.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TickerListSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/underlying/search:
    get:
      summary: Prefix-search active tickers
      operationId: searchUnderlyings
      tags: [Underlying]
      description: |
        Case-insensitive prefix search over the active-tickers universe
        for the requested date. Use to power autocomplete UIs.
      parameters:
        - in: query
          name: q
          required: true
          description: Search prefix (1–10 characters, uppercased server-side).
          schema: { type: string, minLength: 1, maxLength: 10, example: "AAP" }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 50, default: 20 }
        - $ref: "#/components/parameters/DateQuery"
      responses:
        "200":
          description: Matching tickers (capped at 50).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TickerListSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/underlying/top/daily:
    get:
      summary: Top underlyings by daily flow
      operationId: getTopUnderlyingsDaily
      tags: [Underlying]
      description: |
        Top tickers for a single trading day, with call/put premium and
        volume splits, net premium, and call/put ratio. Sortable by
        `premium`, `volume`, `net_premium`, or `call_put_ratio`.
      parameters:
        - in: query
          name: limit
          required: false
          description: Maximum rows to return. Server caps at 500.
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
        - $ref: "#/components/parameters/DateQuery"
        - in: query
          name: min_premium
          schema: { type: number, format: double, minimum: 0 }
        - in: query
          name: min_volume
          schema: { type: integer, minimum: 0 }
        - in: query
          name: order_by
          schema:
            type: string
            enum: [premium, volume, net_premium, call_put_ratio]
            default: premium
        - in: query
          name: order
          schema: { type: string, enum: [asc, desc], default: desc }
      responses:
        "200":
          description: Top underlyings for the day.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TopUnderlyingListSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/underlying/top/weekly:
    get:
      summary: Top underlyings by trailing-5-day flow
      operationId: getTopUnderlyingsWeekly
      tags: [Underlying]
      description: |
        Same shape as `/v1/underlying/top/daily` but rolled up across the
        trailing 5 trading days ending on `date`.
      parameters:
        - in: query
          name: limit
          required: false
          description: Maximum rows to return. Server caps at 500.
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
        - $ref: "#/components/parameters/DateQuery"
        - in: query
          name: min_premium
          schema: { type: number, format: double, minimum: 0 }
        - in: query
          name: min_volume
          schema: { type: integer, minimum: 0 }
        - in: query
          name: order_by
          schema:
            type: string
            enum: [premium, volume, net_premium, call_put_ratio]
            default: premium
        - in: query
          name: order
          schema: { type: string, enum: [asc, desc], default: desc }
      responses:
        "200":
          description: Top underlyings for the trailing week.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TopUnderlyingListSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/underlying/bulk/stats:
    get:
      summary: Bulk underlying stats for a list of tickers
      operationId: getUnderlyingBulkStats
      tags: [Underlying]
      description: |
        Returns a single-day `UnderlyingStats` record per requested ticker.
        Tickers absent from the response had no options activity that day.
      parameters:
        - in: query
          name: tickers
          required: true
          description: Comma-separated list of tickers (max 50).
          schema: { type: string, example: "SPY,AAPL,NVDA" }
        - $ref: "#/components/parameters/DateQuery"
      responses:
        "200":
          description: Per-ticker stats.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UnderlyingStatsListSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/underlying/{ticker}/stats:
    get:
      summary: Daily stats for a single underlying
      operationId: getUnderlyingStats
      tags: [Underlying]
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - $ref: "#/components/parameters/DateQuery"
      responses:
        "200":
          description: Stats for the ticker on the requested date.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UnderlyingStatsSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/underlying/{ticker}/chart:
    get:
      summary: Intraday chart bars for a ticker
      operationId: getUnderlyingChart
      tags: [Underlying]
      description: |
        Returns time-bucketed bars aggregating options activity for the
        underlying (call/put volume + premium, P/C ratio, bid/ask
        execution split) plus the underlying stock price at each
        boundary. The same data that powers the chart modal in the
        Skylit UI.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - in: query
          name: interval
          required: true
          description: Trailing window covered by the bars (e.g. `1D`, `7D`, `30D`).
          schema: { type: string, example: "1D" }
        - in: query
          name: bucket
          required: true
          description: Bucket size.
          schema:
            type: string
            enum: [1min, 5min, 10min, 15min, 30min, 1d, 1w]
      responses:
        "200":
          description: Intraday chart bars.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UnderlyingChartSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/underlying/{ticker}/trades:
    get:
      summary: Raw enriched trades for a ticker
      operationId: getUnderlyingTrades
      tags: [Underlying]
      description: |
        Returns the raw enriched trade rows that feed the chart bars and
        the live feed. Supports rich filtering — sweep-only / multi-leg,
        moneyness, premium floor, DTE / strike / expiration windows.
        See `OptionTradeRow` below.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - in: query
          name: start
          description: |
            Lower time bound — ISO 8601 (e.g. `2026-01-12T09:30:00Z`) or
            Unix seconds. Defaults to start-of-trading-day.
          schema: { type: string }
        - in: query
          name: end
          description: |
            Upper time bound — ISO 8601 or Unix seconds. Defaults to now.
          schema: { type: string }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 500, default: 50 }
        - in: query
          name: only_sweeps
          schema: { type: boolean, default: false }
        - in: query
          name: only_multi_leg
          schema: { type: boolean, default: false }
        - in: query
          name: exclude_multi_leg
          schema: { type: boolean, default: false }
        - in: query
          name: moneyness
          schema: { type: string, enum: [ITM, ATM, OTM] }
        - in: query
          name: min_moneyness_pct
          schema: { type: number, format: double }
        - in: query
          name: max_moneyness_pct
          schema: { type: number, format: double }
        - in: query
          name: min_premium
          schema: { type: number, format: double, minimum: 0 }
        - in: query
          name: min_dte
          schema: { type: integer }
        - in: query
          name: max_dte
          schema: { type: integer }
        - in: query
          name: min_strike
          schema: { type: number, format: double }
        - in: query
          name: max_strike
          schema: { type: number, format: double }
        - in: query
          name: expiration
          schema: { type: string, format: date }
      responses:
        "200":
          description: Filtered enriched trades.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OptionTradeListSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/underlying/{ticker}/by-strike:
    get:
      summary: Premium / volume by strike
      operationId: getUnderlyingByStrike
      tags: [Underlying]
      description: |
        Strike-level distribution of call/put premium, volume, and OI for
        the requested window, plus chain-wide aggregates and a max-pain
        estimate.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - in: query
          name: interval
          schema: { type: string, enum: [1D, 1W, 7D], default: "1D" }
        - in: query
          name: dte_filter
          description: DTE bucket — `all`, `0-7`, `8-30`, `31-90`, or `90+`.
          schema:
            type: string
            enum: ["all", "0-7", "8-30", "31-90", "90+"]
            default: "all"
        - $ref: "#/components/parameters/DateQuery"
      responses:
        "200":
          description: Strike distribution.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StrikeDistributionSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/underlying/{ticker}/by-strike/{strike}/expirations:
    get:
      summary: Premium / volume by expiration for a strike
      operationId: getUnderlyingStrikeExpirations
      tags: [Underlying]
      description: |
        For a single strike on the underlying, breaks the requested
        window's premium and volume out by expiration date.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - in: path
          name: strike
          required: true
          description: Strike price (decimal allowed; e.g. `580` or `580.5`).
          schema: { type: string, example: "580" }
        - in: query
          name: interval
          schema: { type: string, enum: [1D, 1W, 7D], default: "1D" }
        - in: query
          name: dte_filter
          schema:
            type: string
            enum: ["all", "0-7", "8-30", "31-90", "90+"]
            default: "all"
        - $ref: "#/components/parameters/DateQuery"
      responses:
        "200":
          description: Expiration breakdown for the strike.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ExpirationDistributionSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/underlying/{ticker}/expirations:
    get:
      summary: List traded expirations for a ticker
      operationId: getUnderlyingExpirations
      tags: [Underlying]
      description: |
        Returns each expiration that traded on `date`, with per-expiration
        call/put volume + premium and the unique-contract count.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - $ref: "#/components/parameters/DateQuery"
      responses:
        "200":
          description: Expirations with per-expiry totals.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ExpirationListSuccess" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/underlying/{ticker}/chain:
    get:
      summary: Option chain snapshot
      operationId: getUnderlyingChain
      tags: [Underlying]
      description: |
        Snapshot of the option chain for a single expiration on the
        requested date — call & put volume, premium, OI, last IV, and
        last trade price per strike, plus the underlying price.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - in: query
          name: expiration
          required: true
          description: Expiration date (`YYYY-MM-DD`).
          schema: { type: string, format: date, example: "2026-08-21" }
        - in: query
          name: min_volume
          description: Suppress strikes whose total (call+put) volume is below this floor.
          schema: { type: integer, minimum: 0 }
        - $ref: "#/components/parameters/DateQuery"
      responses:
        "200":
          description: Chain snapshot.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ChainSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/underlying/{ticker}/history:
    get:
      summary: Daily history for a ticker
      operationId: getUnderlyingHistory
      tags: [Underlying]
      description: |
        Daily aggregates (premium, volume, call/put split, net premium)
        between `startDate` and `endDate` (inclusive), one row per
        trading day with activity.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - in: query
          name: start_date
          required: true
          schema: { type: string, format: date, example: "2026-01-02" }
        - in: query
          name: end_date
          required: true
          schema: { type: string, format: date, example: "2026-01-31" }
      responses:
        "200":
          description: One row per trading day.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UnderlyingHistorySuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/underlying/{ticker}/rvol:
    get:
      summary: Relative-volume bars for a ticker
      operationId: getUnderlyingRvol
      tags: [Underlying]
      description: |
        Time-bucketed bars with call/put volume + premium and an
        average-volume baseline computed from `avgPeriod` recent days,
        plus aggregate RVOL stats.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - in: query
          name: interval
          description: Trailing window — `{N}D` where N is 1–365 (e.g. `1D`, `7D`, `30D`).
          schema: { type: string, default: "1D", example: "1D" }
        - in: query
          name: bucket
          schema:
            type: string
            enum: [1min, 5min, 10min, 15min, 30min, 1d, 1w]
            default: "5min"
        - in: query
          name: avg_period
          description: Baseline lookback as `{N}d` (e.g. `14d`, `30d`). Max 365 days.
          schema: { type: string, default: "14d" }
        - $ref: "#/components/parameters/DateQuery"
        - in: query
          name: order_by
          schema:
            type: string
            enum: [rvol, volume, premium, time]
            default: time
        - in: query
          name: order
          description: Sort direction. Defaults to `asc` when `order_by=time`, otherwise `desc`.
          schema: { type: string, enum: [asc, desc] }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1 }
        - in: query
          name: format
          schema: { type: string, enum: [full, summary], default: full }
      responses:
        "200":
          description: RVOL bars + aggregate stats.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UnderlyingRvolSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  # ── Contract (per-option discovery & analytics) ────────────────────────
  /v1/contract/top/daily:
    get:
      summary: Top contracts by daily flow
      operationId: getTopContractsDaily
      tags: [Contract]
      description: |
        Single-day top-contract screener with full-spectrum filters
        (premium / volume / OI / IV / DTE / strike windows, call vs put,
        sweep vs multi-leg). Sortable by `premium`, `volume`, `oi`, or `iv`.
      parameters:
        - in: query
          name: limit
          required: false
          description: Maximum rows to return. Server caps at 500.
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
        - $ref: "#/components/parameters/DateQuery"
        - in: query
          name: ticker
          schema: { type: string, example: SPY }
        - in: query
          name: min_premium
          schema: { type: number, format: double }
        - in: query
          name: max_premium
          schema: { type: number, format: double }
        - in: query
          name: min_volume
          schema: { type: integer, minimum: 0 }
        - in: query
          name: max_volume
          schema: { type: integer, minimum: 0 }
        - in: query
          name: min_oi
          schema: { type: integer, minimum: 0 }
        - in: query
          name: max_oi
          schema: { type: integer, minimum: 0 }
        - in: query
          name: right
          schema: { type: string, enum: [C, P] }
        - in: query
          name: min_dte
          schema: { type: integer }
        - in: query
          name: max_dte
          schema: { type: integer }
        - in: query
          name: min_strike
          schema: { type: number, format: double }
        - in: query
          name: max_strike
          schema: { type: number, format: double }
        - in: query
          name: expiration
          schema: { type: string, format: date }
        - in: query
          name: min_iv
          schema: { type: number, format: double }
        - in: query
          name: max_iv
          schema: { type: number, format: double }
        - in: query
          name: order_by
          schema:
            type: string
            enum: [premium, volume, oi, iv]
            default: premium
        - in: query
          name: order
          schema: { type: string, enum: [asc, desc], default: desc }
        - in: query
          name: only_sweeps
          schema: { type: boolean, default: false }
        - in: query
          name: only_multi_leg
          schema: { type: boolean, default: false }
        - in: query
          name: exclude_multi_leg
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: Top contracts for the day.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TopContractListSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/contract/top/weekly:
    get:
      summary: Top contracts by trailing-5-day flow
      operationId: getTopContractsWeekly
      tags: [Contract]
      description: |
        Same shape as `/v1/contract/top/daily`, rolled up over the
        trailing 5 trading days ending on `date`.
      parameters:
        - in: query
          name: limit
          required: false
          description: Maximum rows to return. Server caps at 500.
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
        - $ref: "#/components/parameters/DateQuery"
        - in: query
          name: ticker
          schema: { type: string }
        - in: query
          name: min_premium
          schema: { type: number, format: double }
        - in: query
          name: max_premium
          schema: { type: number, format: double }
        - in: query
          name: min_volume
          schema: { type: integer, minimum: 0 }
        - in: query
          name: max_volume
          schema: { type: integer, minimum: 0 }
        - in: query
          name: min_oi
          schema: { type: integer, minimum: 0 }
        - in: query
          name: max_oi
          schema: { type: integer, minimum: 0 }
        - in: query
          name: right
          schema: { type: string, enum: [C, P] }
        - in: query
          name: min_dte
          schema: { type: integer }
        - in: query
          name: max_dte
          schema: { type: integer }
        - in: query
          name: min_strike
          schema: { type: number, format: double }
        - in: query
          name: max_strike
          schema: { type: number, format: double }
        - in: query
          name: expiration
          schema: { type: string, format: date }
        - in: query
          name: min_iv
          schema: { type: number, format: double }
        - in: query
          name: max_iv
          schema: { type: number, format: double }
        - in: query
          name: order_by
          schema:
            type: string
            enum: [premium, volume, oi, iv]
            default: premium
        - in: query
          name: order
          schema: { type: string, enum: [asc, desc], default: desc }
        - in: query
          name: only_sweeps
          schema: { type: boolean, default: false }
        - in: query
          name: only_multi_leg
          schema: { type: boolean, default: false }
        - in: query
          name: exclude_multi_leg
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: Top contracts for the trailing week.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TopContractListSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/contract/unusual-volume:
    get:
      summary: Contracts with unusual relative volume
      operationId: getContractUnusualVolume
      tags: [Contract]
      description: |
        Contracts whose volume on the target date is anomalously high
        relative to a `avgPeriod`-day baseline. Filters cover RVOL, raw
        volume, OI dynamics, premium, IV, moneyness, sweep / multi-leg,
        and ticker include / exclude lists. Sortable by `rvol`, `volume`,
        `premium`, `vol_oi`, or `oi_change`.
      parameters:
        - $ref: "#/components/parameters/DiscoveryLimit"
        - in: query
          name: min_rvol
          schema: { type: number, format: double, default: 2.0 }
        - in: query
          name: avg_period
          description: Baseline window as `{N}d`. Must be 2–365 days.
          schema: { type: string, default: "10d" }
        - in: query
          name: min_avg_volume
          schema: { type: integer, minimum: 0, default: 100 }
        - in: query
          name: min_premium
          schema: { type: number, format: double }
        - in: query
          name: ticker
          schema: { type: string }
        - in: query
          name: right
          schema: { type: string, enum: [C, P] }
        - in: query
          name: min_dte
          schema: { type: integer }
        - in: query
          name: max_dte
          schema: { type: integer }
        - in: query
          name: min_strike
          schema: { type: number, format: double }
        - in: query
          name: max_strike
          schema: { type: number, format: double }
        - in: query
          name: expiration
          schema: { type: string, format: date }
        - in: query
          name: date
          required: false
          description: |
            Target trading date (`YYYY-MM-DD`). Defaults to the **previous
            calendar day** (not the current trading date) since baselines
            need a settled session.
          schema: { type: string, format: date }
        - in: query
          name: order_by
          schema:
            type: string
            enum: [rvol, volume, premium, vol_oi, oi_change]
            default: rvol
        - in: query
          name: min_vol_oi_ratio
          schema: { type: number, format: double }
        - in: query
          name: min_oi_change
          schema: { type: integer }
        - in: query
          name: max_oi_change
          schema: { type: integer }
        - in: query
          name: only_sweeps
          schema: { type: boolean }
        - in: query
          name: only_multi_leg
          schema: { type: boolean }
        - in: query
          name: exclude_multi_leg
          schema: { type: boolean }
        - in: query
          name: min_oi_change_pct
          schema: { type: number, format: double }
        - in: query
          name: min_bid_imbalance
          schema: { type: number, format: double, minimum: 0, maximum: 1 }
        - in: query
          name: min_ask_imbalance
          schema: { type: number, format: double, minimum: 0, maximum: 1 }
        - in: query
          name: moneyness
          schema: { type: string, enum: [ITM, ATM, OTM] }
        - in: query
          name: min_moneyness_pct
          schema: { type: number, format: double }
        - in: query
          name: max_moneyness_pct
          schema: { type: number, format: double }
        - in: query
          name: min_iv
          schema: { type: number, format: double }
        - in: query
          name: max_iv
          schema: { type: number, format: double }
        - in: query
          name: exclude_tickers
          description: Comma-separated tickers to exclude (e.g. `SPY,QQQ,IWM`).
          schema: { type: string }
      responses:
        "200":
          description: Contracts ranked by the requested metric.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UnusualVolumeListSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/contract/unusual-oi:
    get:
      summary: Contracts with significant OI changes
      operationId: getContractUnusualOi
      tags: [Contract]
      description: |
        Contracts whose open interest changed by at least
        `min_oi_change` (or `min_oi_change_pct`) on the target date. `direction`
        narrows the result to opening (OI ↑) or closing (OI ↓) flow.
      parameters:
        - $ref: "#/components/parameters/DiscoveryLimit"
        - in: query
          name: min_oi_change
          schema: { type: integer, default: 500 }
        - in: query
          name: min_oi_change_pct
          schema: { type: number, format: double, default: 25.0 }
        - in: query
          name: ticker
          schema: { type: string }
        - in: query
          name: right
          schema: { type: string, enum: [C, P] }
        - in: query
          name: min_dte
          schema: { type: integer }
        - in: query
          name: max_dte
          schema: { type: integer }
        - in: query
          name: min_premium
          schema: { type: number, format: double }
        - in: query
          name: min_volume
          schema: { type: integer, minimum: 0 }
        - in: query
          name: date
          required: false
          description: |
            Target trading date (`YYYY-MM-DD`). Defaults to the **previous
            calendar day** (not the current trading date).
          schema: { type: string, format: date }
        - in: query
          name: order_by
          schema:
            type: string
            enum: [oi_change, oi_change_pct, volume, premium]
            default: oi_change
        - in: query
          name: direction
          schema:
            type: string
            enum: [opening, closing, both]
            default: both
        - in: query
          name: only_sweeps
          schema: { type: boolean }
        - in: query
          name: only_multi_leg
          schema: { type: boolean }
        - in: query
          name: exclude_multi_leg
          schema: { type: boolean }
      responses:
        "200":
          description: Contracts ranked by OI change.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UnusualOiListSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/contract/bulk/stats:
    get:
      summary: Bulk contract stats for a list of symbols
      operationId: getContractBulkStats
      tags: [Contract]
      description: |
        Returns a single-day `ContractStats` record per requested OPRA
        symbol. Symbols absent from the response had no activity that day.
      parameters:
        - in: query
          name: symbols
          required: true
          description: Comma-separated OPRA symbols (max 50).
          schema:
            type: string
            example: "SPY__250516C00580000,SPY__250516P00580000"
        - $ref: "#/components/parameters/DateQuery"
      responses:
        "200":
          description: Per-contract stats.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContractStatsListSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/contract/{symbol}/stats:
    get:
      summary: Daily stats for a single contract
      operationId: getContractStats
      tags: [Contract]
      parameters:
        - $ref: "#/components/parameters/OptionSymbol"
        - $ref: "#/components/parameters/DateQuery"
      responses:
        "200":
          description: Stats for the contract on the requested date.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContractStatsSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/contract/{symbol}/chart:
    get:
      summary: Intraday chart bars for a contract
      operationId: getContractChart
      tags: [Contract]
      description: |
        Time-bucketed bars for a single contract — granular bid/mid/ask
        execution split, premium and volume per side, daily cumulative
        totals, VWAP, and (when available) IV and 30D average baselines.
      parameters:
        - $ref: "#/components/parameters/OptionSymbol"
        - in: query
          name: interval
          required: true
          description: Trailing window — `{N}D` where N is 1–365 (e.g. `1D`, `7D`).
          schema: { type: string, example: "1D" }
        - in: query
          name: bucket
          required: true
          schema:
            type: string
            enum: [1min, 5min, 10min, 15min, 30min, 1d, 1w]
      responses:
        "200":
          description: Intraday bars for the contract.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContractChartSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/contract/{symbol}/trades:
    get:
      summary: Raw enriched trades for a contract
      operationId: getContractTrades
      tags: [Contract]
      description: |
        Same enriched trade shape as `/v1/underlying/{ticker}/trades`,
        scoped to a single OPRA contract. Because the contract is fixed,
        chain-level filters (moneyness, strike, DTE, expiration) do not
        apply here.
      parameters:
        - $ref: "#/components/parameters/OptionSymbol"
        - in: query
          name: start
          description: Lower time bound — RFC 3339 or Unix seconds. Defaults to start-of-trading-day.
          schema: { type: string }
        - in: query
          name: end
          description: Upper time bound — RFC 3339 or Unix seconds. Defaults to now.
          schema: { type: string }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 500, default: 50 }
        - in: query
          name: only_sweeps
          schema: { type: boolean }
        - in: query
          name: only_multi_leg
          schema: { type: boolean }
        - in: query
          name: exclude_multi_leg
          schema: { type: boolean }
        - in: query
          name: min_premium
          schema: { type: number, format: double, minimum: 0 }
      responses:
        "200":
          description: Enriched trades for the contract.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OptionTradeListSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/contract/{symbol}/history:
    get:
      summary: Daily history for a contract
      operationId: getContractHistory
      tags: [Contract]
      description: |
        Daily aggregates per trading day in `[startDate, endDate]` — total
        premium / volume, OI dynamics, bid/ask execution split, sweep
        and multi-leg shares, VWAP, last price, IV, and trade count.
      parameters:
        - $ref: "#/components/parameters/OptionSymbol"
        - in: query
          name: start_date
          required: true
          schema: { type: string, format: date }
        - in: query
          name: end_date
          required: true
          schema: { type: string, format: date }
      responses:
        "200":
          description: One row per trading day.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContractHistorySuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/contract/{symbol}/rvol:
    get:
      summary: Relative-volume bars for a contract
      operationId: getContractRvol
      tags: [Contract]
      description: |
        Same shape as `/v1/underlying/{ticker}/rvol` but scoped to a
        single contract.
      parameters:
        - $ref: "#/components/parameters/OptionSymbol"
        - in: query
          name: interval
          description: Trailing window — `{N}D` where N is 1–365.
          schema: { type: string, default: "1D", example: "1D" }
        - in: query
          name: bucket
          schema:
            type: string
            enum: [1min, 5min, 10min, 15min, 30min, 1d, 1w]
            default: "5min"
        - in: query
          name: avg_period
          description: Baseline lookback as `{N}d` (e.g. `14d`). Max 365 days.
          schema: { type: string, default: "14d" }
        - $ref: "#/components/parameters/DateQuery"
        - in: query
          name: order_by
          schema:
            type: string
            enum: [rvol, volume, premium, time]
            default: time
        - in: query
          name: order
          description: Sort direction. Defaults to `asc` when `order_by=time`, otherwise `desc`.
          schema: { type: string, enum: [asc, desc] }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1 }
        - in: query
          name: format
          schema: { type: string, enum: [full, summary], default: full }
      responses:
        "200":
          description: RVOL bars + aggregate stats for the contract.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContractRvolSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /v1/dark-pool/trades:
    get:
      summary: Paginated off-exchange (TRF) prints
      operationId: getDarkPoolTrades
      tags: [Dark Pool]
      description: |
        Server-side filtered dark-pool prints from the off-exchange tape
        (FINRA TRF). Defaults to **today (ET)** with a
        **$1,000,000** minimum notional (the blocks-by-default rule); pass
        `min_notional=0` for the full firehose. The trade-date span is capped
        at **31 days** per request — page with `limit`/`offset` or narrow the
        range for more. Prints carry **no side, BBO, or greeks**. Pagination
        state (`limit`, `offset`, `count`, `hasMore`) is returned in `meta`.
      parameters:
        - name: tickers
          in: query
          required: false
          description: Comma-separated tickers to include (e.g. `AAPL,NVDA`). Omit for all names.
          schema: { type: string, example: "AAPL,NVDA" }
        - name: date
          in: query
          required: false
          description: Single trade date (`YYYY-MM-DD`, ET). Defaults to today (ET).
          schema: { type: string, format: date }
        - name: date_start
          in: query
          required: false
          description: Inclusive start of a trade-date range (`YYYY-MM-DD`, ET). Max span 31 days.
          schema: { type: string, format: date }
        - name: date_end
          in: query
          required: false
          description: Inclusive end of a trade-date range (`YYYY-MM-DD`, ET). Max span 31 days.
          schema: { type: string, format: date }
        - name: time_start
          in: query
          required: false
          description: Inclusive lower bound of the time-of-day window (`HH:MM`, ET).
          schema: { type: string, example: "09:30" }
        - name: time_end
          in: query
          required: false
          description: Inclusive upper bound of the time-of-day window (`HH:MM`, ET).
          schema: { type: string, example: "16:00" }
        - name: min_notional
          in: query
          required: false
          description: Minimum notional (USD). Defaults to 1,000,000. Pass 0 for the firehose.
          schema: { type: number, format: double, default: 1000000 }
        - name: max_notional
          in: query
          required: false
          schema: { type: number, format: double }
        - name: min_size
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: max_size
          in: query
          required: false
          schema: { type: integer, minimum: 0 }
        - name: min_price
          in: query
          required: false
          schema: { type: number, format: double }
        - name: max_price
          in: query
          required: false
          schema: { type: number, format: double }
        - name: sectors
          in: query
          required: false
          description: Comma-separated GICS sectors to include.
          schema: { type: string }
        - name: industries
          in: query
          required: false
          description: Comma-separated GICS industries to include.
          schema: { type: string }
        - name: venue
          in: query
          required: false
          description: Reporting venue filter. Omit for both.
          schema: { type: string, enum: [FINN, FINC] }
        - name: limit
          in: query
          required: false
          description: Page size (server caps at 5000).
          schema: { type: integer, default: 500, minimum: 1, maximum: 5000 }
        - name: offset
          in: query
          required: false
          description: Row offset for pagination.
          schema: { type: integer, default: 0, minimum: 0, maximum: 50000 }
        - name: order
          in: query
          required: false
          description: Sort by trade time.
          schema: { type: string, enum: [asc, desc], default: desc }
      responses:
        "200":
          description: Paginated dark-pool prints for the requested filters.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DarkPoolTradesSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/dark-pool/top-prints/{ticker}:
    get:
      summary: Largest individual dark-pool prints for a ticker
      operationId: getDarkPoolTopPrints
      tags: [Dark Pool]
      description: |
        The top-N largest individual off-exchange prints for `{ticker}` over a
        trailing window, ordered by notional descending. Each row is a single
        TRF print (not an aggregate), useful as a support/resistance anchor.
      parameters:
        - $ref: "#/components/parameters/Ticker"
        - name: top_n
          in: query
          required: false
          description: Number of largest prints to return.
          schema: { type: integer, default: 5, minimum: 1, maximum: 20 }
        - name: lookback_days
          in: query
          required: false
          description: Calendar-day trailing window.
          schema: { type: integer, default: 45, minimum: 1, maximum: 180 }
        - name: as_of_date
          in: query
          required: false
          description: |
            Optional anchor date (`YYYY-MM-DD`); the window becomes
            `[as_of_date - lookback_days, as_of_date]`. Omit for a today-anchored window.
          schema: { type: string, format: date }
      responses:
        "200":
          description: Top-N largest prints for `{ticker}`, ordered by notional.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DarkPoolTopPrintsSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "402": { $ref: "#/components/responses/InsufficientCredits" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "503": { $ref: "#/components/responses/Unavailable" }

  /v1/openapi.json:
    get:
      summary: This OpenAPI specification, as JSON
      operationId: getOpenAPI
      tags: [Meta]
      security: []
      responses:
        "200":
          description: OpenAPI 3.1 document for the Flowseeker public API.
          content:
            application/json:
              schema: { type: object }

components:
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      description: |
        Skylit API key in the `Authorization` header
        (`Authorization: Bearer <key>`).

  parameters:
    Ticker:
      name: ticker
      in: path
      required: true
      description: Underlying ticker symbol (uppercase, e.g. `SPY`, `AAPL`).
      schema: { type: string, example: SPY }

    Timeframe:
      name: timeframe
      in: query
      required: false
      description: |
        Trailing window label for the request. Supported values:
        `5m`, `15m`, `1h`, `4h`, `1d`.
      schema:
        type: string
        enum: [5m, 15m, 1h, 4h, 1d]
        default: "1h"

    StartTime:
      name: start_time
      in: query
      required: true
      description: |
        Lower bound of the window. Accepts RFC 3339
        (`2026-05-27T13:30:00Z`) or Unix seconds.
      schema: { type: string, example: "2026-05-27T13:30:00Z" }

    EndTime:
      name: end_time
      in: query
      required: true
      description: Upper bound of the window (RFC 3339 or Unix seconds).
      schema: { type: string, example: "2026-05-27T20:00:00Z" }

    Bucket:
      name: bucket
      in: query
      required: false
      description: |
        Bucket size for the time series. Coarser buckets (`1d`, `1w`) are rejected on
        intraday endpoints.
      schema:
        type: string
        enum: [1min, 5min, 15min, 30min, 1h]
        default: "5min"

    OptionSymbol:
      name: symbol
      in: path
      required: true
      description: |
        OPRA option symbol in URL-safe form:
        `{ticker}__{YYMMDD}{C|P}{strike×1000, 8 digits}` — the ticker and the
        15-character contract block are joined by a **double underscore**
        (`__`). For example, an AAPL $250 call expiring 2026-01-17 is
        `AAPL__260117C00250000`. (A space-padded 21-char OCC form such as
        `AAPL  260117C00250000` is also accepted on some endpoints, but the
        `__` form is canonical and works across all contract routes.)
      schema:
        type: string
        example: "SPY__250516C00580000"

    DateQuery:
      name: date
      in: query
      required: false
      description: |
        Trading date the request targets, in `YYYY-MM-DD`. Defaults to the
        current trading date (the most recent session that has settled
        enough data to be queryable).
      schema:
        type: string
        format: date
        example: "2026-05-27"

    DiscoveryLimit:
      name: limit
      in: query
      required: false
      description: Maximum rows to return.
      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }

  schemas:
    # ── Envelope ─────────────────────────────────────────────────────────
    Meta:
      type: object
      required: [timestamp, requestId]
      properties:
        timestamp:
          type: string
          format: date-time
          description: Server-side timestamp the response was generated at.
        requestId:
          type: string
          description: Short opaque ID for log correlation.
          example: "d7574836"

    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: |
                Stable machine-readable error code. Common values:
                `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`,
                `RATE_LIMITED`, `insufficient_credits`, `invalid_parameter`, `INTERNAL_ERROR`,
                `UNAVAILABLE`.
              example: NOT_FOUND
            message:
              type: string
              description: Human-readable explanation. Wording may evolve; key off `code`.

    # ── Flow (per-ticker feed) ───────────────────────────────────────────
    AggregateScores:
      type: object
      description: Window-level scoring components.
      required: [vwf, sdf, fir]
      properties:
        vwf:
          type: number
          description: Volume-Weighted Flow score (-100..+100).
        sdf:
          type: number
          description: Sweep-Dominant Flow score (-100..+100).
        fir:
          type: number
          description: Flow Imbalance Ratio (-100..+100).

    FlowTradeScores:
      type: object
      description: Per-trade scoring outputs.
      required:
        [flowScore, flowScoreInterpretation, flowBonus, flowBonusInterpretation,
         baseDirection, convictionMultiplier]
      properties:
        flowScore:
          type: integer
          description: Composite directional score (-100..+100).
          minimum: -100
          maximum: 100
        flowScoreInterpretation:
          type: string
          example: strong_bullish
        flowBonus:
          type: integer
          description: Conviction bonus (0..+100).
          minimum: 0
          maximum: 100
        flowBonusInterpretation:
          type: string
          example: high_conviction
        baseDirection:
          type: integer
          description: Pre-conviction directional score (-100..+100).
        convictionMultiplier:
          type: number
          description: Conviction component of `flowScore`.

    ClusterInfo:
      type: object
      description: |
        Present when `includeClusters=true` and the trade is part of a sweep,
        condor, or other multi-leg cluster.
      required: [clusterId, clusterTradeCount, clusterTotalPremium, clusterTimeSpanSeconds]
      properties:
        clusterId: { type: string }
        clusterTradeCount: { type: integer, minimum: 1 }
        clusterTotalPremium: { type: number }
        clusterTimeSpanSeconds: { type: integer, minimum: 0 }

    FlowTradeItem:
      type: object
      description: |
        One options trade with full Skylit scoring + context. A subset of
        the most-relevant fields is documented inline; the response may add
        new fields under additive evolution rules.
      required:
        [timestamp, tradeId, optionType, strike, expiration, dte,
         contracts, premium, price, bid, ask, mid,
         underlyingPrice, isSweep, isMultiLeg, moneyness, scores]
      properties:
        timestamp: { type: string, format: date-time }
        tradeId: { type: string, example: "flow_188afe42c3a77af2_0" }
        optionType: { type: string, enum: [CALL, PUT] }
        strike: { type: number }
        expiration: { type: string, format: date }
        dte: { type: integer, minimum: 0 }
        dteCategory:
          type: string
          enum: [zero_dte, weekly, monthly, leap]
        dteFactor: { type: number }
        dteMultiplier: { type: number }
        contracts: { type: integer, minimum: 1 }
        premium: { type: number, description: Total premium in USD. }
        price: { type: number, description: Trade price per contract. }
        bid: { type: number }
        ask: { type: number }
        mid: { type: number }
        spreadWidth: { type: number }
        spreadWidthPct: { type: number }
        liquidityGrade:
          type: string
          enum: [A, B, C, D, F]
        underlyingPrice: { type: number }
        isSweep: { type: boolean }
        isMultiLeg: { type: boolean }
        exchangeCount:
          type: integer
          nullable: true
          description: Number of distinct OPRA exchanges that filled the order.
        moneyness:
          type: string
          enum: [DEEP_ITM, ITM, ATM, OTM, DEEP_OTM]
        moneynessPct: { type: number }
        moneynessWeight: { type: number }
        combinedMoneynessDteWeight: { type: number }
        delta: { type: number, nullable: true }
        notionalDeltaExposure: { type: number, nullable: true }
        openInterest: { type: integer, minimum: 0 }
        dailyVolume: { type: integer, minimum: 0 }
        volOiRatio: { type: number, nullable: true }
        volOiScore: { type: integer }
        sizeOiRatio: { type: number, nullable: true }
        sizeOiScore: { type: integer }
        oiIsZero: { type: boolean }
        rvol: { type: number, nullable: true }
        rvolScore: { type: integer }
        rvolCategory: { type: string, nullable: true }
        iv: { type: number, nullable: true }
        ivChangePct: { type: number, nullable: true }
        relativePremium:
          type: number
          description: Premium relative to the contract's average premium.
        scores: { $ref: "#/components/schemas/FlowTradeScores" }
        cluster: { $ref: "#/components/schemas/ClusterInfo" }

    FlowResponse:
      type: object
      required: [ticker, timeframe, trades, aggregate, tradeCount, sweepCount, totalPremium, queryTimeMs]
      properties:
        ticker: { type: string }
        timeframe: { type: string }
        trades:
          type: array
          items: { $ref: "#/components/schemas/FlowTradeItem" }
        aggregate: { $ref: "#/components/schemas/AggregateScores" }
        tradeCount: { type: integer, minimum: 0 }
        sweepCount: { type: integer, minimum: 0 }
        totalPremium: { type: number }
        queryTimeMs: { type: integer, minimum: 0 }

    FlowSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/FlowResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Flow window (aggregate + tide) ───────────────────────────────────
    WindowAggregateScores:
      allOf:
        - $ref: "#/components/schemas/AggregateScores"
        - type: object
          required: [composite]
          properties:
            composite:
              type: number
              description: Composite roll-up of VWF/SDF/FIR for the window.

    WindowPremiumSplit:
      type: object
      required:
        [bullishPremium, bearishPremium, neutralPremium, netPremium,
         bullishCount, bearishCount, neutralCount, sweepPremium]
      properties:
        bullishPremium: { type: number }
        bearishPremium: { type: number }
        neutralPremium: { type: number }
        netPremium: { type: number }
        bullishCount: { type: integer, minimum: 0 }
        bearishCount: { type: integer, minimum: 0 }
        neutralCount: { type: integer, minimum: 0 }
        sweepPremium: { type: number }

    WindowInterpretation:
      type: object
      required: [bias, signalStrength]
      properties:
        bias:
          type: string
          enum: [bullish, bearish, neutral, mixed]
        signalStrength:
          type: string
          enum: [strong, moderate, weak]

    FlowAggregateResponse:
      type: object
      required:
        [ticker, startTime, endTime, tradeCount, sweepCount, totalPremium,
         aggregate, premiumSplit, interpretation, queryTimeMs]
      properties:
        ticker: { type: string }
        startTime: { type: string, format: date-time }
        endTime: { type: string, format: date-time }
        tradeCount: { type: integer, minimum: 0 }
        sweepCount: { type: integer, minimum: 0 }
        totalPremium: { type: number }
        aggregate: { $ref: "#/components/schemas/WindowAggregateScores" }
        premiumSplit: { $ref: "#/components/schemas/WindowPremiumSplit" }
        interpretation: { $ref: "#/components/schemas/WindowInterpretation" }
        queryTimeMs: { type: integer, minimum: 0 }

    FlowAggregateSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/FlowAggregateResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    FlowTideBar:
      type: object
      required:
        [timestamp, timestampEnd, bullishPremium, bearishPremium, neutralPremium,
         netPremium, bullishVolume, bearishVolume, vwf, sdf, fir,
         tradeCount, sweepCount, netPremiumCumulative]
      properties:
        timestamp: { type: integer, description: Unix seconds (bucket start). }
        timestampEnd: { type: integer, description: Unix seconds (bucket end). }
        bullishPremium: { type: number }
        bearishPremium: { type: number }
        neutralPremium: { type: number }
        netPremium: { type: number }
        bullishVolume: { type: integer, minimum: 0 }
        bearishVolume: { type: integer, minimum: 0 }
        vwf: { type: number }
        sdf: { type: number }
        fir: { type: number }
        tradeCount: { type: integer, minimum: 0 }
        sweepCount: { type: integer, minimum: 0 }
        netPremiumCumulative: { type: number }

    FlowTideResponse:
      type: object
      required: [ticker, bucket, startTime, endTime, bars, queryTimeMs]
      properties:
        ticker: { type: string }
        bucket: { type: string }
        startTime: { type: string, format: date-time }
        endTime: { type: string, format: date-time }
        bars:
          type: array
          items: { $ref: "#/components/schemas/FlowTideBar" }
        queryTimeMs: { type: integer, minimum: 0 }

    FlowTideSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/FlowTideResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Flow signals (baseline + momentum + strikes) ─────────────────────
    BaselineBucket:
      type: object
      required:
        [timeOfDay, daysCount, avgTradeCount, stddevTradeCount,
         avgPremium, stddevPremium, avgFir, stddevFir]
      properties:
        timeOfDay: { type: string, example: "14:30" }
        daysCount: { type: integer, minimum: 0 }
        avgTradeCount: { type: number }
        stddevTradeCount: { type: number }
        avgPremium: { type: number }
        stddevPremium: { type: number }
        avgFir: { type: number }
        stddevFir: { type: number }

    BaselineResponse:
      type: object
      required:
        [ticker, bucket, lookbackDays, startTimeOfDay, endTimeOfDay, buckets,
         queryTimeMs]
      properties:
        ticker: { type: string }
        bucket: { type: string }
        lookbackDays: { type: integer }
        startTimeOfDay: { type: string }
        endTimeOfDay: { type: string }
        buckets:
          type: array
          items: { $ref: "#/components/schemas/BaselineBucket" }
        queryTimeMs: { type: integer, minimum: 0 }

    BaselineSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/BaselineResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    MomentumWindowMetrics:
      type: object
      required: [tradeCount, premium, bullishPremium, bearishPremium, netPremium, fir]
      properties:
        tradeCount: { type: integer, minimum: 0 }
        premium: { type: number }
        bullishPremium: { type: number }
        bearishPremium: { type: number }
        netPremium: { type: number }
        fir: { type: number }

    MomentumBaselineRef:
      type: object
      required:
        [timeOfDay, lookbackDays, daysInBaseline,
         avg5mPremium, stddev5mPremium, avg5mFir, stddev5mFir]
      properties:
        timeOfDay: { type: string }
        lookbackDays: { type: integer }
        daysInBaseline: { type: integer, minimum: 0 }
        avg5mPremium: { type: number }
        stddev5mPremium: { type: number }
        avg5mFir: { type: number }
        stddev5mFir: { type: number }

    MomentumSignals:
      type: object
      required: [firZscore5m, premiumZscore5m, trend, interpretation]
      properties:
        firZscore5m:
          type: number
          description: Z-score of the current 5-minute FIR vs baseline.
        premiumZscore5m:
          type: number
          description: Z-score of the current 5-minute premium vs baseline.
        trend:
          type: string
          enum: [accelerating, steady, fading, neutral]
        interpretation:
          type: string
          description: One-sentence human-readable summary.

    MomentumResponse:
      type: object
      required: [ticker, asOf, current5m, current30m, current1h, signals, queryTimeMs]
      properties:
        ticker: { type: string }
        asOf: { type: string, format: date-time }
        current5m: { $ref: "#/components/schemas/MomentumWindowMetrics" }
        current30m: { $ref: "#/components/schemas/MomentumWindowMetrics" }
        current1h: { $ref: "#/components/schemas/MomentumWindowMetrics" }
        baseline:
          oneOf:
            - $ref: "#/components/schemas/MomentumBaselineRef"
            - { type: "null" }
          description: Null when there are insufficient baseline days available.
        signals: { $ref: "#/components/schemas/MomentumSignals" }
        queryTimeMs: { type: integer, minimum: 0 }

    MomentumSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/MomentumResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    StrikeBreakdown:
      type: object
      required:
        [strike, right, dominantExpiration, tradeCount, volume, totalPremium,
         bullishPremium, bearishPremium, netPremium, askPct, bidPct,
         openInterest, volOiRatio]
      properties:
        strike: { type: number }
        right: { type: string, enum: [C, P] }
        dominantExpiration: { type: string, format: date }
        tradeCount: { type: integer, minimum: 0 }
        volume: { type: integer, minimum: 0 }
        totalPremium: { type: number }
        bullishPremium: { type: number }
        bearishPremium: { type: number }
        netPremium: { type: number }
        askPct:
          type: number
          description: Share of premium executed at-or-above ask (aggressive buying).
        bidPct:
          type: number
          description: Share of premium executed at-or-below bid (aggressive selling).
        openInterest: { type: integer, minimum: 0 }
        volOiRatio: { type: number }

    StrikesConcentration:
      type: object
      required: [top3StrikesShare, top3StrikesNetPremium, interpretation]
      properties:
        top3StrikesShare:
          type: number
          description: Share (0–1) of the window's premium concentrated in the top 3 strikes.
        top3StrikesNetPremium: { type: number }
        interpretation: { type: string }

    StrikesResponse:
      type: object
      required: [ticker, startTime, endTime, byStrike, concentration]
      properties:
        ticker: { type: string }
        startTime: { type: string, format: date-time }
        endTime: { type: string, format: date-time }
        byStrike:
          type: array
          items: { $ref: "#/components/schemas/StrikeBreakdown" }
        concentration: { $ref: "#/components/schemas/StrikesConcentration" }

    StrikesSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/StrikesResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Historical compare ───────────────────────────────────────────────
    CurrentMetrics:
      type: object
      required:
        [totalPremium, totalVolume, callPremium, putPremium, netPremium,
         callPutRatio, callVolume, putVolume]
      properties:
        totalPremium: { type: number }
        totalVolume: { type: integer, minimum: 0 }
        callPremium: { type: number }
        putPremium: { type: number }
        netPremium: { type: number }
        callPutRatio: { type: number }
        callVolume: { type: integer, minimum: 0 }
        putVolume: { type: integer, minimum: 0 }

    HistoricalMetrics:
      type: object
      required:
        [avgPremium, avgVolume, avgNetPremium, avgCallPutRatio,
         avgCallPremium, avgPutPremium, daysAnalyzed]
      properties:
        avgPremium: { type: number }
        avgVolume: { type: number }
        avgNetPremium: { type: number }
        avgCallPutRatio: { type: number }
        avgCallPremium: { type: number }
        avgPutPremium: { type: number }
        daysAnalyzed: { type: integer, minimum: 0 }

    VsAverage:
      type: object
      required: [premiumVsAvg, volumeVsAvg, netPremiumVsAvg]
      properties:
        premiumVsAvg:
          type: number
          description: Today's premium as a multiple of the historical average (1.0 = at average).
        volumeVsAvg: { type: number }
        netPremiumVsAvg: { type: number }

    PercentileRankings:
      type: object
      required: [premiumPercentile, volumePercentile, netPremiumPercentile]
      properties:
        premiumPercentile: { type: integer, minimum: 0, maximum: 100 }
        volumePercentile: { type: integer, minimum: 0, maximum: 100 }
        netPremiumPercentile: { type: integer, minimum: 0, maximum: 100 }

    SimilarDay:
      type: object
      required: [date, netPremium, totalPremium, similarityScore]
      properties:
        date: { type: string, format: date }
        netPremium: { type: number }
        totalPremium: { type: number }
        similarityScore:
          type: number
          description: 0..1; higher = more similar to today's flow profile.

    HistoricalCompareResponse:
      type: object
      required:
        [ticker, date, current, historical, vsAverage, percentileRankings, similarDays]
      properties:
        ticker: { type: string }
        date: { type: string, format: date }
        current: { $ref: "#/components/schemas/CurrentMetrics" }
        historical: { $ref: "#/components/schemas/HistoricalMetrics" }
        vsAverage: { $ref: "#/components/schemas/VsAverage" }
        percentileRankings: { $ref: "#/components/schemas/PercentileRankings" }
        similarDays:
          type: array
          items: { $ref: "#/components/schemas/SimilarDay" }

    HistoricalCompareSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/HistoricalCompareResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Sector ──────────────────────────────────────────────────────────
    TopContributor:
      type: object
      required: [ticker, netPremium, callPremium, putPremium, pctOfSector]
      properties:
        ticker: { type: string }
        netPremium: { type: number }
        callPremium: { type: number }
        putPremium: { type: number }
        pctOfSector:
          type: number
          description: Share of the sector's net premium (0–100).

    IndustryBreakdown:
      type: object
      required: [industry, netPremium, totalPremium, pctOfSector, tickerCount]
      properties:
        industry: { type: string }
        netPremium: { type: number }
        totalPremium: { type: number }
        pctOfSector: { type: number }
        tickerCount: { type: integer, minimum: 0 }

    SectorMetrics:
      type: object
      required:
        [totalPremium, callPremium, putPremium, netPremium, totalVolume,
         callVolume, putVolume, netVolume, fir, putCallRatio]
      properties:
        totalPremium: { type: number }
        callPremium: { type: number }
        putPremium: { type: number }
        netPremium: { type: number }
        totalVolume: { type: integer, minimum: 0 }
        callVolume: { type: integer, minimum: 0 }
        putVolume: { type: integer, minimum: 0 }
        netVolume: { type: integer }
        fir: { type: number, description: Flow Imbalance Ratio (-100..+100). }
        putCallRatio: { type: number }

    SectorFlowResponse:
      type: object
      required: [sector, date, metrics, topContributors, industryBreakdown, tickerCount]
      properties:
        sector: { type: string }
        etf:
          type: string
          nullable: true
          description: Sector ETF symbol when resolvable (e.g. `XLK` for Technology).
        date: { type: string, format: date }
        metrics: { $ref: "#/components/schemas/SectorMetrics" }
        topContributors:
          type: array
          items: { $ref: "#/components/schemas/TopContributor" }
        industryBreakdown:
          type: array
          items: { $ref: "#/components/schemas/IndustryBreakdown" }
        tickerCount: { type: integer, minimum: 0 }

    SectorFlowSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/SectorFlowResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Market breadth ───────────────────────────────────────────────────
    IndexSentiment:
      type: object
      required: [ticker, bullishPremium, bearishPremium, netPremium, fir, sentiment]
      properties:
        ticker: { type: string }
        bullishPremium: { type: number }
        bearishPremium: { type: number }
        netPremium: { type: number }
        fir: { type: number }
        sentiment:
          type: string
          enum:
            [strong_bullish, bullish, slightly_bullish, neutral,
             slightly_bearish, bearish, strong_bearish]

    AggregateSentiment:
      type: object
      required: [combinedBullish, combinedBearish, combinedNetPremium, combinedFir, sentiment]
      properties:
        combinedBullish: { type: number }
        combinedBearish: { type: number }
        combinedNetPremium: { type: number }
        combinedFir: { type: number }
        sentiment:
          type: string
          enum:
            [strong_bullish, bullish, slightly_bullish, neutral,
             slightly_bearish, bearish, strong_bearish]

    AdvanceDecline:
      type: object
      required: [advancing, declining, unchanged, total, ratio, breadthPct, marketAvgFir]
      properties:
        advancing: { type: integer, minimum: 0 }
        declining: { type: integer, minimum: 0 }
        unchanged: { type: integer, minimum: 0 }
        total: { type: integer, minimum: 0 }
        ratio:
          type: number
          description: Advancing / declining (capped). 0.0 means no decliners.
        breadthPct:
          type: number
          description: Share (0–100) of tickers classified as advancing.
        marketAvgFir:
          type: number
          description: Mean FIR across all tickers (-100..+100).

    SectorRotation:
      type: object
      required: [sector, etf, fir, netPremium, signal]
      properties:
        sector: { type: string }
        etf: { type: string }
        fir: { type: number }
        netPremium: { type: number }
        signal:
          type: string
          enum: [strong_inflow, inflow, neutral, outflow, strong_outflow]

    MarketBreadthResponse:
      type: object
      required: [date, majorIndices, aggregateSentiment, advanceDecline, sectorRotation]
      properties:
        date: { type: string, format: date }
        majorIndices:
          type: array
          items: { $ref: "#/components/schemas/IndexSentiment" }
        aggregateSentiment: { $ref: "#/components/schemas/AggregateSentiment" }
        advanceDecline: { $ref: "#/components/schemas/AdvanceDecline" }
        sectorRotation:
          type: array
          items: { $ref: "#/components/schemas/SectorRotation" }

    MarketBreadthSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/MarketBreadthResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Sweeps ──────────────────────────────────────────────────────────
    SweepScores:
      type: object
      required: [flowScore, flowBonus]
      properties:
        flowScore:
          type: integer
          minimum: -100
          maximum: 100
          description: Directional Flow Score for the aggregated sweep.
        flowBonus:
          type: integer
          minimum: 0
          maximum: 100
          description: Conviction bonus (0..+100).

    SweepItem:
      type: object
      required:
        [timestamp, optionType, strike, expiration, dte, totalContracts,
         totalPremium, exchangeCount, exchanges, executionTimeMs,
         spreadPosition, moneyness, scores]
      properties:
        timestamp: { type: string, format: date-time }
        optionType: { type: string, enum: [CALL, PUT] }
        strike: { type: number }
        expiration: { type: string, format: date }
        dte: { type: integer, minimum: 0 }
        totalContracts: { type: integer, minimum: 1 }
        totalPremium: { type: number }
        exchangeCount: { type: integer, minimum: 1 }
        exchanges:
          type: array
          items: { type: string }
          description: Distinct OPRA exchange codes that filled the sweep.
        executionTimeMs:
          type: integer
          minimum: 0
          description: Milliseconds between first and last leg of the sweep.
        spreadPosition:
          type: string
          enum: [AT_ASK, ABOVE_MID, AT_MID, BELOW_MID, AT_BID, UNKNOWN]
        moneyness:
          type: string
          enum: [DEEP_ITM, ITM, ATM, OTM, DEEP_OTM]
        scores: { $ref: "#/components/schemas/SweepScores" }

    SweepSummary:
      type: object
      required:
        [totalSweeps, bullishSweeps, bearishSweeps, totalSweepPremium,
         sdf, returnedCount]
      properties:
        totalSweeps:
          type: integer
          minimum: 0
          description: Estimated total logical sweeps for the day (population, not just `returnedCount`).
        bullishSweeps: { type: integer, minimum: 0 }
        bearishSweeps: { type: integer, minimum: 0 }
        totalSweepPremium: { type: number }
        sdf:
          type: number
          description: Sweep Dominance Factor (-100..+100) over the full day.
        returnedCount:
          type: integer
          minimum: 0
          description: Sweeps included in the `sweeps` array (capped by `limit`).

    SweepResponse:
      type: object
      required: [ticker, sweeps, summary]
      properties:
        ticker: { type: string }
        sweeps:
          type: array
          items: { $ref: "#/components/schemas/SweepItem" }
        summary: { $ref: "#/components/schemas/SweepSummary" }

    SweepSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/SweepResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Aggregate scoring (timeframe rollup) ─────────────────────────────
    ScoreComponents:
      type: object
      required: [vwf, sdf, fir, bullishPremium, bearishPremium, sweepPremium]
      properties:
        vwf:
          type: number
          description: Volume-Weighted Flow score (-100..+100).
        sdf:
          type: number
          description: Sweep Dominance Factor.
        fir:
          type: number
          description: Flow Imbalance Ratio (-100..+100).
        bullishPremium: { type: number }
        bearishPremium: { type: number }
        sweepPremium: { type: number }

    TimeframeAggregate:
      type: object
      required:
        [composite, direction, signalStrength, confidence, sweepAlignment,
         tradeCount, totalPremium, sweepCount, avgFlowScore]
      properties:
        composite:
          type: integer
          minimum: -100
          maximum: 100
          description: Composite directional score combining VWF, SDF and FIR (-100..+100).
        direction:
          type: string
          enum: [strong_bullish, bullish, neutral, bearish, strong_bearish]
        signalStrength:
          type: string
          enum: [strong, moderate, weak, neutral]
        confidence:
          type: number
          minimum: 0
          maximum: 1
        sweepAlignment:
          type: boolean
          description: True when VWF and SDF agree in direction.
        components: { $ref: "#/components/schemas/ScoreComponents" }
        tradeCount: { type: integer, minimum: 0 }
        totalPremium: { type: number }
        sweepCount: { type: integer, minimum: 0 }
        avgFlowScore: { type: number }
        timeDecayApplied: { type: boolean, description: Present only when time decay was applied. }

    MoneynessAggregate:
      type: object
      required: [category, composite, tradeCount, premium, pctOfTotal]
      properties:
        category:
          type: string
          enum: [DEEP_ITM, ITM, ATM, OTM, DEEP_OTM]
        composite: { type: integer }
        tradeCount: { type: integer, minimum: 0 }
        premium: { type: number }
        pctOfTotal: { type: number }

    TrendAnalysis:
      type: object
      required: [shortVsLong, momentum, description]
      properties:
        shortVsLong:
          type: string
          enum:
            [stable, bullish_divergence, bearish_divergence, improving, deteriorating]
        momentum:
          type: string
          enum: [stable, accelerating_bullish, accelerating_bearish, mixed]
        description:
          type: string
          description: Human-readable interpretation of the short→long horizon contour.

    AggregateResponse:
      type: object
      required: [ticker, generatedAt, byTimeframe, trend]
      properties:
        ticker: { type: string }
        generatedAt: { type: string, format: date-time }
        byTimeframe:
          type: object
          additionalProperties: { $ref: "#/components/schemas/TimeframeAggregate" }
          description: |
            Map keyed by timeframe id (`1h`, `4h`, `1d`, `7d`, `30d`,
            `90d`, …). Only the requested timeframes appear; missing
            entries indicate the underlying query failed for that horizon.
        trend: { $ref: "#/components/schemas/TrendAnalysis" }
        byMoneyness:
          type: array
          description: Present when `includeMoneyness=true`.
          items: { $ref: "#/components/schemas/MoneynessAggregate" }

    AggregateSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/AggregateResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Vol/OI ──────────────────────────────────────────────────────────
    VolOiMoneynessBucket:
      type: object
      required: [volume, oi, ratio]
      properties:
        volume: { type: integer, minimum: 0 }
        oi: { type: integer, minimum: 0 }
        ratio:
          type: number
          description: Vol/OI ratio for the bucket (0 when OI is zero).

    VolOiByMoneyness:
      type: object
      required: [otm10plus, otm510, otm35, atmItm]
      properties:
        otm10plus:
          allOf:
            - $ref: "#/components/schemas/VolOiMoneynessBucket"
            - description: ≥10% OTM.
        otm510:
          allOf:
            - $ref: "#/components/schemas/VolOiMoneynessBucket"
            - description: 5–10% OTM.
        otm35:
          allOf:
            - $ref: "#/components/schemas/VolOiMoneynessBucket"
            - description: 3–5% OTM.
        atmItm:
          allOf:
            - $ref: "#/components/schemas/VolOiMoneynessBucket"
            - description: ATM/ITM (<3% OTM or already ITM).

    VolOiOptionType:
      type: object
      required: [totalVolume, totalOi, volOiRatio, signal, byMoneyness]
      properties:
        totalVolume: { type: integer, minimum: 0 }
        totalOi: { type: integer, minimum: 0 }
        volOiRatio: { type: number }
        signal:
          type: string
          enum: [strong_accumulation, accumulation, mixed, distribution, low_activity]
        byMoneyness: { $ref: "#/components/schemas/VolOiByMoneyness" }

    VolOiAnalysis:
      type: object
      required: [calls, puts]
      properties:
        calls: { $ref: "#/components/schemas/VolOiOptionType" }
        puts: { $ref: "#/components/schemas/VolOiOptionType" }

    VolOiResponse:
      type: object
      required:
        [ticker, timestamp, timeframe, volOiAnalysis, accumulationScore,
         newPositionEstimatePct, signal]
      properties:
        ticker: { type: string }
        timestamp: { type: string, format: date-time }
        timeframe: { type: string, enum: [daily, weekly] }
        volOiAnalysis: { $ref: "#/components/schemas/VolOiAnalysis" }
        accumulationScore:
          type: integer
          minimum: 0
          maximum: 100
          description: Composite accumulation score (0-100).
        newPositionEstimatePct:
          type: integer
          minimum: 0
          maximum: 100
          description: Estimated share of volume that represents new positions.
        signal:
          type: string
          enum: [strong_accumulation, accumulation, mixed, distribution, low_activity]

    VolOiSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/VolOiResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Moneyness breakdown ─────────────────────────────────────────────
    MoneynessCategoryMetrics:
      type: object
      required: [premium, sentiment, pctOfTotal, tradeCount, weightedPremium]
      properties:
        premium: { type: number }
        sentiment:
          type: integer
          minimum: -100
          maximum: 100
        pctOfTotal: { type: number }
        tradeCount: { type: integer, minimum: 0 }
        weightedPremium:
          type: number
          description: Moneyness-adjusted premium.

    MoneynessOptionTypeBreakdown:
      type: object
      required: [deepItm, itm, atm, otm, deepOtm, totalPremium, totalTrades]
      properties:
        deepItm: { $ref: "#/components/schemas/MoneynessCategoryMetrics" }
        itm: { $ref: "#/components/schemas/MoneynessCategoryMetrics" }
        atm: { $ref: "#/components/schemas/MoneynessCategoryMetrics" }
        otm: { $ref: "#/components/schemas/MoneynessCategoryMetrics" }
        deepOtm: { $ref: "#/components/schemas/MoneynessCategoryMetrics" }
        totalPremium: { type: number }
        totalTrades: { type: integer, minimum: 0 }

    MoneynessFullBreakdown:
      type: object
      required: [calls, puts]
      properties:
        calls: { $ref: "#/components/schemas/MoneynessOptionTypeBreakdown" }
        puts: { $ref: "#/components/schemas/MoneynessOptionTypeBreakdown" }

    MoneynessPatternMetrics:
      type: object
      required: [premium, pctOfTotal, sentiment]
      properties:
        premium: { type: number }
        pctOfTotal: { type: number }
        sentiment: { type: integer }

    MoneynessNotablePattern:
      type: object
      required: [pattern, description, significance]
      properties:
        pattern:
          type: string
          enum:
            [otm_call_accumulation, otm_put_accumulation, atm_concentration,
             lottery_ticket_calls, itm_stock_replacement, heavy_put_skew, heavy_call_skew]
        description:
          type: string
          description: Human-readable description of the pattern.
        significance:
          type: string
          enum: [high, medium, low]
        metrics: { $ref: "#/components/schemas/MoneynessPatternMetrics" }

    MoneynessInterpretation:
      type: object
      required: [convictionFocus, dominantStrategy, signal]
      properties:
        convictionFocus:
          type: string
          enum: [otm_calls, otm_puts, atm, distributed, none]
        dominantStrategy:
          type: string
          enum:
            [speculative_bullish, call_selling, bearish_speculation, put_selling,
             directional_bullish, directional_bearish, mixed, no_activity]
        signal:
          type: string
          enum: [bullish, moderately_bullish, neutral, moderately_bearish, bearish]

    MoneynessResponse:
      type: object
      required: [ticker, timeframe, moneynessBreakdown, notablePatterns, interpretation]
      properties:
        ticker: { type: string }
        timeframe: { type: string }
        moneynessBreakdown: { $ref: "#/components/schemas/MoneynessFullBreakdown" }
        notablePatterns:
          type: array
          items: { $ref: "#/components/schemas/MoneynessNotablePattern" }
        interpretation: { $ref: "#/components/schemas/MoneynessInterpretation" }

    MoneynessSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/MoneynessResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Per-trade scoring ───────────────────────────────────────────────
    TradeScores:
      type: object
      required: [sentiment, urgency, confidence]
      properties:
        sentiment:
          type: integer
          minimum: -100
          maximum: 100
        urgency:
          type: integer
          minimum: 0
          maximum: 100
        confidence:
          type: number
          minimum: 0
          maximum: 1

    TradeInterpretation:
      type: object
      required: [direction, intent, description]
      properties:
        direction:
          type: string
          enum: [bullish, bearish, neutral]
        intent:
          type: string
          description: |
            Intent classification (e.g. `opening_long_call`, `closing_short_put`,
            `aggressive_call_buy`, `passive_call_sell`). Returned as a stable
            `snake_case` token; new values may be added as classification
            improves.
        description:
          type: string
          description: One-sentence human-readable explanation of the trade.

    SpreadAnalysis:
      type: object
      required: [bid, ask, mid, tradePrice, positionInSpread, spreadWidth, spreadPct]
      properties:
        bid: { type: number }
        ask: { type: number }
        mid: { type: number }
        tradePrice: { type: number }
        positionInSpread:
          type: string
          description: |
            Discrete bucket label inferred from the canonical side code
            (`A`/`AA`/`BA` → ask-side, `B`/`BB`/`AB` → bid-side, `M` → mid,
            `N` → no BBO).
          enum: [above_ask, at_ask, below_ask, mid, above_bid, at_bid, below_bid, no_bbo]
        spreadWidth: { type: number }
        spreadPct: { type: number }

    TradeContext:
      type: object
      required:
        [ticker, timestamp, optionType, strike, expiration, premium, size,
         underlyingPrice, tradeType, dte, moneyness, moneynessPct]
      properties:
        ticker: { type: string }
        timestamp: { type: string, format: date-time }
        optionType: { type: string, enum: [call, put] }
        strike: { type: number }
        expiration: { type: string, format: date }
        premium: { type: number }
        size: { type: integer, minimum: 1 }
        underlyingPrice: { type: number }
        tradeType:
          type: string
          enum: [sweep, block, regular]
        dte: { type: integer, minimum: 0 }
        moneyness:
          type: string
          enum: [deep_itm, itm, atm, otm, deep_otm]
        moneynessPct: { type: number }

    TradeScoreResponse:
      type: object
      required: [tradeId, scores, interpretation, spreadAnalysis, tradeContext]
      properties:
        tradeId:
          type: string
          example: "flow_188afe42c3a77af2_0"
        scores: { $ref: "#/components/schemas/TradeScores" }
        interpretation: { $ref: "#/components/schemas/TradeInterpretation" }
        spreadAnalysis: { $ref: "#/components/schemas/SpreadAnalysis" }
        tradeContext: { $ref: "#/components/schemas/TradeContext" }

    TradeScoreSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/TradeScoreResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Chain ratio ──────────────────────────────────────────────────────
    ChainRatios:
      type: object
      required:
        [callPutRatio, askRatio, bidRatio, midRatio, aggressionRatio,
         atmConcentration, otmCallConcentration, otmPutConcentration]
      properties:
        callPutRatio:
          type: number
          description: Call premium / put premium (capped at 999 when puts = 0).
        askRatio:
          type: number
          description: Share (0–1) of trades at or above ask.
        bidRatio:
          type: number
          description: Share (0–1) of trades at or below bid.
        midRatio:
          type: number
          description: Share (0–1) of trades at mid (or with no BBO).
        aggressionRatio:
          type: number
          description: "(askTrades + bidTrades) / midTrades — capped at 999 when midTrades = 0."
        atmConcentration:
          type: number
          description: Share (0–1) of volume in ATM strikes (±3% moneyness).
        otmCallConcentration:
          type: number
          description: Share (0–1) of call volume in OTM strikes.
        otmPutConcentration:
          type: number
          description: Share (0–1) of put volume in OTM strikes.

    RatioInterpretation:
      type: object
      required: [bias, aggression, confidence, description]
      properties:
        bias:
          type: string
          enum: [BULLISH, BEARISH, NEUTRAL, MIXED]
        aggression:
          type: string
          enum: [HIGH, MEDIUM, LOW]
        confidence:
          type: string
          enum: [HIGH, MEDIUM, LOW]
        description: { type: string }

    ChainRatioResponse:
      type: object
      required: [ticker, date, timeframe, tradeCount, totalPremium, chainRatios, interpretation]
      properties:
        ticker: { type: string }
        date: { type: string, format: date }
        timeframe: { type: string }
        tradeCount: { type: integer, minimum: 0 }
        totalPremium: { type: number }
        chainRatios: { $ref: "#/components/schemas/ChainRatios" }
        interpretation: { $ref: "#/components/schemas/RatioInterpretation" }

    ChainRatioSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/ChainRatioResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Contract ratio ──────────────────────────────────────────────────
    ContractRatios:
      type: object
      required: [askRatio, bidRatio, midRatio, aggressionRatio]
      properties:
        askRatio: { type: number }
        bidRatio: { type: number }
        midRatio: { type: number }
        aggressionRatio: { type: number }

    ContractRatioResponse:
      type: object
      required:
        [symbol, ticker, optionType, date, timeframe, tradeCount, totalVolume,
         totalPremium, contractRatios, interpretation]
      properties:
        symbol: { type: string, example: "SPY 250516C00580000" }
        ticker: { type: string }
        optionType:
          type: string
          enum: [CALL, PUT, UNKNOWN]
        date: { type: string, format: date }
        timeframe: { type: string }
        tradeCount: { type: integer, minimum: 0 }
        totalVolume: { type: integer, minimum: 0 }
        totalPremium: { type: number }
        contractRatios: { $ref: "#/components/schemas/ContractRatios" }
        interpretation: { $ref: "#/components/schemas/RatioInterpretation" }

    ContractRatioSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/ContractRatioResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Bull / Bear ──────────────────────────────────────────────────────
    BullBearInterpretation:
      type: object
      required: [bias, strength, confidence, description]
      properties:
        bias:
          type: string
          enum: [BULLISH, BEARISH, NEUTRAL, MIXED]
        strength:
          type: string
          enum: [strong, moderate, weak, uncertain, passive, balanced]
        confidence:
          type: string
          enum: [HIGH, MEDIUM, LOW]
        description: { type: string }

    ChainBullBearMetrics:
      type: object
      required: [bullPct, bearPct, neutralPct, bullBearRatio, callBullPct, putBullPct]
      properties:
        bullPct: { type: number, description: Share (0–100) of volume classified as bullish. }
        bearPct: { type: number }
        neutralPct: { type: number }
        bullBearRatio:
          type: number
          description: bullVolume / bearVolume (capped at 999 when bear = 0).
        callBullPct:
          type: number
          description: Bullish share within calls only (high = aggressive call buying).
        putBullPct:
          type: number
          description: Bullish share within puts only (high = put selling = contrarian bullish).

    ChainBullBearResponse:
      type: object
      required:
        [ticker, date, timeframe, tradeCount, totalVolume, totalPremium,
         metrics, interpretation]
      properties:
        ticker: { type: string }
        date: { type: string, format: date }
        timeframe: { type: string }
        tradeCount: { type: integer, minimum: 0 }
        totalVolume: { type: integer, minimum: 0 }
        totalPremium: { type: number }
        metrics: { $ref: "#/components/schemas/ChainBullBearMetrics" }
        interpretation: { $ref: "#/components/schemas/BullBearInterpretation" }

    ChainBullBearSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/ChainBullBearResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    ContractBullBearMetrics:
      type: object
      required: [bullPct, bearPct, neutralPct, bullBearRatio]
      properties:
        bullPct: { type: number }
        bearPct: { type: number }
        neutralPct: { type: number }
        bullBearRatio: { type: number }

    ContractBullBearResponse:
      type: object
      required:
        [symbol, ticker, optionType, date, timeframe, tradeCount, totalVolume,
         totalPremium, metrics, interpretation]
      properties:
        symbol: { type: string }
        ticker: { type: string }
        optionType:
          type: string
          enum: [CALL, PUT, UNKNOWN]
        date: { type: string, format: date }
        timeframe: { type: string }
        tradeCount: { type: integer, minimum: 0 }
        totalVolume: { type: integer, minimum: 0 }
        totalPremium: { type: number }
        metrics: { $ref: "#/components/schemas/ContractBullBearMetrics" }
        interpretation: { $ref: "#/components/schemas/BullBearInterpretation" }

    ContractBullBearSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/ContractBullBearResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Market overview / tide ───────────────────────────────────────────
    TopTickerSummary:
      type: object
      required: [ticker, totalPremium, totalVolume, callPremium, putPremium, netPremium]
      properties:
        ticker: { type: string }
        totalPremium: { type: number }
        totalVolume: { type: integer, minimum: 0 }
        callPremium: { type: number }
        putPremium: { type: number }
        netPremium: { type: number }

    MarketOverviewResponse:
      type: object
      required:
        [date, totalPremium, totalVolume, callPremium, putPremium, netPremium,
         callVolume, putVolume, callPutRatio, activeTickers, bullishPremium,
         bearishPremium, directionalNet, fir, premiumRvol, topTickers]
      properties:
        date: { type: string, format: date }
        totalPremium: { type: number }
        totalVolume: { type: integer, minimum: 0 }
        callPremium: { type: number }
        putPremium: { type: number }
        netPremium: { type: number }
        callVolume: { type: integer, minimum: 0 }
        putVolume: { type: integer, minimum: 0 }
        callPutRatio: { type: number }
        activeTickers: { type: integer, minimum: 0 }
        bullishPremium: { type: number }
        bearishPremium: { type: number }
        directionalNet: { type: number }
        fir: { type: number }
        premiumRvol:
          type: number
          description: Premium relative to the trailing 20-day baseline at this time of day.
        topTickers:
          type: array
          maxItems: 10
          items: { $ref: "#/components/schemas/TopTickerSummary" }

    MarketOverviewSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/MarketOverviewResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    MarketTideBar:
      type: object
      required:
        [timestamp, timestampEnd, ncp, npp, ncpCumulative, nppCumulative,
         manualNcp, manualNpp, manualNcpCumulative, manualNppCumulative,
         callVolume, putVolume, totalVolume, spyPrice, isGap]
      properties:
        timestamp: { type: integer, description: Unix seconds (bucket start). }
        timestampEnd: { type: integer, description: Unix seconds (bucket end). }
        ncp:
          type: number
          description: Net Call Premium for the bucket (call buying minus call selling).
        npp:
          type: number
          description: Net Put Premium for the bucket.
        ncpCumulative: { type: number }
        nppCumulative: { type: number }
        manualNcp:
          type: number
          description: NCP variant with fewer exclusions applied.
        manualNpp: { type: number }
        manualNcpCumulative: { type: number }
        manualNppCumulative: { type: number }
        callVolume: { type: integer, minimum: 0 }
        putVolume: { type: integer, minimum: 0 }
        totalVolume: { type: integer, minimum: 0 }
        spyPrice:
          type: number
          description: SPY trade price at the bucket boundary, for overlay charts.
        isGap:
          type: boolean
          description: True when this bucket spans a session/holiday gap and contains no real trades.

    MarketTideResponse:
      type: object
      required: [interval, bucket, bars]
      properties:
        interval: { type: string }
        bucket: { type: string }
        bars:
          type: array
          items: { $ref: "#/components/schemas/MarketTideBar" }

    MarketTideSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/MarketTideResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Underlying ───────────────────────────────────────────────────────
    TickerListItem:
      type: object
      required: [ticker, totalPremium, totalVolume]
      properties:
        ticker: { type: string, example: SPY }
        totalPremium:
          type: number
          format: double
          description: Total premium (USD) across all option trades on the day.
        totalVolume:
          type: integer
          minimum: 0
          description: Total option contracts traded.

    TickerListSuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/TickerListItem" }
        meta: { $ref: "#/components/schemas/Meta" }

    TopUnderlyingItem:
      type: object
      required:
        [ticker, totalPremium, totalVolume, callPremium, putPremium,
         callVolume, putVolume, netPremium, callPutRatio]
      properties:
        ticker: { type: string }
        totalPremium: { type: number, format: double }
        totalVolume: { type: integer, minimum: 0 }
        callPremium: { type: number, format: double }
        putPremium: { type: number, format: double }
        callVolume: { type: integer, minimum: 0 }
        putVolume: { type: integer, minimum: 0 }
        netPremium:
          type: number
          format: double
          description: "`callPremium - putPremium`."
        callPutRatio:
          type: number
          format: double
          description: "`callPremium / putPremium` (0 when no put premium)."

    TopUnderlyingListSuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/TopUnderlyingItem" }
        meta: { $ref: "#/components/schemas/Meta" }

    UnderlyingStats:
      type: object
      required:
        [ticker, date, lastPrice, totalPremium, totalVolume, callPremium,
         putPremium, callVolume, putVolume, netPremium, callPutRatio,
         tradeCount, uniqueStrikes, uniqueExpirations]
      properties:
        ticker: { type: string }
        date: { type: string, format: date }
        lastPrice:
          type: number
          format: double
          description: Last traded underlying stock price on `date`.
        totalPremium: { type: number, format: double }
        totalVolume: { type: integer, minimum: 0 }
        callPremium: { type: number, format: double }
        putPremium: { type: number, format: double }
        callVolume: { type: integer, minimum: 0 }
        putVolume: { type: integer, minimum: 0 }
        netPremium: { type: number, format: double }
        callPutRatio: { type: number, format: double }
        tradeCount: { type: integer, minimum: 0 }
        uniqueStrikes: { type: integer, minimum: 0 }
        uniqueExpirations: { type: integer, minimum: 0 }

    UnderlyingStatsSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/UnderlyingStats" }
        meta: { $ref: "#/components/schemas/Meta" }

    UnderlyingStatsListSuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/UnderlyingStats" }
        meta: { $ref: "#/components/schemas/Meta" }

    UnderlyingChartBar:
      type: object
      description: |
        One bucketed bar in the underlying-chart response. Each bar covers
        `[timestamp, timestampEnd)` in Unix UTC seconds.
      required:
        [timestamp, timestampEnd, callVolume, putVolume, candleVolume,
         callPremium, putPremium, candlePremium, stockPrice, pcRatio,
         chainBidPct, chainAskPct]
      properties:
        timestamp:
          type: string
          description: Bucket start, Unix seconds (returned as a string for JS-precision safety).
          example: "1745418600"
        timestampEnd:
          type: string
          example: "1745418900"
        callVolume: { type: integer, minimum: 0 }
        putVolume: { type: integer, minimum: 0 }
        candleVolume:
          type: integer
          minimum: 0
          description: "`callVolume + putVolume`."
        callPremium: { type: number, format: double }
        putPremium: { type: number, format: double }
        candlePremium: { type: number, format: double }
        stockPrice: { type: number, format: double }
        pcRatio:
          type: number
          format: double
          description: "`putVolume / callVolume` (0 when no calls)."
        avgVolume:
          type: number
          format: double
          description: 30D baseline volume for this time-of-day slot. Omitted when insufficient history.
        avgPremium:
          type: number
          format: double
          description: 30D baseline premium for this time-of-day slot.
        chainBidPct:
          type: number
          format: double
          description: "% of bucket volume executed at/below the bid."
        chainAskPct:
          type: number
          format: double
          description: "% of bucket volume executed at/above the ask."

    UnderlyingChartSuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/UnderlyingChartBar" }
        meta: { $ref: "#/components/schemas/Meta" }

    OptionTradeRow:
      type: object
      description: |
        Enriched single-trade row served by the trades endpoints. Most
        fields are always present; `*Pct`, `nextIv`, `agg*`, `strategy*`,
        and `earnings*` are optional.
      required:
        [date, tsEvent, instrumentId, rawSymbol, ticker, expiration,
         strike, right, dte, price, size, side, publisherId, neutralSz,
         totalPremium, underlyingPrice, moneyness, moneynessPercent,
         openInterest, prevOi, dailyVolume, sweepTrade, blockTrade,
         multiLeg, ivDirection, ingestionTimestamp]
      properties:
        date:
          type: integer
          description: Days since 1970-01-01 (compact session date).
        tsEvent:
          type: integer
          format: int64
          description: Trade event timestamp in milliseconds since epoch.
        tsEventUs:
          type: integer
          format: int64
          description: Microsecond-precision timestamp (contract-trades endpoint only).
        instrumentId: { type: integer, format: int64 }
        rawSymbol:
          type: string
          example: "SPY   250516C00580000"
        ticker: { type: string, example: SPY }
        expiration:
          type: integer
          description: Expiration as days since 1970-01-01.
        strike: { type: number, format: double }
        right:
          type: string
          enum: [C, P]
        dte: { type: integer }
        price: { type: number, format: double }
        size: { type: integer, minimum: 0 }
        side:
          type: string
          description: |
            Granular execution-side label — `BB` (below bid), `B` (bid),
            `AB` (above bid), `M` (mid), `BA` (below ask), `A` (ask),
            `AA` (above ask), or `N` (no BBO).
          enum: [BB, B, AB, M, BA, A, AA, N]
        publisherId: { type: integer }
        bidPx: { type: number, format: double, nullable: true }
        askPx: { type: number, format: double, nullable: true }
        bidSz: { type: integer, nullable: true }
        askSz: { type: integer, nullable: true }
        neutralSz: { type: integer }
        totalPremium: { type: number, format: double }
        spread: { type: number, format: double, nullable: true }
        underlyingPrice: { type: number, format: double }
        iv: { type: number, format: double, nullable: true }
        moneyness: { type: string, enum: [ITM, ATM, OTM] }
        moneynessPercent: { type: number, format: double }
        openInterest: { type: integer, minimum: 0 }
        prevOi: { type: integer, minimum: 0 }
        prevClose: { type: number, format: double, nullable: true }
        prevCloseAge:
          type: integer
          minimum: 0
          nullable: true
          description: Trading days back the `prevClose` came from (0 = yesterday).
        priceChange: { type: number, format: double, nullable: true }
        dailyVolume: { type: integer, minimum: 0 }
        sweepTrade: { type: boolean }
        blockTrade: { type: boolean }
        multiLeg: { type: boolean }
        ivDirection:
          type: integer
          enum: [-1, 0, 1]
          description: "-1 = down, 0 = flat/unknown, 1 = up."
        ingestionTimestamp:
          type: integer
          format: int64
          description: Server ingest time in milliseconds since epoch.
        prevIv: { type: number, format: double, nullable: true }
        nextIv: { type: number, format: double, nullable: true }
        premiumPercentile:
          type: integer
          enum: [0, 50, 75, 90, 95, 99]
          description: Bucketed premium percentile band (0 = below P50, 99 = P99+).
        flowScore:
          type: integer
          minimum: -100
          maximum: 100
        chainBidPct: { type: number, format: double }
        chainAskPct: { type: number, format: double }
        contractBidPct: { type: number, format: double }
        contractAskPct: { type: number, format: double }
        aggCount: { type: integer, minimum: 0 }
        aggTotalPremium: { type: number, format: double }
        aggTotalSize: { type: integer, minimum: 0 }
        mlSibling:
          type: boolean
          description: True when this leg was included via spread association rather than its own filter match.
        strategyGroupId: { type: string }
        strategyType: { type: string }
        strategyLegCount: { type: integer, minimum: 1 }
        earningsDte: { type: integer }
        nextEarningsDate: { type: integer }
        cacheMiss: { type: boolean }
        sector: { type: string }
        industry: { type: string }

    OptionTradeListSuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/OptionTradeRow" }
        meta: { $ref: "#/components/schemas/Meta" }

    StrikeDistributionBar:
      type: object
      required:
        [strike, callVolume, putVolume, callPremium, putPremium,
         callOi, putOi]
      properties:
        strike: { type: number, format: double }
        callVolume: { type: integer, minimum: 0 }
        putVolume: { type: integer, minimum: 0 }
        callPremium: { type: number, format: double }
        putPremium: { type: number, format: double }
        callOi: { type: integer, minimum: 0 }
        putOi: { type: integer, minimum: 0 }

    StrikeDistributionResponse:
      type: object
      required:
        [ticker, interval, dteFilter, underlyingPrice,
         totalCallPremium, totalPutPremium, totalCallVolume,
         totalPutVolume, pcRatio, topStrike, strikeCount, maxPain, bars]
      properties:
        ticker: { type: string }
        interval: { type: string, enum: [1D, 1W, 7D] }
        dteFilter:
          type: string
          enum: ["all", "0-7", "8-30", "31-90", "90+"]
        underlyingPrice: { type: number, format: double }
        totalCallPremium: { type: number, format: double }
        totalPutPremium: { type: number, format: double }
        totalCallVolume: { type: integer, minimum: 0 }
        totalPutVolume: { type: integer, minimum: 0 }
        pcRatio: { type: number, format: double }
        topStrike:
          type: number
          format: double
          description: Strike with the highest combined call+put premium.
        strikeCount: { type: integer, minimum: 0 }
        maxPain:
          type: number
          format: double
          description: Strike at which total option-holder payout would be minimized.
        bars:
          type: array
          items: { $ref: "#/components/schemas/StrikeDistributionBar" }

    StrikeDistributionSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/StrikeDistributionResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    ExpirationDistributionBar:
      type: object
      required:
        [expiration, dte, callVolume, putVolume, callPremium, putPremium]
      properties:
        expiration: { type: string, format: date }
        dte: { type: integer }
        callVolume: { type: integer, minimum: 0 }
        putVolume: { type: integer, minimum: 0 }
        callPremium: { type: number, format: double }
        putPremium: { type: number, format: double }

    ExpirationDistributionResponse:
      type: object
      required:
        [ticker, strike, interval, totalCallPremium, totalPutPremium,
         topExpiration, expirationCount, bars]
      properties:
        ticker: { type: string }
        strike: { type: number, format: double }
        interval: { type: string, enum: [1D, 1W, 7D] }
        totalCallPremium: { type: number, format: double }
        totalPutPremium: { type: number, format: double }
        topExpiration: { type: string, format: date }
        expirationCount: { type: integer, minimum: 0 }
        bars:
          type: array
          items: { $ref: "#/components/schemas/ExpirationDistributionBar" }

    ExpirationDistributionSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/ExpirationDistributionResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    ExpirationItem:
      type: object
      required:
        [expiration, dte, callVolume, putVolume, callPremium, putPremium,
         contractCount]
      properties:
        expiration: { type: string, format: date }
        dte: { type: integer }
        callVolume: { type: integer, minimum: 0 }
        putVolume: { type: integer, minimum: 0 }
        callPremium: { type: number, format: double }
        putPremium: { type: number, format: double }
        contractCount: { type: integer, minimum: 0 }

    ExpirationListSuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/ExpirationItem" }
        meta: { $ref: "#/components/schemas/Meta" }

    ChainItem:
      type: object
      required:
        [strike, callVolume, callPremium, callOi, callLastPrice,
         putVolume, putPremium, putOi, putLastPrice]
      properties:
        strike: { type: number, format: double }
        callVolume: { type: integer, minimum: 0 }
        callPremium: { type: number, format: double }
        callOi: { type: integer, minimum: 0 }
        callIv:
          type: number
          format: double
          nullable: true
          description: Last call-side IV in the bucket. May be null when no quotes are available.
        callLastPrice: { type: number, format: double }
        putVolume: { type: integer, minimum: 0 }
        putPremium: { type: number, format: double }
        putOi: { type: integer, minimum: 0 }
        putIv:
          type: number
          format: double
          nullable: true
        putLastPrice: { type: number, format: double }

    ChainResponse:
      type: object
      required: [ticker, expiration, underlyingPrice, strikes]
      properties:
        ticker: { type: string }
        expiration: { type: string, format: date }
        underlyingPrice: { type: number, format: double }
        strikes:
          type: array
          items: { $ref: "#/components/schemas/ChainItem" }

    ChainSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/ChainResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    UnderlyingHistoryItem:
      type: object
      required:
        [date, totalPremium, totalVolume, callPremium, putPremium,
         callVolume, putVolume, netPremium]
      properties:
        date: { type: string, format: date }
        totalPremium: { type: number, format: double }
        totalVolume: { type: integer, minimum: 0 }
        callPremium: { type: number, format: double }
        putPremium: { type: number, format: double }
        callVolume: { type: integer, minimum: 0 }
        putVolume: { type: integer, minimum: 0 }
        netPremium: { type: number, format: double }

    UnderlyingHistorySuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/UnderlyingHistoryItem" }
        meta: { $ref: "#/components/schemas/Meta" }

    RvolStats:
      type: object
      required:
        [todayVolume, todayPremium, avgVolume, avgPremium,
         rvolVolume, rvolPremium, avgDaysCount]
      properties:
        todayVolume: { type: integer, minimum: 0 }
        todayPremium: { type: number, format: double }
        avgVolume: { type: number, format: double }
        avgPremium: { type: number, format: double }
        rvolVolume:
          type: number
          format: double
          description: "`todayVolume / avgVolume`."
        rvolPremium:
          type: number
          format: double
          description: "`todayPremium / avgPremium`."
        avgDaysCount:
          type: integer
          minimum: 0
          description: Number of days actually used in the baseline.

    UnderlyingRvolBar:
      type: object
      required:
        [timestamp, timestampEnd, callVolume, putVolume, volume,
         premium, stockPrice]
      properties:
        timestamp: { type: string }
        timestampEnd: { type: string }
        callVolume: { type: integer, minimum: 0 }
        putVolume: { type: integer, minimum: 0 }
        volume: { type: integer, minimum: 0 }
        premium: { type: number, format: double }
        stockPrice: { type: number, format: double }
        avgCallVolume: { type: number, format: double, nullable: true }
        avgPutVolume: { type: number, format: double, nullable: true }
        avgVolume: { type: number, format: double, nullable: true }
        avgPremium: { type: number, format: double, nullable: true }
        avgDaysCount: { type: integer, nullable: true }

    UnderlyingRvolResponse:
      type: object
      required: [bars, stats, callRvol, putRvol]
      properties:
        bars:
          type: array
          items: { $ref: "#/components/schemas/UnderlyingRvolBar" }
        stats: { $ref: "#/components/schemas/RvolStats" }
        callRvol:
          type: number
          format: double
          description: Aggregate call-volume RVOL.
        putRvol:
          type: number
          format: double
          description: Aggregate put-volume RVOL.

    UnderlyingRvolSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/UnderlyingRvolResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Contract ─────────────────────────────────────────────────────────
    TopContractItem:
      type: object
      required:
        [symbol, ticker, expiration, strike, right, dte, totalPremium,
         totalVolume, openInterest, oiChange, volumeOiRatio, bidVolume,
         askVolume, midVolume, vwap, lastPrice, underlyingPrice, iv,
         tradeCount, sweepVolume, sweepPremium, multiLegVolume, multiLegPremium]
      properties:
        symbol: { type: string, example: "SPY 250516C00580000" }
        ticker: { type: string }
        expiration: { type: string, format: date }
        strike: { type: number, format: double }
        right: { type: string, enum: [C, P] }
        dte: { type: integer }
        totalPremium: { type: number, format: double }
        totalVolume: { type: integer, minimum: 0 }
        openInterest: { type: integer, minimum: 0 }
        oiChange: { type: integer }
        volumeOiRatio: { type: number, format: double }
        bidVolume: { type: integer, minimum: 0 }
        askVolume: { type: integer, minimum: 0 }
        midVolume: { type: integer, minimum: 0 }
        vwap: { type: number, format: double }
        lastPrice: { type: number, format: double }
        underlyingPrice: { type: number, format: double }
        iv: { type: number, format: double }
        tradeCount: { type: integer, minimum: 0 }
        sweepVolume: { type: integer, minimum: 0 }
        sweepPremium: { type: number, format: double }
        multiLegVolume: { type: integer, minimum: 0 }
        multiLegPremium: { type: number, format: double }

    TopContractListSuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/TopContractItem" }
        meta: { $ref: "#/components/schemas/Meta" }

    ContractStats:
      type: object
      required:
        [symbol, ticker, expiration, strike, right, dte, date,
         totalPremium, totalVolume, openInterest, oiChange, bidVolume,
         askVolume, midVolume, vwap, lastPrice, underlyingPrice, iv,
         tradeCount, sweepVolume, sweepPremium, multiLegVolume, multiLegPremium]
      properties:
        symbol: { type: string }
        ticker: { type: string }
        expiration: { type: string, format: date }
        strike: { type: number, format: double }
        right: { type: string, enum: [C, P] }
        dte: { type: integer }
        date: { type: string, format: date }
        totalPremium: { type: number, format: double }
        totalVolume: { type: integer, minimum: 0 }
        openInterest: { type: integer, minimum: 0 }
        oiChange: { type: integer }
        bidVolume: { type: integer, minimum: 0 }
        askVolume: { type: integer, minimum: 0 }
        midVolume: { type: integer, minimum: 0 }
        vwap: { type: number, format: double }
        lastPrice: { type: number, format: double }
        underlyingPrice: { type: number, format: double }
        iv: { type: number, format: double }
        tradeCount: { type: integer, minimum: 0 }
        sweepVolume: { type: integer, minimum: 0 }
        sweepPremium: { type: number, format: double }
        multiLegVolume: { type: integer, minimum: 0 }
        multiLegPremium: { type: number, format: double }

    ContractStatsSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/ContractStats" }
        meta: { $ref: "#/components/schemas/Meta" }

    ContractStatsListSuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/ContractStats" }
        meta: { $ref: "#/components/schemas/Meta" }

    ContractChartBar:
      type: object
      description: |
        One bucketed bar in the contract-chart response.
      required:
        [timestamp, timestampEnd, belowBidVolume, bidVolume,
         aboveBidVolume, midVolume, belowAskVolume, askVolume,
         aboveAskVolume, noSideVolume, candleVolume, candlePremium,
         belowBidPremium, bidPremium, aboveBidPremium, midPremium,
         belowAskPremium, askPremium, aboveAskPremium, noSidePremium,
         dailyVolume, dailyPremium, vwap]
      properties:
        timestamp: { type: string }
        timestampEnd: { type: string }
        belowBidVolume: { type: integer, minimum: 0 }
        bidVolume: { type: integer, minimum: 0 }
        aboveBidVolume: { type: integer, minimum: 0 }
        midVolume: { type: integer, minimum: 0 }
        belowAskVolume: { type: integer, minimum: 0 }
        askVolume: { type: integer, minimum: 0 }
        aboveAskVolume: { type: integer, minimum: 0 }
        noSideVolume: { type: integer, minimum: 0 }
        candleVolume: { type: integer, minimum: 0 }
        candleVolumeNoMl:
          type: integer
          minimum: 0
          description: Single-leg volume (used for multi-leg % calculation).
        candlePremium: { type: number, format: double }
        belowBidPremium: { type: number, format: double }
        bidPremium: { type: number, format: double }
        aboveBidPremium: { type: number, format: double }
        midPremium: { type: number, format: double }
        belowAskPremium: { type: number, format: double }
        askPremium: { type: number, format: double }
        aboveAskPremium: { type: number, format: double }
        noSidePremium: { type: number, format: double }
        dailyVolume: { type: integer, minimum: 0 }
        dailyPremium: { type: number, format: double }
        vwap: { type: number, format: double }
        iv: { type: number, format: double, nullable: true }
        avgVolume: { type: number, format: double }
        avgPremium: { type: number, format: double }

    ContractChartSuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/ContractChartBar" }
        meta: { $ref: "#/components/schemas/Meta" }

    ContractHistoryItem:
      type: object
      required:
        [date, totalPremium, totalVolume, openInterest, oiChange,
         oiChangePct, bidVolume, askVolume, midVolume, bidPct, askPct,
         sweepVolume, multiLegVolume, sweepPct, multiLegPct, vwap,
         lastPrice, underlyingPrice, iv, tradeCount]
      properties:
        date: { type: string, format: date }
        totalPremium: { type: number, format: double }
        totalVolume: { type: integer, minimum: 0 }
        openInterest: { type: integer, minimum: 0 }
        oiChange: { type: integer, format: int64 }
        oiChangePct: { type: number, format: double }
        bidVolume: { type: integer, minimum: 0 }
        askVolume: { type: integer, minimum: 0 }
        midVolume: { type: integer, minimum: 0 }
        bidPct: { type: number, format: double }
        askPct: { type: number, format: double }
        sweepVolume: { type: integer, minimum: 0 }
        multiLegVolume: { type: integer, minimum: 0 }
        sweepPct: { type: number, format: double }
        multiLegPct: { type: number, format: double }
        vwap: { type: number, format: double }
        lastPrice: { type: number, format: double }
        underlyingPrice: { type: number, format: double }
        iv: { type: number, format: double }
        tradeCount: { type: integer, minimum: 0 }

    ContractHistorySuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/ContractHistoryItem" }
        meta: { $ref: "#/components/schemas/Meta" }

    ContractRvolBar:
      type: object
      required: [timestamp, timestampEnd, volume, premium]
      properties:
        timestamp: { type: string }
        timestampEnd: { type: string }
        volume: { type: integer, minimum: 0 }
        premium: { type: number, format: double }
        avgVolume: { type: number, format: double, nullable: true }
        avgPremium: { type: number, format: double, nullable: true }
        avgDaysCount: { type: integer, nullable: true }

    ContractRvolResponse:
      type: object
      required: [bars, stats]
      properties:
        bars:
          type: array
          items: { $ref: "#/components/schemas/ContractRvolBar" }
        stats: { $ref: "#/components/schemas/RvolStats" }

    ContractRvolSuccess:
      type: object
      required: [data, meta]
      properties:
        data: { $ref: "#/components/schemas/ContractRvolResponse" }
        meta: { $ref: "#/components/schemas/Meta" }

    UnusualVolumeItem:
      type: object
      required:
        [symbol, ticker, expiration, strike, right, dte, date, volume,
         avgVolume, rvol, premium, openInterest, prevOi, oiChange,
         oiChangePct, volumeOiRatio, bidVolume, askVolume, bidPct, askPct,
         lastPrice, underlyingPrice, iv, moneynessPct, sweepVolume,
         sweepPremium, multiLegVolume, multiLegPremium]
      properties:
        symbol: { type: string }
        ticker: { type: string }
        expiration: { type: string, format: date }
        strike: { type: number, format: double }
        right: { type: string, enum: [C, P] }
        dte: { type: integer }
        date: { type: string, format: date }
        volume: { type: integer, minimum: 0 }
        avgVolume:
          type: number
          format: double
          description: Baseline volume over the requested `avgPeriod`.
        rvol: { type: number, format: double, description: "`volume / avgVolume`." }
        premium: { type: number, format: double }
        openInterest: { type: integer, minimum: 0 }
        prevOi: { type: integer, minimum: 0 }
        oiChange: { type: integer }
        oiChangePct: { type: number, format: double }
        volumeOiRatio: { type: number, format: double }
        bidVolume: { type: integer, minimum: 0 }
        askVolume: { type: integer, minimum: 0 }
        bidPct: { type: number, format: double }
        askPct: { type: number, format: double }
        lastPrice: { type: number, format: double }
        underlyingPrice: { type: number, format: double }
        iv: { type: number, format: double }
        moneynessPct: { type: number, format: double }
        sweepVolume: { type: integer, minimum: 0 }
        sweepPremium: { type: number, format: double }
        multiLegVolume: { type: integer, minimum: 0 }
        multiLegPremium: { type: number, format: double }

    UnusualVolumeListSuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/UnusualVolumeItem" }
        meta: { $ref: "#/components/schemas/Meta" }

    UnusualOiItem:
      type: object
      required:
        [symbol, ticker, expiration, strike, right, dte, date,
         openInterest, prevOi, oiChange, oiChangePct, volume, premium,
         volumeOiRatio, bidVolume, askVolume, lastPrice, underlyingPrice,
         iv, positionType, sweepVolume, sweepPremium, multiLegVolume,
         multiLegPremium]
      properties:
        symbol: { type: string }
        ticker: { type: string }
        expiration: { type: string, format: date }
        strike: { type: number, format: double }
        right: { type: string, enum: [C, P] }
        dte: { type: integer }
        date: { type: string, format: date }
        openInterest: { type: integer, minimum: 0 }
        prevOi: { type: integer, minimum: 0 }
        oiChange: { type: integer }
        oiChangePct: { type: number, format: double }
        volume: { type: integer, minimum: 0 }
        premium: { type: number, format: double }
        volumeOiRatio: { type: number, format: double }
        bidVolume: { type: integer, minimum: 0 }
        askVolume: { type: integer, minimum: 0 }
        lastPrice: { type: number, format: double }
        underlyingPrice: { type: number, format: double }
        iv: { type: number, format: double }
        positionType:
          type: string
          enum: [opening, closing]
        sweepVolume: { type: integer, minimum: 0 }
        sweepPremium: { type: number, format: double }
        multiLegVolume: { type: integer, minimum: 0 }
        multiLegPremium: { type: number, format: double }

    UnusualOiListSuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/UnusualOiItem" }
        meta: { $ref: "#/components/schemas/Meta" }

    # ── Dark Pool (off-exchange / TRF prints) ────────────────────────────
    DarkPoolPrint:
      type: object
      description: |
        One off-exchange (TRF) print. Carries no side / BBO / greeks.
      required: [timestamp, ticker, price, size, notional, venue, sector, industry]
      properties:
        timestamp:
          type: string
          format: date-time
          description: Trade time, ISO-8601 UTC (millisecond precision).
          example: "2026-07-02T14:31:05.123Z"
        ticker:
          type: string
          description: Underlying (dotted equity symbol, e.g. `BRK.B`).
          example: SPY
        price: { type: number, format: double }
        size: { type: integer, minimum: 0, description: Shares. }
        notional:
          type: number
          format: double
          description: "`price × size` (USD)."
        venue:
          type: string
          enum: [FINN, FINC]
          description: FINRA TRF reporting venue.
        sector: { type: string, description: GICS sector of the underlying. }
        industry: { type: string, description: GICS industry of the underlying. }

    DarkPoolTradesMeta:
      allOf:
        - $ref: "#/components/schemas/Meta"
        - type: object
          required: [limit, offset, count, hasMore]
          properties:
            limit:
              type: integer
              description: Page size applied (after clamping to 1..=5000).
            offset:
              type: integer
              description: Row offset applied.
            count:
              type: integer
              description: Number of prints returned in this page.
            hasMore:
              type: boolean
              description: |
                `true` when the page is full (`count == limit`), so more rows
                may exist. Offset-based heuristic, not an exact total.

    DarkPoolTradesSuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/DarkPoolPrint" }
        meta: { $ref: "#/components/schemas/DarkPoolTradesMeta" }

    DarkPoolTopPrint:
      type: object
      description: One of the largest individual dark-pool prints over the window.
      required: [timestamp, price, notional, size]
      properties:
        timestamp:
          type: string
          format: date-time
          description: Trade time, ISO-8601 UTC.
        price: { type: number, format: double }
        notional:
          type: number
          format: double
          description: "`price × size` (USD)."
        size: { type: integer, minimum: 0, description: Shares. }

    DarkPoolTopPrintsSuccess:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: { $ref: "#/components/schemas/DarkPoolTopPrint" }
        meta: { $ref: "#/components/schemas/Meta" }

  responses:
    BadRequest:
      description: Request validation failed.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            invalidParam:
              value:
                error:
                  code: BAD_REQUEST
                  message: "Invalid parameter 'timeframe': must be one of [1m, 5m, 15m, 1h, 4h, 1d, 1w, 1M]"
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            missingKey:
              value:
                error:
                  code: UNAUTHORIZED
                  message: Authentication required
    Forbidden:
      description: >-
        API key invalid, revoked or expired, or the account's API access is
        suspended (`account_suspended`).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            accountSuspended:
              value:
                error:
                  code: account_suspended
                  message: "API access has been suspended for this account. Contact support."
    NotFound:
      description: Unknown resource (ticker / sector / window with no data).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            noData:
              value:
                error:
                  code: NOT_FOUND
                  message: "No trades found for AAPL on 2026-05-27 with timeframe 1d"
    RateLimited:
      description: Per-minute rate limit exceeded.
      headers:
        Retry-After: { schema: { type: integer } }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            tooFast:
              value:
                error:
                  code: RATE_LIMITED
                  message: "Rate limit of 100 req/min exceeded. Retry after 18s."
    Unavailable:
      description: >-
        Underlying data source temporarily unavailable, or the credit balance
        could not be verified (`credit_check_failed`). Safe to retry.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            ingestionLag:
              value:
                error:
                  code: UNAVAILABLE
                  message: "Live feed is degraded; please retry in a few seconds."
            creditCheckFailed:
              value:
                error:
                  code: credit_check_failed
                  message: "Could not verify credit balance. Please retry."
    InsufficientCredits:
      description: >-
        The account's shared Skylit credit balance is lower than this route's
        cost. Top up to continue. Carries `X-Credits-Remaining: 0`.
      headers:
        X-Credits-Remaining:
          description: Remaining credit balance (0 on this response).
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            outOfCredits:
              value:
                error:
                  code: insufficient_credits
                  message: "Out of credits. Top up to continue making requests."

