vn6295337 Claude Opus 4.5 commited on
Commit
fbe5ac1
·
1 Parent(s): e7b767d

Fix temporal data for calculated metrics (margins, debt_to_equity)

Browse files

financials-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 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 = round((gross_profit["value"] / revenue["value"]) * 100, 2)
 
 
 
261
 
262
  operating_margin = None
263
  if revenue and operating_income and revenue["value"] and operating_income["value"]:
264
- operating_margin = round((operating_income["value"] / revenue["value"]) * 100, 2)
 
 
 
265
 
266
  net_margin = None
267
  if revenue and net_income and revenue["value"] and net_income["value"]:
268
- net_margin = round((net_income["value"] / revenue["value"]) * 100, 2)
 
 
 
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 = round(debt_val / equity_val, 2)
 
 
 
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
- d_to_e = debt.get("debt_to_equity")
 
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
- net_margin = financials.get("net_margin_pct")
 
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
- op_margin = financials.get("operating_margin_pct")
 
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
- d_to_e = debt.get("debt_to_equity")
 
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 = result.get("financials") or {}
274
- debt = result.get("debt") or {}
275
- # Extract temporal data with metrics
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
- net_margin = financials.get("net_margin") or financials.get("net_margin_pct")
285
- if net_margin is not None:
 
 
 
 
 
 
 
 
 
 
286
  await emit_metric(progress_callback, source, "net_margin", net_margin)
287
- eps = financials.get("eps") or {}
 
 
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":