openapi: 3.1.0
info:
  title: TradeWave Data API
  version: 1.0.0
  description: 'Seasonal trading opportunities and ML win-probability scores for TradeWave''s
    15 active markets, plus the tracked daily AI pick. Derived seasonal patterns only
    - no raw market data.

    '
servers:
- url: https://api.tradewave.ai/v1
  description: TradeWave Data API (v1)
security:
- ApiKeyAuth: []
tags:
- name: patterns
  description: Flagship composed endpoints - ranked seasonal-pattern cards (scan)
    and deep-dive (analyze). Prefer these over the low-level primitives.
- name: markets
- name: opportunities
- name: scoring
- name: daily-pick
paths:
  /scan:
    get:
      tags:
      - patterns
      summary: Scan in-scope markets for the best seasonal setups (ranked PatternCards)
      description: 'The flagship scanner (find_best_opportunities). Fans out over
        the caller''s in-scope markets, scores each candidate by edge_score, and returns
        ranked PatternCards. ML fields populate inline on ML-eligible markets (ids
        0-4, 11) up to the caller''s daily ML allowance (free 5/day, unlimited on
        Pro/Business); ml_remaining_today reflects how many ML calls remain today
        (null = unlimited). The reusable price-safe scan core is shared briefly across
        callers, but auth, tier limits, rate limits, ML quota, and ML scoring run
        for every request. Default Sharpe scans enrich only the requested depth; receipt-dependent
        filters and alternate rankings use a bounded 50-row head. evaluated_count
        + enrichment_capped are surfaced, and any partial market failure is explicit
        in market_failures.

        '
      parameters:
      - name: markets
        in: query
        schema:
          type: string
        description: csv of ids, list_markets names, or common aliases ('sp500', 'crypto',
          'europe'), e.g. '2,11' or 'S&P 500 STOCKS,ETFs'. Unknown tokens are reported
          in markets_note; out-of-scope markets are skipped with an upgrade note.
          Default (omitted) = a liquid equities core (DOW 30, NASDAQ 100, S&P 500,
          ETFs) intersected with your scope, NOT every in-scope market - pass markets
          explicitly to scan others.
      - name: window
        in: query
        schema:
          type: string
          default: now
        description: '''now'' | ''next_2_weeks'' | ''next_month'' | a ''from..to''
          range. The underlying primitive (OppList4) is keyed to a SINGLE entry date,
          so the scan evaluates opportunities AS OF the window''s start date and then
          keeps only setups whose entry_date falls inside the window - it does not
          re-scan every date in the range. ''now'' starts today (~10 trading days
          wide). Default ''now''.'
      - name: direction
        in: query
        schema:
          type: string
          enum:
          - long
          - short
      - name: min_win_rate
        in: query
        schema:
          type: number
          minimum: 0
          maximum: 1
        description: filter on historical_win_rate (share of profitable years), NOT
          ml_win_prob. Rows past the enrichment cap are excluded when set.
      - name: min_years
        in: query
        schema:
          type: integer
        description: trust filter - require years_tested >= N
      - name: min_days
        in: query
        schema:
          type: integer
        description: minimum pattern length (holding period) in calendar days, e.g.
          10
      - name: max_days
        in: query
        schema:
          type: integer
        description: maximum pattern length (holding period) in calendar days, e.g.
          90
      - name: min_avg_return
        in: query
        schema:
          type: number
        description: minimum average seasonal profit in PERCENT (e.g. 5 means >= 5%);
          matches stats.avg_return_pct
      - name: min_median_return
        in: query
        schema:
          type: number
        description: minimum median seasonal profit in PERCENT; matches stats.median_return_pct
      - name: min_sharpe
        in: query
        schema:
          type: number
        description: minimum Sharpe ratio, e.g. 1.5; matches stats.sharpe_ratio
      - name: pe_cycle
        in: query
        schema:
          type: string
          enum:
          - consecutive
          - pe
          default: consecutive
        description: 'presidential election cycle mode: ''consecutive'' (default,
          consecutive years) or ''pe'' (the current presidential-cycle position only)'
      - name: years
        in: query
        schema:
          type: integer
          default: 10
        description: 'pattern LOOKBACK: how many years (5-98, data-dependent) to scan
          for patterns. In pe mode this is the number of PE-position occurrences.'
      - name: min_winning_years
        in: query
        schema:
          type: integer
        description: 'of those `years`, the minimum WINNING years for a pattern to
          be listed (the win-rate floor; year2). DEFAULTS to ~90% of `years` (so years=20
          -> a valid 20-18). Must lie in the market''s DETECTION BAND, which is market-specific
          (~75-90%+ winners; e.g. S&P 500 ~85%, Wilshire ~90%, FOREX Liquid ~70% at
          a 20-year lookback). Multi-market scan: out-of-band markets are reported
          in lookback_note (not errored).'
      - name: rank_by
        in: query
        schema:
          type: string
          enum:
          - edge
          - win_rate
          - sharpe
          - ml
          - avg_return
          default: sharpe
        description: ranking key; default sharpe (Sharpe ratio)
      - name: limit
        in: query
        schema:
          type: integer
          default: 25
        description: tier-capped (free 3, dev 100, pro 1000, business 5000)
      - name: view
        in: query
        schema:
          type: string
          enum:
          - full
          - evidence
          - decision
          - table
          default: full
        description: response verbosity. 'full' (API default) = complete cards; 'evidence'
          = full rank-1 winner + lean runners; 'decision' = lean cards; 'table' =
          compact rows. MCP discovery defaults to evidence.
      - name: include
        in: query
        schema:
          type: string
          enum:
          - chart
        description: include=chart attaches the two-chart evidence data/specifications
          to the rank-1 winner only.
      responses:
        '200':
          description: OK
          headers:
            X-TW-Scan-Cache:
              description: Shared scan-core outcome for this request.
              schema:
                type: string
                enum:
                - HIT
                - MISS
                - WAIT
                - BYPASS
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScanResponse'
        '503':
          description: A cold scan is already being built and did not complete within
            the bounded wait. Retry-After is supplied.
        '429':
          $ref: '#/components/responses/RateLimited'
  /analyze/{symbol}:
    get:
      tags:
      - patterns
      summary: Deep-dive one symbol into a single rich PatternCard + alternate setups
      description: 'analyze_symbol - one symbol fused server-side into one best-setup
        PatternCard plus the other candidate setups (compact) and receipts, from OppBySymbol
        + ChartData4 stats + the consolidated seasonal curve (+ MLScore, metered per
        day on every tier). Replaces the multi-call stitch and guarantees win_rate
        consistency across the response. Pin a SPECIFIC setup with entry_date (+days_out)
        or a period/reverse preset - the "click this exact opportunity / change the
        date range" flow - to deep-dive THAT window instead of the auto-picked best;
        pe_cycle + years are the same wave-viewer knobs as on /seasonal-chart.

        '
      parameters:
      - name: symbol
        in: path
        required: true
        schema:
          type: string
      - name: market
        in: query
        schema:
          type: string
        description: permanent resource key '0'..'16'. Optional - resolved if the
          symbol is unique.
      - name: direction
        in: query
        schema:
          type: string
          enum:
          - long
          - short
      - name: days_out
        in: query
        schema:
          type: integer
        description: 'holding period in days (internal: daysOut). With entry_date,
          pins the exact window.'
      - name: entry_date
        in: query
        schema:
          type: string
          format: date
        description: YYYY-MM-DD. Pin analysis to THIS exact opportunity instead of
          the auto-picked best.
      - name: pe_cycle
        in: query
        schema:
          type: string
          enum:
          - consecutive
          - pe
        description: score over presidential-election-cycle years (same phase as the
          entry year) instead of consecutive. Default consecutive.
      - name: years
        in: query
        schema:
          type: integer
          minimum: 1
          maximum: 99
        description: lookback length (default 10).
      - name: period
        in: query
        schema:
          type: string
          enum:
          - jan
          - feb
          - mar
          - apr
          - may
          - jun
          - jul
          - aug
          - sep
          - oct
          - nov
          - dec
          - q1
          - q2
          - q3
          - q4
          - spring
          - summer
          - fall
          - winter
          - ytd
          - year_end
          - buy_hold
        description: wave-viewer date-range preset; overrides entry_date/days_out.
      - name: reverse
        in: query
        schema:
          type: boolean
        description: invert the period to all-of-year-except-that-window.
      - name: view
        in: query
        schema:
          type: string
          enum:
          - full
          - evidence
          - decision
          - table
          default: full
        description: response verbosity. 'full'/'evidence' = complete evidence card;
          'decision' = lean; 'table' = compact. MCP defaults to evidence.
      - name: include
        in: query
        schema:
          type: string
          enum:
          - chart
        description: include=chart attaches the canonical two-chart data and rendering
          specifications. MCP additionally renders them as native image content blocks.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AnalyzeResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
  /markets:
    get:
      tags:
      - markets
      summary: List the 15 active markets (ids 0-13, 16) and the caller's access scope
        + pattern coverage
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  markets:
                    type: array
                    items:
                      $ref: '#/components/schemas/Market'
  /me:
    get:
      tags:
      - markets
      summary: Identity + capabilities for the caller's key (plan, quotas, market
        scope)
      description: 'Who am I and what can I do: the key''s API tier, remaining ML
        allowance today, rate limits, opportunity cap, and the in-scope markets (with
        ml_eligible per market). Powers the MCP `whoami` tool. Read-only - never consumes
        ML allowance. Under MCP OAuth this reflects the END USER''s real tier, not
        the service principal.

        '
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Me'
        '429':
          $ref: '#/components/responses/RateLimited'
  /markets/{market_id}/symbols:
    get:
      tags:
      - markets
      summary: List symbols in a market (pageable)
      description: 'A full market is large (S&P 500 alone is ~150KB), so the list
        pages: `prefix` filters by ticker prefix (case-insensitive) and `limit` caps
        the rows returned. total/matched/count make any truncation explicit, never
        silent (a `note` names how many matching rows were cut).

        '
      parameters:
      - $ref: '#/components/parameters/MarketId'
      - name: prefix
        in: query
        schema:
          type: string
        description: ticker prefix filter, case-insensitive (e.g. 'AA' matches AAPL,
          AAL)
      - name: limit
        in: query
        schema:
          type: integer
        description: max rows returned (after the prefix filter); omitted = all matching
          rows
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  symbols:
                    type: array
                    items:
                      $ref: '#/components/schemas/Symbol'
                  count:
                    type: integer
                    description: rows returned (after prefix + limit)
                  matched:
                    type: integer
                    description: rows matching the prefix filter (before limit)
                  total:
                    type: integer
                    description: total symbols in the market
                  note:
                    type: string
                    description: present only when limit truncated the matches - names
                      how many of how many are shown
  /opportunities:
    get:
      tags:
      - opportunities
      summary: Ranked seasonal opportunities (single entry-date primitive)
      description: 'The core primitive. Returns ranked seasonal trade setups for ONE
        market at a single entry_date (echoed in the response). It does NOT widen
        across a date window - /scan owns windows. ''from'' is accepted; ''to'' is
        accepted but does NOT widen the search (window_supported is false). ML fields
        populate up to the caller''s daily ML allowance on ML-eligible markets (ids
        0-4, 11); the ml block here keeps the legacy names {ml_score, win_prob, pred_return,
        pred_mfe}. ml_remaining_today reflects how many ML calls remain today (null
        = unlimited).

        '
      parameters:
      - name: market
        in: query
        required: true
        schema:
          type: string
        description: market id '0'..'16'
      - name: from
        in: query
        schema:
          type: string
          format: date
        description: entry_date to evaluate; defaults to today
      - name: to
        in: query
        schema:
          type: string
          format: date
        description: ACCEPTED but ignored - does not widen the window (window_supported:false).
          Use /scan for windows.
      - name: direction
        in: query
        schema:
          type: string
          enum:
          - long
          - short
      - name: min_win_rate
        in: query
        schema:
          type: number
          minimum: 0
          maximum: 1
        description: filter on the REAL historical win_rate (share of profitable years),
          not the ML win_prob. Rows are enriched per symbol from ChartData4 'Percent
          Profitable', capped at the first 50 rows; rows past the cap (unknown win_rate)
          are excluded when this filter is set.
      - name: min_days
        in: query
        schema:
          type: integer
        description: minimum pattern length (holding period) in calendar days, e.g.
          10
      - name: max_days
        in: query
        schema:
          type: integer
        description: maximum pattern length (holding period) in calendar days, e.g.
          90
      - name: min_avg_return
        in: query
        schema:
          type: number
        description: minimum average seasonal profit in PERCENT (e.g. 5 means >= 5%)
      - name: min_median_return
        in: query
        schema:
          type: number
        description: minimum median seasonal profit in PERCENT
      - name: min_sharpe
        in: query
        schema:
          type: number
        description: minimum Sharpe ratio, e.g. 1.5
      - name: pe_cycle
        in: query
        schema:
          type: string
          enum:
          - consecutive
          - pe
          default: consecutive
        description: 'presidential election cycle mode: ''consecutive'' (default)
          or ''pe'' (the current cycle position)'
      - name: years
        in: query
        schema:
          type: integer
          default: 10
        description: pattern lookback in years (5-98, data-dependent)
      - name: min_winning_years
        in: query
        schema:
          type: integer
        description: 'of those `years`, the minimum WINNING years (the win-rate floor;
          year2). DEFAULTS to ~90% of `years`. Single market: validated against that
          market''s DETECTION BAND (~75-90%+, market-specific); an out-of-band value
          (e.g. 20-9) returns 400 with the valid range.'
      - name: limit
        in: query
        schema:
          type: integer
          default: 25
        description: tier-capped (free 3, dev 100, pro 1000, business 5000)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OpportunityList'
        '429':
          $ref: '#/components/responses/RateLimited'
  /opportunities/{symbol}:
    get:
      tags:
      - opportunities
      summary: A symbol's top seasonal patterns across the year, ranked by Sharpe
        (the wave-viewer pattern list)
      parameters:
      - name: symbol
        in: path
        required: true
        schema:
          type: string
      - $ref: '#/components/parameters/SymbolPatternsMarket'
      - $ref: '#/components/parameters/SymbolPatternsPeCycle'
      - $ref: '#/components/parameters/SymbolPatternsMinDays'
      - $ref: '#/components/parameters/SymbolPatternsMaxDays'
      - $ref: '#/components/parameters/SymbolPatternsMinAvgReturn'
      - $ref: '#/components/parameters/SymbolPatternsMinMedianReturn'
      - $ref: '#/components/parameters/SymbolPatternsMinSharpe'
      - $ref: '#/components/parameters/SymbolPatternsYears'
      - $ref: '#/components/parameters/SymbolPatternsMinWinningYears'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  opportunities:
                    type: array
                    items:
                      $ref: '#/components/schemas/Opportunity'
  /securities/{symbol}/patterns:
    get:
      tags:
      - opportunities
      summary: A symbol's top seasonal patterns across the year, ranked by Sharpe
        (clearly-named alias)
      description: Same data as GET /opportunities/{symbol} - one security's ranked
        seasonal patterns throughout the year (the wave-viewer pattern dropdown).
        Both endpoints are served by the same handler, so their parameters are identical
        (shared $refs below) and cannot drift apart.
      parameters:
      - name: symbol
        in: path
        required: true
        schema:
          type: string
      - $ref: '#/components/parameters/SymbolPatternsMarket'
      - $ref: '#/components/parameters/SymbolPatternsPeCycle'
      - $ref: '#/components/parameters/SymbolPatternsMinDays'
      - $ref: '#/components/parameters/SymbolPatternsMaxDays'
      - $ref: '#/components/parameters/SymbolPatternsMinAvgReturn'
      - $ref: '#/components/parameters/SymbolPatternsMinMedianReturn'
      - $ref: '#/components/parameters/SymbolPatternsMinSharpe'
      - $ref: '#/components/parameters/SymbolPatternsYears'
      - $ref: '#/components/parameters/SymbolPatternsMinWinningYears'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  opportunities:
                    type: array
                    items:
                      $ref: '#/components/schemas/Opportunity'
  /patterns/{market_id}/{symbol}:
    get:
      tags:
      - opportunities
      parameters:
      - $ref: '#/components/parameters/MarketId'
      - name: symbol
        in: path
        required: true
        schema:
          type: string
      - name: pe_cycle
        in: query
        schema:
          type: string
          enum:
          - consecutive
          - pe
          - pe0
          - pe1
          - pe2
          - pe3
          default: consecutive
        description: 'presidential cycle: consecutive (default), pe (current position),
          or a specific position pe0..pe3 (PE+0 = election years, PE+2 = current in
          2026)'
      - name: years
        in: query
        schema:
          type: integer
          default: 10
        description: lookback count (years, or number of cycle occurrences when pe_cycle
          is a pe* value)
      - name: days_out
        in: query
        schema:
          type: integer
          default: 30
      - name: period
        in: query
        schema:
          type: string
          enum:
          - jan
          - feb
          - mar
          - apr
          - may
          - jun
          - jul
          - aug
          - sep
          - oct
          - nov
          - dec
          - q1
          - q2
          - q3
          - q4
          - spring
          - summer
          - fall
          - winter
          - ytd
          - year_end
          - buy_hold
        description: 'date-range PRESET (overrides entry_date/days_out): a month,
          quarter, season, ytd, year_end, or buy_hold'
      - name: reverse
        in: query
        schema:
          type: boolean
        description: use the COMPLEMENT of the window (e.g. period=mar&reverse=true
          = all year except March). buy_hold cannot be reversed.
      summary: Seasonal pattern stats for a symbol (aggregate stats, no price series)
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pattern'
  /seasonal-chart:
    get:
      tags:
      - opportunities
      summary: Seasonal trendchart data for a symbol (normalized 0-100 curve, no price
        series)
      description: 'The seasonal ''trendchart'' as DATA (points to plot, not an image),
        so callers can chart it or agents can reason over the shape. Returns the appserver''s
        consolidated_seasonal_chart2 curve: a year-averaged, high/low-normalized 0-100
        seasonal index, one point per day over the annual cycle, plus the publishable
        percentage stats and the historical win_rate. The raw price series (the app''s
        ''price'' chart mode) is intentionally NOT exposed.

        '
      parameters:
      - name: market
        in: query
        required: true
        schema:
          type: string
      - name: symbol
        in: query
        required: true
        schema:
          type: string
      - name: entry_date
        in: query
        schema:
          type: string
          format: date
      - name: days_out
        in: query
        schema:
          type: integer
      - name: direction
        in: query
        schema:
          type: string
          enum:
          - long
          - short
      - name: years
        in: query
        schema:
          type: string
        description: lookback window, stays a string
      - name: pe_cycle
        in: query
        schema:
          type: string
          enum:
          - consecutive
          - pe
          - pe0
          - pe1
          - pe2
          - pe3
          default: consecutive
        description: 'presidential cycle for the curve: consecutive (default), pe
          (current position), or a specific position pe0..pe3'
      - name: period
        in: query
        schema:
          type: string
          enum:
          - jan
          - feb
          - mar
          - apr
          - may
          - jun
          - jul
          - aug
          - sep
          - oct
          - nov
          - dec
          - q1
          - q2
          - q3
          - q4
          - spring
          - summer
          - fall
          - winter
          - ytd
          - year_end
          - buy_hold
        description: 'date-range PRESET (overrides entry_date/days_out): a month,
          quarter, season, ytd (year to date), year_end (today to year end), or buy_hold
          (Jan 1 to Jan 1, full year)'
      - name: reverse
        in: query
        schema:
          type: boolean
        description: use the COMPLEMENT of the window - everything except it (e.g.
          period=mar&reverse=true = all year except March). A full-year (buy_hold)
          range cannot be reversed (400).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SeasonalChart'
  /score:
    post:
      tags:
      - scoring
      summary: ML scores for opportunities (all tiers, metered per day)
      description: 'Available on every tier, metered per day (free 5/day, dev 100/day,
        pro/business unlimited). ML-eligible markets only (ids 0-4, 11). Scores up
        to the daily allowance and returns granted (how many were actually scored)
        and ml_remaining_today (integer or null for unlimited). Items beyond the granted
        allowance appear in scores[] unscored with note:"daily ML limit reached -
        upgrade for unlimited". When the allowance is already fully spent the response
        is a 200 with requires:"upgrade" and reason:"ml_daily_limit" instead of a
        scores array.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScoreRequest'
      responses:
        '200':
          description: 'OK - either a scored response (scores array) or a daily-limit
            stub when the allowance is already spent.

            '
          content:
            application/json:
              schema:
                oneOf:
                - $ref: '#/components/schemas/ScoreResponse'
                - $ref: '#/components/schemas/MLDailyLimitReached'
  /daily-pick:
    get:
      tags:
      - daily-pick
      summary: Today's AI pick as a PatternCard, with its live track record
      description: 'The ML-selected featured opportunity, returned as a full PatternCard.
        The card''s receipts ALSO carry live_track_record (the forward-tested record
        of past picks - made in advance, scored later), the strongest receipt. featured_date
        is the date the pick was made; track_record echoes the live record at the
        top level too.

        '
      parameters:
      - name: view
        in: query
        schema:
          type: string
          enum:
          - full
          - evidence
          - decision
          - table
          default: full
        description: 'response verbosity: ''full'' (default), ''evidence'' (full rank-1
          card), ''decision'' (lean), or ''table''.'
      - name: include
        in: query
        schema:
          type: string
          enum:
          - chart
        description: include=chart attaches a `chart` block (Trend Chart curve + per-year
          bars) to the pick card.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DailyPickResponse'
  /daily-pick/track-record:
    get:
      tags:
      - daily-pick
      summary: Historical track record of past daily picks (realized win/loss)
      description: The verifiable, time-stamped performance record. Free-tier accessible
        (the marketing hook).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TrackRecord'
components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: 'Authorization: Bearer <api_key>. Key issued in the dashboard;
        verified via users.api_key_hash (HMAC).'
  parameters:
    MarketId:
      name: market_id
      in: path
      required: true
      schema:
        type: string
      description: permanent resource key '0'..'16'
    SymbolPatternsMarket:
      name: market
      in: query
      required: false
      schema:
        type: string
      description: 'Market id ''0''..''16''. Required only when your key has access
        to more than one market; a single-market key (the free/demo/navigator case)
        may omit it - the one in-scope market is used automatically.

        '
    SymbolPatternsPeCycle:
      name: pe_cycle
      in: query
      schema:
        type: string
        enum:
        - consecutive
        - pe
        default: consecutive
      description: 'presidential election cycle: ''consecutive'' (default) or ''pe''
        (current cycle position)'
    SymbolPatternsMinDays:
      name: min_days
      in: query
      schema:
        type: integer
      description: minimum pattern length in days
    SymbolPatternsMaxDays:
      name: max_days
      in: query
      schema:
        type: integer
      description: maximum pattern length in days
    SymbolPatternsMinAvgReturn:
      name: min_avg_return
      in: query
      schema:
        type: number
      description: minimum average seasonal profit, PERCENT
    SymbolPatternsMinMedianReturn:
      name: min_median_return
      in: query
      schema:
        type: number
      description: minimum median seasonal profit, PERCENT
    SymbolPatternsMinSharpe:
      name: min_sharpe
      in: query
      schema:
        type: number
      description: minimum Sharpe ratio
    SymbolPatternsYears:
      name: years
      in: query
      schema:
        type: integer
        default: 10
      description: pattern lookback in years (5-98, data-dependent)
    SymbolPatternsMinWinningYears:
      name: min_winning_years
      in: query
      schema:
        type: integer
      description: 'of those `years`, the minimum WINNING years (the win-rate floor;
        year2). DEFAULTS to ~90% of `years`; validated against the market''s DETECTION
        BAND (~75-90%+, market-specific); an out-of-band value returns 400 with the
        valid range. Per-symbol patterns exist for market ids 0,1,2,7,9 only; other
        markets return 400.

        '
  responses:
    RateLimited:
      description: 'Rate limit exceeded. X-RateLimit-Scope (mirrored as error.scope
        in the body) names WHICH window tripped: ''minute'' (a short wait; Retry-After
        counts to the next minute boundary) or ''day'' (the daily cap; Retry-After
        counts to the next UTC midnight and the X-RateLimit-* headers describe the
        DAY window). Auto-retry is only sensible for scope ''minute''.

        '
      headers:
        Retry-After:
          schema:
            type: integer
          description: seconds until the tripped window resets
        X-RateLimit-Scope:
          schema:
            type: string
            enum:
            - minute
            - day
          description: which rate window tripped
        X-RateLimit-Limit:
          schema:
            type: integer
          description: the tripped window's limit (per-minute, or per-day when scope=day)
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
          description: epoch seconds when the tripped window resets
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RateLimitError'
  schemas:
    Market:
      type: object
      properties:
        id:
          type: string
          example: '2'
        name:
          type: string
          example: S&P 500 STOCKS
        ml_eligible:
          type: boolean
        in_scope:
          type: boolean
          description: is this market in the caller's tier scope
        pattern_detection:
          $ref: '#/components/schemas/PatternDetection'
          description: which seasonal pattern-detection paths exist for this market
            + an example win-rate band
    Symbol:
      type: object
      properties:
        symbol:
          type: string
          example: AAPL
        name:
          type: string
          example: Apple Inc
    Opportunity:
      type: object
      description: A ranked seasonal trade setup. Returns are percentages, never prices.
      properties:
        symbol:
          type: string
        market:
          type: string
        direction:
          type: string
          enum:
          - long
          - short
          description: long or short - which side of the seasonal pattern the stats
            assume
        entry_date:
          type: string
          format: date
        days_out:
          type: integer
          description: 'holding period in days (internal: daysOut)'
        sharpe_ratio:
          type: number
        avg_profit_pct:
          type: number
        median_profit_pct:
          type: number
        win_rate:
          type: number
          nullable: true
          description: 'REAL historical win rate, 0..1: share of years the seasonal
            window finished profitable (any positive year; no threshold), from ChartData4
            ''Percent Profitable''. Distinct from ml.win_prob. May be null on the
            bulk list past the per-row enrichment cap.'
        years:
          type: string
          description: lookback window label, stays a string
        ml:
          $ref: '#/components/schemas/MLScore'
          description: ML block. Available on every tier, metered per day (free 5/day,
            Dev 100/day, Pro/Business unlimited); null only when the daily ML allowance
            is spent or the market is not ML-eligible (ids 0-4, 11). Legacy names
            (win_prob, pred_return, pred_mfe).
    OpportunityList:
      type: object
      description: 'Single-entry-date ranked list. window_supported is false - this
        primitive does not widen across a date window (use /scan). evaluated_count
        + enrichment_capped report the per-row win_rate enrichment cap so the min_win_rate
        filter is never silently misleading.

        '
      properties:
        entry_date:
          type: string
          format: date
          description: the single entry_date evaluated (echoes 'from', defaults to
            today)
        window_supported:
          type: boolean
          description: always false here - 'to' does not widen the window; /scan owns
            windows
        evaluated_count:
          type: integer
          description: how many candidate rows were enriched with win_rate (the enrichment
            cap, e.g. 50)
        enrichment_capped:
          type: boolean
          description: true when more rows existed than were win_rate-enriched (rows
            past the cap have unknown win_rate)
        ml_remaining_today:
          type: integer
          nullable: true
          description: ML calls remaining today for this key (null = unlimited; 0
            = daily limit reached). Free 5/day, Dev 100/day, Pro/Business unlimited.
        opportunities:
          type: array
          items:
            $ref: '#/components/schemas/Opportunity'
        disclaimer:
          type: string
          description: exact regulatory disclaimer (this is a pattern-bearing response)
    PatternCard:
      type: object
      description: 'THE unit returned by /scan, /analyze/{symbol} and /daily-pick.
        Composed server-side: structured fields for strong agents plus pre-composed
        headline + verdict so weak agents render consistently. DERIVED SEASONAL PATTERNS
        ONLY - all movement is percentages; order_ticket carries NO price level.

        '
      properties:
        rank:
          type: integer
          description: pre-sorted by the endpoint; the agent never re-sorts
        symbol:
          type: string
        market:
          $ref: '#/components/schemas/MarketRef'
        direction:
          type: string
          enum:
          - long
          - short
        bias:
          type: string
          enum:
          - bullish
          - bearish
          - neutral
          description: neutral = low conviction (edge_score < 40 OR historical_win_rate
            < 0.55 OR years_tested < 5); next_step.order_ticket is omitted.
        setup:
          $ref: '#/components/schemas/PatternSetup'
        edge_score:
          type: integer
          description: '0-100 blended ranking score: round(100 * clamp01(0.40*win_rate
            + 0.30*sharpe_n + 0.20*ml_n + 0.10*history_n)). Explainable; v1 default,
            tunable.'
        edge_basis:
          type: string
          description: human-readable dominant factors, e.g. 'win_rate 0.80 x sharpe
            1.4 x 10y history'
        stats:
          $ref: '#/components/schemas/PatternStats'
        ml:
          $ref: '#/components/schemas/PatternCardML'
          nullable: true
          description: null when ML-eligible market (0-4,11) quota is exhausted or
            market is not ML-eligible; populated up to the daily allowance (free 5/day,
            unlimited Pro/Business); tier_notes carries context
        receipts:
          $ref: '#/components/schemas/Receipts'
        next_step:
          $ref: '#/components/schemas/NextStep'
        headline:
          type: string
          description: gateway-composed one-line summary (mandatory). No em-dashes.
        verdict:
          type: string
          description: gateway-composed plain-English read (mandatory)
        alignment:
          $ref: '#/components/schemas/PatternAlignment'
          description: whether the per-instance ML view agrees with the in-sample
            seasonal win rate (a cross-check, not a 4th win rate)
        extend_research:
          $ref: '#/components/schemas/ExtendResearch'
          description: 'structured research hand-off: what TradeWave is blind to +
            what to check with your own tools. Present in the ''full'' view; the ''decision''
            view omits it per card (the envelope-level hand-off carries the methodology
            once per response).'
        chart:
          $ref: '#/components/schemas/CardChart'
          description: present ONLY when the request set include=chart - the Trend
            Chart curve + per-year bars, inline (chart DATA, never an image).
        wave_viewer:
          $ref: '#/components/schemas/WaveViewerLink'
          description: exact server-generated destination for opening this setup in
            TradeWave
        disclaimer:
          type: string
          description: exact regulatory disclaimer, present on every card
        tier_notes:
          type: string
          description: '''ML score shown.'' or the upgrade nudge when the daily ML
            allowance is spent'
        note:
          type: string
          description: optional - e.g. a bare-ticker resolution note when a symbol
            existed in multiple markets and was resolved to the primary US listing
    MarketRef:
      type: object
      properties:
        id:
          type: string
          example: '11'
          description: permanent resource key '0'..'16'
        name:
          type: string
          example: ETFs
    PatternSetup:
      type: object
      description: The timing of the setup. Dates only - no price levels.
      properties:
        entry_date:
          type: string
          format: date
        entry_window:
          type: string
          description: tolerance range (a few trading days), e.g. '2026-07-08 to 2026-07-15'
        hold_days:
          type: integer
          description: seasonal hold in calendar days
        exit_date:
          type: string
          format: date
        timing:
          $ref: '#/components/schemas/SeasonalTiming'
          nullable: true
          description: where the entry window sits relative to today (days_to_entry
            + a plain-language status); null when there is no entry date
    PatternStats:
      type: object
      description: Headline percent-only stats. No price levels.
      properties:
        historical_win_rate:
          type: number
          description: 0..1, share of profitable years (Percent Profitable). DISTINCT
            from ml.ml_win_prob.
        sharpe_ratio:
          type: number
        sharpe_ratio_mfe:
          type: number
          nullable: true
          description: MFE-based Sharpe (appserver 'Sharpe Ratio2'); arrives already
            trade-relative for shorts
        avg_return_pct:
          type: number
        median_return_pct:
          type: number
        std_dev_pct:
          type: number
          nullable: true
          description: dispersion of the yearly returns, PERCENT (sign-agnostic)
        annualized_return_pct:
          type: number
          nullable: true
          description: annualized return, PERCENT (already trade-relative for shorts)
        cumulative_return_pct:
          type: number
          nullable: true
          description: cumulative return over the lookback, PERCENT (already trade-relative
            for shorts)
        years:
          type: string
          description: lookback label, stays a string
    PatternCardML:
      type: object
      description: 'ML output on the PatternCard (all tiers, metered per day - free
        5/day, unlimited on Pro/Business). Note the names differ from the legacy MLScore
        block: ml_win_prob (NOT win_prob), pred_return_pct, pred_mfe_pct.

        '
      properties:
        ml_score:
          type: number
        ml_win_prob:
          type: number
          description: 0..1 model probability. DISTINCT from historical_win_rate.
        pred_return_pct:
          type: number
          description: predicted return %
        pred_mfe_pct:
          type: number
          description: predicted max favorable excursion %
    Receipts:
      type: object
      description: 'The trust layer - always present on a bullish/bearish read. Per-year
        historical evidence built from ChartData4 returns + Percent Profitable. Returns
        are percentages.

        '
      properties:
        years_tested:
          type: integer
        wins:
          type: integer
        losses:
          type: integer
        historical_win_rate:
          type: number
          description: 0..1, share of profitable years (same value as stats.historical_win_rate)
        avg_return_pct:
          type: number
        median_return_pct:
          type: number
        best_year:
          $ref: '#/components/schemas/YearReturn'
        worst_year:
          $ref: '#/components/schemas/YearReturn'
        per_year:
          type: array
          description: one row per lookback year (capped at the lookback length);
            never the 365-point curve
          items:
            type: object
            properties:
              year:
                type: string
              return_pct:
                type: number
              result:
                type: string
                enum:
                - win
                - loss
                description: win if return_pct > 0 (no threshold, matches the UI)
        curve_summary:
          type: object
          nullable: true
          description: shape summary of the HOLD SECTION (entry to exit) of the seasonal
            curve; null when the curve is unavailable
          properties:
            shape:
              type: string
              description: human-readable description of the hold-section trend
            trend:
              type: string
              enum:
              - rising
              - falling
              - flat
              description: direction of the seasonal index over the hold
            change_pts:
              type: number
              description: total index-point change (entry to exit) over the hold
                section
            peak_day:
              type: integer
              description: day INTO the hold (0=entry) where the seasonal index reaches
                its maximum
            trough_day:
              type: integer
              description: day INTO the hold (0=entry) where the seasonal index reaches
                its minimum
        source:
          type: string
          example: TradeWave seasonal model, 10y lookback
        as_of:
          type: string
          format: date
        live_track_record:
          allOf:
          - $ref: '#/components/schemas/TrackRecordSummary'
          nullable: true
          description: ONLY on the daily-pick card - the live forward-tested record
            of past picks (made in advance, scored later). Absent on /scan and /analyze
            cards.
    YearReturn:
      type: object
      properties:
        year:
          type: string
        return_pct:
          type: number
    TrackRecordSummary:
      type: object
      description: Live forward-tested record of past daily picks.
      properties:
        count:
          type: integer
        win_count:
          type: integer
        win_rate:
          type: number
          description: 0..1 realized win rate of past picks
        avg_return_pct:
          type: number
        note:
          type: string
    NextStep:
      type: object
      description: 'The no-broker last mile. order_ticket is broker-agnostic and copyable
        and carries NO price level (side/type/TIF/dates/note only). Omitted on neutral.

        '
      properties:
        headline:
          type: string
        order_ticket:
          type: object
          description: broker-agnostic ticket - NO price level, NO limit price in
            absolute terms
          properties:
            side:
              type: string
              enum:
              - BUY
              - SELL
            symbol:
              type: string
            type:
              type: string
              example: MARKET
            time_in_force:
              type: string
              example: DAY
            suggested_entry_date:
              type: string
              format: date
            suggested_exit_date:
              type: string
              format: date
            note:
              type: string
              description: hold guidance + a 'size to your own risk' nudge. No price
                level.
        copy_text:
          type: string
          description: one-line copyable ticket text
        set_reminder:
          type: object
          properties:
            type:
              type: string
              example: seasonal_entry
            fire_on:
              type: string
              format: date
            message:
              type: string
        framing:
          type: string
          description: '''TradeWave gives the edge and the timing; you place the trade
            at your own broker.'''
    SeasonalTiming:
      type: object
      description: Where the entry window sits relative to today. Calendar days, no
        price.
      properties:
        days_to_entry:
          type: integer
          description: calendar days from today to the entry date; negative once the
            window has opened
        status:
          type: string
          description: plain-language read, e.g. 'window opens in 12 days' | 'in the
            entry window now' | 'window has passed for this cycle'
    PatternAlignment:
      type: object
      description: Does the per-instance ML view agree with the in-sample seasonal
        win rate? A cross-check, never a 4th win rate.
      properties:
        verdict:
          type: string
          enum:
          - seasonal_only
          - aligned
          - aligned_weak
          - divergent
          - mixed
          description: '''seasonal_only'' when no ML score is present to cross-check'
        note:
          type: string
    ExtendResearch:
      type: object
      description: 'The structured research hand-off carried on every card. TradeWave
        supplies a seasonal + ML statistical edge and the timing; the assistant is
        told, in machine-readable form, to extend it with its OWN news / fundamentals
        / macro tools and synthesize. No price, no advice.

        '
      properties:
        tradewave_provides:
          type: string
        blind_to:
          type: array
          items:
            type: string
          description: what TradeWave cannot see (fundamentals, news, catalysts, macro,
            earnings, live price, ...)
        suggested_checks:
          type: array
          items:
            type: string
          description: concrete things to verify with your own tools, incl. an earnings-in-window
            nudge
        loop_back:
          type: string
          description: which TradeWave tools to call again if your findings change
            the picture
        synthesis_rule:
          type: string
          description: keep explicit which facts are TradeWave's vs your own; do not
            invent a catalyst
    PerYearBar:
      type: object
      description: One year's trade return with its favorable/adverse excursion band.
        All PERCENTAGES, never prices. Direction-aware.
      properties:
        year:
          type: string
        net_pct:
          type: number
          description: the trade's net return that year (sign-flipped for shorts)
        mfe_pct:
          type: number
          nullable: true
          description: max FAVORABLE excursion, >= 0 (trade-relative)
        mae_pct:
          type: number
          nullable: true
          description: max ADVERSE excursion, <= 0 (trade-relative)
        result:
          type: string
          enum:
          - win
          - loss
    CardChart:
      type: object
      description: 'Inline chart DATA (present only when include=chart): everything
        needed to draw the Trend Chart and the per-year bar panel client-side. Percentages
        + a 0-100 index only - never a price.

        '
      properties:
        trend_chart:
          type: array
          description: the Trend Chart - year-averaged 0-100 normalized seasonal index,
            one point per day (entry-anchored)
          items:
            type: object
            properties:
              date:
                type: string
                format: date
              index:
                type: number
                description: 0-100 relative seasonal index
        per_year_bars:
          type: array
          items:
            $ref: '#/components/schemas/PerYearBar'
        presentation_order:
          type: array
          items:
            type: string
          description: 'recommended evidence order: year-by-year evidence, then seasonal
            trend'
        recommended_charts:
          type: array
          items:
            type: object
          description: client-neutral chart semantics, labels, data references, units,
            highlights, and purpose
        rendering_instruction:
          type: string
          description: agent instruction to display both charts and preserve their
            units/meaning
        note:
          type: string
    WaveViewerLink:
      type: object
      description: Server-generated deep link to this exact pattern. Authentication
        remains at the TradeWave app boundary.
      properties:
        label:
          type: string
        url:
          type: string
          format: uri
        opens:
          type: string
          enum:
          - Wave Viewer
        pattern:
          type: object
          properties:
            market_id:
              type: string
            symbol:
              type: string
            entry_date:
              type: string
              format: date
            hold_days:
              type: integer
            years:
              type: string
        value:
          type: string
    PatternDetection:
      type: object
      description: Which seasonal pattern-detection paths exist for a market + an
        illustrative win-rate band.
      properties:
        scan_detection:
          type: boolean
          description: scan/opportunity detection (find_best_opportunities) available
            - true for all 15 markets
        by_symbol_detection:
          type: boolean
          description: per-symbol pattern detection (get_symbol_patterns) available
            - true ONLY for market ids 0,1,2,7,9
        lookback_years_range:
          type: array
          items:
            type: integer
          description: '[min, max] lookback (year1) available for the scan path'
        example_band:
          type: string
          description: 'an illustrative win-rate band, e.g. ''20-year lookback: min_winning_years
            18-20 (~90%+ winners)'''
    ScanResponse:
      type: object
      properties:
        generated_at:
          type: string
          format: date-time
        summary:
          type: string
          description: 'the honest one-line lead: how many candidates were evaluated,
            how many shown, how many carry a high-conviction seasonal pattern - plus
            any degradation (upstream win-rate outage), enrichment cap, normalization
            note, or plan cap'
        window:
          type: string
          description: the resolved window (e.g. 'now')
        markets_scanned:
          type: array
          items:
            type: string
          description: the requested in-scope market ids; check market_failures before
            treating the scan as complete
        market_failures:
          type: array
          items:
            type: string
          description: market ids whose upstream request failed and whose results
            were omitted; normally empty
        rank_by:
          type: string
          enum:
          - edge
          - win_rate
          - sharpe
          - ml
          - avg_return
        view:
          type: string
          enum:
          - full
          - evidence
          - decision
          - table
          description: the verbosity applied (echoes the request)
        lookback:
          type: object
          description: the resolved detection knobs (year1, year2)
          properties:
            years:
              type: integer
            min_winning_years:
              type: integer
        count:
          type: integer
          description: number of cards returned
        evaluated_count:
          type: integer
          description: candidates considered before slicing/enrichment
        enrichment_capped:
          type: boolean
          description: true when only the leading requested/bounded candidate depth
            was fully receipt-enriched
        capped_by_plan:
          type: boolean
          description: true when the PLAN's opp_limit (not the caller's own limit)
            was the binding constraint on how many ranked cards came back - the free-tier
            wall, made visible
        shown_of_evaluated:
          type: string
          example: 3 of 6
          description: '''shown of evaluated'' as a display string'
        upgrade_url:
          type: string
          format: uri
          description: present only when capped_by_plan is true
        ml_remaining_today:
          type: integer
          nullable: true
          description: ML calls remaining today for this key (null = unlimited; 0
            = daily limit reached). Free 5/day, Dev 100/day, Pro/Business unlimited.
        opportunities:
          type: array
          description: ranked PatternCards. Under view='evidence', rank 1 is full
            and runners are lean; under decision every card is lean; under table each
            item is a compact row.
          items:
            $ref: '#/components/schemas/PatternCard'
        lookback_note:
          type: string
          description: present only when the (year1, year2) combo is out of band for
            some scanned markets - it names them (they contributed nothing). Raise
            min_winning_years to include them.
        disclaimer:
          type: string
          description: exact regulatory disclaimer (envelope level; guarantees it
            survives a 'table' projection)
    AnalyzeResponse:
      type: object
      properties:
        card:
          $ref: '#/components/schemas/PatternCard'
          description: the best setup as a PatternCard (trimmed under view='decision';
            a compact row under view='table'). May carry a `note` when a bare ticker
            was resolved to a primary listing.
        other_setups:
          type: array
          description: alternate candidate setups for the same symbol (compact - no
            card composition)
          items:
            $ref: '#/components/schemas/CompactSetup'
        view:
          type: string
          enum:
          - full
          - evidence
          - decision
          - table
          description: the verbosity applied (echoes the request)
        disclaimer:
          type: string
          description: exact regulatory disclaimer (envelope level)
        as_of:
          type: string
          format: date
    CompactSetup:
      type: object
      description: A compact alternate setup (percent stats only, no card/receipts/ml).
      properties:
        symbol:
          type: string
        direction:
          type: string
          enum:
          - long
          - short
        entry_date:
          type: string
          format: date
        hold_days:
          type: integer
        historical_win_rate:
          type: number
          description: 0..1, share of profitable years
        sharpe_ratio:
          type: number
        avg_return_pct:
          type: number
        median_return_pct:
          type: number
        years:
          type: string
    DailyPickResponse:
      type: object
      properties:
        card:
          $ref: '#/components/schemas/PatternCard'
          description: the pick as a PatternCard (trimmed under view='decision');
            receipts.live_track_record carries the live record
        featured_date:
          type: string
          format: date
          description: the date the pick was made
        track_record:
          $ref: '#/components/schemas/TrackRecordSummary'
          description: the live forward-tested record (echoed at top level)
        view:
          type: string
          enum:
          - full
          - evidence
          - decision
          - table
          description: the verbosity applied (echoes the request)
        disclaimer:
          type: string
          description: exact regulatory disclaimer (envelope level)
        as_of:
          type: string
          format: date
    ScoreRequest:
      type: object
      properties:
        opportunities:
          type: array
          maxItems: 100
          description: capped at 100 items per request (a larger batch returns 400
            naming the cap - split into chunks)
          items:
            type: object
            required:
            - symbol
            - date
            - days_out
            - direction
            properties:
              symbol:
                type: string
              date:
                type: string
                format: date
              days_out:
                type: integer
              direction:
                type: string
                enum:
                - long
                - short
    MLScore:
      type: object
      description: 'ML output block used by /score and bulk /opportunities (legacy
        field names). Available to all tiers, metered per day (free 5/day, unlimited
        Pro/Business). The PatternCard uses PatternCardML instead (ml_win_prob / pred_return_pct
        / pred_mfe_pct).

        '
      properties:
        ml_score:
          type: number
        win_prob:
          type: number
          description: 0..1 model probability. DISTINCT from the historical win_rate.
            (PatternCard names this ml_win_prob.)
        pred_return:
          type: number
          description: predicted return %
        pred_mfe:
          type: number
          description: predicted max favorable excursion %
    Pattern:
      type: object
      description: Aggregate seasonal-pattern statistics (ChartData4 stats block).
        No raw price series.
      properties:
        symbol:
          type: string
        market:
          type: string
        win_rate:
          type: number
          nullable: true
          description: 'REAL historical win rate, 0..1: share of profitable years
            (no threshold), from ''Percent Profitable''. Also present inside stats
            as percent_profitable.'
        stats:
          type: object
          description: Publishable percent-only subset of the ChartData4 stats; price/volume
            levels (52W high/low, SMA, avg volume) are excluded.
        disclaimer:
          type: string
          description: exact regulatory disclaimer (this is a pattern-bearing response)
    SeasonalChart:
      type: object
      description: 'The seasonal ''trendchart'' as DATA: the appserver''s consolidated_seasonal_chart2
        curve. seasonal_curve is the smooth daily seasonal line - one point per day
        over the ~365-day annual cycle starting at entry_date - where `index` is a
        0-100 RELATIVE seasonal index (each year''s close high/low-normalized to 0-100,
        then AVERAGED across the lookback years). It is a relative SHAPE, not price
        levels and not a percent return: prices cannot be reconstructed (the per-year
        high/low are not exposed and the curve is year-averaged). No raw price series
        is returned.

        '
      properties:
        symbol:
          type: string
        market:
          type: string
        direction:
          type: string
          enum:
          - long
          - short
        entry_date:
          type: string
          format: date
          description: start of the annual cycle (chart_start_date)
        days_out:
          type: integer
          nullable: true
          description: echoed for parity; does not change the full-cycle curve
        years:
          type: string
          description: lookback window label, stays a string
        win_rate:
          type: number
          nullable: true
          description: REAL historical win rate, 0..1, from ChartData4 'Percent Profitable'
            (same as /patterns).
        seasonal_curve:
          type: array
          description: the smooth seasonal line - year-averaged 0-100 normalized index,
            one point per calendar day of the cycle
          items:
            type: object
            properties:
              date:
                type: string
                format: date
              index:
                type: number
                description: 0-100 relative seasonal index (high/low-normalized, averaged
                  across years)
        stats:
          type: object
          description: same publishable percent-only stat block as /patterns
        per_year_bars:
          type: array
          description: per-year bars for the Trend Chart - each completed year's trade
            return plus its favorable/adverse excursion band, returned alongside the
            curve (one-call curve+bars). Direction-aware, percentages only.
          items:
            $ref: '#/components/schemas/PerYearBar'
        disclaimer:
          type: string
          description: exact regulatory disclaimer (this is a pattern-bearing response)
    DailyPick:
      type: object
      properties:
        symbol:
          type: string
        featured_date:
          type: string
          format: date
        direction:
          type: string
          enum:
          - long
          - short
        days_out:
          type: integer
        pattern_summary:
          type: string
        ml:
          $ref: '#/components/schemas/MLScore'
    TrackRecord:
      type: object
      properties:
        summary:
          type: object
          properties:
            count:
              type: integer
            win_count:
              type: integer
            win_rate:
              type: number
            avg_return_pct:
              type: number
        picks:
          type: array
          items:
            type: object
            properties:
              symbol:
                type: string
              featured_date:
                type: string
                format: date
              return_pct:
                type: number
              result:
                type: string
                enum:
                - win
                - loss
                - open
        disclaimer:
          type: string
          description: exact regulatory disclaimer (performance record - 'past performance
            is not indicative...')
    Error:
      type: object
      description: 'The uniform error envelope - EVERY non-2xx response uses it, including
        framework errors (e.g. 405 -> code ''method_not_allowed'' with an Allow header
        naming the valid methods; 404 -> ''not_found''). Never an HTML error page
        from the gateway (the fronting proxy also maps its own 502/504 to this envelope
        on a best-effort basis).

        '
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              description: 'machine-readable, e.g. invalid_request | unauthorized
                | forbidden | not_found | method_not_allowed | rate_limited | upstream_unavailable
                | internal | demo_restricted (403 - the public demo token is limited
                to its 5-ticker allowlist and blocked from enumeration-style endpoints)
                | scan_busy (503 - the scan cache is warming; retry shortly) | service_misconfigured
                (503 - a server-side configuration error; never returned as an authentication
                result)

                '
            message:
              type: string
    RateLimitError:
      type: object
      description: The 429 body - the uniform Error envelope plus scope (which window
        tripped).
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              example: rate_limited
            message:
              type: string
            scope:
              type: string
              enum:
              - minute
              - day
              description: which rate window tripped (mirrors the X-RateLimit-Scope
                header). 'day' means blocked until the next UTC midnight - do not
                auto-retry.
    Me:
      type: object
      description: Identity + capabilities for the caller's key (GET /me). Read-only.
      properties:
        tier:
          type: string
          description: 'The caller''s tier. The purchasable API ladder is free | dev
            | pro | business. Bundled or web-linked principals can also see other
            values depending on how the caller authenticated: ''demo'' (the public
            demo token), ''navigator'' (a web Navigator sub bundled into the API,
            tiers.py INTERNAL_TIERS), or - under MCP OAuth - a web-mirror tier name
            (explorer | navigator | analyst | strategist, apiserver/auth.py _resolve_mcp).
            Do not treat this as a closed enum.

            '
        tier_name:
          type: string
          example: Dev
        ml_remaining_today:
          type: integer
          nullable: true
          description: ML calls remaining today (null = unlimited)
        ml_daily_limit:
          type: integer
          nullable: true
          description: the plan's daily ML allowance (null = unlimited)
        opp_limit:
          type: integer
          description: max results per opportunities/scan call on this plan
        rate:
          type: object
          description: the plan's rate limits
          properties:
            per_minute:
              type: integer
            per_day:
              type: integer
        markets_in_scope:
          type: array
          description: the markets this key can query, each with its ML eligibility
          items:
            type: object
            properties:
              id:
                type: string
                example: '2'
                description: permanent resource key '0'..'16'
              name:
                type: string
                example: S&P 500 STOCKS
              ml_eligible:
                type: boolean
        upgrade_url:
          type: string
          format: uri
    ScoreResponse:
      type: object
      description: 'Successful ML score response. granted = how many items were actually
        scored (may be less than requested if the daily allowance ran out mid-batch).
        Items beyond granted appear in scores[] with note:"daily ML limit reached
        - upgrade for unlimited" and no ML fields. ml_remaining_today is null for
        unlimited tiers (Pro/Business).

        '
      properties:
        scores:
          type: array
          items:
            allOf:
            - $ref: '#/components/schemas/MLScore'
            - type: object
              properties:
                note:
                  type: string
                  description: present only on unscored items beyond the daily allowance
        granted:
          type: integer
          description: number of items that were actually scored
        ml_remaining_today:
          type: integer
          nullable: true
          description: ML calls remaining today after this batch (null = unlimited)
        disclaimer:
          type: string
          description: exact regulatory disclaimer (ML scores are pattern-bearing)
    MLDailyLimitReached:
      type: object
      description: 'Returned (HTTP 200) when the daily ML allowance is already fully
        spent before this request. reason is always ''ml_daily_limit''.

        '
      properties:
        requires:
          type: string
          example: upgrade
        reason:
          type: string
          example: ml_daily_limit
        message:
          type: string
        upgrade_url:
          type: string
          format: uri
        ml_remaining_today:
          type: integer
          example: 0
    UpgradeRequired:
      type: object
      properties:
        requires:
          type: string
          example: pro
        message:
          type: string
        upgrade_url:
          type: string
          format: uri
