@@ -21,6 +21,7 @@ import tickerSectors from "../data/ticker-sectors.json";
2121const CAPITOL_TRADES_URL = "https://www.capitoltrades.com/trades" ;
2222const CACHE_TTL_MS = 6 * 60 * 60 * 1000 ; // 6 hours
2323const PAGES_TO_FETCH = 5 ; // ~60 trades instead of ~12
24+ const MIN_SIGNIFICANT_SCORE = 2 ;
2425
2526// ============================================================================
2627// Types
@@ -459,7 +460,7 @@ export const filterTrades = (trades: CongressTrade[]): CongressTrade[] => {
459460 . filter ( ( t ) => t . ticker && t . ticker !== "N/A" )
460461 . filter ( ( t ) => ! excludedTickerSet . has ( t . ticker ) )
461462 . filter ( ( t ) => t . amountLower >= 100_000 )
462- . filter ( ( t ) => t . score >= 2 )
463+ . filter ( ( t ) => t . score >= MIN_SIGNIFICANT_SCORE )
463464 . sort ( ( a , b ) => b . score - a . score ) ;
464465} ;
465466
@@ -602,22 +603,36 @@ export const formatDeduplicatedItem = (
602603// Data Source
603604// ============================================================================
604605
606+ /**
607+ * Fetch a single page from Capitol Trades. Throws on any non-OK response,
608+ * so the caller (backOff) can retry correctly on retriable HTTP statuses.
609+ */
605610const fetchCapitolTradesPage = async ( page : number ) : Promise < string > => {
606611 const url =
607612 page === 1 ? CAPITOL_TRADES_URL : `${ CAPITOL_TRADES_URL } ?page=${ page } ` ;
608613
609- const response = await backOff (
610- ( ) =>
611- fetch ( url , {
614+ return backOff (
615+ async ( ) => {
616+ const response = await fetch ( url , {
612617 headers : {
613618 "User-Agent" :
614619 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" ,
615620 Accept :
616621 "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" ,
617622 "Accept-Language" : "en-US,en;q=0.9" ,
618623 } ,
619- signal : AbortSignal . timeout ( 30_000 ) ,
620- } ) ,
624+ signal : AbortSignal . timeout ( 15_000 ) ,
625+ } ) ;
626+
627+ // Throw inside the backOff callback so retriable statuses (429, 5xx) are retried.
628+ if ( ! response . ok ) {
629+ throw new Error (
630+ `Capitol Trades page ${ page } returned ${ response . status } ` ,
631+ ) ;
632+ }
633+
634+ return response . text ( ) ;
635+ } ,
621636 {
622637 numOfAttempts : 3 ,
623638 startingDelay : 1000 ,
@@ -632,71 +647,64 @@ const fetchCapitolTradesPage = async (page: number): Promise<string> => {
632647 } ,
633648 } ,
634649 ) ;
635-
636- if ( ! response . ok ) {
637- throw new Error ( `Capitol Trades page ${ page } returned ${ response . status } ` ) ;
638- }
639-
640- return response . text ( ) ;
641650} ;
642651
643- const fetchCapitolTradesHTML = async ( ) : Promise < string > => {
644- // Fetch multiple pages sequentially to avoid rate limiting
652+ /**
653+ * Fetch PAGES_TO_FETCH pages from Capitol Trades and return them as an array
654+ * of HTML strings. Throws if page 1 fails; throws if any later page fails (no
655+ * partial caching).
656+ */
657+ const fetchCapitolTradesPages = async ( ) : Promise < string [ ] > => {
645658 const pages : string [ ] = [ ] ;
646659 for ( let i = 1 ; i <= PAGES_TO_FETCH ; i ++ ) {
647- try {
648- const html = await fetchCapitolTradesPage ( i ) ;
649- pages . push ( html ) ;
650- // Small delay between pages to be polite
651- if ( i < PAGES_TO_FETCH ) {
652- await new Promise ( ( resolve ) => setTimeout ( resolve , 500 ) ) ;
653- }
654- } catch ( error ) {
655- const message = error instanceof Error ? error . message : String ( error ) ;
656- console . warn (
657- `[congress-trades] Failed to fetch page ${ i } , stopping pagination: ${ message } ` ,
658- ) ;
659- break ;
660+ const html = await fetchCapitolTradesPage ( i ) ;
661+ pages . push ( html ) ;
662+ // Small delay between pages to avoid hammering the server
663+ if ( i < PAGES_TO_FETCH ) {
664+ await new Promise ( ( resolve ) => setTimeout ( resolve , 500 ) ) ;
660665 }
661666 }
662- if ( pages . length === 0 ) {
663- throw new Error ( "Failed to fetch any pages from Capitol Trades" ) ;
664- }
665667 console . log ( `[congress-trades] Fetched ${ pages . length } pages` ) ;
666- // Return pages joined — parseCapitolTradesHTML will parse each independently
667- return pages . join ( "\n<!-- PAGE_BREAK -->\n" ) ;
668+ return pages ;
669+ } ;
670+
671+ /**
672+ * Merge parsed trades from multiple HTML pages, deduplicating by trade URL.
673+ * Trades without a URL are always included (Capitol Trades always has URLs,
674+ * but defensive against layout changes).
675+ */
676+ export const mergePageTrades = ( htmlPages : string [ ] ) : CongressTrade [ ] => {
677+ const seenUrls = new Set < string > ( ) ;
678+ const trades : CongressTrade [ ] = [ ] ;
679+ for ( const html of htmlPages ) {
680+ for ( const trade of parseCapitolTradesHTML ( html ) ) {
681+ if ( ! trade . url || ! seenUrls . has ( trade . url ) ) {
682+ if ( trade . url ) seenUrls . add ( trade . url ) ;
683+ trades . push ( trade ) ;
684+ }
685+ }
686+ }
687+ return trades ;
668688} ;
669689
670690export const congressTradesSource : DataSource = {
671691 name : "Congress Trades" ,
672692 priority : 6 ,
673- timeoutMs : 90_000 , // 5 pages × ~15s each worst case
693+ // Budget: 5 pages × 3 attempts × 15s + backoff + 500ms delays ≈ 3.5 min worst-case
694+ timeoutMs : 210_000 ,
674695
675696 fetch : async ( date : Date ) : Promise < BriefingSection > => {
676697 const dateKey = date . toISOString ( ) . split ( "T" ) [ 0 ] ;
677698 const cacheKey = `congress-trades-${ dateKey } ` ;
678699
679700 try {
680- const combinedHtml = await withCache ( cacheKey , fetchCapitolTradesHTML , {
701+ const htmlPages = await withCache ( cacheKey , fetchCapitolTradesPages , {
681702 ttlMs : CACHE_TTL_MS ,
682703 } ) ;
683704
684- // Parse each page separately and merge, deduplicating by trade URL
685- const pages = combinedHtml . split ( "\n<!-- PAGE_BREAK -->\n" ) ;
686- const seenUrls = new Set < string > ( ) ;
687- const allTrades : CongressTrade [ ] = [ ] ;
688- for ( const html of pages ) {
689- const pageTrades = parseCapitolTradesHTML ( html ) ;
690- for ( const trade of pageTrades ) {
691- if ( ! trade . url || ! seenUrls . has ( trade . url ) ) {
692- if ( trade . url ) seenUrls . add ( trade . url ) ;
693- allTrades . push ( trade ) ;
694- }
695- }
696- }
697-
705+ const allTrades = mergePageTrades ( htmlPages ) ;
698706 console . log (
699- `[congress-trades] Parsed ${ allTrades . length } trades across ${ pages . length } pages, filtering...` ,
707+ `[congress-trades] Parsed ${ allTrades . length } trades across ${ htmlPages . length } pages, filtering...` ,
700708 ) ;
701709
702710 const filtered = filterTrades ( allTrades ) ;
@@ -709,7 +717,7 @@ export const congressTradesSource: DataSource = {
709717 items : [
710718 {
711719 text : "No significant trades recently" ,
712- detail : `${ allTrades . length } trades checked across ${ pages . length } pages, none passed filters` ,
720+ detail : `${ allTrades . length } trades checked across ${ htmlPages . length } pages, none passed filters` ,
713721 } ,
714722 ] ,
715723 } ;
0 commit comments