Ports meta-optimizer's Meta Ads execution/approval/creative-analysis layer (agent.py, meta_ads_client.py, baserow_client.py, slack_notifier.py, approval_server.py) and replaces the per-vertical CPL model with the PPL + monthly-capping-per-course model already used by leads-optimizer, via a new airtable_client.py that shares Cursos/Familias/CentroCurso/ CursoMes/Leads Lake with that project and adds Meta Ads Campaigns / MetaCampaignMes alongside its Google Ads Campaigns / GACampaignMes.
73 lines
2.9 KiB
Python
73 lines
2.9 KiB
Python
"""
|
|
Calcula métricas derivadas y urgencia para una campaña de Meta Ads ligada a un
|
|
curso. Puerto directo de leads-optimizer/analyzer.py: las fórmulas de PPL,
|
|
capping y ritmo son agnósticas de canal (Google/Meta), solo cambian los nombres
|
|
de los campos de origen de las métricas de gasto/conversiones.
|
|
"""
|
|
from datetime import datetime
|
|
import calendar
|
|
|
|
|
|
def analyze(campaign_config: dict, leads_entregados: int, ads_metrics: dict) -> dict:
|
|
now = datetime.now()
|
|
dias_mes = calendar.monthrange(now.year, now.month)[1]
|
|
dia_actual = now.day
|
|
ratio_mes = dia_actual / dias_mes
|
|
|
|
capping = campaign_config["capping_mensual"]
|
|
ppl = campaign_config["ppl"]
|
|
cpa_max = campaign_config["cpa_maximo"]
|
|
gasto = ads_metrics.get("spend", 0)
|
|
conversiones_meta = ads_metrics.get("leads", 0)
|
|
|
|
ratio_leads = leads_entregados / capping if capping > 0 else 0
|
|
cpa_actual = gasto / leads_entregados if leads_entregados > 0 else 0
|
|
revenue = leads_entregados * ppl
|
|
margen = (revenue - gasto) / revenue if revenue > 0 else 0
|
|
leads_restantes = capping - leads_entregados
|
|
dias_restantes = dias_mes - dia_actual
|
|
ritmo = ratio_leads - ratio_mes # positivo = adelantado, negativo = atrasado
|
|
|
|
if ratio_leads >= 1.0:
|
|
urgencia = "PAUSAR"
|
|
elif capping > 0 and ratio_leads < ratio_mes - 0.15 and dias_restantes <= 5:
|
|
urgencia = "SPRINT"
|
|
elif ritmo < -0.15:
|
|
urgencia = "ACELERAR"
|
|
elif ritmo > 0.15:
|
|
urgencia = "FRENAR"
|
|
else:
|
|
urgencia = "EN_RITMO"
|
|
|
|
# Discrepancia en ambas direcciones entre el conteo propio de Meta y el de
|
|
# Leads Lake (Airtable) — puede indicar leads fantasma o tracking roto.
|
|
conv_leads_lake_mes = campaign_config.get("conv_leads_lake_mes", leads_entregados)
|
|
discrepancia = abs(conversiones_meta - conv_leads_lake_mes)
|
|
|
|
return {
|
|
"curso": campaign_config["curso"],
|
|
"campaign_id": campaign_config["meta_campaign_id"],
|
|
"ppl": ppl,
|
|
"cpa_maximo": cpa_max,
|
|
"capping": capping,
|
|
"leads_entregados": leads_entregados,
|
|
"leads_restantes": leads_restantes,
|
|
"dias_restantes": dias_restantes,
|
|
"ratio_leads": round(ratio_leads, 3),
|
|
"ratio_mes": round(ratio_mes, 3),
|
|
"ritmo": round(ritmo, 3),
|
|
"urgencia": urgencia,
|
|
"cpa_actual": round(cpa_actual, 2),
|
|
"rentable": cpa_actual <= cpa_max if cpa_actual > 0 else True,
|
|
"margen": round(margen, 3),
|
|
"revenue_estimado": round(revenue, 2),
|
|
"gasto_acumulado": round(gasto, 2),
|
|
"budget_diario_actual": ads_metrics.get("budget_daily", 0),
|
|
"ctr": ads_metrics.get("ctr", 0),
|
|
"clicks": ads_metrics.get("clicks", 0),
|
|
"conversiones_meta": conversiones_meta,
|
|
"discrepancia_tracking": discrepancia,
|
|
"alerta_tracking": discrepancia > 10,
|
|
"status_meta": ads_metrics.get("status", "UNKNOWN"),
|
|
}
|