Spaces:
Sleeping
Sleeping
Upload 3 files
Browse files- edgar_client.py +43 -0
- financial_analyzer.py +58 -1
- mcp_server_sse.py +3 -3
edgar_client.py
CHANGED
|
@@ -60,6 +60,12 @@ class EdgarDataClient:
|
|
| 60 |
self._search_cache = {} # search_key -> result
|
| 61 |
self._search_cache_max_size = 1000 # Limit cache size
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
# Common company aliases for intelligent search
|
| 64 |
self._company_aliases = {
|
| 65 |
'google': ['GOOGL', 'GOOG'],
|
|
@@ -415,6 +421,33 @@ class EdgarDataClient:
|
|
| 415 |
|
| 416 |
self._search_cache[cache_key] = result
|
| 417 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
def get_company_info(self, cik):
|
| 419 |
"""
|
| 420 |
Get basic company information with caching
|
|
@@ -607,6 +640,13 @@ class EdgarDataClient:
|
|
| 607 |
if not self.edgar:
|
| 608 |
print("sec_edgar_api library not installed")
|
| 609 |
return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 610 |
|
| 611 |
try:
|
| 612 |
# Get company financial facts
|
|
@@ -844,6 +884,9 @@ class EdgarDataClient:
|
|
| 844 |
if metric_key in result:
|
| 845 |
break
|
| 846 |
|
|
|
|
|
|
|
|
|
|
| 847 |
return result
|
| 848 |
except Exception as e:
|
| 849 |
print(f"Error getting financial data for period {period}: {e}")
|
|
|
|
| 60 |
self._search_cache = {} # search_key -> result
|
| 61 |
self._search_cache_max_size = 1000 # Limit cache size
|
| 62 |
|
| 63 |
+
# Layer 3: Period data cache (avoid re-parsing XBRL for same period)
|
| 64 |
+
self._period_cache = {} # period_key -> financial data
|
| 65 |
+
self._period_cache_timestamps = {} # period_key -> timestamp
|
| 66 |
+
self._period_cache_ttl = 1800 # 30 minutes cache (financial data changes rarely)
|
| 67 |
+
self._period_cache_max_size = 1000 # Limit cache size
|
| 68 |
+
|
| 69 |
# Common company aliases for intelligent search
|
| 70 |
self._company_aliases = {
|
| 71 |
'google': ['GOOGL', 'GOOG'],
|
|
|
|
| 421 |
|
| 422 |
self._search_cache[cache_key] = result
|
| 423 |
|
| 424 |
+
def _get_period_cache(self, cache_key):
|
| 425 |
+
"""Get cached period data if valid (Layer 3)"""
|
| 426 |
+
if cache_key not in self._period_cache_timestamps:
|
| 427 |
+
return None
|
| 428 |
+
|
| 429 |
+
age = time.time() - self._period_cache_timestamps[cache_key]
|
| 430 |
+
if age < self._period_cache_ttl:
|
| 431 |
+
return self._period_cache.get(cache_key)
|
| 432 |
+
else:
|
| 433 |
+
# Expired, remove from cache
|
| 434 |
+
self._period_cache.pop(cache_key, None)
|
| 435 |
+
self._period_cache_timestamps.pop(cache_key, None)
|
| 436 |
+
return None
|
| 437 |
+
|
| 438 |
+
def _set_period_cache(self, cache_key, result):
|
| 439 |
+
"""Cache period data with size limit (Layer 3)"""
|
| 440 |
+
# LRU-like eviction if cache is full
|
| 441 |
+
if len(self._period_cache) >= self._period_cache_max_size:
|
| 442 |
+
# Remove oldest half
|
| 443 |
+
keys_to_remove = list(self._period_cache.keys())[:self._period_cache_max_size // 2]
|
| 444 |
+
for key in keys_to_remove:
|
| 445 |
+
self._period_cache.pop(key, None)
|
| 446 |
+
self._period_cache_timestamps.pop(key, None)
|
| 447 |
+
|
| 448 |
+
self._period_cache[cache_key] = result
|
| 449 |
+
self._period_cache_timestamps[cache_key] = time.time()
|
| 450 |
+
|
| 451 |
def get_company_info(self, cik):
|
| 452 |
"""
|
| 453 |
Get basic company information with caching
|
|
|
|
| 640 |
if not self.edgar:
|
| 641 |
print("sec_edgar_api library not installed")
|
| 642 |
return {}
|
| 643 |
+
|
| 644 |
+
# Check period cache first (Layer 3)
|
| 645 |
+
cache_key = f"period_{cik}_{period}"
|
| 646 |
+
cached = self._get_period_cache(cache_key)
|
| 647 |
+
if cached is not None:
|
| 648 |
+
print(f"[Cache Hit] get_financial_data_for_period({cik}, {period})")
|
| 649 |
+
return cached.copy() # Return copy to avoid mutation
|
| 650 |
|
| 651 |
try:
|
| 652 |
# Get company financial facts
|
|
|
|
| 884 |
if metric_key in result:
|
| 885 |
break
|
| 886 |
|
| 887 |
+
# Cache the result (Layer 3)
|
| 888 |
+
self._set_period_cache(cache_key, result)
|
| 889 |
+
|
| 890 |
return result
|
| 891 |
except Exception as e:
|
| 892 |
print(f"Error getting financial data for period {period}: {e}")
|
financial_analyzer.py
CHANGED
|
@@ -15,6 +15,41 @@ class FinancialAnalyzer:
|
|
| 15 |
"""
|
| 16 |
self.edgar_client = EdgarDataClient(user_agent)
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
def search_company(self, company_input):
|
| 19 |
"""
|
| 20 |
Search company information (by name, ticker, or CIK) - Optimized version
|
|
@@ -141,6 +176,13 @@ class FinancialAnalyzer:
|
|
| 141 |
Returns:
|
| 142 |
list: List of financial data
|
| 143 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
financial_data = []
|
| 145 |
|
| 146 |
# Step 1: Get company filings to determine what was actually filed
|
|
@@ -258,6 +300,9 @@ class FinancialAnalyzer:
|
|
| 258 |
|
| 259 |
financial_data.append(data)
|
| 260 |
|
|
|
|
|
|
|
|
|
|
| 261 |
return financial_data
|
| 262 |
|
| 263 |
def get_latest_financial_data(self, cik):
|
|
@@ -270,6 +315,13 @@ class FinancialAnalyzer:
|
|
| 270 |
Returns:
|
| 271 |
dict: Latest financial data
|
| 272 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
# Get latest filing year (supports 10-K and 20-F)
|
| 274 |
filings_10k = self.edgar_client.get_company_filings(cik, ['10-K'])
|
| 275 |
filings_20f = self.edgar_client.get_company_filings(cik, ['20-F'])
|
|
@@ -293,7 +345,12 @@ class FinancialAnalyzer:
|
|
| 293 |
return {}
|
| 294 |
|
| 295 |
# Get financial data for latest year
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
|
| 298 |
def format_financial_data(self, financial_data):
|
| 299 |
"""
|
|
|
|
| 15 |
"""
|
| 16 |
self.edgar_client = EdgarDataClient(user_agent)
|
| 17 |
|
| 18 |
+
# Layer 2: Method-level cache (avoid duplicate API calls)
|
| 19 |
+
self._method_cache = {} # method_key -> result
|
| 20 |
+
self._method_cache_timestamps = {} # method_key -> timestamp
|
| 21 |
+
self._method_cache_ttl = 600 # 10 minutes cache
|
| 22 |
+
self._method_cache_max_size = 500 # Limit cache size
|
| 23 |
+
|
| 24 |
+
def _get_method_cache(self, cache_key):
|
| 25 |
+
"""Get cached method result if valid"""
|
| 26 |
+
if cache_key not in self._method_cache_timestamps:
|
| 27 |
+
return None
|
| 28 |
+
|
| 29 |
+
import time
|
| 30 |
+
age = time.time() - self._method_cache_timestamps[cache_key]
|
| 31 |
+
if age < self._method_cache_ttl:
|
| 32 |
+
return self._method_cache.get(cache_key)
|
| 33 |
+
else:
|
| 34 |
+
# Expired, remove from cache
|
| 35 |
+
self._method_cache.pop(cache_key, None)
|
| 36 |
+
self._method_cache_timestamps.pop(cache_key, None)
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
def _set_method_cache(self, cache_key, result):
|
| 40 |
+
"""Cache method result with size limit"""
|
| 41 |
+
# LRU-like eviction if cache is full
|
| 42 |
+
if len(self._method_cache) >= self._method_cache_max_size:
|
| 43 |
+
# Remove oldest half
|
| 44 |
+
keys_to_remove = list(self._method_cache.keys())[:self._method_cache_max_size // 2]
|
| 45 |
+
for key in keys_to_remove:
|
| 46 |
+
self._method_cache.pop(key, None)
|
| 47 |
+
self._method_cache_timestamps.pop(key, None)
|
| 48 |
+
|
| 49 |
+
import time
|
| 50 |
+
self._method_cache[cache_key] = result
|
| 51 |
+
self._method_cache_timestamps[cache_key] = time.time()
|
| 52 |
+
|
| 53 |
def search_company(self, company_input):
|
| 54 |
"""
|
| 55 |
Search company information (by name, ticker, or CIK) - Optimized version
|
|
|
|
| 176 |
Returns:
|
| 177 |
list: List of financial data
|
| 178 |
"""
|
| 179 |
+
# Check method cache first (Layer 2)
|
| 180 |
+
cache_key = f"extract_metrics_{cik}_{years}"
|
| 181 |
+
cached = self._get_method_cache(cache_key)
|
| 182 |
+
if cached is not None:
|
| 183 |
+
print(f"[Cache Hit] extract_financial_metrics({cik}, {years})")
|
| 184 |
+
return cached
|
| 185 |
+
|
| 186 |
financial_data = []
|
| 187 |
|
| 188 |
# Step 1: Get company filings to determine what was actually filed
|
|
|
|
| 300 |
|
| 301 |
financial_data.append(data)
|
| 302 |
|
| 303 |
+
# Cache the result (Layer 2)
|
| 304 |
+
self._set_method_cache(cache_key, financial_data)
|
| 305 |
+
|
| 306 |
return financial_data
|
| 307 |
|
| 308 |
def get_latest_financial_data(self, cik):
|
|
|
|
| 315 |
Returns:
|
| 316 |
dict: Latest financial data
|
| 317 |
"""
|
| 318 |
+
# Check method cache first (Layer 2)
|
| 319 |
+
cache_key = f"latest_data_{cik}"
|
| 320 |
+
cached = self._get_method_cache(cache_key)
|
| 321 |
+
if cached is not None:
|
| 322 |
+
print(f"[Cache Hit] get_latest_financial_data({cik})")
|
| 323 |
+
return cached
|
| 324 |
+
|
| 325 |
# Get latest filing year (supports 10-K and 20-F)
|
| 326 |
filings_10k = self.edgar_client.get_company_filings(cik, ['10-K'])
|
| 327 |
filings_20f = self.edgar_client.get_company_filings(cik, ['20-F'])
|
|
|
|
| 345 |
return {}
|
| 346 |
|
| 347 |
# Get financial data for latest year
|
| 348 |
+
result = self.edgar_client.get_financial_data_for_period(cik, str(latest_filing_year))
|
| 349 |
+
|
| 350 |
+
# Cache the result (Layer 2)
|
| 351 |
+
self._set_method_cache(cache_key, result)
|
| 352 |
+
|
| 353 |
+
return result
|
| 354 |
|
| 355 |
def format_financial_data(self, financial_data):
|
| 356 |
"""
|
mcp_server_sse.py
CHANGED
|
@@ -27,7 +27,7 @@ from contextlib import contextmanager
|
|
| 27 |
app = FastAPI(
|
| 28 |
title="SEC Financial Report MCP Server API",
|
| 29 |
description="Model Context Protocol Server for SEC EDGAR Financial Data",
|
| 30 |
-
version="2.3.
|
| 31 |
)
|
| 32 |
|
| 33 |
# Server startup time for monitoring
|
|
@@ -415,7 +415,7 @@ async def handle_mcp_message(request: MCPRequest):
|
|
| 415 |
},
|
| 416 |
"serverInfo": {
|
| 417 |
"name": "sec-financial-data",
|
| 418 |
-
"version": "2.3.
|
| 419 |
}
|
| 420 |
}
|
| 421 |
).dict()
|
|
@@ -547,7 +547,7 @@ async def health_check():
|
|
| 547 |
return {
|
| 548 |
"status": "healthy",
|
| 549 |
"server": "sec-financial-data",
|
| 550 |
-
"version": "2.3.
|
| 551 |
"protocol": "MCP",
|
| 552 |
"transport": "SSE",
|
| 553 |
"tools_count": len(MCP_TOOLS),
|
|
|
|
| 27 |
app = FastAPI(
|
| 28 |
title="SEC Financial Report MCP Server API",
|
| 29 |
description="Model Context Protocol Server for SEC EDGAR Financial Data",
|
| 30 |
+
version="2.3.5"
|
| 31 |
)
|
| 32 |
|
| 33 |
# Server startup time for monitoring
|
|
|
|
| 415 |
},
|
| 416 |
"serverInfo": {
|
| 417 |
"name": "sec-financial-data",
|
| 418 |
+
"version": "2.3.5"
|
| 419 |
}
|
| 420 |
}
|
| 421 |
).dict()
|
|
|
|
| 547 |
return {
|
| 548 |
"status": "healthy",
|
| 549 |
"server": "sec-financial-data",
|
| 550 |
+
"version": "2.3.5",
|
| 551 |
"protocol": "MCP",
|
| 552 |
"transport": "SSE",
|
| 553 |
"tools_count": len(MCP_TOOLS),
|