Spaces:
Sleeping
Sleeping
Fix temporal data for calculated metrics (margins, debt_to_equity)
Browse filesfinancials-basket/server.py:
- Add create_temporal_metric() helper to inherit temporal data from source metrics
- Update margin calculations (gross, operating, net) to preserve fiscal period
- Update debt_to_equity calculation to preserve fiscal period
- Update SWOT analysis to handle both dict and plain number formats
mcp_client.py:
- Fix structure: result IS the financials data, not nested under "financials" key
- Handle temporal data for net_margin_pct and debt_to_equity dicts
- Add fallback for old format (plain numbers)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- mcp-servers/financials-basket/server.py +49 -13
- mcp_client.py +29 -8
mcp-servers/financials-basket/server.py
CHANGED
|
@@ -47,6 +47,26 @@ logger = logging.getLogger("financials-basket")
|
|
| 47 |
# Thread pool for yfinance (synchronous library)
|
| 48 |
_executor = ThreadPoolExecutor(max_workers=2)
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
# Initialize MCP server
|
| 51 |
server = Server("financials-basket")
|
| 52 |
|
|
@@ -254,18 +274,27 @@ async def fetch_financials(ticker: str) -> dict:
|
|
| 254 |
|
| 255 |
stockholders_equity = get_latest_value(facts, "StockholdersEquity")
|
| 256 |
|
| 257 |
-
# Calculate margins
|
| 258 |
gross_margin = None
|
| 259 |
if revenue and gross_profit and revenue["value"] and gross_profit["value"]:
|
| 260 |
-
gross_margin =
|
|
|
|
|
|
|
|
|
|
| 261 |
|
| 262 |
operating_margin = None
|
| 263 |
if revenue and operating_income and revenue["value"] and operating_income["value"]:
|
| 264 |
-
operating_margin =
|
|
|
|
|
|
|
|
|
|
| 265 |
|
| 266 |
net_margin = None
|
| 267 |
if revenue and net_income and revenue["value"] and net_income["value"]:
|
| 268 |
-
net_margin =
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
# Revenue growth
|
| 271 |
revenue_growth = calculate_growth(facts, "Revenues") or \
|
|
@@ -334,14 +363,17 @@ async def fetch_debt_metrics(ticker: str) -> dict:
|
|
| 334 |
# Get EBITDA or operating income for leverage ratio
|
| 335 |
operating_income = get_latest_value(facts, "OperatingIncomeLoss")
|
| 336 |
|
| 337 |
-
# Debt to equity
|
| 338 |
stockholders_equity = get_latest_value(facts, "StockholdersEquity")
|
| 339 |
debt_to_equity = None
|
| 340 |
if total_debt and stockholders_equity:
|
| 341 |
debt_val = total_debt.get("value", 0) or 0
|
| 342 |
equity_val = stockholders_equity.get("value", 0) or 0
|
| 343 |
if equity_val > 0:
|
| 344 |
-
debt_to_equity =
|
|
|
|
|
|
|
|
|
|
| 345 |
|
| 346 |
return {
|
| 347 |
"ticker": ticker.upper(),
|
|
@@ -913,8 +945,9 @@ def _build_swot_from_fallback(data: dict) -> dict:
|
|
| 913 |
elif growth < 0:
|
| 914 |
swot_summary["weaknesses"].append(f"Declining revenue: {growth}%")
|
| 915 |
|
| 916 |
-
# Debt analysis
|
| 917 |
-
|
|
|
|
| 918 |
if d_to_e is not None:
|
| 919 |
if d_to_e > 2:
|
| 920 |
swot_summary["threats"].append(f"High leverage: {d_to_e}x debt-to-equity")
|
|
@@ -998,8 +1031,9 @@ async def get_sec_fundamentals_basket(ticker: str) -> dict:
|
|
| 998 |
elif growth < 0:
|
| 999 |
swot_summary["weaknesses"].append(f"Declining revenue: {growth}% CAGR (3yr)")
|
| 1000 |
|
| 1001 |
-
# Margins
|
| 1002 |
-
|
|
|
|
| 1003 |
if net_margin is not None:
|
| 1004 |
if net_margin > 15:
|
| 1005 |
swot_summary["strengths"].append(f"High profitability: {net_margin}% net margin")
|
|
@@ -1010,13 +1044,15 @@ async def get_sec_fundamentals_basket(ticker: str) -> dict:
|
|
| 1010 |
elif net_margin < 5:
|
| 1011 |
swot_summary["weaknesses"].append(f"Thin margins: {net_margin}% net margin")
|
| 1012 |
|
| 1013 |
-
|
|
|
|
| 1014 |
if op_margin is not None and op_margin > 20:
|
| 1015 |
swot_summary["strengths"].append(f"Strong operating efficiency: {op_margin}% operating margin")
|
| 1016 |
|
| 1017 |
-
# Analyze debt for SWOT
|
| 1018 |
if debt and "error" not in debt:
|
| 1019 |
-
|
|
|
|
| 1020 |
if d_to_e is not None:
|
| 1021 |
if d_to_e > 2:
|
| 1022 |
swot_summary["threats"].append(f"High leverage: {d_to_e}x debt-to-equity")
|
|
|
|
| 47 |
# Thread pool for yfinance (synchronous library)
|
| 48 |
_executor = ThreadPoolExecutor(max_workers=2)
|
| 49 |
|
| 50 |
+
|
| 51 |
+
def create_temporal_metric(value, source_metric) -> dict:
|
| 52 |
+
"""Create a metric with temporal data inherited from source metric.
|
| 53 |
+
|
| 54 |
+
Args:
|
| 55 |
+
value: The calculated metric value
|
| 56 |
+
source_metric: Source metric dict containing temporal data (end_date, fiscal_year, form)
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
Dict with value and inherited temporal data
|
| 60 |
+
"""
|
| 61 |
+
if source_metric and isinstance(source_metric, dict):
|
| 62 |
+
return {
|
| 63 |
+
"value": value,
|
| 64 |
+
"end_date": source_metric.get("end_date"),
|
| 65 |
+
"fiscal_year": source_metric.get("fiscal_year"),
|
| 66 |
+
"form": source_metric.get("form")
|
| 67 |
+
}
|
| 68 |
+
return {"value": value}
|
| 69 |
+
|
| 70 |
# Initialize MCP server
|
| 71 |
server = Server("financials-basket")
|
| 72 |
|
|
|
|
| 274 |
|
| 275 |
stockholders_equity = get_latest_value(facts, "StockholdersEquity")
|
| 276 |
|
| 277 |
+
# Calculate margins (preserve temporal data from source metrics)
|
| 278 |
gross_margin = None
|
| 279 |
if revenue and gross_profit and revenue["value"] and gross_profit["value"]:
|
| 280 |
+
gross_margin = create_temporal_metric(
|
| 281 |
+
round((gross_profit["value"] / revenue["value"]) * 100, 2),
|
| 282 |
+
revenue
|
| 283 |
+
)
|
| 284 |
|
| 285 |
operating_margin = None
|
| 286 |
if revenue and operating_income and revenue["value"] and operating_income["value"]:
|
| 287 |
+
operating_margin = create_temporal_metric(
|
| 288 |
+
round((operating_income["value"] / revenue["value"]) * 100, 2),
|
| 289 |
+
revenue
|
| 290 |
+
)
|
| 291 |
|
| 292 |
net_margin = None
|
| 293 |
if revenue and net_income and revenue["value"] and net_income["value"]:
|
| 294 |
+
net_margin = create_temporal_metric(
|
| 295 |
+
round((net_income["value"] / revenue["value"]) * 100, 2),
|
| 296 |
+
revenue
|
| 297 |
+
)
|
| 298 |
|
| 299 |
# Revenue growth
|
| 300 |
revenue_growth = calculate_growth(facts, "Revenues") or \
|
|
|
|
| 363 |
# Get EBITDA or operating income for leverage ratio
|
| 364 |
operating_income = get_latest_value(facts, "OperatingIncomeLoss")
|
| 365 |
|
| 366 |
+
# Debt to equity (preserve temporal data)
|
| 367 |
stockholders_equity = get_latest_value(facts, "StockholdersEquity")
|
| 368 |
debt_to_equity = None
|
| 369 |
if total_debt and stockholders_equity:
|
| 370 |
debt_val = total_debt.get("value", 0) or 0
|
| 371 |
equity_val = stockholders_equity.get("value", 0) or 0
|
| 372 |
if equity_val > 0:
|
| 373 |
+
debt_to_equity = create_temporal_metric(
|
| 374 |
+
round(debt_val / equity_val, 2),
|
| 375 |
+
total_debt # Inherit temporal data from total_debt
|
| 376 |
+
)
|
| 377 |
|
| 378 |
return {
|
| 379 |
"ticker": ticker.upper(),
|
|
|
|
| 945 |
elif growth < 0:
|
| 946 |
swot_summary["weaknesses"].append(f"Declining revenue: {growth}%")
|
| 947 |
|
| 948 |
+
# Debt analysis (handle both dict and plain number formats)
|
| 949 |
+
d_to_e_data = debt.get("debt_to_equity")
|
| 950 |
+
d_to_e = d_to_e_data.get("value") if isinstance(d_to_e_data, dict) else d_to_e_data
|
| 951 |
if d_to_e is not None:
|
| 952 |
if d_to_e > 2:
|
| 953 |
swot_summary["threats"].append(f"High leverage: {d_to_e}x debt-to-equity")
|
|
|
|
| 1031 |
elif growth < 0:
|
| 1032 |
swot_summary["weaknesses"].append(f"Declining revenue: {growth}% CAGR (3yr)")
|
| 1033 |
|
| 1034 |
+
# Margins (handle both dict and plain number formats)
|
| 1035 |
+
net_margin_data = financials.get("net_margin_pct")
|
| 1036 |
+
net_margin = net_margin_data.get("value") if isinstance(net_margin_data, dict) else net_margin_data
|
| 1037 |
if net_margin is not None:
|
| 1038 |
if net_margin > 15:
|
| 1039 |
swot_summary["strengths"].append(f"High profitability: {net_margin}% net margin")
|
|
|
|
| 1044 |
elif net_margin < 5:
|
| 1045 |
swot_summary["weaknesses"].append(f"Thin margins: {net_margin}% net margin")
|
| 1046 |
|
| 1047 |
+
op_margin_data = financials.get("operating_margin_pct")
|
| 1048 |
+
op_margin = op_margin_data.get("value") if isinstance(op_margin_data, dict) else op_margin_data
|
| 1049 |
if op_margin is not None and op_margin > 20:
|
| 1050 |
swot_summary["strengths"].append(f"Strong operating efficiency: {op_margin}% operating margin")
|
| 1051 |
|
| 1052 |
+
# Analyze debt for SWOT (handle both dict and plain number formats)
|
| 1053 |
if debt and "error" not in debt:
|
| 1054 |
+
d_to_e_data = debt.get("debt_to_equity")
|
| 1055 |
+
d_to_e = d_to_e_data.get("value") if isinstance(d_to_e_data, dict) else d_to_e_data
|
| 1056 |
if d_to_e is not None:
|
| 1057 |
if d_to_e > 2:
|
| 1058 |
swot_summary["threats"].append(f"High leverage: {d_to_e}x debt-to-equity")
|
mcp_client.py
CHANGED
|
@@ -270,10 +270,9 @@ async def _extract_and_emit_metrics(
|
|
| 270 |
return
|
| 271 |
|
| 272 |
if source == "financials":
|
| 273 |
-
financials
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
revenue = financials.get("revenue") or {}
|
| 277 |
if isinstance(revenue, dict) and revenue.get("value"):
|
| 278 |
await emit_metric(
|
| 279 |
progress_callback, source, "revenue", revenue["value"],
|
|
@@ -281,10 +280,22 @@ async def _extract_and_emit_metrics(
|
|
| 281 |
fiscal_year=revenue.get("fiscal_year"),
|
| 282 |
form=revenue.get("form")
|
| 283 |
)
|
| 284 |
-
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
await emit_metric(progress_callback, source, "net_margin", net_margin)
|
| 287 |
-
|
|
|
|
|
|
|
| 288 |
if isinstance(eps, dict) and eps.get("value"):
|
| 289 |
await emit_metric(
|
| 290 |
progress_callback, source, "EPS", eps["value"],
|
|
@@ -292,8 +303,18 @@ async def _extract_and_emit_metrics(
|
|
| 292 |
fiscal_year=eps.get("fiscal_year"),
|
| 293 |
form=eps.get("form")
|
| 294 |
)
|
|
|
|
|
|
|
|
|
|
| 295 |
debt_to_equity = debt.get("debt_to_equity")
|
| 296 |
-
if debt_to_equity is not None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
await emit_metric(progress_callback, source, "debt_to_equity", debt_to_equity)
|
| 298 |
|
| 299 |
elif source == "volatility":
|
|
|
|
| 270 |
return
|
| 271 |
|
| 272 |
if source == "financials":
|
| 273 |
+
# Result IS the financials data directly (not nested under "financials" key)
|
| 274 |
+
# Revenue has temporal data from SEC EDGAR
|
| 275 |
+
revenue = result.get("revenue") or {}
|
|
|
|
| 276 |
if isinstance(revenue, dict) and revenue.get("value"):
|
| 277 |
await emit_metric(
|
| 278 |
progress_callback, source, "revenue", revenue["value"],
|
|
|
|
| 280 |
fiscal_year=revenue.get("fiscal_year"),
|
| 281 |
form=revenue.get("form")
|
| 282 |
)
|
| 283 |
+
|
| 284 |
+
# Net margin - now a dict with temporal data
|
| 285 |
+
net_margin = result.get("net_margin_pct") or result.get("net_margin")
|
| 286 |
+
if isinstance(net_margin, dict) and net_margin.get("value") is not None:
|
| 287 |
+
await emit_metric(
|
| 288 |
+
progress_callback, source, "net_margin", net_margin["value"],
|
| 289 |
+
end_date=net_margin.get("end_date"),
|
| 290 |
+
fiscal_year=net_margin.get("fiscal_year"),
|
| 291 |
+
form=net_margin.get("form")
|
| 292 |
+
)
|
| 293 |
+
elif isinstance(net_margin, (int, float)):
|
| 294 |
+
# Fallback for old format (plain number)
|
| 295 |
await emit_metric(progress_callback, source, "net_margin", net_margin)
|
| 296 |
+
|
| 297 |
+
# EPS has temporal data
|
| 298 |
+
eps = result.get("eps") or {}
|
| 299 |
if isinstance(eps, dict) and eps.get("value"):
|
| 300 |
await emit_metric(
|
| 301 |
progress_callback, source, "EPS", eps["value"],
|
|
|
|
| 303 |
fiscal_year=eps.get("fiscal_year"),
|
| 304 |
form=eps.get("form")
|
| 305 |
)
|
| 306 |
+
|
| 307 |
+
# Debt metrics are in the debt sub-object from fetch_debt_metrics
|
| 308 |
+
debt = result.get("debt") or {}
|
| 309 |
debt_to_equity = debt.get("debt_to_equity")
|
| 310 |
+
if isinstance(debt_to_equity, dict) and debt_to_equity.get("value") is not None:
|
| 311 |
+
await emit_metric(
|
| 312 |
+
progress_callback, source, "debt_to_equity", debt_to_equity["value"],
|
| 313 |
+
end_date=debt_to_equity.get("end_date"),
|
| 314 |
+
fiscal_year=debt_to_equity.get("fiscal_year"),
|
| 315 |
+
form=debt_to_equity.get("form")
|
| 316 |
+
)
|
| 317 |
+
elif isinstance(debt_to_equity, (int, float)):
|
| 318 |
await emit_metric(progress_callback, source, "debt_to_equity", debt_to_equity)
|
| 319 |
|
| 320 |
elif source == "volatility":
|