---
name: ybx-trading-api
description: Write, review or debug code that trades through the Yellow Box Markets (YBX) Trading API — a Binance-style REST + WebSocket API with native MT5 semantics (lots, hedged positions, bid/ask). Use when a task mentions the YBX / Yellow Box Trading API, X-YBX-APIKEY, trade-api.yellowboxmarkets.com, listenKey user data streams on that host, or an MT5 trading bot built against it.
---

# YBX Trading API

REST and WebSocket API for programmatic trading on Yellow Box Markets MT5 accounts. The conventions
(HMAC signing, headers, error envelope, rate limits, stream subscribe protocol) follow the Binance
USDⓈ-M Futures API; the **semantics are MT5**. There is no wire compatibility with Binance — field
names, enum values and error codes are YBX's own. Never assume a Binance field exists here.

**Pre-release.** Read `reference/known-limitations.md` before testing against a live server: several
operations do not yet reach the statuses the contract describes.

## Reference files (load on demand)

| File | Use it for |
|---|---|
| `reference/endpoints.md` | Every REST endpoint (method, path, security, weight), stream and event, with a link to its page |
| `reference/general-info.md` | Base URLs, signing rules + worked example, `login`, rate limits, data conventions, versioning |
| `reference/order-lifecycle.md` | ACK vs RESULT, statuses, idempotency, failure-mode table, comments, hedging |
| `reference/user-data-streams.md` | listenKey flow, events, ordering, close codes, reconnect |
| `reference/error-codes.md` | Every error code with its exact `msg` |
| `reference/enums.md` | Every enum and the `mt5RetCode` table |
| `reference/known-limitations.md` | Where the current server falls short of the contract |
| `reference/full-docs.md` | The complete documentation in one file — grep it for parameters and response fields |

Online: https://<docs-site>/llms.txt (index) — every page is also available as Markdown at its URL + `.md`.

## Base URLs

| Purpose | URL |
|---|---|
| REST | `https://trade-api.yellowboxmarkets.com` (all paths start `/v1`) |
| Market streams | `wss://trade-stream.yellowboxmarkets.com/ws` (raw), `/stream?streams=` (combined) |
| User data stream | `wss://trade-stream.yellowboxmarkets.com/ws/<listenKey>` |

The hostnames are placeholders until DNS is assigned: **keep base URLs in configuration**. There is
no separate testnet — a key scoped to demo logins is the sandbox; `GET /v1/accounts` reports
`"demo": true` per login. Check it before the first order.

## Authentication and signing

- Header `X-YBX-APIKEY: <key>` on everything except `NONE` endpoints.
- `TRADE` and `USER_DATA` endpoints are SIGNED: add `timestamp` (Unix **ms**), optional
  `recvWindow` (default 5000, max 60000), then `signature` = lowercase hex HMAC-SHA256 of
  `totalParams` keyed with the secret.
- `totalParams` = query string + body, concatenated **verbatim as transmitted**, no separator.
  **Send all parameters in one place** (query for GET/DELETE, form body for POST/PUT), sign the
  exact encoded string, append `&signature=…` last, and never re-encode after signing.
- `-1021` = timestamp outside the window: sync against `GET /v1/time`.
- `USER_STREAM` endpoints (`/v1/listenKey`) need the header only, no signature, no `login`.

```python
import hashlib, hmac, time, urllib.parse, requests

BASE = "https://trade-api.yellowboxmarkets.com"   # from configuration

def signed(method, path, params, key, secret: bytes):
    params = {**params, "recvWindow": 5000, "timestamp": int(time.time() * 1000)}
    query = urllib.parse.urlencode(params)                      # encode once
    sig = hmac.new(secret, query.encode(), hashlib.sha256).hexdigest()
    payload = f"{query}&signature={sig}"                        # send exactly these bytes
    headers = {"X-YBX-APIKEY": key}
    if method in ("GET", "DELETE"):
        return requests.request(method, f"{BASE}{path}?{payload}", headers=headers, timeout=10)
    headers["Content-Type"] = "application/x-www-form-urlencoded"
    return requests.request(method, BASE + path, headers=headers, data=payload, timeout=10)
```

```bash
Q="login=100123&recvWindow=5000&timestamp=$(date +%s000)"
SIG=$(printf '%s' "$Q" | openssl dgst -sha256 -hmac "$YBX_API_SECRET" | sed 's/^.*= //')
curl -H "X-YBX-APIKEY: $YBX_API_KEY" "https://trade-api.yellowboxmarkets.com/v1/account?$Q&signature=$SIG"
```

Validate signing code against the worked example in `reference/general-info.md`
(`login=100123&recvWindow=5000&timestamp=1789012345678` → `13c783f0…a9255b`).

## `login` is mandatory

One key trades **many** MT5 accounts, so every account-scoped endpoint takes `login` (int64).
Missing → `-1102`; out of scope / unknown / inactive → `-2022` (deliberately indistinguishable).
A login in scope with trading disabled reads fine (`tradeAllowed: false`) but trades fail `-2024`.
`GET /v1/accounts` lists exactly the logins the key may trade. `POST /v1/batchOrders` carries
`login` per item. Market-data endpoints take no `login`.

## Orders: idempotency first

- **Always send `newClientOrderId`** (≤36 chars, `[A-Za-z0-9-_.]`, unique per key). It **is** the
  idempotency key: resending the same id never creates a second order — it returns the stored
  outcome. Without it, a retry is a new order.
- Retries are answered from the stored record, not re-validated. Reusing an id for a different
  operation kind or a different `login` → `-1133`. Records are kept ≥ 24 h.
- **Unknown outcome — `-5008`, `-5009`, `503` "execution status is UNKNOWN", or `IN_DOUBT`:** the
  order may have executed. **Never resend under a new `newClientOrderId`.** Either retry the
  identical request (same id), or verify first via `GET /v1/order?login=…&origClientOrderId=…`,
  `GET /v1/positions`, or the user data stream (`DEAL.C` = your client order id).
- `-5015` on a TRADE endpoint: nothing was queued — retry with backoff. `-5001` requote / `-5002`
  price changed: not executed — re-price and send a **new** id. `-11xx` / `-40xx`: fix the request.

### ACK vs RESULT

- `newOrderRespType=ACK` (default, recommended): returns once durably queued — `ACCEPTED` (market)
  or `NEW` (pending). The fill arrives on the user data stream.
- `RESULT`: waits up to 5 s; returns `FILLED`, a 4XX with `status: "REJECTED"` + `mt5RetCode`, or
  still `ACCEPTED` (very likely executed — confirm, do not resend). Only `POST /v1/order`,
  `DELETE /v1/position`, `POST /v1/batchOrders` accept it; `PUT /v1/order`, `DELETE /v1/order`,
  `PUT /v1/position` always wait; `DELETE /v1/allOpenPositions` always ACKs.
- Fan one signal across accounts with `POST /v1/batchOrders` + `ACK`.

### Statuses

Market: `ACCEPTED` → `PARTIALLY_FILLED` → `FILLED` | `REJECTED` | `IN_DOUBT`. Pending: `NEW`,
`PARTIALLY_FILLED`, `FILLED`, `CANCELED`, `EXPIRED`, `REJECTED`. `executedVolume` sums the deals;
`price` is their volume-weighted average. **Treat an unknown status as non-terminal** (new values
may be added). Positions are **hedged**: a BUY against an open SELL opens a second position; close
by `positionId`. SL/TP live on the position (`PUT /v1/position`).

## User data stream

1. `POST /v1/listenKey` → `{"listenKey": …}` (returns the key's existing one if any).
2. Connect `wss://…/ws/<listenKey>` — no SUBSCRIBE; one connection carries **every login** in the
   key's scope; route on `L`.
3. `PUT /v1/listenKey` **every 30 minutes on a timer** (lifetime 60 min). `listenKeyExpired` then
   close = your keepalive is wrong: create a key, reconnect, resnapshot. `DELETE /v1/listenKey`
   to close.

Events: `DEAL` (an execution — **dedupe on `d`**, duplicates are by design; route on `C`, your
client order id; `c` is the stored comment with a 7-char tag appended), `ORDER_UPDATE`,
`POSITION_UPDATE` (state snapshots — keep the newest `E` per entity), `ACCOUNT_UPDATE` (no equity /
margin — read `GET /v1/account`). Ordering holds per login only.

**Reconnect = resnapshot.** Nothing is replayed. After any reconnect: `GET /v1/positions` and
`GET /v1/openOrders` per login, then `GET /v1/userTrades?fromId=<last dealId>`, then apply events.
Close `1011` "queue overflow; resnapshot" means you missed events — reconnect and resnapshot.
Close `1008` "scope empty" — check `GET /v1/accounts`, do not reconnect in a loop. Use
exponential backoff with jitter.

## Rate limits

Per key: `REQUEST_WEIGHT` 1200/min; `ORDERS` 100/10 s and 1200/min (configurable — read
`rateLimits[]` from `GET /v1/exchangeInfo` with your key). Track `X-YBX-USED-WEIGHT-1M`,
`X-YBX-ORDER-COUNT-10S`, `X-YBX-ORDER-COUNT-1M` and slow down before the limit. `429` → stop for
`Retry-After` seconds; `418` = banned for ignoring 429s (escalating) — retrying lengthens it.
Prefer streams over polling.

## Data conventions

- **Decimals are JSON strings** (`"0.10"`, `"2331.42"`) — parse into a decimal type, never float.
- **Volume is in lots**, a multiple of the symbol's `volumeStep` (`-4007` otherwise); limits from
  `GET /v1/exchangeInfo`.
- **Symbols are case-sensitive and exact**, stream names included (`XAUUSD@tick`, never
  `xauusd@tick`; `BTCUSD.cent` ≠ `BTCUSD.CENT`). Never normalise case.
- Times are Unix ms UTC. Ticket ids can exceed 2^53 — no JavaScript `Number`; use BigInt/strings.
- Money is in the account currency; `USC` (cent) accounts are not converted (100 USC = 1 USD).
- Ignore unknown fields, tolerate unknown enum values, index array rows from the front.
- Errors: `{"code": <negative int>, "msg": …}` (+ `mt5RetCode` on execution errors). Branch on
  `code`, never `msg`. A 5XX is not proof the operation failed.

## Current limitations (pre-release)

The trade server currently sends no dealer answer: non-market operations stay `NEW` / `ACCEPTED`
and rejections are not reported — confirm from `ORDER_UPDATE` / `POSITION_UPDATE` or
`GET /v1/openOrders` / `GET /v1/positions`. Address pending orders by `orderId`, not
`origClientOrderId`. Cancels may not apply. Details: `reference/known-limitations.md`.
