Ngân sách linh hoạt – Tài khoản người quản lý

Biểu tượng công cụ

Tập lệnh này sẽ mở rộng Ngân sách linh hoạt để chạy cho nhiều tài khoản trong một tài khoản người quản lý. Ngân sách linh hoạt có thể điều chỉnh linh động ngân sách chiến dịch hằng ngày bằng một lược đồ phân bổ ngân sách tuỳ chỉnh.

Tập lệnh sẽ đọc một bảng tính cho từng tài khoản/chiến dịch được chỉ định và ngân sách tương ứng (được liên kết với ngày bắt đầu và ngày kết thúc), tìm chiến dịch, tính toán ngân sách cho ngày hiện tại, đặt làm ngân sách hằng ngày của chiến dịch và ghi lại kết quả vào bảng tính. Nó sẽ không chạm vào các chiến dịch không được chỉ định trong bảng tính.

Cách hoạt động

Tập lệnh này hoạt động theo cách tương tự như tập lệnh Ngân sách linh hoạt cho một tài khoản. Chức năng bổ sung duy nhất là hỗ trợ nhiều tài khoản thông qua bảng tính được chỉ định.

2 cột đầu tiên chỉ định chiến dịch cần tính toán ngân sách, 3 cột tiếp theo chỉ định thông tin ngân sách của chiến dịch và cột cuối cùng ghi lại kết quả thực thi.

Mã tài khoản phải là tài khoản nhà quảng cáo, không phải tài khoản người quản lý.

Bạn có thể có nhiều ngân sách cho cùng một tài khoản/chiến dịch, nhưng hãy đảm bảo mỗi lần bạn chỉ có một ngân sách đang hoạt động. Nếu không, cách tính ngân sách mới hơn có thể ghi đè ngân sách cũ

Nếu một tài khoản/chiến dịch không được chỉ định trong bảng tính, thì tập lệnh sẽ không đặt ngân sách linh hoạt cho tài khoản/chiến dịch đó.

Thử nghiệm chiến lược ngân sách

Tập lệnh bao gồm mã kiểm thử để mô phỏng hiệu quả của việc chạy trong nhiều ngày. Điều này giúp bạn hiểu rõ hơn về điều sẽ xảy ra khi tập lệnh được lên lịch để chạy hằng ngày trong một khoảng thời gian.

Theo mặc định, tập lệnh này mô phỏng mức phân bổ ngân sách đồng đều là 500 USD chi tiêu trong 10 ngày.

Bạn có thể chạy mã kiểm thử bằng cách gọi testBudgetStrategy thay vì setNewBudget trong phương thức chính:

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

Lệnh gọi hàm setNewBudget được nhận xét, cho biết tập lệnh đang chạy mã kiểm thử. Sau đây là kết quả trong ví dụ:

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

Mỗi ngày, hệ thống sẽ tính toán ngân sách mới để đảm bảo ngân sách được chi tiêu đồng đều mỗi ngày. Sau khi đã phân bổ ngân sách ban đầu, ngân sách sẽ được đặt thành 0 để tạm dừng việc chi tiêu.

Bạn có thể thay đổi chiến lược ngân sách được sử dụng bằng cách thay đổi hàm được sử dụng hoặc bằng cách sửa đổi hàm đó. Tập lệnh đi kèm với 2 chiến lược được tạo sẵn: calculateBudgetEvenlycalculateBudgetWeighted; ví dụ trước vừa kiểm thử chiến lược trước—cập nhật dòng testBudgetStrategy để sử dụng chiến lược sau:

testBudgetStrategy(calculateBudgetWeighted, 10, 500);

Nhấp vào Xem trước và kiểm tra kết quả đầu ra của trình ghi nhật ký. Xin lưu ý rằng chiến lược ngân sách này sẽ phân bổ ít ngân sách hơn sớm trong khoảng thời gian đó và sẽ tăng ngân sách trong vài ngày tiếp theo.

Bạn có thể sử dụng phương thức kiểm thử này để mô phỏng các thay đổi đối với hàm tính toán ngân sách và thử phương pháp phân phối ngân sách của riêng bạn.

Phân bổ ngân sách

Hãy xem xét kỹ hơn chiến lược ngân sách 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);
  }
}

Hàm này nhận các đối số sau:

  • costSoFar: Chiến dịch này đã tích luỹ bao nhiêu chi phí từ startDate đến hôm nay.
  • totalBudget: Số tiền chi tiêu từ startDate đến endDate.
  • daysSoFar: Số ngày đã trôi qua từ startDate đến hôm nay.
  • totalDays: Tổng số ngày từ startDate đến endDate.

Bạn có thể viết hàm của riêng mình miễn là hàm vẫn sử dụng các đối số này. Bằng cách sử dụng các giá trị này, bạn có thể so sánh số tiền bạn đã chi tiêu tính đến thời điểm hiện tại với tổng số tiền chi tiêu và xác định vị trí hiện tại của bạn trong tiến trình cho toàn bộ ngân sách.

Cụ thể, chiến lược ngân sách này sẽ tính ra số tiền còn lại (totalBudgetcostSoFar) và chia số tiền đó cho gấp đôi số ngày còn lại. Chiến lược này sẽ cân nhắc việc phân bổ ngân sách cho đến khi kết thúc chiến dịch. Khi sử dụng chi phí kể từ startDate, chi phí cũng tính đến "những ngày chậm trễ" mà bạn không chi tiêu toàn bộ ngân sách đã đặt.

Lập ngân sách trong giai đoạn sản xuất

Khi đã hài lòng với chiến lược ngân sách, bạn cần thực hiện một vài thay đổi trước khi có thể lên lịch để tập lệnh này chạy hằng ngày.

Trước tiên, hãy cập nhật bảng tính để chỉ định tài khoản, chiến dịch, ngân sách, ngày bắt đầu, ngày kết thúc – một hàng cho mỗi ngân sách chiến dịch.

  • Mã tài khoản: Mã tài khoản (có định dạng xxx-xxx-xxxx) của chiến dịch để áp dụng chiến lược ngân sách.
  • Tên chiến dịch: Tên của chiến dịch để áp dụng chiến lược ngân sách.
  • Ngày bắt đầu: Ngày bắt đầu của chiến lược ngân sách. Đây phải là ngày hiện tại hoặc một ngày trong quá khứ.
  • Ngày kết thúc: Ngày cuối cùng bạn muốn quảng cáo bằng ngân sách này.
  • Tổng ngân sách: Tổng số tiền mà bạn đang cố gắng chi tiêu. Giá trị này được tính bằng đơn vị tiền tệ của tài khoản và có thể vượt quá tuỳ thuộc vào thời điểm chạy tập lệnh theo lịch.

Tiếp theo, hãy tắt thử nghiệm này và bật logic để thực sự thay đổi ngân sách:

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

Kết quả cho mỗi chiến dịch được ghi lại trong cột Kết quả thực thi.

Lập lịch

Hãy lên lịch để tập lệnh này chạy hằng ngày, vào lúc hoặc ngay sau nửa đêm theo múi giờ địa phương để phân bổ ngân sách của ngày tiếp theo nhiều nhất có thể. Tuy nhiên, hãy lưu ý rằng dữ liệu báo cáo được truy xuất như chi phí có thể bị trễ khoảng 3 giờ. Vì vậy, tham số costSoFar có thể đang tham chiếu tổng số của ngày hôm qua cho một tập lệnh được lên lịch để chạy sau nửa đêm.

Thiết lập

  • Hãy nhấp vào nút bên dưới để tạo tập lệnh trong tài khoản Google Ads của bạn.

    Cài đặt mẫu tập lệnh

  • Nhấp vào nút bên dưới để tạo bản sao của bảng tính mẫu.

    Sao chép bảng tính mẫu

  • Cập nhật spreadsheet_url trong tập lệnh của bạn.

  • Lên lịch để chạy tập lệnh Hằng ngày.

Mã nguồn

// 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);
}