Presupuestos flexibles para cuenta de administrador

Ícono de herramientas

Esta secuencia de comandos extiende los presupuestos flexibles para que se ejecuten en varias cuentas con una sola cuenta de administrador. Los presupuestos flexibles pueden ajustar el presupuesto de tu campaña de forma dinámica todos los días con un esquema de distribución personalizado.

La secuencia de comandos lee una hoja de cálculo para cada cuenta o campaña especificada y el presupuesto correspondiente (asociado con la fecha de inicio y la fecha de finalización), encuentra la campaña, calcula el presupuesto del día actual, lo establece como el presupuesto diario de la campaña y registra el resultado en la hoja de cálculo. No tocará las campañas no especificadas en la hoja de cálculo.

Cómo funciona

La secuencia de comandos funciona de la misma manera que la secuencia de comandos de presupuesto flexible para una sola cuenta. La única funcionalidad adicional es que admite varias cuentas mediante la hoja de cálculo especificada.

Las primeras 2 columnas especifican la campaña para la que se calcula un presupuesto, las siguientes 3 especifican su información de presupuesto y la última registra el resultado de la ejecución.

El ID de la cuenta debe ser una cuenta de anunciante, no una cuenta de administrador.

Puedes tener varios presupuestos para la misma cuenta o campaña, pero asegúrate de tener solo un presupuesto activo a la vez. De lo contrario, un cálculo más reciente podría reemplazar al anterior.

Si no se especifica una cuenta o una campaña en la hoja de cálculo, la secuencia de comandos no establecerá un presupuesto flexible para ella.

Prueba estrategias de presupuesto

La secuencia de comandos incluye código de prueba para simular los efectos de ejecutarla durante varios días. Esto te da una mejor idea de lo que sucede cuando la secuencia de comandos está programada para ejecutarse a diario durante un período determinado.

De forma predeterminada, la secuencia de comandos simula una distribución del presupuesto uniforme de $500 invertidos durante 10 días.

Para ejecutar el código de prueba, llama a testBudgetStrategy en lugar de setNewBudget en el método principal:

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

La llamada a función setNewBudget está marcada como comentario, lo que indica que la secuencia de comandos ejecuta código de prueba. Este es el resultado del ejemplo:

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

Se calcula un presupuesto nuevo cada día para garantizar que se invierta de manera uniforme todos los días. Una vez que se excede la asignación inicial de presupuesto, este se establece en cero para detener el gasto.

Puedes cambiar la estrategia de presupuesto usada si cambias qué función se usa o si modificas la función en sí. La secuencia de comandos incluye dos estrategias compiladas con anterioridad: calculateBudgetEvenly y calculateBudgetWeighted. En el ejemplo anterior, se probó la primera. Actualiza la línea testBudgetStrategy para usar la última:

testBudgetStrategy(calculateBudgetWeighted, 10, 500);

Haz clic en Preview y verifica el resultado del registrador. Ten en cuenta que esta estrategia de presupuesto asigna menos presupuesto al principio del período y lo aumenta durante los próximos días.

Puedes usar este método de prueba a fin de simular cambios en las funciones de cálculo de presupuesto y probar tu propio enfoque para distribuir un presupuesto.

Asignación del presupuesto

Analicemos con más detalle la estrategia de presupuesto 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);
  }
}

Esta función toma los siguientes argumentos:

  • costSoFar: Indica cuánto acumuló esta campaña en costos desde el startDate hasta el día de hoy.
  • totalBudget: Cuánto se invertirá de startDate a endDate.
  • daysSoFar: Cuántos días transcurrieron desde el startDate hasta el día de hoy.
  • totalDays: Cantidad total de días entre el startDate y el endDate.

Puedes escribir tu propia función, siempre que tome estos argumentos. Con estos valores, puedes comparar cuánto dinero gastaste hasta el momento con cuánto gastar en total y determinar en qué momento te encuentras dentro del cronograma de todo el presupuesto.

En particular, esta estrategia de presupuesto determina cuánto presupuesto queda (totalBudget - costSoFar) y lo divide por el doble de la cantidad de días restantes. Esto pondera la distribución del presupuesto hacia el final de la campaña. Si se usa el costo desde el startDate, también se tienen en cuenta los “días lentos” en los que no inviertes todo el presupuesto que estableciste.

Presupuesto en producción

Una vez que estés conforme con tu estrategia de presupuesto, deberás realizar algunos cambios antes de programar esta secuencia de comandos para que se ejecute diariamente.

Primero, actualiza la hoja de cálculo para especificar la cuenta, la campaña, el presupuesto, la fecha de inicio y la fecha de finalización, es decir, una fila para el presupuesto de cada campaña.

  • ID de la cuenta: Es el ID de la cuenta (en formato xxx-xxx-xxxx) de la campaña a la que se aplicará la estrategia de presupuesto.
  • Nombre de la campaña: Indica el nombre de la campaña a la que se aplicará la estrategia de presupuesto.
  • Fecha de inicio: Es la fecha de inicio de tu estrategia de presupuesto. Debe ser la fecha actual o un día en el pasado.
  • Fecha de finalización: Es el último día en que deseas publicar anuncios con este presupuesto.
  • Presupuesto total: Es el importe total que intentas invertir. Este valor se indica en la moneda de la cuenta y se puede exceder según el momento en el que esté programada la ejecución de la secuencia de comandos.

A continuación, inhabilita la prueba y habilita la lógica para cambiar el presupuesto:

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

El resultado de cada campaña se registra en la columna Resultado de la ejecución.

Programación

Programa esta secuencia de comandos para que se ejecute diariamente, a la medianoche de la zona horaria local o poco después, de modo que ajuste, en la medida de lo posible, el presupuesto del día siguiente. Sin embargo, ten en cuenta que los datos de informes recuperados, como el costo, podrían retrasarse alrededor de 3 horas, por lo que el parámetro costSoFar podría hacer referencia al total del día anterior para una secuencia de comandos programada para ejecutarse después de la medianoche.

Configuración

Código fuente

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