Skip to content

Commit 17bdb12

Browse files
authored
Merge pull request #1 from manu49/manu49-hedging-feature
Add portfolio hedge recommendation tool with comprehensive risk analysis
2 parents 5060d99 + 8f5441c commit 17bdb12

16 files changed

Lines changed: 3601 additions & 24 deletions

apps/finance-ai-agent-demo/backend/agent/sprawl_tools.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)