# Servicio: Tipo de cambio

El primer dominio del servicio: las series oficiales de tipo de cambio del
Perú — `publicado` (SUNAT) y `cierre` (SBS vía BCRP) — con cobertura y
frescura reales, cotejo diario entre ambas y evidencia descargable por
valor. Esta página da la forma exacta de cada principio de la plataforma en
este dominio.

## Qué es y qué no

- **Es** el registro de dos series diarias, USD, con un valor por fecha de
  serie (`fecha_serie`) y por serie:

  | serie | fuente | qué es |
  |---|---|---|
  | `publicado` | SUNAT (`tipoCambio.txt`) | El tipo de cambio que SUNAT *publica* el día D. Corresponde al cierre SBS del día hábil anterior: es el valor "vigente y publicado" durante el día D. |
  | `cierre` | SBS vía las series estadísticas del BCRP | El tipo de cambio de *cierre de operaciones* del propio día hábil D, que la SBS determina al final del día. |

- `compra` y `venta` son **texto de exactamente 3 decimales** (regla SBS,
  redondeo half-up) y nunca pasan por punto flotante en el servicio;
  parséelos con un tipo decimal, no con `float`. La API siempre entrega
  ambas columnas; la norma de cada obligación indica cuál usar.
- **No es** "el tipo de cambio oficial" a secas: son series distintas con
  usos normativos distintos ([Publicado vs cierre](#publicado-vs-cierre-que-serie-usar)).
- **No es** histórico profundo todavía: ambas series empiezan el
  2026-08-21 ([Cobertura](#cobertura)).
- Solo USD: otra moneda responde `UNSUPPORTED_CURRENCY`; otra serie,
  `UNSUPPORTED_SERIES` ([Errores y límites](errores-y-limites.md#los-codigos)).

## Cobertura

Ambas series empiezan el **2026-08-21**, el día que el registro entró en
producción (`cierre` entró en producción el 2026-08-28 con la semana en
curso). La fuente de la verdad sobre cobertura es `/v1/coverage`: primera y
última fecha por serie y la lista de huecos, cada uno con su razón.

<!-- ejemplo coverage: ambas series desde el 2026-08-21, sin huecos. -->
Respuesta real (2026-09-28, revisión 4414): ambas series desde el 2026-08-21, sin huecos.

```json
{
  "series": [
    {
      "moneda": "USD",
      "serie": "cierre",
      "primera": "2026-08-21",
      "ultima": "2026-09-25",
      "total_fechas": 26,
      "huecos": [],
      "criterio_huecos": "dias habiles lun-vie menos feriados, entre primera y ultima"
    },
    {
      "moneda": "USD",
      "serie": "publicado",
      "primera": "2026-08-21",
      "ultima": "2026-09-28",
      "total_fechas": 39,
      "huecos": [],
      "criterio_huecos": "dias habiles lun-vie menos feriados, entre primera y ultima"
    }
  ],
  "dataset_revision": 4414,
  "request_id": "013a1650-843d-48aa-8668-36778d0c7989",
  "schema_version": "1.0"
}
```
<!-- /ejemplo -->

Un hueco es un día hábil sin observación vigente entre la primera y la
última fecha de la serie:

| `huecos[].razon` | significado |
|---|---|
| `fallo` | La fuente falló ese día y quedó el evento que lo prueba (fuente no disponible o payload inválido). |
| `nd_fuente` | La propia fuente declara no tener dato para esa fecha. |
| `sin_dato` | No hay dato ni causa registrada. |

El criterio de día hábil usa el [calendario](calendario.md) formal de
feriados nacionales del servicio; los "días no laborables" decretados ad hoc
no cuentan como feriados.

**Cadencia medida.** `publicado` se captura todos los días — SUNAT publica
también sábados y domingos — en la madrugada de Lima, con re-consultas
durante el día para detectar correcciones. `cierre` existe solo para días
hábiles y la fuente lo publica **al día hábil siguiente, por la tarde**:
medido entre el 01/09 y el 04/09/2026, siempre en D+1 hábil, entre las
11:00 y las 17:00 de Lima, nunca en D. En una frase: **el cierre de hoy está
disponible mañana por la tarde.**

**Histórico profundo.** Llegará por **observación retroactiva** (backfill):
el BCRP publica sus series hacia atrás por API, y el colector las observará
con exactamente el mismo mecanismo que las capturas diarias — cada valor
histórico será una observación con sus bytes de evidencia, su hash y su
procedencia (`fuente: bcrp`), y su `capturado_en` dirá honestamente cuándo
se observó. Se anunciará aquí cuando exista, no antes.

## Frescura

`/v1/rates/latest` incluye el bloque `frescura`, que compara la última
`fecha_serie` vigente contra la **fecha esperada** de la serie, en zona
`America/Lima`: `estado` (`al_dia` o `atrasada`), `dias_atraso` (días
hábiles; `n_dias_habiles` es su alias histórico), `fecha_esperada`,
`hoy_lima`.

- Para `publicado`, la fecha esperada es **hoy en Lima**: SUNAT publica la
  tabla del día todos los días.
- Para `cierre`, la fecha esperada es **el último día hábil cuyo cierre ya
  tuvo tiempo de publicarse**: el cierre de un día hábil D aparece el
  siguiente día hábil por la tarde, así que D se espera recién cuando ese
  siguiente día hábil pasó las 17:30 de Lima. Sábado y domingo esperan el
  jueves; el lunes antes de las 17:30 el jueves y desde las 17:30 el
  viernes; tras un feriado, el hábil anterior a él (regla vigente desde el
  2026-09-06; la del 2026-09-04 pedía el viernes ya el sábado).

**El día normal** — la serie `cierre`, real:

<!-- ejemplo rates_latest_cierre: un miércoles, el cierre esperado es el del martes y está. -->
Respuesta real (2026-09-28, revisión 4414): un miércoles, el cierre esperado es el del martes y está.

```json
{
  "fecha_serie": "2026-09-25",
  "compra": "3.416",
  "venta": "3.425",
  "fuente": "bcrp",
  "cotejos": [
    {
      "cotejo_id": "b659afc7-4689-40e2-8bcc-0e24700fd657",
      "contraparte": "da733871-75f8-41a9-8536-68d9b8bfcf41",
      "resultado": "ok",
      "regla": "publicado_vs_cierre_prev_habil-1.0",
      "revision": 4411,
      "ts": "2026-09-28T23:30:03.291488Z"
    },
    {
      "cotejo_id": "a25934c1-f2b4-42c0-8c0a-113b2bda4c6c",
      "contraparte": "b749c174-6b6c-410b-abff-6fbbc5819054",
      "resultado": "ok",
      "regla": "publicado_vs_cierre_prev_habil-1.0",
      "revision": 4411,
      "ts": "2026-09-28T23:30:03.291488Z"
    },
    {
      "cotejo_id": "296edd5e-e905-46c7-a865-530de172356a",
      "contraparte": "9ebf8e58-8e12-49c0-ad35-d99c23436576",
      "resultado": "ok",
      "regla": "publicado_vs_cierre_prev_habil-1.0",
      "revision": 4411,
      "ts": "2026-09-28T23:30:03.291488Z"
    }
  ],
  "tiene_discrepancia_abierta": false,
  "marcas": [
    "capturada"
  ],
  "observacion_id": "1dab2387-8ba4-4e45-9997-4593038f6038",
  "capturado_en": "2026-09-28T23:30:02.000000Z",
  "superseded": false,
  "superseded_por": null,
  "frescura": {
    "estado": "al_dia",
    "n_dias_habiles": 0,
    "dias_atraso": 0,
    "fecha_esperada": "2026-09-25",
    "hoy_lima": "2026-09-28",
    "zona": "America/Lima"
  },
  "dataset_revision": 4414,
  "request_id": "a0082e21-76c4-49f0-8088-51655c394ddc",
  "schema_version": "1.0"
}
```
<!-- /ejemplo -->

**El domingo** — ejemplo ilustrativo con la forma exacta de la respuesta y la
regla vigente: un domingo (2026-09-06), la serie `cierre` con dato del jueves
(2026-09-03) está `al_dia`, porque el viernes recién se espera el lunes desde
las 17:30:

```json
"frescura": {"estado": "al_dia", "n_dias_habiles": 0, "dias_atraso": 0,
             "fecha_esperada": "2026-09-03", "hoy_lima": "2026-09-06",
             "zona": "America/Lima"}
```

**El registro atrasado** — si la última fecha vigente está detrás de la
esperada, el servicio lo dice en vez de ocultarlo (ilustrativo: la fuente
lleva dos días hábiles sin poder capturarse):

```json
"frescura": {"estado": "atrasada", "n_dias_habiles": 2, "dias_atraso": 2,
             "fecha_esperada": "2026-08-27", "hoy_lima": "2026-08-27",
             "zona": "America/Lima"}
```

Un registro atrasado tiene además sus alertas abiertas (`/v1/alerts`); para
monitoreo continuo de todas las series existe `/v1/health/freshness`
([plataforma](errores-y-limites.md#alertas-y-salud-la-plataforma-se-vigila-a-si-misma)).

## Consulta histórica

- `GET /v1/rates?currency=USD&series=…&from=…&to=…` devuelve las
  observaciones vigentes del rango (inclusivo, máximo 5 años, 366 ítems por
  página a revisión fija — [paginación](errores-y-limites.md#paginacion-a-revision-fija)).
- `as_of_revision=N` congela la consulta en la revisión N: exactamente lo
  que el registro contenía entonces, respuesta inmutable y cacheable
  ([el modelo](modelo-de-evidencia.md#la-revision-la-linea-de-tiempo-del-registro)).
- **Una fecha sin dato** responde `404 NO_OBSERVATIONS` — ningún ítem,
  ningún sustituto; `/v1/coverage` dice desde cuándo sí. Es la forma en este
  dominio del principio [sin relleno](modelo-de-evidencia.md#sin-relleno-nunca-otra-fecha-como-si-fuera-la-pedida).
  Consulta real pidiendo julio de 2026, anterior a la cobertura:

```sh
curl -s -H "Authorization: Bearer $EKIA_TOKEN" \
  'https://api.eurekia.pe/v1/rates?currency=USD&series=publicado&from=2026-07-01&to=2026-07-31'
```

<!-- ejemplo error_no_observations: un 404 honesto, distinto por diseño de `UNSUPPORTED_CURRENCY`. -->
Respuesta real (2026-09-28): un 404 honesto, distinto por diseño de `UNSUPPORTED_CURRENCY`.

```json
{
  "code": "NO_OBSERVATIONS",
  "message": "no observations in the requested range",
  "request_id": "41799456-0758-4f8a-b9e8-e8ab6572e14f",
  "retryable": false
}
```
<!-- /ejemplo -->

## Integridad

El control de integridad de este dominio es el **cotejo entre fuentes**
(`cotejos[]`, `tiene_discrepancia_abierta` en cada ítem). En producción
desde el 28/08/2026: la regla `publicado_vs_cierre_prev_habil-1.0` compara
el `publicado` de cada día D contra el `cierre` del último día hábil
anterior a D — el mismo número por construcción, así que un cotejo `ok`
confirma ambas cadenas de captura y uno `discrepante` delata un error de
transmisión, parseo o publicación.

Alcance declarado: SUNAT y BCRP comparten cadena de origen (la tabla de la
SBS); el cotejo no detecta un error aguas arriba, común a ambas fuentes.
Los valores posibles, el día discrepante y qué hacer con él están en
[los controles de integridad](modelo-de-evidencia.md#los-controles-de-integridad).
Además, cada valor nuevo pasa un control de plausibilidad (marca
`plausibilidad_baja`, sin descartar el dato).

## Evidencia

La evidencia de cada valor es **pequeña**: los bytes exactos del payload de
la fuente (24 bytes del `tipoCambio.txt`; el paquete sellado del colector
para `cierre`), de `tipo: observacion`. `/v1/observations/{id}` da
`evidencia_id` y el `fragmento` exacto del que se parseó el valor;
`/v1/evidence/{id}` los metadatos y `/v1/evidence/{id}/descarga` los bytes
directos con `ETag` = sha256. Cada descarga cuenta apenas sus bytes en la
cuota. Ejecutado real, paso a paso:
[Verifique la evidencia en 60 segundos](verificar-evidencia.md#ejecutado-real-el-tipo-de-cambio).

## Restricciones y límites

- Solo USD y solo dos series; rango máximo de 5 años por consulta.
- Sin histórico anterior al 2026-08-21 hasta el backfill ([Cobertura](#cobertura)).
- El servicio **no arbitra** entre fuentes ni da asesoría tributaria: expone
  la discrepancia y describe usos típicos
  ([Qué no hacemos](que-no-hacemos.md#no-arbitramos-entre-fuentes-ni-damos-asesoria)).
- Una corrección de la fuente posterior a la última re-consulta del día se
  detecta solo por cotejo posterior: las fuentes no exponen su propio
  histórico.

## Qué guardar

`observacion_id` y `dataset_revision` con cada valor que tome
([qué guardar](modelo-de-evidencia.md#que-guardar-en-su-sistema)). Con ellos,
`/v1/observations/{id}` reconstruye la evidencia y `as_of_revision` lo que
contenía el registro cuando lo tomó.

## Publicado vs cierre: qué serie usar

No existe "el tipo de cambio oficial" a secas: existen **series distintas con
usos normativos distintos**. Esta guía describe usos típicos; no es asesoría
legal ni tributaria — la elección para un caso concreto valídela con su
contador.

| | `publicado` | `cierre` |
|---|---|---|
| Usos típicos | Obligaciones que la norma ancla al valor "publicado" en una fecha: IGV (nacimiento de la obligación), detracciones, retenciones, percepciones. | Obligaciones ancladas al cierre de una fecha: valuación de saldos en moneda extranjera al cierre del ejercicio (Impuesto a la Renta), estados financieros. |
| Columna | La norma de cada obligación indica compra o venta; la API siempre entrega ambas y usted elige la columna que su regla exige. | (ídem) |
| Disponibilidad | El día D en la madrugada de Lima. | El día hábil siguiente a D, por la tarde (medido: entre 11:00 y 17:00 de Lima). Fines de semana y feriados no tienen cierre propio. |

La consecuencia práctica de la diferencia: el `publicado` del martes y el
`cierre` del lunes hábil son el mismo número por construcción — pero *se
citan distinto*. Cuando una norma dice "publicado en la fecha X", la serie
correcta es `publicado` en X, no el cierre de X−1, aunque coincidan: ante una
fiscalización usted sustenta la serie y la fecha que la norma nombra. Por eso
el servicio las sirve separadas y con nombre, en lugar de una sola serie
ambigua.
