# Trading e historial

URL: https://docs.fxapis.com/es/trading

> Órdenes, cierres, órdenes pendientes, órdenes multicuenta y el historial que dejan.



Cómo enviar órdenes, y el registro permanente que dejan.

Si lees una sola sección, lee [Los tres fallos](#the-three-failures). Es la
diferencia entre un cliente que reintenta de forma segura y uno que, de vez en cuando,
abre dos posiciones donde pediste una.

## Enviar una orden de mercado [#placing-a-market-order]

```bash
curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/orders/market" \
  -H "Authorization: Bearer $FXAPIS_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "symbol": "EURUSD",
    "side": "buy",
    "volume": "0.10",
    "stopLoss": "1.0890",
    "takeProfit": "1.1010",
    "deviationPoints": 20,
    "clientOrderId": "strategy-a-0417"
  }'
```

La cuenta debe estar en línea (`ready`). Una cuenta en modo `warm_on_demand` se pone
en línea sola para la orden; consulta [Cuentas](/es/accounts).

### Símbolo o instrumento [#symbol-or-instrument]

`symbol` es el nombre propio del broker para el mercado, exactamente como lo escribe ese broker (`XAUUSD.a`, `US30.cash`). Para operar el mismo mercado en cuentas de distintos brokers, envía `instrument` en su lugar —`XAUUSD`, `US30`, `BTCUSD`— y cada orden opera el símbolo propio de esa cuenta para ese mercado. Consulta [Instrumentos y símbolos](/es/instruments).

### Los volúmenes y los precios son strings [#volumes-and-prices-are-strings]

`"0.10"`, no `0.1`. Esto es obligatorio: un número JSON se rechaza con
`INVALID_REQUEST`, no se acepta en silencio.

Un número JSON es un double IEEE. `0.1 + 0.2` es `0.30000000000000004`, y un volumen
que pasó por un double ya perdió una precisión que no podemos recuperar. Aceptarlo
significaría operar en silencio un tamaño que no pediste, así que la API lo rechaza
mientras todavía puedes corregirlo.

Usa un tipo decimal de tu lado y serialízalo como string.

### Los stops son precios absolutos [#stops-are-absolute-prices]

`stopLoss: "1.0890"` es un precio, no una distancia en pips. Comprobamos el error que
el broker no puede ver —stops del lado equivocado para la dirección que querías— antes
de enviar nada, y lo rechazamos con `400 INVALID_REQUEST`:

* El stop loss de una compra debe estar por debajo de su take profit (el de una venta, por encima).
* Los stops de una orden pendiente se comprueban contra su propio precio o, en una
  stop-limit, contra su precio límite.
* El precio de ejecución de una orden de mercado todavía no se conoce, así que sus
  stops se comprueban contra el mercado —el ask para una compra, el bid para una venta—
  cuando fxapis tiene un precio reciente del símbolo en esa cuenta; si no, se deja al
  broker. Un precio reciente es una [cotización en vivo](#live-prices) ya leída para esa
  cuenta, con un tick de menos de 10 segundos, o si no, el precio de una posición
  abierta en el símbolo. La orden nunca espera a que se obtenga un precio.

Las reglas de distancia y de paso son del broker, y las aplica su `order_check` antes de
enviar nada; un stop que rechaza vuelve como `ORDER_REJECTED` con el motivo del broker.

### Envía siempre un Idempotency-Key [#always-send-an-idempotency-key]

```
Idempotency-Key: 8f14e45f-ea8d-4b1e-9c2a-7b3d5e9f0a11
```

Sin uno, una solicitud que agota el tiempo de espera te deja atascado. No puedes saber
si la orden se envió; reintentar arriesga una segunda posición, y no reintentar arriesga
no tener ninguna. Con uno, reintentar no cuesta nada.

| Situación                                                    | Lo que recibes                                                               |
| ------------------------------------------------------------ | ---------------------------------------------------------------------------- |
| Misma clave, mismo cuerpo, la primera llamada terminó        | La **misma respuesta**, repetida, con `idempotent-replay: true`              |
| Misma clave, mismo cuerpo, la primera llamada sigue en curso | `409 IDEMPOTENCY_IN_FLIGHT`: reintenta en breve                              |
| Misma clave, cuerpo **distinto**                             | `409 IDEMPOTENCY_KEY_REUSED`                                                 |
| Misma clave, después de un `SEND_FAILED`                     | Un intento nuevo de verdad: nada llegó al broker, así que la clave se libera |
| Misma clave, después de un rechazo                           | Se **repite el rechazo**. Para volver a intentarlo, usa una clave nueva      |

Esa última fila es la sutil, y es deliberada. Una clave solo se libera cuando sabemos
que el broker nunca vio la orden. Si el resultado no está resuelto, la clave sigue
reclamada y un reintento repite la respuesta sin resolver en lugar de enviar una segunda
orden.

Las claves se respetan durante 24 horas y su alcance es tu workspace; después, una clave
se libera y se puede volver a usar. Usa una nueva por orden: un UUID es ideal.

## Los tres fallos [#the-three-failures]

Una orden puede fallar de tres maneras, y la respuesta te dice cuál. Una cuarta
respuesta, `NO_PRICE`, se describe junto con ellas: no se envió nada, pero reintentar de
inmediato no puede ayudar.

### `SEND_FAILED` (503): nunca llegó al broker [#send_failed-503--it-never-reached-the-broker]

```json
{"error":{"code":"SEND_FAILED","message":"… the order was never sent. It is safe to retry.",
 "details":[{"orderId":"...","state":"failed","retryable":true}]}}
```

Sabemos con certeza que no se envió nada al broker. La orden queda `failed`.

**Reinténtala.** Es el único estado de orden en el que reenviar es seguro.

### `ORDER_REJECTED` (422): el broker dijo que no [#order_rejected-422--the-broker-said-no]

```json
{"error":{"code":"ORDER_REJECTED","message":"The broker rejected the order: No money",
 "details":[{"orderId":"...","state":"rejected","retryable":false}]}}
```

El broker la rechazó, con un motivo, y no se ejecutó nada. La orden lleva el `retcode`
propio del broker: consúltala con `GET /v1/orders/{id}` y verás `10019` aquí, sin
modificar.

`retryable` te dice si el motivo era transitorio:

* **`retryable: true`**: una recotización (`10004`), un precio que se movió (`10020`),
  sin cotizaciones en este momento (`10021`). El mercado se movió; enviar una orden
  nueva —con un `Idempotency-Key` nuevo— es lo correcto. Mira primero el nuevo precio.
* **`retryable: false`**: margen insuficiente (`10019`), mercado cerrado (`10018`),
  trading desactivado (`10017`), un volumen o un stop inválido. Reintentar solo trae el
  mismo rechazo.

### `NO_PRICE` (422): el broker no tenía precio [#no_price-422--the-broker-had-no-price]

```json
{"error":{"code":"NO_PRICE","message":"The broker has no current price for USDJPY on this account, so nothing was sent. …",
 "details":[{"orderId":"...","state":"failed","retryable":false}]}}
```

La terminal de la cuenta no tenía cotización para el símbolo, así que no envió la orden.
Normalmente el mercado está cerrado, o este broker no ofrece el símbolo con ese nombre.
La orden queda `failed`.

**No reintentes de inmediato.** Revisa el horario de trading del símbolo en
`GET /v1/accounts/{id}/instruments` y reintenta cuando esté cotizando. No se envió nada,
así que se puede reutilizar el mismo `Idempotency-Key`.

### `ORDER_UNRESOLVED` (503): no lo sabemos [#order_unresolved-503--we-do-not-know]

```json
{"error":{"code":"ORDER_UNRESOLVED","message":"… we do not know whether the order reached the broker. Do not resend it.",
 "details":[{"orderId":"...","state":"unknown","retryable":false}]}}
```

**No reenvíes esta orden.** Puede estar activa en el broker ahora mismo.

Esto ocurre cuando un envío agota el tiempo de espera, la conexión se cae a mitad de la
solicitud, o el broker devuelve un código que no dice qué pasó: `10012 TIMEOUT` cancela
*nuestra espera*, no necesariamente la orden.

Consulta `GET /v1/orders/{id}`. Para una orden que abre una posición, confirmamos el
resultado con el broker automáticamente y la sacamos de `unknown` a lo que realmente
pasó; `needsReconciliation` es `true` hasta entonces. Un cierre, un cambio de stop o una
cancelación que termina en `unknown` no se resuelve automáticamente: su `stateDetail` te
indica revisar la posición o la orden en el broker. Consulta
[Confirmar resultados](#confirming-results) más abajo.

Aquí somos cautelosos a propósito. Un resultado que no podemos confirmar es `unknown`,
nunca "probablemente falló", incluso para un código de retorno que nunca habíamos visto.
Equivocarse en esta dirección cuesta una espera corta; equivocarse en la otra cuesta una
posición que nunca pediste.

## Confirmar resultados [#confirming-results]

Una orden de apertura en `unknown` se comprueba automáticamente contra el historial de
deals del broker, cada minuto. Un cierre, un cambio de stop o una cancelación no produce
un deal de apertura con el cual compararse, así que se queda en `unknown` con un
`stateDetail` que te pide revisar en el broker: interpretar una ausencia como "falló"
podría ser un error. `POST /v1/accounts/{id}/reconcile` lo hace de inmediato para una
cuenta.

```bash
curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/reconcile" -H "Authorization: Bearer $FXAPIS_KEY"
```

```json
{"data":{"examined":1,"resolved":1,"stillUnknown":0,"ambiguous":0,
         "dealsIngested":1,"positionsOpen":1,"positionsClosed":0,"skipped":null}}
```

Se puede llamar en cualquier momento, y llamarlo dos veces no tiene riesgo.

### En qué se resuelve [#what-it-resolves-to]

| En el broker                                            | La orden pasa a                              |
| ------------------------------------------------------- | -------------------------------------------- |
| Operaciones que cubren todo el volumen                  | `filled`                                     |
| Operaciones que cubren parte                            | `partially_filled`                           |
| Ninguna operación coincide, y ya pasó suficiente tiempo | `rejected`                                   |
| Ninguna operación coincide todavía                      | sigue en `unknown` por ahora                 |
| Más de una coincidencia posible                         | sigue en `unknown`, contada como `ambiguous` |

Una orden `unknown` nunca pasa a `failed`: ese estado significa que la orden, de forma
demostrable, nunca salió de fxapis, y después de los hechos solo el registro del broker
puede decir qué pasó.

**Cuando no puede estar seguro, no adivina.** Si dos operaciones en el broker podrían ser
tu orden, la orden sigue en `unknown` y se cuenta en `ambiguous`: asignar una ejecución a
la orden equivocada produciría un historial que parece correcto y no lo es. Esas órdenes
necesitan a una persona:

```bash
curl -sS "$FXAPIS_API/v1/orders?state=unknown" -H "Authorization: Bearer $FXAPIS_KEY"
```

El `stateDetail` de la orden dice cuántas candidatas había.

### Los comentarios de tus órdenes [#your-order-comments]

fxapis agrega una etiqueta corta al inicio del comentario de MT5 de cada orden; así se
relaciona el registro del broker con tu orden. El comentario admite 29 caracteres; tu
propio `comment` recibe los 17 que quedan después de la etiqueta, en ASCII simple. Un
comentario más largo se recorta para que quepa, nunca se rechaza.

### Mantiene tu historial completo [#it-keeps-your-history-complete]

Cada comprobación también registra los deals del broker del período, **haya o no algo que
confirmar** —incluidos los deals que no causó ninguna orden tuya—, y actualiza las
posiciones abiertas. Una posición que el broker ya no informa se marca como cerrada, no se
elimina.

## Cerrar una posición [#closing-a-position]

```bash
curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/positions/2104151615/close" \
  -H "Authorization: Bearer $FXAPIS_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{"volume": "0.05"}'
```

Omite `volume` para cerrarla completa.

El ticket a cerrar es el `brokerPositionId` de la posición. Para una posición que abrió tu
propia orden, esa orden también lo lleva, como `order.brokerPositionId`, tomado de la
propia ejecución cuando la orden abre. Una orden pendiente que se ejecuta después también
lo recibe, vinculado desde las posiciones y los deals del broker, normalmente en unos
minutos, y pasa de `working` a `filled` al mismo tiempo (consulta
[Estados de una orden](#order-states)). Mientras siga en `null`, busca la posición en
`GET /v1/accounts/{id}/positions`: una posición nueva lleva el ticket de la orden que la
abrió, que es el `brokerOrderId` de esa orden.

**Una clave de idempotencia importa más aquí que en una orden de apertura.** Un cierre
reintentado no cierra dos veces: en una cuenta hedging abre una posición *opuesta*, y la
cuenta termina con una exposición que nadie eligió.

Un cierre es una orden. Tiene su propio registro con `intent: "close"`, los mismos tres
modos de fallo y la misma regla: `ORDER_UNRESOLVED` significa **no reenviar**. Un cierre
sin resolver es el caso en el que podrías creer que no tienes posición y no ser así.

Dos cosas se rechazan antes de enviar nada:

* Cerrar más de lo que tiene la posición. El mensaje nombra ambos números.
* Un cierre parcial que dejaría menos del volumen mínimo del símbolo (`volumeMin` en su
  [lista de símbolos](/es/instruments)): el broker lo rechaza *después* del cierre, y deja
  una posición demasiado pequeña para operar y demasiado pequeña para cerrar. Ciérrala
  completa, o deja al menos el mínimo.

## Mover un stop [#moving-a-stop]

```bash
curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/positions/2104151615/modify" \
  -H "Authorization: Bearer $FXAPIS_KEY" -H "Content-Type: application/json" \
  -d '{"stopLoss": "1.0890", "takeProfit": null}'
```

**`null` quita un nivel. Omitir el campo lo deja sin cambios.** Que un campo omitido
borre un stop en silencio es la forma en que la protección desaparece sin que nadie lo
elija.

Se registra con `intent: "modify"`, porque ampliar un stop cambia lo que la cuenta
arriesga y pertenece junto a la operación que protege: un stop que se movió antes de una
pérdida es exactamente lo que pregunta una auditoría.

Los stops se comprueban contra el **precio actual del mercado**, no contra la entrada: el
stop loss de una compra debe estar por debajo del mercado y su take profit por encima (en
una venta, al revés). Así que mover un stop a break-even, o arrastrarlo hacia la ganancia,
está permitido. Un stop que el mercado ya superó se rechaza antes de enviar nada, porque
cerraría la posición de inmediato. Los stops de una orden nueva se comprueban contra su
entrada: consulta [Los stops son precios absolutos](#stops-are-absolute-prices).

## Órdenes pendientes [#pending-orders]

```bash
curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/orders/pending" \
  -H "Authorization: Bearer $FXAPIS_KEY" -H "Content-Type: application/json" \
  -d '{"symbol":"EURUSD","side":"buy","kind":"limit","volume":"0.10","price":"1.0900"}'
```

| Tipo         | La compra espera                           | La venta espera |
| ------------ | ------------------------------------------ | --------------- |
| `limit`      | **por debajo** del mercado                 | **por encima**  |
| `stop`       | **por encima** del mercado                 | **por debajo**  |
| `stop_limit` | el stop se activa y luego coloca una limit | igual           |

Una limit espera un precio mejor; una stop espera uno peor y es la forma de operar una
ruptura. Cuando fxapis tiene un precio reciente del símbolo en esa cuenta (consulta
[Los stops son precios absolutos](#stops-are-absolute-prices)), el lado equivocado se
rechaza antes de enviar nada, con un mensaje que nombra la orden que probablemente
querías. Sin él, el broker responde con su propio `INVALID_PRICE`, que no dice nada sobre
en qué sentido debía ir.

Las órdenes pendientes son parte de los planes de pago; en un plan sin ellas, la solicitud
se rechaza con `402 FEATURE_NOT_IN_PLAN`.

Para mover una orden pendiente activa —su precio, sus stops, su precio stop-limit o su
vencimiento— usa `POST /v1/orders/{id}/modify`. Conserva el ticket de la orden, así que
conserva su lugar en la cola del broker; un campo omitido no se toca, y
`"stopLoss": null` quita un stop. Solo se puede mover una orden `working`.

**Una orden pendiente colocada está `working`, no `filled`.** Esa distinción es dinero
real: `filled` significa que existe una posición. Algunos brokers responden a la
*colocación* de una pendiente con `DONE`, el mismo código que devuelve una ejecución, e
interpretarlo como ejecución te diría que estás en el mercado cuando estás esperando
entrar.

Cancela una con `POST /v1/orders/{id}/cancel`. Si ya se activó, el broker la rechaza con
`10035`: en ese punto hay una posición, no una orden, y lo que quieres es un cierre. Una
cancelación exitosa pasa la orden pendiente a `cancelled` y registra la propia
instrucción como `completed`.

## Margen y ganancia, antes de operar [#margin-and-profit-before-you-trade]

```bash
curl -sS -X POST "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/calculate" \
  -H "Authorization: Bearer $FXAPIS_KEY" -H "Content-Type: application/json" \
  -d '{"kind":"margin","symbol":"EURUSD","side":"buy","volume":"0.10","price":"1.1370"}'
# → { "data": { "kind": "margin", "value": "22.74000000" } }
```

No abre nada y se puede llamar libremente. La cuenta debe estar en línea: una fuera de
línea se rechaza con `409 NO_RUNTIME`. Así averiguas que una orden será rechazada por
margen *antes* de enviarla, en lugar de recibir un `10019`. `"kind": "profit"` necesita
un `closePrice`.

Un `value` nulo significa que el broker no quiso responder, lo que no es lo mismo que cero.

## Precios en vivo [#live-prices]

```bash
curl -sS "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/quotes?symbols=EURUSD&instruments=XAUUSD" \
  -H "Authorization: Bearer $FXAPIS_KEY"
# → { "data": [
#     { "symbol": "EURUSD", "instrument": null, "status": "ok", "bid": "1.08450000", "ask": "1.08462000",
#       "spread": 12, "digits": 5, "brokerTime": "2026-10-15 15:00:00.123", "time": "2026-10-15T12:00:00.123Z",
#       "observedAt": "2026-10-15T12:00:00.300Z", "ageMs": 177, "stale": false, "marketOpen": true, "tradeMode": "full" },
#     { "symbol": "XAUUSD.a", "instrument": "XAUUSD", "status": "ok", "bid": "2650.10000000", … } ] }
```

Los precios vienen del propio broker de la cuenta. Cada broker cotiza distinto, y estos
son los precios a los que opera esta cuenta.

* **Qué puedes pedir:** hasta 20 `symbols` del broker e `instruments` en total,
  separados por comas, todos leídos en una llamada. Un instrumento se cotiza como el
  símbolo propio de esta cuenta para ese mercado. La cuenta debe estar en línea:
  `ready`, `executing` o `degraded` (si no, `409 ACCOUNT_NOT_READY`).
* **Cada cotización con un tick lleva `time` (UTC), `brokerTime` (el reloj propio del
  broker, como lo muestra su terminal) y `ageMs`**, la antigüedad del tick cuando se leyó
  la terminal en `observedAt`. Aplica tu propia regla de frescura a partir de esos datos.
* **Un mercado cerrado** responde su último precio con `marketOpen: false` y
  `stale: false`, nunca con un error.
* **`stale`** es true solo mientras el mercado está abierto y el último tick tiene más de
  60 segundos.
* **`marketOpen`** viene de las sesiones de trading propias del broker. Es `null` (y
  `stale` es `false`) hasta que la terminal las haya informado, más o menos un minuto
  después de que una cuenta se pone en línea por primera vez.
* **`tradeMode`** es lo que el broker permite en el símbolo en este momento: `full`,
  `longOnly`, `shortOnly`, `closeOnly` o `disabled`. `closeOnly` y `disabled` no permiten
  posiciones nuevas.
* **Un símbolo que este broker no tiene** responde `status: "unknown_symbol"` en su
  lugar; uno del que todavía no envió precio, `status: "no_price"`.
* **Consultas repetidas:** no hay stream. Las respuestas se comparten durante un cuarto
  de segundo, así que una cotización puede haberse leído hasta 250 ms antes de que la
  pidieras: esa es la antigüedad de la *lectura*, no del precio, que es lo que te dicen
  `ageMs` y `time`. A la terminal de cada cuenta se le pueden hacer 300 consultas por
  minuto con ráfagas de 50, sumando todos los llamadores y todas las claves; las
  respuestas compartidas son gratis. Por encima de eso: `429 RATE_LIMITED` con
  `retry-after`.
* **Una terminal que todavía corre un worker anterior** —algo raro— responde
  `501 CAPABILITY_UNAVAILABLE` en su lugar. No lo reintentes; mientras tanto, el
  `currentPrice` de una posición abierta es el precio.

Los stops de una orden se comprueban contra este precio cuando se leyó uno recientemente:
el ask para una compra, el bid para una venta
([Los stops son precios absolutos](#stops-are-absolute-prices)).

## Historial de precios [#price-history]

**Disponible después del próximo mantenimiento programado.** Hasta que la terminal de la
cuenta se haya actualizado, esto responde `501 CAPABILITY_UNAVAILABLE`.

```bash
curl -sS "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/candles?instrument=XAUUSD&timeframe=H1&count=100" \
  -H "Authorization: Bearer $FXAPIS_KEY"
# → { "data": { "symbol": "XAUUSD.a", "instrument": "XAUUSD", "timeframe": "H1", "digits": 2, "candles": [
#     { "time": "2026-10-23T13:00:00Z", "open": "2650.10", "high": "2655.25", "low": "2648.00",
#       "close": "2652.50", "tickVolume": 1234, "spread": 35 }, … ] } }
```

Barras (velas) del propio broker de la cuenta, para un `symbol` o un `instrument`.

* **`timeframe`:** `M1`, `M5`, `M15`, `M30`, `H1`, `H4`, `D1`, `W1` o `MN1`.
  **`count`:** de 1 a 1,000 barras (100 por defecto), contando hacia atrás desde la
  actual. De la más antigua a la más reciente; la última barra todavía se está formando.
* **`time`** es cuándo abrió la barra, en UTC, convertido con el desfase de reloj actual
  del broker: las barras anteriores al último cambio de horario de verano del broker
  pueden tener una hora de diferencia.
* **`tickVolume`** cuenta los cambios de precio en la barra, no el volumen operado.
  `spread` está en puntos.
* La cuenta debe estar en línea (si no, `409 ACCOUNT_NOT_READY`). Un símbolo que el
  broker no tiene responde `404 SYMBOL_NOT_FOUND`. Cada llamada llega a la terminal y
  cuenta dentro de las mismas 300 por minuto que las cotizaciones.

## Estados de una orden [#order-states]

| Estado             | Significado                                                                      | ¿Reenviar?             |
| ------------------ | -------------------------------------------------------------------------------- | ---------------------- |
| `accepted`         | Recibida, todavía no enviada                                                     | —                      |
| `sending`          | En curso                                                                         | No                     |
| `working`          | Una orden pendiente esperando en el broker. **No existe ninguna posición.**      | No: ya está ahí        |
| `completed`        | Terminó una instrucción que no es una operación: una cancelación, un stop movido | —                      |
| `filled`           | El broker confirmó todo el volumen                                               | No                     |
| `partially_filled` | Ejecutada en parte; el resto aún puede ejecutarse                                | No                     |
| `rejected`         | El broker la rechazó. No se ejecutó nada.                                        | Solo si es `retryable` |
| `failed`           | **Demostrado** que nunca llegó al broker                                         | **Sí**                 |
| `unknown`          | Resultado no establecido                                                         | **Nunca**              |
| `cancelled`        | Retirada antes de ejecutarse                                                     | —                      |
| `expired`          | Una orden pendiente venció en el broker                                          | —                      |

`failed` y `unknown` nunca son intercambiables. `failed` es algo que afirmamos solo con
pruebas.

**Una orden pendiente que se activa** pasa de `working` a `filled` (o
`partially_filled`) cuando su ejecución aparece en el historial de deals del broker o
como una posición con su ticket, normalmente en unos minutos, con un único webhook
`order.filled`. `filledPrice` y `filledVolume` vienen de su deal de ejecución; cuando solo
se conoce la posición, se toman de ella en una cuenta hedging (el volumen solo mientras
siga siendo igual al de la orden) y se dejan en `null` en una cuenta netting, cuya
posición puede contener ejecuciones de otras órdenes, hasta que llega el deal. Una orden
pendiente que el broker cancela o deja vencer por su cuenta todavía no se detecta: se
queda en `working`.

## Historial [#history]

Tres tablas, porque las preguntas son distintas.

### Órdenes: lo que pediste [#orders--what-you-asked-for]

```bash
curl -sS "$FXAPIS_API/v1/orders?state=unknown&limit=50" -H "Authorization: Bearer $FXAPIS_KEY"
```

Cada orden, con el código de retorno del broker conservado exactamente como llegó.
Filtra por `accountId`, `state`, `symbol` y una ventana `since`/`until`.

`state=unknown` es la consulta sobre la que vale la pena construir una alerta: es todo lo
que todavía no tiene un resultado establecido.

### Deals: lo que el broker realmente hizo [#deals--what-the-broker-actually-did]

```bash
curl -sS "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/deals?since=2026-09-01T00:00:00Z" \
  -H "Authorization: Bearer $FXAPIS_KEY"
```

Un deal es el registro del broker de dinero que cambia de manos. Es contra lo que se
concilia un estado de cuenta, y **no** es una fila por orden:

* Una orden puede producir varios deals. Una ejecución parcial deja más de uno.
* **Un deal puede existir sin ninguna orden tuya detrás.** Un stop loss que se activa en
  el broker, un cargo de swap, un depósito, una corrección, o alguien que opera el mismo
  login desde MetaTrader en el escritorio.

Ese segundo punto es la razón por la que este endpoint existe por separado. Un historial
que solo contuviera tus propias órdenes mostraría una cuenta cuyo balance se mueve sin
motivo visible. `orderId` es `null` exactamente en esas filas, para que puedas
distinguirlas.

Ordenados por la marca de tiempo propia del broker, no la nuestra. `dealType` tiene nombre
en lugar de número —`balance`, `commission`, `buy`— porque `2` no significa nada en un
estado de cuenta.

Deals de una orden: `GET /v1/orders/{id}/deals`.

### Posiciones: lo que está abierto ahora [#positions--what-is-open-now]

```bash
curl -sS "$FXAPIS_API/v1/accounts/$FXAPIS_ACCOUNT/positions" -H "Authorization: Bearer $FXAPIS_KEY"
```

Una instantánea con `observedAt` indicando cuándo era cierta. El broker es la fuente de
verdad; esto es lo último que vimos, y la marca de tiempo está ahí para que puedas
distinguir una vista actual de una desactualizada en lugar de suponerlo.

### Paginación [#pagination]

Por cursor, no por offset:

```bash
curl -sS "$FXAPIS_API/v1/orders?limit=100" -H "Authorization: Bearer $FXAPIS_KEY"
# → { "data": [...], "page": { "hasMore": true, "nextCursor": "2026-09-28T14:02:11.482Z" } }

curl -sS "$FXAPIS_API/v1/orders?limit=100&cursor=2026-09-28T14:02:11.482Z" -H "Authorization: Bearer $FXAPIS_KEY"
```

Un offset se desplaza a medida que llegan órdenes nuevas, así que la segunda página se
saltaría filas en silencio: en un historial de trading, justamente las filas agregadas
mientras paginabas.

## Retención [#retention]

Las órdenes y los deals se conservan durante la vida de la cuenta y no se depuran.
Desconectar una cuenta borra sus credenciales; no borra su historial, porque ese historial
es con lo que se resuelve una disputa o una auditoría.

## Un cliente de ejemplo [#a-worked-client]

```python
import uuid, time, httpx

class OrderError(Exception): ...
class Unresolved(OrderError):
    """Do not resend. The order may be live."""

def place(client, account_id, **order):
    key = str(uuid.uuid4())

    for attempt in range(3):
        response = client.post(
            f"/v1/accounts/{account_id}/orders/market",
            json=order,
            # The SAME key on every attempt. That is the whole point: the
            # server decides whether this is a retry or a new order, and it is
            # the only party that can know.
            headers={"Idempotency-Key": key},
        )

        if response.status_code == 201:
            return response.json()["data"]

        error = response.json()["error"]
        code = error["code"]

        if code == "SEND_FAILED":
            # Proven not to have reached the broker.
            time.sleep(2 ** attempt)
            continue

        if code == "IDEMPOTENCY_IN_FLIGHT":
            time.sleep(1)
            continue

        if code == "ORDER_UNRESOLVED":
            # Never retried. Hand the order id to whatever follows up on
            # unresolved orders, and stop.
            raise Unresolved(error["details"][0]["orderId"])

        if code == "ORDER_REJECTED":
            if error["details"][0].get("retryable"):
                # The market moved: a new decision needs a NEW key. Resending
                # with this one would only replay the rejection.
                raise OrderError(f"retryable rejection: {error['message']}")
            raise OrderError(error["message"])

        raise OrderError(f"{code}: {error['message']}")

    raise OrderError("gave up after 3 attempts")
```

La idea clave del ejemplo: se usa la **misma** clave en cada intento, porque el servidor
es el único que puede saber si se trata de un reintento o de una orden nueva;
`ORDER_UNRESOLVED` nunca se reintenta; y un rechazo `retryable` necesita una decisión
nueva con una clave **nueva**.

## Órdenes multicuenta [#multi-account-orders]

Una operación, enviada en muchas cuentas en el mismo momento. La API las llama
*execution waves*.

```bash
curl -sS -X POST "$FXAPIS_API/v1/execution-waves" \
  -H "Authorization: Bearer $FXAPIS_KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{
    "accountIds": ["…", "…", "…"],
    "symbol": "EURUSD", "side": "buy", "volume": "0.01",
    "weights": {"<account-id>": "0.05"},
    "barrierPolicy": "release-ready",
    "executeAt": "2026-10-01T13:30:00Z"
  }'
```

Primero se pone en línea cada cuenta; luego todas las órdenes salen juntas. Consulta
`GET /v1/execution-waves/{id}` para ver el resultado.

`weights` define el tamaño de la operación por cuenta, para cuentas con balances distintos.

### Cada cuenta informa su propio resultado [#each-account-reports-its-own-result]

Los brokers son independientes entre sí, así que cien cuentas no pueden ejecutarse como
una sola transacción: las ejecuciones llegan a precios ligeramente distintos, algunas
cuentas pueden rechazarse por margen y, de vez en cuando, una queda sin respuesta.

Por eso una orden multicuenta **nunca informa un único éxito o fallo.** Se liquida, y
`summary` cuenta lo que pasó:

```json
{"state":"settled","dispatchSpreadMs":30,
 "summary":{"total":100,"filled":97,"rejected":2,"skipped":0,"unresolved":1}}
```

La operación de cada cuenta tiene su propio `orderId` y es una orden normal: se consulta,
se confirma y se audita como cualquier otra. `unresolved` significa lo mismo que en una
orden individual: **no reenvíes**, la estamos confirmando con el broker.

### Cuando algunas cuentas no están en línea a tiempo [#when-some-accounts-are-not-online-in-time]

| `barrierPolicy`  | Qué pasa                                                                                                                         |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `release-ready`  | Opera las cuentas que están en línea; omite el resto. Una entrada parcial es mejor que una tardía.                               |
| `all-or-nothing` | No opera ninguna a menos que todas las cuentas estén en línea. Para estrategias que solo tienen sentido en el conjunto completo. |
| `wait`           | Espera después de `executeAt` hasta que todas las cuentas estén en línea, hasta `expiresAt`.                                     |

`all-or-nothing` se refiere al **envío**, no al resultado: un broker todavía puede rechazar
la orden de una cuenta después de que se envió.

Con `executeAt`, las órdenes salen a esa hora, no antes. Sin él, salen en cuanto todas las
cuentas están en línea: se espera a las cuentas que todavía están iniciando sesión, hasta
60 segundos, y después decide la política.

### Dispersión [#spread]

`dispatchSpreadMs` es el tiempo entre la salida de la orden de la primera cuenta y la de
la última. Es el número sobre el que vale la pena alertar: una dispersión creciente
significa que las cuentas están obteniendo precios distintos. Cada orden multicuenta
informa la suya; estas son de nuestro benchmark contra un broker instantáneo, así que un
broker real agrega su propia latencia encima:

| cuentas | dispersión |
| ------- | ---------- |
| 10      | 17 ms      |
| 50      | 26 ms      |
| 100     | 30 ms      |

Medido de nuestro lado; cada broker agrega su propia latencia encima.

### Cancelar una [#calling-one-off]

`POST /v1/execution-waves/{id}/cancel`, pero solo antes de que haya salido alguna orden.
Una vez que las órdenes se están enviando, no se pueden recuperar.
