openapi: 3.0.3
info:
  title: Exchange Public API v1
  version: 1.0.0
  description: |
    Публичный торговый API биржи: рыночные данные без ключа, приватные ручки — по
    паре `api_key`/`api_secret` с HMAC-подписью запроса.

    Общие правила контура:

    - **Время** везде в миллисекундах epoch, числом.
    - **Деньги и количества** — строками (JSON-число на 8 знаках теряет точность),
      и приниматься от клиента они тоже обязаны строками.
    - **Ошибки** — единый конверт `{"code","message"}`, у 422 добавляется `errors`
      в формате Laravel. `code` машиночитаем и стабилен, `message` — для человека.
    - **Пагинация** одна на весь контур: курсор `from_id` (строго больше) плюс
      `limit` (1..1000, по умолчанию 100). Offset-а нет. Признак последней
      страницы — длина ответа меньше запрошенного `limit`.
    - **Тихого обрезания не бывает:** `limit` вне границ — 422, а не молча 1000.
    - **CORS выключен намеренно.** Контур server-to-server; секрет, попавший в
      браузер, утекает вместе со страницей.

    Подпись, примеры на bash/Python/Node, таблица кодов ошибок и острые углы
    (идемпотентность, защита от повтора, условия вывода) — в
    [руководстве интегратора](/api-docs).
  contact:
    name: Exchange API support
    email: support@amlkyc.tech

servers:
  - url: https://{host}/api/v1
    description: Боевой контур (домен любого из брендов)
    variables:
      host:
        default: wallex.zone
        description: Домен биржи
  - url: http://localhost/api/v1
    description: Локальная разработка

tags:
  - name: System
    description: Служебные ручки контура
  - name: Market Data
    description: Публичные рыночные данные, ключ не нужен
  - name: Account
    description: Чтение своего аккаунта, скоуп `read`
  - name: Trading
    description: Постановка и отмена ордеров, скоуп `trade`
  - name: Withdrawals
    description: Вывод средств, скоуп `withdraw`

# По умолчанию ручка требует подписанного запроса; публичные явно объявляют `security: []`.
security:
  - ApiKeyAuth: []
    ApiTimestamp: []
    ApiSignature: []

paths:
  /time:
    get:
      summary: Серверное время
      description: |
        Клиенту нужно до первой подписи: метка времени вне окна `recv_window`
        (по умолчанию 5000 мс) отбивается как `stale_timestamp`.
      operationId: getServerTime
      tags: [Market Data]
      security: []
      responses:
        '200':
          description: Время сервера
          content:
            application/json:
              schema:
                type: object
                properties:
                  server_time:
                    type: integer
                    format: int64
                    description: Миллисекунды epoch
                    example: 1757500000123
        '429':
          $ref: '#/components/responses/RateLimited'

  /symbols:
    get:
      summary: Спецификации торговых пар
      description: |
        Рынки со статусом `delisted` контур не показывает и данных по ним не
        отдаёт вовсе (404 `unknown_symbol` на всех ручках). `paused` виден со
        своим статусом: рынок, просто исчезнувший из списка, клиент читает как
        ошибку интеграции.
      operationId: getSymbols
      tags: [Market Data]
      security: []
      responses:
        '200':
          description: Список рынков
          content:
            application/json:
              schema:
                type: object
                properties:
                  symbols:
                    type: array
                    items:
                      $ref: '#/components/schemas/Symbol'
        '429':
          $ref: '#/components/responses/RateLimited'

  /ticker:
    get:
      summary: 24-часовой тикер
      description: |
        Ответ всегда список, даже когда запрошен один символ: форма, зависящая от
        наличия параметра, ломает типизированных клиентов. Без `symbol`
        возвращаются все рынки.
      operationId: getTicker
      tags: [Market Data]
      security: []
      parameters:
        - $ref: '#/components/parameters/OptionalSymbol'
      responses:
        '200':
          description: Тикеры
          content:
            application/json:
              schema:
                type: object
                properties:
                  tickers:
                    type: array
                    items:
                      $ref: '#/components/schemas/Ticker'
        '404':
          $ref: '#/components/responses/UnknownSymbol'
        '429':
          $ref: '#/components/responses/RateLimited'

  /depth:
    get:
      summary: Стакан
      operationId: getDepth
      tags: [Market Data]
      security: []
      parameters:
        - $ref: '#/components/parameters/RequiredSymbol'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Снимок книги заявок
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Depth'
        '404':
          $ref: '#/components/responses/UnknownSymbol'
        '422':
          $ref: '#/components/responses/InvalidParameter'
        '429':
          $ref: '#/components/responses/RateLimited'

  /trades:
    get:
      summary: Лента сделок рынка
      description: Свежие сверху. Это сделки всего рынка, а не ваши — свои лежат в `/my-trades`.
      operationId: getMarketTrades
      tags: [Market Data]
      security: []
      parameters:
        - $ref: '#/components/parameters/RequiredSymbol'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Последние сделки
          content:
            application/json:
              schema:
                type: object
                properties:
                  symbol:
                    type: string
                    example: BTC_USDT
                  trades:
                    type: array
                    items:
                      $ref: '#/components/schemas/MarketTrade'
        '404':
          $ref: '#/components/responses/UnknownSymbol'
        '422':
          $ref: '#/components/responses/InvalidParameter'
        '429':
          $ref: '#/components/responses/RateLimited'

  /klines:
    get:
      summary: Свечи
      description: От старых к новым — в порядке, который ждёт любой построитель графика.
      operationId: getKlines
      tags: [Market Data]
      security: []
      parameters:
        - $ref: '#/components/parameters/RequiredSymbol'
        - name: interval
          in: query
          required: false
          description: Размер свечи. Неизвестное значение — 422 `invalid_parameter`.
          schema:
            type: string
            enum: ['1m', '5m', '1h', '1d']
            default: '1m'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Свечи
          content:
            application/json:
              schema:
                type: object
                properties:
                  symbol:
                    type: string
                    example: BTC_USDT
                  interval:
                    type: string
                    example: 1m
                  klines:
                    type: array
                    items:
                      $ref: '#/components/schemas/Kline'
        '404':
          $ref: '#/components/responses/UnknownSymbol'
        '422':
          $ref: '#/components/responses/InvalidParameter'
        '429':
          $ref: '#/components/responses/RateLimited'

  /funding:
    get:
      summary: Ставка финансирования (только перп)
      description: |
        Для спотового рынка — 422 `invalid_parameter`: нулевая ставка означала бы,
        что funding здесь есть и он нулевой.
      operationId: getFunding
      tags: [Market Data]
      security: []
      parameters:
        - $ref: '#/components/parameters/RequiredSymbol'
      responses:
        '200':
          description: Текущая ставка
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Funding'
        '404':
          $ref: '#/components/responses/UnknownSymbol'
        '422':
          $ref: '#/components/responses/InvalidParameter'
        '429':
          $ref: '#/components/responses/RateLimited'

  /ping:
    parameters:
      - $ref: '#/components/parameters/RecvWindow'
    get:
      summary: Проверка ключа и подписи
      description: |
        Отвечает только на корректно подписанный запрос — на ней удобно отладить
        свою реализацию подписи, не рискуя ордером. Скоупа не требует, но ведро
        лимитера общее с чтением (`api-v1-read`).
      operationId: ping
      tags: [System]
      responses:
        '200':
          description: Ключ жив, подпись верна
          content:
            application/json:
              schema:
                type: object
                properties:
                  user_id:
                    type: integer
                    example: 42
                  key:
                    type: string
                    example: ek_9f1c0f5e0a4b4f2f9d3a5c7e1b2d4f60
                  scopes:
                    type: array
                    items:
                      type: string
                      enum: [read, trade, withdraw]
                  server_time:
                    type: integer
                    format: int64
                    example: 1757500000123
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /account:
    parameters:
      - $ref: '#/components/parameters/RecvWindow'
    get:
      summary: Профиль ключа и его владельца
      description: |
        `can_trade`/`can_withdraw` — строго про КЛЮЧ (его скоупы), а гейты
        аккаунта отдаются рядом отдельными полями. Иначе `can_withdraw: false`
        не отличить: «ключу не дали право» или «аккаунт не готов».
      operationId: getAccount
      tags: [Account]
      responses:
        '200':
          description: Профиль
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Account'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /account/balances:
    parameters:
      - $ref: '#/components/parameters/RecvWindow'
    get:
      summary: Балансы по активам и суб-счетам
      description: |
        Демо и реальные средства не смешиваются: демо-аккаунт видит только
        `scope: demo`, обычный — только реальные суб-счета. Режим аккаунта
        лежит в `GET /account` (`is_demo`).
      operationId: getBalances
      tags: [Account]
      responses:
        '200':
          description: Балансы
          content:
            application/json:
              schema:
                type: object
                properties:
                  balances:
                    type: array
                    items:
                      $ref: '#/components/schemas/Balance'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /account/positions:
    parameters:
      - $ref: '#/components/parameters/RecvWindow'
    get:
      summary: Открытые перп-позиции
      description: Позиции нулевого размера не возвращаются — это след закрытия, а не позиция.
      operationId: getPositions
      tags: [Account]
      responses:
        '200':
          description: Позиции
          content:
            application/json:
              schema:
                type: object
                properties:
                  positions:
                    type: array
                    items:
                      $ref: '#/components/schemas/Position'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /orders:
    parameters:
      - $ref: '#/components/parameters/RecvWindow'
    get:
      summary: История ордеров
      description: |
        Ордер по делистнутой паре остаётся в истории и отдаётся без фильтра, но
        отфильтровать по такому символу нельзя: `symbol` делистнутого рынка —
        404 `unknown_symbol`.
      operationId: getOrders
      tags: [Account]
      parameters:
        - $ref: '#/components/parameters/OptionalSymbol'
        - name: status
          in: query
          required: false
          description: |
            Фильтр по статусу. Регистр на входе прощается, неизвестное значение —
            422 `invalid_parameter` со списком допустимых.
          schema:
            $ref: '#/components/schemas/OrderStatus'
        - $ref: '#/components/parameters/FromId'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Страница истории
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/UnknownSymbol'
        '422':
          $ref: '#/components/responses/InvalidParameter'
        '429':
          $ref: '#/components/responses/RateLimited'
    post:
      summary: Разместить ордер
      description: |
        **Идемпотентность.** Повтор с уже использованным `client_order_id`
        возвращает тот же ордер и **200** вместо **201** — это единственный
        способ отличить «создал» от «уже было», не добавляя в тело поля,
        которого нет у остальных ручек. Пространство `client_order_id` —
        ваше: чужой идентификатор вашего ордера не вернёт.

        **`post_only` синхронно не решается.** Скрестившийся с книгой ордер
        уходит в `pending` и становится `rejected` позже, уже в движке: в ответе
        на `POST` этого не видно, статус надо опросить.

        Отказ домена приезжает 422 с машиночитаемым `code` (`insufficient_funds`,
        `tick_lot_mismatch`, `notional_too_small`, …); пауза торгов — 503
        `trading_paused`, единственный отказ, который пройдёт сам.
      operationId: placeOrder
      tags: [Trading]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlaceOrderRequest'
      responses:
        '201':
          description: Ордер создан
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderEnvelope'
        '200':
          description: Повтор по `client_order_id` — вернулся ранее созданный ордер
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/UnknownSymbol'
        '422':
          description: Ошибка тела запроса или отказ торгового домена
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              examples:
                insufficientFunds:
                  value:
                    code: insufficient_funds
                    message: Insufficient balance to place this order.
                    errors:
                      quantity: [Insufficient balance to place this order.]
                badBody:
                  value:
                    code: invalid_parameter
                    message: The request body did not pass validation.
                    errors:
                      quantity: [The quantity must be sent as a JSON string (e.g. "0.015") to avoid float rounding.]
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          description: Торги на паузе — повторить позже
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              example:
                code: trading_paused
                message: Trading is paused for maintenance.
                errors:
                  symbol: [Trading is paused for maintenance.]
    delete:
      summary: Отменить все активные ордера по символу
      description: |
        `symbol` обязателен. Отсутствие параметра — 422, а не «снять всё»: цена
        трактовки опечатки как команды слишком высока. Ответ содержит списки
        `canceled` и `failed`, а не счётчик: клиенту нужно знать, что именно
        осталось висеть.
      operationId: cancelOrders
      tags: [Trading]
      parameters:
        - $ref: '#/components/parameters/RequiredSymbol'
      responses:
        '200':
          description: Результат массовой отмены
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CancelAllResult'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/UnknownSymbol'
        '422':
          $ref: '#/components/responses/InvalidParameter'
        '429':
          $ref: '#/components/responses/RateLimited'

  /orders/open:
    parameters:
      - $ref: '#/components/parameters/RecvWindow'
    get:
      summary: Активные ордера
      description: |
        `pending`, `open` и `partially_filled`. Пагинация та же, что у истории:
        маркетмейкер с тысячей ордеров иначе получил бы молча обрезанный список.
      operationId: getOpenOrders
      tags: [Account]
      parameters:
        - $ref: '#/components/parameters/OptionalSymbol'
        - $ref: '#/components/parameters/FromId'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Активные ордера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/UnknownSymbol'
        '422':
          $ref: '#/components/responses/InvalidParameter'
        '429':
          $ref: '#/components/responses/RateLimited'

  /orders/{id}:
    parameters:
      - $ref: '#/components/parameters/OrderId'
      - $ref: '#/components/parameters/RecvWindow'
    get:
      summary: Один ордер
      description: |
        Чужой, несуществующий и нечисловой `id` отвечают одинаково — 404
        `order_not_found`: разный ответ подтверждал бы существование ордера и
        давал бы перебором карту чужих идентификаторов.
      operationId: getOrder
      tags: [Account]
      responses:
        '200':
          description: Ордер
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/OrderNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
    delete:
      summary: Отменить ордер
      description: |
        Идемпотентна: уже отменённый или исполненный ордер — это 200 с текущим
        состоянием, а не 404 и не 409. Признак «делать больше нечего» — поле
        `status` в ответе. Отмена ордера, ушедшего в движок, асинхронна: ответ
        может вернуть прежний статус.
      operationId: cancelOrder
      tags: [Trading]
      responses:
        '200':
          description: Текущее состояние ордера
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/OrderNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'

  /my-trades:
    parameters:
      - $ref: '#/components/parameters/RecvWindow'
    get:
      summary: Свои исполнения
      description: |
        **Здесь только исполнения ордеров.** Ликвидации и списания/начисления
        funding ордера не имеют и в эту выдачу не попадают вовсе — перп-PnL,
        посчитанный по одним сделкам, разойдётся с балансом. Их движение видно
        в леджере и в `realized_pnl` позиции.
      operationId: getMyTrades
      tags: [Account]
      parameters:
        - $ref: '#/components/parameters/OptionalSymbol'
        - $ref: '#/components/parameters/FromId'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Страница исполнений
          content:
            application/json:
              schema:
                type: object
                properties:
                  trades:
                    type: array
                    items:
                      $ref: '#/components/schemas/Fill'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/UnknownSymbol'
        '422':
          $ref: '#/components/responses/InvalidParameter'
        '429':
          $ref: '#/components/responses/RateLimited'

  /deposits:
    parameters:
      - $ref: '#/components/parameters/RecvWindow'
    get:
      summary: История депозитов
      operationId: getDeposits
      tags: [Account]
      parameters:
        - $ref: '#/components/parameters/FromId'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Страница депозитов
          content:
            application/json:
              schema:
                type: object
                properties:
                  deposits:
                    type: array
                    items:
                      $ref: '#/components/schemas/Deposit'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/InvalidParameter'
        '429':
          $ref: '#/components/responses/RateLimited'

  /withdrawals:
    parameters:
      - $ref: '#/components/parameters/RecvWindow'
    get:
      summary: История выводов
      description: Скоуп `read` (не `withdraw`). Банковские реквизиты наружу не отдаются.
      operationId: getWithdrawals
      tags: [Account]
      parameters:
        - $ref: '#/components/parameters/FromId'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Страница выводов
          content:
            application/json:
              schema:
                type: object
                properties:
                  withdrawals:
                    type: array
                    items:
                      $ref: '#/components/schemas/Withdrawal'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/InvalidParameter'
        '429':
          $ref: '#/components/responses/RateLimited'
    post:
      summary: Вывести средства в блокчейн-сеть
      description: |
        Скоуп `withdraw`. TOTP бот ввести не может, поэтому второй фактор
        ЗАМЕНЁН четырьмя условиями — все проверяются до любого движения денег:

        1. у ключа непустой `ip_allowlist` (иначе 403 `ip_allowlist_required`);
        2. у аккаунта включена 2FA (иначе 403 `two_factor_required`);
        3. адрес есть в адресной книге ЛК для этой пары актив+сеть
           (иначе 403 `address_not_whitelisted`);
        4. адрес пережил охлаждение в 24 часа с момента добавления
           (иначе 403 `address_in_cooldown`).

        Плюс общие гейты: пройденный KYC (403 `kyc_required`), незаблокированный
        аккаунт (403 `account_blocked`) и запрет для демо (422
        `demo_withdraw_forbidden`). Банковского вывода в контуре нет: у реквизитов
        нет адреса, а значит и заменять второй фактор нечем.

        В заявку уходит написание адреса из адресной книги, а не из запроса.
      operationId: createWithdrawal
      tags: [Withdrawals]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWithdrawalRequest'
      responses:
        '201':
          description: Заявка создана
          content:
            application/json:
              schema:
                type: object
                properties:
                  withdrawal:
                    $ref: '#/components/schemas/Withdrawal'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Не выполнено одно из условий вывода
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                ipAllowlistRequired:
                  value:
                    code: ip_allowlist_required
                    message: Withdrawals require an API key restricted to an IP allowlist. Add one in your account settings.
                addressNotWhitelisted:
                  value:
                    code: address_not_whitelisted
                    message: This address is not in your withdrawal address book for this asset and network.
                addressInCooldown:
                  value:
                    code: address_in_cooldown
                    message: This address is still in its cooldown period and becomes usable at 2026-09-12T10:00:00+00:00.
                kycRequired:
                  value:
                    code: kyc_required
                    message: Withdrawals require identity verification.
        '404':
          description: Актива с таким символом не существует
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: unknown_asset
                message: Unknown asset `XYZ`.
        '422':
          description: Ошибка тела запроса, нехватка средств, демо-аккаунт или невыводимый актив
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationError'
              examples:
                insufficientFunds:
                  value:
                    code: insufficient_funds
                    message: Not enough available balance for this withdrawal.
                assetNotWithdrawable:
                  value:
                    code: asset_not_withdrawable
                    message: Asset `BYN` cannot be withdrawn to a blockchain network.
                demoForbidden:
                  value:
                    code: demo_withdraw_forbidden
                    message: Demo accounts cannot withdraw.
        '429':
          $ref: '#/components/responses/RateLimited'

components:

  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY
      description: Публичный идентификатор ключа (`ek_…`), выданный в ЛК.
    ApiTimestamp:
      type: apiKey
      in: header
      name: X-API-TIMESTAMP
      description: |
        Миллисекунды epoch. Окно двустороннее: метка из будущего отбивается так
        же, как из прошлого. Метка обязана быть строго монотонной — два
        одинаковых запроса с одной меткой дают одну подпись, и второй придёт
        как `replayed_request`.
    ApiSignature:
      type: apiKey
      in: header
      name: X-API-SIGNATURE
      description: |
        `lowercase(hex(hmac_sha256(api_secret, canonical)))`, где
        `canonical = TIMESTAMP + "\n" + METHOD + "\n" + PATH + "\n" + QUERY + "\n" + BODY`.
        `PATH` — с ведущим слэшем и без хоста (`/api/v1/orders`), `QUERY` — сырая
        строка без `?` и БЕЗ пересортировки, `BODY` — сырое тело как есть.
        Подробности и примеры — в [руководстве интегратора](/api-docs).

  parameters:
    RecvWindow:
      name: X-API-RECV-WINDOW
      in: header
      required: false
      description: |
        Окно приёма подписи в миллисекундах. По умолчанию 5000, максимум 60000.
        Ноль, не число или значение сверх максимума — 401 `bad_recv_window`.
      schema:
        type: integer
        minimum: 1
        maximum: 60000
        default: 5000
    RequiredSymbol:
      name: symbol
      in: query
      required: true
      description: Символ рынка. Незнакомый (в том числе делистнутый) — 404 `unknown_symbol`.
      schema:
        type: string
        example: BTC_USDT
    OptionalSymbol:
      name: symbol
      in: query
      required: false
      description: |
        Фильтр по рынку. Пустое значение равно отсутствию параметра, незнакомый
        символ — 404 `unknown_symbol` (а не пустой список: он прячет опечатку).
      schema:
        type: string
        example: BTC_USDT
    Limit:
      name: limit
      in: query
      required: false
      description: Размер выборки. Вне границ — 422 `invalid_parameter`, тихого обрезания нет.
      schema:
        type: integer
        minimum: 1
        maximum: 1000
        default: 100
    FromId:
      name: from_id
      in: query
      required: false
      description: |
        Курсор: возвращаются записи со СТРОГО большим `id`, по возрастанию.
        Обычно это `id` последней записи, которая у вас уже есть.
      schema:
        type: integer
        minimum: 0
        example: 0
    OrderId:
      name: id
      in: path
      required: true
      description: Идентификатор ордера на бирже (не `client_order_id`).
      schema:
        type: integer
        example: 1024

  responses:
    Unauthorized:
      description: |
        Ключ или подпись не приняты. `code`: `missing_credentials`, `invalid_key`,
        `key_revoked`, `key_expired`, `invalid_signature`, `stale_timestamp`,
        `replayed_request`, `bad_recv_window`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: invalid_signature
            message: Request signature does not match.
    Forbidden:
      description: |
        Ключ верен, но действие запрещено. `code`: `insufficient_scope`,
        `ip_not_allowed`, `account_blocked`, `kyc_required` и коды условий вывода.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: insufficient_scope
            message: 'This API key is missing a required scope: trade.'
    UnknownSymbol:
      description: Рынок неизвестен контуру (нет такого символа либо он делистнут)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: unknown_symbol
            message: Unknown symbol `BTC_USD`. See GET /api/v1/symbols for the list.
    OrderNotFound:
      description: Ордера нет у этого аккаунта (чужой, несуществующий или нечисловой id)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: order_not_found
            message: No such order for this account.
    InvalidParameter:
      description: Параметр запроса не принят
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: invalid_parameter
            message: Query parameter `limit` must be between 1 and 1000.
    RateLimited:
      description: |
        Превышен лимит. Отдельное ведро считает НЕУДАЧНЫЕ аутентификации по IP —
        оно тоже отвечает `rate_limit_exceeded`.
      headers:
        Retry-After:
          description: Через сколько секунд повторять
          schema:
            type: integer
        X-RateLimit-Limit:
          description: Размер ведра
          schema:
            type: integer
        X-RateLimit-Remaining:
          description: Остаток в ведре
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: rate_limit_exceeded
            message: Too many requests. Retry after the number of seconds in the Retry-After header.

  schemas:

    Error:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Машиночитаемый код ошибки, стабильная часть контракта
          example: unknown_symbol
        message:
          type: string
          description: Текст для человека; на него нельзя опираться программно
          example: Unknown symbol `BTC_USD`. See GET /api/v1/symbols for the list.

    ValidationError:
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          properties:
            errors:
              type: object
              description: Поле → список сообщений (формат Laravel)
              additionalProperties:
                type: array
                items:
                  type: string

    OrderStatus:
      type: string
      enum: [pending, open, partially_filled, filled, canceled, rejected]
      description: |
        `pending` — принят и отправлен в движок; `open` — лежит в книге;
        `rejected` — движок отказал (например, `post_only`, скрестившийся с книгой).

    Symbol:
      type: object
      properties:
        symbol:
          type: string
          example: BTC_USDT
        base_asset:
          type: string
          example: BTC
        quote_asset:
          type: string
          example: USDT
        base_scale:
          type: integer
          description: Число знаков после запятой у базового актива
          example: 8
        quote_scale:
          type: integer
          example: 2
        market_type:
          type: string
          enum: [spot, perp]
        status:
          type: string
          enum: [active, paused]
          description: Делистнутых рынков контур не показывает вовсе
        tick_size:
          type: string
          example: '0.01'
        lot_size:
          type: string
          example: '0.00001'
        min_notional:
          type: string
          example: '10'
        max_notional:
          type: string
          nullable: true
          example: '100000'
        maker_fee:
          type: string
          description: Доля, а не проценты
          example: '0.001'
        taker_fee:
          type: string
          example: '0.002'
        max_leverage:
          type: integer
          nullable: true
          example: 20

    Ticker:
      type: object
      properties:
        symbol:
          type: string
          example: BTC_USDT
        last:
          type: string
          nullable: true
          example: '64150.10'
        open:
          type: string
          nullable: true
        high:
          type: string
          nullable: true
        low:
          type: string
          nullable: true
        change:
          type: string
          nullable: true
          description: Изменение цены за 24 часа в котируемом активе
        volume:
          type: string
          description: Объём в базовом активе за 24 часа
          example: '12.34567890'
        quote_volume:
          type: string
          example: '791234.56'
        trade_count:
          type: integer
          example: 3120
        time:
          type: integer
          format: int64
          example: 1757500000123

    Depth:
      type: object
      properties:
        symbol:
          type: string
          example: BTC_USDT
        sequence:
          type: integer
          format: int64
          description: Номер снимка книги; 0, если снимка ещё нет
          example: 918273
        time:
          type: integer
          format: int64
        bids:
          type: array
          description: Уровни покупок, позиционные пары ["цена","количество"]
          items:
            $ref: '#/components/schemas/DepthLevel'
        asks:
          type: array
          items:
            $ref: '#/components/schemas/DepthLevel'

    DepthLevel:
      type: array
      minItems: 2
      maxItems: 2
      items:
        type: string
      example: ['64150.10', '0.51000000']

    MarketTrade:
      type: object
      properties:
        id:
          type: integer
          format: int64
        price:
          type: string
          example: '64150.10'
        quantity:
          type: string
          example: '0.01000000'
        side:
          type: string
          enum: [buy, sell]
          description: Сторона тейкера
        sequence:
          type: integer
          format: int64
        time:
          type: integer
          format: int64
        is_demo:
          type: boolean
          description: Сделка демо-контура; учтена в объёме тикера

    Kline:
      type: object
      properties:
        open_time:
          type: integer
          format: int64
          description: Начало интервала
        open:
          type: string
        high:
          type: string
        low:
          type: string
        close:
          type: string
        volume:
          type: string
          description: Объём в базовом активе
        quote_volume:
          type: string
        trades:
          type: integer

    Funding:
      type: object
      properties:
        symbol:
          type: string
          example: BTC_USDT_PERP
        funding_rate:
          type: string
          nullable: true
          example: '0.0001'
        next_funding_time:
          type: integer
          format: int64
          nullable: true
          description: null, если следующий funding не запланирован
        interval_seconds:
          type: integer
          nullable: true
          example: 28800

    Account:
      type: object
      properties:
        user_id:
          type: integer
        is_demo:
          type: boolean
          description: Режим аккаунта; демо и реальные данные не смешиваются нигде в контуре
        kyc_status:
          type: string
          enum: [none, new, process, moderate, fail, success, cancelled]
        kyc_verified:
          type: boolean
        two_factor_enabled:
          type: boolean
          description: Обязательна для вывода через API
        api_key:
          type: string
          description: public_id ключа, которым сделан запрос
          example: ek_9f1c0f5e0a4b4f2f9d3a5c7e1b2d4f60
        scopes:
          type: array
          items:
            type: string
            enum: [read, trade, withdraw]
        can_trade:
          type: boolean
          description: Есть ли у КЛЮЧА скоуп trade (гейты аккаунта — отдельные поля)
        can_withdraw:
          type: boolean
        server_time:
          type: integer
          format: int64

    Balance:
      type: object
      properties:
        asset:
          type: string
          example: USDT
        scope:
          type: string
          enum: [wallet, spot, futures, demo]
          description: Суб-счёт леджера. `spot` — легаси-счёт, обнулён миграцией.
        available:
          type: string
          example: '1500.00'
        locked:
          type: string
          description: Зарезервировано под активные ордера
          example: '250.00'

    Position:
      type: object
      properties:
        symbol:
          type: string
        side:
          type: string
          enum: [long, short]
        size:
          type: string
          description: Модуль размера; сторона — в поле side
        entry_price:
          type: string
        mark_price:
          type: string
          nullable: true
        margin:
          type: string
        leverage:
          type: integer
        unrealized_pnl:
          type: string
          nullable: true
          description: null, если нет mark-цены
        realized_pnl:
          type: string
          description: Включает funding и результат ликвидаций, которых нет в /my-trades
        liquidation_price:
          type: string
          nullable: true

    Order:
      type: object
      properties:
        id:
          type: integer
          format: int64
        client_order_id:
          type: string
          nullable: true
          description: Ваш идентификатор; уникален в пределах аккаунта
        symbol:
          type: string
        side:
          type: string
          enum: [buy, sell]
        type:
          type: string
          enum: [limit, market]
        tif:
          type: string
          enum: [gtc, ioc, fok]
        status:
          $ref: '#/components/schemas/OrderStatus'
        price:
          type: string
          nullable: true
          description: null у рыночного ордера
        quantity:
          type: string
        remaining:
          type: string
        filled:
          type: string
          description: quantity - remaining, посчитано на сервере
        post_only:
          type: boolean
        reduce_only:
          type: boolean
        leverage:
          type: integer
          nullable: true
        fee:
          type: string
          description: Нулевая комиссия — это "0", а не null
        fee_asset:
          type: string
          nullable: true
        is_demo:
          type: boolean
        created_at:
          type: integer
          format: int64
        updated_at:
          type: integer
          format: int64

    OrderEnvelope:
      type: object
      properties:
        order:
          $ref: '#/components/schemas/Order'

    OrderList:
      type: object
      properties:
        orders:
          type: array
          description: Пустой массив — данных дальше нет; длина меньше limit — это последняя страница
          items:
            $ref: '#/components/schemas/Order'

    Fill:
      type: object
      properties:
        id:
          type: integer
          format: int64
          description: Курсор для from_id
        order_id:
          type: integer
          format: int64
        symbol:
          type: string
        side:
          type: string
          enum: [buy, sell]
        is_maker:
          type: boolean
        price:
          type: string
        quantity:
          type: string
        quote_quantity:
          type: string
        fee:
          type: string
        fee_asset:
          type: string
          nullable: true
        is_demo:
          type: boolean
        time:
          type: integer
          format: int64

    Deposit:
      type: object
      properties:
        id:
          type: integer
          format: int64
        asset:
          type: string
          nullable: true
          description: null для незнакомого токена, пришедшего на ваш адрес
        network:
          type: string
          example: trx
        address:
          type: string
        tx_id:
          type: string
          nullable: true
        amount:
          type: string
        confirmations:
          type: integer
        required_confirmations:
          type: integer
        status:
          type: string
          enum: [pending, confirmed, credited, ignored]
        created_at:
          type: integer
          format: int64
        credited_at:
          type: integer
          format: int64
          nullable: true

    Withdrawal:
      type: object
      properties:
        id:
          type: integer
          format: int64
        asset:
          type: string
        type:
          type: string
          enum: [crypto, bank]
          description: Через API создаётся только `crypto`; `bank` может прийти в истории из ЛК
        amount:
          type: string
        fee:
          type: string
        address:
          type: string
          nullable: true
          description: Написание берётся из адресной книги, а не из запроса
        network:
          type: string
          nullable: true
        status:
          type: string
          enum: [pending, processing, completed, rejected]
        created_at:
          type: integer
          format: int64
        updated_at:
          type: integer
          format: int64

    CancelAllResult:
      type: object
      properties:
        symbol:
          type: string
        canceled:
          type: array
          items:
            type: integer
            format: int64
        failed:
          type: array
          description: Ордера, которые отменить не удалось — их состояние надо перечитать
          items:
            type: integer
            format: int64

    PlaceOrderRequest:
      type: object
      required: [symbol, side, type, quantity]
      properties:
        symbol:
          type: string
          maxLength: 64
          example: BTC_USDT
        side:
          type: string
          enum: [buy, sell]
        type:
          type: string
          enum: [limit, market]
        quantity:
          type: string
          description: ТОЛЬКО строкой — JSON-число означает, что сумма уже прошла через float
          example: '0.015'
        price:
          type: string
          nullable: true
          description: 'Обязательна для `type: limit`, только строкой'
          example: '64150.10'
        tif:
          type: string
          enum: [gtc, ioc, fok]
          nullable: true
        post_only:
          type: boolean
          description: |
            Недопустим для рыночного ордера. Скрестившийся с книгой post-only
            ордер отклоняется движком асинхронно — статус надо опросить.
        reduce_only:
          type: boolean
          description: Только для перпов
        leverage:
          type: integer
          minimum: 1
          nullable: true
          description: Обязательно для перпа, кроме reduce-only (там берётся из позиции)
        client_order_id:
          type: string
          maxLength: 128
          nullable: true
          description: Ключ идемпотентности в пределах вашего аккаунта
          example: mm-btc-1757500000123

    CreateWithdrawalRequest:
      type: object
      required: [asset, network, address, amount]
      properties:
        asset:
          type: string
          maxLength: 32
          example: USDT
        network:
          type: string
          maxLength: 32
          description: Нормализуется перед проверкой ('TRC-20' → 'trx')
          example: trx
        address:
          type: string
          maxLength: 255
          description: Должен быть в адресной книге для этой пары актив+сеть
        amount:
          type: string
          description: Только строкой; минимум — из настроек актива
          example: '100.5'
