vn6295337 Claude Opus 4.5 commited on
Commit
ab2f4fb
·
1 Parent(s): 5ba49c2

Feat: Industry-specific metrics via SIC code detection + company address

Browse files

- Add SIC-to-sector mapping for 13 industries (config.py)
- Add industry-specific XBRL concept mappings for:
Insurance, Banks, REITs, Oil & Gas, Utilities, Technology,
Healthcare, Retail, Financials, Industrials, Transportation,
Materials, Mining
- Add 60+ industry-specific fields to ParsedFinancials schema
- Extract industry metrics based on detected sector (parser.py)
- Add business_address extraction from SEC EDGAR submissions
- Include company info (with address) in get_all_sources_fundamentals
- Add Company Info section to E2E test report

Tested with CVX (Oil & Gas), V (Financials), L (Insurance)

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

docs/mcp_test_report_CVX.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MCP E2E Test Report: Chevron Corporation (CVX)
2
+
3
+ ## Summary
4
+
5
+ | S/N | MCP | Status | Expected | Actual | Duration | Errors | Warnings |
6
+ |-----|-----|--------|----------|--------|----------|--------|----------|
7
+ | 1 | fundamentals | PASS | 9 | 11 | 12114ms | - | - |
8
+ | 2 | valuation | PASS | 11 | 11 | 8105ms | - | - |
9
+ | 3 | volatility | PASS | 5 | 5 | 5789ms | - | - |
10
+ | 4 | macro | PASS | 4 | 4 | 7236ms | - | - |
11
+ | 5 | news | PASS | - | 4 | 6433ms | - | - |
12
+ | 6 | sentiment | PASS | - | 55 | 5084ms | - | - |
13
+
14
+ ---
15
+
16
+ ## Quantitative Data
17
+
18
+ | S/N | Metric | Value | Data Type | As Of | Source | Category |
19
+ |-----|--------|-------|-----------|-------|--------|----------|
20
+ | 1 | revenue | 193414000000 | FY | 2024-12-31 | SEC EDGAR | Fundamentals |
21
+ | 2 | net_income | 17661000000 | FY | 2024-12-31 | SEC EDGAR | Fundamentals |
22
+ | 3 | net_margin_pct | 9.13 | FY | 2024-12-31 | SEC EDGAR | Fundamentals |
23
+ | 4 | total_assets | 256938000000 | FY | 2024-12-31 | SEC EDGAR | Fundamentals |
24
+ | 5 | total_liabilities | 103781000000 | FY | 2024-12-31 | SEC EDGAR | Fundamentals |
25
+ | 6 | stockholders_equity | 152318000000 | FY | 2024-12-31 | SEC EDGAR | Fundamentals |
26
+ | 7 | oil_gas_revenue | 193414000000 | FY | 2024-12-31 | SEC EDGAR | Fundamentals |
27
+ | 8 | depletion | 17282000000 | FY | 2024-12-31 | SEC EDGAR | Fundamentals |
28
+ | 9 | total_debt | 41543999488 | Point-in-time | 2025-09-30 | Yahoo Finance | Fundamentals |
29
+ | 10 | operating_cash_flow | 31844999168 | TTM | 2025-09-30 | Yahoo Finance | Fundamentals |
30
+ | 11 | free_cash_flow | 15743875072 | TTM | 2025-09-30 | Yahoo Finance | Fundamentals |
31
+ | 12 | current_price | 162.11 | - | 2026-01-12 | yahoo_finance | Valuation |
32
+ | 13 | market_cap | 326622871552.0 | - | 2026-01-12 | yahoo_finance | Valuation |
33
+ | 14 | enterprise_value | 365985988608.0 | - | 2026-01-12 | yahoo_finance | Valuation |
34
+ | 15 | trailing_pe | 22.76826 | - | 2026-01-12 | yahoo_finance | Valuation |
35
+ | 16 | forward_pe | 22.051102 | - | 2026-01-12 | yahoo_finance | Valuation |
36
+ | 17 | ps_ratio | 1.73103 | - | 2026-01-12 | yahoo_finance | Valuation |
37
+ | 18 | pb_ratio | 1.7193798 | - | 2026-01-12 | yahoo_finance | Valuation |
38
+ | 19 | trailing_peg | 3.1673 | - | 2026-01-12 | yahoo_finance | Valuation |
39
+ | 20 | forward_peg | - | - | 2026-01-12 | yahoo_finance | Valuation |
40
+ | 21 | earnings_growth | -0.266 | - | 2026-01-12 | yahoo_finance | Valuation |
41
+ | 22 | revenue_growth | -0.014 | - | 2026-01-12 | yahoo_finance | Valuation |
42
+ | 23 | vix | 15.45 | Daily | 2026-01-08 | FRED (Federal Reserve) | Volatility |
43
+ | 24 | vxn | 20.15 | Daily | 2026-01-08 | FRED (Federal Reserve) | Volatility |
44
+ | 25 | beta | 0.683 | 1Y | 2026-01-09 | Calculated from Yahoo Finance data | Volatility |
45
+ | 26 | historical_volatility | 27.6 | 30D | 2026-01-09 | Calculated from Yahoo Finance data | Volatility |
46
+ | 27 | implied_volatility | 30.0 | Forward | 2026-01-12 | Market Average (estimated) | Volatility |
47
+ | 28 | gdp_growth | 4.3 | Quarterly | 2025Q3 | BEA (Bureau of Economic Analysis) | Macro |
48
+ | 29 | interest_rate | 3.72 | Monthly | 2025-12-01 | FRED (Federal Reserve) | Macro |
49
+ | 30 | cpi_inflation | 2.74 | Monthly | 2025-November | BLS (Bureau of Labor Statistics) | Macro |
50
+ | 31 | unemployment | 4.4 | Monthly | 2025-December | BLS (Bureau of Labor Statistics) | Macro |
51
+
52
+ ---
53
+
54
+ ## Qualitative Data
55
+
56
+ | S/N | Title | Date | Source | Subreddit | URL | Category |
57
+ |-----|-------|------|--------|-----------|-----|----------|
58
+ | 1 | Chevron Corporation (CVX) Latest Stock News & Headlines | - | Tavily | - | [Link](https://finance.yahoo.com/quote/CVX/news/) | News |
59
+ | 2 | CVX: Chevron Corp - Stock Price, Quote and News | - | Tavily | - | [Link](https://www.cnbc.com/quotes/CVX) | News |
60
+ | 3 | CVX Chevron Corporation Stock Price & Overview | - | Tavily | - | [Link](https://seekingalpha.com/symbol/CVX) | News |
61
+ | 4 | Chevron - CVX - Stock Price & News | - | Tavily | - | [Link](https://www.fool.com/quote/nyse/cvx/) | News |
62
+ | 5 | Trump Pushes Venezuela Oil Investment as Political Risks Loom | 2026-01-12 | Finnhub | - | [Link](https://finnhub.io/api/news?id=4bd9554c63299a6d185eb386ac66113a65a3c2142538b39c2e37d66b773dba22) | Sentiment |
63
+ | 6 | Chevron Corporation's (NYSE:CVX) Stock On An Uptrend: Could Fundamentals Be Driv | 2026-01-12 | Finnhub | - | [Link](https://finnhub.io/api/news?id=e73167fa7eed0ebaa552782f612d05724c9bcafa6fd13b2a1ebc2cd040383b13) | Sentiment |
64
+ | 7 | Trump's magic number in Venezuela is oil at $50 per barrel | 2026-01-12 | Finnhub | - | [Link](https://finnhub.io/api/news?id=00eea749a137e2ca61c5570e7caf68884968914395d224e7cf5a9b3402ffbf48) | Sentiment |
65
+ | 8 | Trump ‘Inclined’ to Keep Exxon Out of Venezuela | 2026-01-12 | Finnhub | - | [Link](https://finnhub.io/api/news?id=e8a561541ca1dd06a87ce7602ca45dde4b4f6c902655bb814d58614f86224f66) | Sentiment |
66
+ | 9 | Energy Stocks: Winners And Losers At The Start Of 2026 | 2026-01-11 | Finnhub | - | [Link](https://finnhub.io/api/news?id=fc0a691fdf514129ccf8302385630e7864f71b5a93797d8bd6dfc7a3f95eb1d6) | Sentiment |
67
+ | 10 | Energy Secretary Says at Least a Dozen Oil Companies Eager for Venezuela | 2026-01-11 | Finnhub | - | [Link](https://finnhub.io/api/news?id=ac097cc854e23441d94bba402ebd895d68fe4209ee559a614a813bbdc892d913) | Sentiment |
68
+ | 11 | Energy Is Still My No. 1 Buy - Even With Venezuela, Politics, And Everything Els | 2026-01-11 | Finnhub | - | [Link](https://finnhub.io/api/news?id=d6fee5c4b9143c17c124b39b2089f158e9763ef335413be7c41afa0f3ecfa69c) | Sentiment |
69
+ | 12 | DLN: Diversified Large Value ETF With Risk Screening | 2026-01-11 | Finnhub | - | [Link](https://finnhub.io/api/news?id=f22b9cd12374143fe7f0d9443a755c3efd39b084c1d28926cea65f46dc33a445) | Sentiment |
70
+ | 13 | Jim Mellon Says Venezuela's Oil Recovery Is 5+ Years Away, But US Refiners Could | 2026-01-10 | Finnhub | - | [Link](https://finnhub.io/api/news?id=0ca15289a90e3799333ef758956f7336c1d7afb55a6c734b5cb534ff3aab7a4e) | Sentiment |
71
+ | 14 | Can Chevron Stock Hit $205 in 2026? | 2026-01-10 | Finnhub | - | [Link](https://finnhub.io/api/news?id=20d3d06abcd6c11df1288a9eec97e52017ecabd73d7d9600b21b19d1b344840f) | Sentiment |
docs/mcp_test_report_V.md ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MCP E2E Test Report: Visa Inc. (V)
2
+
3
+ ## Summary
4
+
5
+ | S/N | MCP | Status | Expected | Actual | Duration | Errors | Warnings |
6
+ |-----|-----|--------|----------|--------|----------|--------|----------|
7
+ | 1 | fundamentals | PASS | 9 | 11 | 26990ms | - | - |
8
+ | 2 | valuation | PASS | 11 | 11 | 8717ms | - | - |
9
+ | 3 | volatility | PASS | 5 | 5 | 5323ms | - | - |
10
+ | 4 | macro | PASS | 4 | 4 | 7508ms | - | - |
11
+ | 5 | news | PASS | - | 6 | 4933ms | - | - |
12
+ | 6 | sentiment | PASS | - | 56 | 5479ms | - | - |
13
+
14
+ ---
15
+
16
+ ## Company Info
17
+
18
+ | Field | Value |
19
+ |-------|-------|
20
+ | Name | VISA INC. |
21
+ | CIK | 0001403161 |
22
+ | SIC | 7389 (Services-Business Services, NEC) |
23
+ | State | DE |
24
+ | Fiscal Year End | 0930 |
25
+ | Address | P.O. BOX 8999 |
26
+ | | SAN FRANCISCO, CA 94128-8999 |
27
+
28
+ ---
29
+
30
+ ## Quantitative Data
31
+
32
+ | S/N | Metric | Value | Data Type | As Of | Source | Category |
33
+ |-----|--------|-------|-----------|-------|--------|----------|
34
+ | 1 | revenue | 40000000000 | FY | 2025-09-30 | SEC EDGAR | Fundamentals |
35
+ | 2 | net_income | 20058000000 | FY | 2025-09-30 | SEC EDGAR | Fundamentals |
36
+ | 3 | net_margin_pct | 50.14 | FY | 2025-09-30 | SEC EDGAR | Fundamentals |
37
+ | 4 | total_assets | 99627000000 | FY | 2025-09-30 | SEC EDGAR | Fundamentals |
38
+ | 5 | total_liabilities | 61718000000 | FY | 2025-09-30 | SEC EDGAR | Fundamentals |
39
+ | 6 | stockholders_equity | 26437000000 | FY | 2011-09-30 | SEC EDGAR | Fundamentals |
40
+ | 7 | deferred_revenue | 81000000 | FY | 2015-09-30 | SEC EDGAR | Fundamentals |
41
+ | 8 | goodwill | 19879000000 | FY | 2025-09-30 | SEC EDGAR | Fundamentals |
42
+ | 9 | total_debt | 26083999744 | Point-in-time | 2025-09-30 | Yahoo Finance | Fundamentals |
43
+ | 10 | operating_cash_flow | 23058999296 | TTM | 2025-09-30 | Yahoo Finance | Fundamentals |
44
+ | 11 | free_cash_flow | 20072873984 | TTM | 2025-09-30 | Yahoo Finance | Fundamentals |
45
+ | 12 | current_price | 339.78 | - | 2026-01-12 | yahoo_finance | Valuation |
46
+ | 13 | market_cap | 655898312704.0 | - | 2026-01-12 | yahoo_finance | Valuation |
47
+ | 14 | enterprise_value | 677386649600.0 | - | 2026-01-12 | yahoo_finance | Valuation |
48
+ | 15 | trailing_pe | 33.287148 | - | 2026-01-12 | yahoo_finance | Valuation |
49
+ | 16 | forward_pe | 23.56721 | - | 2026-01-12 | yahoo_finance | Valuation |
50
+ | 17 | ps_ratio | 16.393513 | - | 2026-01-12 | yahoo_finance | Valuation |
51
+ | 18 | pb_ratio | 17.534918 | - | 2026-01-12 | yahoo_finance | Valuation |
52
+ | 19 | trailing_peg | 1.9228 | - | 2026-01-12 | yahoo_finance | Valuation |
53
+ | 20 | forward_peg | - | - | 2026-01-12 | yahoo_finance | Valuation |
54
+ | 21 | earnings_growth | -0.014 | - | 2026-01-12 | yahoo_finance | Valuation |
55
+ | 22 | revenue_growth | 0.115 | - | 2026-01-12 | yahoo_finance | Valuation |
56
+ | 23 | vix | 14.49 | Daily | 2026-01-09 | FRED (Federal Reserve) | Volatility |
57
+ | 24 | vxn | 19.06 | Daily | 2026-01-09 | FRED (Federal Reserve) | Volatility |
58
+ | 25 | beta | 0.787 | 1Y | 2026-01-12 | Calculated from Yahoo Finance data | Volatility |
59
+ | 26 | historical_volatility | 23.82 | 30D | 2026-01-12 | Calculated from Yahoo Finance data | Volatility |
60
+ | 27 | implied_volatility | 30.0 | Forward | 2026-01-12 | Market Average (estimated) | Volatility |
61
+ | 28 | gdp_growth | 4.3 | Quarterly | 2025Q3 | BEA (Bureau of Economic Analysis) | Macro |
62
+ | 29 | interest_rate | 3.72 | Monthly | 2025-12-01 | FRED (Federal Reserve) | Macro |
63
+ | 30 | cpi_inflation | 2.74 | Monthly | 2025-November | BLS (Bureau of Labor Statistics) | Macro |
64
+ | 31 | unemployment | 4.4 | Monthly | 2025-December | BLS (Bureau of Labor Statistics) | Macro |
65
+
66
+ ---
67
+
68
+ ## Qualitative Data
69
+
70
+ | S/N | Title | Date | Source | Subreddit | URL | Category |
71
+ |-----|-------|------|--------|-----------|-----|----------|
72
+ | 1 | Big Tech stocks are getting cheaper, and that could mean gains of up to 60% | 2025-12-16 | MarketWatch | - | [Link](https://www.marketwatch.com/story/big-tech-stocks-are-getting-cheaper-and-that-could-mean-gains-of-up-to-60-fdf1b70c) | News |
73
+ | 2 | Dow, S&P 500 end at records because investors feel good about the economy — beyo | 2025-12-11 | MarketWatch | - | [Link](https://www.marketwatch.com/story/dow-s-p-500-end-at-records-because-investors-feel-good-about-the-economy-beyond-the-ai-boom-0dcad0b9) | News |
74
+ | 3 | Visa Inc. (V) Stock Price, News, Quote & History | - | Tavily | - | [Link](https://ca.finance.yahoo.com/quote/V/) | News |
75
+ | 4 | V: Visa Inc - Stock Price, Quote and News | - | Tavily | - | [Link](https://www.cnbc.com/quotes/V) | News |
76
+ | 5 | Is Visa Inc. (V) One of the Best Major Stocks to Invest in ... | - | Tavily | - | [Link](https://finance.yahoo.com/news/visa-inc-v-one-best-092151784.html) | News |
77
+ | 6 | Visa Inc. (V) Stock Price, Quote, News & Analysis | - | Tavily | - | [Link](https://seekingalpha.com/symbol/V) | News |
78
+ | 7 | Capital One, Credit Cards Dive As Trump Aims To Cap Interest Rates. | 2026-01-12 | Finnhub | - | [Link](https://finnhub.io/api/news?id=f14f8c1ccfdda6a9068faa37a8dde58ea9101ed9679737fd209638d779a84143) | Sentiment |
79
+ | 8 | JPMorgan, Visa Stocks Fall After Trump Calls for Credit-Card Rate Cap | 2026-01-12 | Finnhub | - | [Link](https://finnhub.io/api/news?id=dfaff595a6a512f56a475e0d1b62e7f95f5c4a7c8a793000929d7bb6d2072e98) | Sentiment |
80
+ | 9 | Stocks Fall Pre-Bell as Fed Chair Powell Faces Department of Justice Probe | 2026-01-12 | Finnhub | - | [Link](https://finnhub.io/api/news?id=dc954fbc0276d687ba5f759a9f5a19458cb9e6ac646bc4250440abbdd840b7f0) | Sentiment |
81
+ | 10 | Latest News In Digital Payment - Euronet Expands Through Strategic CrediaBank Pa | 2026-01-12 | Finnhub | - | [Link](https://finnhub.io/api/news?id=a80fd83da78d5f30d43512e15e0dd3fa414afc813a8457c405db530fc1f1c884) | Sentiment |
82
+ | 11 | FIS Launches Industry-First Offering Enabling Banks to Lead and Scale in Agentic | 2026-01-12 | Finnhub | - | [Link](https://finnhub.io/api/news?id=a96f73e317961b213a9ca0f890d2b14b55ff554dc426401b7df31bf50de9de10) | Sentiment |
83
+ | 12 | Major credit card stocks slide after Trump comments on credit card rates | 2026-01-12 | Finnhub | - | [Link](https://finnhub.io/api/news?id=83c74a108744a4273876ed809513a71ef3db9d71ec403ecf36a52f0540cb584f) | Sentiment |
84
+ | 13 | If I Were Starting A Dividend Portfolio In 2026, Here's How I Would Invest | 2026-01-12 | Finnhub | - | [Link](https://finnhub.io/api/news?id=32f4909f469af6dff4c17103f9119e7f14fce514a3beabf244b99a535852eee7) | Sentiment |
85
+ | 14 | 2 Top Dividend Stocks I'd Own Over the Next Decade | 2026-01-11 | Finnhub | - | [Link](https://finnhub.io/api/news?id=5bbdac3350f76a959bd87fa2e497cc760a4b6bb20d30a36f8af8b1cd67ccefd2) | Sentiment |
86
+ | 15 | 3 Dividend Stocks to Buy in 2026 and Hold Forever | 2026-01-11 | Finnhub | - | [Link](https://finnhub.io/api/news?id=60600fbee5d92497ffb0a1450483cacc710a22c0be92f8454b9b9772023b4ca8) | Sentiment |
87
+ | 16 | Does Trump’s 10% Credit Card Rate Cap Make Visa and Mastercard a Buy? | 2026-01-10 | Finnhub | - | [Link](https://finnhub.io/api/news?id=b30fc6d903d414f158b9367342c05ed0f355815db628416a2118ae29aa55edf3) | Sentiment |
mcp-servers/fundamentals-basket/config.py CHANGED
@@ -141,3 +141,260 @@ INSTANCE_PORTS = [8001, 8002, 8003]
141
 
142
  # Instance identification
143
  INSTANCE_ID = os.getenv("INSTANCE_ID", f"financials-default")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
  # Instance identification
143
  INSTANCE_ID = os.getenv("INSTANCE_ID", f"financials-default")
144
+
145
+ # =============================================================================
146
+ # INDUSTRY CLASSIFICATION (SIC Code Mapping)
147
+ # =============================================================================
148
+
149
+ # Specific 4-digit SIC codes that need special handling
150
+ SIC_SPECIFIC_MAP = {
151
+ "6798": "REAL_ESTATE", # Real Estate Investment Trusts (REITs)
152
+ }
153
+
154
+ # First 2 digits of SIC → Sector
155
+ SIC_SECTOR_MAP = {
156
+ # Financials
157
+ "60": "BANKS", # Depository Institutions
158
+ "61": "BANKS", # Non-depository Credit
159
+ "62": "FINANCIALS", # Securities & Commodities
160
+ "63": "INSURANCE", # Insurance Carriers
161
+ "64": "INSURANCE", # Insurance Agents
162
+ "65": "REAL_ESTATE", # Real Estate
163
+ "67": "FINANCIALS", # Holding & Investment (except 6798 REITs)
164
+
165
+ # Energy
166
+ "10": "MINING",
167
+ "12": "MINING",
168
+ "13": "OIL_GAS", # Oil & Gas Extraction
169
+ "29": "OIL_GAS", # Petroleum Refining
170
+ "49": "UTILITIES", # Electric, Gas, Sanitary
171
+
172
+ # Technology
173
+ "35": "TECHNOLOGY", # Industrial Machinery (computers)
174
+ "36": "TECHNOLOGY", # Electronic Equipment
175
+ "38": "TECHNOLOGY", # Instruments
176
+ "73": "TECHNOLOGY", # Business Services (software)
177
+
178
+ # Healthcare
179
+ "28": "HEALTHCARE", # Chemicals (pharma)
180
+ "80": "HEALTHCARE", # Health Services
181
+
182
+ # Consumer
183
+ "52": "RETAIL", # Building Materials Retail
184
+ "53": "RETAIL", # General Merchandise
185
+ "54": "RETAIL", # Food Stores
186
+ "56": "RETAIL", # Apparel
187
+ "57": "RETAIL", # Furniture
188
+ "58": "RETAIL", # Eating Places
189
+ "59": "RETAIL", # Misc Retail (incl. e-commerce)
190
+
191
+ # Industrials
192
+ "37": "INDUSTRIALS", # Transportation Equipment
193
+ "40": "TRANSPORTATION", # Railroad
194
+ "42": "TRANSPORTATION", # Trucking
195
+ "44": "TRANSPORTATION", # Water Transport
196
+ "45": "TRANSPORTATION", # Air Transport
197
+
198
+ # Materials
199
+ "14": "MATERIALS", # Mining (non-metallic)
200
+ "24": "MATERIALS", # Lumber
201
+ "26": "MATERIALS", # Paper
202
+ "32": "MATERIALS", # Stone, Clay, Glass
203
+ "33": "MATERIALS", # Primary Metals
204
+ }
205
+
206
+
207
+ def get_sector_from_sic(sic_code: str) -> str:
208
+ """Get sector classification from SIC code.
209
+
210
+ Checks 4-digit specific codes first (e.g., 6798 for REITs),
211
+ then falls back to 2-digit prefix mapping.
212
+ """
213
+ if not sic_code:
214
+ return "GENERAL"
215
+ sic_str = str(sic_code)
216
+
217
+ # Check 4-digit specific codes first
218
+ if sic_str in SIC_SPECIFIC_MAP:
219
+ return SIC_SPECIFIC_MAP[sic_str]
220
+
221
+ # Fall back to 2-digit prefix
222
+ prefix = sic_str[:2]
223
+ return SIC_SECTOR_MAP.get(prefix, "GENERAL")
224
+
225
+
226
+ # =============================================================================
227
+ # INDUSTRY-SPECIFIC XBRL CONCEPTS
228
+ # =============================================================================
229
+
230
+ # Insurance (SIC 63xx, 64xx)
231
+ INSURANCE_CONCEPTS = {
232
+ "premiums_earned": ["PremiumsEarnedNet", "PremiumsWrittenNet", "PremiumsEarned"],
233
+ "claims_incurred": ["PolicyholderBenefitsAndClaimsIncurredNet", "BenefitsLossesAndExpenses",
234
+ "PolicyholderBenefitsAndClaimsIncurredGross"],
235
+ "underwriting_income": ["UnderwritingIncomeLoss", "UnderwritingResultsPropertyCasualtyInsurance"],
236
+ "investment_income": ["NetInvestmentIncome", "InvestmentIncomeNet", "InvestmentIncomeInterestAndDividend"],
237
+ "loss_ratio": ["LossRatio", "InsuranceLossRatio"],
238
+ "policy_acquisition_costs": ["PolicyAcquisitionCosts", "DeferredPolicyAcquisitionCosts"],
239
+ }
240
+
241
+ # Banks (SIC 60xx, 61xx)
242
+ BANK_CONCEPTS = {
243
+ "net_interest_income": ["InterestIncomeExpenseNet", "NetInterestIncome",
244
+ "InterestIncomeExpenseAfterProvisionForLoanLoss"],
245
+ "provision_credit_losses": ["ProvisionForLoanLeaseAndOtherLosses", "ProvisionForCreditLosses",
246
+ "ProvisionForLoanAndLeaseLosses"],
247
+ "noninterest_income": ["NoninterestIncome"],
248
+ "noninterest_expense": ["NoninterestExpense"],
249
+ "net_loans": ["LoansAndLeasesReceivableNetReportedAmount", "LoansReceivableNet",
250
+ "LoansAndLeasesReceivableNetOfDeferredIncome"],
251
+ "deposits": ["Deposits", "DepositsDomestic"],
252
+ "tier1_capital_ratio": ["TierOneRiskBasedCapitalRatio", "CommonEquityTier1CapitalRatio"],
253
+ "net_charge_offs": ["AllowanceForLoanAndLeaseLossesWriteoffsNet", "ChargeOffsNet"],
254
+ }
255
+
256
+ # REITs (SIC 65xx, 67xx)
257
+ REIT_CONCEPTS = {
258
+ "rental_revenue": ["OperatingLeaseLeaseIncome", "RentalRevenue", "RevenueFromContractWithCustomerExcludingAssessedTax"],
259
+ "noi": ["NetOperatingIncome", "OperatingIncomeLoss"],
260
+ "ffo": ["FundsFromOperations", "FundsFromOperationsPerShare"],
261
+ "property_operating_expenses": ["CostOfPropertyRepairsAndMaintenance", "RealEstateTaxExpense"],
262
+ "occupancy_rate": ["OccupancyRate"],
263
+ "same_store_noi": ["SameStoreNetOperatingIncome"],
264
+ }
265
+
266
+ # Energy - Oil & Gas (SIC 13xx, 29xx)
267
+ ENERGY_OG_CONCEPTS = {
268
+ "oil_gas_revenue": ["RevenueFromContractWithCustomerExcludingAssessedTax", "Revenues",
269
+ "OilAndGasRevenue", "SalesRevenueNet"],
270
+ "production_expense": ["ProductionCosts", "LeaseOperatingExpense", "OilAndGasProductionExpense"],
271
+ "depletion": ["DepletionOfOilAndGasProperties", "DepreciationDepletionAndAmortization"],
272
+ "proved_reserves": ["ProvedDevelopedAndUndevelopedReserves", "ProvedReservesOil", "ProvedReservesGas"],
273
+ "exploration_expense": ["ExplorationExpense", "ExplorationCosts"],
274
+ "impairment": ["ImpairmentOfOilAndGasProperties", "AssetImpairmentCharges"],
275
+ }
276
+
277
+ # Utilities (SIC 49xx)
278
+ UTILITY_CONCEPTS = {
279
+ "electric_revenue": ["ElectricUtilityRevenue", "RegulatedElectricRevenue", "ElectricDomesticRevenue"],
280
+ "gas_revenue": ["GasUtilityRevenue", "RegulatedGasRevenue", "GasDomesticRevenue"],
281
+ "fuel_cost": ["FuelCosts", "CostOfFuel", "FuelExpense"],
282
+ "purchased_power_cost": ["CostOfPurchasedPower", "PurchasedPowerCost"],
283
+ "regulatory_assets": ["RegulatoryAssets"],
284
+ "regulatory_liabilities": ["RegulatoryLiabilities"],
285
+ "rate_base": ["UtilityPlantNet", "ElectricUtilityPlantNet"],
286
+ }
287
+
288
+ # Technology (SIC 35xx, 36xx, 38xx, 73xx)
289
+ TECHNOLOGY_CONCEPTS = {
290
+ "rd_expense": ["ResearchAndDevelopmentExpense", "ResearchAndDevelopmentExpenseExcludingAcquiredInProcessCost"],
291
+ "deferred_revenue": ["DeferredRevenue", "ContractWithCustomerLiability", "DeferredRevenueNoncurrent"],
292
+ "subscription_revenue": ["SubscriptionRevenue", "SaaSRevenue", "RecurringRevenue"],
293
+ "cost_of_revenue": ["CostOfRevenue", "CostOfGoodsAndServicesSold", "CostOfServices"],
294
+ "stock_compensation": ["ShareBasedCompensation", "AllocatedShareBasedCompensationExpense"],
295
+ "intangible_assets": ["IntangibleAssetsNetExcludingGoodwill", "FiniteLivedIntangibleAssetsNet"],
296
+ "goodwill": ["Goodwill"],
297
+ "acquired_ip": ["BusinessCombinationRecognizedIdentifiableAssetsAcquiredAndLiabilitiesAssumedIntangibleAssetsOtherThanGoodwill"],
298
+ }
299
+
300
+ # Healthcare / Pharmaceuticals (SIC 28xx, 80xx)
301
+ HEALTHCARE_CONCEPTS = {
302
+ "rd_expense": ["ResearchAndDevelopmentExpense", "ResearchAndDevelopmentExpenseExcludingAcquiredInProcessCost"],
303
+ "cost_of_revenue": ["CostOfRevenue", "CostOfGoodsAndServicesSold"],
304
+ "selling_general_admin": ["SellingGeneralAndAdministrativeExpense", "GeneralAndAdministrativeExpense"],
305
+ "acquired_iprd": ["ResearchAndDevelopmentInProcess", "AcquiredInProcessResearchAndDevelopment"],
306
+ "milestone_payments": ["CollaborativeArrangementMilestonePayments", "LicenseAndCollaborationRevenue"],
307
+ "inventory": ["InventoryNet", "InventoryFinishedGoodsNetOfReserves"],
308
+ "product_revenue": ["RevenueFromContractWithCustomerExcludingAssessedTax", "ProductSalesRevenue"],
309
+ "license_revenue": ["LicenseRevenue", "RoyaltyRevenue", "LicenseAndServicesRevenue"],
310
+ }
311
+
312
+ # Retail (SIC 52xx-59xx)
313
+ RETAIL_CONCEPTS = {
314
+ "cost_of_goods_sold": ["CostOfGoodsSold", "CostOfGoodsAndServicesSold", "CostOfRevenue"],
315
+ "inventory": ["InventoryNet", "RetailRelatedInventoryMerchandise"],
316
+ "selling_general_admin": ["SellingGeneralAndAdministrativeExpense"],
317
+ "store_count": ["NumberOfStores", "NumberOfRestaurants"],
318
+ "depreciation": ["DepreciationAndAmortization", "Depreciation"],
319
+ "lease_expense": ["OperatingLeaseExpense", "OperatingLeaseCost", "LeaseAndRentalExpense"],
320
+ "same_store_sales": ["SameStoreSales", "ComparableStoreSalesGrowth"],
321
+ "ecommerce_revenue": ["OnlineRevenue", "DigitalRevenue", "ECommerceRevenue"],
322
+ }
323
+
324
+ # Financials - Non-Bank (SIC 62xx, 67xx - Securities, Asset Management)
325
+ FINANCIALS_CONCEPTS = {
326
+ "advisory_fees": ["InvestmentAdvisoryFees", "AssetManagementFees", "AdvisoryFees"],
327
+ "assets_under_management": ["AssetsUnderManagement", "ClientAssetsUnderManagement"],
328
+ "trading_revenue": ["PrincipalTransactionsRevenue", "TradingRevenue", "GainLossOnInvestments"],
329
+ "commission_revenue": ["CommissionsAndFees", "BrokerageCommissionsRevenue"],
330
+ "compensation_expense": ["LaborAndRelatedExpense", "CompensationAndBenefitsExpense", "EmployeeBenefitsAndShareBasedCompensation"],
331
+ "investment_income": ["InvestmentIncomeNet", "NetInvestmentIncome"],
332
+ "performance_fees": ["IncentiveFeeRevenue", "PerformanceBasedFees"],
333
+ "fund_expenses": ["FundExpenses", "InvestmentCompanyGeneralPartnerAdvisoryService"],
334
+ }
335
+
336
+ # Industrials / Manufacturing (SIC 37xx)
337
+ INDUSTRIALS_CONCEPTS = {
338
+ "cost_of_goods_sold": ["CostOfGoodsSold", "CostOfGoodsAndServicesSold"],
339
+ "inventory": ["InventoryNet", "InventoryRawMaterialsAndSupplies", "InventoryWorkInProcess", "InventoryFinishedGoods"],
340
+ "depreciation": ["DepreciationAndAmortization", "Depreciation"],
341
+ "backlog": ["Backlog", "UnfilledOrders", "OrderBacklog"],
342
+ "capital_expenditure": ["PaymentsToAcquirePropertyPlantAndEquipment", "CapitalExpendituresIncurredButNotYetPaid"],
343
+ "property_plant_equipment": ["PropertyPlantAndEquipmentNet", "PropertyPlantAndEquipmentGross"],
344
+ "pension_expense": ["DefinedBenefitPlanNetPeriodicBenefitCost", "PensionAndOtherPostretirementBenefitExpense"],
345
+ "warranty_expense": ["ProductWarrantyExpense", "StandardProductWarrantyAccrual"],
346
+ }
347
+
348
+ # Transportation (SIC 40xx-45xx)
349
+ TRANSPORTATION_CONCEPTS = {
350
+ "operating_revenue": ["OperatingRevenue", "RevenueFromContractWithCustomerExcludingAssessedTax"],
351
+ "fuel_expense": ["AircraftFuelExpense", "FuelCosts", "FuelExpense"],
352
+ "labor_expense": ["SalariesWagesAndBenefits", "LaborAndRelatedExpense"],
353
+ "depreciation": ["DepreciationAndAmortization", "Depreciation"],
354
+ "maintenance_expense": ["AircraftMaintenanceMaterialsAndRepairs", "MaintenanceAndRepairsExpense"],
355
+ "revenue_passenger_miles": ["RevenuePassengerMiles", "PassengerRevenueMiles"],
356
+ "available_seat_miles": ["AvailableSeatMiles", "AvailableSeatMilesASMs"],
357
+ "load_factor": ["PassengerLoadFactor", "LoadFactor"],
358
+ "fleet_size": ["NumberOfAircraft", "FleetSize"],
359
+ }
360
+
361
+ # Materials (SIC 14xx, 24xx, 26xx, 32xx, 33xx)
362
+ MATERIALS_CONCEPTS = {
363
+ "cost_of_goods_sold": ["CostOfGoodsSold", "CostOfGoodsAndServicesSold"],
364
+ "inventory": ["InventoryNet", "InventoryRawMaterialsAndSupplies"],
365
+ "depreciation": ["DepreciationDepletionAndAmortization", "DepreciationAndAmortization"],
366
+ "energy_costs": ["UtilitiesExpense", "EnergyCosts", "NaturalGasPurchases"],
367
+ "environmental_liabilities": ["AccruedEnvironmentalLossContingencies", "EnvironmentalLossContingencyStatementOfFinancialPositionExtensibleListNotDisclosed"],
368
+ "property_plant_equipment": ["PropertyPlantAndEquipmentNet"],
369
+ "capital_expenditure": ["PaymentsToAcquirePropertyPlantAndEquipment"],
370
+ "raw_materials": ["InventoryRawMaterialsAndSupplies", "RawMaterials"],
371
+ }
372
+
373
+ # Mining (SIC 10xx, 12xx)
374
+ MINING_CONCEPTS = {
375
+ "mining_revenue": ["RevenueFromContractWithCustomerExcludingAssessedTax", "MiningRevenue", "Revenues"],
376
+ "cost_of_production": ["CostOfGoodsSold", "ProductionCosts", "MiningCosts"],
377
+ "depletion": ["DepletionOfMinesAndMineralDeposits", "DepreciationDepletionAndAmortization"],
378
+ "exploration_expense": ["ExplorationExpense", "MineralExplorationCosts", "ExplorationCosts"],
379
+ "reclamation_liabilities": ["AssetRetirementObligation", "MineReclamationAndClosingLiability"],
380
+ "mineral_reserves": ["ProvedAndProbableMineralReserves", "MineralReserves"],
381
+ "depreciation": ["DepreciationAndAmortization"],
382
+ "royalty_expense": ["RoyaltyExpense", "MiningRoyalties"],
383
+ }
384
+
385
+ # Map sector to concept dictionary
386
+ INDUSTRY_CONCEPTS = {
387
+ "INSURANCE": INSURANCE_CONCEPTS,
388
+ "BANKS": BANK_CONCEPTS,
389
+ "REAL_ESTATE": REIT_CONCEPTS,
390
+ "OIL_GAS": ENERGY_OG_CONCEPTS,
391
+ "UTILITIES": UTILITY_CONCEPTS,
392
+ "TECHNOLOGY": TECHNOLOGY_CONCEPTS,
393
+ "HEALTHCARE": HEALTHCARE_CONCEPTS,
394
+ "RETAIL": RETAIL_CONCEPTS,
395
+ "FINANCIALS": FINANCIALS_CONCEPTS,
396
+ "INDUSTRIALS": INDUSTRIALS_CONCEPTS,
397
+ "TRANSPORTATION": TRANSPORTATION_CONCEPTS,
398
+ "MATERIALS": MATERIALS_CONCEPTS,
399
+ "MINING": MINING_CONCEPTS,
400
+ }
mcp-servers/fundamentals-basket/models/schemas.py CHANGED
@@ -76,15 +76,123 @@ class ParsedFinancials:
76
  source: str = "Unknown"
77
  as_of: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d"))
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  def to_dict(self) -> dict:
80
  """Convert to dictionary for JSON serialization."""
81
  result = {
82
  "ticker": self.ticker,
83
  "source": self.source,
84
  "as_of": self.as_of,
 
85
  }
86
 
87
- # Add temporal metrics
 
 
 
88
  for field_name in [
89
  "revenue", "net_income", "gross_profit", "operating_income",
90
  "gross_margin_pct", "operating_margin_pct", "net_margin_pct",
@@ -94,6 +202,52 @@ class ParsedFinancials:
94
  if value:
95
  result[field_name] = value.to_dict() if isinstance(value, TemporalMetric) else value
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  return result
98
 
99
 
 
76
  source: str = "Unknown"
77
  as_of: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d"))
78
 
79
+ # Industry classification
80
+ sector: str = "GENERAL"
81
+ sic_code: str = ""
82
+
83
+ # Insurance-specific metrics (SIC 63xx, 64xx)
84
+ premiums_earned: Optional[TemporalMetric] = None
85
+ claims_incurred: Optional[TemporalMetric] = None
86
+ underwriting_income: Optional[TemporalMetric] = None
87
+ investment_income: Optional[TemporalMetric] = None
88
+ policy_acquisition_costs: Optional[TemporalMetric] = None
89
+
90
+ # Bank-specific metrics (SIC 60xx, 61xx)
91
+ net_interest_income: Optional[TemporalMetric] = None
92
+ provision_credit_losses: Optional[TemporalMetric] = None
93
+ noninterest_income: Optional[TemporalMetric] = None
94
+ noninterest_expense: Optional[TemporalMetric] = None
95
+ net_loans: Optional[TemporalMetric] = None
96
+ deposits: Optional[TemporalMetric] = None
97
+ tier1_capital_ratio: Optional[TemporalMetric] = None
98
+
99
+ # REIT-specific metrics (SIC 65xx, 67xx)
100
+ rental_revenue: Optional[TemporalMetric] = None
101
+ noi: Optional[TemporalMetric] = None
102
+ ffo: Optional[TemporalMetric] = None
103
+ property_operating_expenses: Optional[TemporalMetric] = None
104
+
105
+ # Energy/Oil & Gas-specific metrics (SIC 13xx, 29xx)
106
+ oil_gas_revenue: Optional[TemporalMetric] = None
107
+ production_expense: Optional[TemporalMetric] = None
108
+ depletion: Optional[TemporalMetric] = None
109
+ exploration_expense: Optional[TemporalMetric] = None
110
+ impairment: Optional[TemporalMetric] = None
111
+
112
+ # Utility-specific metrics (SIC 49xx)
113
+ electric_revenue: Optional[TemporalMetric] = None
114
+ gas_revenue: Optional[TemporalMetric] = None
115
+ fuel_cost: Optional[TemporalMetric] = None
116
+ regulatory_assets: Optional[TemporalMetric] = None
117
+ rate_base: Optional[TemporalMetric] = None
118
+
119
+ # Technology-specific metrics (SIC 35xx, 36xx, 38xx, 73xx)
120
+ rd_expense: Optional[TemporalMetric] = None
121
+ deferred_revenue: Optional[TemporalMetric] = None
122
+ subscription_revenue: Optional[TemporalMetric] = None
123
+ cost_of_revenue: Optional[TemporalMetric] = None
124
+ stock_compensation: Optional[TemporalMetric] = None
125
+ intangible_assets: Optional[TemporalMetric] = None
126
+ goodwill: Optional[TemporalMetric] = None
127
+ acquired_ip: Optional[TemporalMetric] = None
128
+
129
+ # Healthcare-specific metrics (SIC 28xx, 80xx)
130
+ selling_general_admin: Optional[TemporalMetric] = None
131
+ acquired_iprd: Optional[TemporalMetric] = None
132
+ milestone_payments: Optional[TemporalMetric] = None
133
+ inventory: Optional[TemporalMetric] = None
134
+ product_revenue: Optional[TemporalMetric] = None
135
+ license_revenue: Optional[TemporalMetric] = None
136
+
137
+ # Retail-specific metrics (SIC 52xx-59xx)
138
+ cost_of_goods_sold: Optional[TemporalMetric] = None
139
+ store_count: Optional[TemporalMetric] = None
140
+ depreciation: Optional[TemporalMetric] = None
141
+ lease_expense: Optional[TemporalMetric] = None
142
+ same_store_sales: Optional[TemporalMetric] = None
143
+ ecommerce_revenue: Optional[TemporalMetric] = None
144
+
145
+ # Financials-specific metrics (SIC 62xx, 67xx - non-bank)
146
+ advisory_fees: Optional[TemporalMetric] = None
147
+ assets_under_management: Optional[TemporalMetric] = None
148
+ trading_revenue: Optional[TemporalMetric] = None
149
+ commission_revenue: Optional[TemporalMetric] = None
150
+ compensation_expense: Optional[TemporalMetric] = None
151
+ performance_fees: Optional[TemporalMetric] = None
152
+ fund_expenses: Optional[TemporalMetric] = None
153
+
154
+ # Industrials-specific metrics (SIC 37xx)
155
+ backlog: Optional[TemporalMetric] = None
156
+ capital_expenditure: Optional[TemporalMetric] = None
157
+ property_plant_equipment: Optional[TemporalMetric] = None
158
+ pension_expense: Optional[TemporalMetric] = None
159
+ warranty_expense: Optional[TemporalMetric] = None
160
+
161
+ # Transportation-specific metrics (SIC 40xx-45xx)
162
+ operating_revenue: Optional[TemporalMetric] = None
163
+ fuel_expense: Optional[TemporalMetric] = None
164
+ labor_expense: Optional[TemporalMetric] = None
165
+ maintenance_expense: Optional[TemporalMetric] = None
166
+ revenue_passenger_miles: Optional[TemporalMetric] = None
167
+ available_seat_miles: Optional[TemporalMetric] = None
168
+ load_factor: Optional[TemporalMetric] = None
169
+ fleet_size: Optional[TemporalMetric] = None
170
+
171
+ # Materials-specific metrics (SIC 14xx, 24xx, 26xx, 32xx, 33xx)
172
+ energy_costs: Optional[TemporalMetric] = None
173
+ environmental_liabilities: Optional[TemporalMetric] = None
174
+ raw_materials: Optional[TemporalMetric] = None
175
+
176
+ # Mining-specific metrics (SIC 10xx, 12xx)
177
+ mining_revenue: Optional[TemporalMetric] = None
178
+ cost_of_production: Optional[TemporalMetric] = None
179
+ reclamation_liabilities: Optional[TemporalMetric] = None
180
+ mineral_reserves: Optional[TemporalMetric] = None
181
+ royalty_expense: Optional[TemporalMetric] = None
182
+
183
  def to_dict(self) -> dict:
184
  """Convert to dictionary for JSON serialization."""
185
  result = {
186
  "ticker": self.ticker,
187
  "source": self.source,
188
  "as_of": self.as_of,
189
+ "sector": self.sector,
190
  }
191
 
192
+ if self.sic_code:
193
+ result["sic_code"] = self.sic_code
194
+
195
+ # Add temporal metrics - universal fields
196
  for field_name in [
197
  "revenue", "net_income", "gross_profit", "operating_income",
198
  "gross_margin_pct", "operating_margin_pct", "net_margin_pct",
 
202
  if value:
203
  result[field_name] = value.to_dict() if isinstance(value, TemporalMetric) else value
204
 
205
+ # Add industry-specific fields (only if present)
206
+ industry_fields = [
207
+ # Insurance
208
+ "premiums_earned", "claims_incurred", "underwriting_income",
209
+ "investment_income", "policy_acquisition_costs",
210
+ # Banks
211
+ "net_interest_income", "provision_credit_losses", "noninterest_income",
212
+ "noninterest_expense", "net_loans", "deposits", "tier1_capital_ratio",
213
+ # REITs
214
+ "rental_revenue", "noi", "ffo", "property_operating_expenses",
215
+ # Energy
216
+ "oil_gas_revenue", "production_expense", "depletion",
217
+ "exploration_expense", "impairment",
218
+ # Utilities
219
+ "electric_revenue", "gas_revenue", "fuel_cost",
220
+ "regulatory_assets", "rate_base",
221
+ # Technology
222
+ "rd_expense", "deferred_revenue", "subscription_revenue", "cost_of_revenue",
223
+ "stock_compensation", "intangible_assets", "goodwill", "acquired_ip",
224
+ # Healthcare
225
+ "selling_general_admin", "acquired_iprd", "milestone_payments",
226
+ "inventory", "product_revenue", "license_revenue",
227
+ # Retail
228
+ "cost_of_goods_sold", "store_count", "depreciation", "lease_expense",
229
+ "same_store_sales", "ecommerce_revenue",
230
+ # Financials
231
+ "advisory_fees", "assets_under_management", "trading_revenue",
232
+ "commission_revenue", "compensation_expense", "performance_fees", "fund_expenses",
233
+ # Industrials
234
+ "backlog", "capital_expenditure", "property_plant_equipment",
235
+ "pension_expense", "warranty_expense",
236
+ # Transportation
237
+ "operating_revenue", "fuel_expense", "labor_expense", "maintenance_expense",
238
+ "revenue_passenger_miles", "available_seat_miles", "load_factor", "fleet_size",
239
+ # Materials
240
+ "energy_costs", "environmental_liabilities", "raw_materials",
241
+ # Mining
242
+ "mining_revenue", "cost_of_production", "reclamation_liabilities",
243
+ "mineral_reserves", "royalty_expense",
244
+ ]
245
+
246
+ for field_name in industry_fields:
247
+ value = getattr(self, field_name)
248
+ if value:
249
+ result[field_name] = value.to_dict() if isinstance(value, TemporalMetric) else value
250
+
251
  return result
252
 
253
 
mcp-servers/fundamentals-basket/services/orchestrator.py CHANGED
@@ -13,7 +13,7 @@ import logging
13
  from datetime import datetime
14
  from typing import Optional, Dict, Any
15
 
16
- from config import TOOL_TIMEOUT
17
  from models.schemas import (
18
  TemporalMetric,
19
  ParsedFinancials,
@@ -97,6 +97,7 @@ class OrchestratorService:
97
  "sic_description": submissions.get("sicDescription"),
98
  "state_of_incorporation": submissions.get("stateOfIncorporation"),
99
  "fiscal_year_end": submissions.get("fiscalYearEnd"),
 
100
  "source": "SEC EDGAR",
101
  }
102
 
@@ -141,8 +142,14 @@ class OrchestratorService:
141
  if not facts:
142
  return await self._get_yfinance_financials(ticker)
143
 
144
- # Parse financials
145
- financials = self.parser.parse_financials(facts, ticker)
 
 
 
 
 
 
146
  return financials.to_dict()
147
 
148
  except (APITimeoutError, CircuitOpenError) as e:
@@ -261,17 +268,20 @@ class OrchestratorService:
261
  if not facts:
262
  raise ValueError("No company facts available")
263
 
264
- # Parse all metrics
265
- financials = self.parser.parse_financials(facts, ticker)
 
 
 
 
 
 
266
  debt = self.parser.parse_debt_metrics(facts, ticker)
267
  cash_flow = self.parser.parse_cash_flow(facts, ticker)
268
 
269
  # Build SWOT
270
  swot = self.parser.build_swot_summary(financials, debt, cash_flow)
271
 
272
- # Get company info
273
- company_info = await self.get_company_info(ticker)
274
-
275
  # Build basket
276
  basket = FinancialsBasket(
277
  ticker=ticker,
@@ -396,16 +406,20 @@ class OrchestratorService:
396
  "data": yahoo_result.get("data"),
397
  }
398
 
 
 
 
399
  return {
400
  "group": "source_comparison",
401
  "ticker": ticker,
 
402
  "sources": sources,
403
  "source": "fundamentals-basket",
404
  "as_of": datetime.now().strftime("%Y-%m-%d"),
405
  }
406
 
407
  async def _get_sec_data_safe(self, ticker: str) -> Dict[str, Any]:
408
- """Get SEC data with error handling. Returns 6 universal metrics only."""
409
  try:
410
  cik = await self._get_cik_with_cache(ticker)
411
  if not cik:
@@ -415,7 +429,12 @@ class OrchestratorService:
415
  if not facts:
416
  return {"error": "No facts available", "source": "SEC EDGAR"}
417
 
418
- financials = self.parser.parse_financials(facts, ticker)
 
 
 
 
 
419
 
420
  # Helper to convert TemporalMetric to dict (include all temporal fields)
421
  def to_metric_dict(tm):
@@ -429,18 +448,112 @@ class OrchestratorService:
429
  "form": tm.form,
430
  }
431
 
432
- # Only 6 universal metrics (works across all industries)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  return {
434
  "source": "SEC EDGAR XBRL",
435
  "as_of": datetime.now().strftime("%Y-%m-%d"),
436
- "data": {
437
- "revenue": to_metric_dict(financials.revenue),
438
- "net_income": to_metric_dict(financials.net_income),
439
- "net_margin_pct": to_metric_dict(financials.net_margin_pct),
440
- "total_assets": to_metric_dict(financials.total_assets),
441
- "total_liabilities": to_metric_dict(financials.total_liabilities),
442
- "stockholders_equity": to_metric_dict(financials.stockholders_equity),
443
- },
444
  }
445
 
446
  except Exception as e:
 
13
  from datetime import datetime
14
  from typing import Optional, Dict, Any
15
 
16
+ from config import TOOL_TIMEOUT, get_sector_from_sic
17
  from models.schemas import (
18
  TemporalMetric,
19
  ParsedFinancials,
 
97
  "sic_description": submissions.get("sicDescription"),
98
  "state_of_incorporation": submissions.get("stateOfIncorporation"),
99
  "fiscal_year_end": submissions.get("fiscalYearEnd"),
100
+ "business_address": submissions.get("addresses", {}).get("business", {}),
101
  "source": "SEC EDGAR",
102
  }
103
 
 
142
  if not facts:
143
  return await self._get_yfinance_financials(ticker)
144
 
145
+ # Get company info for SIC-based sector detection
146
+ company_info = await self.get_company_info(ticker)
147
+ sic_code = company_info.get("sic", "")
148
+ sector = get_sector_from_sic(sic_code)
149
+ logger.info(f"Detected sector for {ticker}: {sector} (SIC: {sic_code})")
150
+
151
+ # Parse financials with industry-specific metrics
152
+ financials = self.parser.parse_financials(facts, ticker, sector=sector, sic_code=sic_code)
153
  return financials.to_dict()
154
 
155
  except (APITimeoutError, CircuitOpenError) as e:
 
268
  if not facts:
269
  raise ValueError("No company facts available")
270
 
271
+ # Get company info for SIC-based sector detection
272
+ company_info = await self.get_company_info(ticker)
273
+ sic_code = company_info.get("sic", "")
274
+ sector = get_sector_from_sic(sic_code)
275
+ logger.info(f"SEC Basket - Detected sector for {ticker}: {sector} (SIC: {sic_code})")
276
+
277
+ # Parse all metrics with industry-specific extraction
278
+ financials = self.parser.parse_financials(facts, ticker, sector=sector, sic_code=sic_code)
279
  debt = self.parser.parse_debt_metrics(facts, ticker)
280
  cash_flow = self.parser.parse_cash_flow(facts, ticker)
281
 
282
  # Build SWOT
283
  swot = self.parser.build_swot_summary(financials, debt, cash_flow)
284
 
 
 
 
285
  # Build basket
286
  basket = FinancialsBasket(
287
  ticker=ticker,
 
406
  "data": yahoo_result.get("data"),
407
  }
408
 
409
+ # Get company info for response (includes business_address)
410
+ company_info = await self.get_company_info(ticker)
411
+
412
  return {
413
  "group": "source_comparison",
414
  "ticker": ticker,
415
+ "company": company_info,
416
  "sources": sources,
417
  "source": "fundamentals-basket",
418
  "as_of": datetime.now().strftime("%Y-%m-%d"),
419
  }
420
 
421
  async def _get_sec_data_safe(self, ticker: str) -> Dict[str, Any]:
422
+ """Get SEC data with error handling. Returns universal + industry-specific metrics."""
423
  try:
424
  cik = await self._get_cik_with_cache(ticker)
425
  if not cik:
 
429
  if not facts:
430
  return {"error": "No facts available", "source": "SEC EDGAR"}
431
 
432
+ # Get company info for SIC-based sector detection
433
+ company_info = await self.get_company_info(ticker)
434
+ sic_code = company_info.get("sic", "")
435
+ sector = get_sector_from_sic(sic_code)
436
+
437
+ financials = self.parser.parse_financials(facts, ticker, sector=sector, sic_code=sic_code)
438
 
439
  # Helper to convert TemporalMetric to dict (include all temporal fields)
440
  def to_metric_dict(tm):
 
448
  "form": tm.form,
449
  }
450
 
451
+ # Universal metrics (works across all industries)
452
+ data = {
453
+ "revenue": to_metric_dict(financials.revenue),
454
+ "net_income": to_metric_dict(financials.net_income),
455
+ "net_margin_pct": to_metric_dict(financials.net_margin_pct),
456
+ "total_assets": to_metric_dict(financials.total_assets),
457
+ "total_liabilities": to_metric_dict(financials.total_liabilities),
458
+ "stockholders_equity": to_metric_dict(financials.stockholders_equity),
459
+ }
460
+
461
+ # Add industry-specific metrics if available
462
+ if sector == "INSURANCE":
463
+ data.update({
464
+ "premiums_earned": to_metric_dict(financials.premiums_earned),
465
+ "claims_incurred": to_metric_dict(financials.claims_incurred),
466
+ "underwriting_income": to_metric_dict(financials.underwriting_income),
467
+ "investment_income": to_metric_dict(financials.investment_income),
468
+ })
469
+ elif sector == "BANKS":
470
+ data.update({
471
+ "net_interest_income": to_metric_dict(financials.net_interest_income),
472
+ "provision_credit_losses": to_metric_dict(financials.provision_credit_losses),
473
+ "noninterest_income": to_metric_dict(financials.noninterest_income),
474
+ "deposits": to_metric_dict(financials.deposits),
475
+ })
476
+ elif sector == "REAL_ESTATE":
477
+ data.update({
478
+ "rental_revenue": to_metric_dict(financials.rental_revenue),
479
+ "noi": to_metric_dict(financials.noi),
480
+ "ffo": to_metric_dict(financials.ffo),
481
+ })
482
+ elif sector == "OIL_GAS":
483
+ data.update({
484
+ "oil_gas_revenue": to_metric_dict(financials.oil_gas_revenue),
485
+ "production_expense": to_metric_dict(financials.production_expense),
486
+ "depletion": to_metric_dict(financials.depletion),
487
+ })
488
+ elif sector == "UTILITIES":
489
+ data.update({
490
+ "electric_revenue": to_metric_dict(financials.electric_revenue),
491
+ "gas_revenue": to_metric_dict(financials.gas_revenue),
492
+ "fuel_cost": to_metric_dict(financials.fuel_cost),
493
+ })
494
+ elif sector == "TECHNOLOGY":
495
+ data.update({
496
+ "rd_expense": to_metric_dict(financials.rd_expense),
497
+ "deferred_revenue": to_metric_dict(financials.deferred_revenue),
498
+ "cost_of_revenue": to_metric_dict(financials.cost_of_revenue),
499
+ "goodwill": to_metric_dict(financials.goodwill),
500
+ })
501
+ elif sector == "HEALTHCARE":
502
+ data.update({
503
+ "rd_expense": to_metric_dict(financials.rd_expense),
504
+ "cost_of_revenue": to_metric_dict(financials.cost_of_revenue),
505
+ "inventory": to_metric_dict(financials.inventory),
506
+ "selling_general_admin": to_metric_dict(financials.selling_general_admin),
507
+ })
508
+ elif sector == "RETAIL":
509
+ data.update({
510
+ "cost_of_goods_sold": to_metric_dict(financials.cost_of_goods_sold),
511
+ "inventory": to_metric_dict(financials.inventory),
512
+ "selling_general_admin": to_metric_dict(financials.selling_general_admin),
513
+ "depreciation": to_metric_dict(financials.depreciation),
514
+ })
515
+ elif sector == "FINANCIALS":
516
+ data.update({
517
+ "advisory_fees": to_metric_dict(financials.advisory_fees),
518
+ "trading_revenue": to_metric_dict(financials.trading_revenue),
519
+ "compensation_expense": to_metric_dict(financials.compensation_expense),
520
+ "investment_income": to_metric_dict(financials.investment_income),
521
+ })
522
+ elif sector == "INDUSTRIALS":
523
+ data.update({
524
+ "cost_of_goods_sold": to_metric_dict(financials.cost_of_goods_sold),
525
+ "inventory": to_metric_dict(financials.inventory),
526
+ "backlog": to_metric_dict(financials.backlog),
527
+ "capital_expenditure": to_metric_dict(financials.capital_expenditure),
528
+ })
529
+ elif sector == "TRANSPORTATION":
530
+ data.update({
531
+ "operating_revenue": to_metric_dict(financials.operating_revenue),
532
+ "fuel_expense": to_metric_dict(financials.fuel_expense),
533
+ "labor_expense": to_metric_dict(financials.labor_expense),
534
+ "depreciation": to_metric_dict(financials.depreciation),
535
+ })
536
+ elif sector == "MATERIALS":
537
+ data.update({
538
+ "cost_of_goods_sold": to_metric_dict(financials.cost_of_goods_sold),
539
+ "inventory": to_metric_dict(financials.inventory),
540
+ "depreciation": to_metric_dict(financials.depreciation),
541
+ "capital_expenditure": to_metric_dict(financials.capital_expenditure),
542
+ })
543
+ elif sector == "MINING":
544
+ data.update({
545
+ "mining_revenue": to_metric_dict(financials.mining_revenue),
546
+ "cost_of_production": to_metric_dict(financials.cost_of_production),
547
+ "depletion": to_metric_dict(financials.depletion),
548
+ "exploration_expense": to_metric_dict(financials.exploration_expense),
549
+ })
550
+
551
  return {
552
  "source": "SEC EDGAR XBRL",
553
  "as_of": datetime.now().strftime("%Y-%m-%d"),
554
+ "sector": sector,
555
+ "sic_code": sic_code,
556
+ "data": data,
 
 
 
 
 
557
  }
558
 
559
  except Exception as e:
mcp-servers/fundamentals-basket/services/parser.py CHANGED
@@ -25,6 +25,21 @@ from config import (
25
  DEBT_TO_EQUITY_ELEVATED,
26
  DEBT_TO_EQUITY_LOW,
27
  RD_HIGH_INVESTMENT,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  )
29
  from models.schemas import (
30
  TemporalMetric,
@@ -322,7 +337,9 @@ class ParserService:
322
  def parse_financials(
323
  self,
324
  facts: Dict[str, Any],
325
- ticker: str
 
 
326
  ) -> ParsedFinancials:
327
  """
328
  Parse financial metrics from XBRL facts.
@@ -330,11 +347,13 @@ class ParserService:
330
  Args:
331
  facts: Company facts dict from SEC EDGAR
332
  ticker: Stock ticker symbol
 
 
333
 
334
  Returns:
335
- ParsedFinancials with all metrics
336
  """
337
- # Extract core metrics
338
  revenue = self.get_latest_value(facts, REVENUE_CONCEPTS)
339
  net_income = self.get_latest_value(facts, NET_INCOME_CONCEPTS)
340
  gross_profit = self.get_latest_value(facts, GROSS_PROFIT_CONCEPTS)
@@ -373,6 +392,50 @@ class ParserService:
373
  if revenue_growth_val is not None:
374
  revenue_growth_3yr = self.create_temporal_metric(revenue_growth_val, revenue)
375
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  return ParsedFinancials(
377
  ticker=ticker.upper(),
378
  revenue=revenue,
@@ -387,8 +450,171 @@ class ParserService:
387
  total_liabilities=total_liabilities,
388
  stockholders_equity=stockholders_equity,
389
  source="SEC EDGAR XBRL",
 
 
 
390
  )
391
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  def parse_debt_metrics(
393
  self,
394
  facts: Dict[str, Any],
 
25
  DEBT_TO_EQUITY_ELEVATED,
26
  DEBT_TO_EQUITY_LOW,
27
  RD_HIGH_INVESTMENT,
28
+ # Industry-specific concepts
29
+ INDUSTRY_CONCEPTS,
30
+ INSURANCE_CONCEPTS,
31
+ BANK_CONCEPTS,
32
+ REIT_CONCEPTS,
33
+ ENERGY_OG_CONCEPTS,
34
+ UTILITY_CONCEPTS,
35
+ TECHNOLOGY_CONCEPTS,
36
+ HEALTHCARE_CONCEPTS,
37
+ RETAIL_CONCEPTS,
38
+ FINANCIALS_CONCEPTS,
39
+ INDUSTRIALS_CONCEPTS,
40
+ TRANSPORTATION_CONCEPTS,
41
+ MATERIALS_CONCEPTS,
42
+ MINING_CONCEPTS,
43
  )
44
  from models.schemas import (
45
  TemporalMetric,
 
337
  def parse_financials(
338
  self,
339
  facts: Dict[str, Any],
340
+ ticker: str,
341
+ sector: str = "GENERAL",
342
+ sic_code: str = ""
343
  ) -> ParsedFinancials:
344
  """
345
  Parse financial metrics from XBRL facts.
 
347
  Args:
348
  facts: Company facts dict from SEC EDGAR
349
  ticker: Stock ticker symbol
350
+ sector: Industry sector (INSURANCE, BANKS, REAL_ESTATE, OIL_GAS, UTILITIES, GENERAL)
351
+ sic_code: SIC code from SEC EDGAR
352
 
353
  Returns:
354
+ ParsedFinancials with all metrics (universal + industry-specific)
355
  """
356
+ # Extract core metrics (universal)
357
  revenue = self.get_latest_value(facts, REVENUE_CONCEPTS)
358
  net_income = self.get_latest_value(facts, NET_INCOME_CONCEPTS)
359
  gross_profit = self.get_latest_value(facts, GROSS_PROFIT_CONCEPTS)
 
392
  if revenue_growth_val is not None:
393
  revenue_growth_3yr = self.create_temporal_metric(revenue_growth_val, revenue)
394
 
395
+ # Initialize industry-specific fields
396
+ industry_metrics = {}
397
+
398
+ # Extract industry-specific metrics based on sector
399
+ if sector == "INSURANCE":
400
+ industry_metrics = self._extract_insurance_metrics(facts)
401
+ logger.info(f"Extracted insurance metrics for {ticker}: {list(industry_metrics.keys())}")
402
+ elif sector == "BANKS":
403
+ industry_metrics = self._extract_bank_metrics(facts)
404
+ logger.info(f"Extracted bank metrics for {ticker}: {list(industry_metrics.keys())}")
405
+ elif sector == "REAL_ESTATE":
406
+ industry_metrics = self._extract_reit_metrics(facts)
407
+ logger.info(f"Extracted REIT metrics for {ticker}: {list(industry_metrics.keys())}")
408
+ elif sector == "OIL_GAS":
409
+ industry_metrics = self._extract_energy_metrics(facts)
410
+ logger.info(f"Extracted energy metrics for {ticker}: {list(industry_metrics.keys())}")
411
+ elif sector == "UTILITIES":
412
+ industry_metrics = self._extract_utility_metrics(facts)
413
+ logger.info(f"Extracted utility metrics for {ticker}: {list(industry_metrics.keys())}")
414
+ elif sector == "TECHNOLOGY":
415
+ industry_metrics = self._extract_technology_metrics(facts)
416
+ logger.info(f"Extracted technology metrics for {ticker}: {list(industry_metrics.keys())}")
417
+ elif sector == "HEALTHCARE":
418
+ industry_metrics = self._extract_healthcare_metrics(facts)
419
+ logger.info(f"Extracted healthcare metrics for {ticker}: {list(industry_metrics.keys())}")
420
+ elif sector == "RETAIL":
421
+ industry_metrics = self._extract_retail_metrics(facts)
422
+ logger.info(f"Extracted retail metrics for {ticker}: {list(industry_metrics.keys())}")
423
+ elif sector == "FINANCIALS":
424
+ industry_metrics = self._extract_financials_metrics(facts)
425
+ logger.info(f"Extracted financials metrics for {ticker}: {list(industry_metrics.keys())}")
426
+ elif sector == "INDUSTRIALS":
427
+ industry_metrics = self._extract_industrials_metrics(facts)
428
+ logger.info(f"Extracted industrials metrics for {ticker}: {list(industry_metrics.keys())}")
429
+ elif sector == "TRANSPORTATION":
430
+ industry_metrics = self._extract_transportation_metrics(facts)
431
+ logger.info(f"Extracted transportation metrics for {ticker}: {list(industry_metrics.keys())}")
432
+ elif sector == "MATERIALS":
433
+ industry_metrics = self._extract_materials_metrics(facts)
434
+ logger.info(f"Extracted materials metrics for {ticker}: {list(industry_metrics.keys())}")
435
+ elif sector == "MINING":
436
+ industry_metrics = self._extract_mining_metrics(facts)
437
+ logger.info(f"Extracted mining metrics for {ticker}: {list(industry_metrics.keys())}")
438
+
439
  return ParsedFinancials(
440
  ticker=ticker.upper(),
441
  revenue=revenue,
 
450
  total_liabilities=total_liabilities,
451
  stockholders_equity=stockholders_equity,
452
  source="SEC EDGAR XBRL",
453
+ sector=sector,
454
+ sic_code=sic_code,
455
+ **industry_metrics,
456
  )
457
 
458
+ # =========================================================================
459
+ # INDUSTRY-SPECIFIC EXTRACTION METHODS
460
+ # =========================================================================
461
+
462
+ def _extract_insurance_metrics(self, facts: Dict[str, Any]) -> Dict[str, Optional[TemporalMetric]]:
463
+ """Extract insurance-specific metrics from XBRL facts."""
464
+ return {
465
+ "premiums_earned": self.get_latest_value(facts, INSURANCE_CONCEPTS["premiums_earned"]),
466
+ "claims_incurred": self.get_latest_value(facts, INSURANCE_CONCEPTS["claims_incurred"]),
467
+ "underwriting_income": self.get_latest_value(facts, INSURANCE_CONCEPTS["underwriting_income"]),
468
+ "investment_income": self.get_latest_value(facts, INSURANCE_CONCEPTS["investment_income"]),
469
+ "policy_acquisition_costs": self.get_latest_value(facts, INSURANCE_CONCEPTS["policy_acquisition_costs"]),
470
+ }
471
+
472
+ def _extract_bank_metrics(self, facts: Dict[str, Any]) -> Dict[str, Optional[TemporalMetric]]:
473
+ """Extract bank-specific metrics from XBRL facts."""
474
+ return {
475
+ "net_interest_income": self.get_latest_value(facts, BANK_CONCEPTS["net_interest_income"]),
476
+ "provision_credit_losses": self.get_latest_value(facts, BANK_CONCEPTS["provision_credit_losses"]),
477
+ "noninterest_income": self.get_latest_value(facts, BANK_CONCEPTS["noninterest_income"]),
478
+ "noninterest_expense": self.get_latest_value(facts, BANK_CONCEPTS["noninterest_expense"]),
479
+ "net_loans": self.get_latest_value(facts, BANK_CONCEPTS["net_loans"]),
480
+ "deposits": self.get_latest_value(facts, BANK_CONCEPTS["deposits"]),
481
+ "tier1_capital_ratio": self.get_latest_value(facts, BANK_CONCEPTS["tier1_capital_ratio"], unit="pure"),
482
+ }
483
+
484
+ def _extract_reit_metrics(self, facts: Dict[str, Any]) -> Dict[str, Optional[TemporalMetric]]:
485
+ """Extract REIT-specific metrics from XBRL facts."""
486
+ return {
487
+ "rental_revenue": self.get_latest_value(facts, REIT_CONCEPTS["rental_revenue"]),
488
+ "noi": self.get_latest_value(facts, REIT_CONCEPTS["noi"]),
489
+ "ffo": self.get_latest_value(facts, REIT_CONCEPTS["ffo"]),
490
+ "property_operating_expenses": self.get_latest_value(facts, REIT_CONCEPTS["property_operating_expenses"]),
491
+ }
492
+
493
+ def _extract_energy_metrics(self, facts: Dict[str, Any]) -> Dict[str, Optional[TemporalMetric]]:
494
+ """Extract energy/oil & gas-specific metrics from XBRL facts."""
495
+ return {
496
+ "oil_gas_revenue": self.get_latest_value(facts, ENERGY_OG_CONCEPTS["oil_gas_revenue"]),
497
+ "production_expense": self.get_latest_value(facts, ENERGY_OG_CONCEPTS["production_expense"]),
498
+ "depletion": self.get_latest_value(facts, ENERGY_OG_CONCEPTS["depletion"]),
499
+ "exploration_expense": self.get_latest_value(facts, ENERGY_OG_CONCEPTS["exploration_expense"]),
500
+ "impairment": self.get_latest_value(facts, ENERGY_OG_CONCEPTS["impairment"]),
501
+ }
502
+
503
+ def _extract_utility_metrics(self, facts: Dict[str, Any]) -> Dict[str, Optional[TemporalMetric]]:
504
+ """Extract utility-specific metrics from XBRL facts."""
505
+ return {
506
+ "electric_revenue": self.get_latest_value(facts, UTILITY_CONCEPTS["electric_revenue"]),
507
+ "gas_revenue": self.get_latest_value(facts, UTILITY_CONCEPTS["gas_revenue"]),
508
+ "fuel_cost": self.get_latest_value(facts, UTILITY_CONCEPTS["fuel_cost"]),
509
+ "regulatory_assets": self.get_latest_value(facts, UTILITY_CONCEPTS["regulatory_assets"]),
510
+ "rate_base": self.get_latest_value(facts, UTILITY_CONCEPTS["rate_base"]),
511
+ }
512
+
513
+ def _extract_technology_metrics(self, facts: Dict[str, Any]) -> Dict[str, Optional[TemporalMetric]]:
514
+ """Extract technology-specific metrics from XBRL facts."""
515
+ return {
516
+ "rd_expense": self.get_latest_value(facts, TECHNOLOGY_CONCEPTS["rd_expense"]),
517
+ "deferred_revenue": self.get_latest_value(facts, TECHNOLOGY_CONCEPTS["deferred_revenue"]),
518
+ "subscription_revenue": self.get_latest_value(facts, TECHNOLOGY_CONCEPTS["subscription_revenue"]),
519
+ "cost_of_revenue": self.get_latest_value(facts, TECHNOLOGY_CONCEPTS["cost_of_revenue"]),
520
+ "stock_compensation": self.get_latest_value(facts, TECHNOLOGY_CONCEPTS["stock_compensation"]),
521
+ "intangible_assets": self.get_latest_value(facts, TECHNOLOGY_CONCEPTS["intangible_assets"]),
522
+ "goodwill": self.get_latest_value(facts, TECHNOLOGY_CONCEPTS["goodwill"]),
523
+ "acquired_ip": self.get_latest_value(facts, TECHNOLOGY_CONCEPTS["acquired_ip"]),
524
+ }
525
+
526
+ def _extract_healthcare_metrics(self, facts: Dict[str, Any]) -> Dict[str, Optional[TemporalMetric]]:
527
+ """Extract healthcare/pharma-specific metrics from XBRL facts."""
528
+ return {
529
+ "rd_expense": self.get_latest_value(facts, HEALTHCARE_CONCEPTS["rd_expense"]),
530
+ "cost_of_revenue": self.get_latest_value(facts, HEALTHCARE_CONCEPTS["cost_of_revenue"]),
531
+ "selling_general_admin": self.get_latest_value(facts, HEALTHCARE_CONCEPTS["selling_general_admin"]),
532
+ "acquired_iprd": self.get_latest_value(facts, HEALTHCARE_CONCEPTS["acquired_iprd"]),
533
+ "milestone_payments": self.get_latest_value(facts, HEALTHCARE_CONCEPTS["milestone_payments"]),
534
+ "inventory": self.get_latest_value(facts, HEALTHCARE_CONCEPTS["inventory"]),
535
+ "product_revenue": self.get_latest_value(facts, HEALTHCARE_CONCEPTS["product_revenue"]),
536
+ "license_revenue": self.get_latest_value(facts, HEALTHCARE_CONCEPTS["license_revenue"]),
537
+ }
538
+
539
+ def _extract_retail_metrics(self, facts: Dict[str, Any]) -> Dict[str, Optional[TemporalMetric]]:
540
+ """Extract retail-specific metrics from XBRL facts."""
541
+ return {
542
+ "cost_of_goods_sold": self.get_latest_value(facts, RETAIL_CONCEPTS["cost_of_goods_sold"]),
543
+ "inventory": self.get_latest_value(facts, RETAIL_CONCEPTS["inventory"]),
544
+ "selling_general_admin": self.get_latest_value(facts, RETAIL_CONCEPTS["selling_general_admin"]),
545
+ "store_count": self.get_latest_value(facts, RETAIL_CONCEPTS["store_count"], unit="pure"),
546
+ "depreciation": self.get_latest_value(facts, RETAIL_CONCEPTS["depreciation"]),
547
+ "lease_expense": self.get_latest_value(facts, RETAIL_CONCEPTS["lease_expense"]),
548
+ "same_store_sales": self.get_latest_value(facts, RETAIL_CONCEPTS["same_store_sales"], unit="pure"),
549
+ "ecommerce_revenue": self.get_latest_value(facts, RETAIL_CONCEPTS["ecommerce_revenue"]),
550
+ }
551
+
552
+ def _extract_financials_metrics(self, facts: Dict[str, Any]) -> Dict[str, Optional[TemporalMetric]]:
553
+ """Extract financials (non-bank) metrics from XBRL facts."""
554
+ return {
555
+ "advisory_fees": self.get_latest_value(facts, FINANCIALS_CONCEPTS["advisory_fees"]),
556
+ "assets_under_management": self.get_latest_value(facts, FINANCIALS_CONCEPTS["assets_under_management"]),
557
+ "trading_revenue": self.get_latest_value(facts, FINANCIALS_CONCEPTS["trading_revenue"]),
558
+ "commission_revenue": self.get_latest_value(facts, FINANCIALS_CONCEPTS["commission_revenue"]),
559
+ "compensation_expense": self.get_latest_value(facts, FINANCIALS_CONCEPTS["compensation_expense"]),
560
+ "investment_income": self.get_latest_value(facts, FINANCIALS_CONCEPTS["investment_income"]),
561
+ "performance_fees": self.get_latest_value(facts, FINANCIALS_CONCEPTS["performance_fees"]),
562
+ "fund_expenses": self.get_latest_value(facts, FINANCIALS_CONCEPTS["fund_expenses"]),
563
+ }
564
+
565
+ def _extract_industrials_metrics(self, facts: Dict[str, Any]) -> Dict[str, Optional[TemporalMetric]]:
566
+ """Extract industrials/manufacturing metrics from XBRL facts."""
567
+ return {
568
+ "cost_of_goods_sold": self.get_latest_value(facts, INDUSTRIALS_CONCEPTS["cost_of_goods_sold"]),
569
+ "inventory": self.get_latest_value(facts, INDUSTRIALS_CONCEPTS["inventory"]),
570
+ "depreciation": self.get_latest_value(facts, INDUSTRIALS_CONCEPTS["depreciation"]),
571
+ "backlog": self.get_latest_value(facts, INDUSTRIALS_CONCEPTS["backlog"]),
572
+ "capital_expenditure": self.get_latest_value(facts, INDUSTRIALS_CONCEPTS["capital_expenditure"]),
573
+ "property_plant_equipment": self.get_latest_value(facts, INDUSTRIALS_CONCEPTS["property_plant_equipment"]),
574
+ "pension_expense": self.get_latest_value(facts, INDUSTRIALS_CONCEPTS["pension_expense"]),
575
+ "warranty_expense": self.get_latest_value(facts, INDUSTRIALS_CONCEPTS["warranty_expense"]),
576
+ }
577
+
578
+ def _extract_transportation_metrics(self, facts: Dict[str, Any]) -> Dict[str, Optional[TemporalMetric]]:
579
+ """Extract transportation-specific metrics from XBRL facts."""
580
+ return {
581
+ "operating_revenue": self.get_latest_value(facts, TRANSPORTATION_CONCEPTS["operating_revenue"]),
582
+ "fuel_expense": self.get_latest_value(facts, TRANSPORTATION_CONCEPTS["fuel_expense"]),
583
+ "labor_expense": self.get_latest_value(facts, TRANSPORTATION_CONCEPTS["labor_expense"]),
584
+ "depreciation": self.get_latest_value(facts, TRANSPORTATION_CONCEPTS["depreciation"]),
585
+ "maintenance_expense": self.get_latest_value(facts, TRANSPORTATION_CONCEPTS["maintenance_expense"]),
586
+ "revenue_passenger_miles": self.get_latest_value(facts, TRANSPORTATION_CONCEPTS["revenue_passenger_miles"], unit="pure"),
587
+ "available_seat_miles": self.get_latest_value(facts, TRANSPORTATION_CONCEPTS["available_seat_miles"], unit="pure"),
588
+ "load_factor": self.get_latest_value(facts, TRANSPORTATION_CONCEPTS["load_factor"], unit="pure"),
589
+ "fleet_size": self.get_latest_value(facts, TRANSPORTATION_CONCEPTS["fleet_size"], unit="pure"),
590
+ }
591
+
592
+ def _extract_materials_metrics(self, facts: Dict[str, Any]) -> Dict[str, Optional[TemporalMetric]]:
593
+ """Extract materials-specific metrics from XBRL facts."""
594
+ return {
595
+ "cost_of_goods_sold": self.get_latest_value(facts, MATERIALS_CONCEPTS["cost_of_goods_sold"]),
596
+ "inventory": self.get_latest_value(facts, MATERIALS_CONCEPTS["inventory"]),
597
+ "depreciation": self.get_latest_value(facts, MATERIALS_CONCEPTS["depreciation"]),
598
+ "energy_costs": self.get_latest_value(facts, MATERIALS_CONCEPTS["energy_costs"]),
599
+ "environmental_liabilities": self.get_latest_value(facts, MATERIALS_CONCEPTS["environmental_liabilities"]),
600
+ "property_plant_equipment": self.get_latest_value(facts, MATERIALS_CONCEPTS["property_plant_equipment"]),
601
+ "capital_expenditure": self.get_latest_value(facts, MATERIALS_CONCEPTS["capital_expenditure"]),
602
+ "raw_materials": self.get_latest_value(facts, MATERIALS_CONCEPTS["raw_materials"]),
603
+ }
604
+
605
+ def _extract_mining_metrics(self, facts: Dict[str, Any]) -> Dict[str, Optional[TemporalMetric]]:
606
+ """Extract mining-specific metrics from XBRL facts."""
607
+ return {
608
+ "mining_revenue": self.get_latest_value(facts, MINING_CONCEPTS["mining_revenue"]),
609
+ "cost_of_production": self.get_latest_value(facts, MINING_CONCEPTS["cost_of_production"]),
610
+ "depletion": self.get_latest_value(facts, MINING_CONCEPTS["depletion"]),
611
+ "exploration_expense": self.get_latest_value(facts, MINING_CONCEPTS["exploration_expense"]),
612
+ "reclamation_liabilities": self.get_latest_value(facts, MINING_CONCEPTS["reclamation_liabilities"]),
613
+ "mineral_reserves": self.get_latest_value(facts, MINING_CONCEPTS["mineral_reserves"], unit="pure"),
614
+ "depreciation": self.get_latest_value(facts, MINING_CONCEPTS["depreciation"]),
615
+ "royalty_expense": self.get_latest_value(facts, MINING_CONCEPTS["royalty_expense"]),
616
+ }
617
+
618
  def parse_debt_metrics(
619
  self,
620
  facts: Dict[str, Any],
tests/test_mcp_e2e.py CHANGED
@@ -534,6 +534,35 @@ def generate_report(results: List[MCPTestResult], ticker: str, company_name: str
534
  warnings = "; ".join(r.warnings) if r.warnings else "-"
535
  lines.append(f"| {i} | {r.name} | {r.status} | {expected} | {r.item_count} | {r.duration_ms}ms | {errors} | {warnings} |")
536
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  # Quantitative Data
538
  lines.extend([
539
  "",
 
534
  warnings = "; ".join(r.warnings) if r.warnings else "-"
535
  lines.append(f"| {i} | {r.name} | {r.status} | {expected} | {r.item_count} | {r.duration_ms}ms | {errors} | {warnings} |")
536
 
537
+ # Company Info (from fundamentals)
538
+ fund_result = next((r for r in results if r.name == "fundamentals"), None)
539
+ if fund_result and fund_result.data:
540
+ company = fund_result.data.get("company", {})
541
+ if company:
542
+ lines.extend([
543
+ "",
544
+ "---",
545
+ "",
546
+ "## Company Info",
547
+ "",
548
+ f"| Field | Value |",
549
+ f"|-------|-------|",
550
+ f"| Name | {company.get('name', '-')} |",
551
+ f"| CIK | {company.get('cik', '-')} |",
552
+ f"| SIC | {company.get('sic', '-')} ({company.get('sic_description', '-')}) |",
553
+ f"| State | {company.get('state_of_incorporation', '-')} |",
554
+ f"| Fiscal Year End | {company.get('fiscal_year_end', '-')} |",
555
+ ])
556
+ # Business address
557
+ addr = company.get("business_address", {})
558
+ if addr:
559
+ street = addr.get("street1", "")
560
+ if addr.get("street2"):
561
+ street += f", {addr.get('street2')}"
562
+ city_state_zip = f"{addr.get('city', '')}, {addr.get('stateOrCountry', '')} {addr.get('zipCode', '')}"
563
+ lines.append(f"| Address | {street} |")
564
+ lines.append(f"| | {city_state_zip} |")
565
+
566
  # Quantitative Data
567
  lines.extend([
568
  "",