# =============================================================================
# PMJ 公开 API —— OpenAPI 3.1 真相源（api-docs-prd §7 L1）
#
# 归属：**本仓（Fathom）的真相源**。pmj-stack/ 下的 vendored 仓按 fork 处理，
#   主线在本仓，只从上游选择性取用（见 docs/PMJ_MERGE_PLAN.md §6）。契约要改就改
#   这一份；pmj-stack/pmj_web 的 openapi/public-v1.yaml 快照与
#   src/features/docs/api-docs.data.ts 都由 pmj_web 的 scripts/sync-api-docs.mjs
#   从本文件生成，**勿手改那两个产物**。
#
# 与上游的关系：内容 = upstream pmj-market/pmj master c84d8705
#   + 未合并分支 docs/openapi-external-copy 的 109 处 schema/参数字段描述
#   + 本仓修正（见下「纪律 4」的六处）。上游把 400/401/404/503 的深语义压成一行、
#   挪去文档站 Errors 页，本仓保留写在 yaml 上的长版本——这里的 yaml 是被人直接读、
#   被 OpenApiContractTest 直接守的文件，不能依赖另一个仓的页面兜底。
#
# 范围 = PRD §5.1（下单闭环，17 op）+ §5.2（同优先级扩展，8 op）。
#   不收录：/internal/**、admin、X-Service-Key 入账、运营闸门、PX catalog(P1)。
#
# 纪律：
#   1. 路径一律**网关形态**（/api/v1/...，端口 8090）。Controller 本地 mapping 与
#      网关改写规则差一层，不要拿注解直接当公开路径(exchange-order 的 /orders/**
#      经 orders-write 路由剥掉 /api/v1 前缀；exchange-account 的余额读端点重写进
#      /internal/accounts/{owner}，公开侧只承诺 /api/v1/accounts/{owner})。
#   2. 本文件由 `OpenApiContractTest` 焊在网关路由上：写一条网关不存在的路径，
#      测试红。反向覆盖（现网每个公开端点都已收录）是 M3 的 springdoc-in-test。
#   3. 枚举与字段名与后端 JSON 逐字一致；数值定点整数（E4 价 / E2 量 / E6 金额）,
#      示例禁止浮点美元。语义深度（202 / cursor / minCursor / 404 双义）见
#      docs/API.md——本文件引用它，不复述走样。
#   4. **不要在 flow mapping `{ ... }` 里写含半角逗号的 description**：逗号会被 YAML
#      当成条目分隔符，description 被截断、后半截变成一个值为 null 的野生 key，而且
#      解析不报错——生成的文档站会静悄悄少半句话。上游两版共踩六处(master:tickE4、
#      SessionResponse.expiresAt、HeartbeatDeregisteredResponse.removed；分支另加
#      cursor、priceE4、payloadCorrupted)，本仓已全部改块式。同理，**长 description 不要
#      在 yaml 里折行**：plain scalar 的折行会被 YAML 折成一个半角空格，中文中间就多出
#      一个空格（「7d 绝对 上限双闸门」），生成的文档站照抄。
# =============================================================================

openapi: 3.1.0

info:
  title: PMJ Public API
  version: 0.1.0
  description: |
    PMJ 预测市场交易所的公开 REST API（网关形态）。

    **四条硬契约**（适用于所有写接口与带 `minCursor` 的单市场读接口）:

    1. **202 = 命令已持久 ≠ 已成交**。所有写接口返回 202 时唯一
       保证是命令已被命令日志确认持久化；撮合结果用响应体里的 `cursor` 配合
       状态查询端点确认。
    2. **cursor 是不透明字符串**：只承诺原样传回同一市场的查询端点当
       `minCursor`，不承诺结构、不承诺可比。
    3. **`minCursor` 只对单市场查询有意义**。跨分区/跨时间的读端点
       （`GET /markets`、`GET /positions`、`/trades`、`/candles`）传了直接 400。
    4. **404 有两重含义且逐字节不可区分**：投影未追上，或资源真的不存在
       ——拿到 202 之后立刻查很可能先 404，带同一个 cursor 轮询，不要把第一个
       404 当最终结果。

    **单位**：价格 `*E4`(1e-4 USD)、数量 `*E2`（1e-2 份）、金额 `*E6`
    (1e-6 USD)，一律定点整数；浮点换算只发生在展示层。

    **鉴权**：除标注 Anon 的端点外均需 `Authorization: Bearer {token}`;
    token 由 `POST /api/v1/auth/verify`（或 google/email 登录）签发。
    身份一律从会话推导——没有任何端点接受"路径/请求体里传 trader"。

servers:
  - url: http://localhost:8090
    description: 本地网关（浏览器侧另有 Next 同源代理 /api/exchange/v1 → 网关，
      不代理 WebSocket）
  - url: https://{deployment}:8090
    description: 部署环境（testnet / 生产按部署填）
    variables:
      deployment:
        default: testnet-example

tags:
  - name: Authentication
    description: Sui 钱包签名登录与免钱包登录（google/email 默认关闭）。
      会话双闸门：24h 空闲滑动窗口 + 7d 绝对上限。
  - name: Markets
    description: 市场发现与详情。列表为跨分区读，不支持 minCursor。
  - name: Market Data
    description: 盘口 / 报价 / 成交流水 / K 线。除单市场 quote/book 外均为跨时间读，不支持 minCursor。
  - name: Events
    description: 事件目录（PX 发现层，公开读）。marketId 可直接用于下单。
  - name: PX Account
    description: PX 账户线：余额、持仓、订单、成交、资金流水（Bearer，身份从会话推导）。
  - name: PX Margin
    description: 杠杆、借贷与强平。写操作同步执行，失败体是 {code, message}。
  - name: InfoAI
    description: AI 推荐与顾问。公开读，可永久降级（enabled=false / advice=null 都走 200）。
  - name: Orders
    description: 下单 / 撤单 / 心跳 / 赎回。写接口一律 202。
  - name: Account
    description: 余额快照。owner 必须等于会话地址。
  - name: Positions
    description: 持仓查询与逐笔结算状态。trader 从会话推导，路径不带 trader 段。
  - name: WebSocket
    description: 交易 WS 握手。

security:
  - bearerAuth: []

paths:
  # ===========================================================================
  # Authentication
  # ===========================================================================
  /api/v1/auth/nonce:
    post:
      tags: [Authentication]
      summary: 取登录待签原文
      description: |
        后端生成 nonce 与**完整待签原文**（`message`）。客户端把 `message` 原样
        交给钱包 `signPersonalMessage`——不要自己拼登录消息，校验是对原文做的。
      security: []
      operationId: authNonce
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NonceRequest'
      responses:
        '200':
          description: 待签原文已就绪
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NonceResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/auth/verify:
    post:
      tags: [Authentication]
      summary: 验签换会话
      description: |
        校验顺序：先比对原文、再验签、最后消费 nonce——顺序反了会被廉价
        登录阻断。成功返回 Bearer 会话；同一地址任何入口登录得到同一 trader。
        `signature`/`bytes` 是钱包 `signPersonalMessage` 返回值的直接透传（base64）。
      security: []
      operationId: authVerify
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyRequest'
      responses:
        '200':
          description: 会话已签发
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          description: 验签失败（`auth_failed`；与 token 验签失败合并，反枚举）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: auth_failed
                message: 验签失败
                recoverable: false

  /api/v1/auth/me:
    get:
      tags: [Authentication]
      summary: 当前会话信息
      description: |
        前端从本地恢复令牌后打一次，确认令牌有效、属于哪个地址、何时过期。
        `expiresAt` 按空闲窗口计算，不是 7d 绝对上限，别拿它当"到这一刻一定有效"。
      operationId: authMe
      responses:
        '200':
          description: 会话有效
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MeResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/auth/logout:
    post:
      tags: [Authentication]
      summary: 吊销当前会话
      description: 吊销已撤销的 token 是 no-op，天然幂等。
      operationId: authLogout
      responses:
        '204':
          description: 已吊销
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ===========================================================================
  # Markets
  # ===========================================================================
  /api/v1/markets:
    get:
      tags: [Markets]
      summary: 市场列表
      description: |
        跨分区查询，**不支持 `minCursor`**——传了直接 400，不是静默忽略。分页用 `cursor`(keyset)，响应 `nextCursor` 原样带回。
      security: []
      operationId: listMarkets
      parameters:
        - name: q
          in: query
          description: 关键词搜索（问题文本）
          schema: { type: string }
        - name: sort
          in: query
          description: 排序方式
          schema:
            type: string
            enum: [newest, ending_soon, default]
            default: newest
        - name: status
          in: query
          description: 状态过滤
          schema:
            type: string
            enum: [open, resolved, paused, closed, all]
            default: all
        - name: tag
          in: query
          description: 按标签过滤
          schema: { type: string }
        - name: series
          in: query
          description: 按事件系列过滤
          schema: { type: string }
        - name: limit
          in: query
          description: 每页条数（默认 20，有服务端上限）
          schema: { type: integer, minimum: 1, default: 20 }
        - name: cursor
          in: query
          description: keyset 分页游标（上一页响应的 nextCursor）
          schema: { type: string }
      responses:
        '200':
          description: 市场分页
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketsPage'
        '400':
          $ref: '#/components/responses/BadRequest'

  /api/v1/markets/{marketId}:
    get:
      tags: [Markets]
      summary: 市场详情
      description: |
        路径参数接受引擎 marketId 或 slug。含裁决四字段（`winningOutcome` /
        `resolvedAtS` / `disputeWindowEndsAt` / `engineState`）:
        「已裁决」判据是 `winningOutcome != null`，不是 `tradeable == false`；
        争议窗口是否结束是时间派生态，服务端不单独固化。
      security: []
      operationId: getMarket
      parameters:
        - name: marketId
          in: path
          required: true
          description: 引擎市场 id 或 slug
          schema: { type: string }
      responses:
        '200':
          description: 市场摘要
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarketSummary'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/markets/{marketId}/quote:
    get:
      tags: [Market Data]
      summary: 最优买卖报价
      description: |
        YES/NO 双侧最优价（NO 侧是 YES 的互补价，盘口坐标没有 outcome 维度）。
        空侧为 `null`，不是 0。单市场查询，支持 `minCursor` 读己之写。
      security: []
      operationId: getQuote
      parameters:
        - $ref: '#/components/parameters/MarketId'
        - $ref: '#/components/parameters/MinCursor'
      responses:
        '200':
          description: 双侧最优价
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuoteResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/markets/{marketId}/book:
    get:
      tags: [Market Data]
      summary: 盘口深度
      description: |
        YES 计价的买卖档位（`priceE4`/`sizeE2`）。`depth` 默认 10，服务端钳到
        100。支持 `minCursor`。
      security: []
      operationId: getBook
      parameters:
        - $ref: '#/components/parameters/MarketId'
        - name: depth
          in: query
          description: 返回的档位数量
          schema: { type: integer, minimum: 1, default: 10 }
        - $ref: '#/components/parameters/MinCursor'
      responses:
        '200':
          description: 盘口快照
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BookResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/markets/{marketId}/chain-binding:
    get:
      tags: [Markets]
      summary: 查询链上市场绑定
      description: |
        签 OrderV1 用的链上 Market 对象 id 唯一读取路径。**只有 200 可以签名
        下单**；404(`chain_binding_not_found`)/409（`chain_binding_not_active`,
        `status` 给原态）/503（`chain_binding_source_unavailable`）四种响应刻意
        分开，任何失败都不返回占位值。客户端可缓存 200（绑定 ACTIVE 后不可变），
        **不要缓存 404/409**。`packageId` 是建市场的合约包地址，不是签名域里的
        `package_id`（那个取自链上 ProtocolConfig）;`account_epoch` 不在这里，
        由前端直接读链。
      security: []
      operationId: getChainBinding
      parameters:
        - $ref: '#/components/parameters/MarketId'
      responses:
        '200':
          description: 绑定 ACTIVE
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChainBindingResponse'
        '404':
          description: 尚无绑定（`chain_binding_not_found`）,keeper 还没建链上市场
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: chain_binding_not_found
                message: 表里没有这一行，keeper 还没为它建市场
                recoverable: false
        '409':
          description: 绑定存在但不可用（`chain_binding_not_active`）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChainBindingError'
              example:
                error: chain_binding_not_active
                status: PENDING
        '503':
          description: clearing 不可达（`chain_binding_source_unavailable`）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: chain_binding_source_unavailable
                message: clearing 不可达，可退避重试
                recoverable: true

  # ===========================================================================
  # Events（PX 发现层，公开读；网关匿名白名单）
  # ===========================================================================
  /api/v1/px/events:
    get:
      tags: [Events]
      summary: 事件列表
      description: |
        事件列表。用 limit / offset 翻页。q、tag、sub 最长 100 字。
      security: []
      operationId: listEvents
      parameters:
        - name: tag
          in: query
          description: 大类标签 slug（如 crypto）
          schema: { type: string, maxLength: 100 }
        - name: sub
          in: query
          description: 细分标签 slug;`tag` 的下级
          schema: { type: string, maxLength: 100 }
        - name: q
          in: query
          description: 标题关键词
          schema: { type: string, maxLength: 100 }
        - name: sort
          in: query
          description: 非法枚举 400。缺省 volume24h
          schema:
            type: string
            enum: [volume24h, volume, liquidity, newest, endingSoon]
            default: volume24h
        - name: status
          in: query
          description: 非法枚举 400。缺省 active
          schema:
            type: string
            enum: [active, resolved, all]
            default: active
        - name: limit
          in: query
          description: 每页条数，缺省 20，服务端钳到 100
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - name: offset
          in: query
          description: 跳过条数；超大值静默钳到 10000，得到空页
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        '200':
          description: 事件一页
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxEventsPage'
        '400':
          $ref: '#/components/responses/BadRequest'

  /api/v1/px/events/{slugOrId}:
    get:
      tags: [Events]
      summary: 事件详情
      description: |
        路径可以是 id 或 slug。展示状态看 state，不要用 active / closed。
      security: []
      operationId: getEvent
      parameters:
        - $ref: '#/components/parameters/PxSlugOrId'
      responses:
        '200':
          description: 事件详情
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxEventDetail'
        '404':
          description: 事件不存在（eventId 与 slug 都解析不到）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/px/tags:
    get:
      tags: [Events]
      summary: 事件标签
      description: |
        标签列表。within 可下钻细分；未知父级返回空列表。
      security: []
      operationId: listEventTags
      parameters:
        - name: within
          in: query
          description: 父标签 slug；省略 = 只返回顶级
          schema: { type: string }
      responses:
        '200':
          description: 标签列表
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxTagsResponse'

  /api/v1/px/markets/{idOrSlug}:
    get:
      tags: [Events]
      summary: 事件下的市场详情
      description: |
        发现层市场详情。marketId 可直接用于下单。价格为字符串。
      security: []
      operationId: getPxMarket
      parameters:
        - $ref: '#/components/parameters/PxIdOrSlug'
      responses:
        '200':
          description: PX 市场详情
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxMarketDetail'
        '404':
          description: 市场不存在（marketId 与 slug 都解析不到）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/px/markets/{idOrSlug}/history:
    get:
      tags: [Events]
      summary: 市场概率历史
      description: |
        走势采样，不是交易 K 线。outcome 仅 YES 或 NO。时间与价格为字符串。
      security: []
      operationId: getPxMarketHistory
      parameters:
        - $ref: '#/components/parameters/PxIdOrSlug'
        - name: outcome
          in: query
          description: 结算方向；缺省 YES
          schema: { type: string, enum: [YES, NO], default: YES }
        - name: interval
          in: query
          schema:
            type: string
            enum: [1h, 6h, 1d, 1w, 1m, max]
            default: 1d
        - name: fidelity
          in: query
          description: 采样桶数，缺省 10，上限 500
          schema: { type: integer, minimum: 1, maximum: 500, default: 10 }
      responses:
        '200':
          description: 采样点
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxHistoryResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          description: 市场不存在
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/px/updown:
    get:
      tags: [Events]
      summary: 5 分钟涨跌视图
      description: |
        五分钟涨跌。rounds 下有 previous / current / next，槽位可能为空。
        锚价未定时不要用 0 代替。下单仍走交易接口。
      security: []
      operationId: getUpdown
      parameters:
        - name: series
          in: query
          description: 系列 id，如 btc-up-down-5m
          schema: { type: string }
      responses:
        '200':
          description: 涨跌视图
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxUpdownResponse'
        '400':
          $ref: '#/components/responses/BadRequest'

  # ===========================================================================
  # PX Account（账户与流水，Bearer；身份从会话推导，路径不带地址段）
  # ===========================================================================
  /api/v1/px/account:
    get:
      tags: [PX Account]
      summary: 账户余额快照
      description: |
        发现层的账户摘要，与交易线的 `GET /api/v1/accounts/{owner}` **不是同一口径**：
        这里是 E6 字符串、字段名是 `cashE6` / `reservedE6`，那边是 E6 整数、字段名是
        `availableE6` / `frozenE6`。对账脚本认准一条，不要混着读。

        `positionCostBasisE6` **恒为 0**：成本读模型尚未建设，这里返回诚实的 0 而不是
        拿市值冒充成本。
      operationId: getPxAccount
      responses:
        '200':
          description: 账户摘要
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxAccount'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/px/positions:
    get:
      tags: [PX Account]
      summary: 我的持仓
      description: |
        一次返回全部持仓，不分页。**大量字段是诚实的空**：`costBasisE6` / `realizedPnlE6` /
        `avgEntryE6` 零值、`lastPriceE6` 可能为 null（标记价不可得时不拿 0 冒充）、
        杠杆相关字段在无融资时为零。这些留给成本读模型回填，不要据此算盈亏。

        `priceState` 说明标记价的可信度，`markUpdatedAtMs` 是它的时刻——陈旧行情要靠
        这两个字段自己判定，服务端不替你拦。
      operationId: listPxPositions
      responses:
        '200':
          description: 持仓列表
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxPositionsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/px/orders:
    get:
      tags: [PX Account]
      summary: 我的订单
      description: |
        最新在前。`createdAt` 取的是**订单行最后更新时间**，不是下单时间——读模型里没有
        下单时间列。要真实下单时刻请用交易线的命令日志 cursor，不要拿这个字段做时序推断。

        价格与数量是 E6 字符串，`outcome` 是 `A`/`B`，与交易线的 E4/E2 + `YES`/`NO` 不同。
      operationId: listPxOrders
      parameters:
        - name: limit
          in: query
          description: 返回条数上限
          schema: { type: integer, minimum: 1, default: 100 }
      responses:
        '200':
          description: 订单列表
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxOrdersResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/px/fills:
    get:
      tags: [PX Account]
      summary: 我的成交
      description: |
        **不是逐笔成交明细**：本端点从已成交订单推导，**一单一笔**。一张分多次成交的订单
        在这里只有一行，价格是该单的汇总口径。需要逐笔成交请订阅交易线 WS 的
        `account:{trader}:fills`。

        `liquidity` **恒为 `USER`**——maker/taker 区分尚未接入，不要据此算返佣。
      operationId: listPxFills
      parameters:
        - name: limit
          in: query
          description: 返回条数上限
          schema: { type: integer, minimum: 1, default: 100 }
      responses:
        '200':
          description: 成交列表
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxFillsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/px/ledger:
    get:
      tags: [PX Account]
      summary: 资金流水
      description: |
        账户资金变更逐条流水，最新在前。`balanceAfterE6` 是该条生效后的余额，
        `refId` 指向引发它的对象（订单、强平、结算等，按 `kind` 决定值域）。

        这是对账的唯一权威读端点：持仓与订单是投影，流水是账本。
      operationId: listPxLedger
      parameters:
        - name: limit
          in: query
          description: 返回条数上限
          schema: { type: integer, minimum: 1, default: 100 }
      responses:
        '200':
          description: 流水列表
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxLedgerResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/px/position-history:
    get:
      tags: [PX Account]
      summary: 平仓历史
      description: |
        **当前恒返回空数组**：平仓历史读模型尚未建设。端点已按最终契约形状对外，
        接上数据源后字段不变——现在不要把空数组读成「此账户没有平仓过」。
      operationId: listPxPositionHistory
      responses:
        '200':
          description: 平仓历史（当前恒空）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxPositionHistoryResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ===========================================================================
  # PX Margin（杠杆、借贷与强平，Bearer）
  # ===========================================================================
  /api/v1/px/credit:
    get:
      tags: [PX Margin]
      summary: 信用状态
      description: |
        含息债务、净值、健康度与可借上限。`healthBps` 低于 `maintenanceBps` 会**自动触发
        强平**——这是服务端行为，不需要也不接受调用方确认。

        `healthBps` 可为 null（无债务时健康度无定义），此时 `healthy` 为 true。
        不要把 null 当 0 处理。
      operationId: getPxCredit
      responses:
        '200':
          description: 信用状态
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxCredit'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/px/liquidations:
    get:
      tags: [PX Margin]
      summary: 强平记录
      description: |
        本账户的强平历史。`badDebtE6` 非零表示处置所得不足以覆盖债务，差额由保险池承担——
        这条记录是已发生的事实，不是待处理事项。
      operationId: listPxLiquidations
      responses:
        '200':
          description: 强平记录
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxLiquidationsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/px/borrow:
    post:
      tags: [PX Margin]
      summary: 借入
      description: |
        计入债务并**持续计息**，现金入账。同步执行，成功直接返回最新信用状态——
        没有 202 语义，也没有 cursor。

        失败是 400 + `{code, message}`，**不是**统一错误体 `{error, message, recoverable}`。
        `code` ∈ `INVALID_AMOUNT` / `EXCEEDS_MAX_BORROW`。辖区准入不通过同样在此拦下。
      operationId: pxBorrow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PxAmountRequest'
            example:
              amountE6: "1000000000"
      responses:
        '200':
          description: 借入成功，返回最新信用状态
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxCredit'
        '400':
          description: 金额非法或超过可借额度
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxCreditWriteError'
              example:
                code: EXCEEDS_MAX_BORROW
                message: 可借额度不足
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/px/repay:
    post:
      tags: [PX Margin]
      summary: 还款
      description: |
        偿还债务，现金出账。`code` ∈ `INVALID_AMOUNT` / `EXCEEDS_DEBT` /
        `INSUFFICIENT_BALANCE`——还款额不能超过当前债务，也不能超过可用余额。
      operationId: pxRepay
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PxAmountRequest'
            example:
              amountE6: "500000000"
      responses:
        '200':
          description: 还款成功，返回最新信用状态
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxCredit'
        '400':
          description: 金额非法、超过债务或余额不足
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxCreditWriteError'
              example:
                code: EXCEEDS_DEBT
                message: 还款金额超过当前债务
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/px/lend:
    post:
      tags: [PX Margin]
      summary: 出借
      description: |
        从可用余额划出本金供他人借入，**不产生债务**。`code` ∈ `INVALID_AMOUNT` /
        `INSUFFICIENT_BALANCE`。已出借本金记在 `lendDepositE6`，用 `/lend/withdraw` 收回。
      operationId: pxLend
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PxAmountRequest'
            example:
              amountE6: "2000000000"
      responses:
        '200':
          description: 出借成功，返回最新信用状态
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxCredit'
        '400':
          description: 金额非法或可用余额不足
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxCreditWriteError'
              example:
                code: INSUFFICIENT_BALANCE
                message: 可用余额不足
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/px/lend/withdraw:
    post:
      tags: [PX Margin]
      summary: 提取出借本金
      description: |
        收回已出借的本金，现金入账。`code` ∈ `INVALID_AMOUNT` / `EXCEEDS_LEND_DEPOSIT`——
        提取额不能超过 `lendDepositE6`。本金被借出期间可提取额度会下降。
      operationId: pxLendWithdraw
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PxAmountRequest'
            example:
              amountE6: "2000000000"
      responses:
        '200':
          description: 提取成功，返回最新信用状态
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxCredit'
        '400':
          description: 金额非法或超过已出借本金
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxCreditWriteError'
              example:
                code: EXCEEDS_LEND_DEPOSIT
                message: 提取金额超过已出借本金
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ===========================================================================
  # InfoAI（公开读，可永久降级）
  # ===========================================================================
  /api/v1/px/ai/picks:
    get:
      tags: [InfoAI]
      summary: AI 热度推荐
      description: |
        **当前恒返回 `enabled=false` 与空列表**：分析数据源尚未建设。这是有意的降级契约
        而不是故障——`enabled=false` 时前端整卡隐藏，调用方也应当跳过而不是报错重试。
      operationId: getPxAiPicks
      security: []
      responses:
        '200':
          description: 推荐列表（当前恒为关闭态）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxAiPicksResponse'
              example:
                enabled: false
                picks: []

  /api/v1/px/ai/ask:
    post:
      tags: [InfoAI]
      summary: AI 自然语言顾问
      description: |
        公开端点，**不与账户关联，query 不落库**。`query` 非空且 ≤ 200 字符。

        两种降级都走 HTTP 200，不报错：`enabled=false` 表示功能未开启；
        `advice=null` 表示本次 LLM 调用或解析失败。调用方按这两个字段判定，
        不要依赖状态码。
      operationId: pxAiAsk
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PxAiAskRequest'
            example:
              query: BTC 今晚会涨吗
      responses:
        '200':
          description: 顾问回复（含两种降级态）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxAiAskResponse'

  /api/v1/px/crypto/{coin}/history:
    get:
      tags: [Events]
      summary: 加密现货价回放
      description: |
        发现页行情条用的现货价窗口。**当前恒返回空序列**：数据源尚未建设。
        `priceText` 是展示用字符串（如 `97350.00`），不是定点整数——不要拿它做计算。
      operationId: getPxCryptoHistory
      security: []
      parameters:
        - name: coin
          in: path
          required: true
          description: 币种符号，如 BTC
          schema: { type: string }
        - name: fromMs
          in: query
          required: true
          description: 窗口起点，epoch 毫秒
          schema: { type: integer, format: int64 }
        - name: toMs
          in: query
          required: true
          description: 窗口终点，epoch 毫秒
          schema: { type: integer, format: int64 }
      responses:
        '200':
          description: 现货价采样点（当前恒空）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PxCryptoHistoryResponse'

  # ===========================================================================
  # Orders（写接口经网关路由到 exchange-order;/api/v1 前缀由网关剥除）
  # ===========================================================================
  /api/v1/orders:
    post:
      tags: [Orders]
      summary: 下单
      description: |
        **202 = 命令已持久 ≠ 已成交**。响应体没有撮合结果，拿
        `cursor` 去 `GET /markets/{marketId}/orders/{orderId}?minCursor=…` 轮询。

        `clientOrderId`（可选）是**幂等键**：传入后重试同一笔不会再产生第二条
        命令（按 `(trader, clientOrderId)` 去重，回放返回首次的原始响应）；不传
        则重试可能重复下单，风险原样保留。`clientOrderId` 与响应里的
        `orderId` 是两个值域，不能混用——`orderId` 永远由服务端生成。

        带 `auth`（链上订单签名）时做字节级一致性校验，失败 400 且
        **命令未落日志、资金未冻结**，修好装配再发，重试无意义。503 的
        `error` ∈ `command_log_unavailable` / `seq_unavailable` /
        `duplicate_in_flight`（均可恢复重试）。

        **`tif: FOK` 与 `auth` 不能同时用。** 链下引擎完整支持 FOK（全量成交否则整单
        拒绝），但链上结算合约把 FOK 的 `order_type` 当作保留值直接 abort
        （`unsupported_order_type`）。链下这一层**不会替你拦**：签名 FOK 单会被正常受理、
        返回 202、冻结资金，最后卡在结算。签名单请用 GTC 或 GTD。
      operationId: placeOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlaceOrderRequest'
            example:
              marketId: mkt-0001
              outcome: YES
              side: BUY
              priceE4: 5200
              sizeE2: 1000
              tif: GTC
              postOnly: false
      responses:
        '202':
          description: 命令已持久化（未撮合）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderAcceptedResponse'
        '400':
          description: 校验失败（字段校验，或带 auth 时的准入一致性校验）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: invalid_request
                message: 字段校验失败；带 auth 时为准入一致性校验失败，命令未落日志、资金未冻结
                recoverable: false
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: 可恢复故障（见 description）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: command_log_unavailable
                message: 命令日志暂不可写，可退避重试
                recoverable: true
    get:
      tags: [Orders]
      summary: 我的订单列表
      description: |
        身份从会话推导。`status` 支持逗号分隔多值（「仍在簿上」传
        `status=OPEN,PARTIAL`）。不带 `marketId` 时是跨分区查询，不支持
        `minCursor`（传了 400）；带 `marketId` 退化为单市场查询，此时支持。
      operationId: listMyOrders
      parameters:
        - name: marketId
          in: query
          description: 按市场过滤；带上后本端点退化为单市场查询，支持 minCursor
          schema: { type: string }
        - name: status
          in: query
          description: 逗号分隔的状态过滤
          schema: { type: string }
        - name: limit
          in: query
          description: 返回条数上限
          schema: { type: integer, minimum: 1, default: 100 }
        - $ref: '#/components/parameters/MinCursor'
      responses:
        '200':
          description: 订单列表
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/OrderStatusResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/markets/{marketId}/orders/{orderId}:
    get:
      tags: [Orders]
      summary: 查询订单状态
      description: |
        下单 202 响应里的 `cursor` 就是为这里准备的：`?minCursor=…`。404 双义
        （投影未追上 / 订单不存在，逐字节不可区分）——第一个 404
        带同一个 cursor 重试，不是最终结果。不是本人的订单同样 404 而非 403
        （避免枚举订单归属）。
      operationId: getOrderStatus
      parameters:
        - $ref: '#/components/parameters/MarketId'
        - name: orderId
          in: path
          required: true
          description: 服务端生成的订单 id（下单 202 响应体返回）
          schema: { type: string }
        - $ref: '#/components/parameters/MinCursor'
      responses:
        '200':
          description: 订单当前状态
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderStatusResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/orders/cancel:
    post:
      tags: [Orders]
      summary: 撤单
      description: |
        202 语义。请求体 `orderId` 与 `clientOrderId` **二选一**（都传以
        `orderId` 为准；都不传 400）。`clientOrderId` 反查复用下单去重存储，
        查不到 404 `client_order_id_not_found`（不会静默当 no-op）。撤一个已
        撤销/不存在的订单是无副作用 no-op，不接幂等去重。
      operationId: cancelOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CancelOrderRequest'
      responses:
        '202':
          description: 撤单命令已持久化
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommandAcceptedResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/orders/cancel-all:
    post:
      tags: [Orders]
      summary: 撤销某市场全部挂单
      description: |
        202 语义。同一请求重试会再落一条 CancelAll 命令、撤到 0 单——无资金副作用，重复调用安全。幂等键刻意不接（撤单与下单的风险类别不同）。
      operationId: cancelAllOrders
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CancelAllRequest'
      responses:
        '202':
          description: 命令已持久化
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommandAcceptedResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/orders/redeem:
    post:
      tags: [Orders]
      summary: 赎回
      description: |
        裁决后持赢方代币者按面值（1.0000 USD/份）赎回。202 fire-and-forget：
        引擎侧校验「市场已终局且有赢方持仓」，**拒绝同样静默**——redeem 之后
        唯一的可观测出口是 `GET /markets/{marketId}/redemption`。
      operationId: redeem
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RedeemRequest'
      responses:
        '202':
          description: 命令已持久化
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommandAcceptedResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/markets/{marketId}/redemption:
    get:
      tags: [Orders]
      summary: 我的赎回结果
      description: |
        无行 = 尚未生效 / 引擎侧拒绝 / 投影未追上，统一 404 且不可区分；支持单市场 `minCursor`。`payoutE6` 恒等于 `sizeE2 × 10000`。
      operationId: getRedemption
      parameters:
        - $ref: '#/components/parameters/MarketId'
        - $ref: '#/components/parameters/MinCursor'
      responses:
        '200':
          description: 赎回结果
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RedemptionResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/orders/heartbeat:
    post:
      tags: [Orders]
      summary: 交易心跳
      description: |
        做市商定期打心跳并声明要保护的市场，超时未续则交易所代撤这些市场的全部
        挂单。**返回 200 不是 202**——不是命令，截止时刻同步写登记簿。客户端
        逻辑只有一句：每隔 `timeout/3` 打一次。`timeoutSeconds` 有部署级上下限
        （默认 5s–300s）,`marketIds` 上限默认 64、自动去重。部署关闭时返回
        **501 `heartbeat_disabled`**（不是 400）。
      operationId: heartbeat
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HeartbeatRequest'
      responses:
        '200':
          description: 保护生效
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HeartbeatResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '501':
          description: 该部署未启用心跳（`heartbeat_disabled`）
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error: heartbeat_disabled
                message: 该部署未启用心跳
                recoverable: false

  /api/v1/orders/heartbeat/deregister:
    post:
      tags: [Orders]
      summary: 注销心跳（正常下线）
      description: |
        撤掉登记但**不**触发全撤——收工时挂单去留是做市商自己的决定。幂等：
        `removed=false` 表示本来没有登记，不是错误。
      operationId: heartbeatDeregister
      responses:
        '200':
          description: 已注销
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HeartbeatDeregisteredResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ===========================================================================
  # Market Data（公共成交 / K 线）
  # ===========================================================================
  /api/v1/markets/{marketId}/trades:
    get:
      tags: [Market Data]
      summary: 成交流水
      description: |
        按 `(ts_ms DESC, seq DESC)` 倒序；`ts_ms` 缺失的历史行排最末尾不丢弃。
        分页是 keyset:`nextCursor` 非空则原样带回 `before` 翻更早一段。
        **跨时间分析型读，传 `minCursor` 直接 400**。
      security: []
      operationId: listTrades
      parameters:
        - $ref: '#/components/parameters/MarketId'
        - name: before
          in: query
          description: 不透明游标（上一页 nextCursor）
          schema: { type: string }
        - name: limit
          in: query
          description: 每页条数（1–500）
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
      responses:
        '200':
          description: 成交一页
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TradesPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /api/v1/markets/{marketId}/candles:
    get:
      tags: [Market Data]
      summary: K 线（OHLCV）
      description: |
        `interval` 命名预设 `1m/5m/1h/1d`（默认 `1h`），未知值 400;`from`/`to`
        过滤**桶起点** ∈ [from, to)，可选。按桶起点升序，无成交的桶不返回。
        传 `minCursor` 直接 400。与逐笔成交同源（fills 聚合），可对账。
      security: []
      operationId: getCandles
      parameters:
        - $ref: '#/components/parameters/MarketId'
        - name: interval
          in: query
          description: K 线粒度
          schema:
            type: string
            enum: [1m, 5m, 1h, 1d]
            default: 1h
        - name: from
          in: query
          description: epoch 毫秒
          schema: { type: integer, format: int64 }
        - name: to
          in: query
          description: epoch 毫秒
          schema: { type: integer, format: int64 }
      responses:
        '200':
          description: K 线数组
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/CandleResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  # ===========================================================================
  # Account / Positions / Settlement
  # ===========================================================================
  /api/v1/accounts/{owner}:
    get:
      tags: [Account]
      summary: 余额快照
      description: |
        `owner` 必须等于会话地址（服务端强制，杜绝跨用户读）。`fillReservedE6`
        是第三档：链下已成交、链上结算中的卖方收益，待确认后释放进
        `availableE6`。无 C 端 REST 充提——资金进出走链上 + 索引器。
      operationId: getAccount
      parameters:
        - name: owner
          in: path
          required: true
          description: 要查询的地址，必须等于会话地址
          schema: { type: string }
      responses:
        '200':
          description: 余额
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountBalance'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: owner 与会话地址不一致
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/v1/markets/{marketId}/position:
    get:
      tags: [Positions]
      summary: 单市场持仓
      description: |
        持仓只能查自己的——路径不带 trader 段。`chainState` 两态：SETTLING = 该 trader 在该市场仍有未确认结算 job;
        CONFIRMED = 其余。clearing 不可达时整页保守降级 SETTLING（展示层状态，
        不是资金可用性）。支持单市场 `minCursor`。
      operationId: getMarketPosition
      parameters:
        - $ref: '#/components/parameters/MarketId'
        - $ref: '#/components/parameters/MinCursor'
      responses:
        '200':
          description: 持仓
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PositionResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/positions:
    get:
      tags: [Positions]
      summary: 全部市场持仓
      description: |
        一次返回，每行多 `marketId`。跨分区读，**不支持 `minCursor`**（传了
        400；单市场端点才支持）。
      operationId: listMyPositions
      responses:
        '200':
          description: 持仓列表
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/MarketPositionResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/me/settlements:
    get:
      tags: [Positions]
      summary: 未完成的链上结算（逐笔）
      description: |
        本人名下全部**未完成**的链上结算，最新在前。`settlementStatus` 产品口径：
        SETTLING ← QUEUED/SUBMITTED;RETRYING ← FAILED_RETRYABLE;FAILED ←
        FAILED_TERMINAL（账本与链上已分叉，处置前别基于它报价）。**不在列表里
        = 已结算**。clearing 不可用时返回 503，不降级成空列表——空列表等于
        "全部已结算"，对风险查询是方向性错误。
      operationId: listMySettlements
      responses:
        '200':
          description: 未完成结算列表
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/SettlementStatus'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'

  # ===========================================================================
  # WebSocket 握手
  # ===========================================================================
  /api/v1/ws/handshake-token:
    post:
      tags: [WebSocket]
      summary: 签发短时 WS 握手令牌
      description: |
        换短时 token 用于 WS 连接。**WS 本体不经本文档的 REST 面**：交易线在
        stream 的 `/ws`（`{action,topic}` 协议）,PX 线在 `/api/v1/stream`
        （网关升级转发；Next 的 `/api/exchange` rewrite 不代理 WebSocket）。
        
      operationId: wsHandshakeToken
      responses:
        '200':
          description: 令牌已签发
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WsHandshakeToken'
        '401':
          $ref: '#/components/responses/Unauthorized'

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 会话 Bearer token（auth/verify 或 google/email 登录签发）。
        Rebate 门户的 token 与站点交易会话分离，不要混用打交易 API。

  parameters:
    MarketId:
      name: marketId
      in: path
      required: true
      description: 引擎市场 id（读取类端点不接受 slug，除非该端点文档另有说明）
      schema: { type: string }
    MinCursor:
      name: minCursor
      in: query
      required: false
      description: 读己之写续读点（下单 202 响应体的 cursor 原样传入）。投影未追上时该查询返回 404，与"资源不存在"不可区分——带同一个 cursor 轮询。
      schema: { type: string }

    PxIdOrSlug:
      name: idOrSlug
      in: path
      required: true
      description: PX 市场 marketId 或 slug（与引擎市场是同一套 id）
      schema: { type: string }
    PxSlugOrId:
      name: slugOrId
      in: path
      required: true
      description: 事件 eventId 或 slug，同一套解析，不必拆成两条接口
      schema: { type: string }
  responses:
    BadRequest:
      description: 请求非法（字段校验、未知枚举、跨分区/跨时间端点传了 minCursor 等）
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: invalid_request
            message: 请求字段校验失败
            recoverable: false
    Unauthorized:
      description: 缺失/无效/过期的会话（`auth_required`；会话受 24h 空闲 + 7d 绝对上限双闸门，一直在用的令牌也会在签发 7 天后过期，重新登录即可）
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: auth_required
            message: 需要登录
            recoverable: true
    NotFound:
      description: 资源不存在——对带 minCursor 的查询，404 同时可能是"投影未追上"，两重含义逐字节不可区分，带同一个 cursor 轮询而不是当成终态
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: market_not_found
            message: 未找到
            recoverable: false
    ServiceUnavailable:
      description: 下游依赖暂不可用（可重试；对结算查询是明说"不知道"，不降级成空列表）
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error: settlement_state_unavailable
            message: 下游依赖暂不可用，可退避重试
            recoverable: true

  schemas:
    NonceRequest:
      type: object
      required: [address]
      properties:
        address: { type: string, description: Sui 钱包地址 }
    NonceResponse:
      type: object
      required: [nonce, message, expiresAt]
      properties:
        nonce: { type: string, description: 一次性随机数 }
        message:
          type: string
          description: 后端生成的完整待签原文，原样交给钱包签名
        expiresAt: { type: integer, format: int64, description: epoch 毫秒 }
    VerifyRequest:
      type: object
      required: [address, signature, bytes]
      properties:
        address: { type: string, description: 钱包地址 }
        signature:
          type: string
          description: 钱包 signPersonalMessage 返回的签名（base64 透传）
        bytes:
          type: string
          description: 被签原文（base64 透传；校验是对原文比对，不要自行拼装）
    SessionResponse:
      type: object
      required: [token, address, expiresAt]
      properties:
        token: { type: string, description: Bearer 会话令牌 }
        address: { type: string, description: 会话地址 }
        expiresAt:
          type: integer
          format: int64
          description: epoch 毫秒，按空闲窗口算
    MeResponse:
      type: object
      required: [address, country, expiresAt]
      properties:
        address: { type: string, description: 会话地址 }
        country: { type: string, description: 会话推导的辖区（ISO 国家码） }
        expiresAt: { type: integer, format: int64, description: 空闲窗口过期时刻（epoch 毫秒） }

    MarketSummary:
      type: object
      description: 市场摘要（列表与详情同一形状）
      required: [marketId, slug, question, tickE4, tradeable, cutoffTs, clarifications]
      properties:
        marketId: { type: string, description: 引擎市场 id }
        slug: { type: string, description: URL 标识 }
        question: { type: string, description: 问题文案 }
        tickE4:
          type: integer
          description: 价格档位（1e-4 USD）,per-market 可配置
        tradeable: { type: boolean, description: 当前是否可下单 }
        resolutionSource: { type: string, nullable: true, description: 裁决数据源 }
        cutoffTs: { type: integer, format: int64, description: 撮合截止（epoch 秒） }
        underlyingCoin: { type: string, nullable: true, description: 底层币种（如 BTC） }
        winningOutcome:
          type: string
          nullable: true
          enum: [YES, NO, null]
          description: 裁决赢方；null = 未裁决。「已裁决」的判据就是它非空
        resolvedAtS:
          type: integer
          format: int64
          nullable: true
          description: 裁决命令固化的 Unix 秒（非投影落库时间）
        disputeWindowEndsAt:
          type: integer
          format: int64
          nullable: true
          description: 争议窗口结束（epoch 秒）；是否已结束由客户端与当前时间比较
        engineState:
          type: string
          nullable: true
          enum: [READY, OPEN, PAUSED, CLOSED, null]
          description: 引擎侧生命周期；null = 迁移记录早于本字段，按未知处理
        onChainCutoffMs:
          type: integer
          format: int64
          nullable: true
          description: 链上撮合窗口（epoch 毫秒）
        clarifications:
          type: array
          description: 澄清记录
          items: { type: object, additionalProperties: true }
        seriesId: { type: string, nullable: true, description: 所属周期性事件系列 }
    MarketsPage:
      type: object
      required: [markets, nextCursor]
      properties:
        markets:
          type: array
          description: 市场摘要列表
          items: { $ref: '#/components/schemas/MarketSummary' }
        nextCursor:
          type: string
          nullable: true
          description: keyset 游标；null = 没有下一页

    QuoteResponse:
      type: object
      description: 双侧最优价；空侧为 null 不是 0。NO 侧是 YES 的互补价
      required: [market, yesBidE4, yesAskE4, noBidE4, noAskE4, spreadE4]
      properties:
        market: { type: string, description: 市场 id }
        yesBidE4: { type: integer, nullable: true, description: YES 最优买价；该侧无挂单为 null }
        yesAskE4: { type: integer, nullable: true, description: YES 最优卖价；该侧无挂单为 null }
        noBidE4: { type: integer, nullable: true, description: NO 最优买价（= 10000 − yesAsk） }
        noAskE4: { type: integer, nullable: true, description: NO 最优卖价（= 10000 − yesBid） }
        spreadE4: { type: integer, nullable: true, description: YES 侧买卖价差 }
    DepthLevel:
      type: object
      required: [priceE4, sizeE2]
      properties:
        priceE4: { type: integer, description: 价格档位（1e-4 USD） }
        sizeE2: { type: integer, format: int64, description: 该档位挂单量（1e-2 份） }
    BookResponse:
      type: object
      required: [market, yesBids, yesAsks]
      properties:
        market: { type: string, description: 市场 id }
        yesBids:
          type: array
          description: YES 侧买档，价格从优到劣
          items: { $ref: '#/components/schemas/DepthLevel' }
        yesAsks:
          type: array
          description: YES 侧卖档
          items: { $ref: '#/components/schemas/DepthLevel' }

    ChainBindingResponse:
      type: object
      required: [marketId, onchainMarketId, packageId, status]
      properties:
        marketId: { type: string, description: 链下市场 id }
        onchainMarketId:
          type: string
          description: 链上 Market 对象 id（签 OrderV1 用的 market_id）
        packageId:
          type: string
          description: 建市场的合约包地址；签名域 package_id 取自链上 ProtocolConfig，两者会不同
        status: { type: string, enum: [ACTIVE, PENDING, FAILED], description: 只有 ACTIVE 可以签名下单 }

    AccountBalance:
      type: object
      description: 余额快照（均 E6 定点字符串口径由网关透传为整数；浮点换算只发生在展示层）
      required: [availableE6, frozenE6, fillReservedE6, totalDepositsE6]
      properties:
        availableE6: { type: integer, format: int64, description: 可用余额 }
        frozenE6: { type: integer, format: int64, description: 冻结金额（挂单占用） }
        fillReservedE6:
          type: integer
          format: int64
          description: 链下已成交、链上结算中的卖方收益，确认后释放
        totalDepositsE6: { type: integer, format: int64, description: 累计净入金 }

    OrderAuthRequest:
      type: object
      description: 链上订单签名载荷（不传则走无签名路径，链上模式市场会拒）
      required: [orderBytes, signature]
      properties:
        orderBytes:
          type: string
          description: OrderV1 字节（hex;market_id 必须是链上 Market 对象 id）
        signature: { type: string, description: 订单签名（hex） }
        accountEpoch:
          type: integer
          nullable: true
          description: 前端直接读链获得（TradingAccount.order_epoch），后端不代理
    PlaceOrderRequest:
      type: object
      required: [marketId, outcome, side, priceE4, sizeE2]
      properties:
        marketId: { type: string, description: 引擎市场 id }
        clientOrderId:
          type: string
          description: 调用方自选幂等键；不传则重试可能重复下单
        outcome: { type: string, enum: [YES, NO], description: 结算方向 }
        side: { type: string, enum: [BUY, SELL], description: 买卖方向 }
        priceE4:
          type: integer
          minimum: 1
          maximum: 9999
          description: 1e-4 USD；须为市场 tickE4 整数倍
        sizeE2:
          type: integer
          format: int64
          minimum: 1
          description: 1e-2 份
        tif:
          type: string
          enum: [GTC, GTD, FAK, FOK]
          default: GTC
          description: |-
            不传等同 GTC。GTD 必须同时给 `expiration`。FAK 立即成交可成交部分、剩余撤销；
            FOK 全量成交否则整单拒绝（拒绝理由 `FOK 无法全量成交`）。
            **`FOK` 只在不带 `auth` 的链下单上可用**——带链上签名时它在结算阶段必定失败，
            见本接口说明
        expiration:
          type: integer
          format: int64
          description: Unix 秒；GTD 必填，带 auth 的签名单必填
        postOnly:
          type: boolean
          default: false
          description: true = 只挂不吃；若会立即成交则整单拒绝。不传等同 false
        auth:
          $ref: '#/components/schemas/OrderAuthRequest'
    OrderAcceptedResponse:
      type: object
      description: 202 体——命令已持久 ≠ 已成交；orderId 由服务端生成（与 clientOrderId
        不同值域）;cursor 原样带回状态查询当 minCursor
      required: [orderId, cursor, commandId]
      properties:
        orderId:
          type: string
          description: 服务端生成的订单 id，用于后续查询与撤单
        clientOrderId:
          type: string
          nullable: true
          description: 请求传入的幂等键原样回显；未传为 null
        cursor:
          type: string
          description: 读己之写续读点，原样传给状态查询的 minCursor
        commandId: { type: string, description: 命令标识 }
    CommandAcceptedResponse:
      type: object
      description: 撤单/撤全/赎回的 202 体（无 clientOrderId 回显）
      required: [cursor, commandId]
      properties:
        cursor: { type: string, description: 读己之写续读点 }
        commandId: { type: string, description: 命令标识 }
    CancelOrderRequest:
      type: object
      description: orderId 与 clientOrderId 二选一（都传以 orderId 为准，都不传 400）
      required: [marketId]
      properties:
        marketId: { type: string, description: 市场 id }
        orderId:
          type: string
          nullable: true
          description: 服务端生成的订单 id（与 clientOrderId 二选一）
        clientOrderId:
          type: string
          nullable: true
          description: 下单时自选的幂等键，可反查对应订单（与 orderId 二选一）
    CancelAllRequest:
      type: object
      required: [marketId]
      properties:
        marketId: { type: string, description: 要清空挂单的市场 id }
    RedeemRequest:
      type: object
      required: [marketId]
      properties:
        marketId: { type: string, description: 要赎回的市场 id }
    OrderStatusResponse:
      type: object
      description: 订单状态（状态机：OPEN → PARTIAL → FILLED / CANCELLED）
      required: [orderId, marketId, outcome, side, priceE4, sizeE2, filledE2, status]
      properties:
        orderId: { type: string, description: 服务端生成的订单 id }
        marketId: { type: string, description: 所属市场 }
        outcome: { type: string, enum: [YES, NO], description: 结算方向 }
        side: { type: string, enum: [BUY, SELL], description: 买卖方向 }
        priceE4: { type: integer, description: 委托价（1e-4 USD） }
        sizeE2: { type: integer, format: int64, description: 委托量（1e-2 份） }
        filledE2: { type: integer, format: int64, description: 已成交量（1e-2 份） }
        status:
          type: string
          enum: [OPEN, PARTIAL, FILLED, CANCELLED]
          description: OPEN=在簿；PARTIAL=部分成交；FILLED=全部成交；CANCELLED=已撤销
        reason: { type: string, nullable: true, description: 终态/拒绝原因（可空） }
    HeartbeatRequest:
      type: object
      required: [timeoutSeconds, marketIds]
      properties:
        timeoutSeconds:
          type: integer
          description: 部署级上下限内自选（默认 5s–300s）；建议每 timeout/3 打一次
        marketIds:
          type: array
          items: { type: string }
          description: 显式列出要保护的市场（上限默认 64，自动去重）
    HeartbeatResponse:
      type: object
      required: [expiresAtS, marketIds]
      properties:
        expiresAtS: { type: integer, format: int64, description: 本次心跳截止（epoch 秒） }
        marketIds:
          type: array
          description: 实际生效的保护市场列表
          items: { type: string }
    HeartbeatDeregisteredResponse:
      type: object
      required: [removed]
      properties:
        removed:
          type: boolean
          description: false = 本来没有登记，幂等不是错误

    RedemptionResponse:
      type: object
      required: [marketId, trader, winning, sizeE2, payoutE6, redeemedAtS]
      properties:
        marketId: { type: string }
        trader: { type: string, description: 会话地址回显 }
        winning: { type: string, enum: [YES, NO], description: 裁决赢方 }
        sizeE2: { type: integer, format: int64, description: 赎回份额（1e-2 份） }
        payoutE6:
          type: integer
          format: int64
          description: 恒等于 sizeE2 × 10000（面值 1.0000 USD/份）
        redeemedAtS: { type: integer, format: int64, description: 裁决命令固化的 Unix 秒 }

    TradeResponse:
      type: object
      description: 市场视角一笔成交；价格 E4 / 数量 E2
      required: [takerOrderId, makerOrderId, priceE4, sizeE2, settlement, seq]
      properties:
        takerOrderId: { type: string, description: 吃单订单 id }
        makerOrderId: { type: string, description: 挂单订单 id }
        priceE4:
          type: integer
          description: 成交价，YES 计价（1e-4）
        sizeE2: { type: integer, format: int64, description: 成交量（1e-2 份） }
        settlement:
          type: string
          enum: [MINT, MATCH, MERGE, REDEEM]
          description: MINT=铸币；MATCH=对碰；MERGE=合并；REDEEM=赎回
        seq: { type: integer, format: int64, description: 引擎内序号（同市场单调） }
        tsMs:
          type: integer
          format: int64
          nullable: true
          description: 撮合时刻（epoch 毫秒）；未记录时间戳的历史成交为 null，排在流水最末尾
    TradesPage:
      type: object
      required: [trades, nextCursor]
      properties:
        trades:
          type: array
          description: 成交列表，最新在前
          items: { $ref: '#/components/schemas/TradeResponse' }
        nextCursor:
          type: string
          nullable: true
          description: 不透明游标（编码 (ts_ms, seq)）;null = 已到最后一页
    CandleResponse:
      type: object
      required: [bucketStartMs, openE4, highE4, lowE4, closeE4, volumeE2, tradeCount]
      properties:
        bucketStartMs: { type: integer, format: int64, description: 桶起点（epoch 毫秒） }
        openE4: { type: integer, description: 开盘价（1e-4） }
        highE4: { type: integer, description: 最高价（1e-4） }
        lowE4: { type: integer, description: 最低价（1e-4） }
        closeE4: { type: integer, description: 收盘价（1e-4） }
        volumeE2: { type: integer, format: int64, description: 成交量（1e-2 份） }
        tradeCount: { type: integer, description: 成交笔数 }

    PositionResponse:
      type: object
      description: 单市场持仓
      required: [trader, balanceE6, yesE2, noE2, chainState]
      properties:
        trader: { type: string, description: 服务端回显的会话地址 }
        balanceE6: { type: integer, format: int64, description: 保证金（1e-6 USD） }
        yesE2: { type: integer, format: int64, description: YES 份额（1e-2） }
        noE2: { type: integer, format: int64, description: NO 份额（1e-2） }
        chainState:
          type: string
          enum: [SETTLING, CONFIRMED]
          description: SETTLING=有未确认结算 job;CONFIRMED=全部确认
    MarketPositionResponse:
      type: object
      description: 跨市场持仓一行 = 单市场形状 + marketId
      required: [marketId, trader, balanceE6, yesE2, noE2, chainState]
      properties:
        marketId: { type: string, description: 所属市场 }
        trader: { type: string, description: 会话地址回显 }
        balanceE6: { type: integer, format: int64, description: 保证金（1e-6 USD） }
        yesE2: { type: integer, format: int64, description: YES 份额（1e-2） }
        noE2: { type: integer, format: int64, description: NO 份额（1e-2） }
        chainState:
          type: string
          enum: [SETTLING, CONFIRMED]
          description: SETTLING=有未确认结算 job;CONFIRMED=全部确认
    SettlementStatus:
      type: object
      description: 逐笔链上结算
      required: [jobId, marketId, role, settlementStatus, takerIsYes, yesPriceE4,
        sizeE2, attempts, payloadCorrupted]
      properties:
        jobId:
          type: string
          description: partition-offset-fillIndex，稳定可幂等去重
        marketId: { type: string, description: 市场 id }
        role: { type: string, enum: [MAKER, TAKER], description: 本人在这笔成交中的角色 }
        settlementStatus:
          type: string
          enum: [SETTLING, RETRYING, FAILED]
          description: SETTLING=结算中；RETRYING=失败重试中；FAILED=终局失败，待人工介入
        takerIsYes: { type: boolean, description: taker 腿方向（true = taker 买 YES） }
        yesPriceE4: { type: integer, description: 成交 YES 价（1e-4）;payloadCorrupted=true 时为 0 }
        sizeE2: { type: integer, format: int64, description: 成交量（1e-2 份）;payloadCorrupted=true 时为 0 }
        attempts: { type: integer, description: 链上提交尝试次数 }
        lastError: { type: string, nullable: true, description: 最近一次失败原因（运维排查用） }
        chainTxDigest: { type: string, nullable: true, description: 已广播交易的链上摘要 }
        payloadCorrupted:
          type: boolean
          description: true = job 载荷损坏，价量明细已降级为 0
        createdAt: { type: string, description: job 创建时刻 }
        updatedAt: { type: string, description: 最近状态变更时刻 }

    WsHandshakeToken:
      type: object
      required: [token]
      properties:
        token: { type: string, description: 短时握手令牌 }
    ChainBindingError:
      type: object
      description: 链上绑定端点专用的错误体。**不是**统一 ErrorResponse——没有 message /
        recoverable；409 额外带 status 原态（PENDING 可重试、FAILED 不可）
      required: [error]
      properties:
        error: { type: string }
        status:
          type: string
          nullable: true
          description: 绑定行的原始状态；仅 409 带，404 为 null
    ErrorResponse:
      type: object
      description: 统一错误体。404 的固定字面量是 market_not_found（双义：投影未追上或资源不存在）；
        写接口 503 的 error 区分 command_log_unavailable / seq_unavailable /
        duplicate_in_flight
      required: [error, recoverable]
      properties:
        error: { type: string }
        message: { type: string, nullable: true }
        recoverable: { type: boolean, description: true = 可退避重试；false（如 invariant_violation）= 不可恢复 }

    PxEventCard:
      type: object
      required:
        - eventId
        - slug
        - title
        - imageUrl
        - iconUrl
        - negRisk
        - active
        - closed
        - state
        - endDateMs
        - volumeUsdE6
        - volume24hUsdE6
        - liquidityUsdE6
        - seriesId
        - topMarkets
      properties:
        eventId: { type: string, description: 事件 id }
        slug: { type: string, description: 事件 slug }
        title: { type: string, description: 事件标题 }
        imageUrl: { type: string, nullable: true, description: 封面 }
        iconUrl: { type: string, nullable: true, description: 图标 }
        negRisk: { type: boolean, description: 是否负风险事件组 }
        active: { type: boolean, description: 存在开放市场；可与 closed 同时为 true }
        closed: { type: boolean, description: 存在非开放市场；连续系列底下有历史轮时恒 true }
        state:
          type: string
          enum: [OPEN, ENDED, UPCOMING]
          description: 展示用三态。OPEN=有开放市场；ENDED=有市场但都不开放；UPCOMING=还没有市场
        endDateMs: { type: string, nullable: true, description: 截止毫秒字符串；连续系列对齐当前轮 }
        volumeUsdE6: { type: string, description: 子市场成交额汇总，E6 字符串 }
        volume24hUsdE6: { type: string, description: 子市场 24h 成交额汇总，E6 字符串 }
        liquidityUsdE6: { type: string, description: 子市场流动性汇总，E6 字符串 }
        seriesId: { type: string, nullable: true, description: 连续轮次系列 id；非连续事件为 null，前端据此选路由 }
        topMarkets:
          type: array
          description: 卡片热门市场。普通事件按 24h 成交量取前 3；连续系列只给当前轮一条
          items: { $ref: '#/components/schemas/PxMarketSummary' }
    PxEventDetail:
      type: object
      required:
        - eventId
        - slug
        - title
        - imageUrl
        - iconUrl
        - negRisk
        - active
        - closed
        - state
        - endDateMs
        - volumeUsdE6
        - volume24hUsdE6
        - liquidityUsdE6
        - description
        - startDateMs
        - seriesId
        - tags
        - markets
      properties:
        eventId: { type: string, description: 事件 id }
        slug: { type: string, description: 事件 slug }
        title: { type: string, description: 事件标题 }
        imageUrl: { type: string, nullable: true, description: 封面 }
        iconUrl: { type: string, nullable: true, description: 图标 }
        negRisk: { type: boolean, description: 是否负风险事件组 }
        active: { type: boolean, description: 存在开放市场 }
        closed: { type: boolean, description: 存在非开放市场 }
        state:
          type: string
          enum: [OPEN, ENDED, UPCOMING]
          description: 展示用三态，不要用 active/closed 拼状态
        endDateMs: { type: string, nullable: true, description: 截止毫秒字符串 }
        volumeUsdE6: { type: string, description: 子市场成交额汇总，E6 字符串 }
        volume24hUsdE6: { type: string, description: 子市场 24h 成交额汇总，E6 字符串 }
        liquidityUsdE6: { type: string, description: 子市场流动性汇总，E6 字符串 }
        description: { type: string, nullable: true, description: 事件说明 }
        startDateMs: { type: string, nullable: true, description: 开始毫秒字符串 }
        seriesId: { type: string, nullable: true, description: 连续轮次系列 id；非连续为 null }
        tags:
          type: array
          description: 事件标签（{label, slug} 对象，不是 slug 列表）
          items: { $ref: '#/components/schemas/PxEventTag' }
        markets:
          type: array
          description: 该事件下全部市场（不是卡片的 top 3）
          items: { $ref: '#/components/schemas/PxMarketSummary' }
    PxEventTag:
      type: object
      required: [label, slug]
      properties:
        label: { type: string, description: 展示文案 }
        slug: { type: string, description: 拼链接 / 过滤用 }
    PxEventsPage:
      type: object
      required: [events, nextOffset]
      properties:
        events:
          type: array
          description: 本页事件卡片
          items: { $ref: '#/components/schemas/PxEventCard' }
        nextOffset:
          type: integer
          format: int64
          nullable: true
          description: 下一页 offset;null = 没有下一页（JSON number / null，不是字符串）
    PxHistoryPoint:
      type: object
      required: [tsMs, priceE6]
      properties:
        tsMs: { type: string, description: 采样时刻，毫秒字符串 }
        priceE6: { type: string, description: 该时刻价格，E6 字符串 }
    PxHistoryResponse:
      type: object
      required: [points]
      properties:
        points:
          type: array
          items: { $ref: '#/components/schemas/PxHistoryPoint' }
    PxMarketDetail:
      type: object
      description: PxMarketSummary 平铺 conditionId / eventTitle / eventSlug
      required:
        - marketId
        - eventId
        - question
        - slug
        - groupItemTitle
        - outcomeALabel
        - outcomeBLabel
        - status
        - resolution
        - priceTickE6
        - minOrderSizeE6
        - outcomeAPriceE6
        - outcomeBPriceE6
        - volume24hUsdE6
        - volumeUsdE6
        - liquidityUsdE6
        - endDateMs
        - hasBooks
        - iconUrl
        - oneDayPriceChange
        - conditionId
        - eventTitle
        - eventSlug
      properties:
        marketId: { type: string, description: 引擎市场 id，下单用这个 }
        eventId: { type: string, description: 所属事件 }
        question: { type: string, description: 市场问题 }
        slug: { type: string, description: 市场 slug }
        groupItemTitle: { type: string, nullable: true, description: 组内短标题 }
        outcomeALabel: { type: string, description: YES 侧展示文案 }
        outcomeBLabel: { type: string, description: NO 侧展示文案 }
        status: { type: string, description: 目录侧市场状态 }
        resolution: { type: string, nullable: true, description: 裁决结果 }
        priceTickE6: { type: string, nullable: true, description: 档位，E6 字符串 }
        minOrderSizeE6: { type: string, nullable: true, description: 最小下单量，E6 字符串 }
        outcomeAPriceE6: { type: string, nullable: true, description: YES 侧最新价，E6 字符串 }
        outcomeBPriceE6: { type: string, nullable: true, description: NO 侧最新价，E6 字符串 }
        volume24hUsdE6: { type: string, nullable: true, description: 24h 成交额，E6 字符串 }
        volumeUsdE6: { type: string, nullable: true, description: 累计成交额，E6 字符串 }
        liquidityUsdE6: { type: string, nullable: true, description: 盘口流动性，E6 字符串 }
        endDateMs: { type: string, nullable: true, description: 截止时刻，毫秒字符串 }
        hasBooks: { type: boolean, description: 是否已有盘口 }
        iconUrl: { type: string, nullable: true, description: 图标 URL }
        oneDayPriceChange: { type: string, nullable: true, description: 24h 涨跌展示字符串 }
        conditionId: { type: string, nullable: true, description: 链上 condition；未绑定时可为 null }
        eventTitle: { type: string, description: 所属事件标题 }
        eventSlug: { type: string, description: 所属事件 slug }
    PxMarketSummary:
      type: object
      description: 发现层市场摘要。金额/价格/时间为 E6 或毫秒的 JSON 字符串，与交易 MarketSummary 不是同一形状
      required:
        - marketId
        - eventId
        - question
        - slug
        - groupItemTitle
        - outcomeALabel
        - outcomeBLabel
        - status
        - resolution
        - priceTickE6
        - minOrderSizeE6
        - outcomeAPriceE6
        - outcomeBPriceE6
        - volume24hUsdE6
        - volumeUsdE6
        - liquidityUsdE6
        - endDateMs
        - hasBooks
        - iconUrl
        - oneDayPriceChange
      properties:
        marketId: { type: string, description: 引擎市场 id，下单用这个 }
        eventId: { type: string, description: 所属事件 }
        question: { type: string, description: 市场问题 }
        slug: { type: string, description: 市场 slug }
        groupItemTitle: { type: string, nullable: true, description: 组内短标题；无组时可为 null }
        outcomeALabel: { type: string, description: YES 侧展示文案（不是 YES 本身） }
        outcomeBLabel: { type: string, description: NO 侧展示文案 }
        status: { type: string, description: 目录侧市场状态，常见 OPEN / RESOLVED }
        resolution: { type: string, nullable: true, description: 裁决结果 YES/NO 等；未裁决为 null 或 UNRESOLVED }
        priceTickE6: { type: string, nullable: true, description: 档位，E6 字符串 }
        minOrderSizeE6: { type: string, nullable: true, description: 最小下单量，E6 字符串 }
        outcomeAPriceE6: { type: string, nullable: true, description: YES 侧最新价，E6 字符串（600000 = 0.60） }
        outcomeBPriceE6: { type: string, nullable: true, description: NO 侧最新价，E6 字符串 }
        volume24hUsdE6: { type: string, nullable: true, description: 24h 成交额，E6 字符串 }
        volumeUsdE6: { type: string, nullable: true, description: 累计成交额，E6 字符串 }
        liquidityUsdE6: { type: string, nullable: true, description: 盘口流动性，E6 字符串 }
        endDateMs: { type: string, nullable: true, description: 截止时刻，毫秒字符串 }
        hasBooks: { type: boolean, description: 是否已有盘口 }
        iconUrl: { type: string, nullable: true, description: 图标 URL }
        oneDayPriceChange: { type: string, nullable: true, description: 24h 涨跌展示字符串（如 0.100），不是定点整数 }
    PxTag:
      type: object
      required: [tagId, label, slug, eventCount]
      properties:
        tagId: { type: string, description: 标签 id }
        label: { type: string, description: 展示文案 }
        slug: { type: string, description: 过滤 / 链接用 slug }
        eventCount: { type: integer, format: int64, description: 仍有开放市场的事件数（JSON number） }
    PxTagsResponse:
      type: object
      required: [tags]
      properties:
        tags:
          type: array
          items: { $ref: '#/components/schemas/PxTag' }
    PxUpdownResponse:
      type: object
      required: [series, seriesList, serverTimeMs, windowMs, rounds, history, spot]
      properties:
        series: { type: string, description: 当前系列 id }
        seriesList:
          type: array
          items: { type: string }
          description: 可切换的 active 系列 id
        serverTimeMs: { type: string, description: 服务端时钟，毫秒字符串 }
        windowMs: { type: string, description: 一轮窗口长度，毫秒字符串（5 分钟 = 300000） }
        rounds: { $ref: '#/components/schemas/PxUpdownRounds' }
        history:
          type: array
          description: 历史轮（不含 current/next）
          items: { $ref: '#/components/schemas/PxUpdownRound' }
        spot:
          $ref: '#/components/schemas/PxUpdownSpot'
          nullable: true
          description: 现货快照；没有则为 null
    PxUpdownRound:
      type: object
      description: 涨跌一轮 = 市场摘要字段 + 轮次窗口与锚价，扁平不嵌套
      required:
        - marketId
        - eventId
        - question
        - slug
        - groupItemTitle
        - outcomeALabel
        - outcomeBLabel
        - status
        - resolution
        - priceTickE6
        - minOrderSizeE6
        - outcomeAPriceE6
        - outcomeBPriceE6
        - volume24hUsdE6
        - volumeUsdE6
        - liquidityUsdE6
        - endDateMs
        - hasBooks
        - iconUrl
        - oneDayPriceChange
        - roundStartMs
        - roundEndMs
        - openPriceE8
        - closePriceE8
        - anchorPending
      properties:
        marketId: { type: string, description: 引擎市场 id }
        eventId: { type: string, description: 所属事件 }
        question: { type: string, description: 市场问题 }
        slug: { type: string, description: 市场 slug }
        groupItemTitle: { type: string, nullable: true, description: 组内短标题 }
        outcomeALabel: { type: string, description: YES 侧展示文案 }
        outcomeBLabel: { type: string, description: NO 侧展示文案 }
        status: { type: string, description: 目录侧市场状态 }
        resolution: { type: string, nullable: true, description: 裁决结果 }
        priceTickE6: { type: string, nullable: true, description: 档位，E6 字符串 }
        minOrderSizeE6: { type: string, nullable: true, description: 最小下单量，E6 字符串 }
        outcomeAPriceE6: { type: string, nullable: true, description: YES 侧最新价，E6 字符串 }
        outcomeBPriceE6: { type: string, nullable: true, description: NO 侧最新价，E6 字符串 }
        volume24hUsdE6: { type: string, nullable: true, description: 24h 成交额，E6 字符串 }
        volumeUsdE6: { type: string, nullable: true, description: 累计成交额，E6 字符串 }
        liquidityUsdE6: { type: string, nullable: true, description: 盘口流动性，E6 字符串 }
        endDateMs: { type: string, nullable: true, description: 截止毫秒字符串 }
        hasBooks: { type: boolean, description: 是否已有盘口 }
        iconUrl: { type: string, nullable: true, description: 图标 URL }
        oneDayPriceChange: { type: string, nullable: true, description: 24h 涨跌展示字符串 }
        roundStartMs: { type: string, description: 本轮开盘毫秒字符串 }
        roundEndMs: { type: string, description: 本轮收盘毫秒字符串 }
        openPriceE8: { type: string, nullable: true, description: 开盘锚价（Pyth e8 字符串）；就是上一轮的 closePriceE8 }
        closePriceE8: { type: string, nullable: true, description: 收盘锚价（Pyth e8 字符串） }
        anchorPending: { type: boolean, description: true = 锚价未封，必须展示待定，禁止用 0 或上一轮数冒充 }
    PxUpdownRounds:
      type: object
      required: [previous, current, next]
      properties:
        previous:
          $ref: '#/components/schemas/PxUpdownRound'
          nullable: true
          description: 上一轮；没有则为 null
        current:
          $ref: '#/components/schemas/PxUpdownRound'
          nullable: true
          description: 当前轮；没有则为 null
        next:
          $ref: '#/components/schemas/PxUpdownRound'
          nullable: true
          description: 已预开的下一轮；没有则为 null
    PxUpdownSpot:
      type: object
      required: [symbol, priceText, tsMs]
      properties:
        symbol: { type: string, description: 展示用币种，如 BTC }
        priceText: { type: string, description: 现货展示价（如 97350.00），不是定点整数 }
        tsMs: { type: string, description: 报价时刻，毫秒字符串 }
    PxAccount:
      type: object
      description: PX 账户摘要。**全部 E6 字符串**，与交易线的 E6 整数不同口径
      required: [cashE6, reservedE6, positionCostBasisE6]
      properties:
        cashE6: { type: string, description: 可用现金，E6 字符串 }
        reservedE6: { type: string, description: 挂单占用，E6 字符串 }
        positionCostBasisE6:
          type: string
          description: 持仓成本。成本读模型未建设，**恒为 "0"**，不要据此算盈亏
    PxPosition:
      type: object
      description: 一条持仓。无数据源的字段是诚实的空（零值或 null），不拿别的数冒充
      required: [marketId, outcome, quantityE6]
      properties:
        marketId: { type: string, description: 引擎市场 id，下单用这个 }
        marketSlug: { type: string, nullable: true, description: 市场 slug }
        question: { type: string, nullable: true, description: 市场问题 }
        outcome: { type: string, description: 持仓方向，A 或 B }
        outcomeLabel: { type: string, nullable: true, description: 该侧展示文案 }
        resolution: { type: string, nullable: true, description: 裁决结果；未裁决为 null }
        status: { type: string, nullable: true, description: 市场状态 }
        quantityE6: { type: string, description: 持仓数量，E6 字符串 }
        reservedE6: { type: string, description: 其中被挂单占用的数量，E6 字符串 }
        costBasisE6: { type: string, description: 持仓成本。读模型未建设，当前为零 }
        realizedPnlE6: { type: string, description: 已实现盈亏。读模型未建设，当前为零 }
        avgEntryE6: { type: string, description: 平均开仓价。读模型未建设，当前为零 }
        lastPriceE6:
          type: string
          nullable: true
          description: 最新标记价，E6 字符串；不可得时为 null，**不要当 0**
        priceState: { type: string, nullable: true, description: 标记价可信度；陈旧行情由调用方据此自行判定 }
        markUpdatedAtMs: { type: string, nullable: true, description: 标记价时刻，毫秒字符串 }
        midPriceE6: { type: string, nullable: true, description: 盘口中价，E6 字符串 }
        marketValueE6: { type: string, nullable: true, description: 市值，E6 字符串 }
        unrealizedPnlE6: { type: string, nullable: true, description: 未实现盈亏，E6 字符串 }
        roeBps: { type: string, nullable: true, description: 回报率，基点 }
        initialMarginE6: { type: string, description: 初始保证金，E6 字符串；无融资时为零 }
        borrowedE6: { type: string, description: 该仓位占用的借入本金，E6 字符串；无融资时为零 }
        liquidationPriceE6: { type: string, nullable: true, description: 强平价，E6 字符串；无融资时为 null }
        bankruptcyPriceE6: { type: string, nullable: true, description: 破产价，E6 字符串；无融资时为 null }
        distanceToLiqBps: { type: string, nullable: true, description: 距强平价的距离，基点 }
        openedAtMs: { type: string, nullable: true, description: 建仓时刻，毫秒字符串 }
    PxPositionsResponse:
      type: object
      required: [positions]
      properties:
        positions:
          type: array
          description: 全部持仓，不分页
          items:
            $ref: '#/components/schemas/PxPosition'
    PxOrder:
      type: object
      description: 一条订单。价格数量为 E6 字符串，outcome 是 A/B——与交易线的 E4/E2 + YES/NO 不同
      required: [orderId, marketId, outcome, side, status]
      properties:
        orderId: { type: string, description: 订单 id }
        marketId: { type: string, description: 引擎市场 id }
        marketSlug: { type: string, nullable: true, description: 市场 slug }
        question: { type: string, nullable: true, description: 市场问题 }
        outcome: { type: string, description: 方向，A 或 B }
        outcomeLabel: { type: string, nullable: true, description: 该侧展示文案 }
        side: { type: string, description: 买卖方向 }
        orderType: { type: string, description: 订单类型，LIMIT 或 MARKET }
        tif: { type: string, nullable: true, description: 有效期策略 }
        limitPriceE6: { type: string, nullable: true, description: 限价，E6 字符串 }
        quantityE6: { type: string, description: 下单数量，E6 字符串 }
        filledQuantityE6: { type: string, description: 已成交数量，E6 字符串 }
        status: { type: string, description: 订单状态 }
        createdAt:
          type: string
          nullable: true
          description: 订单行**最后更新时间**，不是下单时间——读模型没有下单时间列
    PxOrdersResponse:
      type: object
      required: [orders]
      properties:
        orders:
          type: array
          description: 订单列表，最新在前
          items:
            $ref: '#/components/schemas/PxOrder'
    PxFill:
      type: object
      description: 一条成交。**一单一笔**，由已成交订单推导，不是逐笔成交明细
      required: [fillId, side, priceE6, quantityE6]
      properties:
        fillId: { type: string, description: 成交 id }
        marketSlug: { type: string, nullable: true, description: 市场 slug }
        question: { type: string, nullable: true, description: 市场问题 }
        outcomeLabel: { type: string, nullable: true, description: 该侧展示文案 }
        side: { type: string, description: 买卖方向 }
        priceE6: { type: string, description: 成交价，E6 字符串；分多次成交时是该单的汇总口径 }
        quantityE6: { type: string, description: 成交数量，E6 字符串 }
        quoteE6: { type: string, description: 成交金额，E6 字符串 }
        liquidity:
          type: string
          description: 流动性角色。maker/taker 区分未接入，**恒为 "USER"**，不要据此算返佣
        createdAt: { type: string, nullable: true, description: 成交时刻 }
    PxFillsResponse:
      type: object
      required: [fills]
      properties:
        fills:
          type: array
          description: 成交列表，最新在前
          items:
            $ref: '#/components/schemas/PxFill'
    PxLedgerEntry:
      type: object
      description: 一条资金流水。这是对账的权威口径——持仓与订单是投影，流水是账本
      required: [entryId, kind, amountE6, balanceAfterE6]
      properties:
        entryId: { type: string, description: 流水 id }
        kind: { type: string, description: 变更类型；决定 refId 的值域 }
        amountE6: { type: string, description: 变更金额，E6 字符串；出账为负 }
        balanceAfterE6: { type: string, description: 该条生效后的余额，E6 字符串 }
        refId: { type: string, nullable: true, description: 引发该条的对象 id，按 kind 决定值域 }
        createdAt: { type: string, nullable: true, description: 发生时刻 }
    PxLedgerResponse:
      type: object
      required: [entries]
      properties:
        entries:
          type: array
          description: 资金流水，最新在前
          items:
            $ref: '#/components/schemas/PxLedgerEntry'
    PxPositionHistoryItem:
      type: object
      description: 一条平仓记录。读模型未建设，当前列表恒空——形状已按最终契约固定
      required: [historyId]
      properties:
        historyId: { type: string, description: 记录 id }
        marketSlug: { type: string, nullable: true, description: 市场 slug }
        question: { type: string, nullable: true, description: 市场问题 }
        outcome: { type: string, nullable: true, description: 方向，A 或 B }
        outcomeLabel: { type: string, nullable: true, description: 该侧展示文案 }
        quantityE6: { type: string, nullable: true, description: 平仓数量，E6 字符串 }
        avgEntryE6: { type: string, nullable: true, description: 平均开仓价，E6 字符串 }
        avgExitE6: { type: string, nullable: true, description: 平均平仓价，E6 字符串 }
        initialMarginE6: { type: string, nullable: true, description: 初始保证金，E6 字符串 }
        borrowedE6: { type: string, nullable: true, description: 借入本金，E6 字符串 }
        realizedPnlE6: { type: string, nullable: true, description: 已实现盈亏，E6 字符串 }
        closeReason: { type: string, nullable: true, description: 平仓原因 }
        resolution: { type: string, nullable: true, description: 裁决结果 }
    PxPositionHistoryResponse:
      type: object
      required: [history]
      properties:
        history:
          type: array
          description: 平仓历史。**当前恒为空数组**，不要读成「没有平仓过」
          items:
            $ref: '#/components/schemas/PxPositionHistoryItem'
    PxCredit:
      type: object
      description: 信用状态。healthBps 低于 maintenanceBps 会自动触发强平
      required: [debtE6, equityE6, grossE6, healthy, borrowAprBps, maxBorrowE6, lendDepositE6, maintenanceBps, initialMarginBps]
      properties:
        debtE6: { type: string, description: 含息债务，E6 字符串 }
        equityE6: { type: string, description: 账户净值，E6 字符串 }
        grossE6: { type: string, description: 总敞口，E6 字符串 }
        healthBps:
          type: string
          nullable: true
          description: 健康度，基点。**无债务时为 null**（健康度无定义），不要当 0
        healthy: { type: boolean, description: 是否在维持线之上；无债务时为 true }
        borrowAprBps: { type: string, description: 借入年化利率，基点 }
        maxBorrowE6: { type: string, description: 当前可借上限，E6 字符串 }
        lendDepositE6: { type: string, description: 已出借本金，E6 字符串 }
        maintenanceBps: { type: string, description: 维持保证金率，基点；healthBps 低于它触发强平 }
        initialMarginBps: { type: string, description: 初始保证金率，基点 }
    PxAmountRequest:
      type: object
      description: 借 / 还 / 出借 / 提取共用的请求体
      required: [amountE6]
      properties:
        amountE6: { type: string, description: 金额，E6 字符串；必须为正数 }
    PxCreditWriteError:
      type: object
      description: 信用写操作的失败体。**不是**统一错误体——没有 recoverable，字段是 code 而非 error
      required: [code, message]
      properties:
        code:
          type: string
          description: 失败码，取值见各端点说明
          enum: [INVALID_AMOUNT, EXCEEDS_MAX_BORROW, EXCEEDS_DEBT, INSUFFICIENT_BALANCE, EXCEEDS_LEND_DEPOSIT]
        message: { type: string, description: 失败原因 }
    PxLiquidation:
      type: object
      description: 一条强平记录。已发生的事实，不是待处理事项
      required: [liquidationId, outcome]
      properties:
        liquidationId: { type: string, description: 强平记录 id }
        question: { type: string, nullable: true, description: 市场问题 }
        marketSlug: { type: string, nullable: true, description: 市场 slug }
        outcome: { type: string, description: 被处置的方向，A 或 B }
        seizedQuantityE6: { type: string, description: 被处置的数量，E6 字符串 }
        proceedsE6: { type: string, description: 处置所得，E6 字符串 }
        debtRepaidE6: { type: string, description: 其中用于偿债的部分，E6 字符串 }
        penaltyE6: { type: string, description: 强平罚金，E6 字符串 }
        badDebtE6:
          type: string
          description: 坏账。非零表示处置所得不足以覆盖债务，差额由保险池承担
        createdAt: { type: string, nullable: true, description: 强平发生时刻 }
    PxLiquidationsResponse:
      type: object
      required: [liquidations]
      properties:
        liquidations:
          type: array
          description: 强平记录，最新在前
          items:
            $ref: '#/components/schemas/PxLiquidation'
    PxCryptoPoint:
      type: object
      required: [tsMs, priceText]
      properties:
        tsMs: { type: string, description: 采样时刻，毫秒字符串 }
        priceText: { type: string, description: 现货展示价（如 97350.00），不是定点整数 }
    PxCryptoHistoryResponse:
      type: object
      required: [points]
      properties:
        points:
          type: array
          description: 现货价采样点。数据源未建设，**当前恒为空数组**
          items:
            $ref: '#/components/schemas/PxCryptoPoint'
    PxAiPick:
      type: object
      required: [eventId, slug, title, heat]
      properties:
        eventId: { type: string, description: 事件 id }
        slug: { type: string, description: 事件 slug }
        title: { type: string, description: 事件标题 }
        heat: { type: integer, description: 热度分。**JSON number，不是字符串** }
        reason: { type: string, nullable: true, description: 推荐理由 }
    PxAiPicksResponse:
      type: object
      description: 推荐响应。enabled=false 是有意的降级契约，不是故障
      required: [enabled, picks]
      properties:
        enabled: { type: boolean, description: 功能是否开启。数据源未建设，**当前恒为 false** }
        picks:
          type: array
          description: 推荐列表；关闭态为空数组
          items:
            $ref: '#/components/schemas/PxAiPick'
    PxAiAskRequest:
      type: object
      required: [query]
      properties:
        query: { type: string, maxLength: 200, description: 自然语言问题，非空且不超过 200 字符 }
    PxAiAskPick:
      type: object
      required: [eventId, slug, title]
      properties:
        eventId: { type: string, description: 事件 id }
        slug: { type: string, description: 事件 slug }
        title: { type: string, description: 事件标题 }
        endDateMs: { type: string, nullable: true, description: 截止时刻，毫秒字符串 }
        pctText: { type: string, nullable: true, description: 概率展示文本 }
        action: { type: string, nullable: true, description: 建议动作 }
        sizeHint: { type: string, nullable: true, description: 建议仓位提示 }
    PxAiAskResponse:
      type: object
      description: 顾问回复。两种降级都走 200——enabled=false 是未开启，advice=null 是本次失败
      required: [enabled, picks]
      properties:
        enabled: { type: boolean, description: 功能是否开启 }
        advice:
          type: string
          nullable: true
          description: 顾问回复。**null 表示本次调用或解析失败**，不是「没有建议」
        picks:
          type: array
          description: 相关事件；可为空数组
          items:
            $ref: '#/components/schemas/PxAiAskPick'
