Add 3-day/7-day multi-window analysis for trend-based decisions

- Fetch campaign metrics for 1d/3d/7d windows at run start (2 extra API calls total)
- Pass spend_3d/leads_3d/cpl_3d and spend_7d/leads_7d/cpl_7d to Claude decide()
- DECIDE_SYSTEM: decisions based on cpl_3d (primary) vs cpl_7d (trend direction)
- PAUSE rule now uses leads_3d==0 + spend_3d >= 3x max_cpl threshold
- Adset and ad analysis now use 3-day aggregated data (get_period_*_metrics)
- Add get_period_campaign_metrics/adset/ad methods to MetaAdsClient
- Print shows 1d/3d/7d CPL and leads per campaign for easy trend reading
- UNIT_SYSTEM and AD_SYSTEM note data covers last 3 days

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Manuel 2026-06-13 22:48:04 +02:00
parent 8b6ec106ee
commit 6ec3342661
3 changed files with 54 additions and 9 deletions

View File

@ -11,12 +11,19 @@ Eres un experto en optimización de campañas de Meta Ads para generación de le
Cada campaña tiene un CPL máximo (coste por lead objetivo) que define el límite aceptable.
USA SIEMPRE como unidad de moneda. Responde SIEMPRE en español.
REGLAS DE DECISIÓN:
1. CPL > max_cpl REDUCE_BUDGET (nunca PAUSE por CPL alto).
2. CPL <= max_cpl con bajo volumen INCREASE_BUDGET si hay margen.
3. Frecuencia > 3.0 considera rotar creatividades o ampliar audiencia.
4. CTR < 1% problema de creatividad, revisar anuncios.
5. PAUSE solo si: leads == 0 Y gasto >= 3 × max_cpl (fallo técnico grave de tracking/píxel). En cualquier otro caso de bajo rendimiento usa REDUCE_BUDGET, nunca PAUSE.
Recibirás métricas en tres ventanas temporales:
- spend/leads/cpl: ayer (1 día, contexto inmediato)
- spend_3d/leads_3d/cpl_3d: últimos 3 días (ventana PRINCIPAL para decisiones)
- spend_7d/leads_7d/cpl_7d: últimos 7 días (tendencia de fondo)
REGLAS DE DECISIÓN (basadas principalmente en cpl_3d y leads_3d):
1. cpl_3d > max_cpl REDUCE_BUDGET (nunca PAUSE por CPL alto).
2. cpl_3d <= max_cpl con bajo volumen INCREASE_BUDGET si hay margen.
3. Si cpl_3d < cpl_7d tendencia mejorando: más permisivo con INCREASE_BUDGET.
4. Si cpl_3d > cpl_7d × 1.2 tendencia empeorando: más estricto, considera REDUCE_BUDGET.
5. CTR < 1% problema de creatividad, revisar anuncios.
6. PAUSE solo si: leads_3d == 0 Y spend_3d >= 3 × max_cpl (fallo técnico grave de tracking/píxel).
En cualquier otro caso de bajo rendimiento usa REDUCE_BUDGET, nunca PAUSE.
Responde SOLO con JSON válido, sin texto adicional ni markdown:
{
@ -67,6 +74,7 @@ def decide(analysis: dict) -> dict:
UNIT_SYSTEM = """
Eres un analista experto en Meta Ads. Analiza las métricas del conjunto de anuncios indicado.
Los datos corresponden a los últimos 3 días (ventana estándar de análisis).
USA SIEMPRE como unidad de moneda. Responde SIEMPRE en español.
Si el conjunto tiene cost_cap_eur (cap de coste), compara el CPL actual con ese cap e indica si está
por encima, dentro o por debajo del límite, y cuánto margen queda (o cuánto se supera).
@ -76,6 +84,7 @@ Responde SOLO con JSON válido (sin markdown):
AD_SYSTEM = """
Eres un analista experto en Meta Ads. Analiza las métricas del anuncio indicado.
Los datos corresponden a los últimos 3 días (ventana estándar de análisis).
USA SIEMPRE como unidad de moneda. Responde SIEMPRE en español.
Responde SOLO con JSON válido (sin markdown):
{"evaluacion": "resumen del rendimiento en 2 frases usando €", "recomendacion": "una acción concreta", "accion": "PAUSE o MAINTAIN"}

View File

@ -168,6 +168,24 @@ class MetaAdsClient:
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
return self.get_ad_metrics(campaign_id, yesterday, yesterday)
def get_period_campaign_metrics(self, days: int) -> dict:
today = datetime.now()
date_to = (today - timedelta(days=1)).strftime("%Y-%m-%d")
date_from = (today - timedelta(days=days)).strftime("%Y-%m-%d")
return self.get_campaign_metrics(date_from, date_to)
def get_period_adset_metrics(self, campaign_id: str, days: int) -> list:
today = datetime.now()
date_to = (today - timedelta(days=1)).strftime("%Y-%m-%d")
date_from = (today - timedelta(days=days)).strftime("%Y-%m-%d")
return self.get_adset_metrics(campaign_id, date_from, date_to)
def get_period_ad_metrics(self, campaign_id: str, days: int) -> list:
today = datetime.now()
date_to = (today - timedelta(days=1)).strftime("%Y-%m-%d")
date_from = (today - timedelta(days=days)).strftime("%Y-%m-%d")
return self.get_ad_metrics(campaign_id, date_from, date_to)
def get_ads_with_creatives(self, campaign_id: str) -> list:
"""
Returns active ads for a campaign with their thumbnail URLs for creative analysis.

24
run.py
View File

@ -145,6 +145,12 @@ def run():
metrics_all = meta.get_yesterday_metrics()
print(f"{len(metrics_all)} campaigns active yesterday.\n")
# ── 3-day and 7-day metrics for trend analysis ────────────────────────────
print(f"→ Fetching 3-day and 7-day metrics...")
metrics_3d = meta.get_period_campaign_metrics(days=3)
metrics_7d = meta.get_period_campaign_metrics(days=7)
print(f" ✓ Multi-window data ready.\n")
# ── Analyze campaigns & propose actions ───────────────────────────────────
actions_proposed_list = []
campaign_details = {} # {cid: {vertical, margin, adsets, ads}}
@ -156,10 +162,13 @@ def run():
max_cpl = vertical_cpls.get(vertical, config.META_TARGET_CPL) or config.META_TARGET_CPL
margin = round((max_cpl - metrics["cpl"]) * metrics["leads"], 2) if metrics["leads"] > 0 else round(-metrics["spend"], 2)
m3 = metrics_3d.get(cid, {})
m7 = metrics_7d.get(cid, {})
analysis = {
"campaign_id": cid,
"name": metrics["name"],
"status": metrics["status"],
# Yesterday (1d) — contexto inmediato
"spend": metrics["spend"],
"leads": metrics["leads"],
"cpl": metrics["cpl"],
@ -168,6 +177,14 @@ def run():
"cpm": metrics["cpm"],
"impressions": metrics["impressions"],
"clicks": metrics["clicks"],
# 3 días — ventana principal para decisiones
"spend_3d": m3.get("spend", 0.0),
"leads_3d": m3.get("leads", 0),
"cpl_3d": m3.get("cpl", 0.0),
# 7 días — tendencia de fondo
"spend_7d": m7.get("spend", 0.0),
"leads_7d": m7.get("leads", 0),
"cpl_7d": m7.get("cpl", 0.0),
}
try:
@ -200,7 +217,8 @@ def run():
action_type = "MAINTAIN"
print(f" {metrics['name'][:52]}")
print(f" Spend: {metrics['spend']}€ Leads: {metrics['leads']} CPL: {metrics['cpl']}€ MaxCPL: {max_cpl}€ Margen: {margin:+.2f}")
print(f" CPL: 1d={metrics['cpl']}€ / 3d={m3.get('cpl',0.0):.2f}€ / 7d={m7.get('cpl',0.0):.2f}€ MaxCPL: {max_cpl}€ Margen: {margin:+.2f}")
print(f" Leads: 1d={metrics['leads']} / 3d={m3.get('leads',0)} / 7d={m7.get('leads',0)} Spend 1d: {metrics['spend']}")
print(f" Vertical: {vertical} Decision: {action_type}{(decision.get('justification') or '')[:70]}")
if decision.get("alert"):
print(f" ALERT: {decision['alert']}")
@ -236,7 +254,7 @@ def run():
# ── Ad set analysis ────────────────────────────────────────────────
adsets_detail = []
try:
for as_m in meta.get_yesterday_adset_metrics(cid)[:5]:
for as_m in meta.get_period_adset_metrics(cid, days=3)[:5]:
bid = adset_bids.get(as_m["id"], {})
as_m["bid_strategy"] = bid.get("bid_strategy", "")
as_m["cost_cap_eur"] = bid.get("cost_cap_eur")
@ -249,7 +267,7 @@ def run():
# ── Ad analysis ────────────────────────────────────────────────────
ads_detail = []
try:
for ad_m in meta.get_yesterday_ad_metrics(cid)[:5]:
for ad_m in meta.get_period_ad_metrics(cid, days=3)[:5]:
ad_m["max_cpl"] = max_cpl
result = analyze_unit(ad_m, "ad")
ad_entry = {**ad_m, **result}