openapi: 3.1.0
jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
info:
  title: SparkData Service API
  version: 1.5.0
  summary: Multi-tenant, agent-friendly China market data service
  description: |
    SparkData provides tenant and API-key administration, contract-driven aggregate
    access to all 371 contract-driven operations, normalized equities, ETFs, indexes, latest quotes,
    complete bar timeframes, JSON/NDJSON responses, and best-effort SSE quote
    streaming. MySQL stores control-plane state and Redis stores data caches plus
    rate-limit counters. Persistence, audit, billing, durable replay, tick trades,
    and order APIs are outside this release.
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://test.spark-data.cn
    description: Production K3s endpoint
  - url: http://localhost:8080
    description: Optional local K3s endpoint
tags:
  - name: System
    description: Kubernetes liveness and dependency readiness.
  - name: Control Plane
    description: Operator-only tenant, quota, and API-key management.
  - name: Instruments
    description: Normalized equity, ETF, and index security-master resources.
  - name: Market Data
    description: Cached quotes, bars, and best-effort streams.
  - name: Agent Context
    description: Agent-oriented compound views assembled from reusable cached resources.
  - name: Aggregate Data
    description: Nine agent-facing capability APIs covering all 371 published operations.
security:
  - TenantApiKey: []
paths:
  /health/live:
    get:
      tags: [System]
      operationId: getLiveness
      summary: Check process liveness
      security: []
      responses:
        '200':
          description: Process is alive
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
  /health/ready:
    get:
      tags: [System]
      operationId: getReadiness
      summary: Check MySQL and Redis readiness
      security: []
      responses:
        '200':
          description: Dependencies are ready
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Health'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/admin/tenants:
    get:
      tags: [Control Plane]
      operationId: listTenants
      summary: List tenants
      security:
        - AdminApiKey: []
      responses:
        '200':
          description: Tenant list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          $ref: '#/components/responses/Unavailable'
    post:
      tags: [Control Plane]
      operationId: createTenant
      summary: Create a tenant with service limits
      security:
        - AdminApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTenant'
      responses:
        '201':
          description: Tenant created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/admin/tenants/{tenant_id}:
    parameters:
      - $ref: '#/components/parameters/TenantIdPath'
    get:
      tags: [Control Plane]
      operationId: getTenant
      summary: Get a tenant
      security:
        - AdminApiKey: []
      responses:
        '200':
          description: Tenant found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          $ref: '#/components/responses/Unavailable'
    patch:
      tags: [Control Plane]
      operationId: updateTenant
      summary: Update tenant status or limits
      security:
        - AdminApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTenant'
      responses:
        '200':
          description: Tenant updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TenantResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/admin/tenants/{tenant_id}/api-keys:
    parameters:
      - $ref: '#/components/parameters/TenantIdPath'
    get:
      tags: [Control Plane]
      operationId: listApiKeys
      summary: List API-key metadata without secrets
      security:
        - AdminApiKey: []
      responses:
        '200':
          description: API-key metadata list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeysResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          $ref: '#/components/responses/Unavailable'
    post:
      tags: [Control Plane]
      operationId: createApiKey
      summary: Issue an API key; plaintext is returned once
      security:
        - AdminApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateApiKey'
      responses:
        '201':
          description: API key issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssuedApiKeyResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/admin/tenants/{tenant_id}/api-keys/{api_key_id}:
    delete:
      tags: [Control Plane]
      operationId: revokeApiKey
      summary: Revoke an API key immediately
      security:
        - AdminApiKey: []
      parameters:
        - $ref: '#/components/parameters/TenantIdPath'
        - $ref: '#/components/parameters/ApiKeyIdPath'
      responses:
        '204':
          description: API key revoked
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/discovery:
    get:
      tags: [Aggregate Data]
      operationId: describeDiscovery
      summary: List exact discovery operation contracts
      parameters: [{$ref: '#/components/parameters/OperationFilter'}]
      responses:
        '200': {$ref: '#/components/responses/OperationContracts'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
    post:
      tags: [Aggregate Data]
      operationId: queryDiscovery
      summary: Invoke a discovery or screening operation
      requestBody:
        $ref: '#/components/requestBodies/AggregateQuery'
      responses:
        '200': {$ref: '#/components/responses/AggregateResult'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
  /v1/entities:
    get:
      tags: [Aggregate Data]
      operationId: describeEntities
      summary: List exact entity operation contracts
      parameters: [{$ref: '#/components/parameters/OperationFilter'}]
      responses:
        '200': {$ref: '#/components/responses/OperationContracts'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
    post:
      tags: [Aggregate Data]
      operationId: queryEntities
      summary: Invoke an entity or reference-data operation
      requestBody:
        $ref: '#/components/requestBodies/AggregateQuery'
      responses:
        '200': {$ref: '#/components/responses/AggregateResult'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
  /v1/market-data:
    get:
      tags: [Aggregate Data]
      operationId: describeMarketData
      summary: List exact market-data operation contracts
      parameters: [{$ref: '#/components/parameters/OperationFilter'}]
      responses:
        '200': {$ref: '#/components/responses/OperationContracts'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
    post:
      tags: [Aggregate Data]
      operationId: queryMarketData
      summary: Invoke a unified market-data operation
      requestBody:
        $ref: '#/components/requestBodies/AggregateQuery'
      responses:
        '200': {$ref: '#/components/responses/AggregateResult'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
  /v1/fundamentals:
    get:
      tags: [Aggregate Data]
      operationId: describeFundamentals
      summary: List exact fundamentals operation contracts
      parameters: [{$ref: '#/components/parameters/OperationFilter'}]
      responses:
        '200': {$ref: '#/components/responses/OperationContracts'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
    post:
      tags: [Aggregate Data]
      operationId: queryFundamentals
      summary: Invoke a fundamentals or forecast operation
      requestBody:
        $ref: '#/components/requestBodies/AggregateQuery'
      responses:
        '200': {$ref: '#/components/responses/AggregateResult'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
  /v1/ownership:
    get:
      tags: [Aggregate Data]
      operationId: describeOwnership
      summary: List exact ownership operation contracts
      parameters: [{$ref: '#/components/parameters/OperationFilter'}]
      responses:
        '200': {$ref: '#/components/responses/OperationContracts'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
    post:
      tags: [Aggregate Data]
      operationId: queryOwnership
      summary: Invoke an ownership or portfolio operation
      requestBody:
        $ref: '#/components/requestBodies/AggregateQuery'
      responses:
        '200': {$ref: '#/components/responses/AggregateResult'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
  /v1/events:
    get:
      tags: [Aggregate Data]
      operationId: describeEvents
      summary: List exact event operation contracts
      parameters: [{$ref: '#/components/parameters/OperationFilter'}]
      responses:
        '200': {$ref: '#/components/responses/OperationContracts'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
    post:
      tags: [Aggregate Data]
      operationId: queryEvents
      summary: Invoke an event, corporate-action, or risk operation
      requestBody:
        $ref: '#/components/requestBodies/AggregateQuery'
      responses:
        '200': {$ref: '#/components/responses/AggregateResult'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
  /v1/analytics:
    get:
      tags: [Aggregate Data]
      operationId: describeAnalytics
      summary: List exact analytics operation contracts
      parameters: [{$ref: '#/components/parameters/OperationFilter'}]
      responses:
        '200': {$ref: '#/components/responses/OperationContracts'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
    post:
      tags: [Aggregate Data]
      operationId: queryAnalytics
      summary: Invoke an analytics, valuation, or ranking operation
      requestBody:
        $ref: '#/components/requestBodies/AggregateQuery'
      responses:
        '200': {$ref: '#/components/responses/AggregateResult'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
  /v1/economy:
    get:
      tags: [Aggregate Data]
      operationId: describeEconomy
      summary: List exact economy operation contracts
      parameters: [{$ref: '#/components/parameters/OperationFilter'}]
      responses:
        '200': {$ref: '#/components/responses/OperationContracts'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
    post:
      tags: [Aggregate Data]
      operationId: queryEconomy
      summary: Invoke a macro, regional, or industry-economy operation
      requestBody:
        $ref: '#/components/requestBodies/AggregateQuery'
      responses:
        '200': {$ref: '#/components/responses/AggregateResult'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
  /v1/content:
    get:
      tags: [Aggregate Data]
      operationId: describeContent
      summary: List exact content operation contracts
      parameters: [{$ref: '#/components/parameters/OperationFilter'}]
      responses:
        '200': {$ref: '#/components/responses/OperationContracts'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
    post:
      tags: [Aggregate Data]
      operationId: queryContent
      summary: Invoke a research, filing, news, or policy operation
      requestBody:
        $ref: '#/components/requestBodies/AggregateQuery'
      responses:
        '200': {$ref: '#/components/responses/AggregateResult'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        '503': {$ref: '#/components/responses/Unavailable'}
  /v1/agent/context/{instrument_id}:
    get:
      tags: [Agent Context]
      operationId: getAgentContext
      summary: Get a comprehensive cached context for one equity, ETF, or index
      description: |
        Combines the company profile, latest quote, and requested bars in one
        agent-friendly response. The operation does not create a monolithic cache
        entry: it reuses canonical per-instrument quote and time-partitioned bar
        fragments, so overlapping requests from different users share cache hits.
        When `start` and `end` are both omitted, the service returns up to the last
        365 calendar days, capped by the tenant's bar-range limit. Provider data is
        included verbatim beside normalized fields. Both dates must be supplied
        together when overriding the default range.
      parameters:
        - $ref: '#/components/parameters/InstrumentIdPath'
        - name: start
          in: query
          required: false
          description: Inclusive UTC timestamp; must be paired with `end`.
          schema:
            type: string
            format: date-time
        - name: end
          in: query
          required: false
          description: Exclusive UTC timestamp; must be paired with `start`.
          schema:
            type: string
            format: date-time
        - $ref: '#/components/parameters/Timeframe'
        - $ref: '#/components/parameters/Adjustment'
        - $ref: '#/components/parameters/Sort'
      responses:
        '200':
          description: Agent context assembled from reusable cached resources
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentContextResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/instruments/{instrument_id}:
    get:
      tags: [Instruments]
      operationId: getInstrument
      summary: Get a normalized equity, ETF, or index
      parameters:
        - $ref: '#/components/parameters/InstrumentIdPath'
      responses:
        '200':
          description: Instrument found
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            X-Cache:
              $ref: '#/components/headers/XCache'
            X-Tenant-Id:
              $ref: '#/components/headers/XTenantId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InstrumentResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/market-data/bars:
    get:
      tags: [Market Data]
      operationId: listBars
      summary: Query equity, ETF, or index OHLCV bars across complete timeframes
      description: |
        Uses an inclusive `start` and exclusive `end`. JSON is the default.
        Send `Accept: application/x-ndjson` for one bar per line followed by a
        `stream_end` control record. Intraday bars are derived from upstream
        sampled cumulative quotes and are explicitly marked `snapshot_derived`;
        daily bars come from upstream OHLCV endpoints. Longer equity bars come
        from upstream period endpoints; longer ETF and index bars are aggregated
        from upstream daily bars and marked `daily_derived`.
      parameters:
        - $ref: '#/components/parameters/Symbols'
        - $ref: '#/components/parameters/StartTime'
        - $ref: '#/components/parameters/EndTime'
        - $ref: '#/components/parameters/Timeframe'
        - $ref: '#/components/parameters/Adjustment'
        - $ref: '#/components/parameters/Feed'
        - $ref: '#/components/parameters/Sort'
      responses:
        '200':
          description: Bars or an NDJSON bar stream
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BarsResponse'
            application/x-ndjson:
              schema:
                $ref: '#/components/schemas/BarStreamRecord'
              x-streaming: true
              x-record-delimiter: newline
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/market-data/quotes/latest:
    get:
      tags: [Market Data]
      operationId: getLatestQuotes
      summary: Get latest quotes for multiple equities, ETFs, or indexes
      parameters:
        - $ref: '#/components/parameters/Symbols'
        - $ref: '#/components/parameters/Feed'
      responses:
        '200':
          description: Latest quotes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuotesResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          $ref: '#/components/responses/Unavailable'
  /v1/market-data/stream:
    get:
      tags: [Market Data]
      operationId: streamMarketData
      summary: Stream best-effort latest quotes with SSE
      description: |
        Emits `quote`, `heartbeat`, and `reconnect` events. This MVP deliberately
        provides neither durability nor replay. A new connection starts from the
        current quote. The regular OpenAPI HTTP operation describes both transports;
        a separate AsyncAPI contract is unnecessary for this unidirectional SSE API.
      parameters:
        - $ref: '#/components/parameters/Symbols'
        - $ref: '#/components/parameters/Feed'
        - $ref: '#/components/parameters/HeartbeatSeconds'
      responses:
        '200':
          description: SSE quote stream
          content:
            text/event-stream:
              schema:
                $ref: '#/components/schemas/MarketEvent'
              x-streaming: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '503':
          $ref: '#/components/responses/Unavailable'
components:
  securitySchemes:
    TenantApiKey:
      type: http
      scheme: bearer
      bearerFormat: SparkData tenant API key
      description: Tenant key beginning with `spk_`; stored only as SHA-256 in MySQL.
    AdminApiKey:
      type: http
      scheme: bearer
      bearerFormat: SparkData bootstrap admin key
      description: Kubernetes-secret-backed operator key for control-plane operations.
  parameters:
    TenantIdPath:
      name: tenant_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    ApiKeyIdPath:
      name: api_key_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
    InstrumentIdPath:
      name: instrument_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/InstrumentId'
      example: XSHG:600570
    Symbols:
      name: symbols
      in: query
      required: true
      description: Comma-separated canonical IDs; tenant limits apply.
      style: form
      explode: false
      schema:
        type: array
        minItems: 1
        maxItems: 100
        uniqueItems: true
        items:
          $ref: '#/components/schemas/InstrumentId'
      example: [XSHG:600570, XSHE:000001]
    StartTime:
      name: start
      in: query
      required: true
      description: Inclusive UTC timestamp.
      schema:
        type: string
        format: date-time
    EndTime:
      name: end
      in: query
      required: true
      description: Exclusive UTC timestamp; tenant range limits apply.
      schema:
        type: string
        format: date-time
    Timeframe:
      name: timeframe
      in: query
      required: false
      schema:
        $ref: '#/components/schemas/BarTimeframe'
        default: 1d
    Adjustment:
      name: adjustment
      in: query
      required: false
      description: ETF and index bars accept `raw` only.
      schema:
        type: string
        enum: [raw, forward, backward]
        default: raw
    Feed:
      name: feed
      in: query
      required: false
      deprecated: true
      description: Compatibility routing hint; the currently deployed source exposes one feed.
      schema:
        type: string
        enum: [auto, realtime, delayed]
        default: auto
    Sort:
      name: sort
      in: query
      required: false
      schema:
        type: string
        enum: [asc, desc]
        default: asc
    HeartbeatSeconds:
      name: heartbeat_seconds
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 60
        default: 20
    OperationFilter:
      name: operation
      in: query
      required: false
      description: Case-sensitive operation name. Omit to list every operation in the capability.
      schema:
        type: string
        minLength: 1
  headers:
    XRequestId:
      description: Request correlation identifier.
      schema:
        type: string
        format: uuid
    XCache:
      description: Aggregate Redis cache result.
      schema:
        $ref: '#/components/schemas/CacheStatus'
    XTenantId:
      description: Authenticated tenant identifier.
      schema:
        type: string
        format: uuid
  requestBodies:
    AggregateQuery:
      required: true
      description: |
        `operation` must be one of the operations assigned to this capability in
        the published operation catalog. `parameters` uses the exact contract field names.
        Enumeration parameters accept a dictionary code, caption, reference code,
        or canonical A-share ID; SparkData resolves and validates the upstream code.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AggregateQuery'
  responses:
    OperationContracts:
      description: Exact generated workbook contracts for the selected capability
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        X-Cache:
          $ref: '#/components/headers/XCache'
        X-Tenant-Id:
          $ref: '#/components/headers/XTenantId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/OperationContractsResponse'
    AggregateResult:
      description: Complete upstream response plus resolved enum inputs and cache metadata
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        X-Cache:
          $ref: '#/components/headers/XCache'
        X-Tenant-Id:
          $ref: '#/components/headers/XTenantId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/AggregateResponse'
    BadRequest:
      description: Invalid request
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Unauthorized:
      description: Missing, malformed, expired, revoked, or unknown API key
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Forbidden:
      description: API key lacks the required scope
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    NotFound:
      description: Resource not found
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Conflict:
      description: Resource state prevents the operation
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    TooManyRequests:
      description: Tenant API-key rate limit exceeded
      headers:
        Retry-After:
          schema:
            type: integer
            minimum: 1
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Unavailable:
      description: MySQL, Redis, or upstream dependency is unavailable
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
  schemas:
    Health:
      type: object
      additionalProperties: false
      required: [status]
      properties:
        status:
          type: string
          const: ok
    InstrumentId:
      type: string
      pattern: '^(XSHG|XSHE|XBSE):[0-9]{6}$'
      description: Canonical MIC-prefixed ID for a supported mainland equity, ETF, or index.
    Decimal:
      type: string
      pattern: '^-?(0|[1-9][0-9]*)(\.[0-9]+)?$'
    BarTimeframe:
      type: string
      enum: [1m, 5m, 15m, 30m, 1h, 1d, 1w, 1mo, 1q, 1y]
    CacheStatus:
      type: string
      enum: [hit, miss, mixed, bypass]
    ResponseMeta:
      type: object
      additionalProperties: false
      required: [request_id, schema_version, cache_status, source, generated_at]
      properties:
        request_id:
          type: string
          format: uuid
        schema_version:
          type: string
          const: '1.0'
        cache_status:
          $ref: '#/components/schemas/CacheStatus'
        source:
          type: string
          enum: [sparkdata-market-data, sparkdata-control-plane]
        generated_at:
          type: string
          format: date-time
    Tenant:
      type: object
      additionalProperties: false
      required: [id, name, status, requests_per_minute, max_symbols_per_request, max_bar_range_days, created_at, updated_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          minLength: 1
          maxLength: 128
        status:
          type: string
          enum: [active, suspended]
        requests_per_minute:
          type: integer
          minimum: 1
          maximum: 100000
        max_symbols_per_request:
          type: integer
          minimum: 1
          maximum: 100
        max_bar_range_days:
          type: integer
          minimum: 1
          maximum: 3650
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreateTenant:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 128
        requests_per_minute:
          type: integer
          minimum: 1
          maximum: 100000
          default: 600
        max_symbols_per_request:
          type: integer
          minimum: 1
          maximum: 100
          default: 50
        max_bar_range_days:
          type: integer
          minimum: 1
          maximum: 3650
          default: 366
    UpdateTenant:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 128
        status:
          type: string
          enum: [active, suspended]
        requests_per_minute:
          type: integer
          minimum: 1
          maximum: 100000
        max_symbols_per_request:
          type: integer
          minimum: 1
          maximum: 100
        max_bar_range_days:
          type: integer
          minimum: 1
          maximum: 3650
    ApiKey:
      type: object
      additionalProperties: false
      required: [id, tenant_id, name, key_prefix, scopes, status, expires_at, created_at, revoked_at]
      properties:
        id:
          type: string
          format: uuid
        tenant_id:
          type: string
          format: uuid
        name:
          type: string
        key_prefix:
          type: string
          description: Non-secret prefix for identifying the key.
        scopes:
          type: array
          items:
            type: string
            enum: [market-data:read]
        status:
          type: string
          enum: [active, revoked]
        expires_at:
          type: [string, 'null']
          format: date-time
        created_at:
          type: string
          format: date-time
        revoked_at:
          type: [string, 'null']
          format: date-time
    IssuedApiKey:
      allOf:
        - $ref: '#/components/schemas/ApiKey'
        - type: object
          required: [api_key]
          properties:
            api_key:
              type: string
              pattern: '^spk_[0-9a-f]{64}$'
              writeOnly: true
    CreateApiKey:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 128
        scopes:
          type: array
          minItems: 1
          maxItems: 8
          default: [market-data:read]
          items:
            type: string
            enum: [market-data:read]
        expires_at:
          type: [string, 'null']
          format: date-time
    Instrument:
      type: object
      additionalProperties: false
      required: [instrument_id, symbol, name, company_name, mic, asset_class, currency, timezone, status, industry, concepts, provider_data]
      properties:
        instrument_id:
          $ref: '#/components/schemas/InstrumentId'
        symbol:
          type: string
          pattern: '^[0-9]{6}$'
        name:
          type: [string, 'null']
        company_name:
          type: [string, 'null']
        mic:
          type: string
          enum: [XSHG, XSHE, XBSE]
        asset_class:
          type: string
          enum: [equity, etf, index]
        currency:
          type: string
        timezone:
          type: string
        status:
          type: string
        industry:
          type: [string, 'null']
        concepts:
          type: array
          items:
            type: string
        provider_data:
          $ref: '#/components/schemas/ProviderData'
    ProviderData:
      type: object
      additionalProperties: false
      required: [operation, raw]
      properties:
        operation:
          type: string
          description: Exact operation name from the published source contract or dictionary service.
        raw:
          description: Exact untrimmed upstream envelope, row, or rows used for a derived record.
    Capability:
      type: string
      enum: [discovery, entities, market_data, fundamentals, ownership, events, analytics, economy, content]
    ContractDataType:
      type: string
      enum: [string, string_array, number, number_array, date, date_array]
    InputContract:
      type: object
      additionalProperties: false
      required: [name, upstream_name, display_name, data_type, description, required, enum_group, enum_declaration, fixed_value]
      properties:
        name:
          type: string
          description: Public parameter name accepted by SparkData.
        upstream_name:
          type: string
          description: Exact field name sent to the live upstream.
        display_name:
          type: string
        data_type:
          $ref: '#/components/schemas/ContractDataType'
        description:
          type: string
        required:
          type: boolean
        enum_group:
          type: [string, 'null']
          description: Live dictionary group when the workbook marks the input as enumerated.
        enum_declaration:
          type: [string, 'null']
          description: Unmodified enumeration declaration from the workbook.
        fixed_value:
          type: [string, 'null']
          description: Value marked as fixed by the workbook and inserted by SparkData.
    OutputContract:
      type: object
      additionalProperties: false
      required: [name, display_name, data_type, description]
      properties:
        name:
          type: string
        display_name:
          type: string
        data_type:
          $ref: '#/components/schemas/ContractDataType'
        description:
          type: string
    OperationContract:
      type: object
      additionalProperties: false
      required: [api_id, sheet_name, display_name, operation, description, method, path, capability, last_modified, inputs, outputs]
      properties:
        api_id:
          type: integer
          minimum: 1
          maximum: 371
        sheet_name:
          type: string
        display_name:
          type: string
        operation:
          type: string
        description:
          type: string
        method:
          type: string
          const: POST
        path:
          type: string
          pattern: '^/'
        capability:
          $ref: '#/components/schemas/Capability'
        last_modified:
          type: string
        inputs:
          type: array
          items:
            $ref: '#/components/schemas/InputContract'
        outputs:
          type: array
          items:
            $ref: '#/components/schemas/OutputContract'
    OperationContractsResponse:
      type: object
      additionalProperties: false
      required: [meta, data]
      properties:
        meta:
          $ref: '#/components/schemas/ResponseMeta'
        data:
          type: array
          items:
            $ref: '#/components/schemas/OperationContract'
    AggregateQuery:
      type: object
      additionalProperties: false
      required: [operation]
      properties:
        operation:
          type: string
          minLength: 1
          description: Case-sensitive Excel operation name, for example FinancialStatement.
        parameters:
          type: object
          default: {}
          description: Exact Excel input names and values; undeclared fields are rejected.
          additionalProperties: true
    EnumResolution:
      type: object
      additionalProperties: false
      required: [field, group_name, input, code, caption, provider_data]
      properties:
        field:
          type: string
        group_name:
          type: string
        input: {}
        code:
          type: string
        caption:
          type: [string, 'null']
        provider_data:
          $ref: '#/components/schemas/ProviderData'
    AggregateData:
      type: object
      additionalProperties: false
      required: [api_id, capability, operation, resolved_parameters, enum_resolutions, provider_data]
      properties:
        api_id:
          type: integer
          minimum: 1
          maximum: 371
        capability:
          $ref: '#/components/schemas/Capability'
        operation:
          type: string
        resolved_parameters:
          type: object
          additionalProperties: true
          description: Exact request sent upstream after fixed-value insertion and enum resolution.
        enum_resolutions:
          type: array
          items:
            $ref: '#/components/schemas/EnumResolution'
        provider_data:
          $ref: '#/components/schemas/ProviderData'
    AggregateResponse:
      type: object
      additionalProperties: false
      required: [meta, data]
      properties:
        meta:
          $ref: '#/components/schemas/ResponseMeta'
        data:
          $ref: '#/components/schemas/AggregateData'
    PriceLevel:
      type: object
      additionalProperties: false
      required: [price, size]
      properties:
        price:
          $ref: '#/components/schemas/Decimal'
        size:
          $ref: '#/components/schemas/Decimal'
    Quote:
      type: object
      additionalProperties: false
      required: [instrument_id, event_time, last_price, previous_close, open, high, low, change, change_percent, volume, turnover, trade_status, bid_levels, ask_levels, provider_data]
      properties:
        instrument_id:
          $ref: '#/components/schemas/InstrumentId'
        event_time:
          type: string
          format: date-time
        last_price:
          $ref: '#/components/schemas/Decimal'
        previous_close:
          $ref: '#/components/schemas/Decimal'
        open:
          $ref: '#/components/schemas/Decimal'
        high:
          $ref: '#/components/schemas/Decimal'
        low:
          $ref: '#/components/schemas/Decimal'
        change:
          oneOf:
            - $ref: '#/components/schemas/Decimal'
            - type: 'null'
        change_percent:
          oneOf:
            - $ref: '#/components/schemas/Decimal'
            - type: 'null'
        volume:
          $ref: '#/components/schemas/Decimal'
        turnover:
          $ref: '#/components/schemas/Decimal'
        trade_status:
          type: string
        bid_levels:
          type: array
          items:
            $ref: '#/components/schemas/PriceLevel'
        ask_levels:
          type: array
          items:
            $ref: '#/components/schemas/PriceLevel'
        provider_data:
          $ref: '#/components/schemas/ProviderData'
    Bar:
      type: object
      additionalProperties: false
      required: [instrument_id, event_time, trading_date, timeframe, open, high, low, close, previous_close, volume, turnover, trade_count, suspended, adjustment, aggregation_source, provider_data]
      properties:
        instrument_id:
          $ref: '#/components/schemas/InstrumentId'
        event_time:
          type: string
          format: date-time
          description: Start of the interval in UTC.
        trading_date:
          type: [string, 'null']
          format: date
          description: Upstream trading or period-end date; null for derived intraday bars.
        timeframe:
          $ref: '#/components/schemas/BarTimeframe'
        open:
          $ref: '#/components/schemas/Decimal'
        high:
          $ref: '#/components/schemas/Decimal'
        low:
          $ref: '#/components/schemas/Decimal'
        close:
          $ref: '#/components/schemas/Decimal'
        previous_close:
          oneOf:
            - $ref: '#/components/schemas/Decimal'
            - type: 'null'
        volume:
          $ref: '#/components/schemas/Decimal'
        turnover:
          oneOf:
            - $ref: '#/components/schemas/Decimal'
            - type: 'null'
        trade_count:
          type: [integer, 'null']
          minimum: 0
        suspended:
          type: boolean
        adjustment:
          type: string
          enum: [raw, forward, backward]
          description: ETF and index bars accept `raw` only.
        aggregation_source:
          type: string
          enum: [provider, snapshot_derived, daily_derived]
        provider_data:
          $ref: '#/components/schemas/ProviderData'
    StreamControl:
      type: object
      additionalProperties: false
      required: [control_type, request_id, event_time]
      properties:
        control_type:
          type: string
          enum: [stream_end, heartbeat, reconnect]
        request_id:
          type: string
          format: uuid
        event_time:
          type: string
          format: date-time
        records_sent:
          type: integer
          minimum: 0
    MarketEvent:
      oneOf:
        - $ref: '#/components/schemas/Quote'
        - $ref: '#/components/schemas/StreamControl'
    BarStreamRecord:
      oneOf:
        - $ref: '#/components/schemas/Bar'
        - $ref: '#/components/schemas/StreamControl'
    AgentContext:
      type: object
      additionalProperties: false
      required: [instrument_id, instrument, latest_quote, bars, upstream_operations]
      properties:
        instrument_id:
          $ref: '#/components/schemas/InstrumentId'
        instrument:
          $ref: '#/components/schemas/Instrument'
        latest_quote:
          oneOf:
            - $ref: '#/components/schemas/Quote'
            - type: 'null'
        bars:
          type: array
          items:
            $ref: '#/components/schemas/Bar'
        upstream_operations:
          type: array
          minItems: 1
          maxItems: 3
          items:
            type: string
    AgentContextResponse:
      type: object
      additionalProperties: false
      required: [meta, data]
      properties:
        meta:
          $ref: '#/components/schemas/ResponseMeta'
        data:
          $ref: '#/components/schemas/AgentContext'
    InstrumentResponse:
      $ref: '#/components/schemas/InstrumentResponseObject'
    InstrumentResponseObject:
      type: object
      additionalProperties: false
      required: [meta, data]
      properties:
        meta:
          $ref: '#/components/schemas/ResponseMeta'
        data:
          $ref: '#/components/schemas/Instrument'
    BarsResponse:
      type: object
      additionalProperties: false
      required: [meta, data]
      properties:
        meta:
          $ref: '#/components/schemas/ResponseMeta'
        data:
          type: array
          items:
            $ref: '#/components/schemas/Bar'
    QuotesResponse:
      type: object
      additionalProperties: false
      required: [meta, data]
      properties:
        meta:
          $ref: '#/components/schemas/ResponseMeta'
        data:
          type: array
          items:
            $ref: '#/components/schemas/Quote'
    TenantResponse:
      type: object
      additionalProperties: false
      required: [meta, data]
      properties:
        meta:
          $ref: '#/components/schemas/ResponseMeta'
        data:
          $ref: '#/components/schemas/Tenant'
    TenantsResponse:
      type: object
      additionalProperties: false
      required: [meta, data]
      properties:
        meta:
          $ref: '#/components/schemas/ResponseMeta'
        data:
          type: array
          items:
            $ref: '#/components/schemas/Tenant'
    ApiKeysResponse:
      type: object
      additionalProperties: false
      required: [meta, data]
      properties:
        meta:
          $ref: '#/components/schemas/ResponseMeta'
        data:
          type: array
          items:
            $ref: '#/components/schemas/ApiKey'
    IssuedApiKeyResponse:
      type: object
      additionalProperties: false
      required: [meta, data]
      properties:
        meta:
          $ref: '#/components/schemas/ResponseMeta'
        data:
          $ref: '#/components/schemas/IssuedApiKey'
    InvalidParameter:
      type: object
      additionalProperties: false
      required: [name, reason]
      properties:
        name:
          type: string
        reason:
          type: string
    Problem:
      type: object
      additionalProperties: false
      required: [type, title, status, detail, request_id, code, invalid_parameters]
      properties:
        type:
          type: string
          format: uri-reference
        title:
          type: string
        status:
          type: integer
          minimum: 400
          maximum: 599
        detail:
          type: string
        request_id:
          type: string
          format: uuid
        code:
          type: string
        invalid_parameters:
          type: array
          items:
            $ref: '#/components/schemas/InvalidParameter'
