| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- #!/usr/bin/env python
- """一次性执行 2026-07-29 八广告分组调价实验。默认只预览。"""
- from __future__ import annotations
- import argparse
- import json
- import sys
- import uuid
- from dataclasses import asdict, dataclass
- from datetime import datetime
- from decimal import Decimal, ROUND_HALF_UP
- from pathlib import Path
- from typing import Any
- from dotenv import load_dotenv
- ROOT = Path(__file__).resolve().parent
- REPO_ROOT = ROOT.parents[1]
- MINIAPP_ROOT = ROOT.parent / "auto_put_ad_mini"
- for path in (ROOT, MINIAPP_ROOT, REPO_ROOT):
- if str(path) not in sys.path:
- sys.path.insert(0, str(path))
- load_dotenv(MINIAPP_ROOT / ".env", override=False)
- load_dotenv(REPO_ROOT / ".env", override=False)
- from run_once import SHANGHAI # noqa: E402
- from storage import ( # noqa: E402
- advisory_lock,
- clear_bid_experiment_hold,
- initialize_schema,
- insert_action_log,
- set_bid_experiment_hold,
- upsert_realtime_account_scope,
- )
- from tencent_client import ( # noqa: E402
- ACTIVE_STATUS,
- TencentClient,
- current_bid_fen,
- resolve_bid_field,
- )
- @dataclass(frozen=True)
- class ExperimentAd:
- group: str
- factor: Decimal
- agency: str
- audience_name: str
- account_id: int
- adgroup_id: int
- adgroup_name: str
- hold_realtime_bid: bool = False
- EXPERIMENT_ADS = (
- ExperimentAd("下调10%", Decimal("0.90"), "小程序-代投-像素", "R50*已转化", 84207102, 107115658070, "R50*已转化-含小程序"),
- ExperimentAd("下调10%", Decimal("0.90"), "小程序-自动化", "泛人群", 86748335, 117650209221, "泛人群-20260716-关键页面-targeted", True),
- ExperimentAd("下调5%", Decimal("0.95"), "小程序-代投-像素", "", 82037573, 116560437525, "泛人群-公小朋-45+-0713"),
- ExperimentAd("下调5%", Decimal("0.95"), "小程序-代投-翱鲨", "回流330以上人群", 86532580, 118337357855, "票圈-图片-朋友圈+公小-0719-zc-R330人群-关键页"),
- ExperimentAd("上调5%", Decimal("1.05"), "小程序-代投-棱镜", "R50*泛知识*时政历史", 79911661, 108000527445, "0612-朋友圈+公众号小程序-关键页r50历史-玉"),
- ExperimentAd("上调5%", Decimal("1.05"), "小程序-自动化", "回流330以上人群", 86197371, 115272976405, "回流330以上人群-20260708-关键页面-targeted", True),
- ExperimentAd("上调10%", Decimal("1.10"), "小程序-代投-翱鲨", "R50*泛知识*生活科普", 83290935, 108653638689, "票圈-图片-朋友圈+公小-0615-zc-R50*泛知识*生活科普-关键页-2"),
- ExperimentAd("上调10%", Decimal("1.10"), "小程序-代投-棱镜", "wx*商业", 85504853, 119352748609, "0722朋友圈+公众号小程序-wx*商业*玉-点击"),
- )
- def target_bid_fen(current_bid: int, factor: Decimal) -> int:
- return int(
- (Decimal(current_bid) * factor).quantize(
- Decimal("1"), rounding=ROUND_HALF_UP
- )
- )
- def prepare(client: TencentClient) -> list[dict[str, Any]]:
- rows: list[dict[str, Any]] = []
- for spec in EXPERIMENT_ADS:
- ad = client.get_ad(spec.account_id, spec.adgroup_id)
- actual_name = str(ad.get("adgroup_name") or "").strip()
- if actual_name != spec.adgroup_name:
- raise ValueError(
- f"广告名称不一致: account={spec.account_id} "
- f"adgroup={spec.adgroup_id} actual={actual_name!r}"
- )
- status = str(ad.get("configured_status") or "")
- if status != ACTIVE_STATUS:
- raise ValueError(
- f"广告当前不是正常状态: account={spec.account_id} "
- f"adgroup={spec.adgroup_id} status={status}"
- )
- bid_field = resolve_bid_field(ad, None)
- current_bid = current_bid_fen(ad, bid_field)
- if current_bid is None or current_bid <= 0:
- raise ValueError(
- f"广告当前出价无效: account={spec.account_id} "
- f"adgroup={spec.adgroup_id} field={bid_field}"
- )
- rows.append(
- {
- **asdict(spec),
- "factor": str(spec.factor),
- "configured_status": status,
- "bid_field": bid_field,
- "current_bid_fen": current_bid,
- "target_bid_fen": target_bid_fen(current_bid, spec.factor),
- "status": "planned",
- }
- )
- return rows
- def write_report(rows: list[dict[str, Any]], mode: str) -> Path:
- output_dir = ROOT / "outputs"
- output_dir.mkdir(parents=True, exist_ok=True)
- timestamp = datetime.now(SHANGHAI).strftime("%Y%m%d_%H%M%S")
- path = output_dir / f"bid_experiment_20260729_{mode}_{timestamp}.json"
- path.write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
- return path
- def release_holds(apply: bool) -> int:
- now = datetime.now(SHANGHAI)
- rows = [
- {
- "account_id": spec.account_id,
- "adgroup_id": spec.adgroup_id,
- "adgroup_name": spec.adgroup_name,
- "status": "planned",
- }
- for spec in EXPERIMENT_ADS
- if spec.hold_realtime_bid
- ]
- if apply:
- with advisory_lock("tencent_realtime_control") as acquired:
- if not acquired:
- raise RuntimeError("实时调控正在执行,未获得数据库锁")
- for row in rows:
- clear_bid_experiment_hold(
- int(row["account_id"]),
- int(row["adgroup_id"]),
- action_at=now,
- )
- row["status"] = "success"
- report = write_report(rows, "release_apply" if apply else "release_dry_run")
- print(json.dumps({"rows": rows, "report": str(report)}, ensure_ascii=False))
- return 0
- def register_pause_only_scope(apply: bool) -> int:
- rows = [
- {
- "account_id": spec.account_id,
- "audience_name": spec.audience_name,
- "control_mode": "PAUSE_ONLY",
- "status": "planned",
- }
- for spec in EXPERIMENT_ADS
- ]
- if apply:
- for row in rows:
- upsert_realtime_account_scope(
- account_id=int(row["account_id"]),
- control_mode="PAUSE_ONLY",
- audience_name=str(row["audience_name"]),
- bid_scene=None,
- source="bid_experiment_20260729",
- note="八账户只参与实时CPM关停",
- )
- row["status"] = "success"
- report = write_report(
- rows,
- "scope_apply" if apply else "scope_dry_run",
- )
- print(json.dumps({"rows": rows, "report": str(report)}, ensure_ascii=False))
- return 0
- def apply_experiment(client: TencentClient, rows: list[dict[str, Any]]) -> int:
- run_id = f"bid-exp-20260729-{uuid.uuid4().hex[:12]}"
- now = datetime.now(SHANGHAI)
- failures = 0
- with advisory_lock("tencent_realtime_control") as acquired:
- if not acquired:
- raise RuntimeError("实时调控正在执行,未获得数据库锁")
- for row in rows:
- before = int(row["current_bid_fen"])
- target = int(row["target_bid_fen"])
- try:
- readback = client.update_ad(
- int(row["account_id"]),
- int(row["adgroup_id"]),
- bid_field=str(row["bid_field"]),
- target_bid_fen=target,
- )
- row["readback_bid_fen"] = int(readback[row["bid_field"]])
- if row["hold_realtime_bid"]:
- try:
- set_bid_experiment_hold(
- account_id=int(row["account_id"]),
- adgroup_id=int(row["adgroup_id"]),
- adgroup_name=str(row["adgroup_name"]),
- bid_field=str(row["bid_field"]),
- base_bid_fen=target,
- action_at=now,
- reason="20260729分组调价实验_手动解除",
- )
- except Exception:
- client.update_ad(
- int(row["account_id"]),
- int(row["adgroup_id"]),
- bid_field=str(row["bid_field"]),
- target_bid_fen=before,
- )
- raise
- row["status"] = "success"
- except Exception as exc:
- failures += 1
- row["status"] = "failed"
- row["error"] = str(exc)
- try:
- insert_action_log(
- {
- "run_id": run_id,
- "control_date": now.date(),
- "decision": "BID_EXPERIMENT",
- "account_id": row["account_id"],
- "adgroup_id": row["adgroup_id"],
- "adgroup_name": row["adgroup_name"],
- "bid_field": row["bid_field"],
- "base_bid_fen": row["target_bid_fen"],
- "before_bid_fen": row["current_bid_fen"],
- "target_bid_fen": row["target_bid_fen"],
- "before_status": row["configured_status"],
- "apply_mode": True,
- "execution_status": row["status"],
- "error_message": row.get("error"),
- }
- )
- except Exception as exc:
- row["audit_error"] = str(exc)
- report = write_report(rows, "apply")
- print(
- json.dumps(
- {"run_id": run_id, "failures": failures, "report": str(report)},
- ensure_ascii=False,
- )
- )
- return 1 if failures else 0
- def main() -> int:
- parser = argparse.ArgumentParser(description="执行20260729八广告分组调价实验")
- parser.add_argument("--apply", action="store_true", help="真实修改腾讯出价")
- parser.add_argument(
- "--release-hold",
- action="store_true",
- help="只解除两条自动化广告的实时出价冻结,不修改腾讯价格",
- )
- parser.add_argument(
- "--register-pause-only",
- action="store_true",
- help="只把八个账户登记为实时CPM关停范围,不再次调价",
- )
- args = parser.parse_args()
- initialize_schema()
- if args.release_hold:
- return release_holds(args.apply)
- if args.register_pause_only:
- return register_pause_only_scope(args.apply)
- client = TencentClient()
- rows = prepare(client)
- if args.apply:
- return apply_experiment(client, rows)
- report = write_report(rows, "dry_run")
- print(json.dumps({"rows": rows, "report": str(report)}, ensure_ascii=False))
- return 0
- if __name__ == "__main__":
- raise SystemExit(main())
|