vn6295337 Claude Opus 4.5 commited on
Commit
287f0b8
·
1 Parent(s): 30dbe43

fix: Update _extract_and_emit_metrics for flattened data structure

Browse files

- Remove "data" wrapper lookups (structure was flattened)
- Fix source names: bea_bls → bea/bls/fred separate
- Fix valuation metric extraction (unwrap {value, as_of} dicts)
- Fix news/sentiment to use source-keyed arrays

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

Files changed (2) hide show
  1. .obsidian/workspace.json +1 -1
  2. mcp_client.py +74 -59
.obsidian/workspace.json CHANGED
@@ -185,6 +185,7 @@
185
  },
186
  "active": "369b0d31e1527f56",
187
  "lastOpenFiles": [
 
188
  "app.py.tmp.30809.1768319312452",
189
  "app.py.tmp.30809.1768319301570",
190
  "configs/__pycache__/output_schemas.cpython-311.pyc",
@@ -209,7 +210,6 @@
209
  "docs/mcp_test_report_V.md",
210
  "docs/alphavantage_data_schema.md",
211
  "__pycache__/mcp_client.cpython-311.pyc.140325135886896",
212
- "mcp-servers/volatility-basket/__pycache__/server.cpython-311.pyc.140157304405200",
213
  "docs/bls_data_schema.md",
214
  "docs/bea_data_schema.md",
215
  "docs/mcp_test_report_CVX.md",
 
185
  },
186
  "active": "369b0d31e1527f56",
187
  "lastOpenFiles": [
188
+ "__pycache__/mcp_client.cpython-311.pyc.139862298601136",
189
  "app.py.tmp.30809.1768319312452",
190
  "app.py.tmp.30809.1768319301570",
191
  "configs/__pycache__/output_schemas.cpython-311.pyc",
 
210
  "docs/mcp_test_report_V.md",
211
  "docs/alphavantage_data_schema.md",
212
  "__pycache__/mcp_client.cpython-311.pyc.140325135886896",
 
213
  "docs/bls_data_schema.md",
214
  "docs/bea_data_schema.md",
215
  "docs/mcp_test_report_CVX.md",
mcp_client.py CHANGED
@@ -472,9 +472,9 @@ async def _extract_and_emit_metrics(
472
  return
473
 
474
  if source == "fundamentals":
475
- # Multi-source structure: {"sec_edgar": {"data": {...}}, "yahoo_finance": {"data": {...}}}
476
- sec_data = _get_nested_value(result, "sec_edgar", "data") or {}
477
- yf_data = _get_nested_value(result, "yahoo_finance", "data") or {}
478
 
479
  # Revenue - prefer SEC EDGAR (primary source)
480
  revenue = sec_data.get("revenue") or yf_data.get("revenue") or {}
@@ -525,106 +525,121 @@ async def _extract_and_emit_metrics(
525
  await emit_metric(progress_callback, source, "debt_to_equity", debt_to_equity)
526
 
527
  elif source == "volatility":
528
- # Multi-source: {"yahoo_finance": {"data": {...}}, "alpha_vantage": {"data": {...}}, "market_volatility_context": {...}}
529
- yf_data = _get_nested_value(result, "yahoo_finance", "data") or {}
530
- av_data = _get_nested_value(result, "alpha_vantage", "data") or {}
531
- market_ctx = result.get("market_volatility_context") or {}
532
- # Each metric has its own date field (VIX uses "date", beta/hist_vol use "as_of")
533
-
534
- # VIX from market context (normalized to have "date" field)
535
- vix = market_ctx.get("vix") or {}
536
  if isinstance(vix, dict) and vix.get("value") is not None:
537
- await emit_metric(progress_callback, source, "VIX", vix["value"], end_date=vix.get("date"))
 
 
538
 
539
- # Beta - prefer Yahoo Finance (raw dict has "as_of" field)
540
- beta = yf_data.get("beta") or av_data.get("beta")
541
  if isinstance(beta, dict) and beta.get("value") is not None:
542
  await emit_metric(progress_callback, source, "beta", beta["value"], end_date=beta.get("as_of"))
543
  elif isinstance(beta, (int, float)):
544
  await emit_metric(progress_callback, source, "beta", beta)
545
 
546
- # Historical Volatility (raw dict has "as_of" field)
547
- hist_vol = yf_data.get("historical_volatility") or av_data.get("historical_volatility")
548
  if isinstance(hist_vol, dict) and hist_vol.get("value") is not None:
549
  await emit_metric(progress_callback, source, "hist_vol", hist_vol["value"], end_date=hist_vol.get("as_of"))
550
  elif isinstance(hist_vol, (int, float)):
551
  await emit_metric(progress_callback, source, "hist_vol", hist_vol)
552
 
553
  elif source == "macro":
554
- # Multi-source: {"bea_bls": {"data": {...}}, "fred": {"data": {...}}}
555
- bea_bls = _get_nested_value(result, "bea_bls", "data") or {}
556
- fred = _get_nested_value(result, "fred", "data") or {}
557
- # Each metric has its own "date" field (actual data date, e.g., Q3 2025)
558
 
559
- # GDP Growth - prefer BEA/BLS
560
- gdp = bea_bls.get("gdp_growth") or fred.get("gdp_growth") or {}
561
  if isinstance(gdp, dict) and gdp.get("value") is not None:
562
- await emit_metric(progress_callback, source, "GDP_growth", gdp["value"], end_date=gdp.get("date"))
563
  elif isinstance(gdp, (int, float)):
564
  await emit_metric(progress_callback, source, "GDP_growth", gdp)
565
 
566
- # Interest Rate - FRED only
567
  interest = fred.get("interest_rate") or {}
568
  if isinstance(interest, dict) and interest.get("value") is not None:
569
- await emit_metric(progress_callback, source, "interest_rate", interest["value"], end_date=interest.get("date"))
570
  elif isinstance(interest, (int, float)):
571
  await emit_metric(progress_callback, source, "interest_rate", interest)
572
 
573
- # Inflation (CPI)
574
- inflation = bea_bls.get("cpi_inflation") or fred.get("cpi_inflation") or {}
575
  if isinstance(inflation, dict) and inflation.get("value") is not None:
576
- await emit_metric(progress_callback, source, "inflation", inflation["value"], end_date=inflation.get("date"))
577
  elif isinstance(inflation, (int, float)):
578
  await emit_metric(progress_callback, source, "inflation", inflation)
579
 
580
- # Unemployment
581
- unemployment = bea_bls.get("unemployment") or fred.get("unemployment") or {}
582
  if isinstance(unemployment, dict) and unemployment.get("value") is not None:
583
- await emit_metric(progress_callback, source, "unemployment", unemployment["value"], end_date=unemployment.get("date"))
584
  elif isinstance(unemployment, (int, float)):
585
  await emit_metric(progress_callback, source, "unemployment", unemployment)
586
 
587
  elif source == "valuation":
588
- # Multi-source: {"yahoo_finance": {"data": {...}}, "alpha_vantage": {"data": {...}}}
589
- yf_data = _get_nested_value(result, "yahoo_finance", "data") or {}
590
- av_data = _get_nested_value(result, "alpha_vantage", "data") or {}
591
- # Use regular_market_time (actual market data timestamp) or fallback to as_of
592
- market_time = _get_nested_value(result, "yahoo_finance", "regular_market_time") or result.get("as_of")
593
-
594
- # P/E Ratio - prefer Yahoo Finance
595
- pe_trailing = yf_data.get("trailing_pe") or av_data.get("trailing_pe")
596
- if pe_trailing is not None:
597
- await emit_metric(progress_callback, source, "P/E", pe_trailing, end_date=market_time)
 
 
598
 
599
  # P/B Ratio
600
- pb_ratio = yf_data.get("pb_ratio") or av_data.get("pb_ratio")
601
- if pb_ratio is not None:
602
- await emit_metric(progress_callback, source, "P/B", pb_ratio, end_date=market_time)
 
 
603
 
604
  # P/S Ratio
605
- ps_ratio = yf_data.get("ps_ratio") or av_data.get("ps_ratio")
606
- if ps_ratio is not None:
607
- await emit_metric(progress_callback, source, "P/S", ps_ratio, end_date=market_time)
 
 
608
 
609
  # EV/EBITDA
610
- ev_ebitda = yf_data.get("ev_ebitda") or av_data.get("ev_ebitda")
611
- if ev_ebitda is not None:
612
- await emit_metric(progress_callback, source, "EV/EBITDA", ev_ebitda, end_date=market_time)
 
 
613
 
614
  elif source == "news":
615
- # News-basket returns normalized "items" array
616
- items = result.get("items") or []
617
- if items and isinstance(items, list) and len(items) > 0:
618
- await emit_metric(progress_callback, source, "items_found", len(items))
 
 
 
 
619
  else:
620
  await emit_metric(progress_callback, source, "status", "No recent news found")
621
 
622
  elif source == "sentiment":
623
- # Sentiment-basket returns raw content (items) without scoring
624
- # Scoring is applied downstream by analyzer
625
- items = result.get("items") or []
626
- if items and isinstance(items, list) and len(items) > 0:
627
- await emit_metric(progress_callback, source, "items_found", len(items))
 
 
 
628
  else:
629
  await emit_metric(progress_callback, source, "status", "No sentiment content found")
630
 
 
472
  return
473
 
474
  if source == "fundamentals":
475
+ # Multi-source structure (flattened): {"sec_edgar": {...}, "yahoo_finance": {...}}
476
+ sec_data = result.get("sec_edgar") or {}
477
+ yf_data = result.get("yahoo_finance") or {}
478
 
479
  # Revenue - prefer SEC EDGAR (primary source)
480
  revenue = sec_data.get("revenue") or yf_data.get("revenue") or {}
 
525
  await emit_metric(progress_callback, source, "debt_to_equity", debt_to_equity)
526
 
527
  elif source == "volatility":
528
+ # Multi-source (flattened): {"fred": {...}, "yahoo_finance": {...}}
529
+ fred_data = result.get("fred") or {}
530
+ yf_data = result.get("yahoo_finance") or {}
531
+
532
+ # VIX from FRED
533
+ vix = fred_data.get("vix") or {}
 
 
534
  if isinstance(vix, dict) and vix.get("value") is not None:
535
+ await emit_metric(progress_callback, source, "VIX", vix["value"], end_date=vix.get("as_of"))
536
+ elif isinstance(vix, (int, float)):
537
+ await emit_metric(progress_callback, source, "VIX", vix)
538
 
539
+ # Beta from Yahoo Finance
540
+ beta = yf_data.get("beta") or {}
541
  if isinstance(beta, dict) and beta.get("value") is not None:
542
  await emit_metric(progress_callback, source, "beta", beta["value"], end_date=beta.get("as_of"))
543
  elif isinstance(beta, (int, float)):
544
  await emit_metric(progress_callback, source, "beta", beta)
545
 
546
+ # Historical Volatility from Yahoo Finance
547
+ hist_vol = yf_data.get("historical_volatility") or {}
548
  if isinstance(hist_vol, dict) and hist_vol.get("value") is not None:
549
  await emit_metric(progress_callback, source, "hist_vol", hist_vol["value"], end_date=hist_vol.get("as_of"))
550
  elif isinstance(hist_vol, (int, float)):
551
  await emit_metric(progress_callback, source, "hist_vol", hist_vol)
552
 
553
  elif source == "macro":
554
+ # Multi-source (flattened): {"bea": {...}, "bls": {...}, "fred": {...}}
555
+ bea = result.get("bea") or {}
556
+ bls = result.get("bls") or {}
557
+ fred = result.get("fred") or {}
558
 
559
+ # GDP Growth from BEA
560
+ gdp = bea.get("gdp_growth") or {}
561
  if isinstance(gdp, dict) and gdp.get("value") is not None:
562
+ await emit_metric(progress_callback, source, "GDP_growth", gdp["value"], end_date=gdp.get("as_of"))
563
  elif isinstance(gdp, (int, float)):
564
  await emit_metric(progress_callback, source, "GDP_growth", gdp)
565
 
566
+ # Interest Rate from FRED
567
  interest = fred.get("interest_rate") or {}
568
  if isinstance(interest, dict) and interest.get("value") is not None:
569
+ await emit_metric(progress_callback, source, "interest_rate", interest["value"], end_date=interest.get("as_of"))
570
  elif isinstance(interest, (int, float)):
571
  await emit_metric(progress_callback, source, "interest_rate", interest)
572
 
573
+ # Inflation (CPI) from BLS
574
+ inflation = bls.get("cpi_inflation") or {}
575
  if isinstance(inflation, dict) and inflation.get("value") is not None:
576
+ await emit_metric(progress_callback, source, "inflation", inflation["value"], end_date=inflation.get("as_of"))
577
  elif isinstance(inflation, (int, float)):
578
  await emit_metric(progress_callback, source, "inflation", inflation)
579
 
580
+ # Unemployment from BLS
581
+ unemployment = bls.get("unemployment") or {}
582
  if isinstance(unemployment, dict) and unemployment.get("value") is not None:
583
+ await emit_metric(progress_callback, source, "unemployment", unemployment["value"], end_date=unemployment.get("as_of"))
584
  elif isinstance(unemployment, (int, float)):
585
  await emit_metric(progress_callback, source, "unemployment", unemployment)
586
 
587
  elif source == "valuation":
588
+ # Multi-source (flattened): {"yahoo_finance": {...}, "alpha_vantage": {...}}
589
+ yf_data = result.get("yahoo_finance") or {}
590
+ av_data = result.get("alpha_vantage") or {}
591
+ # Use regular_market_time from yahoo_finance for timestamp
592
+ market_time = yf_data.get("regular_market_time")
593
+
594
+ # P/E Ratio - prefer Yahoo Finance (wrapped in {value, as_of})
595
+ pe_data = yf_data.get("trailing_pe") or av_data.get("trailing_pe") or {}
596
+ if isinstance(pe_data, dict) and pe_data.get("value") is not None:
597
+ await emit_metric(progress_callback, source, "P/E", pe_data["value"], end_date=pe_data.get("as_of") or market_time)
598
+ elif isinstance(pe_data, (int, float)):
599
+ await emit_metric(progress_callback, source, "P/E", pe_data, end_date=market_time)
600
 
601
  # P/B Ratio
602
+ pb_data = yf_data.get("pb_ratio") or av_data.get("pb_ratio") or {}
603
+ if isinstance(pb_data, dict) and pb_data.get("value") is not None:
604
+ await emit_metric(progress_callback, source, "P/B", pb_data["value"], end_date=pb_data.get("as_of") or market_time)
605
+ elif isinstance(pb_data, (int, float)):
606
+ await emit_metric(progress_callback, source, "P/B", pb_data, end_date=market_time)
607
 
608
  # P/S Ratio
609
+ ps_data = yf_data.get("ps_ratio") or av_data.get("ps_ratio") or {}
610
+ if isinstance(ps_data, dict) and ps_data.get("value") is not None:
611
+ await emit_metric(progress_callback, source, "P/S", ps_data["value"], end_date=ps_data.get("as_of") or market_time)
612
+ elif isinstance(ps_data, (int, float)):
613
+ await emit_metric(progress_callback, source, "P/S", ps_data, end_date=market_time)
614
 
615
  # EV/EBITDA
616
+ ev_data = yf_data.get("ev_ebitda") or av_data.get("ev_ebitda") or {}
617
+ if isinstance(ev_data, dict) and ev_data.get("value") is not None:
618
+ await emit_metric(progress_callback, source, "EV/EBITDA", ev_data["value"], end_date=ev_data.get("as_of") or market_time)
619
+ elif isinstance(ev_data, (int, float)):
620
+ await emit_metric(progress_callback, source, "EV/EBITDA", ev_data, end_date=market_time)
621
 
622
  elif source == "news":
623
+ # News-basket returns source-keyed structure: {"tavily": [...], "nyt": [...], "newsapi": [...]}
624
+ total_items = 0
625
+ for news_source in ["tavily", "nyt", "newsapi"]:
626
+ items = result.get(news_source) or []
627
+ if isinstance(items, list):
628
+ total_items += len(items)
629
+ if total_items > 0:
630
+ await emit_metric(progress_callback, source, "items_found", total_items)
631
  else:
632
  await emit_metric(progress_callback, source, "status", "No recent news found")
633
 
634
  elif source == "sentiment":
635
+ # Sentiment-basket returns source-keyed structure: {"finnhub": [...], "reddit": [...]}
636
+ total_items = 0
637
+ for sent_source in ["finnhub", "reddit"]:
638
+ items = result.get(sent_source) or []
639
+ if isinstance(items, list):
640
+ total_items += len(items)
641
+ if total_items > 0:
642
+ await emit_metric(progress_callback, source, "items_found", total_items)
643
  else:
644
  await emit_metric(progress_callback, source, "status", "No sentiment content found")
645