Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- edgar_client.py +51 -15
- financial_analyzer.py +15 -8
- mcp_server_sse.py +3 -3
edgar_client.py
CHANGED
|
@@ -273,9 +273,17 @@ class EdgarDataClient:
|
|
| 273 |
year = int(period)
|
| 274 |
quarter = None
|
| 275 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
# Get company filings to find accession number and primary document
|
| 277 |
filings = self.get_company_filings(cik, form_types=target_forms)
|
| 278 |
-
filings_map = {} # Map:
|
| 279 |
|
| 280 |
# Build filing map for quick lookup
|
| 281 |
for filing in filings:
|
|
@@ -289,14 +297,16 @@ class EdgarDataClient:
|
|
| 289 |
file_year = int(filing_date[:4]) if len(filing_date) >= 4 else 0
|
| 290 |
|
| 291 |
# Store filing if it matches the period year
|
| 292 |
-
|
|
|
|
| 293 |
key = f"{form_type}_{file_year}"
|
| 294 |
if key not in filings_map:
|
| 295 |
filings_map[key] = {
|
| 296 |
"accession_number": accession_number,
|
| 297 |
"primary_document": primary_document,
|
| 298 |
"form_type": form_type,
|
| 299 |
-
"filing_date": filing_date
|
|
|
|
| 300 |
}
|
| 301 |
|
| 302 |
# Iterate through each financial metric
|
|
@@ -304,15 +314,26 @@ class EdgarDataClient:
|
|
| 304 |
# Support multiple possible tags
|
| 305 |
for metric_tag in metric_tags:
|
| 306 |
# Search both US-GAAP and IFRS tags
|
|
|
|
| 307 |
metric_data = None
|
| 308 |
data_source = None
|
| 309 |
|
| 310 |
-
if
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
|
| 317 |
if metric_data:
|
| 318 |
units = metric_data.get("units", {})
|
|
@@ -368,11 +389,16 @@ class EdgarDataClient:
|
|
| 368 |
# Strategy 3: Allow fy to differ by 1 year (fiscal year vs calendar year mismatch)
|
| 369 |
elif not matched_entry and fy > 0 and abs(fy - year) <= 1 and (fp == "FY" or fp == "" or not fp):
|
| 370 |
matched_entry = entry
|
| 371 |
-
# Strategy 4:
|
| 372 |
-
elif not matched_entry and form == "20-F"
|
| 373 |
frame = entry.get("frame", "")
|
| 374 |
-
if
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
# If quarterly data not found, try finding from annual report (fallback strategy)
|
| 378 |
if not matched_entry and quarter and target_forms_annual:
|
|
@@ -394,10 +420,20 @@ class EdgarDataClient:
|
|
| 394 |
# Get form and accession info
|
| 395 |
form_type = matched_entry.get("form", "")
|
| 396 |
accn_from_facts = matched_entry.get('accn', '').replace('-', '')
|
|
|
|
|
|
|
| 397 |
|
| 398 |
# Try to get accession_number and primary_document from filings
|
| 399 |
-
|
| 400 |
-
filing_info =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
|
| 402 |
if filing_info:
|
| 403 |
# Use filing info from get_company_filings
|
|
|
|
| 273 |
year = int(period)
|
| 274 |
quarter = None
|
| 275 |
|
| 276 |
+
# Detect if company uses 20-F (foreign filer)
|
| 277 |
+
is_20f_filer = False
|
| 278 |
+
all_filings = self.get_company_filings(cik)
|
| 279 |
+
if all_filings:
|
| 280 |
+
form_types_used = set(f.get('form_type', '') for f in all_filings[:20])
|
| 281 |
+
if '20-F' in form_types_used and '10-K' not in form_types_used:
|
| 282 |
+
is_20f_filer = True
|
| 283 |
+
|
| 284 |
# Get company filings to find accession number and primary document
|
| 285 |
filings = self.get_company_filings(cik, form_types=target_forms)
|
| 286 |
+
filings_map = {} # Map: form_year -> {accession_number, primary_document, filing_date, form_type}
|
| 287 |
|
| 288 |
# Build filing map for quick lookup
|
| 289 |
for filing in filings:
|
|
|
|
| 297 |
file_year = int(filing_date[:4]) if len(filing_date) >= 4 else 0
|
| 298 |
|
| 299 |
# Store filing if it matches the period year
|
| 300 |
+
# For 20-F, also check year-1 (fiscal year may differ from filing year)
|
| 301 |
+
if file_year == year or (is_20f_filer and form_type == '20-F' and file_year in [year - 1, year + 1]):
|
| 302 |
key = f"{form_type}_{file_year}"
|
| 303 |
if key not in filings_map:
|
| 304 |
filings_map[key] = {
|
| 305 |
"accession_number": accession_number,
|
| 306 |
"primary_document": primary_document,
|
| 307 |
"form_type": form_type,
|
| 308 |
+
"filing_date": filing_date,
|
| 309 |
+
"file_year": file_year
|
| 310 |
}
|
| 311 |
|
| 312 |
# Iterate through each financial metric
|
|
|
|
| 314 |
# Support multiple possible tags
|
| 315 |
for metric_tag in metric_tags:
|
| 316 |
# Search both US-GAAP and IFRS tags
|
| 317 |
+
# For 20-F filers, prioritize IFRS
|
| 318 |
metric_data = None
|
| 319 |
data_source = None
|
| 320 |
|
| 321 |
+
if is_20f_filer:
|
| 322 |
+
# Check IFRS first for 20-F filers
|
| 323 |
+
if metric_tag in ifrs_full:
|
| 324 |
+
metric_data = ifrs_full[metric_tag]
|
| 325 |
+
data_source = "ifrs-full"
|
| 326 |
+
elif metric_tag in us_gaap:
|
| 327 |
+
metric_data = us_gaap[metric_tag]
|
| 328 |
+
data_source = "us-gaap"
|
| 329 |
+
else:
|
| 330 |
+
# Check US-GAAP first for 10-K filers
|
| 331 |
+
if metric_tag in us_gaap:
|
| 332 |
+
metric_data = us_gaap[metric_tag]
|
| 333 |
+
data_source = "us-gaap"
|
| 334 |
+
elif metric_tag in ifrs_full:
|
| 335 |
+
metric_data = ifrs_full[metric_tag]
|
| 336 |
+
data_source = "ifrs-full"
|
| 337 |
|
| 338 |
if metric_data:
|
| 339 |
units = metric_data.get("units", {})
|
|
|
|
| 389 |
# Strategy 3: Allow fy to differ by 1 year (fiscal year vs calendar year mismatch)
|
| 390 |
elif not matched_entry and fy > 0 and abs(fy - year) <= 1 and (fp == "FY" or fp == "" or not fp):
|
| 391 |
matched_entry = entry
|
| 392 |
+
# Strategy 4: Enhanced matching for 20-F - check frame field and end date
|
| 393 |
+
elif not matched_entry and form == "20-F":
|
| 394 |
frame = entry.get("frame", "")
|
| 395 |
+
# Match if CY{year} in frame OR end date contains year OR fiscal year within range
|
| 396 |
+
if (f"CY{year}" in frame or
|
| 397 |
+
(str(year) in end_date and len(end_date) >= 4 and end_date[:4] == str(year)) or
|
| 398 |
+
(fy > 0 and abs(fy - year) <= 1)):
|
| 399 |
+
# Additional check: prefer entries with FY period
|
| 400 |
+
if fp == "FY" or fp == "" or not fp:
|
| 401 |
+
matched_entry = entry
|
| 402 |
|
| 403 |
# If quarterly data not found, try finding from annual report (fallback strategy)
|
| 404 |
if not matched_entry and quarter and target_forms_annual:
|
|
|
|
| 420 |
# Get form and accession info
|
| 421 |
form_type = matched_entry.get("form", "")
|
| 422 |
accn_from_facts = matched_entry.get('accn', '').replace('-', '')
|
| 423 |
+
filed_date = matched_entry.get('filed', '')
|
| 424 |
+
filed_year = int(filed_date[:4]) if filed_date and len(filed_date) >= 4 else year
|
| 425 |
|
| 426 |
# Try to get accession_number and primary_document from filings
|
| 427 |
+
# For 20-F, try multiple year keys since filing year may differ
|
| 428 |
+
filing_info = None
|
| 429 |
+
possible_keys = [f"{form_type}_{year}"]
|
| 430 |
+
if form_type == "20-F":
|
| 431 |
+
possible_keys.extend([f"20-F_{filed_year}", f"20-F_{year-1}", f"20-F_{year+1}"])
|
| 432 |
+
|
| 433 |
+
for filing_key in possible_keys:
|
| 434 |
+
if filing_key in filings_map:
|
| 435 |
+
filing_info = filings_map[filing_key]
|
| 436 |
+
break
|
| 437 |
|
| 438 |
if filing_info:
|
| 439 |
# Use filing info from get_company_filings
|
financial_analyzer.py
CHANGED
|
@@ -86,6 +86,10 @@ class FinancialAnalyzer:
|
|
| 86 |
if not all_annual_filings:
|
| 87 |
return []
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
# Step 2: Extract filing years from annual reports
|
| 90 |
# Use filing_date to determine the years we should query
|
| 91 |
filing_year_map = {} # Map: filing_year -> list of filings
|
|
@@ -147,6 +151,7 @@ class FinancialAnalyzer:
|
|
| 147 |
|
| 148 |
# Step 5: Generate period list for target years
|
| 149 |
# For each year: FY -> Q4 -> Q3 -> Q2 -> Q1 (descending order)
|
|
|
|
| 150 |
periods = []
|
| 151 |
for file_year in target_years:
|
| 152 |
# Try to get fiscal year from mapping, otherwise use filing year
|
|
@@ -160,14 +165,16 @@ class FinancialAnalyzer:
|
|
| 160 |
'filing_year': file_year
|
| 161 |
})
|
| 162 |
|
| 163 |
-
#
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
| 171 |
|
| 172 |
# Step 6: Get financial data for each period
|
| 173 |
for idx, period_info in enumerate(periods):
|
|
|
|
| 86 |
if not all_annual_filings:
|
| 87 |
return []
|
| 88 |
|
| 89 |
+
# Detect if company is a 20-F filer (foreign company)
|
| 90 |
+
is_20f_filer = len(filings_20f) > 0 and len(filings_10k) == 0
|
| 91 |
+
has_quarterly = False # 20-F filers typically don't have quarterly reports
|
| 92 |
+
|
| 93 |
# Step 2: Extract filing years from annual reports
|
| 94 |
# Use filing_date to determine the years we should query
|
| 95 |
filing_year_map = {} # Map: filing_year -> list of filings
|
|
|
|
| 151 |
|
| 152 |
# Step 5: Generate period list for target years
|
| 153 |
# For each year: FY -> Q4 -> Q3 -> Q2 -> Q1 (descending order)
|
| 154 |
+
# For 20-F filers: only FY (no quarterly data)
|
| 155 |
periods = []
|
| 156 |
for file_year in target_years:
|
| 157 |
# Try to get fiscal year from mapping, otherwise use filing year
|
|
|
|
| 165 |
'filing_year': file_year
|
| 166 |
})
|
| 167 |
|
| 168 |
+
# Only add quarterly data for 10-K filers (not for 20-F filers)
|
| 169 |
+
if not is_20f_filer:
|
| 170 |
+
# Then add quarterly data in descending order: Q4, Q3, Q2, Q1
|
| 171 |
+
for quarter in range(4, 0, -1):
|
| 172 |
+
periods.append({
|
| 173 |
+
'period': f"{fiscal_year}Q{quarter}",
|
| 174 |
+
'type': 'quarterly',
|
| 175 |
+
'fiscal_year': fiscal_year,
|
| 176 |
+
'filing_year': file_year
|
| 177 |
+
})
|
| 178 |
|
| 179 |
# Step 6: Get financial data for each period
|
| 180 |
for idx, period_info in enumerate(periods):
|
mcp_server_sse.py
CHANGED
|
@@ -22,7 +22,7 @@ import sys
|
|
| 22 |
app = FastAPI(
|
| 23 |
title="SEC Financial Report MCP Server",
|
| 24 |
description="Model Context Protocol Server for SEC EDGAR Financial Data",
|
| 25 |
-
version="2.
|
| 26 |
)
|
| 27 |
|
| 28 |
# Server startup time for monitoring
|
|
@@ -374,7 +374,7 @@ async def handle_mcp_message(request: MCPRequest):
|
|
| 374 |
},
|
| 375 |
"serverInfo": {
|
| 376 |
"name": "sec-financial-data",
|
| 377 |
-
"version": "2.
|
| 378 |
}
|
| 379 |
}
|
| 380 |
).dict()
|
|
@@ -619,7 +619,7 @@ async def health_check():
|
|
| 619 |
return {
|
| 620 |
"status": "healthy",
|
| 621 |
"server": "sec-financial-data",
|
| 622 |
-
"version": "2.
|
| 623 |
"protocol": "MCP",
|
| 624 |
"transport": "SSE",
|
| 625 |
"tools_count": len(MCP_TOOLS),
|
|
|
|
| 22 |
app = FastAPI(
|
| 23 |
title="SEC Financial Report MCP Server",
|
| 24 |
description="Model Context Protocol Server for SEC EDGAR Financial Data",
|
| 25 |
+
version="2.1.0"
|
| 26 |
)
|
| 27 |
|
| 28 |
# Server startup time for monitoring
|
|
|
|
| 374 |
},
|
| 375 |
"serverInfo": {
|
| 376 |
"name": "sec-financial-data",
|
| 377 |
+
"version": "2.1.0"
|
| 378 |
}
|
| 379 |
}
|
| 380 |
).dict()
|
|
|
|
| 619 |
return {
|
| 620 |
"status": "healthy",
|
| 621 |
"server": "sec-financial-data",
|
| 622 |
+
"version": "2.1.0",
|
| 623 |
"protocol": "MCP",
|
| 624 |
"transport": "SSE",
|
| 625 |
"tools_count": len(MCP_TOOLS),
|