docs(launch-watch): add 89109-1 and the launch-time lessons

Sync from SW-ads-global: six benchmark accounts through 89109-1
(2026-09-30), playbook and watch.py thresholds updated to match.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 20:35:31 +08:00
co-authored by Claude Opus 5.5
parent 9f9e70ea2d
commit 012475ff71
4 changed files with 238 additions and 203 deletions
@@ -203,6 +203,10 @@ if prev and prev.get("fresh") != fresh_iso and prev.get("day") == today:
alerts.append(f"先赚后亏 {short(name)}({camps[name]['external_campaign_id']}):最近 10 分钟 {ds:.2f}/{dr:.2f},连续 {streak[name]} 个窗口亏;今天 {sp:.2f} ROAS {rv / sp:.2f},峰值 {peak[name]:.2f} → 建议关这个计划")
st["snap"] = {"fresh": fresh_iso, "day": today, "camps": {n: v for n, v in agg.items()}}
# ---- lifecycle clock: stages follow this account's own ramp, not the wall clock (operator 2026-09-30) ----
ramp = [h for h in sorted(hsum) if hsum[h][0] >= 20 and h < now.hour]
since_ramp = (now.hour + now.minute / 60 - ramp[0]) if ramp else None # hours since the first >= 20 USD hour began
# ---- rule: budget 80% / 95% while profitable; say whether adding still makes sense ----
for name, (sp, rv) in agg.items():
c = camps[name]; bud = f(c.get("budget")); ro = rv / sp if sp else 0.0
@@ -212,13 +216,12 @@ for name, (sp, rv) in agg.items():
key = f"budget{pct}|{c['external_campaign_id']}|{bud}"
if key not in rep:
rep[key] = {"sp": sp, "ro": ro}
late = now.hour >= 17
hint = "已过 17 点,手册建议不加" if late else ("ROAS > 1.3,可以加" if ro > 1.3 else "ROAS 不到 1.3,加不加看你")
late = since_ramp is not None and since_ramp >= 5
hint = "起量已满 5 小时,手册建议不加" if late else ("ROAS > 1.3,可以加" if ro > 1.3 else "ROAS 不到 1.3,加不加看你")
alerts.append(f"预算快用完 {short(name)}:今天 {sp:.0f}/{bud:.0f}({sp / bud * 100:.0f}%)ROAS {ro:.2f} → {hint}")
break
# ---- judgement: first full hours after the account starts spending ----
ramp = [h for h in sorted(hsum) if hsum[h][0] >= 20 and h < now.hour]
if ramp and "judge" not in lc:
h0 = ramp[0]; r0 = hsum[h0][1] / hsum[h0][0]
verdict = "好户" if r0 >= 1.2 else ("观察" if r0 >= 0.9 else "差户信号")
@@ -274,8 +277,8 @@ if hours_in >= 4 and tsp >= 50 and cum < 0.90 and w30 and w30[2] < 0.90:
H.append(f"开投 {hours_in:.1f} 小时累计 ROAS {cum:.2f} < 0.90,最近 30 分钟 {w30[2]:.2f} 也 < 0.90")
if w30 and w30[0] >= 30 and w30[2] < 0.80:
H.append(f"最近 30 分钟放量 {w30[0]:.2f} USD 但 ROAS 只有 {w30[2]:.2f} < 0.80")
if (now.hour >= 18 or hours_in >= 8) and w30 and w30[0] >= 3 and w30[2] < 1.0:
H.append(f"进入收尾期(18 点后或开投满 8 小时),最近 30 分钟 ROAS {w30[2]:.2f} < 1")
if ((since_ramp is not None and since_ramp >= 6) or hours_in >= 8) and w30 and w30[0] >= 3 and w30[2] < 1.0:
H.append(f"进入收尾期(起量后满 6 小时或开投满 8 小时),最近 30 分钟 ROAS {w30[2]:.2f} < 1")
if tsp >= 50 and cum >= 1.0 and lc.get("was_below_1") and "breakeven" not in lc:
lc["breakeven"] = now.strftime("%H:%M")
H.append(f"累计 ROAS 回到 1 以上({cum:.2f}),可以考虑收尾锁住")
@@ -345,7 +348,7 @@ except (OSError, ValueError, KeyError):
print("\n".join(alerts) if alerts else "一轮无异常")
top = sorted(agg.items(), key=lambda kv: -kv[1][0])[:3]
stage = "等量期" if not lc.get("had_spend") and hours_in < 2 else ("判断期" if hours_in < 3 else ("放量期" if now.hour < 18 and hours_in < 8 else "收尾期"))
stage = ("等量期" if not ramp else ("判断期" if since_ramp < 3 else ("放量期" if since_ramp < 6 and hours_in < 8 else "收尾期"))) if hours_in < 8 else "收尾期"
w30t = f";最近 30 分钟 {w30[0]:.2f}/{w30[1]:.2f} ROAS {w30[2]:.2f}" if w30 else ""
lag = f";按天数据落后,TikTok 小时数合计 {h_total:.2f}" if h_total - tsp > 2 else ""
print(f"[{stage} {hours_in:.1f}h|{len(camps)} 个在投计划 今天 {tsp:.2f}/{trv:.2f} ROAS {cum:.2f}{w30t};"