vn6295337 Claude Opus 4.5 commited on
Commit
b3daac3
·
1 Parent(s): 6810cb2

Fix: Metric extraction to match MCP data structure

Browse files

- Fix fundamentals to use sec_edgar/yahoo_finance structure
- Fix valuation to use yahoo_finance.data structure
- Fix volatility to use yahoo_finance.data + market_volatility_context
- Fix macro to use bea_bls/fred structure
- Reduce critic source_data truncation to 4000 chars (was 8000)
- Fallback to multi_source if metrics path empty

Fixes "No metrics extracted" error and 413 Payload Too Large.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Files changed (2) hide show
  1. src/nodes/analyzer.py +40 -29
  2. src/nodes/critic.py +2 -2
src/nodes/analyzer.py CHANGED
@@ -579,10 +579,17 @@ def _extract_key_metrics(raw_data: str) -> dict:
579
  }
580
 
581
  # Extract fundamentals with temporal data
 
 
582
  fin = metrics.get("fundamentals", {})
 
 
583
  if fin and "error" not in fin:
584
- fin_data = fin.get("fundamentals", {})
585
- debt_data = fin.get("debt", {})
 
 
 
586
  extracted["fundamentals"] = {
587
  "revenue": _extract_temporal_metric(fin_data.get("revenue", {})),
588
  "revenue_cagr_3yr": fin_data.get("revenue_growth_3yr"),
@@ -590,42 +597,41 @@ def _extract_key_metrics(raw_data: str) -> dict:
590
  "gross_margin": _extract_temporal_metric(fin_data.get("gross_margin_pct", {})),
591
  "operating_margin": _extract_temporal_metric(fin_data.get("operating_margin_pct", {})),
592
  "eps": _extract_temporal_metric(fin_data.get("eps", {})),
593
- "debt_to_equity": _extract_temporal_metric(debt_data.get("debt_to_equity", {})),
594
- "free_cash_flow": _extract_temporal_metric(fin.get("cash_flow", {}).get("free_cash_flow", {})),
595
  "net_income": _extract_temporal_metric(fin_data.get("net_income", {})),
596
  }
597
 
598
  # Extract valuation (with temporal data)
 
599
  val = metrics.get("valuation", {})
 
 
600
  if val and "error" not in val:
601
- val_metrics = val.get("metrics", {})
602
- pe = val_metrics.get("pe_ratio", {})
603
- # Get valuation date from sources or response-level
604
- val_date = (
605
- val.get("sources", {}).get("yahoo_finance", {}).get("regular_market_time")
606
- or val.get("as_of")
607
- or (val.get("generated_at", "")[:10] if val.get("generated_at") else None)
608
- )
609
  extracted["valuation"] = {
610
- "pe_trailing": {"value": pe.get("trailing") if isinstance(pe, dict) else pe, "end_date": val_date},
611
- "pe_forward": {"value": pe.get("forward") if isinstance(pe, dict) else None, "end_date": val_date},
612
- "pb_ratio": {"value": val_metrics.get("pb_ratio"), "end_date": val_date},
613
- "ps_ratio": {"value": val_metrics.get("ps_ratio"), "end_date": val_date},
614
- "ev_ebitda": {"value": val_metrics.get("ev_ebitda"), "end_date": val_date},
615
  "valuation_signal": val.get("overall_signal"),
616
  "as_of": val_date,
617
  }
618
 
619
  # Extract volatility (with temporal data)
 
620
  vol = metrics.get("volatility", {})
 
 
621
  if vol and "error" not in vol:
622
- vol_metrics = vol.get("metrics", {})
623
- # Get response-level date as fallback
624
  vol_date = vol.get("generated_at", "")[:10] if vol.get("generated_at") else None
625
- # Extract each metric with its own date (or fallback to response date)
626
- vix_data = vol_metrics.get("vix", {})
627
- beta_data = vol_metrics.get("beta", {})
628
- hv_data = vol_metrics.get("historical_volatility", {})
629
  extracted["volatility"] = {
630
  "beta": {"value": beta_data.get("value") if isinstance(beta_data, dict) else beta_data,
631
  "end_date": beta_data.get("date") or vol_date if isinstance(beta_data, dict) else vol_date},
@@ -637,14 +643,19 @@ def _extract_key_metrics(raw_data: str) -> dict:
637
  }
638
 
639
  # Extract macro (with temporal data)
 
640
  macro = metrics.get("macro", {})
 
 
641
  if macro and "error" not in macro:
642
- macro_metrics = macro.get("metrics", {})
643
- # Each macro metric has its own date/period
644
- gdp = macro_metrics.get("gdp_growth", {})
645
- interest = macro_metrics.get("interest_rate", {})
646
- inflation = macro_metrics.get("cpi_inflation", {})
647
- unemp = macro_metrics.get("unemployment", {})
 
 
648
  extracted["macro"] = {
649
  "gdp_growth": {"value": gdp.get("value") if isinstance(gdp, dict) else gdp,
650
  "end_date": gdp.get("date") or gdp.get("period") if isinstance(gdp, dict) else None},
 
579
  }
580
 
581
  # Extract fundamentals with temporal data
582
+ # Structure: metrics.fundamentals = {"sec_edgar": {"data": {...}}, "yahoo_finance": {"data": {...}}}
583
+ # Also check multi_source.fundamentals_all for the same structure
584
  fin = metrics.get("fundamentals", {})
585
+ if not fin or "error" in fin:
586
+ fin = data.get("multi_source", {}).get("fundamentals_all", {})
587
  if fin and "error" not in fin:
588
+ # Use SEC EDGAR as primary, Yahoo Finance as fallback
589
+ sec_data = fin.get("sec_edgar", {}).get("data", {})
590
+ yf_data = fin.get("yahoo_finance", {}).get("data", {})
591
+ # Merge with SEC as primary
592
+ fin_data = {**yf_data, **sec_data} # SEC overwrites YF where both exist
593
  extracted["fundamentals"] = {
594
  "revenue": _extract_temporal_metric(fin_data.get("revenue", {})),
595
  "revenue_cagr_3yr": fin_data.get("revenue_growth_3yr"),
 
597
  "gross_margin": _extract_temporal_metric(fin_data.get("gross_margin_pct", {})),
598
  "operating_margin": _extract_temporal_metric(fin_data.get("operating_margin_pct", {})),
599
  "eps": _extract_temporal_metric(fin_data.get("eps", {})),
600
+ "debt_to_equity": _extract_temporal_metric(fin_data.get("debt_to_equity", {})),
601
+ "free_cash_flow": _extract_temporal_metric(fin_data.get("free_cash_flow", {})),
602
  "net_income": _extract_temporal_metric(fin_data.get("net_income", {})),
603
  }
604
 
605
  # Extract valuation (with temporal data)
606
+ # Structure: {"yahoo_finance": {"data": {...}, "regular_market_time": "..."}}
607
  val = metrics.get("valuation", {})
608
+ if not val or "error" in val:
609
+ val = data.get("multi_source", {}).get("valuation_all", {})
610
  if val and "error" not in val:
611
+ yf_val = val.get("yahoo_finance", {}).get("data", {})
612
+ val_date = val.get("yahoo_finance", {}).get("regular_market_time")
 
 
 
 
 
 
613
  extracted["valuation"] = {
614
+ "pe_trailing": {"value": yf_val.get("trailing_pe"), "end_date": val_date},
615
+ "pe_forward": {"value": yf_val.get("forward_pe"), "end_date": val_date},
616
+ "pb_ratio": {"value": yf_val.get("pb_ratio"), "end_date": val_date},
617
+ "ps_ratio": {"value": yf_val.get("ps_ratio"), "end_date": val_date},
618
+ "ev_ebitda": {"value": yf_val.get("ev_ebitda"), "end_date": val_date},
619
  "valuation_signal": val.get("overall_signal"),
620
  "as_of": val_date,
621
  }
622
 
623
  # Extract volatility (with temporal data)
624
+ # Structure: {"yahoo_finance": {"data": {...}}, "market_volatility_context": {"vix": {...}, "vxn": {...}}}
625
  vol = metrics.get("volatility", {})
626
+ if not vol or "error" in vol:
627
+ vol = data.get("multi_source", {}).get("volatility_all", {})
628
  if vol and "error" not in vol:
629
+ yf_vol = vol.get("yahoo_finance", {}).get("data", {})
630
+ mkt_ctx = vol.get("market_volatility_context", {})
631
  vol_date = vol.get("generated_at", "")[:10] if vol.get("generated_at") else None
632
+ vix_data = mkt_ctx.get("vix", {})
633
+ beta_data = yf_vol.get("beta", {})
634
+ hv_data = yf_vol.get("historical_volatility", {})
 
635
  extracted["volatility"] = {
636
  "beta": {"value": beta_data.get("value") if isinstance(beta_data, dict) else beta_data,
637
  "end_date": beta_data.get("date") or vol_date if isinstance(beta_data, dict) else vol_date},
 
643
  }
644
 
645
  # Extract macro (with temporal data)
646
+ # Structure: {"bea_bls": {"data": {...}}, "fred": {"data": {...}}}
647
  macro = metrics.get("macro", {})
648
+ if not macro or "error" in macro:
649
+ macro = data.get("multi_source", {}).get("macro_all", {})
650
  if macro and "error" not in macro:
651
+ bea_bls = macro.get("bea_bls", {}).get("data", {})
652
+ fred = macro.get("fred", {}).get("data", {})
653
+ # Merge sources (BEA/BLS primary, FRED fallback)
654
+ macro_data = {**fred, **bea_bls}
655
+ gdp = macro_data.get("gdp_growth", {})
656
+ interest = macro_data.get("interest_rate", {})
657
+ inflation = macro_data.get("cpi_inflation", {})
658
+ unemp = macro_data.get("unemployment", {})
659
  extracted["macro"] = {
660
  "gdp_growth": {"value": gdp.get("value") if isinstance(gdp, dict) else gdp,
661
  "end_date": gdp.get("date") or gdp.get("period") if isinstance(gdp, dict) else None},
src/nodes/critic.py CHANGED
@@ -182,8 +182,8 @@ def run_llm_evaluation(report: str, source_data: str, iteration: int, llm) -> di
182
  Returns:
183
  Evaluation result dict with scores, status, and feedback
184
  """
185
- # Truncate source data if too long
186
- max_source_len = 8000
187
  if len(source_data) > max_source_len:
188
  source_data = source_data[:max_source_len] + "\n... [truncated]"
189
 
 
182
  Returns:
183
  Evaluation result dict with scores, status, and feedback
184
  """
185
+ # Truncate source data if too long (Groq has ~8K token limit)
186
+ max_source_len = 4000
187
  if len(source_data) > max_source_len:
188
  source_data = source_data[:max_source_len] + "\n... [truncated]"
189