# 腾讯广告实时调控

独立管理实时投放指标采集、状态判断和腾讯广告调控，不依赖广告创建或历史调价分析流程。

## 实时业务收入预测

第一阶段从 `loghubods.ads_ad_own_package_detail_15min` 读取当天
`package_cost_times_today`，预测当天最终收入并计算建议总成本。日表仅补充 CPM、
曝光、DAU 等业务指标和次日最终对账，不进入预测收入公式。该能力只采集、计算和
落库，不调用腾讯写接口，也不发送飞书审批。

当前主模型使用“加权30分钟速度 × 历史同时间点剩余倍数”：最近15分钟收入权重
默认为 2/3，前一个15分钟权重为 1/3；每个历史完整日在同一预测时点计算
`(全天收入-当时累计收入)/加权速度`，发布 P10/P50/P90 和 MAD。实时任务只读取
`REVENUE_SPEED_PARAMETER_VERSION` 指定的已发布参数，P50 是正式预测，P10/P90
形成经验区间。参数缺失、样本不足、窗口不连续或速度非正时会停止本轮预测并记录
明确原因，不能现场重算参数或改用其他算法兜底。

表中的 `report_date` 是窗口开始时间，预测与报表统一按窗口结束时间
`report_date + 15 分钟`记录。默认从 06:15 开始，按自然15分钟刻度预测一次；
建议总成本默认等于预测收入除以3.5。临时预算口径从
`loghubods.opengid_base_data` 读取昨日完整日渠道成本，
将小程序投流以外的渠道成本全部预留；小程序目标成本等于建议总成本减去该预留，
最低为 0。该口径只用于预测验证，其他渠道尚不具备实时成本。
原始快照与版本化预测分别保存在 `revenue_forecast_observation` 和
`revenue_forecast_result`；历史速度样本和已发布参数分别保存在
`revenue_forecast_speed_sample`、`revenue_forecast_speed_parameter`。
参数版本发布后不可覆盖；增加训练日期或调整权重时必须使用新的
`REVENUE_SPEED_PARAMETER_VERSION`。

发布参数版本：

```bash
.venv/bin/python \
  examples/tencent_realtime_control/build_revenue_speed_parameters.py \
  --start-date 20260727 --end-date 20260805 \
  --parameter-version revenue_speed_params_v1
```

单次执行：

```bash
.venv/bin/python \
  examples/tencent_realtime_control/run_revenue_forecast.py
```

仅为排查数据而在配置时段外运行：

```bash
.venv/bin/python \
  examples/tencent_realtime_control/run_revenue_forecast.py \
  --ignore-runtime-window
```

常驻任务由 `ad-control-service` 承载，默认关闭。完整环境变量见 `.env.example`。

严格走步回测，每个预测日只使用此前日期：

```bash
.venv/bin/python \
  examples/tencent_realtime_control/backtest_revenue_speed_forecast.py \
  --history-start 20260727 \
  --start-date 20260728 --end-date 20260805
```

回测只读取 ODPS，不写 MySQL，也不会调用腾讯或飞书。

## 数据和范围

- 从 `loghubods.advertiser_data_da_hour` 读取当天整体小时数据。
- CPM 直接使用 `真实cpm_总`。
- 固定过滤名称、广告主、公司、行业、客户和落地页类型均为 `SUM`。
- 实时管理账户取历史 `ad_creation_account_config` 与
  `account_whitelist.enabled=1` 的交集；关闭创建配置只停止新建和补创意，
  不会让既有广告退出实时管理。
- 普通出价使用 `bid_amount`，最大转化量使用 `custom_cost_cap`。
- 每条广告首次纳管时将腾讯当前实际出价持久化为基础出价。

## 调控规则

北京时间默认每天 `12:00-21:00` 每 10 分钟检查一次。广告自身投放时段
仍由 `time_series` 管理，在实时调控开始前按基础出价正常投放：

- `CPM > 250`：基础出价上调 5%。
- `190 <= CPM <= 250`：恢复基础出价并恢复策略暂停的广告。
- `CPM < 190`：恢复基础出价，将广告开始日期延后至次日；保持广告开启，次日
  进入 `time_series` 投放时段后由腾讯自动投放。
- 同一广告每天最多上调一次；恢复基础价后，当天也不会再次上调。
- 到达 `RTC_STOP_HOUR`：恢复基础出价，不主动暂停广告。广告由腾讯的
  `time_series` 在非投放时段自动停止配量，次日进入投放时段后自动投放。
- 次日进入 `RTC_START_HOUR` 后直接读取 CPM 并执行当轮决策，不再重复执行晨间基础价恢复。
- 截止时会恢复由低 CPM 策略暂停的广告，但不打开人工暂停广告。

每个真实发生调价、延后投放或状态修改的窗口会生成一张飞书在线表格
并发送到调控群。
群消息包含触发时间、数据分区、当前 CPM、决策、影响账户/广告数、动作分布和
受影响广告当日累计消耗。表格包含日期、数据分区、当前 CPM、账户、广告 ID、
广告名称、人群包、广告当日累计消耗/曝光/点击、基础出价、调整前出价、执行后
当前出价和动作。
同一窗口的所有成功动作合并在一张表中；无动作、dry-run 和已达到当日上调
上限的广告不发送通知。飞书发送失败不会回滚已经成功的腾讯操作，本地 xlsx
仍保存在 `outputs/`，错误会写入本轮 JSON 报告和进程日志。

腾讯写接口返回 `code=0` 后不会立即判定成功。流程会按广告 ID 重新读取腾讯
广告并核对本次修改的出价和状态字段，默认最多校验 3 次、间隔 1 秒。只有读回
值与目标一致才更新成功状态并进入成功通知；持续不一致按失败写入动作审计，
飞书卡片标红并在表格中展示错误。

ODPS 小时分区可能延迟。当天尚未出现 `06` 点及之后的分区时只等待，不使用
夜间低流量分区做关停判断。最新分区相对当前时间最多允许延迟 2 小时，超过后
本轮不执行基于 CPM 的调价、恢复或关停。临时暂停通过 `begin_date` 延后至次日，
由腾讯在 `time_series` 的 06:00 时段自动恢复投放，不调用主动开启接口。

状态保存在 MySQL，使用数据库锁避免多 Pod 同时执行。相同分区和相同决策默认
不重复扫描腾讯；每 60 分钟至少刷新一次广告清单以纳管新广告。

## 飞书自然语言控制

`ad-control-service` 复用唯一飞书 WebSocket 接收运营命令。只有指定命令群内、
允许列表中的发送人明确 @机器人时才响应；普通群消息和私聊不会进入解析流程。

标准命令始终使用确定性解析：

```text
@机器人 暂停全部
@机器人 暂停 86748335,86197371
@机器人 停止 86748335
@机器人 恢复 86748335
@机器人 查询暂停状态
@机器人 查询今天自动化账户消耗
@机器人 查询今天全部账户消耗
```

开启 `RTC_NL_COMMAND_ENABLED=1` 后，无法由标准解析器识别的表达才会交给模型。
模型只返回动作和范围 JSON，不能读取数据库或调用腾讯接口。裸“暂停”会进入
5 分钟多轮补问；“暂停”只到下一投放日 06:00，“停止”持续到人工恢复。
今日消耗查询不需要二次确认，触发时实时读取腾讯 API，只回复账户数、有消耗账户数、
总消耗、曝光、点击和转化总览，不展示逐账户明细。部分账户查询失败时会明确标记为部分数据。

“全部”范围包括历史自动化账户与 `realtime_control_account_scope` 中显式纳管的
账户。历史自动化账户始终使用 `FULL` 模式；同一账户同时出现在显式纳管范围时，
不能被 `PAUSE_ONLY` 降级。预览会查询腾讯当日广告报表，逐账户展示影响广告数和今日消耗；报表失败
时禁止确认。所有写操作必须在 10 分钟内回复带命令 ID 的确认消息。重叠的待确认
命令会被拒绝，执行时只处理预览阶段冻结的广告 ID，并继续使用共享数据库锁和
腾讯写后回读。

上线顺序：

1. 保持 `RTC_APPLY_ENABLED=0`，设置 `RTC_NL_COMMAND_ENABLED=1` 验证对话和预览。
2. 确认账户范围、广告数、当日消耗和恢复时间正确。
3. 再设置 `RTC_APPLY_ENABLED=1`，重建 `ad-control-service`。

## 初始化

```bash
.venv/bin/python examples/tencent_realtime_control/init_db.py
```

## 数据检查

读取上海时区当天分时 CPM：

```bash
.venv/bin/python examples/tencent_realtime_control/fetch_daily_hourly_cpm.py
```

指定日期：

```bash
.venv/bin/python examples/tencent_realtime_control/fetch_daily_hourly_cpm.py \
  --date 20260724
```

导出近 7 天小时级 ROI 统计，并导入飞书在线表格；脚本只读取 ODPS 和上传表格，
不会调用腾讯写接口：

```bash
conda run -n agent python \
  examples/tencent_realtime_control/export_historical_hourly_roi.py
```

输出会包含本地 xlsx 路径和 `sheet_url=...`。默认统计截至当天的 7 个自然日，
每天包含 `06:00-22:00` 共 17 个小时。每行同时包含当前小时值，以及当天
`00:00` 至当前小时的累计总收入、累计总投放成本、累计小程序成本和对应收入比。
小程序投流收入从 `loghubods.ad_pqtid_behavior_info` 按 `dt/hh` 分区汇总
`真实收入`，并过滤 `增长渠道来源=小程序投流-稳定`；报表同时展示小时收入、
累计收入，以及它们相对小程序投流成本的比例。
上传后的飞书链接默认设置为获得链接者可编辑。也可以指定历史区间：

```bash
conda run -n agent python \
  examples/tencent_realtime_control/export_historical_hourly_roi.py \
  --end-date 20260811 --days 7
```

统计时段可通过 `--start-hour` 和 `--end-hour` 调整，结束小时包含在结果中。

如只想生成本地文件用于校验，不上传飞书：

```bash
conda run -n agent python \
  examples/tencent_realtime_control/export_historical_hourly_roi.py \
  --end-date 20260811 --days 7 --skip-upload
```

## 单次执行

默认 dry-run，不修改腾讯：

```bash
.venv/bin/python examples/tencent_realtime_control/run_once.py
```

验证 CPM 分支：

```bash
.venv/bin/python examples/tencent_realtime_control/run_once.py \
  --at 2026-07-24T15:00:00 \
  --cpm-override 260
```

使用真实账户、广告、出价和当日指标模拟完整通知链路，不修改腾讯广告：

```bash
.venv/bin/python examples/tencent_realtime_control/run_once.py \
  --at 2026-07-24T15:00:00 \
  --cpm-override 260 \
  --test-notification
```

模拟卡片、动作列和表格标题都会标记“模拟”。`--test-notification` 不能与
`--apply` 同时使用。

真实执行：

```bash
.venv/bin/python examples/tencent_realtime_control/run_once.py --apply
```

## 一次性延后生效日期

需要让全部自动化广告在指定日期才开始投放时，先 dry-run：

```bash
.venv/bin/python \
  examples/tencent_realtime_control/schedule_automated_ads_from_date.py \
  --begin-date 2026-07-26
```

确认账户数、广告数、待更新数和待恢复数后真实执行：

```bash
.venv/bin/python \
  examples/tencent_realtime_control/schedule_automated_ads_from_date.py \
  --begin-date 2026-07-26 \
  --apply
```

脚本只处理当前启用的自动化账户。它保留广告原有 `end_date` 和
`time_series`，逐账户更新 `begin_date` 并回读校验；日期更新成功后只恢复
由实时策略暂停的广告，不会打开人工暂停广告。默认 dry-run，必须显式传
`--apply` 才会修改腾讯广告。

## Docker 调度

统一控制服务在投放时段按 10 分钟边界执行，20 点收尾后休眠到次日 6 点：

```bash
RTC_APPLY_ENABLED=0 \
  .venv/bin/python \
  examples/tencent_realtime_control/run_control_service.py
```

默认所有执行入口均为 dry-run，必须设置 `RTC_APPLY_ENABLED=1` 或显式传
`--apply` 才会调用腾讯写接口。
`--at` 和 `--cpm-override` 只允许 dry-run。

生产使用统一完整镜像：

```bash
docker build \
  -f Dockerfile.auto_put_ad_mini \
  -t ad-put-agent:local .
```

Compose 从同一镜像启动 `ad-control-service` 和 `ad-daily-service`。完整部署和
dry-run 切换步骤见
`examples/auto_put_ad_mini/docs/unified_services_deployment.md`。
生产 Compose 文件兼容当前 ECS 的 Docker Compose `2.27.0`。

## 日级 ROI 逐行审批

`ad-daily-service` 可在每天 11:00 计算版本化的三日 ROI 指标并发布可编辑飞书表；
`ad-control-service` 每 60 秒读取黄色审批列并处理逐行动作：

```text
打开群内 ROI 在线表格，在“小程序投流”工作表黄色“审批选择”列逐行选择“批准”或“拒绝”。
“批准”即为最终确认，`ad-control-service` 轮询后自动执行，不再需要群聊二次确认。
```

`DAILY_ROI_ENABLED=1` 只开启计算和通知；同时设置 `ROI_APPLY_ENABLED=1` 和
`ROI_SHEET_APPROVAL_ENABLED=1` 才允许表格批准后
暂停低 ROI 动态创意或提高高 ROI 广告的永久基础出价。ROI 腾讯写操作与实时
CPM 调控共用 `RTC_DB_LOCK_NAME`，并在写入后回读验证。详细开关和分阶段上线
步骤见统一部署文档。

当前指标版本 `north_star_roi_t15_v8`、报表版本 `roi_report_v41` 消费 `ROI_FISSION_PARAMETER_VERSION` 指定的
已发布传播裂变参数，默认 `20260712_A0-A15_v2`。T0 收入使用实际值，仅预测
T1-T15 裂变增量。参数只影响日级 ROI 计算和建议，
不参与实时 CPM 决策。正式参数由更新脚本发布到 MySQL，日级任务只读取数据库且
不会现场重算或回退到镜像文件。ROI 在线表同时发送到 ROI 通知群和
`FEISHU_OPERATOR_CHAT_ID`，重复群 ID 自动去重。

## ECS 首次迁移 ODPS 配置

仅用于旧 `auto_put_ad_mini` 容器仍在运行、但 ECS 的
`/home/server/.env` 缺少 ODPS 环境变量时。命令不会在终端打印密钥值。

先从旧容器迁移配置：

```bash
cd /home/server

sed -i \
  '/^ODPS_ACCESS_ID=/d;/^ODPS_ACCESS_SECRET=/d;/^ODPS_PROJECT=/d' \
  /home/server/.env

docker exec auto_put_ad_mini python -c 'import ast,shlex; tree=ast.parse(open("/app/examples/auto_put_ad_mini/tools/odps_module.py",encoding="utf-8").read()); values={target.attr:node.value.value for node in ast.walk(tree) if isinstance(node,ast.Assign) and isinstance(node.value,ast.Constant) for target in node.targets if isinstance(target,ast.Attribute)}; print("ODPS_ACCESS_ID="+shlex.quote(values["accessId"])); print("ODPS_ACCESS_SECRET="+shlex.quote(values["accessSecret"])); print("ODPS_PROJECT=loghubods")' >> /home/server/.env

chmod 600 /home/server/.env
```

确认变量名已经写入，不显示变量值：

```bash
grep -E '^ODPS_(ACCESS_ID|ACCESS_SECRET|PROJECT)=' /home/server/.env \
  | cut -d= -f1
```

验证 ODPS 查询：

```bash
cd /home/server
VERSION=latest docker compose --env-file /dev/null run --rm \
  ad_control_service \
  python /app/examples/tencent_realtime_control/fetch_daily_hourly_cpm.py \
  --date 20260724
```

这只是旧配置迁移手段。迁移完成后应轮换已经硬编码在旧代码和 Git 历史中的
AccessKey，并将新值只保存在受权限保护的生产环境配置中。

## 环境变量

生产部署参考仓库根目录 `runtime.env.example`；本目录 `.env.example` 仅用于本地兼容。部署必须提供 ODPS、MySQL、腾讯 token 服务和飞书应用
配置，不能在代码中写入凭证。通知群优先读取 `RTC_FEISHU_CHAT_ID`，为空时
复用广告调控流程的 `FEISHU_AD_PROJECT_CHAT_ID`；不会回退发送到个人或运营
审批群。`RTC_FEISHU_NOTIFY_ENABLED=0` 可临时关闭
通知。
