Skip to content

Commit 2c4efb0

Browse files
🐛 Fix keyword matching in bid-review interactive mode
- Fix enum vs string comparison for keyword status (enum_value) - Fix paused keyword/ad group filtering in report data - Increase keyword list limit from 10 to 200 to find all keywords - Improve warning message to show active keyword count
1 parent 2ac1037 commit 2c4efb0

File tree

1 file changed

+180
-0
lines changed

1 file changed

+180
-0
lines changed

asa_api_cli/optimize.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
GranularityType,
2020
KeywordCreate,
2121
KeywordMatchType,
22+
KeywordUpdate,
2223
Money,
2324
NegativeKeywordCreate,
2425
Selector,
@@ -1044,6 +1045,10 @@ def review_keyword_bids(
10441045
str | None,
10451046
typer.Option("--output", "-o", help="Export to CSV file"),
10461047
] = None,
1048+
interactive: Annotated[
1049+
bool,
1050+
typer.Option("--interactive", "-i", help="Interactive mode to adjust bids"),
1051+
] = False,
10471052
) -> None:
10481053
"""Review keyword bids and their performance.
10491054
@@ -1061,6 +1066,7 @@ def review_keyword_bids(
10611066
asa optimize bid-review --country AU --weak # Focus on weak performers
10621067
asa optimize bid-review --days 14 --min-impressions 100
10631068
asa optimize bid-review --output keywords.csv
1069+
asa optimize bid-review --weak --interactive # Interactively increase weak bids
10641070
"""
10651071
client = get_client()
10661072

@@ -1102,6 +1108,14 @@ def review_keyword_bids(
11021108
for row in report.row:
11031109
if not row.metadata.keyword or not row.total:
11041110
continue
1111+
# Skip if no ad_group_id (needed for updates)
1112+
if not row.metadata.ad_group_id:
1113+
continue
1114+
# Skip paused/deleted keywords and ad groups
1115+
if row.metadata.keyword_status and enum_value(row.metadata.keyword_status) != "ACTIVE":
1116+
continue
1117+
if row.metadata.ad_group_status and enum_value(row.metadata.ad_group_status) != "ENABLED":
1118+
continue
11051119

11061120
impressions = row.total.impressions or 0
11071121
taps = row.total.taps or 0
@@ -1270,6 +1284,172 @@ def review_keyword_bids(
12701284
f"\n[yellow]💡 {weak} keywords have weak bid strength - consider increasing bids[/yellow]"
12711285
)
12721286

1287+
# Interactive mode for bid adjustments
1288+
if interactive:
1289+
# Filter to weak/moderate keywords for interactive adjustment
1290+
adjustable = [k for k in keyword_analyses if k.bid_strength in ("WEAK", "MODERATE")]
1291+
1292+
if not adjustable:
1293+
print_info("No weak or moderate keywords to adjust")
1294+
return
1295+
1296+
console.print()
1297+
console.rule("[bold]Interactive Bid Adjustment")
1298+
console.print()
1299+
console.print("[dim]For each keyword, choose an action:[/dim]")
1300+
console.print("[dim] • Enter a number to set new bid (e.g., '2.50')[/dim]")
1301+
console.print("[dim] • +N or +N% to increase (e.g., '+0.50' or '+20%')[/dim]")
1302+
console.print("[dim] • 's' to skip, 'q' to quit[/dim]")
1303+
console.print()
1304+
1305+
changes_made = 0
1306+
for i, kw in enumerate(adjustable, 1):
1307+
# Skip if missing required IDs
1308+
if not kw.ad_group_id or kw.ad_group_id == 0:
1309+
continue
1310+
1311+
console.rule(f"[bold]{i}/{len(adjustable)}")
1312+
console.print()
1313+
1314+
# Show keyword details
1315+
strength_color = "red" if kw.bid_strength == "WEAK" else "yellow"
1316+
console.print(f"[bold]Keyword:[/bold] {kw.keyword_text} [dim]ID: {kw.keyword_id}[/dim]")
1317+
console.print(
1318+
f"[bold]Campaign:[/bold] {kw.campaign_name} ({kw.country}) [dim]ID: {kw.campaign_id}[/dim]"
1319+
)
1320+
console.print(f"[bold]Ad Group:[/bold] {kw.ad_group_name} [dim]ID: {kw.ad_group_id}[/dim]")
1321+
console.print()
1322+
console.print(
1323+
f" Current bid: [{strength_color}]{kw.current_bid:.2f} {kw.currency}[/{strength_color}]"
1324+
)
1325+
console.print(f" Impressions: {kw.impressions:,}")
1326+
ttr_display = f"{kw.ttr * 100:.1f}%" if kw.ttr else "—"
1327+
console.print(f" TTR: {ttr_display}")
1328+
console.print(f" Strength: [{strength_color}]{kw.bid_strength}[/{strength_color}]")
1329+
if kw.avg_cpt:
1330+
console.print(f" Avg CPT: {kw.avg_cpt:.2f} {kw.currency}")
1331+
console.print()
1332+
1333+
# Suggest a new bid (20% increase for weak, 10% for moderate)
1334+
if kw.bid_strength == "WEAK":
1335+
suggested = round(kw.current_bid * Decimal("1.20"), 2)
1336+
else:
1337+
suggested = round(kw.current_bid * Decimal("1.10"), 2)
1338+
1339+
console.print(f"[bold]Suggested:[/bold] {suggested:.2f} {kw.currency}")
1340+
console.print()
1341+
1342+
action = (
1343+
typer.prompt(
1344+
"New bid",
1345+
default=str(suggested),
1346+
show_default=True,
1347+
)
1348+
.strip()
1349+
.lower()
1350+
)
1351+
1352+
if action in ("q", "quit"):
1353+
print_info("Quitting...")
1354+
break
1355+
elif action in ("s", "skip", ""):
1356+
console.print("[dim]Skipped[/dim]")
1357+
console.print()
1358+
continue
1359+
1360+
# Parse the new bid
1361+
try:
1362+
if action.startswith("+"):
1363+
# Relative increase
1364+
if action.endswith("%"):
1365+
pct = Decimal(action[1:-1]) / 100
1366+
new_bid = round(kw.current_bid * (1 + pct), 2)
1367+
else:
1368+
new_bid = kw.current_bid + Decimal(action[1:])
1369+
else:
1370+
new_bid = Decimal(action)
1371+
1372+
if new_bid <= 0:
1373+
print_warning("Invalid bid (must be positive), skipping")
1374+
continue
1375+
1376+
except Exception:
1377+
print_warning(f"Invalid input '{action}', skipping")
1378+
continue
1379+
1380+
# Apply the change
1381+
# Fetch keywords from ad group to find matching keyword by text
1382+
try:
1383+
with spinner("Fetching keywords from ad group..."):
1384+
keywords = list(
1385+
client.campaigns(kw.campaign_id).ad_groups(kw.ad_group_id).keywords.list(limit=200)
1386+
)
1387+
except AppleSearchAdsError as e:
1388+
print_error(
1389+
"Failed to fetch keywords",
1390+
f"{e.message} (campaign={kw.campaign_id}, adgroup={kw.ad_group_id})",
1391+
)
1392+
continue
1393+
1394+
# Find matching ACTIVE keyword by text
1395+
actual_keyword = next(
1396+
(
1397+
k
1398+
for k in keywords
1399+
if k.text.lower() == kw.keyword_text.lower() and enum_value(k.status) == "ACTIVE"
1400+
),
1401+
None,
1402+
)
1403+
if not actual_keyword:
1404+
active_keywords = [k.text for k in keywords if enum_value(k.status) == "ACTIVE"]
1405+
sample = active_keywords[:10]
1406+
ellipsis = "..." if len(active_keywords) > 10 else ""
1407+
print_warning(
1408+
f"Keyword '{kw.keyword_text}' not found in ad group "
1409+
f"(found {len(active_keywords)} active keywords: {sample}{ellipsis})"
1410+
)
1411+
continue
1412+
1413+
try:
1414+
with spinner(f"Updating keyword {actual_keyword.id}..."):
1415+
# Must use bulk endpoint - Apple API doesn't support single keyword PUT
1416+
client.campaigns(kw.campaign_id).ad_groups(kw.ad_group_id).keywords.update_bulk(
1417+
[
1418+
(
1419+
actual_keyword.id,
1420+
KeywordUpdate(
1421+
bid_amount=Money(
1422+
amount=str(new_bid),
1423+
currency=kw.currency,
1424+
)
1425+
),
1426+
)
1427+
]
1428+
)
1429+
except AppleSearchAdsError as e:
1430+
print_error("Update failed", f"{e.message} (keyword_id={actual_keyword.id})")
1431+
continue
1432+
except Exception as e:
1433+
print_error("Update failed", str(e))
1434+
continue
1435+
1436+
print_success(f"Updated: {kw.current_bid:.2f}{new_bid:.2f} {kw.currency}")
1437+
changes_made += 1
1438+
console.print()
1439+
1440+
# Summary
1441+
console.print()
1442+
if changes_made > 0:
1443+
print_result_panel(
1444+
"Bid Review Complete",
1445+
{
1446+
"Keywords reviewed": str(len(adjustable)),
1447+
"Bids updated": str(changes_made),
1448+
},
1449+
)
1450+
else:
1451+
print_info("No changes made")
1452+
12731453
except AppleSearchAdsError as e:
12741454
handle_api_error(e)
12751455
raise typer.Exit(1) from None

0 commit comments

Comments
 (0)