Spaces:
Runtime error
Runtime error
| """Kill/winner rules (SPEC_NEW §7.8, Phase 5) — read-only reporting. | |
| v1 NEVER auto-pauses on Meta; these helpers only *surface* candidates for the | |
| `report` command (and the UI). Thresholds derive from `TARGET_CPA` (env). | |
| - **Kill:** spend ≥ 1.5× target CPA with 0 purchases, OR hook rate (3s/impr) | |
| < 15% after 2,000 impressions → surface (would mark video `killed`). | |
| - **Winner:** ≥ 3 purchases and CPA ≤ 0.8× target CPA → surface for manual scaling. | |
| Both aggregate `metrics_daily` per `ad_id` (sum the counters), join | |
| `publishes → videos` to attach `video_id`, `ad_name`, and `angle` | |
| (`videos.tags->>'angle'`). All divisions are null-safe via `nullif`, all sums | |
| via `coalesce`. | |
| """ | |
| from __future__ import annotations | |
| from . import db | |
| from .config import get_settings | |
| # Aggregate metrics_daily per ad_id and attach the creative's video/angle. One | |
| # row per published ad_id that has at least one metrics_daily row. nullif guards | |
| # every division (impressions/purchases can be 0 or null); coalesce guards sums. | |
| _BASE_CTE = """ | |
| with agg as ( | |
| select | |
| m.ad_id, | |
| sum(coalesce(m.impressions, 0)) as impressions, | |
| sum(coalesce(m.spend, 0)) as spend, | |
| sum(coalesce(m.purchases, 0)) as purchases, | |
| sum(coalesce(m.revenue, 0)) as revenue, | |
| sum(coalesce(m.video_3s, 0)) as video_3s | |
| from metrics_daily m | |
| group by m.ad_id | |
| ), | |
| joined as ( | |
| select | |
| p.video_id, | |
| agg.ad_id, | |
| p.ad_name, | |
| v.tags->>'angle' as angle, | |
| agg.impressions, | |
| agg.spend, | |
| agg.purchases, | |
| agg.revenue, | |
| agg.video_3s, | |
| agg.video_3s::numeric / nullif(agg.impressions, 0) as hook_rate, | |
| agg.spend / nullif(agg.purchases, 0) as cpa | |
| from agg | |
| join publishes p on p.ad_id = agg.ad_id | |
| left join videos v on v.id = p.video_id | |
| ) | |
| """ | |
| def kill_candidates() -> list[dict]: | |
| """Ads that should be killed (SPEC_NEW §7.8). Read-only — surfaced, not paused. | |
| KILL when spend ≥ 1.5×TARGET_CPA AND purchases = 0, OR impressions ≥ 2000 | |
| AND hook_rate < 0.15. `reason` is a short human-readable string. | |
| """ | |
| target_cpa = get_settings().target_cpa | |
| rows = db.fetch_all( | |
| _BASE_CTE | |
| + """ | |
| select | |
| video_id, ad_id, ad_name, angle, | |
| case | |
| when spend >= %(spend_kill)s and purchases = 0 | |
| then 'spend ' || round(spend) || ' SEK, 0 purchases' | |
| else 'hook rate ' || round(coalesce(hook_rate, 0) * 100) || | |
| '%% < 15%% over ' || impressions || ' impressions' | |
| end as reason, | |
| spend, impressions, purchases, hook_rate | |
| from joined | |
| where (spend >= %(spend_kill)s and purchases = 0) | |
| or (impressions >= 2000 and coalesce(hook_rate, 0) < 0.15) | |
| order by spend desc | |
| """, | |
| {"spend_kill": 1.5 * target_cpa}, | |
| ) | |
| return rows | |
| def winner_candidates() -> list[dict]: | |
| """Ads worth scaling (SPEC_NEW §7.8). Read-only — surfaced for manual scaling. | |
| WINNER when purchases ≥ 3 AND CPA ≤ 0.8×TARGET_CPA. | |
| """ | |
| target_cpa = get_settings().target_cpa | |
| rows = db.fetch_all( | |
| _BASE_CTE | |
| + """ | |
| select | |
| video_id, ad_id, ad_name, angle, | |
| purchases, cpa, revenue, spend | |
| from joined | |
| where purchases >= 3 | |
| and cpa is not null | |
| and cpa <= %(cpa_win)s | |
| order by cpa asc | |
| """, | |
| {"cpa_win": 0.8 * target_cpa}, | |
| ) | |
| return rows | |