Monday, October 9, 2017

Limit AdWords Overdelivery to Any Amount You Want


Google announced that daily budgets will now be able to overdeliver up to 100% rather than 20% as had been the case since the earliest days of AdWords.

Overdelivery allows Google to help advertisers meet monthly budget targets by making up for slow traffic days by spending more money on high volume days. They assume that advertisers will divide their monthly budget by 30.4, and set this as the daily budget. Then when there may not be a whole lot of traffic on Saturdays and Sundays, Google can exceed the daily budget on Mondays and Tuesdays when there might be more people looking for what the advertiser sells.

Here's Google's announcement:


To truly control budgets they way you need, you'll probably want to use tools, automations, and AdWords Scripts. Here's a very basic script that lets you enforce a more strict overdelivery for a campaign. The script assumes that your daily budgets are the baseline of what you'd like to deliver. Use the setting 'allowedOverdeliveryPercentage' to control a maximum spend for the day by setting a value between 0% and 100%. The script fetches every active campaign's daily budget and accrued cost for the day. If the cost exceeds the daily budget + the allowed percentage of overdelivery, it will label that campaign and pause it.

Important Notes:
  • Remember to use another automation to re-enable all paused campaigns during the first hour of every day. You can look for campaigns that have the label set by the script and re-enable those. 
  • The script can be run once per hour so you may still exceed the total cost until the next time the script runs. 
  • The script doesn't deal with shared budgets.
  • The script doesn't deal with shopping and video campaigns. Making it work for those is really easy, you just have to update the campaigns call to use the video and shopping methods for getting campaigns.
If you need more control over budgets, and you don't want to do any coding, consider our prebuilt scripts available as part of an Optmyzr subscription (Optmyzr is my company).



/******************************************
*
* Version 1.0 
* Created By: Frederick Vallaeys
* FreeAdWordsScripts.com
******************************************/
function main() {
  
  var allowedOverdeliveryPercentage = 0.2; // set percentage as decimal, i.e. 20% should be set as 0.2
  var labelName = "paused by overdelivery checker script";
  
  AdWordsApp.createLabel(labelName, "automatic label needed to reenable campaigns");
  
  var campaigns = AdWordsApp.campaigns()
   .withCondition("Status = ENABLED")
   .withCondition("Cost > 0")
   .forDateRange("TODAY");
  
  var campaignIterator = campaigns.get();
  
  while (campaignIterator.hasNext()) {
    var campaign = campaignIterator.next();
    var campaignName = campaign.getName();
    var budgetAmount = campaign.getBudget().getAmount();
    var costToday = campaign.getStatsFor("TODAY").getCost();
    
    if(costToday > budgetAmount * (1 + allowedOverdeliveryPercentage)) {
      Logger.log(campaignName + " has spent " + costToday + " which is more than allowed.");
      campaign.applyLabel(labelName);
      campaign.pause();
    } else {
      Logger.log(campaignName + " has spent " + costToday + " and can continue to run.");
    }
  }

}

Sunday, October 1, 2017

How to Keep AdWords Scripts Running When the AdWords API Changes



The AdWords API is regularly updated by Google with their latest capabilities. While it's great not to have to wait too long to get access to new capabilities, it comes with a downside too: AdWords Scripts may stop working on the day Scripts switch to using a newer version of the API.

The reason is that new API version may rename or remove metrics and attributes. An AdWords Script that is not updated with these latest names will stop working. 

You can find the release dates of new API versions here and the table looks like this:


The release date is not always the date that AdWords Scripts start to use the newer version. As a result, it is very tricky to ensure that scripts you write continue to work. 

Luckily there is a solution and it's as simple as telling your script which API version it should use by including the optional apiVersion argument. 

A reporting call without the API version: 
var report2 = AdWordsApp.report(
     'SELECT AdGroupId, Id, KeywordText, Impressions, Clicks ' +
     'FROM   KEYWORDS_PERFORMANCE_REPORT ' +
     'DURING 20130101,20130301');
And that same call with the API version:
var report2 = AdWordsApp.report(
     'SELECT AdGroupId, Id, KeywordText, Impressions, Clicks ' +
     'FROM   KEYWORDS_PERFORMANCE_REPORT ' +
     'DURING 20130101,20130301', {
       apiVersion: 'v201605'
     });

By telling the script which API version to use, you guarantee that it will continue to work on the day that Google switches the default the a new version because you now control the switch that tells the script when your code has been updated and should start using a new API version.

You'll still need to do the migration at some point, but you'll have several months to do so. The sunset date in the table above indicates the final day that a script can use a particular API version. After that date, the old version will cease to work.

Note that you do NOT have to go through every API version. It's completely acceptable to skip a version if you don't need any of its capabilities. For example, say you were using v201609. Since it doesn't sunset until October 2, 2017, you could have waited for the release of v201708 on August 9, 2017, and skipped the 2 API versions in between.

The scripts in the Optmyzr Enhanced Scripts library handle all of these API transitions automatically for our users so if you'd rather not deal with API versions, it's a great solution to try. (Optmyzr is my employer)

Wednesday, August 16, 2017

Automatically add AdWords Data to a Google Slide

Have you ever had to give a presentation about the performance of an AdWords account and spent a lot of time copy-and-pasting data from AdWords into your slides? If so, now you can automatically push data from AdWords into Google Slides.



This script leverages the recently announced integration of AdWords Scripts with the Google Slides API. Because this is one of the advanced APIs, the code is a bit more complicated and you will have to enable the Google Slides API from the script through an additional authorization step.

The code below appends a new slide to your Google Slide deck and adds some basic AdWords metrics. You can modify this code to add exactly the data from AdWords you want.


/* 
// AdWords Script: Add a Slide with AdWords Data
// --------------------------------------------------------------
// Copyright 2017 Optmyzr Inc., All Rights Reserved
// 
// This script takes a Google Presentation as input and appends a slide with basic AdWords metrics.
// Use this to automate creating an appendix of AdWords data to existing PPC report slides.
// The AW data we append is basic but can easily be tweaked to your own needs.
//
// For more PPC management tools and reports, visit www.optmyzr.com
//
*/

// Update this line with the presentation you want to edit. 
// E.g. this is for presentation https://docs.google.com/presentation/d/1RxIzTJC6Jwwd3H5aaRjA-zj3d5IhcG9uOTuOfwk8PUg/edit#slide=id.optmyzr_slide_a1f911e6-9538-427d-9e2f-12fdc951f752
var PRESENTATION_ID = "1RxIzTJC6Jwwd3H5aaRjA-zj3d5IhcG9uOTuOfwk8PUg"

function main() {
  
  var pageId = createSlide(PRESENTATION_ID);
  
  // Get the page element IDs for a basic TITLE_AND_BODY layout
  var baseElementId = readPageElementIds(PRESENTATION_ID, pageId);
  var titleId = baseElementId + "_0";
  var textId = baseElementId + "_1";
  
  // Edit the following with the text for the slide's title
  var titleText = "Automatically Fetched AdWords Data";
  updateElement(PRESENTATION_ID, titleId, titleText);
  
  // The next line gets text for the body section
  var dataForSlide = getLastMonthData();
  updateElement(PRESENTATION_ID, textId, dataForSlide);
  
  Logger.log("Done updating slides at https://docs.google.com/presentation/d/" + PRESENTATION_ID);
  
}

function getLastMonthData() {
  var currentAccount = AdWordsApp.currentAccount();
  //Logger.log('Customer ID: ' + currentAccount.getCustomerId() +
  //    ', Currency Code: ' + currentAccount.getCurrencyCode() +
  //    ', Timezone: ' + currentAccount.getTimeZone());
  var stats = currentAccount.getStatsFor('LAST_MONTH');
  var clicks = stats.getClicks();
  var impressions = stats.getImpressions();
  var text = clicks + " clicks from " + impressions + " impressions.";
  return(text);
}

function createSlide(presentationId) {
  // You can specify the ID to use for the slide, as long as it's unique.
  var pageId = Utilities.getUuid();

  var requests = [{
    "createSlide": {
      "objectId": pageId,
      //"insertionIndex": 1,
      "slideLayoutReference": {
        "predefinedLayout": "TITLE_AND_BODY"
      }
    }
  }];
  var slide =
      Slides.Presentations.batchUpdate({'requests': requests}, presentationId);
  //Logger.log(slide);
  //Logger.log("Created Slide with ID: " + slide.replies[0].createSlide.objectId);
  
  return (pageId);
}

function updateElement(presentationId, elementId, textToAdd) {
  
  var requests = [{
      "insertText": {
        "objectId": elementId,
        "text": textToAdd,
      }
    }];
  var result =
      Slides.Presentations.batchUpdate({'requests': requests}, presentationId);
  //Logger.log(result);
}

function readPageElementIds(presentationId, pageId) {
  // You can use a field mask to limit the data the API retrieves
  // in a get request, or what fields are updated in an batchUpdate.
  var response = Slides.Presentations.Pages.get(
      presentationId, pageId, {"fields": "pageElements.objectId"});
  //Logger.log(response);
 var objectIds = response.pageElements[0].objectId;
  var parts = objectIds.split("_");
  var objectIdBase = parts[0] + "_" + parts[1];
  //Logger.log("objectIdBase: " + objectIdBase);
  return(objectIdBase);
}

We maintain the most current version of this code on GitHub.

For a fully automated way to create PPC reports with interesting visualizations like Quality Score, a word cloud, a cause chart, or a heatmap, take a look at Optmyzr, my company.

Thanks,
Fred

Thursday, June 8, 2017

Pull Stock Quotes Into AdWords Scripts Using Yahoo! Finance API

I was recently asked on Twitter if I had ever seen a script that used stock market performance to adjust bids. Honestly I never have, but I have been asked about this ability multiple times. So I thought I'd build something to do just that.

Finding a reliable and free API for stock data is a little difficult, but everyone seems to point to a somewhat hidden Yahoo! Finance API. Despite the fact that there are multiple libraries built around it, I couldn't find much in the way of documentation other than a StackOverflow post that talks about it. So long story short, this API could stop working at anytime, so use at your own risk.

Here is some sample code to get you started using this. The code below simply looks up a few quotes (one from Bitcoin) and loads them into a Google Spreadsheet of your choosing. Pretty straightforward. The one confusing thing is the "f=" parameter that you need to pass to the API. It is documented a little bit in this blog post but is still pretty confusing. It is a string of one or two character codes that is used to define the columns you want to return. For most people, the symbol, name, and current price should be enough. Feel free to customize it as needed.

Thanks,
Russ
/******************************************
* Yahoo Finance API Class Example
* Version 1.0 
* Created By: Russ Savage
* FreeAdWordsScripts.com
******************************************/
function main() {
  var sheetUrl = 'ENTER A GOOGLE SHEET URL HERE';
  
  var yfa = new YahooFinanceAPI({
    symbols: ['^GSPC','VTI','^IXIC','BTCUSD=X'],
    f: 'snl1' // or something longer like this 'sl1abb2b3d1t1c1ohgv'
  });
  for(var key in yfa.results) {
    Logger.log(Utilities.formatString('Name: "%s", Symbol: "%s", Last Trade Price: $%s', 
                                      yfa.results[key].name,
                                      key,
                                      yfa.results[key].last_trade_price_only));
  }
  
  var includeColumnHeaders = true;
  var sheetData = yfa.toGoogleSheet(includeColumnHeaders);
  var ss = SpreadsheetApp.openByUrl(sheetUrl).getActiveSheet();
  for(var i in sheetData) {
    ss.appendRow(sheetData[i]);
  }
}

Just copy the follow code into the bottom of your AdWords script and you should be good to go.
/******************************************
* Yahoo Finance API Class
* Use this to pull stock market quotes from Yahoo Finance
* Version 1.0 
* Created By: Russ Savage
* FreeAdWordsScripts.com
******************************************/
function YahooFinanceAPI(configVars) {
  var QUERY_URL_BASE = 'https://query.yahooapis.com/v1/public/yql';
  var FINANCE_URL_BASE = 'http://download.finance.yahoo.com/d/quotes.csv';
  this.configVars = configVars;
  
  /*************
   * The results are stored here in a 
   * map where the key is the ticker symbol
   * { 'AAPL' : { ... }, 'GOOG' : { ... }
   *************/
  this.results = {};
  
  /************
   * Function used to refresh the results
   * from Yahoo! Finance API. Called automatically
   * during object reaction.
   ************/
  this.refresh = function() {
    var queryUrl = getQueryUrl(this.configVars);
    var resp = UrlFetchApp.fetch(queryUrl,{muteHttpExceptions:true});
    if(resp.getResponseCode() == 200) {
      var jsonResp = JSON.parse(resp.getContentText());
      if(jsonResp.query.count == 1) {
        var row = jsonResp.query.results.row;
        this.results[row.symbol] = row;
      } else if(jsonResp.query.count > 1) {
        for(var i in jsonResp.query.results.row) {
          var row = jsonResp.query.results.row[i];
          this.results[row.symbol] = row;
        }
      }
    } else {
      throw resp.getContentText();
    }
  }
  
  /************
   * Translates the results into a 2d array
   * to make it easier to add into a Google Sheet.
   * includeColumnHeaders - true or false if you want
   *   headers returned in the results.
   ************/
  this.toGoogleSheet = function(includeColumnHeaders) {
    if(!this.results) { return [[]]; }
    var retVal = [];
    var headers = null;
    for(var key in this.results) {
      if(!headers) {
        headers = Object.keys(this.results[key]).sort();
      }
      var row = [];
      for(var i in headers) {
        row.push(this.results[key][headers[i]]);
      }
      retVal.push(row);
    }
    if(includeColumnHeaders) {
      return [headers].concat(retVal);
    } else {
      return retVal;
    }
  }
  
  // Perform a refresh on object creation.
  this.refresh();
  
  // Private functions
  
  /************
   * Builds Yahoo Finance Url
   ************/
  function getFinanceUrl(configVars) {
    var financeUrlParams = {
      s : configVars.symbols.join(','),
      f : configVars.f,
      e : '.json'
    }
    return FINANCE_URL_BASE + serialize(financeUrlParams);
  }
  
  /************
   * Builds Yahoo Query Url
   ************/
  function getQueryUrl(configVars) {
    var financeUrl = getFinanceUrl(configVars);
    var cols = fToCols(configVars.f);
    var queryTemplate = "select * from csv where url='%s' and columns='%s'";
    var query = Utilities.formatString(queryTemplate, financeUrl,cols.join(','));
    var params = {
      q : query,
      format : 'json'
    }
    var finalRestUrl = QUERY_URL_BASE + serialize(params);
    return finalRestUrl;
  }

  /************
   * This function translates the f parameter
   * into actual field names to use for columns
   ************/
  function fToCols(f) {
    var cols = [];
    var chunk = '';
    var fBits = f.split('').reverse();
    for(var i in fBits) {
      chunk = (fBits[i] + chunk);
      if(fLookup(chunk)) {
        cols.push(fLookup(chunk));
        chunk = '';
      }
    }
    return cols.reverse();
  }
  
  /************
   * Copied from: http://stackoverflow.com/a/18116302
   * This function converts a hash into 
   * a url encoded query string.
   ************/
  function serialize( obj ) {
    return '?'+
      Object.keys(obj).reduce(
        function(a,k) { 
          a.push(k+'='+encodeURIComponent(obj[k]));
          return a
        },
        []).join('&');
  }
  
  /************
   * Adapted from http://www.jarloo.com/yahoo_finance/
   * This function maps f codes into 
   * friendly column names.
   ************/
  function fLookup(f){
    return{
      a:'ask',b:'bid',b2:'ask realtime',b3:'bid realtime',p:'previous close',o:'open',
      y:'dividend yield',d:'dividend per share',r1:'dividend pay date',
      q:'ex-dividend date',c1:'change',c:'change & percent change',c6:'change realtime',
      k2:'change percent realtime',p2:'change in percent',d1:'last trade date',
      d2:'trade date',t1:'last trade time',c8:'after hours change realtime',
      c3:'commission',g:'days low',h:'days high',k1:'last trade realtime with time',
      l:'last trade with time',l1:'last trade price only',t8:'1 yr target price',
      m5:'change from 200 day moving average',m6:'percent change from 200 day moving average',
      m7:'change from 50 day moving average',m8:'percent change from 50 day moving average',
      m3:'50 day moving average',m4:'200 day moving average',w1:'days value change',
      w4:'days value change realtime',p1:'price paid',m:'days range',m2:'days range realtime',
      g1:'holdings gain percent',g3:'annualized gain',g4:'holdings gain',
      g5:'holdings gain percent realtime',g6:'holdings gain realtime',t7:'ticker trend',
      t6:'trade links',i5:'order book realtime',l2:'high limit',l3:'low limit',
      v1:'holdings value',v7:'holdings value realtime',s6: 'revenue',k:'52 week high',
      j:'52 week low',j5:'change from 52 week low',k4:'change from 52 week high',
      j6:'percent change from 52 week low',k5:'percent change from 52 week high',
      w:'52 week range',v:'more info',j1:'market capitalization',j3:'market cap realtime',
      f6:'float shares',n:'name',n4:'notes',s:'symbol',s1:'shares owned',x:'stock exchange',
      j2:'shares outstanding',v:'volume',a5:'ask size',b6:'bid size',k3:'last trade size',
      a2:'average daily volume',e:'earnings per share',e7:'eps estimate current year',
      e8:'eps estimate next year',e9:'eps estimate next quarter',b4:'book value',j4:'ebitda',
      p5:'price sales',p6:'price book',r:'pe ratio',r2:'pe ratio realtime',r5:'peg ratio',
      r6:'price eps estimate current year',r7:'price eps estimate next year',s7:'short ratio'
    }[f];
  }
}