> ## Documentation Index
> Fetch the complete documentation index at: https://docs.innova-trading.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Proveedores de Señales

> Enviá las señales de tu estrategia a Innova Trading por HTTP, sin compartir tu código

## Qué es

Si corrés tu propia estrategia, la podés conectar al motor de copia de Innova
**sin compartir tu código con nosotros**. Vos corrés la estrategia en tu
propio servidor y nos mandás solo la señal ya generada. Nosotros la
validamos, comprobamos que siga fresca y la copiamos a las cuentas
suscritas.

<Note>
  El proveedor nunca nos envía su código, solo señales.
</Note>

Todos los ejemplos usan el host de producción `https://cloud.innova-trading.com`.
Desarrollá siempre primero contra el **endpoint de prueba** (`/signals/test`).

## Cómo pedir acceso

El acceso lo da Innova. Todavía no hay alta por cuenta propia.

1. Pedile a tu contacto en Innova un **token de proveedor** y el
   **`x-community-slug`** de tu comunidad (por ejemplo `mi-comunidad`).
2. Guardá los dos en privado. Nunca los pegues en chats, mails ni repositorios.
3. Empezá por el endpoint de prueba (`/signals/test`) antes de mandar algo real.

## Endpoints

| Método | URL | Qué hace |
| - | - | - |
| `POST` | `/api/v1/provider/signals` | **Real.** Valida, filtra y publica la señal al motor de copia. |
| `POST` | `/api/v1/provider/signals/test` | **Prueba.** Hace las mismas validaciones y te dice qué habría pasado, **sin publicar ni abrir nada**. |
| `GET` | `/api/external/bars?symbol=&timeframe=&limit=` | **Velas** para tu estrategia (hasta 1000 por pedido; la última sigue en formación). Mismo token y `x-community-slug`. |
| `GET` | `/api/v1/provider/signals/{signal_id}` | Relee una señal que ya mandaste, con tu propio `signal_id`. |
| `GET` | `/api/v1/provider/signals/decisions` | Últimas decisiones sobre tus señales, **incluyendo los rechazos y su motivo**. Acepta `?limit=` (1 a 200, por defecto 50). |

## Autenticación

Dos cabeceras en todas las llamadas:

```text theme={null}
Authorization: Bearer <TU_TOKEN>
x-community-slug: <TU_COMUNIDAD>
Content-Type: application/json
```

* Acá la llave **no** es un texto libre: el token es personal y te lo entrega
  Innova (ver "Cómo pedir acceso"). La cabecera `x-community-slug` es
  **obligatoria** en todas las llamadas.
* El token es personal: quien lo tenga puede mandar
  señales en tu nombre, así que no lo compartas ni lo pegues en chats o mails.
* Sin token, o con un token revocado: **HTTP 401**.
* Tus señales quedan aisladas dentro de tu comunidad.

## El JSON que nos mandás

```json theme={null}
{
  "signal_id": "mi-estrategia-EURUSD-15m-20261011T1430Z",
  "symbol": "EURUSD",
  "direction": "buy",
  "entry": 1.10000,
  "generated_at": "2026-10-11T14:30:05.120Z",
  "sl": 1.09500,
  "tps": [1.10500, 1.11000, 1.11500],
  "lots": 0.10,
  "risk_percent": 1.0,
  "strategy": "mi-estrategia",
  "comment": "cruce confirmado en M15",
  "timeframe": 15
}
```

| Campo | Tipo | Obligatorio | Detalle |
| - | - | - | - |
| `signal_id` | texto (1-120) | **Sí** | **Tu** identificador. Es la clave de idempotencia. |
| `symbol` | texto (2-20) | **Sí** | Ej. `EURUSD`, `XAUUSD`. Lo pasamos a mayúsculas. |
| `direction` | texto | **Sí** | `buy` o `sell` (también aceptamos `long` / `short`). |
| `entry` | número o `"market"` | **Sí** | Precio de entrada, o `"market"` para usar el precio vivo. |
| `generated_at` | ISO-8601 | **Sí** | Momento en que **tu estrategia** generó la señal, no el del envío. Usá UTC con `Z`. |
| `sl` | número | No | Stop loss. |
| `tps` | lista de números | No | Take profits en orden (TP1, TP2, TP3...). |
| `lots` | número > 0 | No | Lote sugerido. El lote final lo define cada seguidor. |
| `risk_percent` | número 0-100 | No | Riesgo sugerido. Ídem. |
| `strategy` | texto (máx. 40) | No | Nombre de la estrategia. Define a qué fuente se suscriben las cuentas. |
| `comment` | texto (máx. 500) | No | Nota libre; te la devolvemos tal cual. |
| `timeframe` | entero >= 0 | No | En minutos. |

<Warning>
  **No controlamos que `sl` y `tps` estén del lado correcto de `entry`.** Hoy
  el servidor no rechaza un stop loss del lado equivocado (por ejemplo un `sl`
  por encima de `entry` en un `buy`). Validalo en tu propio código antes de
  enviar: un stop mal puesto se acepta tal como lo mandás.
</Warning>

<Warning>
  **Un campo desconocido hace fallar la llamada con HTTP 422.** Es a propósito:
  si escribís `stop_loss` en lugar de `sl`, preferimos avisarte antes que
  aceptar una señal sin stop. Un `generated_at` sin zona horaria se interpreta
  como UTC. Mantené el reloj de tu servidor sincronizado por NTP.
</Warning>

## Idempotencia

La combinación **(tu proveedor, tu `signal_id`)** es única de nuestro lado.

* Si reintentás por un corte de red, mandá **el mismo `signal_id`** (y el
  mismo cuerpo).
* La segunda llamada devuelve `"status": "duplicate"` con el resultado de la
  primera y no se abre ninguna operación adicional.
* Nunca reutilices un `signal_id` para una señal distinta: la segunda quedaría
  descartada como repetida.
* También respondemos `duplicate` con el código `active_same_direction` si ya
  tenés una señal abierta en el mismo símbolo y la misma dirección. No
  apilamos posiciones.

Un buen `signal_id` es determinista: estrategia + símbolo + timeframe + hora de
la vela. Así, reiniciar tu proceso sobre la misma vela nunca duplica.

## Frescura

Una señal que llega tarde no se puede copiar a un precio que ya no existe.
Antes de publicar evaluamos:

| Regla | Qué mira | Valor por defecto |
| - | - | - |
| Antigüedad | `now - generated_at` | máximo **120 s** |
| Reloj adelantado | `generated_at` en el futuro | tolerancia **30 s** |
| Deriva de precio | cuánto se movió el mercado **en contra** de tu `entry` | máximo **0,2 %** |
| Distancia absoluta de precio | cuánto se aleja tu `entry` del precio vivo, **para cualquiera de los dos lados** | máximo **1 %** |
| Solo si mejoró | acepta señales tardías únicamente si el precio mejoró para el seguidor | **desactivado** |

Hoy estos valores son **globales** (iguales para todos los proveedores). Si tu
estrategia opera en marcos más largos y 120 s te queda corto, avisanos.

La regla de deriva de 0,2 % es direccional: un movimiento **a favor** nunca
rechaza la señal. La de 1 % absoluto no: un `entry` a más de 1 % del mercado
vivo, para cualquiera de los dos lados, se rechaza con `price_drift_exceeded`,
porque ese precio no existe en el mercado. Si no hay cotización viva para
comparar, una señal con `entry` numérico también se rechaza (`no_market_price`,
o `price_drift_exceeded` si el precio no se puede medir).

## Códigos de rechazo

Ramificá siempre por `code`, nunca por el texto de `detail`.

| `code` | Qué pasó | Qué hacer |
| - | - | - |
| `stale` | La señal llegó más vieja que el máximo permitido. | Revisá la latencia de tu proceso; no reintentes con el mismo `generated_at`. |
| `clock_ahead` | Tu `generated_at` está en el futuro más allá de la tolerancia. | El reloj de tu servidor está mal. Sincronizá por NTP y usá UTC. |
| `price_drift_exceeded` | El mercado se movió en contra más del límite, o tu `entry` está a más de 1 % del precio vivo. | Es correcto que se rechace: entrar ahí sería otra operación. |
| `no_improvement` | Señal tardía y el precio no mejoró (regla opcional, si está activada). | Nada; es el comportamiento pedido. |
| `missing_generated_at` | Falta `generated_at` o no es una fecha ISO-8601 válida. En esta ruta la API responde antes con un HTTP `422` de validación (el campo es obligatorio en el contrato), así que este código no llega al cliente. | Agregalo. |
| `no_market_price` | Mandaste `"entry": "market"` y no teníamos precio vivo del símbolo. | Reintentá, o mandá un precio numérico. |
| `bus_unavailable` | Guardamos la señal pero nuestro bus interno estaba caído: **no se copió**. | Avisanos. Es un problema nuestro. |
| `duplicate` | Ya habíamos recibido ese `signal_id`. | Nada. |
| `active_same_direction` | Ya hay una señal tuya abierta en la misma dirección. | Nada. |

**Nada se descarta en silencio.** Toda decisión, aceptada o rechazada, queda
registrada y la podés consultar en `GET /api/v1/provider/signals/decisions`.

## La respuesta

```json theme={null}
{
  "accepted": true,
  "status": "published",
  "code": "ok",
  "detail": "Señal aceptada y publicada al motor de copia.",
  "signal_id": "mi-estrategia-EURUSD-15m-20261011T1430Z",
  "signal_source": "provider:aaaaaaaa:mi-estrategia",
  "symbol": "EURUSD",
  "direction": "buy",
  "generated_at": "2026-10-11T14:30:05.120000+00:00",
  "received_at": "2026-10-11T14:30:05.480000+00:00",
  "published": true,
  "dry_run": false,
  "freshness": { "accepted": true, "code": "fresh", "age_seconds": 0.36 }
}
```

(Respuesta abreviada: trae más campos.)

* `status`: `published` (se copió), `simulated` (prueba), `duplicate` o `rejected`.
* `published` es `true` **solo** si la señal llegó al motor de copia. Si es
  `false`, mirá `code`.
* `signal_source` es el identificador con el que se suscriben las cuentas a tu
  estrategia. Pasáselo al dueño de tu comunidad.
* `received_at - generated_at` es tu latencia de entrega. Registrala.

Códigos HTTP: `200` siempre que la llamada haya sido bien formada (incluso si
la señal fue rechazada: el motivo va en `code`), `401` sin token válido, `409`
si no se pudo guardar la señal por un conflicto raro de unicidad (reintentá con otro
`signal_id`), `422` si el JSON está mal, `429` si superás 120 señales por minuto.

## Prueba (sandbox)

`POST /api/v1/provider/signals/test` es donde deberías desarrollar. Hace lo
mismo que el real (valida el JSON, corre el filtro de frescura, resuelve el
símbolo contra las cuentas suscritas, calcula el lote y el plan de cierres
parciales) y devuelve exactamente qué pasaría, incluidos los motivos de
rechazo. **Nunca** publica ni abre nada.

La diferencia visible: `"status": "simulated"`, `"dry_run": true`,
`"published": false`, y un bloque extra `execution_preview`. Si
`targets_matched` es `0`, tu señal es válida pero todavía no hay ninguna
cuenta suscrita a tu `signal_source` (eso lo configura el dueño de la
comunidad).

## Ejemplos

<CodeGroup>
  ```python Python theme={null}
  import requests
  from datetime import datetime, timezone

  BASE = "https://cloud.innova-trading.com"
  headers = {
      "Authorization": "Bearer <TU_TOKEN>",
      "x-community-slug": "<TU_COMUNIDAD>",
  }
  now = datetime.now(timezone.utc)
  signal = {
      "signal_id": "mi-estrategia-EURUSD-15m-20261011T1430Z",  # mismo id en cada reintento
      "symbol": "EURUSD",
      "direction": "buy",
      "entry": 1.10000,
      "generated_at": now.strftime("%Y-%m-%dT%H:%M:%S.") + f"{now.microsecond // 1000:03d}Z",
      "sl": 1.09500,
      "tps": [1.10500, 1.11000],
      "strategy": "mi-estrategia",
      "timeframe": 15,
  }

  # Primero la prueba: /signals/test no publica nada
  resp = requests.post(f"{BASE}/api/v1/provider/signals/test", json=signal,
                       headers=headers, timeout=10)
  body = resp.json()
  print(resp.status_code, body["status"], body["code"])
  ```

  ```bash curl theme={null}
  curl -X POST "https://cloud.innova-trading.com/api/v1/provider/signals/test" \
    -H "Authorization: Bearer <TU_TOKEN>" \
    -H "x-community-slug: <TU_COMUNIDAD>" \
    -H "Content-Type: application/json" \
    -d '{
      "signal_id": "mi-estrategia-EURUSD-15m-20261011T1430Z",
      "symbol": "EURUSD",
      "direction": "buy",
      "entry": 1.10000,
      "generated_at": "2026-10-11T14:30:05Z",
      "sl": 1.09500,
      "tps": [1.10500, 1.11000],
      "strategy": "mi-estrategia"
    }'
  ```

  ```bash curl (decisiones) theme={null}
  curl "https://cloud.innova-trading.com/api/v1/provider/signals/decisions?limit=20" \
    -H "Authorization: Bearer <TU_TOKEN>" \
    -H "x-community-slug: <TU_COMUNIDAD>"
  ```
</CodeGroup>

Para enviar de verdad, la misma llamada cambiando `/signals/test` por `/signals`.

<Note>
  Reemplazá `<TU_TOKEN>` y `<TU_COMUNIDAD>` por los valores que te damos.
  Nunca los subas a un repositorio.
</Note>

## Checklist de integración

1. Pedile a tu contacto en Innova el token y el `x-community-slug`.
2. Mandá una señal a `/signals/test`. Esperá `"status": "simulated"`.
3. Provocá un rechazo a propósito (mandá un `generated_at` de hace una hora) y
   confirmá que tu código maneja `code: "stale"`.
4. Mandá un campo inventado y confirmá que tu código maneja el `422`.
5. Verificá que tu código reintente con el mismo `signal_id` cuando una llamada falla (el kit de Python, cuando esté disponible, lo hace solo). El sandbox siempre responde `simulated`: `duplicate` solo aparece en `/signals` real, así que lo vas a ver ahí por primera vez. Es lo esperado.
6. Recién ahí pasá a `/signals`.
7. Pasanos el `signal_source` que devuelve la respuesta, para suscribir las
   cuentas.

## Convertir tu indicador de PineScript

<Note>
  **Próximamente.** Esta sección y la siguiente describen el kit de Python
  (`innova-signal-kit`), que todavía no se puede descargar. Si querés acceso
  anticipado, pedíselo a tu contacto en Innova. Mientras tanto usá el contrato
  HTTP descripto arriba, que funciona hoy.
</Note>

Si tu estrategia vive en TradingView como PineScript, no la reescribís a mano. Todo
esto viene en el kit de Python (un paquete pip, ver "Kit de Python listo para usar"
más abajo). Trae un prompt listo (`PROMPT-CONVERTIR-PINE.md`, que `innova-provider init` copia a tu carpeta, o se imprime con `innova-provider prompt`)
que pegás en un asistente de IA junto con tu código Pine, y te devuelve tu
`strategy.py` en el formato que espera el kit.

<Warning>
  **Innova no recibe tu código Pine.** Pero la IA a la que se lo pegues sí: un
  asistente en la nube (Claude, ChatGPT u otro) es un tercero y recibe tu Pine. Si tu
  estrategia es sensible, usá una IA local o revisá antes las condiciones de privacidad
  de ese servicio.
</Warning>

1. Pegá el prompt y tu código Pine en tu asistente, y guardá el resultado como `strategy.py`.
2. Exportá de TradingView tus velas y las señales de tu indicador a dos archivos CSV. La lista de operaciones del Strategy Tester trae la hora de la vela *siguiente* a la señal: en ese caso agregá `--expected-shift 1` al comando del paso 3.
3. Corré `innova-provider parity --bars bars.csv --expected expected.csv --symbol EURUSD --timeframe 15 --strategy-name mi-estrategia`.
4. **Criterio de aceptación: coincide el 99 % de las señales.** `--warmup` por defecto es tu
   `REQUIRED_BARS` (en vivo, `evaluate` siempre arranca con esa historia completa): no hace
   falta pasarlo. **Verificá la convergencia:** corré la herramienta con dos valores de
   `REQUIRED_BARS` (por ejemplo 300 y 600) y quedate con el mayor si el resultado mejora; la
   herramienta también imprime un `AVISO:` cuando con más historia sube el %. Los indicadores
   con estado que depende del camino (supertrend, ATR/RMA/EMA, trailing stops) no convergen
   con "5 veces el período": medido con un supertrend + RSI, `REQUIRED_BARS = 70` dio 94 % y
   `150` dio 100 %; empezá en 300. La herramienta responde `NO APROBADO: historia insuficiente`
   (aun con 100 %) cuando el CSV tiene menos de 4 veces `REQUIRED_BARS` velas (el mensaje dice
   la cifra exacta) o cuando se comparan menos de 10 señales. Tu CSV tiene que cumplir las dos
   condiciones: al menos 4 veces `REQUIRED_BARS` velas Y al menos 10 señales de tu Pine en ese
   tramo; si tu Pine da menos, exportá más historia hasta alcanzarlas (con M15 y poca actividad
   pueden hacer falta meses); las señales del Pine anteriores a
   la primera vela evaluada se ignoran y solo se informan. Si igual no llega, la herramienta lista cada diferencia.
   Pegáselas a tu asistente y pedile que corrija la conversión.
5. Recién después, pasá al sandbox (`/signals/test`) y, al final, a `/signals`.

Hay cosas que no se convierten solas: indicadores que leen otro timeframe
(`request.security`), lógica dentro de la vela y estrategias que dependen de una
posición abierta. El prompt las marca como "Dudas" para que decidas.

## Kit de Python listo para usar

<Warning>
  **Próximamente, pedinos acceso.** El kit no está publicado: todavía no hay
  `pip install` público. Pedíselo a tu contacto en Innova. No dependas de él
  para tu integración hoy; usá el contrato HTTP (ver "¿Preferís otro lenguaje?").
</Warning>

Tenemos un kit en Python (cliente con reintentos y medición de latencia, un
runner que espera el cierre de cada vela y una plantilla de estrategia donde
solo editás `strategy.py`). Es un paquete pip llamado `innova-signal-kit`, con un
único comando: `innova-provider`.

### Instalación

Python 3.9 o más nuevo.

Las instrucciones de instalación te las pasamos cuando te den acceso al kit.
Todavía no hay paquete en un índice público.

Tu `strategy.py` **no** es parte del paquete: vive en tu propia carpeta, en tu
servidor, e Innova nunca lo recibe.

### Flujo

```bash theme={null}
innova-provider init mi-estrategia    # crea la carpeta: strategy.py, config.env, README.md
cd mi-estrategia
# 1. completá config.env (token y slug: te los damos nosotros)
# 2. reemplazá evaluate() en strategy.py por tu lógica
innova-provider parity --bars bars.csv --expected expected.csv \
    --symbol EURUSD --timeframe 15 --strategy-name mi-estrategia   # 99 % o más
innova-provider history --bars-csv bars.csv ...   # opcional: tus señales pasadas (marcadas backtest)
innova-provider run --dry-run --once  # sandbox: no publica nada
innova-provider run                   # real
```

`run` acepta `--dry-run`, `--once`, `--env-file`, `--margin`, `--bars`,
`--fetch-attempts` y `-v`; `parity` acepta las opciones descritas arriba. El
`README` dentro del kit explica todo.

### Si la red falla

* El cliente hace hasta 4 intentos por envío (el primero y 3 reintentos). Si igual no llega, el log dice
  `SEÑAL PERDIDA` con la hora de la vela y el `signal_id`.
* Si la causa fue la red o un 5xx, el runner **la reintenta una vez en el ciclo siguiente con
  el mismo `signal_id`** (el servidor no duplica), solo mientras la vela siga dentro de los 120 s
  de frescura (en M1 sí, en velas más largas no; el log lo dice). Un 401 o un 422 no se reintentan.
* Tras un corte largo el log dice `Se omitieron N velas sin evaluar (desde X hasta Y)`: el
  runner solo evalúa la última vela cerrada, así que las señales de esas velas no se envían ni
  se reponen (llegarían viejas). Un reinicio entre el cierre de una vela y el próximo ciclo
  también pierde esa vela.
* En `--dry-run` no se aplica el límite de 120 s de antigüedad de la vela: una prueba con el
  mercado cerrado pasa y esa misma vela se saltearía en real. La latencia
  `generated_at->received_at` que se informa incluye los reintentos del cliente.

### Tus señales pasadas (historial)

```bash theme={null}
innova-provider history --bars-csv bars.csv --symbol EURUSD --timeframe 15 \
    --strategy-name mi-estrategia             # tus últimas 50 señales pasadas, con tus propias velas
innova-provider history --last 100            # velas bajadas de /api/external/bars
```

**Qué es.** Tu estrategia corre **en tu máquina** sobre velas pasadas y da tus últimas N señales
(50 por defecto, `--last N`). Usa la misma ventana que en vivo (`REQUIRED_BARS` velas cerradas, la
última es la vela recién cerrada). Las velas salen de `--bars-csv` (mismo formato que `parity`) o,
si no lo pasás, de `/api/external/bars` paginando hacia atrás con `before_time` (hasta 1000 por
pedido, como máximo `--max-bars`, 10 000 por defecto; si no alcanza para N señales, el comando dice
cuántas encontró y por qué se detuvo). El resultado es `history.json` en tu carpeta: cada señal
trae solo los campos del contrato más `kind: "backtest"` y `source: "kit-history"`; `generated_at`
es el cierre de la vela de la señal y el `signal_id` empieza con `backtest-`. El comando también
imprime un resumen (cuántas señales, rango de fechas, buy/sell).

**Qué no es.** No es un backtest con PnL: nada simula SL ni TP y no hay curva de capital; solo
sabés qué señales habría dado tu estrategia. Está **siempre** marcado `backtest`, así que no se
puede confundir con una señal en vivo, y nunca se mezcla con el registro en vivo. Tu código y tus
parámetros no salen de tu máquina: solo viajan los campos del contrato.

**Límite conocido hoy.** El servidor acepta `before_time` pero todavía no lo aplica, así que bajar
más de una página de velas no funciona: el comando se detiene con un mensaje claro. Usá
`--bars-csv` con tu propio historial.

**Enviarlo: el envío automático desde el kit todavía no está disponible.** El endpoint
`POST /api/v1/provider/signals/history` ya existe en el servidor, pero el flag `--send` del kit
todavía no está disponible (`--send` falla con un error y no envía nada). Si lo enviás vos por
HTTP, no mandes `history.json` tal cual: tiene las claves de primer nivel `kind`, `source` y
`summary`, y el endpoint rechaza las claves desconocidas con un 422 `extra_forbidden`. El cuerpo de
`POST /api/v1/provider/signals/history` es `{"signals": [ ... ]}`: solo el array que está bajo la
clave `signals` de `history.json`, sin `kind`, `source` ni `summary`. Mandá como máximo 50 señales
por pedido (partí en lotes si tenés más; una lista más larga recibe un 422 `too_long`). Cada
elemento de ese array trae los campos del contrato más `kind: "backtest"` y `source`, que el
endpoint acepta por señal; en el historial `entry` tiene que ser un número.

### Límites del historial

Estos límites aplican al historial que enviás a `POST /api/v1/provider/signals/history` (el
endpoint al que se refiere la nota "Enviarlo" de arriba).

| Límite | Por defecto | Qué controla |
| - | - | - |
| Retención | **90 días** | Qué tan vieja puede ser una señal pasada: su `generated_at` no puede ser más antiguo que esto. |
| Tope por proveedor | **500 señales** | Cuántas señales de historial podés tener guardadas en total. |

**Cada comunidad puede tener los suyos.** El equipo de Innova (admin de plataforma) puede ajustar
ambos límites para tu comunidad sin release (rango permitido: retención de 7 a 365 días,
tope de 50 a 5000 señales). Los valores vigentes para vos vuelven en cada respuesta del endpoint,
en los campos `retention_days` y `max_per_provider`: guiate por esos, no por los de arriba.

Cuando una señal no se guarda, su entrada en `results` trae `status: "rejected"` y uno de estos
`code`. El resto del lote se procesa igual.

| `code` | Por qué | Qué hacer |
| - | - | - |
| `too_old` | `generated_at` es más viejo que la ventana de retención. | Mandá solo señales dentro de la ventana (`retention_days` en la respuesta). Las más viejas no se aceptan; no las reintentes. |
| `provider_cap_reached` | Ya tenés `max_per_provider` señales de historial guardadas. No se borra nada por vos. | Dejá de enviar más. Si necesitás más lugar, pedile al equipo de Innova (admin de plataforma) que revise el tope de tu comunidad. Reintentar la misma señal no cambia nada. |
| `generated_at_in_future` | `generated_at` está en el futuro, o el reloj de tu máquina va adelantado. | Revisá el reloj de tu máquina y la hora de vela que enviás. Corregí el valor y reenviá con el mismo `signal_id`. |

Las señales que ya enviaste se responden como `duplicate` incluso estando en el tope, así que
reintentar un lote es siempre seguro. Bajar un límite nunca borra lo que ya está guardado.

### ¿Preferís otro lenguaje?

Este es el camino que funciona hoy. No hay SDK de JS ni de otro lenguaje, y no lo prometemos: el contrato es HTTP
simple y sirve desde cualquier lenguaje. Lo que tenés que implementar:

* **Endpoints** (ver "Endpoints" arriba): leer velas de `/api/external/bars`,
  enviar señales a `/api/v1/provider/signals/test` (sandbox) y después a
  `/api/v1/provider/signals`.
* **Cabeceras**: `Authorization: Bearer <token>` y `x-community-slug`.
* **Campos**: exactamente los de "El JSON que nos mandás"; un campo desconocido da `422`.
* **Stop y objetivos**: controlá de tu lado que `sl` y `tps` estén del lado
  correcto de `entry`; el servidor no lo controla.
* **`signal_id`**: determinista (estrategia + símbolo + timeframe + hora de la
  vela), y reintentar con el mismo. Ver "Idempotencia".
* **Guard de 120 s**: generá la señal al cierre de la vela y enviala solo si ese
  cierre fue hace 120 s o menos. `generated_at` es "ahora" y el servidor la
  rechaza como `stale` pasados 120 s (ver "Frescura"). No envíes la señal de una
  vela vieja (fin de semana, datos atrasados).
* **Velas**: usá solo las cerradas; descartá la que está en formación.

Las secciones de arriba (endpoints, JSON, idempotencia, frescura, códigos de
rechazo) son la referencia del contrato. El kit de Python es la implementación
de referencia: podés portar su `runner` y su módulo `parity` línea por línea.

## Próximos pasos

<CardGroup cols={2}>
  <Card title="Autenticación" icon="key" href="/es/authentication">
    Cómo funcionan los tokens de API
  </Card>

  <Card title="Mejores Prácticas de Señales" icon="check" href="/es/guides/signal-best-practices">
    Tips para mejores señales
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.