灵活预算 - 经理账号

工具图标

此脚本扩展了灵活预算功能,可以针对单个经理帐号下的多个帐号运行该脚本。“灵活预算”可以按照自定义的预算分配方案每天动态调整广告系列预算。

该脚本会读取每个指定帐号/广告系列和相应预算(与开始日期和结束日期相关联)的电子表格,找到广告系列,计算当天的预算,将其设置为广告系列的每日预算,并在电子表格中记录结果。而不会影响电子表格中未指定的广告系列。

运作方式

此脚本的运作方式与单帐号“灵活预算”脚本相同。 唯一的附加功能是通过指定的电子表格支持多个帐号。

前两列指定要为其计算预算的广告系列,接下来的三列指定其预算信息,最后一列记录执行结果。

账号 ID 应该是广告客户账号,而不是经理账号。

您可以为同一个帐号/广告系列设置多项预算,但要确保一次只有一个有效预算,否则较新的预算计算结果可能会覆盖旧预算

如果电子表格中未指定帐号/广告系列,脚本将不会为其设置灵活预算。

测试预算策略

脚本会添加测试代码来模拟运行多天的效果。这可以让您更好地了解,在脚本在一段时间内按计划每天运行时,会发生什么情况。

默认情况下,此脚本会模拟在 10 天内支出 500 美元的均匀预算分配。

您可以通过在主方法中调用 testBudgetStrategy(而不是 setNewBudget)来运行测试代码:

function main() {
  testBudgetStrategy(calculateBudgetEvenly, 10, 500);
  //  setNewBudget(calculateBudgetWeighted);
}

setNewBudget 函数调用已被注释掉,这表示脚本正在运行测试代码。以下是上述示例的输出结果:

Day 1.0 of 10.0, new budget 50.0, cost so far 0.0
Day 2.0 of 10.0, new budget 50.0, cost so far 50.0
Day 3.0 of 10.0, new budget 50.0, cost so far 100.0
Day 4.0 of 10.0, new budget 50.0, cost so far 150.0
Day 5.0 of 10.0, new budget 50.0, cost so far 200.0
Day 6.0 of 10.0, new budget 50.0, cost so far 250.0
Day 7.0 of 10.0, new budget 50.0, cost so far 300.0
Day 8.0 of 10.0, new budget 50.0, cost so far 350.0
Day 9.0 of 10.0, new budget 50.0, cost so far 400.0
Day 10.0 of 10.0, new budget 50.0, cost so far 450.0
Day 11.0 of 10.0, new budget 0.0, cost so far 500.0

每天都会计算新预算,以确保均匀支出预算。超出初始预算配额后,预算会设为 0 以停止支出。

您可以通过更改使用的函数或修改函数本身来更改预算策略。该脚本有两个预构建的策略:calculateBudgetEvenlycalculateBudgetWeighted;上一个示例刚刚测试了前者 - 将 testBudgetStrategy 行更新为使用后者:

testBudgetStrategy(calculateBudgetWeighted, 10, 500);

点击预览并检查记录器输出。请注意,此预算策略在预算期的早期阶段分配的预算较少,并在接下来的几天内增加预算。

您可以使用这种测试方法来模拟预算计算函数的变更,并尝试用自己的方法分配预算。

预算分配

下面我们来详细了解一下“calculateBudgetWeighted”预算策略:

// One calculation logic that distributes remaining budget in a weighted manner
function calculateBudgetWeighted(costSoFar, totalBudget, daysSoFar, totalDays) {
  const daysRemaining = totalDays - daysSoFar;
  const budgetRemaining = totalBudget - costSoFar;
  if (daysRemaining <= 0) {
    return budgetRemaining;
  } else {
    return budgetRemaining / (2 * daysRemaining - 1);
  }
}

此函数接受以下参数:

  • costSoFar:此广告系列从 startDate到今天累积的费用。
  • totalBudget:从 startDateendDate的支出金额。
  • daysSoFar:从 startDate 到今天过去了多少天。
  • totalDaysstartDateendDate 之间的总天数。

您可以编写自己的函数,但必须包含这些参数。使用这些值,您可以比较到目前为止已经支出的金额与总预算金额,并确定您当前在整个预算时间表内的位置。

具体而言,此预算策略会计算还剩余多少预算 (totalBudget - costSoFar),并将其除以剩余天数的两倍。这会衡量广告系列投放末期预算分配的效果。 通过使用从 startDate 开始计算的费用,还会考虑因流量不够高而未能全部用完所设预算的那些天。

在生产环境中设置预算

在对预算策略感到满意后,您需要先进行一些更改,然后才能安排此脚本每天运行。

首先,更新电子表格以指定帐号、广告系列、预算、开始日期、结束日期(每个广告系列预算各占一行)。

  • 帐号 ID:要应用此预算策略的广告系列的帐号 ID(格式为 xxx-xxx-xxxx)。
  • 广告系列名称:要应用此预算策略的广告系列的名称。
  • 开始日期:预算策略的开始日期。该日期应该是当前或过去的日期。
  • 结束日期:您希望使用此预算投放广告的最后一天。
  • 总预算:您要支出的总金额。此值以账号币种为单位,可以超出该值,具体取决于脚本计划运行的时间。

接下来,停用测试并启用逻辑来实际更改预算:

function main() {
  //  testBudgetStrategy(calculateBudgetEvenly, 10, 500);
  setNewBudget(calculateBudgetWeighted);
}

每个广告系列的结果会记录在 Execution Result 列中。

正在安排

将此脚本设置为每天在本地时区的午夜或过后不久运行,从而尽可能地充分利用新的一天的预算。但请注意,检索到的报告数据(如费用)可能会延迟大约 3 小时,因此,对于安排在午夜后运行的脚本,costSoFar 参数可能引用了昨天的总金额。

初始设置

  • 点击下面的按钮,在您的 Google Ads 帐号中创建脚本。

    安装脚本模板

  • 点击下面的按钮以创建电子表格模板的副本。

    复制模板电子表格

  • 更新脚本中的 spreadsheet_url

  • 将脚本设为每天运行。

源代码

// Copyright 2015, Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @name MCC Flexible Budgets
 *
 * @overview The MCC Flexible Budgets script dynamically adjusts campaign budget
 *     daily for accounts under an MCC account with a custom budget distribution
 *     scheme. See
 *     https://developers.google.com/google-ads/scripts/docs/solutions/manager-flexible-budgets
 *     for more details.
 *
 * @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
 *
 * @version 2.1
 *
 * @changelog
 * - version 2.1
 *   - Split into info, config, and code.
 *  - version 2.0
 *   - Updated to use new Google Ads scripts features.
 * - version 1.0.4
 *   - Add support for video and shopping campaigns.
 * - version 1.0.3
 *   - Added validation for external spreadsheet setup.
 * - version 1.0.2
 *   - Fix a minor bug in variable naming.
 *   - Use setAmount on the budget instead of campaign.setBudget.
 * - version 1.0.1
 *   - Improvements to time zone handling.
 * - version 1.0
 *   - Released initial version.
 */
/**
 * Configuration to be used for the Flexible Budgets script.
 */
CONFIG = {
  // URL of the default spreadsheet template. This should be a copy of
  // https://docs.google.com/spreadsheets/d/17wocOgrLeRWF1Qi_BjEigCG0qVMebFHrbUS-Vk_kpLg/copy
  // Make sure the sheet is owned by or shared with same Google user executing the script
  'spreadsheet_url': 'YOUR_SPREADSHEET_URL',

  'advanced_options': {
    // Please fix the following variables if you need to reformat the
    // spreadsheet
    // column numbers of each config column. Column A in your spreadsheet has
    // column number of 1, B has number of 2, etc.
    'column': {
      'accountId': 2,
      'campaignName': 3,
      'startDate': 4,
      'endDate': 5,
      'totalBudget': 6,
      'results': 7
    },

    // Actual config (without header and margin) starts from this row
    'config_start_row': 5
  }
};

const SPREADSHEET_URL = CONFIG.spreadsheet_url;
const COLUMN = CONFIG.advanced_options.column;
const CONFIG_START_ROW = CONFIG.advanced_options.config_start_row;

function main() {
  // Uncomment the following function to test your budget strategy function
  // testBudgetStrategy(calculateBudgetEvenly, 10, 500);
  setNewBudget(calculateBudgetWeighted);
}

// Core logic for calculating and setting campaign daily budget
function setNewBudget(budgetFunc) {
  console.log(`Using spreadsheet - ${SPREADSHEET_URL}.`);
  const spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
  spreadsheet.setSpreadsheetTimeZone(AdsApp.currentAccount().getTimeZone());
  const sheet = spreadsheet.getSheets()[0];

  const endRow = sheet.getLastRow();

  const mccAccount = AdsApp.currentAccount();
  sheet.getRange(2, 6, 1, 2).setValue(mccAccount.getCustomerId());

  const today = new Date();

  for (let i = CONFIG_START_ROW; i <= endRow; i++) {
    console.log(`Processing row ${i}`);

    const accountId = sheet.getRange(i, COLUMN.accountId).getValue();
    const campaignName = sheet.getRange(i, COLUMN.campaignName).getValue();
    const startDate = new Date(sheet.getRange(i, COLUMN.startDate).getValue());
    const endDate = new Date(sheet.getRange(i, COLUMN.endDate).getValue());
    const totalBudget = sheet.getRange(i, COLUMN.totalBudget).getValue();
    const resultCell = sheet.getRange(i, COLUMN.results);

    const accountIter = AdsManagerApp.accounts().withIds([accountId]).get();
    if (!accountIter.hasNext()) {
      resultCell.setValue('Unknown account');
      continue;
    }
    const account = accountIter.next();
    AdsManagerApp.select(account);

    const campaign = getCampaign(campaignName);
    if (!campaign) {
      resultCell.setValue('Unknown campaign');
      continue;
    }

    if (today < startDate) {
      resultCell.setValue('Budget not started yet');
      continue;
    }
    if (today > endDate) {
      resultCell.setValue('Budget already finished');
      continue;
    }

    const costSoFar = campaign
                          .getStatsFor(
                              getDateStringInTimeZone('yyyyMMdd', startDate),
                              getDateStringInTimeZone('yyyyMMdd', endDate))
                          .getCost();
    const daysSoFar = datediff(startDate, today);
    const totalDays = datediff(startDate, endDate);
    const newBudget = budgetFunc(costSoFar, totalBudget, daysSoFar, totalDays);
    campaign.getBudget().setAmount(newBudget);
    console.log(
        `AccountId=${accountId}, CampaignName=${campaignName}, ` +
        `StartDate=${startDate}, EndDate=${endDate}, ` +
        `CostSoFar=${costSoFar}, DaysSoFar=${daysSoFar}, ` +
        `TotalDays=${totalDays}, NewBudget=${newBudget}'`);
    resultCell.setValue(`Set today's budget to ${newBudget}`);
  }

  // update "Last execution" timestamp
  sheet.getRange(1, 3).setValue(today);
  AdsManagerApp.select(mccAccount);
}

// One calculation logic that distributes remaining budget evenly
function calculateBudgetEvenly(costSoFar, totalBudget, daysSoFar, totalDays) {
  const daysRemaining = totalDays - daysSoFar;
  const budgetRemaining = totalBudget - costSoFar;
  if (daysRemaining <= 0) {
    return budgetRemaining;
  } else {
    return budgetRemaining / daysRemaining;
  }
}

// One calculation logic that distributes remaining budget in a weighted manner
function calculateBudgetWeighted(costSoFar, totalBudget, daysSoFar, totalDays) {
  const daysRemaining = totalDays - daysSoFar;
  const budgetRemaining = totalBudget - costSoFar;
  if (daysRemaining <= 0) {
    return budgetRemaining;
  } else {
    return budgetRemaining / (2 * daysRemaining - 1);
  }
}

// Test function to verify budget calculation logic
function testBudgetStrategy(budgetFunc, totalDays, totalBudget) {
  let daysSoFar = 0;
  let costSoFar = 0;
  while (daysSoFar <= totalDays + 2) {
    const newBudget = budgetFunc(costSoFar, totalBudget, daysSoFar, totalDays);
    console.log(
        `Day ${daysSoFar + 1} of ${totalDays}, ` +
        `new budget ${newBudget}, cost so far ${costSoFar}`);
    costSoFar += newBudget;
    daysSoFar += 1;
  }
}

// Return number of days between two dates, rounded up to nearest whole day.
function datediff(from, to) {
  const millisPerDay = 1000 * 60 * 60 * 24;
  return Math.ceil((to - from) / millisPerDay);
}

// Produces a formatted string representing a given date in a given time zone.
function getDateStringInTimeZone(format, date, timeZone) {
  date = date || new Date();
  timeZone = timeZone || AdsApp.currentAccount().getTimeZone();
  return Utilities.formatDate(date, timeZone, format);
}

/**
 * Finds a campaign by name, whether it is a regular, video, or shopping
 * campaign, by trying all in sequence until it finds one.
 *
 * @param {string} campaignName The campaign name to find.
 * @return {Object} The campaign found, or null if none was found.
 */
function getCampaign(campaignName) {
  const selectors =
      [AdsApp.campaigns(), AdsApp.videoCampaigns(), AdsApp.shoppingCampaigns()];
  for (const selector of selectors) {
    const campaignIter =
        selector.withCondition(`CampaignName = "${campaignName}"`).get();
    if (campaignIter.hasNext()) {
      return campaignIter.next();
    }
  }
  return null;
}

/**
 * Validates the provided spreadsheet URL to make sure that it's set up
 * properly. Throws a descriptive error message if validation fails.
 *
 * @param {string} spreadsheeturl The URL of the spreadsheet to open.
 * @return {Spreadsheet} The spreadsheet object itself, fetched from the URL.
 * @throws {Error} If the spreadsheet URL hasn't been set
 */
function validateAndGetSpreadsheet(spreadsheeturl) {
  if (spreadsheeturl == 'YOUR_SPREADSHEET_URL') {
    throw new Error(
        'Please specify a valid Spreadsheet URL. You can find' +
        ' a link to a template in the associated guide for this script.');
  }
  return SpreadsheetApp.openByUrl(spreadsheeturl);
}