@@ -581,3 +581,135 @@ def convergent_search_sprawl(
581581 return f"No results found for account '{ account_id } '"
582582
583583 return json .dumps (all_results , default = str )
584+
585+
586+ # ---------------------------------------------------------------------------
587+ # suggest_portfolio_hedge (PostgreSQL sprawl mode)
588+ # ---------------------------------------------------------------------------
589+
590+
591+ def suggest_portfolio_hedge_sprawl (pg_conn , args , query_logger ):
592+ """Hedge recommendation tool for sprawl/PostgreSQL mode.
593+
594+ Mirrors _suggest_portfolio_hedge in tools.py but uses PostgreSQL syntax
595+ (%(param)s placeholders, JSONB operators, window functions).
596+ """
597+ import json as _json
598+
599+ account_id = args .get ("account_id" , "" )
600+ risk_focus = args .get ("risk_focus" , "all" )
601+
602+ sql = """
603+ WITH holdings AS (
604+ SELECT
605+ ph.holding_id,
606+ ph.asset_class,
607+ ph.instrument_name,
608+ ph.ticker,
609+ ph.sector,
610+ ph.region,
611+ ph.risk_rating,
612+ ph.current_value,
613+ SUM(ph.current_value) OVER () AS total_value,
614+ ROUND(
615+ ph.current_value / NULLIF(SUM(ph.current_value) OVER (), 0) * 100, 2
616+ ) AS pct_of_portfolio,
617+ ca.risk_profile,
618+ ca.metadata->>'esg_mandate' AS esg_mandate,
619+ ca.metadata->>'max_single_position' AS max_position,
620+ ca.metadata->'excluded_sectors' AS excluded_sectors
621+ FROM portfolio_holdings ph
622+ JOIN client_accounts ca ON ca.account_id = ph.account_id
623+ WHERE ph.account_id = %(account_id)s
624+ ),
625+ sector_exposure AS (
626+ SELECT sector,
627+ ROUND(SUM(pct_of_portfolio)::numeric, 2) AS sector_pct
628+ FROM holdings
629+ GROUP BY sector
630+ ORDER BY sector_pct DESC
631+ ),
632+ region_exposure AS (
633+ SELECT region,
634+ ROUND(SUM(pct_of_portfolio)::numeric, 2) AS region_pct
635+ FROM holdings
636+ GROUP BY region
637+ ORDER BY region_pct DESC
638+ ),
639+ asset_class_exposure AS (
640+ SELECT asset_class,
641+ ROUND(SUM(pct_of_portfolio)::numeric, 2) AS asset_class_pct
642+ FROM holdings
643+ GROUP BY asset_class
644+ ORDER BY asset_class_pct DESC
645+ ),
646+ high_risk_positions AS (
647+ SELECT holding_id, instrument_name, ticker, sector, region,
648+ risk_rating, pct_of_portfolio
649+ FROM holdings
650+ WHERE risk_rating >= 7
651+ ORDER BY pct_of_portfolio DESC
652+ )
653+ SELECT 'HOLDING' AS row_type,
654+ h.holding_id AS id,
655+ h.instrument_name AS label,
656+ h.ticker AS ticker,
657+ h.sector AS sector,
658+ h.region AS region,
659+ h.asset_class AS asset_class,
660+ h.risk_rating AS risk_rating,
661+ h.pct_of_portfolio AS pct,
662+ h.risk_profile AS risk_profile,
663+ h.esg_mandate AS esg_mandate,
664+ h.max_position AS max_position,
665+ h.excluded_sectors::text AS excluded_sectors
666+ FROM holdings h
667+ UNION ALL
668+ SELECT 'SECTOR', sector, sector, NULL, NULL, NULL, NULL,
669+ NULL, sector_pct, NULL, NULL, NULL, NULL
670+ FROM sector_exposure
671+ UNION ALL
672+ SELECT 'REGION', region, region, NULL, NULL, NULL, NULL,
673+ NULL, region_pct, NULL, NULL, NULL, NULL
674+ FROM region_exposure
675+ UNION ALL
676+ SELECT 'ASSET_CLASS', asset_class, asset_class, NULL, NULL, NULL, NULL,
677+ NULL, asset_class_pct, NULL, NULL, NULL, NULL
678+ FROM asset_class_exposure
679+ UNION ALL
680+ SELECT 'HIGH_RISK', holding_id, instrument_name, ticker, sector, region,
681+ NULL, risk_rating, pct_of_portfolio, NULL, NULL, NULL, NULL
682+ FROM high_risk_positions
683+ """
684+
685+ columns = [
686+ "ROW_TYPE" ,
687+ "ID" ,
688+ "LABEL" ,
689+ "TICKER" ,
690+ "SECTOR" ,
691+ "REGION" ,
692+ "ASSET_CLASS" ,
693+ "RISK_RATING" ,
694+ "PCT" ,
695+ "RISK_PROFILE" ,
696+ "ESG_MANDATE" ,
697+ "MAX_POSITION" ,
698+ "EXCLUDED_SECTORS" ,
699+ ]
700+
701+ rows , _ = execute_query (
702+ pg_conn ,
703+ sql ,
704+ {"account_id" : account_id },
705+ query_logger ,
706+ description = f"Hedge analysis (PostgreSQL): { account_id } " ,
707+ )
708+
709+ if not rows :
710+ return f"No holdings found for account '{ account_id } '"
711+
712+ from agent .tools import _build_hedge_recommendations
713+
714+ results = [dict (zip (columns , row , strict = False )) for row in rows ]
715+ return _json .dumps (_build_hedge_recommendations (results , account_id , risk_focus ), default = str )
0 commit comments