Mercado eléctrico mayorista de El Salvador 7 de 8
Mercado mayorista VII. Validación automática de declaraciones, con código que corre
Los seis artículos anteriores describieron un mecanismo que descansa sobre una sola cosa: que los datos declarados sean correctos. Nadie mide en tiempo real el precio del combustible de una central. Se declara, se valida contra una estructura aprobada, y el resultado ordena el parque y fija el precio de la energía.
Validar eso a mano no escala, y no por volumen. Cuatro participantes térmicos por cincuenta y dos semanas son doscientas declaraciones al año, que una persona revisa sin problema. No escala por otra razón: una revisión manual no deja rastro de qué se revisó. Seis meses después, cuando alguien pregunte por qué se aceptó la declaración de la semana 34, la respuesta tiene que ser un registro, no un recuerdo.
Este artículo es el código de esa validación. Está en el repositorio del sitio,
en proyectos/validador-cvc,
corre con la biblioteca estándar de Python y no necesita nada instalado. Las
cifras que aparecen abajo son la salida de correrlo, no un ejemplo escrito a
mano.
La decisión de diseño que ordena todo
Un validador se puede escribir de dos maneras. La primera es una función que recibe la declaración y va encadenando condicionales. Funciona, se escribe en una tarde, y es imposible de auditar: para saber qué valida hay que leer el código completo, y para agregar una regla hay que meter mano en el medio.
La segunda es tratar cada regla como un objeto declarativo y dejar que un motor tonto las recorra. Eso cuesta un poco más al principio y devuelve tres cosas que la primera no da. El catálogo se puede enumerar sin ejecutarlo, se puede versionar, y cada hallazgo puede decir contra qué numeral se evaluó.
@dataclass(frozen=True)
class Regla:
id: str
titulo: str
ambito: Ambito
severidad: Severidad
referencia: str # el numeral del ROBCP que la obliga
evaluar: Callable[..., Resultado]
El campo referencia es el que convierte esto en una herramienta regulatoria y
no en un chequeo de datos. Cuando el validador rechaza una declaración, la
respuesta a "¿por qué?" no puede ser "lo dijo el sistema": tiene que ser
"Anexo 04, numeral 4.9, la fuente internacional no es la aprobada para esa
central".
Los cuatro ámbitos, y por qué importan
El ámbito de una regla dice qué necesita para decidir, y de ahí se sigue qué ocurre cuando esa información falta.
Una regla de campo mira un solo valor: si la declaración llegó el jueves antes de las diez, si el inventario está referenciado a 60 °F. Siempre se puede evaluar.
Una de registro cruza campos del mismo registro entre sí: que el precio puesto en planta declarado sea efectivamente la suma de sus componentes dividida por el poder calorífico, que el inventario final sea el inicial más compras menos consumo. También siempre se puede evaluar, y es la clase de regla más barata y más valiosa que existe, porque verifica una identidad y no una expectativa.
Una de serie necesita al mismo declarante a lo largo de varias semanas: detectar un salto de precio, detectar un valor congelado. Al inicio de la serie no hay contra qué comparar.
Una de cruce necesita una fuente independiente: la estructura aprobada, el SIMEC, la referencia internacional. Si la contraparte no llegó, la regla no se puede evaluar.
Y acá está la decisión de diseño más discutible del catálogo: "no evaluable" es un resultado, no un silencio. Si una regla se salta porque le faltó el insumo y el informe no lo dice, ese informe no distingue entre "esto pasó la revisión" y "esto no se revisó". En una corrida limpia esa diferencia no importa; el día que alguien reclame, es lo único que importa.
Por la misma razón el registro guarda también las reglas que pasaron. Un informe que solo lista fallas es indistinguible del informe de una corrida que nunca se hizo.
Rechazo o alerta
La pregunta que separa las dos severidades es una sola: ¿la discrepancia admite una explicación legítima?
Una identidad aritmética que no cuadra no admite explicación. Si el precio declarado no es la suma de sus componentes dividida por el poder calorífico, o el declarante se equivocó al llenar el formato o alguno de los cuatro números está mal, y en cualquiera de los dos casos el valor no se puede usar para calcular un costo variable. Rechaza.
Un salto de precio del veinte por ciento sí admite explicación: la cotización internacional se movió. La pregunta correcta no es cuánto subió sino si la referencia subió lo mismo. Alerta, y con el dato de contraste adentro del hallazgo para que quien lo lea no tenga que ir a buscarlo.
La regla del inventario mínimo por cota del embalse es alerta y no rechazo por una razón distinta, que es institucional y no técnica: el incumplimiento del piso ya tiene su sanción propia en el reglamento, que es la penalización de la tasa de salida forzada. El validador de la declaración no es el lugar donde eso se castiga; es el lugar donde se detecta y se reporta.
Las doce reglas
Son las mismas doce del artículo III de esta serie, convertidas en código. Cada una lleva su numeral.
| Regla | Qué verifica | Ámbito | Severidad | Referencia |
|---|---|---|---|---|
| R01 | La estructura de costos está vigente | cruce | rechazo | Anexo 04, 4.3 y 4.7 |
| R02 | La fuente internacional es la aprobada | cruce | rechazo | Anexo 04, 4.9 |
| R03 | La ventana de promedio del FOB es la de la fórmula | cruce | rechazo | Anexo 04, 4.1 b |
| R04 | Llegó el jueves antes de las 10:00 | campo | rechazo | Anexo 04, 10.1 y 12.1 a |
| R05 | La internación se actualizó por fórmula, no por valor nuevo | cruce | rechazo | Anexo 04, 7.2 |
| R06 | El PCpep es sus componentes sobre el poder calorífico | registro | rechazo | Anexo 04, 4.2 |
| R07 | El inventario está referenciado y los instrumentos certificados | campo | rechazo | Anexo 04, 4.12.3 d |
| R08 | Inicial más compras menos consumo da el final | registro | rechazo | Anexo 04, 8.2 |
| R09 | El consumo cuadra con la generación medida por el SIMEC | cruce | alerta | Anexo 04, 8.3 con Anexo 16 |
| R10 | El inventario cumple el piso de la cota del embalse | cruce | alerta | Anexo 04, 9.1 y 9.4 |
| R11 | El salto de precio sigue a la referencia internacional | serie | alerta | Anexo 04, 7.1 |
| R12 | El precio no lleva semanas congelado | serie | alerta | criterio propio |
R12 es la única que no sale del reglamento, y por eso su referencia dice "criterio propio". Detecta un valor idéntico durante tres semanas seguidas mientras la referencia internacional se movió. No es una irregularidad tipificada: es una firma de que alguien está copiando la declaración anterior en vez de calcularla.
R09 merece un párrafo aparte porque es la única del catálogo que no depende de ningún documento que aporte el declarante. Toma la generación que registró el SIMEC, la multiplica por el consumo específico de la curva auditada vigente y compara contra el consumo de combustible declarado. Las dos fuentes son independientes entre sí, así que si no cuadran hay algo que explicar en un lado o en el otro. Es la verificación más cara de montar y la más difícil de burlar.
La corrida
Los datos son sintéticos, con estructura realista y sin ninguna cifra atribuida a un generador real: cuatro participantes, tres combustibles, doce semanas, cuarenta y ocho declaraciones. Sobre esa base se sembraron doce defectos en posiciones conocidas.
Que los defectos estén declarados en el código importa, porque permite lo siguiente:
$ python datos_sinteticos.py
self-check: los 12 defectos sembrados fueron detectados
Un validador que no encuentra lo que se le sembró no sirve, y eso hay que probarlo, no suponerlo. Esa línea es lo primero que hay que correr después de tocar cualquier regla.
Cuarenta y ocho declaraciones por doce reglas dan quinientas setenta y seis evaluaciones: quinientas treinta y ocho pasaron, veintidós fallaron y dieciséis no se pudieron evaluar. Diecisiete declaraciones quedaron rechazadas, o sea una tasa de aceptación del 64.6 %.
Doce defectos, veintidós hallazgos
Esa diferencia es la parte interesante de la corrida, y no son falsos positivos. Son tres fenómenos distintos que conviene saber distinguir antes de mirar un informe real.
Cascada. Un solo hecho estructural produce muchos hallazgos. El participante GAMMA declara desde la semana 1 pero su estructura de costos entra en vigencia en la semana 6, así que R01 falla cinco veces. Es un defecto, no cinco. Un informe que ordena por severidad y no por causa raíz hace que quien lo lee crea que tiene cinco problemas.
Propagación. Una regla de serie compara contra la semana anterior, así que una semana corrupta ensucia también la comparación siguiente. R05 falla en la semana 6 de ALFA, que es donde se sembró el defecto, y vuelve a fallar en la semana 7 porque el valor esperado se calcula desde el valor corrupto de la 6. Lo mismo pasa con R11 en DELTA, que falla en la semana del salto y en la siguiente, cuando el precio vuelve a su nivel.
Solapamiento. Dos reglas independientes atrapan el mismo defecto desde ángulos distintos, y eso es una virtud y no un ruido. El precio congelado de ALFA lo detecta R12, que es la regla escrita para eso, pero también lo detecta R06: un precio que se copia de tres semanas atrás deja de cuadrar con sus propios componentes. Y el salto de DELTA lo detecta R11 y también R06, porque el declarante subió el precio sin mover ninguno de los números que lo forman.
Ese solapamiento es la razón para tener reglas de identidad aunque parezcan triviales. R06 solo verifica una división, y termina atrapando dos defectos que nadie escribió pensando en ella.
Los dieciséis casos no evaluables son todos estructurales y ninguno es un problema: R05 y R11 necesitan una semana anterior, R12 necesita tres, y al principio de cada serie eso no existe. El informe los agrupa por causa en vez de listarlos uno por uno.
El registro auditable
Lo que sale de la corrida no es solo una lista de problemas. Es esto:
{
"corrida": "2026-08-23T16:06:51+00:00",
"version_catalogo": "2026.08.1",
"hash_insumo_sha256": "876d7f5a7c17244865495f3dd6edcce5…",
"declaraciones": 48,
"reglas": 12,
"evaluaciones": 576,
"estructuras_aplicadas": {
"PM-ALFA/fuel_oil": "EST-2025-014",
"PM-BETA/diesel": "EST-2025-021",
"PM-GAMMA/gas_natural": "EST-2026-003",
"PM-DELTA/fuel_oil": "EST-2024-009"
},
"conteo_estado": { "OK": 538, "FALLA": 22, "NO_EVALUABLE": 16 },
"tasa_aceptacion": 0.6458
}
Cinco campos hacen todo el trabajo. El hash del insumo fija exactamente qué datos se validaron, de modo que una discusión posterior no puede ser sobre qué archivo era. La versión del catálogo fija con qué reglas, porque las reglas cambian y una declaración aceptada en marzo se evaluó con el catálogo de marzo. Las estructuras aplicadas fijan contra qué referencia se comparó cada participante, que es la pregunta que el Anexo 04 obliga a poder responder. El conteo por estado incluye los OK. Y la marca de tiempo dice cuándo.
Eso es un registro auditable. Un log dice qué pasó; un registro auditable dice contra qué se validó.
Lo que le falta
Vale enumerarlo, para que no parezca terminado.
No hay persistencia: cada corrida es independiente y no se compara con la anterior, así que no puede decir "esta regla empeoró respecto del mes pasado". No hay flujo de resolución: un hallazgo no se puede marcar como revisado, con quién lo revisó y qué decidió, y sin eso el registro documenta la detección pero no la gestión. No hay agrupación por causa raíz, que es justamente lo que haría falta para que la cascada de R01 se lea como un problema y no como cinco. Y las tolerancias están declaradas en un solo lugar, que es correcto, pero no están justificadas contra ninguna medición: son un criterio razonable y nada más, y un sistema en producción debería derivarlas de la dispersión histórica de cada campo.
Fuentes
- Código completo:
proyectos/validador-cvcen el repositorio de este sitio.reglas.pyes el catálogo,validador.pyel motor y el registro,datos_sinteticos.pylos datos con sus defectos sembrados y el self-check. Solo biblioteca estándar de Python 3.13. - Reglamento de Operación del Sistema de Transmisión y del Mercado Mayorista
Basado en Costos de Producción (ROBCP), Anexos, versión actualizada a junio
de 2026. Unidad de Transacciones. Anexo 04, Precios de los Combustibles:
cada regla del catálogo lleva su numeral en el campo
referencia, y todos salen de la lectura documentada en el tercer artículo de esta serie. Anexo 16, Curvas de Consumo Específico de Calor: el consumo específico que R09 usa para cruzar contra el SIMEC. Copia local:normativa/robcp-anexos.pdf. Consultado el 22 de agosto de 2026. - Datos: sintéticos, generados por
datos_sinteticos.pycon semilla fija. Ninguna cifra corresponde a un generador real de El Salvador. Lo que se copia de la realidad es la estructura del proceso, no sus valores.
El registro completo de documentos primarios de la serie, con su estado de verificación, está en Fuentes primarias.