AdWords script: Auto bid to position

This AdWords script automatically calculates optimal positions for keywords and adjusts CPCs accordingly.

This script adjusts CPCs based on the account's current average position and impression shares (budget and position). Whenever an account has lost impression shares lower than 20% the account is considered well balanced and the script makes no changes.

Always run an example before running the script. The log output clearly explains all changes that were made and what the changes are based upon. The "changes" tab shows what changes were actually made. An example log output:

> BBidpos script ran on 2018-01-01 om 11:11 for specified period LAST_30_DAYS
- Current avg. pos search              [broad: 2.7]           [exact: 3.6]
- At least 2 impressions and 0 clicks
- Lost impression share                [position: 86.31%]     [budget 0.09%]
   - Not limited by budget, increase cpcs with max 86.31%
   - auto values                       [broad: 2.7 and 1.7]    [exact: 3.6 and 2.6]
 
Auto values ON
44 BROAD CPCs increased (with 15%)
10 EXACT CPCs increased (with 25%)
10 BROAD CPCs lowered (with 4%)
2 EXACT CPCs lowered (with 4%)

In order to run the script, nothing needs to be changed.

/*
    : increases or lowers CPCs based on specified position range
    : auto values can be used, this is calculated using account impression share
        and current account average position
    : always run an example and check log file before running the script

    note: when calculating impression share all types of campaigns are used
*/

// if set to 'Y', a recommended position is calculated and used in the functions
var USE_RECOMMENDED_VALUES = 'Y'; // Y or N

// ********* Manual settings *********
var MIN_POS_BROAD = 2.4;
var MIN_POS_EXACT = 1.9;
var MAX_POS_BROAD = 1.9;
var MAX_POS_EXACT = 1.4;

var MIN_CPC_BROAD = 0.1;
var MIN_CPC_EXACT = 0.1;
var MAX_CPC_BROAD = 2.0;
var MAX_CPC_EXACT = 3.0;

// e.g. 0.1 = 10% changes €2.00 to €2.20
var BID_ADD_BROAD = 0.08;
var BID_ADD_EXACT = 0.12;
var BID_SUB_BROAD = 0.08;
var BID_SUB_EXACT = 0.12;

// ********* Advanced settings **********
var IMPRESSIONS_TRESHOLD = 2;
var CLICKS_TRESHOLD = 0;
var DRANGE = 'LAST_30_DAYS'

// exclusions, campaign name do not include 'xxx' are not changed
var EXC_CAMPAIGN = 'XXXXX';


// do not alter ********************************************************************************************

// global variables
posBroad = 0;
posExact = 0;
posBroadHi = 0;
posBroadLo = 0;
posExactHi = 0;
posExactLo = 0;
var bidmode = '';

// values for autobids
autoadd = 1.0; // position
percadd = 0.25; // percentage added or subtracted
percaddbroad = 0.15;
percaddlo = 0.04;

function main() {
  
  var dateToday = new Date();
  Logger.log('> BBidpos script ran on ' + Utilities.formatDate(dateToday, 'Europe/Amsterdam', 'yyyy-MM-dd\' om \'HH:mm') + ' for specified period ' + DRANGE);
  
  // finds position and calculates position range
  avgposBE();
  
  if (USE_RECOMMENDED_VALUES != 'Y') {
    Logger.log('- Range                          [broad: ' + MAX_POS_BROAD + ' and ' + MIN_POS_BROAD + ']   [exact: ' + MAX_POS_EXACT + ' and ' + MIN_POS_EXACT + ']');
    if((BID_ADD_BROAD + BID_ADD_EXACT)>(BID_SUB_BROAD + BID_SUB_EXACT)) {
      Logger.log("- Set for higher cpcs");
    } else {
      Logger.log("- Set for lower cpcs");
    };
  }
  
  Logger.log('- At least ' + IMPRESSIONS_TRESHOLD + ' impressions and ' + CLICKS_TRESHOLD + ' clicks');
  
  // finds impression share
  isLimited();
  Logger.log(' ');
  
  // applies changes to keyword CPCs
  doBid();
}

function doBid() {
  
  if (USE_RECOMMENDED_VALUES == 'Y') {
    
    Logger.log('Auto values ON');
    
    if (bidmode == 'cpchi') {
      
      posBidHi(posBroad, MAX_CPC_BROAD, percaddbroad, 'BROAD'); // adds hi cpc to position worse than posBroad (current)
      posBidHi(posExact, MAX_CPC_EXACT, percadd, 'EXACT'); // adds hi cpc to position worse than posExact (current)
      posBidLo(posBroadLo, MIN_CPC_BROAD, percaddlo, 'BROAD'); // lowers lo cpc to position better than posBroad - autoadd
      posBidLo(posExactLo, MIN_CPC_EXACT, percaddlo, 'EXACT'); // lowers lo cpc to position better than posExact - autoadd
      
    } else if (bidmode == 'cpclo') {
      
      posBidHi(posBroadHi, MAX_CPC_BROAD, percaddlo, 'BROAD'); // adds lo cpc to position worse than posBroad + autoadd
      posBidHi(posExactHi, MAX_CPC_EXACT, percaddlo, 'EXACT'); // adds lo cpc to position worse than posExact + autoadd
      posBidLo(posBroad, MIN_CPC_BROAD, percaddbroad, 'BROAD'); // lowers hi cpc to position better than posBroad (current)
      posBidLo(posExact, MIN_CPC_EXACT, percadd, 'EXACT'); // lowers hi cpc to position better than posExact (current)
      
    } else {
      Logger.log('No auto bids - account is well balanced');
    }
    
    
  } else {
    
    // E.g. keyword has pos 3.1 cpc €2 maxcpc €3
    posBidHi(MIN_POS_BROAD, MAX_CPC_BROAD, BID_ADD_BROAD, 'BROAD');
    posBidHi(MIN_POS_EXACT, MAX_CPC_EXACT, BID_ADD_EXACT, 'EXACT');
    // E.g. keywords has pos 1.1 cpc €0.30 mincpc €0,25
    posBidLo(MAX_POS_BROAD, MIN_CPC_BROAD, BID_SUB_BROAD, 'BROAD');
    posBidLo(MAX_POS_EXACT, MIN_CPC_EXACT, BID_SUB_EXACT, 'EXACT');
    
  }
}

function posBidHi(minPos, maxCpc, bidAdj, matchType) { // cpc hi, position improves
  var keyRaise = AdWordsApp.keywords()
  .withCondition('Impressions >= ' + IMPRESSIONS_TRESHOLD)
  .withCondition('Clicks >= ' + CLICKS_TRESHOLD)
  .withCondition('AveragePosition > ' + minPos) //e.g. x > 3
  .withCondition('MaxCpc < ' + maxCpc)
  .withCondition('CampaignName DOES_NOT_CONTAIN_IGNORE_CASE ' + EXC_CAMPAIGN)
  .withCondition('KeywordMatchType = ' + matchType)
  .orderBy('AveragePosition ASC')
  .forDateRange(DRANGE)
  .get();
  
  var changer = 0;
  
  while (keyRaise.hasNext()) {
    var keyword = keyRaise.next();
    keyword.setMaxCpc(keyword.getMaxCpc() * (1 + bidAdj));
    var changer = changer + 1;
  }
  Logger.log(changer + ' ' + matchType + ' CPCs increased (with ' + bidAdj * 100 + '%)');
}

function posBidLo(maxPos, minCpc, bidAdj, matchType) { // 'lower/worse' positions
  var keyLower = AdWordsApp.keywords()
  .withCondition('Impressions >= ' + IMPRESSIONS_TRESHOLD)
  .withCondition('Clicks >= ' + CLICKS_TRESHOLD)
  .withCondition('CampaignName DOES_NOT_CONTAIN_IGNORE_CASE ' + EXC_CAMPAIGN)
  .withCondition('AveragePosition < ' + maxPos) //e.g. x < 1.3
  .withCondition('MaxCpc > ' + minCpc) // e.g. x > €0.10 (niks aan doen als hij lager is)
  .withCondition('KeywordMatchType = ' + matchType)
  .orderBy('AveragePosition ASC')
  .forDateRange(DRANGE)
  .get();
  
  var changer = 0;
  
  while (keyLower.hasNext()) {
    var keyword = keyLower.next();
    keyword.setMaxCpc(keyword.getMaxCpc() * (1 - bidAdj));
    var changer = changer + 1;
  }
  
  Logger.log(changer + ' ' + matchType + ' CPCs lowered (with ' + bidAdj * 100 + '%)');
}

function avgposBE() {
  var BEbroad = AdWordsApp.keywords()
  .withCondition('Impressions > 0')
  .withCondition('CampaignName DOES_NOT_CONTAIN ' + EXC_CAMPAIGN)
  .withCondition('KeywordMatchType = BROAD')
  .orderBy('AveragePosition ASC')
  .forDateRange(DRANGE)
  .get();
  
  BEtotImp = 0;
  BEimpPos = 0;
  
  while (BEbroad.hasNext()) {
    var keyword = BEbroad.next();
    var stats = keyword.getStatsFor(DRANGE);
    var impressions = stats.getImpressions();
    var pos = stats.getAveragePosition();
    BEtotImp = BEtotImp + impressions;
    BEimpPos = BEimpPos + (impressions * pos);
  }
  
  var Ebroad = AdWordsApp.keywords()
  .withCondition('Impressions > 0')
  .withCondition('CampaignName DOES_NOT_CONTAIN ' + EXC_CAMPAIGN)
  .withCondition('KeywordMatchType = EXACT')
  .orderBy('AveragePosition ASC')
  .forDateRange(DRANGE)
  .get();
  
  EtotImp = 0;
  EimpPos = 0;
  
  while (Ebroad.hasNext()) {
    var keyword = Ebroad.next();
    var stats = keyword.getStatsFor(DRANGE);
    var impressions = stats.getImpressions();
    var pos = stats.getAveragePosition();
    EtotImp = EtotImp + impressions;
    EimpPos = EimpPos + (impressions * pos);
  }
  
  posBroad = parseFloat((BEimpPos / BEtotImp).toFixed(1));
  posExact = parseFloat((EimpPos / EtotImp).toFixed(1));
  
  if ((posBroad - autoadd) < (1.1)) {
    posBroadLo = 1.1;
  } else {
    posBroadLo = posBroad - autoadd;
  }
  if ((posExact - autoadd) < (1.1)) {
    posExactLo = 1.1;
  } else {
    posExactLo = posExact - autoadd;
  }
  
  posBroadHi = posBroad + autoadd;
  posExactHi = posExact + autoadd;
  
  Logger.log("- Current avg. pos search      [broad: " + posBroad + "]          [exact: " + posExact + "]");
}

// calculates auto values and puts it in global vars and logs it
function isLimited() {
  
  var limitedInfo = AdWordsApp.report(
    "SELECT SearchBudgetLostImpressionShare, SearchRankLostImpressionShare, SearchImpressionShare, SearchExactMatchImpressionShare " +
    "FROM   ACCOUNT_PERFORMANCE_REPORT " +
    " DURING " + DRANGE);
  
  var rows = limitedInfo.rows(); // runs through AWQL
  while (rows.hasNext()) {
    
    var row = rows.next();
    var budgetimpshare = row['SearchBudgetLostImpressionShare'];
    var bif = parseFloat(budgetimpshare)/100;
    var rankimpshare = row['SearchRankLostImpressionShare'];
    var rif = parseFloat(rankimpshare)/100;
    var impshare = row['SearchImpressionShare'];
    var imf = parseFloat(impshare)/100;
    var exactimpshare = row['SearchExactMatchImpressionShare'];
    var eif = parseFloat(exactimpshare)/100;
    
    Logger.log('- Lost impression share             [position: ' + rankimpshare + ']     [budget ' + budgetimpshare + ']');
    
    if (bif > 0.20) {
      Logger.log('   - Increase budget (' + budgetimpshare + ') or lower cpcs');
      Logger.log('   - auto values            [broad: ' + posBroadHi + ' en ' + posBroad + ']    [exact: ' + posExactHi + ' en ' + posExact + ']');
      bidmode = 'cpclo';
    } else {
      if (rif > 0.20) {
        Logger.log('   - Not limited by budget, increase cpcs with max ' + rankimpshare);
        Logger.log('   - auto values                         [broad: ' + posBroad + ' en ' + posBroadLo + ']    [exact: ' + posExact + ' en ' + posExactLo + ']');
        bidmode = 'cpchi';
      } else {
        Logger.log('   - Account budget and cpcs well balanced');
        bidmode = 'balance';
      }
    }
  }
}