Órdenes idempotentes en MT5: nunca dupliques una ejecución al reintentar
Por qué reintentar una orden de MetaTrader 5 puede abrir una segunda posición, y cómo lo evita el Idempotency-Key de fxapis: claves de 24 horas, repeticiones, conflictos de solicitudes en curso y de reutilización, el resultado desconocido y patrones de claves para señales, copiadores y colas.
Toda integración de trading termina enviando la misma orden dos veces. Una conexión móvil se cae después del toque, un balanceador de carga agota el tiempo de espera, una cola vuelve a entregar un trabajo, un usuario hace doble clic. En la mayoría de las APIs, una solicitud duplicada es una molestia. En una API de trading es una segunda posición: exposición real que nadie eligió.
Esta guía explica de dónde vienen los duplicados, cómo se comporta exactamente el
encabezado Idempotency-Key en fxapis y cómo elegir las claves para que un reintento
siempre sea seguro.
Por qué los reintentos duplican operaciones
Cuando tu código envía una orden, pueden pasar tres cosas: vuelve la respuesta, vuelve un error, o no vuelve nada. El tercer caso es el difícil. La solicitud pudo morir antes de llegar al broker, o el broker pudo ejecutarla y la respuesta se perdió en el camino de vuelta. Desde tu lado, ambos casos son indistinguibles.
MetaTrader 5 lo hace concreto. Su servidor de trading responde con códigos de retorno, y algunos de ellos —un timeout, por ejemplo— describen lo que pasó con la espera, no con la orden. Una orden que "agotó el tiempo de espera" bien puede estar abierta.
Sin una forma de preguntar "¿mi solicitud anterior ya hizo esto?", un cliente tiene dos malas opciones: reenviar y arriesgar una doble ejecución, o rendirse y arriesgar perder la operación. La clave de idempotencia es esa pregunta, hecha por el servidor en tu nombre.
Cómo funciona el Idempotency-Key
Envía cualquier string único, de hasta 200 caracteres, en el encabezado
Idempotency-Key de una orden:
curl -sS -X POST "https://api.fxapis.com/v1/accounts/$ACCOUNT_ID/orders/market" \
-H "Authorization: Bearer $FXAPIS_KEY" \
-H "Idempotency-Key: open:signal_9931:member_4821" \
-H "Content-Type: application/json" \
-d '{ "symbol": "XAUUSD", "side": "buy", "volume": "0.05" }'Se respeta en las tres llamadas que pueden crear o quitar exposición: órdenes de mercado, órdenes pendientes y cerrar una posición. La primera solicitud con una clave la reclama; todo lo que viene después lo decide ese reclamo:
| Envías | Recibes |
|---|---|
| Misma clave, mismo cuerpo, la primera solicitud terminó | El mismo estado y cuerpo, repetidos, con el encabezado idempotent-replay: true. No se envía nada al broker. |
| Misma clave, mismo cuerpo, la primera solicitud sigue en curso | 409 IDEMPOTENCY_IN_FLIGHT. Espera un momento y envíala de nuevo con la misma clave. |
| Misma clave, cuerpo o endpoint distinto | 409 IDEMPOTENCY_KEY_REUSED. No se coloca nada. Es un error en cómo se generan las claves. |
Misma clave después de SEND_FAILED | Un intento nuevo de verdad. Está demostrado que la orden nunca llegó al broker, así que la clave se liberó. |
| Misma clave en un cierre, cuando la posición ya no está | 404 POSITION_NOT_FOUND. Un cierre busca la posición antes de comprobar la clave, así que un cierre que ya se hizo responde esto en lugar de repetir su 201: tómalo como hecho. |
Las claves se recuerdan durante 24 horas y pertenecen a tu workspace; después, una clave se libera y una solicitud con ella se trata como nueva. Una clave usada en un endpoint no se puede usar en otro: abrir y cerrar necesitan claves distintas.
Qué se repite, exactamente
Lo que sea con lo que terminó la primera solicitud. Eso incluye los fallos:
- Un 201 de ejecución se repite como el mismo 201.
- Un 422
ORDER_REJECTEDse repite como el mismo rechazo. Si el broker lo rechazó porque el precio se movió (retryable: true) y decides volver a operar, esa es una decisión nueva: usa una clave nueva. - Un 503
ORDER_UNRESOLVEDse repite como el mismo 503. La clave sigue reclamada a propósito: este es exactamente el caso en el que una segunda orden debe ser imposible.
Una clave solo se libera cuando sabemos que nada llegó al broker: después de
SEND_FAILED, y cuando la solicitud se rechazó antes de que existiera ninguna orden —por
ejemplo, una cuenta que no pudo ponerse en línea a tiempo (ACCOUNT_NOT_READY).
Reintentarlas con la misma clave coloca la orden normalmente.
El resultado desconocido
ORDER_UNRESOLVED (503) significa que todavía no sabemos si el broker ejecutó la orden. La
orden se registra en estado unknown, y puede estar activa ahora mismo.
Nunca la reenvíes: ni con la misma clave (recibirías el mismo 503) y menos con una clave nueva (así es como ocurren las dobles ejecuciones). En su lugar:
- Toma
details[0].orderIddel error. - Consulta
GET /v1/orders/{id}cada pocos segundos. - Confirmamos el resultado contra los propios registros del broker automáticamente. La orden pasa a
filled,partially_filledorejected, según lo que realmente ocurrió. - Para confirmarlo de inmediato en lugar de esperar, llama a
POST /v1/accounts/{id}/reconcile.
Si los registros del broker coinciden con más de una operación, la orden se deja en
unknown en lugar de adivinar, y necesita a una persona. GET /v1/orders?state=unknown
es la lista sobre la que vale la pena alertar. Los detalles están en
Trading → Confirmar resultados.
Un bucle de reintentos que siempre es seguro
import time, uuid, requests
RETRY_SAME_KEY = {"SEND_FAILED", "IDEMPOTENCY_IN_FLIGHT", "ACCOUNT_NOT_READY", "NO_RUNTIME", "RATE_LIMITED"}
def place_once(session, url, body, key):
for attempt in range(6):
try:
r = session.post(url, json=body, headers={"Idempotency-Key": key}, timeout=90)
except requests.RequestException:
time.sleep(min(2 ** attempt, 10)) # no answer seen: same key, ask again
continue
if r.status_code == 201:
return r.json()["data"]
error = r.json()["error"]
if error["code"] == "ORDER_UNRESOLVED":
return {"state": "unknown", "id": error["details"][0]["orderId"]} # poll it; never resend
if error["code"] in RETRY_SAME_KEY:
time.sleep(int(r.headers.get("retry-after", 0)) or min(2 ** attempt, 10))
continue
raise RuntimeError(f"{error['code']}: {error['message']}") # rejected, invalid, quota…
raise RuntimeError("gave up; the same key is still safe to retry later")La regla que codifica: la clave se elige una vez, antes del primer intento, y nunca cambia entre intentos. Generar un UUID nuevo dentro del bucle anula todo el mecanismo. Hay programas completos en siete lenguajes en las guías por lenguaje.
Elegir las claves
Una clave debe identificar la decisión de operar, no la solicitud HTTP. Dos solicitudes que llevan la misma decisión deben llevar la misma clave; dos decisiones distintas nunca deben compartir una.
Una acción del usuario → una clave creada cuando se toma la acción
Para un botón en tu app, crea la clave cuando la orden se crea en tu base de datos, guárdala en esa fila y envíala en cada intento. Si tu proceso se cae entre el envío y el registro de la respuesta, el proceso reiniciado encuentra la fila, envía la misma clave y recibe la respuesta original.
// When the user confirms — before any request is sent.
const intent = await db.orderIntents.insert({
userId, accountId, symbol, side, volume,
idempotencyKey: `intent:${crypto.randomUUID()}`,
});
// Later, and on every retry, from any worker:
await placeOnce(intent.accountId, intent, intent.idempotencyKey);Una señal que toman muchos miembros → señal + miembro
open:signal_9931:member_4821Un miembro que toca dos veces, su teléfono que reintenta y tu worker que reintenta se reducen a una sola orden. Otro miembro, u otra señal, recibe otra clave. Este es el patrón que usa Señales y click-to-trade.
Un copiador de operaciones → deal de la maestra + seguidora
copy:open:master_deal_5518823:follower_7f3a
copy:close:master_deal_5518901:follower_7f3aEl id del deal del broker de la cuenta maestra es único y estable, así que por muchas veces que tu poller vea el mismo deal, cada seguidora recibe una sola orden por él. Consulta Construye un copiador de operaciones de MT5.
Cierres y cierres parciales
Para un cierre completo, close:{accountId}:{ticket} es una buena clave. Para cierres
parciales de la misma posición, agrega algo que distinga una decisión de la siguiente
—un número de secuencia o el id de la regla que se activó—, porque un segundo cierre parcial
con la misma clave y el mismo cuerpo se repetiría, no se ejecutaría, y uno con la misma
clave pero otro volumen se rechazaría como IDEMPOTENCY_KEY_REUSED.
Un cierre repetido cuando la posición ya no está devuelve 404 POSITION_NOT_FOUND
("That position is already closed"). Tómalo como hecho, no como un error que reintentar.
Trabajos de colas
Las colas entregan al menos una vez. Pon la clave en el payload del trabajo cuando se encola —nunca la generes dentro del trabajo— para que un trabajo reentregado la reutilice.
Órdenes multicuenta
Las órdenes multicuenta también aceptan un
Idempotency-Key. Se guarda con la propia orden: volver a enviar la misma clave devuelve la
orden multicuenta que creó esa clave —200 con idempotent-replay: true— en lugar de crear
otra. A diferencia de las órdenes individuales, el cuerpo no se compara: la misma clave con
otro cuerpo igual devuelve la primera orden multicuenta, no un 409. Esto importa más que en
ningún otro lugar, porque una orden multicuenta duplicada es una operación duplicada en todas
las cuentas a la vez. Usa una clave por decisión, por ejemplo wave:signal_9931.
Lo que la idempotencia no cubre
- Otras formas de entrar a la misma cuenta. Una clave elimina duplicados de las solicitudes a
fxapis. Si alguien también opera la cuenta desde MetaTrader en el escritorio u otra plataforma, esas
operaciones son suyas: aparecen en los deals
con
orderId: null. - Claves de más de 24 horas. Después de ese tiempo, una clave se libera y la misma clave inicia una
orden nueva. No confíes en ella para eliminar el duplicado de un reintento de la orden de ayer;
revisa primero
GET /v1/orders. - Mover stops y cancelar.
modifyycancelno aceptan el encabezado. Los niveles son precios absolutos, así que repetir unmodifyo vuelve a fijar los mismos niveles o el broker lo rechaza como sin cambios; ninguna de las dos cosas abre ni cierra nada.
Lista de comprobación
- Cada orden de mercado, orden pendiente y cierre envía un
Idempotency-Key. - La clave se crea una vez, se guarda con la intención y se reutiliza en cada reintento.
- Las claves identifican decisiones: señal + miembro, deal de la maestra + seguidora, el id de una intención guardada.
- Abrir, cerrar y cada cierre parcial tienen claves distintas.
-
ORDER_UNRESOLVEDnunca se reenvía; la orden se consulta hasta que sale deunknown. -
ORDER_REJECTEDconretryable: truesolo se reintenta como una decisión nueva, con una clave nueva. - Se monitorea
GET /v1/orders?state=unknown.
Relacionado: Errores, Trading → Los tres fallos, y las guías de Python y Node.js para programas completos.
Copy trading
Copy an MT5 master account's trades to follower accounts, managed by fxapis: sizing, symbol mapping, risk limits, an executions log, webhooks, and no code on your side. Or build your own copier from multi-account orders.
Webhooks
Get a signed HTTPS POST when an order fills, a position opens or closes, an account changes state, or a multi-account order finishes.