@@ -538,6 +538,10 @@ class CorrelatedSearchTerm:
538538 keyword_text : str | None = None
539539 current_bid : Decimal | None = None
540540 currency : str | None = None
541+ # Performance data for bid strength
542+ impressions : int = 0
543+ taps : int = 0
544+ ttr : float | None = None
541545
542546 @property
543547 def share_range (self ) -> str :
@@ -562,6 +566,18 @@ def is_matched(self) -> bool:
562566 """Whether this search term was matched to a keyword."""
563567 return self .keyword_id is not None
564568
569+ @property
570+ def bid_strength (self ) -> str :
571+ """Estimate bid strength based on impressions and TTR."""
572+ if self .impressions == 0 :
573+ return "UNKNOWN"
574+ ttr = self .ttr or 0.0
575+ if self .impressions >= 1000 and ttr >= 0.05 :
576+ return "STRONG"
577+ elif self .impressions >= 100 and ttr >= 0.02 :
578+ return "MODERATE"
579+ return "WEAK"
580+
565581
566582@dataclass
567583class KeywordInfo :
@@ -575,16 +591,25 @@ class KeywordInfo:
575591 ad_group_name : str
576592 bid_amount : Decimal
577593 currency : str
594+ # Performance data
595+ impressions : int = 0
596+ taps : int = 0
597+ ttr : float | None = None
578598
579599
580600def _build_keyword_index (
581601 client : "AppleSearchAdsClient" ,
582602 country : str ,
603+ start_date : date | None = None ,
604+ end_date : date | None = None ,
583605) -> dict [str , list [KeywordInfo ]]:
584606 """Build an index of keywords by text for a given country.
585607
586608 Returns a dict mapping lowercase keyword text -> list of KeywordInfo
587609 (multiple campaigns may have the same keyword).
610+
611+ If start_date and end_date are provided, also fetches performance data
612+ for bid strength calculation.
588613 """
589614 keyword_index : dict [str , list [KeywordInfo ]] = {}
590615
@@ -596,6 +621,25 @@ def _build_keyword_index(
596621 if country .upper () not in [c .upper () for c in campaign .countries_or_regions ]:
597622 continue
598623
624+ # If date range provided, fetch keyword performance report
625+ keyword_performance : dict [int , tuple [int , int , float | None ]] = {} # keyword_id -> (impr, taps, ttr)
626+ if start_date and end_date :
627+ try :
628+ report = client .reports .keywords (
629+ campaign_id = campaign .id ,
630+ start_date = start_date ,
631+ end_date = end_date ,
632+ granularity = GranularityType .DAILY ,
633+ )
634+ for row in report .row :
635+ if row .metadata .keyword_id and row .total :
636+ impr = row .total .impressions or 0
637+ taps = row .total .taps or 0
638+ ttr = taps / impr if impr > 0 else None
639+ keyword_performance [row .metadata .keyword_id ] = (impr , taps , ttr )
640+ except AppleSearchAdsError :
641+ pass
642+
599643 # Get ad groups for this campaign
600644 try :
601645 ad_groups = list (client .campaigns (campaign .id ).ad_groups .find (Selector ().where ("status" , "==" , "ENABLED" )))
@@ -611,6 +655,10 @@ def _build_keyword_index(
611655
612656 for keyword in keywords :
613657 kw_text = keyword .text .lower ()
658+
659+ # Get performance data if available
660+ perf = keyword_performance .get (keyword .id , (0 , 0 , None ))
661+
614662 info = KeywordInfo (
615663 keyword_id = keyword .id ,
616664 keyword_text = keyword .text ,
@@ -620,6 +668,9 @@ def _build_keyword_index(
620668 ad_group_name = ad_group .name ,
621669 bid_amount = Decimal (keyword .bid_amount .amount ) if keyword .bid_amount else Decimal ("0" ),
622670 currency = keyword .bid_amount .currency if keyword .bid_amount else "USD" ,
671+ impressions = perf [0 ],
672+ taps = perf [1 ],
673+ ttr = perf [2 ],
623674 )
624675
625676 if kw_text not in keyword_index :
@@ -717,13 +768,13 @@ def correlate_impression_share(
717768 countries_in_data = {item .country .upper () for item in aggregated .values () if item .country }
718769 print_info (f"Found data for { len (countries_in_data )} countries: { ', ' .join (sorted (countries_in_data ))} " )
719770
720- # Step 2: Build keyword index for each country
771+ # Step 2: Build keyword index for each country (with performance data)
721772 keyword_indices : dict [str , dict [str , list [KeywordInfo ]]] = {}
722773 total_keywords = 0
723774
724775 for ctry in countries_in_data :
725- with spinner (f"Building keyword index for { ctry } ..." ):
726- keyword_indices [ctry ] = _build_keyword_index (client , ctry )
776+ with spinner (f"Building keyword index for { ctry } (with performance data) ..." ):
777+ keyword_indices [ctry ] = _build_keyword_index (client , ctry , start_date , end_date )
727778 kw_count = sum (len (v ) for v in keyword_indices [ctry ].values ())
728779 total_keywords += kw_count
729780
@@ -757,6 +808,9 @@ def correlate_impression_share(
757808 keyword_text = match .keyword_text ,
758809 current_bid = match .bid_amount ,
759810 currency = match .currency ,
811+ impressions = match .impressions ,
812+ taps = match .taps ,
813+ ttr = match .ttr ,
760814 )
761815 )
762816 else :
@@ -815,6 +869,10 @@ def correlate_impression_share(
815869 "keyword_text" ,
816870 "current_bid" ,
817871 "currency" ,
872+ "impressions" ,
873+ "taps" ,
874+ "ttr" ,
875+ "bid_strength" ,
818876 ]
819877 )
820878 for row in correlated :
@@ -832,6 +890,10 @@ def correlate_impression_share(
832890 row .keyword_text or "" ,
833891 row .current_bid or "" ,
834892 row .currency or "" ,
893+ row .impressions if row .is_matched else "" ,
894+ row .taps if row .is_matched else "" ,
895+ f"{ row .ttr :.4f} " if row .ttr else "" ,
896+ row .bid_strength if row .is_matched else "" ,
835897 ]
836898 )
837899 print_success (f"Exported { len (correlated )} rows to { output } " )
@@ -851,9 +913,9 @@ def correlate_impression_share(
851913 table .add_column ("Search Term" , style = "cyan" , max_width = 30 )
852914 table .add_column ("Ctry" , style = "dim" , width = 4 )
853915 table .add_column ("Share" , justify = "right" , width = 8 )
854- table .add_column ("Campaign" , style = "magenta" , max_width = 25 )
855- table .add_column ("Keyword " , style = "dim " , max_width = 20 )
856- table .add_column ("Bid " , justify = "right " , width = 10 )
916+ table .add_column ("Campaign" , style = "magenta" , max_width = 20 )
917+ table .add_column ("Bid " , justify = "right " , width = 8 )
918+ table .add_column ("Strength " , justify = "center " , width = 8 )
857919 table .add_column ("Pop" , justify = "center" , width = 3 )
858920
859921 for row in correlated [:display_limit ]:
@@ -864,17 +926,30 @@ def correlate_impression_share(
864926 elif row .high_share < 0.5 :
865927 share_style = "yellow"
866928
867- bid_display = f"{ row .current_bid :.2f} { row .currency } " if row .current_bid else "[dim]—[/dim]"
868- campaign_display = row .campaign_name [:25 ] if row .campaign_name else "[dim]Not matched[/dim]"
869- keyword_display = row .keyword_text [:20 ] if row .keyword_text else ""
929+ bid_display = f"{ row .current_bid :.2f} " if row .current_bid else "[dim]—[/dim]"
930+ campaign_display = row .campaign_name [:20 ] if row .campaign_name else "[dim]Not matched[/dim]"
931+
932+ # Bid strength display
933+ if row .is_matched :
934+ strength = row .bid_strength
935+ if strength == "STRONG" :
936+ strength_display = "[green]STRONG[/green]"
937+ elif strength == "MODERATE" :
938+ strength_display = "[yellow]MOD[/yellow]"
939+ elif strength == "WEAK" :
940+ strength_display = "[red]WEAK[/red]"
941+ else :
942+ strength_display = "[dim]—[/dim]"
943+ else :
944+ strength_display = "[dim]—[/dim]"
870945
871946 table .add_row (
872947 row .search_term [:30 ],
873948 row .country ,
874949 f"[{ share_style } ]{ row .share_range } [/{ share_style } ]" ,
875950 campaign_display ,
876- keyword_display ,
877951 bid_display ,
952+ strength_display ,
878953 str (row .search_popularity ) if row .search_popularity else "—" ,
879954 )
880955
@@ -888,6 +963,21 @@ def correlate_impression_share(
888963 console .print (f" [green]Matched to keywords:[/green] { matched_count } " )
889964 console .print (f" [yellow]Unmatched (opportunities):[/yellow] { unmatched_count } " )
890965
966+ # Bid strength summary for matched keywords
967+ matched_items = [c for c in correlated if c .is_matched ]
968+ if matched_items :
969+ strong = sum (1 for c in matched_items if c .bid_strength == "STRONG" )
970+ moderate = sum (1 for c in matched_items if c .bid_strength == "MODERATE" )
971+ weak = sum (1 for c in matched_items if c .bid_strength == "WEAK" )
972+ unknown = sum (1 for c in matched_items if c .bid_strength == "UNKNOWN" )
973+
974+ console .print ("\n [dim]Bid Strength (matched keywords):[/dim]" )
975+ console .print (f" [green]Strong:[/green] { strong } " )
976+ console .print (f" [yellow]Moderate:[/yellow] { moderate } " )
977+ console .print (f" [red]Weak:[/red] { weak } " )
978+ if unknown > 0 :
979+ console .print (f" [dim]No data:[/dim] { unknown } " )
980+
891981 low_share_matched = sum (1 for c in correlated if c .is_matched and c .high_share and c .high_share < 0.3 )
892982 if low_share_matched > 0 :
893983 console .print (
0 commit comments