/ofertar
🧾 Ofertador (precio fijo cerrado)
Genera una oferta a precio fijo cerrado por periodo (P1..P6), calculada a partir de los componentes del producto indexado (hedging, apuntamiento, peajes, servicios de ajuste, pérdidas, tasa municipal, coste financiero, comisión y risk premium). Aunque la consolidación es un precio fijo, el desglose se expone en la respuesta para justificar el precio ante el cliente.
📝 Descripción
Dado un rango futuro (fecha de inicio + duración en meses)
y un consumo expresado bien como CUPS (uno o varios), bien
como tarifa_atr + consumo (total o por periodos), el ofertador:
- Perfila el consumo del cliente sobre el último año móvil disponible en
prices.precios_ofertador_a2_c2. - Calcula el hedging promediando
settlement_price_electricenprices.futuros_electricospara losdatedel rango de oferta. - Aplica el apuntamiento proyectado por periodo, los
componentes horarios (SSAA, pagos por capacidad), los regulados BOE
del
year_regulado(por defecto, año en curso) y los aportes secuenciales de pérdidas, tasa y coste financiero. - Suma
comisionyrisk_premiumpara consolidar elprecio_total. - Opcionalmente aplica un reajuste post-cálculo por tarifa
(mover
precio_totalde periodos concretos preservando el ponderado por consumo).
Si se aportan varios CUPS, los válidos se agrupan por tarifa ATR
y se genera un lote por tarifa. Los CUPS que fallan quedan en
entrada.cups_invalidos.
PENINSULA. Un CUPS con geo_zone extrapeninsular se procesa
igualmente en PENINSULA con warning en el log.
🔐 Autenticación
Cabecera Authorization: Bearer <TOKEN>. Acepta token
estático API_SECUR_TOKEN o JWT firmado con
JWT_SECRET / JWT_SECRET_1.
📥 Cuerpo de la petición
Campos requeridos
| Campo | Tipo | Descripción |
|---|---|---|
fecha_inicio_oferta |
string (YYYY-MM-DD) | Debe ser día 1 de un mes y estrictamente futura. |
duracion_meses |
integer ≥ 1 | La oferta termina el último día del mes
fecha_inicio + duracion_meses - 1 día. |
Entrada del consumo — elige UN camino
Camino A: por CUPS (recomendado si se dispone del CUPS).
| Campo | Tipo | Descripción |
|---|---|---|
cups |
string | array<string> | Un CUPS o una lista. Se llama a SIPS en paralelo (hasta 8 hilos) y
se agrupan por tarifa_atr. Los CUPS que fallan van a
entrada.cups_invalidos. |
token_sips |
string | Opcional. Si no se aporta, se obtiene automáticamente. |
Camino B: sin CUPS, aportando tarifa + consumo.
| Campo | Tipo | Descripción |
|---|---|---|
tarifa_atr |
string | '2.0TD'..'6.4TD' o códigos '018'..'023'. |
consumo_total_kwh |
number | Consumo total del rango histórico. Alternativa a
consumo_periodos_kwh. |
consumo_periodos_kwh |
object {P1..P6: number} | Consumo desglosado por periodos. Si se aportan ambos, tiene
preferencia sobre consumo_total_kwh. |
Parámetros de la oferta (opcionales)
| Campo | Tipo | Default | Descripción |
|---|---|---|---|
tipo_campania |
string | 'campaña' |
Valor existente en la columna tipo de
prices.precios_campania_index_b2b. Si el par
(tarifa_atr, tipo_campania) no existe, el lote se
devuelve con error. Caso especial
'manual': no se consulta la tabla y el
fee se toma de los parámetros desvios_eur_mwh,
margen_eur_mwh, gos_eur_mwh y
resto_eur_mwh. |
desvios_eur_mwh |
number | null |
Componente desvíos del fee (€/MWh). Obligatorio
cuando tipo_campania='manual'; prohibido en cualquier
otro caso (error 400). |
margen_eur_mwh |
number | null |
Componente margen del fee (€/MWh). Obligatorio
cuando tipo_campania='manual'; prohibido en cualquier
otro caso. |
gos_eur_mwh |
number | null |
Componente GdOs solares del fee (€/MWh).
Obligatorio cuando
tipo_campania='manual'; prohibido en cualquier
otro caso. |
resto_eur_mwh |
number | 0.0 |
Componente resto del fee (€/MWh). Opcional en modo
manual (default 0); prohibido en cualquier otro
caso. Se suma al FNEE BOE para formar
aportacion_fondos. |
comision_eur_mwh |
number | 0.0 |
€/MWh sumados al precio indexado efectivo para calcular
precio_total. |
risk_premium_eur_mwh |
number | 0.0 |
€/MWh de prima de riesgo, sumados al precio indexado efectivo. |
coste_financiero_pct |
number | 0.25 |
% aplicado como (1 + cf/100). |
hedging_eur_mwh |
number | Supabase | Si se aporta, se salta la consulta a Supabase (útil para offline/testing). |
year_regulado |
integer | Año en curso | Año a usar para los componentes regulados BOE (peajes/cargos, capacidad, remuneración OMIE/REE, FNEE, TP). |
servicios_ajuste_eur_mwh |
number | null | null |
Prima de riesgo de SSAA. null deja el ponderado del
histórico; 0 fuerza cero en todos los periodos;
X>0 reescala para que el ponderado por consumo del
conjunto del rango sea exactamente X. |
fecha_inicio_historico |
string (YYYY-MM-DD) | Último año móvil | Rango histórico para el perfilado. Si se aporta, debe ir junto a
fecha_fin_historico. |
fecha_fin_historico |
string (YYYY-MM-DD) | Último año móvil | Ver arriba. |
Reajuste post-cálculo (opcional)
| Campo | Tipo | Default | Descripción |
|---|---|---|---|
ajustes_por_tarifa |
object | null |
Dict indexado por tarifa ATR. Cada entrada admite claves
'P1'..'P6' (delta €/MWh a aplicar; positivo sube,
negativo baja) y 'delta_ponderado' (shift €/MWh del
ponderado por consumo del lote; 0 = preservarlo). Ver ejemplo abajo. |
metodo_ajuste |
string | 'inverso_peso' |
'inverso_peso': reparte el balance inversamente
proporcional al consumo (los periodos con menos peso reciben
mayor Δ €/MWh; descarga el impacto sobre las horas valle).
'uniforme': reparte el balance entre los periodos
NO listados. 'ninguno': no redistribuye; el
ponderado se moverá según los deltas. |
tolerancia_ajuste_eur_mwh |
number | 0.15 |
Tolerancia €/MWh entre el ponderado final y el objetivo. |
incluir_detalle_horario no se acepta desde
el API: el DataFrame horario no es serializable a JSON. Para auditoría
horaria, usa la librería directamente.
Ejemplo — oferta con lista de CUPS
{
"fecha_inicio_oferta": "2026-08-01",
"duracion_meses": 12,
"cups": ["ES0021000013654684FH", "ES0021000012601426YE"],
"tipo_campania": "campaña",
"comision_eur_mwh": 0.5,
"risk_premium_eur_mwh": 0.6,
"coste_financiero_pct": 0.25,
"servicios_ajuste_eur_mwh": 12
}
Ejemplo — sin CUPS con consumo por periodos y reajuste
{
"fecha_inicio_oferta": "2026-08-01",
"duracion_meses": 12,
"tarifa_atr": "3.0TD",
"consumo_periodos_kwh": {
"P1": 200000, "P2": 150000, "P3": 250000,
"P4": 100000, "P5": 120000, "P6": 180000
},
"tipo_campania": "campaña",
"comision_eur_mwh": 0.5,
"risk_premium_eur_mwh": 0.6,
"servicios_ajuste_eur_mwh": 12,
"hedging_eur_mwh": 60.0,
"ajustes_por_tarifa": {
"3.0TD": {"P1": -3.0, "P4": 1.0, "delta_ponderado": 0.2}
},
"metodo_ajuste": "inverso_peso"
}
Ejemplo — fee de campaña manual (tipo_campania='manual')
Cuando la combinación (tarifa, tipo_campania) no está en la
tabla o se quiere probar un fee "a mano" sin tocarla, se usa
'manual' y se aportan las componentes del fee como parámetros
(desvios_eur_mwh, margen_eur_mwh,
gos_eur_mwh obligatorios; resto_eur_mwh
opcional, default 0). En este modo la tabla no se consulta.
{
"fecha_inicio_oferta": "2026-08-01",
"duracion_meses": 12,
"tarifa_atr": "3.0TD",
"consumo_total_kwh": 500000,
"tipo_campania": "manual",
"desvios_eur_mwh": 2.5,
"margen_eur_mwh": 6.0,
"gos_eur_mwh": 1.2,
"resto_eur_mwh": 0.0,
"comision_eur_mwh": 0.5,
"risk_premium_eur_mwh": 0.6,
"servicios_ajuste_eur_mwh": 12
}
📤 Respuesta exitosa (200 OK)
El resultado se estructura en 4 bloques de nivel superior
(oferta, parametros, entrada,
lotes). Cada lote contiene los sub-bloques
consumo, inputs_calculo,
energia_eur_mwh, potencia_eur_kw_anio,
componentes_precio_final y opcionalmente ajuste.
Principios de diseño del payload:
- Cada dato aparece una sola vez. Los escalares comunes
a todos los periodos viven en
energia_eur_mwh.escalares_comunesy no se repiten dentro de cadapor_periodo[Pi]. - Energía y potencia separadas (bloques
energia_eur_mwhvspotencia_eur_kw_anio). - Periodos como claves en los dicts de vista por periodo:
{"P1": {...}, ..., "P6": {...}, "ponderado": {...}}. El ponderado por consumo es una entrada más con las mismas claves. - Los sufijos
_eur_mwh/_eur_kw_aniose omiten dentro de bloques cuyo nombre ya lo indica.
Estructura general
{
"oferta": {
"fecha_inicio": "2026-08-01",
"fecha_fin": "2027-07-31",
"duracion_meses": 12,
"geo_zone": "PENINSULA",
"tipo_campania": "campaña",
"rango_historico": {"fecha_inicio": "2025-01-01", "fecha_fin": "2025-12-31"}
},
"parametros": {
"hedging_eur_mwh": 68.4321,
"comision_eur_mwh": 0.5,
"risk_premium_eur_mwh": 0.6,
"servicios_ajuste_eur_mwh": 12.0,
"coste_financiero_pct": 0.25,
"year_regulado": 2026
},
"entrada": {
"cups_invalidos": [{"cups": "ES...", "error": "SIPS timeout"}]
},
"lotes": [ /* ver más abajo */ ]
}
Cada lote (caso OK)
{
"tarifa_atr": "3.0TD",
"cups": ["ES0021000013654684FH", "ES0021000012601426YE"],
"num_cups": 2,
"consumo": {
"total_kwh": 1234567.89,
"por_periodo_kwh": {"P1": 200000, "P2": 150000, "P3": 250000,
"P4": 100000, "P5": 120000, "P6": 180000},
"peso_periodo": {"P1": 0.1621, "P2": 0.1216, "P3": 0.2027,
"P4": 0.0811, "P5": 0.0973, "P6": 0.1459}
},
"inputs_calculo": {
"fee_campania_eur_mwh": {
"desvios": 0.75, "margen": 6.5, "gos": 1.2, "resto": 2.05, "fnee": 3.5
},
"regulados_boe_eur_mwh": {
"peajes_cargos": {"P1": 24.31, "P2": 18.09, "P3": 4.83, "P4": 4.83, "P5": 4.83, "P6": 0.83},
"pagos_capacidad": {"P1": 3.05, "P2": 1.52, "P3": 0.19, "P4": 0.19, "P5": 0.19, "P6": 0.19},
"remuneracion_omie": 0.12,
"remuneracion_ree": 0.24
}
},
"energia_eur_mwh": {
"escalares_comunes": {
"hedging": 68.4321,
"remuneracion_omie": 0.12,
"remuneracion_ree": 0.24,
"desvios": 0.75,
"gdos_solares": 1.20,
"margen_neto": 6.50,
"aportacion_fondos": 5.55,
"comision": 0.50,
"risk_premium": 0.60
},
"por_periodo": {
"P1": {
"apuntamiento_pct": 8.4321,
"apuntamiento": 5.7712,
"servicios_ajuste": 12.0000,
"pagos_capacidad": 3.0512,
"peajes_cargos": 24.3110,
"perdidas": 6.4210,
"tasa_municipal": 2.0140,
"coste_financiero": 0.3430,
"precio_indexado": 128.9155,
"precio_indexado_efectivo":137.6935,
"precio_total": 138.7935
},
"P2": { ... }, "P3": { ... }, "P4": { ... }, "P5": { ... }, "P6": { ... },
"ponderado": { /* mismas claves que un periodo */ }
}
},
"potencia_eur_kw_anio": {
"P1": 22.980000, "P2": 18.440000, "P3": 11.120000,
"P4": 8.310000, "P5": 5.020000, "P6": 0.650000
},
"componentes_precio_final": {
"P1": {
"PRECIO TERMINO DE ENERGIA": 138.79,
"HEDGING": 68.43,
"APUNTAMIENTO": 5.77,
"SERVICIOS DE AJUSTE": 12.00,
"DESVIOS": 0.75,
"GDOS SOLARES": 1.20,
"PERDIDAS": 6.42,
"RISK PREMIUM": 0.60,
"APORTACION FONDOS": 5.55,
"MARGEN NETO": 6.50,
"COMISION": 0.50,
"PEAJES, CARGOS Y PAGOS CAPAC.": 27.36,
"OTROS COSTES": 2.72
},
"P2": { /* 13 categorías */ }, ..., "P6": { /* 13 categorías */ },
"ponderado": { /* 13 categorías */ }
}
}
Notas sobre los bloques
consumo:por_periodo_kwhviene siempre relleno (derivado del perfilado, aunque el input haya sidoconsumo_total_kwh).peso_periodoes la fracción de consumo de cada periodo sobre el total.energia_eur_mwh:precio_total = precio_indexado_efectivo + escalares_comunes.comision + escalares_comunes.risk_premium. Los escalares se aplican una sola vez; NO están repetidos dentro depor_periodo[Pi].apuntamiento_pctdel ponderado se recalcula comoapuntamiento / hedging · 100.potencia_eur_kw_anio: peajes + cargos BOE de potencia por periodo, en €/kW·año. Independiente de la energía.componentes_precio_final: vista comercial invertida (periodo → 13 categorías) lista para pintar como tabla (columna por periodo + columnaponderado).
♻️ Reajuste inline (lote.ajuste)
Si se informa ajustes_por_tarifa en la petición, cada lote
afectado devuelve inline el bloque ajuste
con el resumen. Los lotes sin ajuste no llevan esa clave.
"ajuste": {
"tarifa_atr": "3.0TD",
"ponderado_original_eur_mwh": 148.32,
"ponderado_objetivo_eur_mwh": 148.52,
"ponderado_final_eur_mwh": 148.52,
"deltas_aplicados_eur_mwh": {"P1": -3.0, "P2": 0.84, "P3": 0.84,
"P4": 1.84, "P5": 0.84, "P6": 0.84}
}
Además, dentro de energia_eur_mwh.por_periodo[P] aparece la
clave reajuste con el delta neto acumulado
aplicado a ese periodo (positivo = subida). Se acumula si el ajuste se
invoca varias veces sobre el mismo lote.
❌ Lote con error
Cuando el lote no puede calcularse (fee de campaña ausente para el par
(tarifa_atr, tipo_campania), sin datos horarios, etc.),
el lote lleva solo tarifa_atr, cups,
num_cups y la clave error. Ni
consumo, ni inputs_calculo, ni
energia_eur_mwh, ni potencia_eur_kw_anio
aparecen en ese caso. El resto de lotes se calcula normalmente.
{
"tarifa_atr": "6.4TD",
"cups": [...],
"num_cups": 3,
"error": "fee indexado: No hay precio campaña en precios_campania_index_b2b para tarifa=6.4TD tipo=campaña"
}
❌ Errores HTTP
| Código | Causa |
|---|---|
400 |
Validación de negocio: fecha no día 1, fecha en pasado, duración inválida, sin cups ni tarifa_atr, rango histórico invertido, clave no reconocida en ajustes_por_tarifa, ponderado fuera de tolerancia, etc. |
401 |
Falta token o token inválido. |
500 |
Error inesperado (traza en logs). |
💻 Ejemplos
cURL
curl -X POST "https://api.imaginaenergia.com/ofertar" \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d '{
"fecha_inicio_oferta": "2026-08-01",
"duracion_meses": 12,
"tarifa_atr": "3.0TD",
"consumo_total_kwh": 1000000,
"tipo_campania": "campaña",
"comision_eur_mwh": 0.5,
"risk_premium_eur_mwh": 0.6,
"servicios_ajuste_eur_mwh": 12
}'
Python (requests)
import requests
body = {
"fecha_inicio_oferta": "2026-08-01",
"duracion_meses": 12,
"cups": ["ES0021000013654684FH", "ES0021000012601426YE"],
"tipo_campania": "campaña",
"comision_eur_mwh": 0.5,
"risk_premium_eur_mwh": 0.6,
"servicios_ajuste_eur_mwh": 12,
"ajustes_por_tarifa": {
"3.0TD": {"P1": -3.0, "delta_ponderado": 0.2}
},
}
r = requests.post(
"https://api.imaginaenergia.com/ofertar",
headers={"Authorization": f"Bearer {jwt}"},
json=body,
timeout=120,
)
res = r.json()
for lote in res["lotes"]:
if "error" in lote:
print(lote["tarifa_atr"], "→", lote["error"])
continue
pond = lote["energia_eur_mwh"]["por_periodo"]["ponderado"]
print(lote["tarifa_atr"], "€/MWh medio:", pond["precio_total"])