openapi: 3.0.3
info:
  title: Treasury Risk Analyzer API
  description: >-
    Deterministic financial intelligence for autonomous AI Treasury/CFO
    agents, paid per call in USDC on Base via x402 — no account, no API key.
    Two payment rails are accepted on every paid route: Circle Gateway
    batched payments and standard x402 `exact` (EIP-3009) payments, so any
    x402 v2 client can pay. Exposes three atomic capabilities — treasury/portfolio risk
    analysis, FX exposure analysis, and liquidity forecasting — plus a
    composite POST /treasury-decision endpoint that orchestrates all three
    to answer "can we safely execute this payment today?" All share one
    deterministic engine and one agent-native response format. No LLM is
    used to produce or estimate a financial value. Requests are validated
    before payment: invalid input returns a free 400 and is never charged.
  version: "1.10.0"
  contact:
    name: Kyootek
    email: cm@kyootek.io
    url: https://agents.kyootek.io
  x-guidance: >-
    Start with GET /demo (free) to see a full real output. For "can we pay
    this invoice today?" call POST /treasury-decision; for a cash runway
    check call POST /liquidity. Invalid input returns a free 400; paid routes
    answer 402 with x402 v2 requirements (USDC on Base). Plain-language docs:
    https://agents.kyootek.io/docs
  license:
    name: ISC
    url: https://opensource.org/license/isc-license-txt
servers:
  - url: https://portfolio-risk-analyzer-g9ao.onrender.com
    description: Public production deployment (Base mainnet)

paths:
  /:
    get:
      summary: Machine-readable service description
      description: Free endpoint for agent discovery — name, mission, endpoints, and pricing.
      operationId: getServiceDescription
      security: []
      responses:
        "200":
          description: Service description.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServiceDescription"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /health:
    get:
      summary: Liveness check
      description: Free endpoint. Always returns 200 while the service is up.
      operationId: getHealth
      security: []
      responses:
        "200":
          description: Service is healthy.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /demo:
    get:
      summary: Free demo on fixed sample data
      description: >-
        Runs one of the paid tools on the fixed example input documented in
        this spec and returns the exact, full output the paid route returns,
        plus the paid route to call next. Does not accept custom input.
        Default tool is the treasury decision for a EUR 750,000 supplier
        invoice. Limited per IP (default 20 calls per hour; see the
        RateLimit-* response headers).
      operationId: getDemo
      security: []
      parameters:
        - name: tool
          in: query
          required: false
          schema:
            type: string
            enum: [treasury-decision, treasury-risk, fx-exposure, liquidity]
            default: treasury-decision
      responses:
        "200":
          description: Demo result on fixed sample data.
          content:
            application/json:
              schema:
                type: object
                required: [status, demo, tool, input, result, next_step]
                properties:
                  status:
                    type: string
                    example: success
                  demo:
                    type: boolean
                    example: true
                  notice:
                    type: string
                  tool:
                    type: string
                  scenario:
                    type: string
                  input:
                    type: object
                    description: The fixed input that was analysed (same shape as the paid route's request body).
                  result:
                    type: object
                    description: The paid route's full response for that input.
                  next_step:
                    type: object
                    properties:
                      method:
                        type: string
                      url:
                        type: string
                      price:
                        type: string
                  other_demos:
                    type: array
                    items:
                      type: string
        "400":
          description: Unknown `tool` value.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"

  /treasury-risk:
    post:
      summary: Deterministic treasury/portfolio risk analysis
      description: >-
        Computes volatility, Sharpe/Sortino/Calmar, VaR/Expected Shortfall,
        multi-dimensional concentration, per-asset risk contribution, and
        correlated multi-factor stress scenarios (shocks propagate to
        unshocked symbols via covariance-based beta mapping, not treated as
        zero) from historical per-period returns and portfolio weights.
        Requires USDC payment via the x402 protocol (Circle
        Gateway) — see the 402 response below for the payment flow. Also
        available at the legacy path POST /analyze (identical behavior).
      operationId: postTreasuryRisk
      security: []
      x-payment-info:
        price:
          mode: fixed
          currency: USD
          amount: "0.10"
        protocols:
          - x402: {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AnalyzeRequest"
            examples:
              corporateTreasury:
                summary: Multi-currency corporate treasury — USD/EUR/GBP cash, USDC and a sukuk fund, 36 monthly returns in USD
                value:
                  baseCurrency: USD
                  returns:
                    USDCASH: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
                    EURCASH: [0.0322, 0.0186, -0.0166, -0.0109, 0.0172, -0.0254, 0.0003, -0.0122, -0.008, 0.0095, 0.0107, -0.0056, -0.0353, -0.0401, 0.001, -0.0176, -0.0535, -0.0263, 0.0014, 0.0294, -0.0106, 0.0028, 0.0199, 0.0112, 0.0097, 0.0084, -0.0262, -0.0016, 0.0029, 0.032, -0.0095, -0.0194, 0.008, 0.0141, 0.056, 0.0251]
                    GBPCASH: [0.0321, -0.0108, -0.0218, -0.0401, 0.0243, -0.0144, -0.0006, -0.012, -0.0218, 0.016, 0.0238, -0.0233, -0.0113, -0.0051, 0.0211, -0.0173, -0.0332, -0.0291, 0.0412, -0.0067, 0.0319, 0.0095, 0.0217, 0.0371, -0.0059, -0.0362, 0.0034, 0.0031, 0.0034, 0.0079, 0.0037, 0.0111, -0.0047, -0.014, -0.0048, 0.0047]
                    USDC: [0.001, 0.0001, -0.0002, 0.0002, 0.0001, 0.0002, -0.0001, 0, -0.0003, -0.0001, 0.0007, 0.0007, -0.0001, 0.0003, -0.0004, -0.0004, 0.0003, -0.0007, -0.0003, 0.0002, -0.0007, 0, -0.0002, -0.0003, 0.0001, -0.0001, 0.0005, -0.0008, 0.0005, -0.0003, 0.0002, -0.0005, -0.0004, -0.0004, -0.0003, -0.0001]
                    SUKUK: [-0.0052, 0.0038, 0.0132, 0.015, 0.0009, 0.015, 0.0024, -0.0044, -0.0009, -0.0006, 0.0014, 0.001, -0.0002, 0, 0.0129, 0.0017, 0.0096, 0.0013, 0.0167, 0.0007, 0.0033, 0.0057, -0.0009, 0.0004, -0.0088, -0.0094, 0.0067, 0.0081, 0.0105, -0.0008, 0.0093, -0.0054, -0.0037, 0.0064, -0.0019, 0.0055]
                  weights: { USDCASH: 0.45, EURCASH: 0.25, GBPCASH: 0.1, USDC: 0.12, SUKUK: 0.08 }
                  riskFreeRate: 0
                  periodsPerYear: 12
                  confidenceLevels: [0.95, 0.99]
                  currencyExposure: { USD: 0.65, EUR: 0.25, GBP: 0.10 }
                  stressScenarios:
                    - name: "EUR -10% vs USD"
                      shocks: { EURCASH: -0.1 }
                    - name: "USD strength: EUR -8%, GBP -8%"
                      shocks: { EURCASH: -0.08, GBPCASH: -0.08 }
      responses:
        "200":
          description: Risk analysis report (agent-native envelope).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnalyzeReport"
        "400":
          description: Invalid request body (see `error.message` for the specific validation failure). Input is validated before payment is requested, so a 400 is never charged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: >-
            Payment Required (x402). No valid payment was presented. The
            `payment-required` response header carries a base64-encoded JSON
            object describing how to pay — see `PaymentRequirements` below.
            Use an x402-aware client (e.g. `circle services pay`, `@x402/fetch`
            or the `treasury-risk-mcp` MCP server) to pay and retry
            automatically; do not attempt to construct the payment by hand.
          headers:
            payment-required:
              description: Base64-encoded JSON PaymentRequirements object.
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                description: Empty body; payment details are in the `payment-required` header.
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /analyze:
    post:
      summary: Deterministic treasury/portfolio risk analysis (legacy alias)
      description: Identical to POST /treasury-risk, kept for backward compatibility with earlier integrations.
      operationId: postAnalyzeLegacyAlias
      security: []
      x-payment-info:
        price:
          mode: fixed
          currency: USD
          amount: "0.10"
        protocols:
          - x402: {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AnalyzeRequest"
      responses:
        "200":
          description: Risk analysis report (agent-native envelope).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AnalyzeReport"
        "400":
          description: Invalid request body. Validated before payment is requested, so a 400 is never charged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "402":
          description: Payment Required (x402) — see POST /treasury-risk for the full payment flow documentation.

  /fx-exposure:
    post:
      summary: Deterministic FX exposure analysis
      description: >-
        Computes gross/net exposure, currency concentration, natural hedges,
        stress scenarios, and hedge priority from a list of receivable/
        payable/cash positions by currency. Requires USDC payment via the
        x402 protocol (Circle Gateway) — see the 402 response for the
        payment flow.
      operationId: postFxExposure
      security: []
      x-payment-info:
        price:
          mode: fixed
          currency: USD
          amount: "0.25"
        protocols:
          - x402: {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/FxAnalyzeRequest"
            examples:
              corporateTreasury:
                summary: EUR receivable, GBP payable, both unhedged
                value:
                  baseCurrency: USD
                  positions:
                    - { currency: EUR, type: receivable, amount: 500000, hedged: false }
                    - { currency: GBP, type: payable, amount: 200000, hedged: false }
                  fxRates: { EUR: 1.08, GBP: 1.27 }
                  stressShockPct: 0.10
      responses:
        "200":
          description: FX exposure report (agent-native envelope).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FxAnalyzeReport"
        "400":
          description: Invalid request body. Validated before payment is requested, so a 400 is never charged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: >-
            Payment Required (x402). Payment requirements are carried in the
            base64-encoded `payment-required` response header, not the body.
          headers:
            payment-required:
              description: Base64-encoded JSON PaymentRequirements object.
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                description: Empty body; payment details are in the `payment-required` header.
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /liquidity:
    post:
      summary: Deterministic liquidity forecast
      description: >-
        Projects daily cash balance from opening cash and a list of scheduled
        cash flow events over a forecast horizon, flags minimum-buffer
        breaches, and classifies liquidity status. Optionally also computes
        standard working-capital ratios (DSO/DPO/DIO/cash conversion cycle)
        from a supplied balance snapshot. Requires USDC payment via
        the x402 protocol (Circle Gateway) — see the 402 response for the
        payment flow.
      operationId: postLiquidity
      security: []
      x-payment-info:
        price:
          mode: fixed
          currency: USD
          amount: "0.50"
        protocols:
          - x402: {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LiquidityRequest"
            examples:
              bufferBreach:
                summary: Opening cash 800k, one 500k outflow, 500k required buffer
                value:
                  openingCash: 800000
                  startDate: "2026-09-14"
                  horizonDays: 30
                  minimumCashBuffer: 500000
                  cashFlows:
                    - { date: "2026-09-27", amount: -500000, category: payable, description: "Supplier invoice" }
      responses:
        "200":
          description: Liquidity forecast report (agent-native envelope).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LiquidityReport"
        "400":
          description: Invalid request body. Validated before payment is requested, so a 400 is never charged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: >-
            Payment Required (x402). Payment requirements are carried in the
            base64-encoded `payment-required` response header, not the body.
          headers:
            payment-required:
              description: Base64-encoded JSON PaymentRequirements object.
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                description: Empty body; payment details are in the `payment-required` header.
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

  /treasury-decision:
    post:
      summary: Composite payment decision ("can we safely execute this payment today?")
      description: >-
        Orchestrates the liquidity, FX exposure, and (optionally) treasury
        risk capabilities to produce a PAY/REVIEW/DO_NOT_PAY decision with a
        confidence score. The payment is injected as an outflow into the
        liquidity forecast and as a new unhedged payable position in the FX
        exposure analysis. Requires USDC payment via the x402 protocol
        (Circle Gateway) — see the 402 response for the payment flow. This
        is decision-support output, not an instruction to execute any
        transaction.
      operationId: postTreasuryDecision
      security: []
      x-payment-info:
        price:
          mode: fixed
          currency: USD
          amount: "2.00"
        protocols:
          - x402: {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TreasuryDecisionRequest"
            examples:
              centralScenario:
                summary: "Can we pay a EUR 750,000 invoice today?"
                value:
                  baseCurrency: USD
                  payment: { amount: 750000, currency: EUR, date: "2026-09-14" }
                  liquidity: { openingCash: 5000000, startDate: "2026-09-14", horizonDays: 30, minimumCashBuffer: 1000000 }
                  fxExposure: { fxRates: { EUR: 1.08 } }
      responses:
        "200":
          description: Treasury decision report (agent-native envelope).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TreasuryDecisionReport"
        "400":
          description: Invalid request body (may be prefixed with `liquidity:`, `fxExposure:`, or `treasuryRisk:` if the error originates in that sub-section). Validated before payment is requested, so a 400 is never charged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          description: >-
            Payment Required (x402). Payment requirements are carried in the
            base64-encoded `payment-required` response header, not the body.
          headers:
            payment-required:
              description: Base64-encoded JSON PaymentRequirements object.
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                description: Empty body; payment details are in the `payment-required` header.
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
        "500":
          $ref: "#/components/responses/InternalError"
        "503":
          $ref: "#/components/responses/ServiceUnavailable"

components:
  securitySchemes:
    x402:
      type: apiKey
      in: header
      name: PAYMENT-SIGNATURE
      description: >-
        Pay-per-call in USDC on Base mainnet (eip155:8453) via x402 v2 — no
        account or API key. Call the endpoint without this header to receive
        a 402 whose base64 `payment-required` header lists two accepted
        options for the same price and payee: (1) Circle Gateway batched
        (`extra.name: GatewayWalletBatched`, e.g. `circle services pay` or the
        `@circle-fin/x402-batching` client) and (2) standard x402 `exact`
        with EIP-3009 USDC (`extra.name: USD Coin`, e.g. `@x402/fetch`,
        `@x402/axios` or the Coinbase x402 SDK). Sign one option and retry
        with this header set. Invalid request bodies are rejected with a free
        400 before any payment is requested.
  responses:
    Unauthorized:
      description: >-
        Reserved for future use. This service currently has no
        authentication layer beyond per-call x402 payment on the paid routes.
    Forbidden:
      description: Reserved for future use (e.g. access restrictions on a given resource).
    NotFound:
      description: The requested path does not exist.
    RateLimited:
      description: >-
        Too many requests. Enforced by the application on the free GET /demo
        route (per IP, fixed one-hour window; see the Retry-After header).
        On other routes a 429 may originate from the hosting platform.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    InternalError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    ServiceUnavailable:
      description: >-
        Service temporarily unavailable (e.g. cold start on the free hosting
        tier, or platform-level maintenance).

  schemas:
    ServiceDescription:
      type: object
      properties:
        name:
          type: string
          example: Treasury Risk Analyzer
        mission:
          type: string
        category:
          type: string
        endpoints:
          type: object
          additionalProperties:
            type: object
            properties:
              price:
                type: string
              description:
                type: string
        ai_involvement:
          type: string

    ErrorEnvelope:
      type: object
      properties:
        status:
          type: string
          enum: [error]
        error:
          type: object
          properties:
            code:
              type: string
              example: INVALID_INPUT
            message:
              type: string

    PaymentRequirements:
      type: object
      description: Decoded shape of the base64 `payment-required` response header (x402 v2).
      properties:
        x402Version:
          type: integer
          example: 2
        resource:
          type: object
          properties:
            url: { type: string }
            description: { type: string }
            mimeType: { type: string }
        accepts:
          type: array
          items:
            type: object
            properties:
              scheme: { type: string, example: exact }
              network: { type: string, example: "eip155:8453", description: "CAIP-2 network id (eip155:8453 = Base mainnet)" }
              asset: { type: string, description: USDC contract address }
              amount: { type: string, description: Amount in the asset's smallest unit (e.g. 100000 = $0.10 USDC at 6 decimals) }
              payTo: { type: string, description: Seller wallet address }
              maxTimeoutSeconds: { type: integer }

    AnalyzeRequest:
      type: object
      required: [returns, weights]
      properties:
        returns:
          type: object
          description: Per-period historical return series by symbol. All series must be the same length (≥2 observations).
          additionalProperties:
            type: array
            items: { type: number }
          example: { USDCASH: [0, 0, 0], EURCASH: [0.0121, -0.0187, 0.0065], GBPCASH: [0.0094, -0.0203, 0.0071] }
        weights:
          type: object
          description: Portfolio weights by symbol. Must cover exactly the same symbols as `returns` and sum to 1.
          additionalProperties: { type: number }
          example: { USDCASH: 0.6, EURCASH: 0.3, GBPCASH: 0.1 }
        riskFreeRate:
          type: number
          description: Annualized risk-free rate, e.g. 0.04 for 4%.
          default: 0
        periodsPerYear:
          type: number
          description: Periods per year in the return series (252 for daily equities, 365 for daily crypto).
          default: 252
        confidenceLevels:
          type: array
          items: { type: number }
          description: "Confidence levels for VaR/Expected Shortfall. Supported: 0.9, 0.95, 0.975, 0.99, 0.995."
          default: [0.95, 0.99]
        baseCurrency:
          type: string
          default: USD
        minimumAcceptableReturn:
          type: number
          description: Per-period MAR used for downside deviation / Sortino ratio.
          default: 0
        benchmarkReturns:
          type: array
          items: { type: number }
          description: Optional per-period benchmark series (same length as each `returns` series) to compute beta and correlation.
        currencyExposure:
          type: object
          description: Optional weight distribution by currency, for concentration analysis.
          additionalProperties: { type: number }
        geographyExposure:
          type: object
          description: Optional weight distribution by geography, for concentration analysis.
          additionalProperties: { type: number }
        sectorExposure:
          type: object
          description: Optional weight distribution by sector, for concentration analysis.
          additionalProperties: { type: number }
        stressScenarios:
          type: array
          description: >-
            Optional stress scenarios. Shocks on the listed symbols propagate to
            unshocked symbols via covariance-based beta mapping (correlated
            impact), alongside the direct-only impact.
          items:
            type: object
            properties:
              name: { type: string }
              shocks:
                type: object
                description: Fractional shock per symbol, e.g. -0.3 for a 30% drop.
                additionalProperties: { type: number }

    AnalyzeReport:
      type: object
      properties:
        status:
          type: string
          enum: [success]
        analysis_id:
          type: string
          format: uuid
        timestamp:
          type: string
          format: date-time
        portfolio:
          type: object
          properties:
            base_currency: { type: string }
            assets: { type: integer }
        risk_metrics:
          type: object
          properties:
            annualized_return: { type: number }
            annualized_volatility: { type: number }
            annualized_downside_deviation: { type: number }
            sharpe_ratio: { type: number, nullable: true }
            sortino_ratio: { type: number, nullable: true }
            calmar_ratio: { type: number, nullable: true }
            diversification_ratio: { type: number, nullable: true }
            beta: { type: number, nullable: true }
            correlation_to_benchmark: { type: number, nullable: true }
            max_drawdown: { type: number }
            value_at_risk:
              type: object
              additionalProperties:
                type: object
                properties:
                  parametric: { type: number, nullable: true }
                  historical: { type: number }
            expected_shortfall:
              type: object
              additionalProperties:
                type: object
                properties:
                  parametric: { type: number, nullable: true }
                  historical: { type: number }
            hhi: { type: number }
        concentration:
          type: object
          additionalProperties:
            type: object
            properties:
              hhi: { type: number }
              effectiveCount: { type: number, nullable: true }
        risk_contribution:
          type: object
          additionalProperties: { type: number }
        stress_test:
          type: array
          items:
            type: object
            properties:
              name: { type: string }
              portfolioImpact: { type: number, description: "Total portfolio impact including both direct shocks and correlated implied shocks." }
              directOnlyImpact: { type: number, description: "Naive portfolio impact from directShocks alone, ignoring correlation to unshocked symbols — kept for comparison." }
              directShocks:
                type: object
                description: The user-specified shocks that matched a portfolio symbol.
                additionalProperties: { type: number }
              impliedShocks:
                type: object
                description: >-
                  Shocks inferred for portfolio symbols not directly named in the scenario, via
                  factor-beta mapping on the historical covariance matrix of the directly-shocked
                  symbols. Empty if every portfolio symbol was directly shocked, or if none of the
                  scenario's shocks matched a portfolio symbol. A value is exactly 0 when the
                  relationship was not propagated — see `propagation` for the reason.
                additionalProperties: { type: number }
              propagation:
                type: object
                description: >-
                  Per unshocked symbol, why its implied shock is what it is. `propagated` only when
                  the regression on the usable drivers is significant (F-test p < 0.05);
                  `not_significant` (implied shock 0), `zero_variance` (the symbol never moved
                  historically), or `no_usable_driver` (every shocked symbol was excluded, see
                  excludedDrivers).
                additionalProperties:
                  type: object
                  properties:
                    status: { type: string, enum: [propagated, not_significant, zero_variance, no_usable_driver] }
                    rSquared: { type: number, nullable: true }
                    pValue: { type: number, nullable: true }
              excludedDrivers:
                type: array
                description: >-
                  Shocked symbols NOT used to infer other symbols' moves: `zero_variance` (no
                  historical movement to learn from) or `extrapolation` (the shock exceeds 50 of the
                  symbol's own per-period standard deviations — a regime change such as a stablecoin
                  depeg, outside the range the correlation was estimated on). Their direct shock
                  still counts in portfolioImpact.
                items:
                  type: object
                  properties:
                    symbol: { type: string }
                    reason: { type: string, enum: [zero_variance, extrapolation] }
                    sigmaMultiple: { type: number, nullable: true }
              unmatchedSymbols:
                type: array
                description: Scenario shock keys that did not match any portfolio symbol (e.g. a macro factor name) — excluded from both direct and implied impact.
                items: { type: string }
        risk_classification:
          type: object
          properties:
            level: { type: string, enum: [LOW, MEDIUM, HIGH] }
            score: { type: integer, minimum: 0, maximum: 100 }
        warnings:
          type: array
          items: { type: string }
        data_quality:
          type: object
          properties:
            sample_size: { type: integer }
            minimum_recommended_sample_size: { type: integer }
            sufficient_sample_size: { type: boolean }
            benchmark_provided: { type: boolean }
        methodology:
          type: object
          additionalProperties: { type: string }
        limitations:
          type: array
          items: { type: string }

    FxPosition:
      type: object
      required: [currency, type, amount]
      properties:
        currency: { type: string, example: EUR }
        type: { type: string, enum: [receivable, payable, cash] }
        amount: { type: number, minimum: 0 }
        hedged: { type: boolean, default: false }

    FxAnalyzeRequest:
      type: object
      required: [positions, fxRates]
      properties:
        baseCurrency:
          type: string
          default: USD
        positions:
          type: array
          items:
            $ref: "#/components/schemas/FxPosition"
        fxRates:
          type: object
          description: Units of baseCurrency per 1 unit of each foreign currency present in `positions`.
          additionalProperties: { type: number }
          example: { EUR: 1.08, GBP: 1.27 }
        stressShockPct:
          type: number
          description: Fractional depreciation applied to each foreign currency in the stress scenario.
          default: 0.10
        totalTreasuryValueBase:
          type: number
          description: Optional total treasury value (in baseCurrency) used to normalize the risk score. Defaults to total gross FX exposure if omitted.

    FxAnalyzeReport:
      type: object
      properties:
        status: { type: string, enum: [success] }
        analysis_id: { type: string, format: uuid }
        timestamp: { type: string, format: date-time }
        base_currency: { type: string }
        fx_metrics:
          type: object
          properties:
            gross_exposure: { type: object, additionalProperties: { type: number } }
            net_exposure: { type: object, additionalProperties: { type: number } }
            hedged_net_exposure: { type: object, additionalProperties: { type: number } }
            unhedged_net_exposure: { type: object, additionalProperties: { type: number } }
            unhedged_ratio: { type: number }
            currency_concentration:
              type: object
              properties:
                hhi: { type: number }
                effectiveCount: { type: number, nullable: true }
        natural_hedges:
          type: array
          items:
            type: object
            properties:
              currency: { type: string }
              offsetAmount: { type: number }
        stress_test:
          type: array
          items:
            type: object
            properties:
              scenario: { type: string }
              currency: { type: string }
              impact: { type: number, description: "Economic P&L in base currency of the shock on the UNHEDGED net exposure (hedged positions assumed fully offset)." }
              impactBeforeHedges: { type: number, description: "Same shock applied to the whole net exposure, ignoring hedges — for comparison." }
        hedge_priority:
          type: array
          items: { type: string }
        risk_classification:
          type: object
          properties:
            level: { type: string, enum: [LOW, MEDIUM, HIGH] }
            score: { type: integer, minimum: 0, maximum: 100 }
        warnings:
          type: array
          items: { type: string }
        data_quality: { type: object }
        methodology: { type: object, additionalProperties: { type: string } }
        limitations:
          type: array
          items: { type: string }

    CashFlow:
      type: object
      required: [date, amount]
      properties:
        date: { type: string, format: date, example: "2026-09-27" }
        amount:
          type: number
          description: Signed amount; positive is an inflow, negative is an outflow.
        category: { type: string, maxLength: 50, example: payable }
        description: { type: string, maxLength: 200 }

    LiquidityRequest:
      type: object
      required: [openingCash, horizonDays, minimumCashBuffer]
      properties:
        baseCurrency:
          type: string
          default: USD
        openingCash: { type: number }
        startDate:
          type: string
          format: date
          description: Defaults to today (UTC) if omitted.
        horizonDays: { type: integer, minimum: 1, maximum: 3650 }
        minimumCashBuffer: { type: number, minimum: 0 }
        cashFlows:
          type: array
          items:
            $ref: "#/components/schemas/CashFlow"
          description: Each date must be on or after startDate; dates beyond the horizon are excluded from the projection (and flagged in warnings/data_quality) rather than rejected.
        workingCapital:
          $ref: "#/components/schemas/WorkingCapitalInput"

    WorkingCapitalInput:
      type: object
      description: >-
        Optional standard accounting ratios (DSO/DPO/DIO/cash conversion cycle)
        computed from a balance snapshot and period flow — independent of the
        cashFlows-based daily projection above.
      required: [accountsReceivable, accountsPayable, periodCreditSales, periodCOGS, periodDays]
      properties:
        accountsReceivable: { type: number, minimum: 0, description: "AR balance snapshot, in baseCurrency." }
        accountsPayable: { type: number, minimum: 0, description: "AP balance snapshot, in baseCurrency." }
        periodCreditSales: { type: number, minimum: 0, description: "Credit sales over the reference period, in baseCurrency." }
        periodCOGS: { type: number, minimum: 0, description: "Cost of goods sold / purchases over the reference period, in baseCurrency." }
        periodDays: { type: number, minimum: 0, exclusiveMinimum: true, maximum: 3650, description: "Length of the reference period in days (e.g. 365 for a trailing year, 90 for a quarter).", example: 365 }
        inventory: { type: number, minimum: 0, description: "Optional inventory balance snapshot — enables days_inventory_outstanding and a full cash conversion cycle." }

    LiquidityReport:
      type: object
      properties:
        status: { type: string, enum: [success] }
        analysis_id: { type: string, format: uuid }
        timestamp: { type: string, format: date-time }
        base_currency: { type: string }
        forecast:
          type: object
          properties:
            start_date: { type: string, format: date }
            horizon_days: { type: integer }
            opening_cash: { type: number }
            minimum_cash_position: { type: number }
            minimum_date: { type: string, format: date }
            projected_cash_peak: { type: number }
            peak_date: { type: string, format: date }
            required_buffer: { type: number }
            shortfall: { type: number }
            buffer_breach: { type: boolean }
        liquidity_status: { type: string, enum: [HEALTHY, WARNING, CRITICAL] }
        recommended_action: { type: string, nullable: true }
        projection:
          type: array
          items:
            type: object
            properties:
              date: { type: string, format: date }
              cashBalance: { type: number }
        working_capital:
          type: object
          description: Present only if workingCapital was supplied in the request.
          properties:
            days_sales_outstanding: { type: number, nullable: true }
            days_payable_outstanding: { type: number, nullable: true }
            days_inventory_outstanding: { type: number, nullable: true }
            cash_conversion_cycle: { type: number, nullable: true }
            includes_inventory: { type: boolean }
        risk_classification:
          type: object
          properties:
            level: { type: string, enum: [LOW, MEDIUM, HIGH] }
            score: { type: integer, minimum: 0, maximum: 100 }
        warnings:
          type: array
          items: { type: string }
        data_quality: { type: object }
        methodology: { type: object, additionalProperties: { type: string } }
        limitations:
          type: array
          items: { type: string }

    PaymentInput:
      type: object
      required: [amount, currency]
      properties:
        amount: { type: number, minimum: 0, exclusiveMinimum: true }
        currency: { type: string, example: EUR }
        date: { type: string, format: date, description: "Defaults to liquidity.startDate (or today) if omitted." }
        description: { type: string, maxLength: 200 }

    TreasuryDecisionRequest:
      type: object
      required: [payment, liquidity, fxExposure]
      properties:
        baseCurrency:
          type: string
          default: USD
        payment:
          $ref: "#/components/schemas/PaymentInput"
        liquidity:
          description: Same shape as LiquidityRequest, minus baseCurrency (the payment is auto-injected as an additional cash flow outflow).
          type: object
          required: [openingCash, horizonDays, minimumCashBuffer]
          properties:
            openingCash: { type: number }
            startDate: { type: string, format: date }
            horizonDays: { type: integer, minimum: 1, maximum: 3650 }
            minimumCashBuffer: { type: number, minimum: 0 }
            cashFlows:
              type: array
              items:
                $ref: "#/components/schemas/CashFlow"
            workingCapital:
              $ref: "#/components/schemas/WorkingCapitalInput"
        fxExposure:
          description: Same shape as the /fx-exposure request body minus baseCurrency; positions is optional (the payment is auto-injected as a new unhedged payable position). fxRates must include the payment currency's rate if it differs from baseCurrency.
          type: object
          properties:
            positions:
              type: array
              items:
                $ref: "#/components/schemas/FxPosition"
            fxRates:
              type: object
              additionalProperties: { type: number }
            stressShockPct: { type: number, default: 0.10 }
            totalTreasuryValueBase: { type: number }
        treasuryRisk:
          description: Optional. Same shape as AnalyzeRequest (treasury/portfolio risk) — if omitted, that dimension is not evaluated and is excluded from the decision matrix.
          $ref: "#/components/schemas/AnalyzeRequest"

    TreasuryDecisionReport:
      type: object
      properties:
        status: { type: string, enum: [success] }
        analysis_id: { type: string, format: uuid }
        timestamp: { type: string, format: date-time }
        base_currency: { type: string }
        payment:
          type: object
          properties:
            amount: { type: number }
            currency: { type: string }
            amount_base_currency: { type: number }
            date: { type: string, format: date }
        decision: { type: string, enum: [PAY, REVIEW, DO_NOT_PAY] }
        confidence: { type: number, minimum: 0, maximum: 1, description: "Deterministic data-completeness score, not a probability of a good outcome." }
        components:
          type: object
          properties:
            liquidity:
              type: object
              properties:
                status: { type: string, enum: [HEALTHY, WARNING, CRITICAL] }
                minimum_cash_position: { type: number }
                minimum_date: { type: string, format: date }
                required_buffer: { type: number }
                shortfall: { type: number }
            fx_exposure:
              type: object
              properties:
                risk_level: { type: string, enum: [LOW, MEDIUM, HIGH] }
                score: { type: integer }
                context: { type: string, enum: [full_book, payment_only], description: "full_book if fxExposure.positions was supplied; payment_only if the payment itself was the only position, in which case risk_level reflects its own stress impact rather than book-level concentration." }
                net_exposure_payment_currency: { type: number }
                currency_concentration_hhi: { type: number, nullable: true }
            treasury_risk:
              type: object
              properties:
                evaluated: { type: boolean }
                risk_level: { type: string, enum: [LOW, MEDIUM, HIGH] }
                score: { type: integer }
        warnings:
          type: array
          items: { type: string }
        data_quality: { type: object }
        methodology: { type: object, additionalProperties: { type: string } }
        limitations:
          type: array
          items: { type: string }
