Monday, March 18, 2013

Dynamically Adjust Campaign Budgets

UPDATE 2013-04-05: There is an updated version of this script. Check it out. Dynamically Adjust Campaign Budgets v2.0.

UPDATE 2013-04-07: A big thank you to FoxSUP for helping me track down an issue with updating the budgets. Fixed line 78 to multiply the current budget by 1+to_change instead of just to_change. Also fixed a bug in calculating the change (line 76).

Here is a request from a reader:
I manage many small business PPC accounts, and some of these accounts have multiple campaigns, and they usually have a relatively small monthly click budget. I'm looking for a way to pause ALL campaigns if the entire account has spent over a certain amount month-to-date.

This is actually a pretty easy script to put together. Below is a script that will do just that. You can set the MONTHLY_BUDGET at the beginning of the script and run this script daily on your account. Once the campaigns have a total cost greater than the budget specified, it will pause all the campaigns in the account.

Then, on the first of the next month, it will enable those campaigns once again. If you make no changes to the script below, it should do just that. But let's go one step further.

You actually have the power to get and set your campaign budgets using scripts. So let's say your monthly budget is $100, but you want to make sure your ads are spaced out through the month. I have added a function below called _adjust_campaign_budget() which can be enabled through the flag ADJUST_BUDGETS at the top of the script.

The script will then attempt to calculate a run rate for your campaign to figure out if you are going to meet your budget or not. If you are going to go over, it will lower the budget of each campaign (weighted by campaign cost) so that you come in at your target. If you are going to under-spend, it will also attempt to increase your campaign budget to try to allow you to hit your goal. I have also added a _reset_budgets() function to the end that gets called on the first of the month. If you run this script more frequently, you should enable the code for checking if it is the first hour of the first day of the month.

Now understandably, this script comes with a few cautions. THIS SCRIPT MAY CAUSE YOU TO SPEND A LOT OF MONEY. I'm sure the campaigns I was testing this on were quite a bit smaller than your campaigns, with budgets to match.

Thanks,
Russ

/******************************************
* Keep Your Campaigns In Budget
* Version 1.1
* ChangeLog v1.1 
*   - cleaned up code
*   - added ability for any dates
* Created By: Russ Savage
* FreeAdWordsScripts.com
******************************************/
// Let's set some constants
var MONTHLY_BUDGET = 5000.00;

//If you want to work with a monthly budget, leave START_DATE and END_DATE blank.
var TIMEFRAME = "THIS_MONTH";
//But if you want to work with a specific timeframe, fill these in.
//Use the format yyyyMMdd, so for Jan 12th, 2014, you would put 20140112.
var START_DATE = '';
var END_DATE = '';
 
//Set this to true if you want to adjust budgets or
//keep set to false if you want to just pause all the campaigns
//when you hit your budget
var ADJUST_BUDGETS = false;
var DECIMAL_PLACES = 3;
 
function main() {
  var totalCostMTD = getTotalCost();
  var isFirstOfTheMonth = ((new Date()).getDate() == 1);
  if(START_DATE && END_DATE) {
    var today = new Date();
    today.setHours(0,0,0,0);
    var startDate = new Date(START_DATE.substring(0,4),
                             parseFloat(START_DATE.substring(4,6))-1,
                             START_DATE.substring(6,8));
    isFirstOfTheMonth = (startDate.getTime() == today.getTime());
  }
  //if you run this script more than once per day, uncomment the next line
  //isFirstOfTheMonth = (isFirstOfTheMonth && ((new Date()).getHour() == 0));
  Logger.log("Total cost: " + totalCostMTD + 
           ", Monthly budget:" + MONTHLY_BUDGET +
           ", isFirstOfTheMonth: "+isFirstOfTheMonth);
   
  if(ADJUST_BUDGETS) {
    if(isFirstOfTheMonth) {
      resetBudgets();
    } else {
      adjustCampaignBudget(totalCostMTD);
    }
  } else {
    if(totalCostMTD >= MONTHLY_BUDGET) {
      //If we have hit the limit, pause all ads
      enableOrDisableCampaigns(true);
    } else {
      // let's check if it's the first day of the month
      if((new Date()).getDate() == 1) {
        //enable all the campaigns
        enableOrDisableCampaigns(false);
      }
    }
  }
}
 
// Returns the total cost for the set TIMEFRAME
function getTotalCost() {
  var campIter = AdWordsApp.campaigns().get();
   
  var totalCost = 0;
  while(campIter.hasNext()) {
    if(START_DATE && END_DATE) {
      totalCost += campIter.next().getStatsFor(START_DATE,END_DATE).getCost();
    } else {
      totalCost += campIter.next().getStatsFor(TIMEFRAME).getCost();
    }
  }
  return totalCost;
}
 
// Enables or Disables All Campaigns In Account
function enableOrDisableCampaigns(shouldDisable) {
  var campIter = AdWordsApp.campaigns().get();
  while(campIter.hasNext()) { 
    if(shouldDisable) { 
      campIter.next().pause(); 
    } else { 
      campIter.next().enable(); 
    }
  }
}
 
// Calculates run rate and adjusts campaign bids as needed.
function adjustCampaignBudget(myTotalCost) {
  var today = new Date();
  // Accounting for December
  var eom;
  if(START_DATE && END_DATE) {
    eom = new Date(END_DATE.substring(0,4),
                   parseFloat(END_DATE.substring(4,2))-1,
                   END_DATE.substring(6,2));
  } else {
    eom = (today.getMonth() == 11) ? new Date(today.getFullYear()+1,0,1) : 
                                     new Date(today.getFullYear(),today.getMonth()+1,1);
  }
  var daysLeft = Math.round((eom-today)/1000/60/60/24);
  var daysSpent;
  if(START_DATE && END_DATE) {
    var startDate = new Date(START_DATE.substring(0,4),
                             parseFloat(START_DATE.substring(4,2))-1,
                             START_DATE.substring(6,2));
    daysSpent = Math.round((today-startDate)/1000/60/60/24);
  } else {
    daysSpent = today.getDate();
  }
  var runRate = round(myTotalCost/daysSpent);
  var projectedTotal = myTotalCost + (runRate * daysLeft);
  var percOver = round((MONTHLY_BUDGET-projectedTotal)/projectedTotal);
   
  changeSpend(percOver,myTotalCost);
}
 
//Adjusts the budget for a given campaign based on percentage of total spend
//Note: if the cost of a campaign is $0 mtd, the budget is not changed.
function changeSpend(percToChange,myTotalCost) {
  var campIter = AdWordsApp.campaigns().withCondition("Status = ENABLED").get();
   
  while(campIter.hasNext()) {
    var camp = campIter.next();
    var campCost = (START_DATE && END_DATE) ? camp.getStatsFor(START_DATE,END_DATE).getCost()
                                            : camp.getStatsFor(TIMEFRAME).getCost();
    var percOfTotal = round(campCost/myTotalCost);
    //If there is no cost for the campaign, let's not change it.
    var toChange = (percOfTotal) ? (percOfTotal*percToChange) : 0;
    camp.setBudget(camp.getBudget()*(1+toChange));
  }
}
 
// Resets the budget evenly across all campaigns
function resetBudgets() {
  Logger.log('Resetting budgets at the first of the period.');
  var campIter = AdWordsApp.campaigns().withCondition("Status = ENABLED").get();
  var campCount = 0;
  while(campIter.hasNext()) {
    campCount++;
    campIter.next();
  }
  campIter = AdWordsApp.campaigns().withCondition("Status = ENABLED").get();
  while(campIter.hasNext()) {
    campIter.next().setBudget(MONTHLY_BUDGET/campCount);
  }
}

// A helper function to make rounding a little easier
function round(value) {
  var decimals = Math.pow(10,DECIMAL_PLACES);
  return Math.round(value*decimals)/decimals;
}

21 comments:

  1. Excellent script... I was about half way done with something very similar, and this post helped to resolve few obstacles...Thanks!

    ReplyDelete
  2. Thanks again for the script. In regards to the campaigns reactivating on the first of the month, would this script enable every campaign that is currently in a "paused" state in the account, whether or not a campaign was enabled the prior month?

    If so, which lines of the code should be removed? Personally, I'm fine with going back into an acct on the first of the month and manually reactivating specific campaigns.

    Thanks!

    ReplyDelete
    Replies
    1. Hi Mark, You are correct, this script will enable all paused campaigns in the account. Probably the easiest way to only run this script on certain campaigns would be to use labels. So let's assume you create a label called 'script' and apply it to the campaigns you want to manage the budget for.

      Then, anywhere in the script where you see a camp_iter (lines 45, 56, 84, and 98), add the following .withCondition("LabelNames CONTAINS_ANY ['script']") just before the .get();

      var camp_iter = AdWordsApp.campaigns()
      .withCondition("Status = ENABLED")
      .withCondition("LabelNames CONTAINS_ANY ['script']")
      .get();

      That way the script will only operate on certain campaigns.

      Thanks,
      Russ

      Delete
  3. I love this script, I set it up with the labels to control individual campaign budgets and its working great.

    I have also tried it with all campaigns, but I like to spend different amounts on different campaigns and it does not keep those values. Any ideas on how best to achieve this?

    Thanks for your time and effort posting useful AdWords Scripts.

    ReplyDelete
    Replies
    1. Hi FoxSUP, Yeah, right now, the script just simply divides your monthly budget equally across all your campaigns (line 106).

      There are a few different ways you could solve the budget thing. I think the easiest way might be to store the campaign information in an external spreadsheet and read those values at the start of each month. That way, you could adjust budgets without changing the script.

      The change to this was pretty substantial so I put it in a separate post: Dynamically Adjust Campaign Budgets v2.0.

      Thanks,
      Russ

      Delete
    2. This comment has been removed by a blog administrator.

      Delete
    3. Hi FoxSUP, I made a small change to multiple the budgets by 30.5 to handle the daily to monthly issue.

      Thanks,
      Russ

      Delete
  4. For anyone adjusting budgets using this script, I fixed a bug that should adjust the campaign budgets correctly. A big thanks to FoxSUP for helping me track down some issues.

    Thanks,
    Russ

    ReplyDelete
  5. Hi Russel,

    I enjoyed all the scripts on your blog and i found them very help full for my MCC and i have to thank you for this.

    I have a question witch i think it apply to this script: It's there a way to see in a spreadsheet the total budget and the cost of all the campaigns in an account?
    The goal is to correlate the dates between total cost and the budget for last 30 days or last 7 days (weekly).

    Thanks.

    ReplyDelete
    Replies
    1. Hi Dany, I think you might be better off using this script to store that data independently: Store Account Performance Report in a Google Doc. Let me know if that solves your problem.

      Thanks,
      Russ

      Delete
    2. Hi Russel,

      I looked up for that script but it seems i don't have the possibility to extract the budget from the ACCOUNT_PERFORMANCE_REPORT when i put 'Budget' in the var columns.

      "Column 'Budget' is not valid for report type ACCOUNT_PERFORMANCE_REPORT. Double-check your SELECT clause. (line 33) "

      My goal is to see if the Cost is reaching he's Budget.

      Thanks.

      Delete
    3. Hi Dany, You're correct, for some reason, that report doesn't work. The one you want is BUDGET_PERFORMANCE_REPORT. I tried to make the other script generic enough to use with most of the reports available from AdWords. To update the other script, all you need to do it replace ACCOUNT_PERFORMANCE_REPORT with BUDGET_PERFORMANCE_REPORT, update the column list using the values here: BUDGET_PERFORMANCE_REPORT columns, and you should be good to go.

      Let me know if that works for you.
      Thanks,
      Russ

      Delete
    4. Hi Russel,

      Thanks man, the BUDGET_PERFORMANCE_REPORT works ok, and it is that one was i looking for but the thing is (problem) it imports in docs all the campaigns even if it is deleted or paused.
      Any tips to import only the enable campaings?

      My script looks like this:

      //-----------------------------------
      // Store Budget Performance Report In A Google Doc
      // Created By: Russ Savage
      // FreeAdWordsScripts.com
      //-----------------------------------
      function main() {
      var spreadsheet_url = "spreadsheet_url";
      var date_range = 'LAST_WEEK';
      var columns = ['AssociatedCampaignName',
      'AssociatedCampaignStatus',
      'Amount',
      'Cost'];
      var columns_str = columns.join(',') + " ";

      var sheet = getSpreadsheet(spreadsheet_url).getActiveSheet();
      if(sheet.getRange('A1:A1').getValues()[0][0] == "") {
      sheet.clear();
      sheet.appendRow(columns);
      }

      var report_iter = AdWordsApp.report(
      'SELECT ' + columns_str +
      'FROM BUDGET_PERFORMANCE_REPORT ' +
      'DURING ' +date_range, {
      apiVersion: 'v201302'
      }).rows();

      while(report_iter.hasNext()) {
      var row = report_iter.next();
      var row_array = [];
      for(var i in columns) {
      row_array.push(row[columns[i]]);
      }
      sheet.appendRow(row_array);
      }
      }

      function getSpreadsheet(spreadsheetUrl) {
      var matches = new RegExp('key=([^&#]*)').exec(spreadsheetUrl);
      if (!matches || !matches[1]) {
      throw 'Invalid spreadsheet URL: ' + spreadsheetUrl;
      }
      var spreadsheetId = matches[1];
      return SpreadsheetApp.openById(spreadsheetId);
      }

      Delete
    5. Hi Dany, In order to only get active campaigns, you should add a WHERE clause to your AWQL statement. So right before the DURING clause, add "WHERE AssociatedCampaignStatus = 'ACTIVE'".

      Thanks,
      Russ

      Delete
  6. Thanks a lot for useful information. This is exactly what I need!

    ReplyDelete
  7. Myprepaidcenter also activate cards online, in USA, UK and Canada.

    ReplyDelete
  8. Excellent script... I was about half way done with something very similar, and this post helped to resolve few obstacles) I'm just learning, so it's perfect info for me. If you need resume help you can get it at https://top-papers.com/where-to-buy-resume-paper.

    ReplyDelete



  9. INSTEAD OF GETTING A LOAN,, I GOT SOMETHING NEW
    Get $10,050 USD every week, for six months!

    See how it works
    Do you know you can hack into any ATM machine with a hacked ATM card??
    Make up you mind before applying, straight deal...
    Order for a blank ATM card now and get millions within a week!: contact us
    via email address:: besthackersworld58@gmail.com or whats-app +1(323)-723-2568

    We have specially programmed ATM cards that can be use to hack ATM
    machines, the ATM cards can be used to withdraw at the ATM or swipe, at
    stores and POS. We sell this cards to all our customers and interested
    buyers worldwide, the card has a daily withdrawal limit of $2,500 on ATM
    and up to $50,000 spending limit in stores depending on the kind of card
    you order for:: and also if you are in need of any other cyber hack
    services, we are here for you anytime any day.
    Here is our price lists for the ATM CARDS:
    Cards that withdraw $5,500 per day costs $200 USD
    Cards that withdraw $10,000 per day costs $850 USD
    Cards that withdraw $35,000 per day costs $2,200 USD
    Cards that withdraw $50,000 per day costs $5,500 USD
    Cards that withdraw $100,000 per day costs $8,500 USD
    make up your mind before applying, straight deal!!!

    The price include shipping fees and charges, order now: contact us via
    email address::besthackersworld58@gmail.com or whats-app +1(323)-723-2568

    ReplyDelete
  10. Do you need to hack into any, databaseserver spy on Facebook,Emails, Whatsapp, Viber, Snapchat, Instagram and many more.
    I urge you to get in touch with the best people for the job, i have confirm the service when i need to spy on my spouse phone. They are good at Phone Cloning and Bitcoin/binary minning and any other hack job.
    Thanks guys for the team work HACKINTECHNOLOGYATGMAILDOTCOM
    +12132951376(WHATSAPP)

    ReplyDelete