If you think bidadjustments for every hour of each day is too much, this script offers the solution. Like the bidadjustment scripts on devices, locations and audiences, it automatically sets bidadjustments for all blocks in a campaign's adschedule. You can arrange this adschedule per campaign as desired.
With the latest adjustments in this script, it is possible to calculate the bid adjustments based on CPA or ROAS. The CPA or ROAS of a location is compared to the CPA or ROAS of the corresponding campaign.
Settings
LOG: Specify whether the script should report the intermediate steps by adjusting the value to 'true'.
DATE_RANGE: The script looks at the statistics over this period.
MINIMUM_CONVERSIONS: Set a minimum number of conversions for an ad schedule. If a location has less conversions, we won't set a bidadjustment.
MINIMUM_CONVERSIONVALUE: Set a minimum conversionvalue for an ad schedule. If an ad schedule has less revenue, we won't set a bidadjustment.
MINIMUM_COST: Set a minimum amount of cost for an ad schedule. If an ad schedule has spend less, we won't set a bidadjustment.
MINIMUM_CLICKS: Set a minimum number of clicks for an ad schedule. If an ad schedule has less clicks, we won't set a bidadjustment.
KPI: Calculate the bidadjustment based on 'CPA' or 'ROAS'.
CAMPAIGN_LABEL: Select campaigns by using a label.
MAX_BID: The bidadjustment will not be higher than this. 1.3 = +30% or 2 = +100%.
MIN_BID: The bidadjustment will not be lower than this. 0.75 = -25% or 0.5 = -50%.
The script
// Copyright 2021. Increase BV. All Rights Reserved.
//
// Created By: Tibbe van Asten
// for Increase B.V.
//
// Created: 19-03-2020
// Last update: 09-11-2021
//
// ABOUT THE SCRIPT
// With this script we adjust the biddings for target adschedule in
// active campaigns.
//
////////////////////////////////////////////////////////////////////
var config = {
LOG : true,
DATE_RANGE : "LAST_30_DAYS",
// Optional: Use minimum conversions and/or cost to select target adschedules
// Leave empty to skip
MINIMUM_CONVERSIONS : "",
MINIMUM_CONVERSIONVALUE : "",
MINIMUM_COST : "",
MINIMUM_CLICKS : "",
// Set bidadjustments based on CPA or ROAS
KPI : "CPA",
// Optional: Use a campaignlabel to make a selection.
// Leave empty to skip
CAMPAIGN_LABEL : "",
// Bidadjustments won't be higher then MAX_BID and not lower then MIN_BID
// Examples: 1.2 = +20%, 0.8 = -20%, 2 = +100%
MAX_BID : 1.2,
MIN_BID : 0.8
}
////////////////////////////////////////////////////////////////////
function main() {
if(config.KPI != "CPA" && config.KPI != "ROAS"){
throw Error("Set KPI to 'CPA' or 'ROAS' only!");
}
var map = {};
// Collecting all target ad schedule data
var report = AdsApp.report(
"SELECT Id, CampaignName, CampaignId, BidModifier, Conversions, ConversionValue, Cost, Clicks " +
"FROM CAMPAIGN_AD_SCHEDULE_TARGET_REPORT " +
"WHERE CampaignStatus = ENABLED " +
" DURING " + config.DATE_RANGE
);
var rows = report.rows();
while(rows.hasNext()){
var row = rows.next();
// Check thresholds
if(config.MINIMUM_CONVERSIONS != "" || config.MINIMUM_CONVERSIONVALUE != "" ||config.MINIMUM_COST != "" || config.MINIMUM_CLICKS != ""){
if(row["Conversions"] < config.MINIMUM_CONVERSIONS || row["ConversionValue"] < config.MINIMUM_CONVERSIONVALUE || row["Cost"] < config.MINIMUM_COST || row["Clicks"] < config.MINIMUM_CLICKS) continue;
}
// Skip campaign when not set to manual bidding. And check label.
var campaignIterator = AdsApp.campaigns().withIds([row["CampaignId"]]).get();
while(campaignIterator.hasNext()){
var campaign = campaignIterator.next();
if(campaign.bidding().getStrategyType() != "MANUAL_CPC" && campaign.bidding().getStrategyType() != "MANUAL_CPM" && campaign.bidding().getStrategyType() != "MANUAL_CPV") continue;
if(config.CAMPAIGN_LABEL != ""){ while(!campaign.labels().withCondition("Name = '" + config.CAMPAIGN_LABEL + "'").get().hasNext()) continue; }
var campaignKpi = parseFloat(getCampaignKpi(campaign.getId())).toFixed(2);
var oldBid = parseFloat(1 + (row["BidModifier"].split("%")[0] / 100)).toFixed(2);
// Calculate KPI's and new bid
if(config.KPI == "CPA"){ var scheduleKpi = parseFloat(row["Cost"] / row["Conversions"]).toFixed(2); var newBid = parseFloat(campaignKpi / scheduleKpi).toFixed(2); }
if(config.KPI == "ROAS"){ var scheduleKpi = parseFloat(row["ConversionValue"] / row["Cost"]).toFixed(2); var newBid = parseFloat(scheduleKpi / campaignKpi).toFixed(2); }
// If there is a CPA or ROAS, we will set a bidadjustment
if(isNaN(newBid) === false && isFinite(scheduleKpi) === true) {
if(newBid < config.MIN_BID) { newBid = parseFloat(config.MIN_BID).toFixed(2); }
if(newBid > config.MAX_BID) { newBid = parseFloat(config.MAX_BID).toFixed(2); }
// Add to map
if(newBid != oldBid) {
if(map[row["Id"] + "," + row["CampaignId"]] == null) {
map[row["Id"] + "," + row["CampaignId"]] = [];
}
map[row["Id"] + "," + row["CampaignId"]].push([row["Id"], row["CampaignName"], newBid]);
if(config.LOG === true){
Logger.log("Ad Schedule " + row["Id"] + " in " + row["CampaignName"]);
Logger.log("Current bidadjustment: " + row["BidModifier"] + " = " + oldBid);
Logger.log("Ad Schedule " + config.KPI + ": " + scheduleKpi + ", Campaign " + config.KPI + ": " + campaignKpi + ", New bid: " + newBid);
Logger.log("------------");
}
} // Add to map
} // Check bidadjustment
} // campaignIterator
} // rowIterator
if(config.LOG === true){
Logger.log(" ");
Logger.log("...Loop through ad schedules");
Logger.log(" ");
}
for (var key in map) {
var campaignIterator = AdsApp
.campaigns()
.withIds([key.split(",")[1]])
.get();
while(campaignIterator.hasNext()){
var campaign = campaignIterator.next();
var ids = [];
ids.push(key.split(",")[1]);
ids.push(key.split(",")[0]);
var schedulesIterator = campaign
.targeting()
.adSchedules()
.withIds([ids])
.get();
while(schedulesIterator.hasNext()){
var schedule = schedulesIterator.next();
schedule.setBidModifier(parseFloat(map[key][0][2]));
Logger.log("Bidadjustment of " + map[key][0][2] + " set for " + map[key][0][0] + " in " + map[key][0][1]);
} // locationIterator
} // campaignIterator
} // keyIterator
} // function main()
////////////////////////////////////////////////////////////////////
function getCampaignKpi(campaignId){
var report = AdsApp.report(
"SELECT Cost, Conversions, ConversionValue " +
"FROM CAMPAIGN_PERFORMANCE_REPORT " +
"WHERE CampaignId = '" + campaignId + "' " +
"DURING " + config.DATE_RANGE
);
var rows = report.rows();
while(rows.hasNext()){
var campaign = rows.next();
if(config.KPI == "CPA"){ var campaignKpi = campaign["Cost"] / campaign["Conversions"]; }
if(config.KPI == "ROAS"){ var campaignKpi = campaign["ConversionValue"] / campaign["Cost"]; }
} // campaignIterator
return campaignKpi;
} // function getCampaignKpi()
Show whole script!
Loading Comments
The Experts
Tibbe van AstenTeam Lead Performance Marketing
Nils RooijmansWater Cooler Topics
Martijn KraanFreelance PPC Specialist
Bas BaudoinTeamlead SEA @ Happy Leads
Jermaya LeijenDigital Marketing Strategist
Krzysztof BycinaPPC Specialist from Poland
How about you?JOIN US!
Caring
Adsscripts.com is all about sharing knowledge. In the current market, PPC specialists like to keep their knowledge and experience to themselves. We're convinced that sharing knowledge can ensure that everyone gets better at their work. We want to change this by sharing our knowledge about scripts with everyone.
Do you also want to contribute? We are open to new ideas and feedback on everything you find on Adsscripts.com.