Spaces:
Sleeping
Sleeping
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>
- .obsidian/workspace.json +1 -1
- 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": {
|
| 476 |
-
sec_data =
|
| 477 |
-
yf_data =
|
| 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
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
#
|
| 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("
|
|
|
|
|
|
|
| 538 |
|
| 539 |
-
# Beta
|
| 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
|
| 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: {"
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
|
| 559 |
-
# GDP Growth
|
| 560 |
-
gdp =
|
| 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("
|
| 563 |
elif isinstance(gdp, (int, float)):
|
| 564 |
await emit_metric(progress_callback, source, "GDP_growth", gdp)
|
| 565 |
|
| 566 |
-
# Interest Rate
|
| 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("
|
| 570 |
elif isinstance(interest, (int, float)):
|
| 571 |
await emit_metric(progress_callback, source, "interest_rate", interest)
|
| 572 |
|
| 573 |
-
# Inflation (CPI)
|
| 574 |
-
inflation =
|
| 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("
|
| 577 |
elif isinstance(inflation, (int, float)):
|
| 578 |
await emit_metric(progress_callback, source, "inflation", inflation)
|
| 579 |
|
| 580 |
-
# Unemployment
|
| 581 |
-
unemployment =
|
| 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("
|
| 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": {
|
| 589 |
-
yf_data =
|
| 590 |
-
av_data =
|
| 591 |
-
# Use regular_market_time
|
| 592 |
-
market_time =
|
| 593 |
-
|
| 594 |
-
# P/E Ratio - prefer Yahoo Finance
|
| 595 |
-
|
| 596 |
-
if
|
| 597 |
-
await emit_metric(progress_callback, source, "P/E",
|
|
|
|
|
|
|
| 598 |
|
| 599 |
# P/B Ratio
|
| 600 |
-
|
| 601 |
-
if
|
| 602 |
-
await emit_metric(progress_callback, source, "P/B",
|
|
|
|
|
|
|
| 603 |
|
| 604 |
# P/S Ratio
|
| 605 |
-
|
| 606 |
-
if
|
| 607 |
-
await emit_metric(progress_callback, source, "P/S",
|
|
|
|
|
|
|
| 608 |
|
| 609 |
# EV/EBITDA
|
| 610 |
-
|
| 611 |
-
if
|
| 612 |
-
await emit_metric(progress_callback, source, "EV/EBITDA",
|
|
|
|
|
|
|
| 613 |
|
| 614 |
elif source == "news":
|
| 615 |
-
# News-basket returns
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
else:
|
| 620 |
await emit_metric(progress_callback, source, "status", "No recent news found")
|
| 621 |
|
| 622 |
elif source == "sentiment":
|
| 623 |
-
# Sentiment-basket returns
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
|