JC321 commited on
Commit
bbf5389
·
verified ·
1 Parent(s): 6d91fb6

Upload 3 files

Browse files
Files changed (3) hide show
  1. edgar_client.py +43 -0
  2. financial_analyzer.py +58 -1
  3. 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
- return self.edgar_client.get_financial_data_for_period(cik, str(latest_filing_year))
 
 
 
 
 
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.4"
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.4"
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.4",
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),