Volver a la documentación
POST /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:

  1. Perfila el consumo del cliente sobre el último año móvil disponible en prices.precios_ofertador_a2_c2.
  2. Calcula el hedging promediando settlement_price_electric en prices.futuros_electricos para los date del rango de oferta.
  3. 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.
  4. Suma comision y risk_premium para consolidar el precio_total.
  5. Opcionalmente aplica un reajuste post-cálculo por tarifa (mover precio_total de 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.

ℹ️ Geografía: el ofertador siempre calcula en 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

CampoTipoDescripció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).

CampoTipoDescripció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.

CampoTipoDescripció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)

CampoTipoDefaultDescripció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)

CampoTipoDefaultDescripció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:

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

♻️ 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ódigoCausa
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"])