baba521 commited on
Commit
716f1cd
·
1 Parent(s): fc64af9

测试集成mcp

Browse files
EasyFinancialAgent/chat_direct.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Financial AI Assistant - Direct Method Library (不依赖 HTTP)
3
+ 直接导入并调用 easy_financial_mcp.py 中的函数
4
+ 支持本地和 HF Space 部署
5
+ """
6
+
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ # 添加服务模块路径
11
+ PROJECT_ROOT = Path(__file__).parent.parent.absolute()
12
+ sys.path.insert(0, str(PROJECT_ROOT))
13
+
14
+ # 直接导入 MCP 中定义的函数
15
+ try:
16
+ from EasyFinancialAgent.easy_financial_mcp import (
17
+ search_company as _search_company,
18
+ get_company_info as _get_company_info,
19
+ get_company_filings as _get_company_filings,
20
+ get_financial_data as _get_financial_data,
21
+ extract_financial_metrics as _extract_financial_metrics,
22
+ get_latest_financial_data as _get_latest_financial_data,
23
+ advanced_search_company as _advanced_search_company
24
+ )
25
+ MCP_DIRECT_AVAILABLE = True
26
+ print("[FinancialAI] ✓ Direct MCP functions imported successfully")
27
+ except ImportError as e:
28
+ MCP_DIRECT_AVAILABLE = False
29
+ print(f"[FinancialAI] ✗ Failed to import MCP functions: {e}")
30
+ # 定义占位符函数
31
+ def _advanced_search_company(x):
32
+ return {"error": "MCP not available"}
33
+ def _get_company_info(x):
34
+ return {"error": "MCP not available"}
35
+ def _get_company_filings(x):
36
+ return {"error": "MCP not available"}
37
+ def _get_latest_financial_data(x):
38
+ return {"error": "MCP not available"}
39
+ def _extract_financial_metrics(x, y=3):
40
+ return {"error": "MCP not available"}
41
+
42
+
43
+ # ============================================================
44
+ # 便捷方法 - 公司搜索相关
45
+ # ============================================================
46
+
47
+ def search_company_direct(company_input):
48
+ """
49
+ 搜索公司信息(直接调用)
50
+
51
+ Args:
52
+ company_input: 公司名称或代码
53
+
54
+ Returns:
55
+ 搜索结果
56
+
57
+ Example:
58
+ result = search_company_direct("Apple")
59
+ """
60
+ if not MCP_DIRECT_AVAILABLE:
61
+ return {"error": "MCP functions not available"}
62
+
63
+ try:
64
+ return _advanced_search_company(company_input)
65
+ except Exception as e:
66
+ return {"error": str(e)}
67
+
68
+
69
+ def get_company_info_direct(cik):
70
+ """
71
+ 获取公司详细信息(直接调用)
72
+
73
+ Args:
74
+ cik: 公司 CIK 代码
75
+
76
+ Returns:
77
+ 公司信息
78
+
79
+ Example:
80
+ result = get_company_info_direct("0000320193")
81
+ """
82
+ if not MCP_DIRECT_AVAILABLE:
83
+ return {"error": "MCP functions not available"}
84
+
85
+ try:
86
+ return _get_company_info(cik)
87
+ except Exception as e:
88
+ return {"error": str(e)}
89
+
90
+
91
+ def get_company_filings_direct(cik):
92
+ """
93
+ 获取公司 SEC 文件列表(直接调用)
94
+
95
+ Args:
96
+ cik: 公司 CIK 代码
97
+
98
+ Returns:
99
+ 文件列表
100
+
101
+ Example:
102
+ result = get_company_filings_direct("0000320193")
103
+ """
104
+ if not MCP_DIRECT_AVAILABLE:
105
+ return {"error": "MCP functions not available"}
106
+
107
+ try:
108
+ return _get_company_filings(cik)
109
+ except Exception as e:
110
+ return {"error": str(e)}
111
+
112
+
113
+ # ============================================================
114
+ # 便捷方法 - 财务数据相关
115
+ # ============================================================
116
+
117
+ def get_latest_financial_data_direct(cik):
118
+ """
119
+ 获取公司最新财务数据(直接调用)
120
+
121
+ Args:
122
+ cik: 公司 CIK 代码
123
+
124
+ Returns:
125
+ 最新财务数据
126
+
127
+ Example:
128
+ result = get_latest_financial_data_direct("0000320193")
129
+ """
130
+ if not MCP_DIRECT_AVAILABLE:
131
+ return {"error": "MCP functions not available"}
132
+
133
+ try:
134
+ return _get_latest_financial_data(cik)
135
+ except Exception as e:
136
+ return {"error": str(e)}
137
+
138
+
139
+ def extract_financial_metrics_direct(cik, years=5):
140
+ """
141
+ 提取多年财务指标趋势(直接调用)
142
+
143
+ Args:
144
+ cik: 公司 CIK 代码
145
+ years: 年数(默认 3 年)
146
+
147
+ Returns:
148
+ 财务指标数据
149
+
150
+ Example:
151
+ result = extract_financial_metrics_direct("0000320193", years=5)
152
+ """
153
+ if not MCP_DIRECT_AVAILABLE:
154
+ return {"error": "MCP functions not available"}
155
+
156
+ try:
157
+ return _extract_financial_metrics(cik, years)
158
+ except Exception as e:
159
+ return {"error": str(e)}
160
+
161
+
162
+ # ============================================================
163
+ # 高级方法 - 综合查询
164
+ # ============================================================
165
+
166
+ def query_company_direct(company_input, get_filings=True, get_metrics=True):
167
+ """
168
+ 综合查询公司信息(直接调用)
169
+ 包括搜索、基本信息、文件列表和财务指标
170
+
171
+ Args:
172
+ company_input: 公司名称或代码
173
+ get_filings: 是否获取文件列表
174
+ get_metrics: 是否获取财务指标
175
+
176
+ Returns:
177
+ 综合结果字典,包含 search, company_info, filings, metrics
178
+
179
+ Example:
180
+ result = query_company_direct("Apple", get_filings=True, get_metrics=True)
181
+ """
182
+ from datetime import datetime
183
+
184
+ result = {
185
+ "timestamp": datetime.now().isoformat(),
186
+ "query_input": company_input,
187
+ "status": "success",
188
+ "data": {
189
+ "company_search": None,
190
+ "company_info": None,
191
+ "filings": None,
192
+ "metrics": None
193
+ },
194
+ "errors": []
195
+ }
196
+
197
+ if not MCP_DIRECT_AVAILABLE:
198
+ result["status"] = "error"
199
+ result["errors"].append("MCP functions not available")
200
+ return result
201
+
202
+ try:
203
+ # 1. 搜索公司
204
+ search_result = search_company_direct(company_input)
205
+ if "error" in search_result:
206
+ result["errors"].append(f"Search error: {search_result['error']}")
207
+ result["status"] = "error"
208
+ return result
209
+
210
+ result["data"]["company_search"] = search_result
211
+
212
+ # 从搜索结果提取 CIK
213
+ cik = None
214
+ if isinstance(search_result, dict):
215
+ cik = search_result.get("cik")
216
+ elif isinstance(search_result, (list, tuple)) and len(search_result) > 0:
217
+ # 从列表中获取第一个元素
218
+ try:
219
+ first_item = search_result[0] if isinstance(search_result, (list, tuple)) else None
220
+ if isinstance(first_item, dict):
221
+ cik = first_item.get("cik")
222
+ except (IndexError, TypeError):
223
+ pass
224
+
225
+ if not cik:
226
+ result["errors"].append("Could not extract CIK from search result")
227
+ result["status"] = "error"
228
+ return result
229
+
230
+ # 2. 获取公司信息
231
+ company_info = get_company_info_direct(cik)
232
+ if "error" not in company_info:
233
+ result["data"]["company_info"] = company_info
234
+ else:
235
+ result["errors"].append(f"Failed to get company info: {company_info.get('error')}")
236
+
237
+ # 3. 获取文件列表
238
+ if get_filings:
239
+ filings = get_company_filings_direct(cik)
240
+ if "error" not in filings:
241
+ result["data"]["filings"] = filings
242
+ else:
243
+ result["errors"].append(f"Failed to get filings: {filings.get('error')}")
244
+
245
+ # 4. 获取财务指标
246
+ if get_metrics:
247
+ metrics = extract_financial_metrics_direct(cik, years=3)
248
+ if "error" not in metrics:
249
+ result["data"]["metrics"] = metrics
250
+ else:
251
+ result["errors"].append(f"Failed to get metrics: {metrics.get('error')}")
252
+
253
+ except Exception as e:
254
+ result["status"] = "error"
255
+ result["errors"].append(f"Exception: {str(e)}")
256
+ import traceback
257
+ result["errors"].append(traceback.format_exc())
258
+
259
+ return result
260
+
261
+
262
+ # ============================================================
263
+ # 测试函数
264
+ # ============================================================
265
+
266
+ if __name__ == "__main__":
267
+ print("\n" + "="*60)
268
+ print("Financial AI Assistant - Direct Method Test")
269
+ print("="*60)
270
+
271
+ # 测试 1: 公司搜索
272
+ print("\n1. 搜索公司 (Apple)...")
273
+ result = search_company_direct("Apple")
274
+ print(f" 结果: {result}")
275
+
276
+ # 测试 2: 公司摘要
277
+ print("\n2. 获取 Tesla 完整信息...")
278
+ summary = query_company_direct("Tesla", get_filings=False, get_metrics=False)
279
+ print(f" 状态: {summary['status']}")
280
+ print(f" 数据: {summary['data']}")
281
+ print(f" 错误: {summary['errors']}")
282
+
283
+ # 测试 3: 财务指标
284
+ print("\n3. 获取 Microsoft 财务指标...")
285
+ if summary['data']['company_search']:
286
+ cik = summary['data']['company_search'].get('cik')
287
+ if cik:
288
+ metrics = extract_financial_metrics_direct(cik, years=3)
289
+ print(f" 指标: {metrics}")
290
+
291
+ print("\n" + "="*60)
EasyFinancialAgent/easy_financial_mcp.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Server for SEC EDGAR Financial Data - FastMCP Implementation
3
+ Uses Anthropic official FastMCP SDK for cleaner, more maintainable code
4
+ """
5
+
6
+ from mcp.server.fastmcp import FastMCP
7
+ from EasyReportDataMCP.edgar_client import EdgarDataClient
8
+ from EasyReportDataMCP.financial_analyzer import FinancialAnalyzer
9
+
10
+ # Initialize EDGAR clients
11
+ edgar_client = EdgarDataClient(
12
+ user_agent="Juntao Peng Financial Report Metrics App ([email protected])"
13
+ )
14
+
15
+ financial_analyzer = FinancialAnalyzer(
16
+ user_agent="Juntao Peng Financial Report Metrics App ([email protected])"
17
+ )
18
+
19
+ # Create FastMCP server with pure JSON response and stateless HTTP
20
+ mcp = FastMCP("sec-financial-data", json_response=True, stateless_http=True)
21
+
22
+
23
+ @mcp.tool()
24
+ def search_company(company_name: str) -> dict:
25
+ """
26
+ Search for a company by name in SEC EDGAR database.
27
+
28
+ Args:
29
+ company_name: Company name to search (e.g., Microsoft, Apple, Tesla)
30
+
31
+ Returns:
32
+ dict: Company information including CIK, name, and ticker symbol
33
+ """
34
+ result = edgar_client.search_company_by_name(company_name)
35
+ if result:
36
+ return result
37
+ else:
38
+ return {"error": f"No company found with name: {company_name}"}
39
+
40
+
41
+ @mcp.tool()
42
+ def get_company_info(cik: str) -> dict:
43
+ """
44
+ Get detailed company information including name, tickers, SIC code, and industry description.
45
+
46
+ Args:
47
+ cik: Company CIK code (10-digit format, e.g., 0000789019)
48
+
49
+ Returns:
50
+ dict: Company information
51
+ """
52
+ result = edgar_client.get_company_info(cik)
53
+ if result:
54
+ return result
55
+ else:
56
+ return {"error": f"No company found with CIK: {cik}"}
57
+
58
+
59
+ @mcp.tool()
60
+ def get_company_filings(cik: str, form_types: list[str] | None = None) -> dict:
61
+ """
62
+ Get list of company SEC filings (10-K, 10-Q, 20-F, etc.) with filing dates and document links.
63
+
64
+ Args:
65
+ cik: Company CIK code
66
+ form_types: Optional filter by form types (e.g., [10-K, 10-Q])
67
+
68
+ Returns:
69
+ dict: Filings list with total count and limited results
70
+ """
71
+ result = edgar_client.get_company_filings(cik, form_types)
72
+ if result:
73
+ limited_result = result[:20]
74
+ return {
75
+ "total": len(result),
76
+ "returned": len(limited_result),
77
+ "filings": limited_result
78
+ }
79
+ else:
80
+ return {"error": f"No filings found for CIK: {cik}"}
81
+
82
+
83
+ @mcp.tool()
84
+ def get_financial_data(cik: str, period: str) -> dict:
85
+ """
86
+ Get financial data for a specific period including revenue, net income, EPS, operating expenses, and cash flow.
87
+
88
+ Args:
89
+ cik: Company CIK code
90
+ period: Period in format YYYY for annual or YYYYQX for quarterly (e.g., 2024, 2024Q3)
91
+
92
+ Returns:
93
+ dict: Financial data for the specified period
94
+ """
95
+ result = edgar_client.get_financial_data_for_period(cik, period)
96
+ if result and "period" in result:
97
+ return result
98
+ else:
99
+ return {"error": f"No financial data found for CIK: {cik}, Period: {period}"}
100
+
101
+
102
+ @mcp.tool()
103
+ def extract_financial_metrics(cik: str, years: int = 3) -> dict:
104
+ """
105
+ Extract comprehensive financial metrics for multiple years including both annual and quarterly data.
106
+ Returns data in chronological order (newest first): FY -> Q4 -> Q3 -> Q2 -> Q1.
107
+
108
+ Args:
109
+ cik: Company CIK code
110
+ years: Number of recent years to extract (1-10, default: 3)
111
+
112
+ Returns:
113
+ dict: Financial metrics with periods and data
114
+ """
115
+ if years < 1 or years > 10:
116
+ return {"error": "Years parameter must be between 1 and 10"}
117
+
118
+ # Check if company has filings (use tuple for caching)
119
+ filings_10k = edgar_client.get_company_filings(cik, ('"10-K"',))
120
+ filings_20f = edgar_client.get_company_filings(cik, ('"20-F"',))
121
+ total_filings = len(filings_10k) + len(filings_20f)
122
+
123
+ if total_filings == 0:
124
+ return {
125
+ "error": f"No annual filings found for CIK: {cik}",
126
+ "suggestion": "Please check if the CIK is correct"
127
+ }
128
+
129
+ # Extract metrics
130
+ metrics = financial_analyzer.extract_financial_metrics(cik, years)
131
+
132
+ if metrics:
133
+ formatted = financial_analyzer.format_financial_data(metrics)
134
+ return {
135
+ "periods": len(formatted),
136
+ "data": formatted
137
+ }
138
+ else:
139
+ # Return debug info
140
+ debug_info = {
141
+ "error": f"No financial metrics extracted for CIK: {cik}",
142
+ "debug": {
143
+ "cik": cik,
144
+ "years_requested": years,
145
+ "filings_found": {
146
+ "10-K": len(filings_10k),
147
+ "20-F": len(filings_20f)
148
+ },
149
+ "latest_filings": []
150
+ },
151
+ "suggestion": "Try using get_latest_financial_data or get_financial_data with a specific period"
152
+ }
153
+
154
+ # Add latest filing dates
155
+ all_filings = filings_10k + filings_20f
156
+ for filing in all_filings[:5]:
157
+ debug_info["debug"]["latest_filings"].append({
158
+ "form": filing.get("form_type"),
159
+ "date": filing.get("filing_date")
160
+ })
161
+
162
+ return debug_info
163
+
164
+
165
+ @mcp.tool()
166
+ def get_latest_financial_data(cik: str) -> dict:
167
+ """
168
+ Get the most recent financial data available for a company.
169
+
170
+ Args:
171
+ cik: Company CIK code
172
+
173
+ Returns:
174
+ dict: Latest financial data
175
+ """
176
+ result = financial_analyzer.get_latest_financial_data(cik)
177
+ if result and "period" in result:
178
+ return result
179
+ else:
180
+ return {"error": f"No latest financial data found for CIK: {cik}"}
181
+
182
+
183
+ @mcp.tool()
184
+ def advanced_search_company(company_input: str) -> dict:
185
+ """
186
+ Advanced search supporting both company name and CIK code. Automatically detects input type.
187
+
188
+ Args:
189
+ company_input: Company name, ticker, or CIK code
190
+
191
+ Returns:
192
+ dict: Company information
193
+ """
194
+ result = financial_analyzer.search_company(company_input)
195
+ if result.get("error"):
196
+ return {"error": result["error"]}
197
+ return result
198
+
199
+
200
+ # For production deployment
201
+ if __name__ == "__main__":
202
+ import os
203
+
204
+ # Set port from environment (HF Space sets PORT=7860)
205
+ port = int(os.getenv("PORT", "7860"))
206
+ host = os.getenv("HOST", "0.0.0.0")
207
+
208
+ # Monkeypatch uvicorn.Config to use our port
209
+ import uvicorn
210
+ original_config_init = uvicorn.Config.__init__
211
+
212
+ def patched_init(self, *args, **kwargs):
213
+ kwargs['host'] = host
214
+ kwargs['port'] = port
215
+ return original_config_init(self, *args, **kwargs)
216
+
217
+ uvicorn.Config.__init__ = patched_init
218
+
219
+ # Run FastMCP with SSE transport
220
+ mcp.run(transport="sse")
EasyReportDataMCP/__pycache__/edgar_client.cpython-311.pyc ADDED
Binary file (28.8 kB). View file
 
EasyReportDataMCP/__pycache__/financial_analyzer.cpython-311.pyc ADDED
Binary file (11.6 kB). View file
 
EasyReportDataMCP/__pycache__/report_mcp.cpython-311.pyc ADDED
Binary file (21.7 kB). View file
 
EasyReportDataMCP/edgar_client.py ADDED
@@ -0,0 +1,693 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """EDGAR API Client Module"""
2
+
3
+ import requests
4
+ from requests.adapters import HTTPAdapter
5
+ from urllib3.util.retry import Retry
6
+ try:
7
+ from sec_edgar_api.EdgarClient import EdgarClient
8
+ except ImportError:
9
+ EdgarClient = None
10
+ import json
11
+ import time
12
+ from functools import wraps
13
+ import threading
14
+
15
+
16
+ class EdgarDataClient:
17
+ def __init__(self, user_agent="Juntao Peng Financial Report Metrics App ([email protected])"):
18
+ """Initialize EDGAR client"""
19
+ self.user_agent = user_agent
20
+ self.last_request_time = 0
21
+ self.min_request_interval = 0.11 # SEC allows 10 requests/second, use 0.11s to be safe
22
+ self.request_timeout = 30 # 30 seconds timeout for HTTP requests
23
+ self.max_retries = 3 # Maximum retry attempts
24
+ self._lock = threading.Lock() # Thread-safe rate limiting
25
+
26
+ # Configure requests session with connection pooling and retry logic
27
+ self.session = requests.Session()
28
+ retry_strategy = Retry(
29
+ total=3,
30
+ backoff_factor=1,
31
+ status_forcelist=[429, 500, 502, 503, 504],
32
+ allowed_methods=["HEAD", "GET", "OPTIONS"]
33
+ )
34
+ adapter = HTTPAdapter(
35
+ max_retries=retry_strategy,
36
+ pool_connections=10,
37
+ pool_maxsize=20,
38
+ pool_block=False
39
+ )
40
+ self.session.mount("http://", adapter)
41
+ self.session.mount("https://", adapter)
42
+ self.session.headers.update({"User-Agent": user_agent})
43
+
44
+ # Cache for frequently accessed data
45
+ self._company_cache = {} # Cache company info to avoid repeated calls
46
+ self._cache_ttl = 300 # 5 minutes cache TTL (for company info)
47
+ self._tickers_cache_ttl = 3600 # 1 hour for company tickers (rarely changes)
48
+ self._cache_timestamps = {}
49
+
50
+ # Fast lookup indexes for company tickers
51
+ self._ticker_index = {} # ticker -> company data
52
+ self._cik_index = {} # cik -> company data
53
+ self._name_lower_index = {} # lowercase name -> company data
54
+ self._index_loaded = False
55
+
56
+ if EdgarClient:
57
+ self.edgar = EdgarClient(user_agent=user_agent)
58
+ else:
59
+ self.edgar = None
60
+
61
+ def _rate_limit(self):
62
+ """Thread-safe rate limiting to comply with SEC API limits (10 requests/second)"""
63
+ with self._lock:
64
+ current_time = time.time()
65
+ time_since_last_request = current_time - self.last_request_time
66
+
67
+ if time_since_last_request < self.min_request_interval:
68
+ sleep_time = self.min_request_interval - time_since_last_request
69
+ time.sleep(sleep_time)
70
+
71
+ self.last_request_time = time.time()
72
+
73
+ def _is_cache_valid(self, cache_key):
74
+ """Check if cache entry is still valid"""
75
+ if cache_key not in self._cache_timestamps:
76
+ return False
77
+ age = time.time() - self._cache_timestamps[cache_key]
78
+ # Use longer TTL for company tickers list
79
+ ttl = self._tickers_cache_ttl if cache_key == "company_tickers_json" else self._cache_ttl
80
+ return age < ttl
81
+
82
+ def _get_cached(self, cache_key):
83
+ """Get cached data if valid"""
84
+ if self._is_cache_valid(cache_key):
85
+ return self._company_cache.get(cache_key)
86
+ return None
87
+
88
+ def _set_cache(self, cache_key, data):
89
+ """Set cache data with timestamp"""
90
+ self._company_cache[cache_key] = data
91
+ self._cache_timestamps[cache_key] = time.time()
92
+
93
+ def _make_request_with_retry(self, url, headers=None, use_session=True):
94
+ """Make HTTP request with retry logic and timeout"""
95
+ if headers is None:
96
+ headers = {"User-Agent": self.user_agent}
97
+
98
+ for attempt in range(self.max_retries):
99
+ try:
100
+ self._rate_limit()
101
+ if use_session:
102
+ response = self.session.get(url, headers=headers, timeout=self.request_timeout)
103
+ else:
104
+ response = requests.get(url, headers=headers, timeout=self.request_timeout)
105
+ response.raise_for_status()
106
+ return response
107
+ except requests.exceptions.Timeout:
108
+ print(f"Request timeout (attempt {attempt + 1}/{self.max_retries}): {url}")
109
+ if attempt == self.max_retries - 1:
110
+ raise
111
+ time.sleep(2 ** attempt) # Exponential backoff
112
+ except requests.exceptions.HTTPError as e:
113
+ if e.response.status_code == 429: # Too Many Requests
114
+ wait_time = 2 ** attempt
115
+ print(f"Rate limited, waiting {wait_time}s (attempt {attempt + 1}/{self.max_retries})")
116
+ time.sleep(wait_time)
117
+ if attempt == self.max_retries - 1:
118
+ raise
119
+ else:
120
+ raise
121
+ except Exception as e:
122
+ print(f"Request error (attempt {attempt + 1}/{self.max_retries}): {e}")
123
+ if attempt == self.max_retries - 1:
124
+ raise
125
+ time.sleep(2 ** attempt)
126
+
127
+ return None
128
+
129
+ def _load_company_tickers(self, force_refresh=False):
130
+ """Load and index company tickers data"""
131
+ cache_key = "company_tickers_json"
132
+
133
+ # Check if already loaded and cache is valid
134
+ if self._index_loaded and not force_refresh and self._is_cache_valid(cache_key):
135
+ return self._get_cached(cache_key)
136
+
137
+ # Check cache first
138
+ companies = self._get_cached(cache_key) if not force_refresh else None
139
+
140
+ if not companies:
141
+ try:
142
+ # Download company tickers
143
+ url = "https://www.sec.gov/files/company_tickers.json"
144
+ print(f"Downloading company tickers from SEC...")
145
+
146
+ response = self._make_request_with_retry(url)
147
+ if not response:
148
+ print("Failed to download company tickers")
149
+ return None
150
+
151
+ companies = response.json()
152
+ # Cache for 1 hour
153
+ self._set_cache(cache_key, companies)
154
+ print(f"Loaded {len(companies)} companies")
155
+ except Exception as e:
156
+ print(f"Error loading company tickers: {e}")
157
+ return None
158
+ else:
159
+ print(f"Using cached company tickers ({len(companies)} companies)")
160
+
161
+ # Build fast lookup indexes
162
+ self._ticker_index = {}
163
+ self._cik_index = {}
164
+ self._name_lower_index = {}
165
+
166
+ for _, company in companies.items():
167
+ cik = str(company["cik_str"]).zfill(10)
168
+ ticker = company["ticker"]
169
+ name = company["title"]
170
+
171
+ company_data = {
172
+ "cik": cik,
173
+ "name": name,
174
+ "ticker": ticker
175
+ }
176
+
177
+ # Index by ticker (lowercase for case-insensitive)
178
+ self._ticker_index[ticker.lower()] = company_data
179
+
180
+ # Index by CIK
181
+ self._cik_index[cik] = company_data
182
+
183
+ # Index by exact name (lowercase)
184
+ self._name_lower_index[name.lower()] = company_data
185
+
186
+ self._index_loaded = True
187
+ print(f"Built indexes: {len(self._ticker_index)} tickers, {len(self._cik_index)} CIKs")
188
+ return companies
189
+
190
+ def get_company_by_cik(self, cik):
191
+ """Fast lookup of company info by CIK (from cached tickers)"""
192
+ # Ensure data is loaded
193
+ self._load_company_tickers()
194
+
195
+ # Normalize CIK
196
+ cik_normalized = str(cik).zfill(10)
197
+
198
+ # Fast index lookup
199
+ return self._cik_index.get(cik_normalized)
200
+
201
+ def get_company_by_ticker(self, ticker):
202
+ """Fast lookup of company info by ticker"""
203
+ # Ensure data is loaded
204
+ self._load_company_tickers()
205
+
206
+ # Fast index lookup (case-insensitive)
207
+ return self._ticker_index.get(ticker.lower())
208
+
209
+ def search_company_by_name(self, company_name):
210
+ """Search company CIK by company name with caching and optimized search"""
211
+ try:
212
+ # Load company tickers and build indexes
213
+ companies = self._load_company_tickers()
214
+
215
+ if not companies:
216
+ return None
217
+
218
+ # Prepare search input
219
+ search_name = company_name.lower().strip()
220
+
221
+ # Optimize: Use fast index lookups first
222
+ # Priority 1: Exact ticker match (fastest - O(1) hash lookup)
223
+ if search_name in self._ticker_index:
224
+ return self._ticker_index[search_name].copy()
225
+
226
+ # Priority 2: Exact name match (fast - O(1) hash lookup)
227
+ if search_name in self._name_lower_index:
228
+ return self._name_lower_index[search_name].copy()
229
+
230
+ # Priority 3: Exact CIK match (fast - O(1) hash lookup)
231
+ # Handle CIK input (8-10 digits)
232
+ if search_name.isdigit() and len(search_name) >= 8:
233
+ cik_normalized = search_name.zfill(10)
234
+ if cik_normalized in self._cik_index:
235
+ return self._cik_index[cik_normalized].copy()
236
+
237
+ # Priority 4: Partial matches (slower - requires iteration)
238
+ # Only execute if exact matches fail
239
+ matches = []
240
+ for ticker_lower, company_data in self._ticker_index.items():
241
+ name_lower = company_data["name"].lower()
242
+
243
+ # Partial match in name or ticker
244
+ if search_name in name_lower or search_name in ticker_lower:
245
+ matches.append(company_data.copy())
246
+ # Optimize: Stop after finding 10 matches to avoid scanning all 13,000+
247
+ if len(matches) >= 10:
248
+ break
249
+
250
+ # Return first partial match
251
+ if matches:
252
+ return matches[0]
253
+ else:
254
+ return None
255
+
256
+ except Exception as e:
257
+ print(f"Error searching company: {e}")
258
+ return None
259
+
260
+ def get_company_info(self, cik):
261
+ """
262
+ Get basic company information with caching
263
+
264
+ Args:
265
+ cik (str): Company CIK code
266
+
267
+ Returns:
268
+ dict: Dictionary containing company information
269
+ """
270
+ if not self.edgar:
271
+ print("sec_edgar_api library not installed")
272
+ return None
273
+
274
+ # Check cache first
275
+ cache_key = f"info_{cik}"
276
+ cached = self._get_cached(cache_key)
277
+ if cached:
278
+ return cached
279
+
280
+ try:
281
+ # Add timeout wrapper for sec-edgar-api calls
282
+ import signal
283
+
284
+ def timeout_handler(signum, frame):
285
+ raise TimeoutError("SEC API call timeout")
286
+
287
+ # Set alarm for 30 seconds (only works on Unix-like systems)
288
+ try:
289
+ signal.signal(signal.SIGALRM, timeout_handler)
290
+ signal.alarm(30)
291
+ submissions = self.edgar.get_submissions(cik=cik)
292
+ signal.alarm(0) # Cancel alarm
293
+ except AttributeError:
294
+ # Windows doesn't support SIGALRM, use direct call
295
+ submissions = self.edgar.get_submissions(cik=cik)
296
+
297
+ result = {
298
+ "cik": cik,
299
+ "name": submissions.get("name", ""),
300
+ "tickers": submissions.get("tickers", []),
301
+ "sic": submissions.get("sic", ""),
302
+ "sic_description": submissions.get("sicDescription", "")
303
+ }
304
+
305
+ # Cache the result
306
+ self._set_cache(cache_key, result)
307
+ return result
308
+ except TimeoutError:
309
+ print(f"Timeout getting company info for CIK: {cik}")
310
+ return None
311
+ except Exception as e:
312
+ print(f"Error getting company info: {e}")
313
+ return None
314
+
315
+ def get_company_filings(self, cik, form_types=None):
316
+ """
317
+ Get all company filing documents with caching
318
+
319
+ Args:
320
+ cik (str): Company CIK code
321
+ form_types (list): List of form types, e.g., ['10-K', '10-Q'], None for all types
322
+
323
+ Returns:
324
+ list: List of filing documents
325
+ """
326
+ if not self.edgar:
327
+ print("sec_edgar_api library not installed")
328
+ return []
329
+
330
+ # Check cache first (cache all filings, filter later)
331
+ cache_key = f"filings_{cik}"
332
+ cached = self._get_cached(cache_key)
333
+
334
+ if not cached:
335
+ try:
336
+ # Add timeout wrapper
337
+ import signal
338
+
339
+ def timeout_handler(signum, frame):
340
+ raise TimeoutError("SEC API call timeout")
341
+
342
+ try:
343
+ signal.signal(signal.SIGALRM, timeout_handler)
344
+ signal.alarm(30)
345
+ submissions = self.edgar.get_submissions(cik=cik)
346
+ signal.alarm(0)
347
+ except AttributeError:
348
+ # Windows fallback
349
+ submissions = self.edgar.get_submissions(cik=cik)
350
+
351
+ # Extract filing information
352
+ filings = []
353
+ recent = submissions.get("filings", {}).get("recent", {})
354
+
355
+ # Get data from each field
356
+ form_types_list = recent.get("form", [])
357
+ filing_dates = recent.get("filingDate", [])
358
+ accession_numbers = recent.get("accessionNumber", [])
359
+ primary_documents = recent.get("primaryDocument", [])
360
+
361
+ # Iterate through all filings
362
+ for i in range(len(form_types_list)):
363
+ filing_date = filing_dates[i] if i < len(filing_dates) else ""
364
+ accession_number = accession_numbers[i] if i < len(accession_numbers) else ""
365
+ primary_document = primary_documents[i] if i < len(primary_documents) else ""
366
+
367
+ filing = {
368
+ "form_type": form_types_list[i],
369
+ "filing_date": filing_date,
370
+ "accession_number": accession_number,
371
+ "primary_document": primary_document
372
+ }
373
+
374
+ filings.append(filing)
375
+
376
+ # Cache all filings
377
+ self._set_cache(cache_key, filings)
378
+ cached = filings
379
+
380
+ except TimeoutError:
381
+ print(f"Timeout getting company filings for CIK: {cik}")
382
+ return []
383
+ except Exception as e:
384
+ print(f"Error getting company filings: {e}")
385
+ return []
386
+
387
+ # Filter by form type if specified
388
+ if form_types:
389
+ return [f for f in cached if f.get("form_type") in form_types]
390
+ return cached
391
+
392
+ def get_company_facts(self, cik):
393
+ """
394
+ Get all company financial facts data with caching and timeout
395
+
396
+ Args:
397
+ cik (str): Company CIK code
398
+
399
+ Returns:
400
+ dict: Company financial facts data
401
+ """
402
+ if not self.edgar:
403
+ print("sec_edgar_api library not installed")
404
+ return {}
405
+
406
+ # Check cache first
407
+ cache_key = f"facts_{cik}"
408
+ cached = self._get_cached(cache_key)
409
+ if cached:
410
+ return cached
411
+
412
+ try:
413
+ # Add timeout wrapper
414
+ import signal
415
+
416
+ def timeout_handler(signum, frame):
417
+ raise TimeoutError("SEC API call timeout")
418
+
419
+ try:
420
+ signal.signal(signal.SIGALRM, timeout_handler)
421
+ signal.alarm(45) # 45 seconds for facts (larger dataset)
422
+ facts = self.edgar.get_company_facts(cik=cik)
423
+ signal.alarm(0)
424
+ except AttributeError:
425
+ # Windows fallback
426
+ facts = self.edgar.get_company_facts(cik=cik)
427
+
428
+ # Cache the result
429
+ self._set_cache(cache_key, facts)
430
+ return facts
431
+ except TimeoutError:
432
+ print(f"Timeout getting company facts for CIK: {cik}")
433
+ return {}
434
+ except Exception as e:
435
+ print(f"Error getting company facts: {e}")
436
+ return {}
437
+
438
+ def get_financial_data_for_period(self, cik, period):
439
+ """
440
+ Get financial data for a specific period (supports annual and quarterly)
441
+
442
+ Args:
443
+ cik (str): Company CIK code
444
+ period (str): Period in format 'YYYY' or 'YYYYQX' (e.g., '2025' or '2025Q3')
445
+
446
+ Returns:
447
+ dict: Financial data dictionary
448
+ """
449
+ if not self.edgar:
450
+ print("sec_edgar_api library not installed")
451
+ return {}
452
+
453
+ try:
454
+ # Get company financial facts
455
+ facts = self.get_company_facts(cik)
456
+
457
+ if not facts:
458
+ return {}
459
+
460
+ # Extract us-gaap and ifrs-full financial data (20-F may use IFRS)
461
+ us_gaap = facts.get("facts", {}).get("us-gaap", {})
462
+ ifrs_full = facts.get("facts", {}).get("ifrs-full", {})
463
+
464
+ # Define financial metrics and their XBRL tags
465
+ # Include multiple possible tags to improve match rate (including US-GAAP and IFRS tags)
466
+ financial_metrics = {
467
+ "total_revenue": ["Revenues", "RevenueFromContractWithCustomerExcludingAssessedTax", "RevenueFromContractWithCustomerIncludingAssessedTax", "SalesRevenueNet", "RevenueFromContractWithCustomer", "Revenue"],
468
+ "net_income": ["NetIncomeLoss", "ProfitLoss", "NetIncome", "ProfitLossAttributableToOwnersOfParent"],
469
+ "earnings_per_share": ["EarningsPerShareBasic", "EarningsPerShare", "BasicEarningsPerShare", "BasicEarningsLossPerShare"],
470
+ "operating_expenses": ["OperatingExpenses", "OperatingCostsAndExpenses", "OperatingExpensesExcludingDepreciationAndAmortization", "CostsAndExpenses", "GeneralAndAdministrativeExpense", "CostOfRevenue", "ResearchAndDevelopmentExpense", "SellingAndMarketingExpense"],
471
+ "operating_cash_flow": ["NetCashProvidedByUsedInOperatingActivities", "NetCashProvidedUsedInOperatingActivities", "NetCashFlowsFromUsedInOperatingActivities", "CashFlowsFromUsedInOperatingActivities"],
472
+ }
473
+
474
+ # Store result
475
+ result = {"period": period}
476
+
477
+ # Determine target form types to search
478
+ if 'Q' in period:
479
+ # Quarterly data, mainly search 10-Q (20-F usually doesn't have quarterly reports)
480
+ target_forms = ["10-Q"]
481
+ target_forms_annual = ["10-K", "20-F"] # for fallback
482
+ year = int(period.split('Q')[0])
483
+ quarter = period.split('Q')[1]
484
+ else:
485
+ # Annual data, search 10-K and 20-F annual forms
486
+ target_forms = ["10-K", "20-F"]
487
+ target_forms_annual = target_forms
488
+ year = int(period)
489
+ quarter = None
490
+
491
+ # Detect if company uses 20-F (foreign filer)
492
+ is_20f_filer = False
493
+ all_filings = self.get_company_filings(cik)
494
+ if all_filings:
495
+ form_types_used = set(f.get('form_type', '') for f in all_filings[:20])
496
+ if '20-F' in form_types_used and '10-K' not in form_types_used:
497
+ is_20f_filer = True
498
+
499
+ # Get company filings to find accession number and primary document
500
+ filings = self.get_company_filings(cik, form_types=target_forms)
501
+ filings_map = {} # Map: form_year -> {accession_number, primary_document, filing_date, form_type}
502
+
503
+ # Build filing map for quick lookup
504
+ for filing in filings:
505
+ form_type = filing.get("form_type", "")
506
+ filing_date = filing.get("filing_date", "")
507
+ accession_number = filing.get("accession_number", "")
508
+ primary_document = filing.get("primary_document", "")
509
+
510
+ if filing_date and accession_number:
511
+ # Extract year from filing_date (format: YYYY-MM-DD)
512
+ file_year = int(filing_date[:4]) if len(filing_date) >= 4 else 0
513
+
514
+ # Store filing if it matches the period year
515
+ # For 20-F, also check year-1 (fiscal year may differ from filing year)
516
+ if file_year == year or (is_20f_filer and form_type == '20-F' and file_year in [year - 1, year + 1]):
517
+ key = f"{form_type}_{file_year}"
518
+ if key not in filings_map:
519
+ filings_map[key] = {
520
+ "accession_number": accession_number,
521
+ "primary_document": primary_document,
522
+ "form_type": form_type,
523
+ "filing_date": filing_date,
524
+ "file_year": file_year
525
+ }
526
+
527
+ # Iterate through each financial metric
528
+ for metric_key, metric_tags in financial_metrics.items():
529
+ # Support multiple possible tags
530
+ for metric_tag in metric_tags:
531
+ # Search both US-GAAP and IFRS tags
532
+ # For 20-F filers, prioritize IFRS
533
+ metric_data = None
534
+ data_source = None
535
+
536
+ if is_20f_filer:
537
+ # Check IFRS first for 20-F filers
538
+ if metric_tag in ifrs_full:
539
+ metric_data = ifrs_full[metric_tag]
540
+ data_source = "ifrs-full"
541
+ elif metric_tag in us_gaap:
542
+ metric_data = us_gaap[metric_tag]
543
+ data_source = "us-gaap"
544
+ else:
545
+ # Check US-GAAP first for 10-K filers
546
+ if metric_tag in us_gaap:
547
+ metric_data = us_gaap[metric_tag]
548
+ data_source = "us-gaap"
549
+ elif metric_tag in ifrs_full:
550
+ metric_data = ifrs_full[metric_tag]
551
+ data_source = "ifrs-full"
552
+
553
+ if metric_data:
554
+ units = metric_data.get("units", {})
555
+
556
+ # Find USD unit data (supports USD and USD/shares)
557
+ usd_data = None
558
+ if "USD" in units:
559
+ usd_data = units["USD"]
560
+ elif "USD/shares" in units and metric_key == "earnings_per_share":
561
+ # EPS uses USD/shares unit
562
+ usd_data = units["USD/shares"]
563
+
564
+ if usd_data:
565
+ # Try exact match first, then loose match
566
+ matched_entry = None
567
+
568
+ # Search for data in the specified period
569
+ for entry in usd_data:
570
+ form = entry.get("form", "")
571
+ fy = entry.get("fy", 0)
572
+ fp = entry.get("fp", "")
573
+ end_date = entry.get("end", "")
574
+
575
+ if not end_date or len(end_date) < 4:
576
+ continue
577
+
578
+ entry_year = int(end_date[:4])
579
+
580
+ # Check if form type matches
581
+ if form in target_forms:
582
+ if quarter:
583
+ # Quarterly data match
584
+ if entry_year == year and fp == f"Q{quarter}":
585
+ # If already matched, compare end date, choose the latest
586
+ if matched_entry:
587
+ if entry.get("end", "") > matched_entry.get("end", ""):
588
+ matched_entry = entry
589
+ else:
590
+ matched_entry = entry
591
+ else:
592
+ # Annual data match - prioritize fiscal year (fy) field
593
+ # Strategy 1: Exact match by fiscal year
594
+ if fy == year and (fp == "FY" or fp == "" or not fp):
595
+ # If already matched, compare end date, choose the latest
596
+ if matched_entry:
597
+ if entry.get("end", "") > matched_entry.get("end", ""):
598
+ matched_entry = entry
599
+ else:
600
+ matched_entry = entry
601
+ # Strategy 2: Match by end date year (when fy not available or doesn't match)
602
+ elif not matched_entry and entry_year == year and (fp == "FY" or fp == "" or not fp):
603
+ matched_entry = entry
604
+ # Strategy 3: Allow fy to differ by 1 year (fiscal year vs calendar year mismatch)
605
+ elif not matched_entry and fy > 0 and abs(fy - year) <= 1 and (fp == "FY" or fp == "" or not fp):
606
+ matched_entry = entry
607
+ # Strategy 4: Enhanced matching for 20-F - check frame field and end date
608
+ elif not matched_entry and form == "20-F":
609
+ frame = entry.get("frame", "")
610
+ # Match if CY{year} in frame OR end date contains year OR fiscal year within range
611
+ if (f"CY{year}" in frame or
612
+ (str(year) in end_date and len(end_date) >= 4 and end_date[:4] == str(year)) or
613
+ (fy > 0 and abs(fy - year) <= 1)):
614
+ # Additional check: prefer entries with FY period
615
+ if fp == "FY" or fp == "" or not fp:
616
+ matched_entry = entry
617
+
618
+ # If quarterly data not found, try finding from annual report (fallback strategy)
619
+ if not matched_entry and quarter and target_forms_annual:
620
+ for entry in usd_data:
621
+ form = entry.get("form", "")
622
+ end_date = entry.get("end", "")
623
+ fp = entry.get("fp", "")
624
+
625
+ if form in target_forms_annual and end_date:
626
+ # Check if end date is within this quarter range
627
+ if str(year) in end_date and f"Q{quarter}" in fp:
628
+ matched_entry = entry
629
+ break
630
+
631
+ # Apply matched data
632
+ if matched_entry:
633
+ result[metric_key] = matched_entry.get("val", 0)
634
+
635
+ # Get form and accession info
636
+ form_type = matched_entry.get("form", "")
637
+ accn_from_facts = matched_entry.get('accn', '').replace('-', '')
638
+ filed_date = matched_entry.get('filed', '')
639
+ filed_year = int(filed_date[:4]) if filed_date and len(filed_date) >= 4 else year
640
+
641
+ # Try to get accession_number and primary_document from filings
642
+ # For 20-F, try multiple year keys since filing year may differ
643
+ filing_info = None
644
+ possible_keys = [f"{form_type}_{year}"]
645
+ if form_type == "20-F":
646
+ possible_keys.extend([f"20-F_{filed_year}", f"20-F_{year-1}", f"20-F_{year+1}"])
647
+
648
+ for filing_key in possible_keys:
649
+ if filing_key in filings_map:
650
+ filing_info = filings_map[filing_key]
651
+ break
652
+
653
+ if filing_info:
654
+ # Use filing info from get_company_filings
655
+ accession_number = filing_info["accession_number"].replace('-', '')
656
+ primary_document = filing_info["primary_document"]
657
+
658
+ # Generate complete source URL
659
+ if primary_document:
660
+ result["source_url"] = f"https://www.sec.gov/Archives/edgar/data/{cik}/{accession_number}/{primary_document}"
661
+ else:
662
+ result["source_url"] = f"https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK={cik}&type={form_type}&dateb=&owner=exclude&count=100"
663
+ else:
664
+ # Fallback to company browse page if filing not found
665
+ result["source_url"] = f"https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK={cik}&type={form_type}&dateb=&owner=exclude&count=100"
666
+
667
+ result["source_form"] = form_type
668
+ result["data_source"] = data_source
669
+
670
+ # Add detailed information
671
+ result[f"{metric_key}_details"] = {
672
+ "tag": metric_tag,
673
+ "form": matched_entry.get("form", ""),
674
+ "fy": matched_entry.get("fy", 0),
675
+ "fp": matched_entry.get("fp", ""),
676
+ "val": matched_entry.get("val", 0),
677
+ "start": matched_entry.get("start", ""),
678
+ "end": matched_entry.get("end", ""),
679
+ "accn": matched_entry.get("accn", ""),
680
+ "filed": matched_entry.get("filed", ""),
681
+ "frame": matched_entry.get("frame", ""),
682
+ "data_source": data_source
683
+ }
684
+
685
+ # If data is found, break out of tag loop
686
+ if metric_key in result:
687
+ break
688
+
689
+ return result
690
+ except Exception as e:
691
+ print(f"Error getting financial data for period {period}: {e}")
692
+ return {}
693
+
EasyReportDataMCP/financial_analyzer.py ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Financial Data Analysis Module"""
2
+
3
+ from .edgar_client import EdgarDataClient
4
+ from datetime import datetime
5
+ import json
6
+
7
+
8
+ class FinancialAnalyzer:
9
+ def __init__(self, user_agent="Juntao Peng Financial Report Metrics App ([email protected])"):
10
+ """
11
+ Initialize financial analyzer
12
+
13
+ Args:
14
+ user_agent (str): User agent string for identifying request source
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
21
+
22
+ Args:
23
+ company_input (str): Company name, ticker, or CIK
24
+
25
+ Returns:
26
+ dict: Company information
27
+ """
28
+ # Strip whitespace
29
+ company_input = company_input.strip()
30
+
31
+ # Strategy 1: If input is numeric and looks like CIK (8-10 digits), use fast CIK lookup
32
+ if company_input.isdigit() and len(company_input) >= 8:
33
+ # Normalize CIK to 10 digits
34
+ cik = company_input.zfill(10)
35
+
36
+ # Try fast lookup first (from cached tickers)
37
+ basic_info = self.edgar_client.get_company_by_cik(cik)
38
+
39
+ if basic_info:
40
+ # Fast path succeeded, now get detailed info
41
+ company_info = self.edgar_client.get_company_info(cik)
42
+ if company_info:
43
+ return company_info
44
+ else:
45
+ # Fallback to basic info if detailed fetch fails
46
+ return {
47
+ "cik": basic_info['cik'],
48
+ "name": basic_info['name'],
49
+ "tickers": [basic_info['ticker']] if basic_info.get('ticker') else [],
50
+ "_source": "basic_cik_lookup"
51
+ }
52
+ else:
53
+ # CIK not found in cache, try full API call
54
+ company_info = self.edgar_client.get_company_info(cik)
55
+ if company_info:
56
+ return company_info
57
+ else:
58
+ return {"error": "Company not found for specified CIK"}
59
+
60
+ # Strategy 2: Check if it looks like a ticker (short uppercase)
61
+ input_length = len(company_input)
62
+ is_likely_ticker = input_length <= 5 and company_input.isupper()
63
+
64
+ if is_likely_ticker:
65
+ # Try fast ticker lookup first
66
+ basic_info = self.edgar_client.get_company_by_ticker(company_input)
67
+
68
+ if basic_info:
69
+ # Fast ticker lookup succeeded - return enriched basic info
70
+ return {
71
+ "cik": basic_info['cik'],
72
+ "name": basic_info['name'],
73
+ "tickers": [basic_info['ticker']] if basic_info.get('ticker') else [],
74
+ "ein": None, # Not available in basic search
75
+ "fiscal_year_end": None, # Not available in basic search
76
+ "sic_description": None, # Not available in basic search
77
+ "_source": "quick_ticker_search",
78
+ "_note": "Basic info from ticker search. Use get_company_info for full details."
79
+ }
80
+
81
+ # Strategy 3: General search by name/ticker
82
+ # This returns basic info: {cik, name, ticker}
83
+ basic_info = self.edgar_client.search_company_by_name(company_input)
84
+
85
+ if not basic_info:
86
+ return {"error": "No matching company found"}
87
+
88
+ # Strategy 4: Decide whether to fetch detailed info
89
+ # For ticker-like searches, return basic info quickly
90
+ if is_likely_ticker:
91
+ # Quick response with basic info
92
+ return {
93
+ "cik": basic_info['cik'],
94
+ "name": basic_info['name'],
95
+ "tickers": [basic_info['ticker']] if basic_info.get('ticker') else [],
96
+ "ein": None,
97
+ "fiscal_year_end": None,
98
+ "sic_description": None,
99
+ "_source": "quick_search",
100
+ "_note": "Basic info from ticker search. Use get_company_info for full details."
101
+ }
102
+
103
+ # For name searches, fetch detailed info (worth the extra API call)
104
+ company_info = self.edgar_client.get_company_info(basic_info['cik'])
105
+
106
+ if company_info:
107
+ return company_info
108
+ else:
109
+ # Fallback to basic info if detailed fetch fails
110
+ return {
111
+ "cik": basic_info['cik'],
112
+ "name": basic_info['name'],
113
+ "tickers": [basic_info['ticker']] if basic_info.get('ticker') else [],
114
+ "_source": "basic_search_fallback"
115
+ }
116
+
117
+ def get_company_filings_list(self, cik, form_types=None):
118
+ """
119
+ Get company filings list
120
+
121
+ Args:
122
+ cik (str): Company CIK
123
+ form_types (list): List of form types (default: ['10-K', '10-Q'])
124
+
125
+ Returns:
126
+ list: Filings list
127
+ """
128
+ if form_types is None:
129
+ form_types = ['10-K', '10-Q']
130
+ filings = self.edgar_client.get_company_filings(cik, form_types)
131
+ return filings
132
+
133
+ def extract_financial_metrics(self, cik, years=3):
134
+ """
135
+ Extract financial metrics for specified number of years
136
+
137
+ Args:
138
+ cik (str): Company CIK
139
+ years (int): Number of years to extract, default is 3 years
140
+
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
147
+ filings_10k = self.edgar_client.get_company_filings(cik, ['10-K'])
148
+ filings_20f = self.edgar_client.get_company_filings(cik, ['20-F'])
149
+ all_annual_filings = filings_10k + filings_20f
150
+
151
+ if not all_annual_filings:
152
+ return []
153
+
154
+ # Detect if company is a 20-F filer (foreign company)
155
+ is_20f_filer = len(filings_20f) > 0 and len(filings_10k) == 0
156
+ has_quarterly = False # 20-F filers typically don't have quarterly reports
157
+
158
+ # Step 2: Extract filing years from annual reports
159
+ # Use filing_date to determine the years we should query
160
+ filing_year_map = {} # Map: filing_year -> list of filings
161
+
162
+ for filing in all_annual_filings:
163
+ filing_date = filing.get('filing_date', '')
164
+ if filing_date and len(filing_date) >= 4:
165
+ try:
166
+ file_year = int(filing_date[:4])
167
+ if file_year not in filing_year_map:
168
+ filing_year_map[file_year] = []
169
+ filing_year_map[file_year].append(filing)
170
+ except ValueError:
171
+ continue
172
+
173
+ if not filing_year_map:
174
+ return []
175
+
176
+ # Step 3: Sort years in descending order and take the most recent N years
177
+ sorted_years = sorted(filing_year_map.keys(), reverse=True)
178
+ target_years = sorted_years[:years]
179
+
180
+ # Step 4: For each target year, we need to find the fiscal year from Company Facts
181
+ # Get company facts to map filing years to fiscal years
182
+ facts = self.edgar_client.get_company_facts(cik)
183
+ filing_to_fiscal_year = {} # Map: filing_year -> fiscal_year
184
+
185
+ if facts:
186
+ # Try to map filing years to fiscal years using Company Facts
187
+ for data_source in ["us-gaap", "ifrs-full"]:
188
+ if data_source in facts.get("facts", {}):
189
+ source_data = facts["facts"][data_source]
190
+
191
+ # Look for Revenue tag to get fiscal year mapping
192
+ revenue_tags = ["Revenues", "RevenueFromContractWithCustomerExcludingAssessedTax",
193
+ "Revenue", "RevenueFromContractWithCustomer"]
194
+
195
+ for tag in revenue_tags:
196
+ if tag in source_data:
197
+ units = source_data[tag].get("units", {})
198
+ if "USD" in units:
199
+ for entry in units["USD"]:
200
+ form = entry.get("form", "")
201
+ fy = entry.get("fy", 0)
202
+ filed = entry.get("filed", "") # Filing date
203
+ fp = entry.get("fp", "")
204
+
205
+ # Map filing year to fiscal year
206
+ if form in ["10-K", "20-F"] and fy > 0 and filed and (fp == "FY" or not fp):
207
+ if len(filed) >= 10: # Format: YYYY-MM-DD
208
+ try:
209
+ file_year = int(filed[:4])
210
+ # Store the mapping: filing_year -> fiscal_year
211
+ if file_year not in filing_to_fiscal_year:
212
+ filing_to_fiscal_year[file_year] = fy
213
+ except ValueError:
214
+ continue
215
+ break # Found revenue tag, no need to check more
216
+
217
+ # Step 5: Generate period list for target years
218
+ # For each year: FY -> Q4 -> Q3 -> Q2 -> Q1 (descending order)
219
+ # For 20-F filers: only FY (no quarterly data)
220
+ periods = []
221
+ for file_year in target_years:
222
+ # Try to get fiscal year from mapping, otherwise use filing year
223
+ fiscal_year = filing_to_fiscal_year.get(file_year, file_year)
224
+
225
+ # First add annual data for this fiscal year
226
+ periods.append({
227
+ 'period': str(fiscal_year),
228
+ 'type': 'annual',
229
+ 'fiscal_year': fiscal_year,
230
+ 'filing_year': file_year
231
+ })
232
+
233
+ # Only add quarterly data for 10-K filers (not for 20-F filers)
234
+ if not is_20f_filer:
235
+ # Then add quarterly data in descending order: Q4, Q3, Q2, Q1
236
+ for quarter in range(4, 0, -1):
237
+ periods.append({
238
+ 'period': f"{fiscal_year}Q{quarter}",
239
+ 'type': 'quarterly',
240
+ 'fiscal_year': fiscal_year,
241
+ 'filing_year': file_year
242
+ })
243
+
244
+ # Step 6: Get financial data for each period
245
+ for idx, period_info in enumerate(periods):
246
+ period = period_info['period']
247
+ fiscal_year = period_info['fiscal_year']
248
+
249
+ data = self.edgar_client.get_financial_data_for_period(cik, period)
250
+
251
+ if data and "period" in data:
252
+ # Add fiscal year prefix for annual data
253
+ if period_info['type'] == 'annual':
254
+ data["period"] = f"FY{fiscal_year}"
255
+
256
+ # Add sequence number to maintain order
257
+ data["_sequence"] = idx
258
+
259
+ financial_data.append(data)
260
+
261
+ return financial_data
262
+
263
+ def get_latest_financial_data(self, cik):
264
+ """
265
+ Get latest financial data
266
+
267
+ Args:
268
+ cik (str): Company CIK
269
+
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'])
276
+ filings = filings_10k + filings_20f
277
+
278
+ if not filings:
279
+ return {}
280
+
281
+ # Get latest filing year
282
+ latest_filing_year = None
283
+ for filing in filings:
284
+ if 'filing_date' in filing and filing['filing_date']:
285
+ try:
286
+ filing_year = int(filing['filing_date'][:4])
287
+ if latest_filing_year is None or filing_year > latest_filing_year:
288
+ latest_filing_year = filing_year
289
+ except ValueError:
290
+ continue
291
+
292
+ if latest_filing_year is None:
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
+ """
300
+ Format financial data for display
301
+
302
+ Args:
303
+ financial_data (dict or list): Financial data
304
+
305
+ Returns:
306
+ dict or list: Formatted financial data
307
+ """
308
+ if isinstance(financial_data, list):
309
+ # Sort by _sequence to maintain correct order (FY -> Q4 -> Q3 -> Q2 -> Q1)
310
+ sorted_data = sorted(financial_data, key=lambda x: x.get("_sequence", 999))
311
+ formatted_data = []
312
+ for data in sorted_data:
313
+ formatted_data.append(self._format_single_financial_data(data))
314
+ return formatted_data
315
+ else:
316
+ return self._format_single_financial_data(financial_data)
317
+
318
+ def _format_single_financial_data(self, data):
319
+ """
320
+ Format single financial data entry
321
+
322
+ Args:
323
+ data (dict): Financial data
324
+
325
+ Returns:
326
+ dict: Formatted financial data
327
+ """
328
+ formatted = data.copy()
329
+
330
+ # Ensure all key fields exist, even if None
331
+ key_fields = ['total_revenue', 'net_income', 'earnings_per_share', 'operating_expenses', 'operating_cash_flow', 'source_url', 'source_form']
332
+ for key in key_fields:
333
+ if key not in formatted:
334
+ formatted[key] = None
335
+
336
+ # No longer perform unit conversion, keep original values
337
+ # Format EPS, keep two decimal places
338
+ if 'earnings_per_share' in formatted and isinstance(formatted['earnings_per_share'], (int, float)):
339
+ formatted['earnings_per_share'] = round(formatted['earnings_per_share'], 2)
340
+
341
+ return formatted
342
+
EasyReportDataMCP/mcp_server_sse.py ADDED
@@ -0,0 +1,615 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Server for Hugging Face Space Deployment
3
+ Uses Server-Sent Events (SSE) transport for remote MCP access
4
+ Fully compatible with MCP clients like Claude Desktop
5
+ Returns clean JSON without emoji formatting
6
+ """
7
+
8
+ from fastapi import FastAPI, Request, HTTPException
9
+ from fastapi.responses import StreamingResponse, JSONResponse, HTMLResponse, FileResponse
10
+ from fastapi.staticfiles import StaticFiles
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from pydantic import BaseModel
13
+ from typing import Dict, Any, List, Optional
14
+ import json
15
+ import asyncio
16
+ from datetime import datetime
17
+ from edgar_client import EdgarDataClient
18
+ from financial_analyzer import FinancialAnalyzer
19
+ import time
20
+ import sys
21
+ import signal
22
+ import os
23
+ from pathlib import Path
24
+ from contextlib import contextmanager
25
+
26
+ # Initialize FastAPI app
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.1"
31
+ )
32
+
33
+ # Server startup time for monitoring
34
+ server_start_time = time.time()
35
+ request_count = 0
36
+ error_count = 0
37
+
38
+ # Configure CORS for remote access
39
+ app.add_middleware(
40
+ CORSMiddleware,
41
+ allow_origins=["*"],
42
+ allow_credentials=True,
43
+ allow_methods=["*"],
44
+ allow_headers=["*"],
45
+ )
46
+
47
+ # Request tracking middleware with timeout protection
48
+ @app.middleware("http")
49
+ async def track_requests(request: Request, call_next):
50
+ global request_count, error_count
51
+ request_count += 1
52
+ start_time = time.time()
53
+
54
+ try:
55
+ # Set a timeout for the entire request
56
+ response = await asyncio.wait_for(
57
+ call_next(request),
58
+ timeout=120.0 # 2 minutes total timeout
59
+ )
60
+ if response.status_code >= 400:
61
+ error_count += 1
62
+ return response
63
+ except asyncio.TimeoutError:
64
+ error_count += 1
65
+ print(f"Request timeout after {time.time() - start_time:.2f}s: {request.url.path}")
66
+ return JSONResponse(
67
+ status_code=504,
68
+ content={"error": "Request timeout", "message": "The request took too long to process"}
69
+ )
70
+ except Exception as e:
71
+ error_count += 1
72
+ print(f"Request error: {e}")
73
+ raise
74
+
75
+ # Initialize EDGAR clients
76
+ edgar_client = EdgarDataClient(
77
+ user_agent="Juntao Peng Financial Report Metrics App ([email protected])"
78
+ )
79
+
80
+ financial_analyzer = FinancialAnalyzer(
81
+ user_agent="Juntao Peng Financial Report Metrics App ([email protected])"
82
+ )
83
+
84
+ # Preload company tickers data on startup for better performance
85
+ print("[Startup] Preloading company tickers data...")
86
+ try:
87
+ edgar_client._load_company_tickers()
88
+ print("[Startup] Company tickers preloaded successfully")
89
+ except Exception as e:
90
+ print(f"[Startup] Warning: Failed to preload company tickers: {e}")
91
+ print("[Startup] Will load on first request")
92
+
93
+
94
+ # ==================== MCP Protocol Implementation ====================
95
+
96
+ class MCPRequest(BaseModel):
97
+ jsonrpc: str = "2.0"
98
+ id: Optional[Any] = None
99
+ method: str
100
+ params: Optional[Dict[str, Any]] = None
101
+
102
+
103
+ class MCPResponse(BaseModel):
104
+ jsonrpc: str = "2.0"
105
+ id: Optional[Any] = None
106
+ result: Optional[Any] = None
107
+ error: Optional[Dict[str, Any]] = None
108
+
109
+
110
+ # MCP Tools Definition
111
+ MCP_TOOLS = [
112
+ {
113
+ "name": "search_company",
114
+ "description": "Search for a company by name in SEC EDGAR database. Returns company CIK, name, and ticker symbol.",
115
+ "inputSchema": {
116
+ "type": "object",
117
+ "properties": {
118
+ "company_name": {
119
+ "type": "string",
120
+ "description": "Company name to search (e.g., 'Microsoft', 'Apple', 'Tesla')"
121
+ }
122
+ },
123
+ "required": ["company_name"]
124
+ }
125
+ },
126
+ {
127
+ "name": "get_company_info",
128
+ "description": "Get detailed company information including name, tickers, SIC code, and industry description.",
129
+ "inputSchema": {
130
+ "type": "object",
131
+ "properties": {
132
+ "cik": {
133
+ "type": "string",
134
+ "description": "Company CIK code (10-digit format, e.g., '0000789019')"
135
+ }
136
+ },
137
+ "required": ["cik"]
138
+ }
139
+ },
140
+ {
141
+ "name": "get_company_filings",
142
+ "description": "Get list of company SEC filings (10-K, 10-Q, 20-F, etc.) with filing dates and document links.",
143
+ "inputSchema": {
144
+ "type": "object",
145
+ "properties": {
146
+ "cik": {
147
+ "type": "string",
148
+ "description": "Company CIK code"
149
+ },
150
+ "form_types": {
151
+ "type": "array",
152
+ "items": {"type": "string"},
153
+ "description": "Optional: Filter by form types (e.g., ['10-K', '10-Q'])"
154
+ }
155
+ },
156
+ "required": ["cik"]
157
+ }
158
+ },
159
+ {
160
+ "name": "get_financial_data",
161
+ "description": "Get financial data for a specific period including revenue, net income, EPS, operating expenses, and cash flow.",
162
+ "inputSchema": {
163
+ "type": "object",
164
+ "properties": {
165
+ "cik": {
166
+ "type": "string",
167
+ "description": "Company CIK code"
168
+ },
169
+ "period": {
170
+ "type": "string",
171
+ "description": "Period in format 'YYYY' for annual or 'YYYYQX' for quarterly (e.g., '2024', '2024Q3')"
172
+ }
173
+ },
174
+ "required": ["cik", "period"]
175
+ }
176
+ },
177
+ {
178
+ "name": "extract_financial_metrics",
179
+ "description": "Extract comprehensive financial metrics for multiple years including both annual and quarterly data. Returns data in chronological order (newest first).",
180
+ "inputSchema": {
181
+ "type": "object",
182
+ "properties": {
183
+ "cik": {
184
+ "type": "string",
185
+ "description": "Company CIK code"
186
+ },
187
+ "years": {
188
+ "type": "integer",
189
+ "description": "Number of recent years to extract (1-10, default: 3)",
190
+ "minimum": 1,
191
+ "maximum": 10,
192
+ "default": 3
193
+ }
194
+ },
195
+ "required": ["cik"]
196
+ }
197
+ },
198
+ {
199
+ "name": "get_latest_financial_data",
200
+ "description": "Get the most recent financial data available for a company.",
201
+ "inputSchema": {
202
+ "type": "object",
203
+ "properties": {
204
+ "cik": {
205
+ "type": "string",
206
+ "description": "Company CIK code"
207
+ }
208
+ },
209
+ "required": ["cik"]
210
+ }
211
+ },
212
+ {
213
+ "name": "advanced_search_company",
214
+ "description": "Advanced search supporting both company name and CIK code. Automatically detects input type.",
215
+ "inputSchema": {
216
+ "type": "object",
217
+ "properties": {
218
+ "company_input": {
219
+ "type": "string",
220
+ "description": "Company name, ticker, or CIK code"
221
+ }
222
+ },
223
+ "required": ["company_input"]
224
+ }
225
+ }
226
+ ]
227
+
228
+
229
+ @contextmanager
230
+ def timeout_context(seconds):
231
+ """Context manager for timeout on Unix-like systems"""
232
+ def timeout_handler(signum, frame):
233
+ raise TimeoutError(f"Operation timeout after {seconds} seconds")
234
+
235
+ # Only works on Unix-like systems
236
+ try:
237
+ old_handler = signal.signal(signal.SIGALRM, timeout_handler)
238
+ signal.alarm(seconds)
239
+ try:
240
+ yield
241
+ finally:
242
+ signal.alarm(0)
243
+ signal.signal(signal.SIGALRM, old_handler)
244
+ except (AttributeError, ValueError):
245
+ # Windows or signal not available
246
+ yield
247
+
248
+
249
+ def execute_tool(tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
250
+ """Execute MCP tool and return clean JSON result with enhanced timeout protection"""
251
+ try:
252
+ # Use context manager for timeout (60 seconds per tool)
253
+ with timeout_context(60):
254
+ if tool_name == "search_company":
255
+ result = edgar_client.search_company_by_name(arguments["company_name"])
256
+ if result:
257
+ return {
258
+ "type": "text",
259
+ "text": json.dumps(result, ensure_ascii=False)
260
+ }
261
+ else:
262
+ return {
263
+ "type": "text",
264
+ "text": json.dumps({
265
+ "error": f"No company found with name: {arguments['company_name']}"
266
+ }, ensure_ascii=False)
267
+ }
268
+
269
+ elif tool_name == "get_company_info":
270
+ result = edgar_client.get_company_info(arguments["cik"])
271
+ if result:
272
+ return {
273
+ "type": "text",
274
+ "text": json.dumps(result, ensure_ascii=False)
275
+ }
276
+ else:
277
+ return {
278
+ "type": "text",
279
+ "text": json.dumps({
280
+ "error": f"No company found with CIK: {arguments['cik']}"
281
+ }, ensure_ascii=False)
282
+ }
283
+
284
+ elif tool_name == "get_company_filings":
285
+ form_types = arguments.get("form_types")
286
+ result = edgar_client.get_company_filings(arguments["cik"], form_types)
287
+ if result:
288
+ # Limit to 20 results
289
+ limited_result = result[:20]
290
+ return {
291
+ "type": "text",
292
+ "text": json.dumps({
293
+ "total": len(result),
294
+ "returned": len(limited_result),
295
+ "filings": limited_result
296
+ }, ensure_ascii=False)
297
+ }
298
+ else:
299
+ return {
300
+ "type": "text",
301
+ "text": json.dumps({
302
+ "error": f"No filings found for CIK: {arguments['cik']}"
303
+ }, ensure_ascii=False)
304
+ }
305
+
306
+ elif tool_name == "get_financial_data":
307
+ result = edgar_client.get_financial_data_for_period(arguments["cik"], arguments["period"])
308
+ if result and "period" in result:
309
+ return {
310
+ "type": "text",
311
+ "text": json.dumps(result, ensure_ascii=False)
312
+ }
313
+ else:
314
+ return {
315
+ "type": "text",
316
+ "text": json.dumps({
317
+ "error": f"No financial data found for CIK: {arguments['cik']}, Period: {arguments['period']}"
318
+ }, ensure_ascii=False)
319
+ }
320
+
321
+ elif tool_name == "extract_financial_metrics":
322
+ years = arguments.get("years", 3)
323
+ if years < 1 or years > 10:
324
+ return {
325
+ "type": "text",
326
+ "text": json.dumps({
327
+ "error": "Years parameter must be between 1 and 10"
328
+ }, ensure_ascii=False)
329
+ }
330
+
331
+ metrics = financial_analyzer.extract_financial_metrics(arguments["cik"], years)
332
+ if metrics:
333
+ formatted = financial_analyzer.format_financial_data(metrics)
334
+ return {
335
+ "type": "text",
336
+ "text": json.dumps({
337
+ "periods": len(formatted),
338
+ "data": formatted
339
+ }, ensure_ascii=False)
340
+ }
341
+ else:
342
+ return {
343
+ "type": "text",
344
+ "text": json.dumps({
345
+ "error": f"No financial metrics found for CIK: {arguments['cik']}"
346
+ }, ensure_ascii=False)
347
+ }
348
+
349
+ elif tool_name == "get_latest_financial_data":
350
+ result = financial_analyzer.get_latest_financial_data(arguments["cik"])
351
+ if result and "period" in result:
352
+ return {
353
+ "type": "text",
354
+ "text": json.dumps(result, ensure_ascii=False)
355
+ }
356
+ else:
357
+ return {
358
+ "type": "text",
359
+ "text": json.dumps({
360
+ "error": f"No latest financial data found for CIK: {arguments['cik']}"
361
+ }, ensure_ascii=False)
362
+ }
363
+
364
+ elif tool_name == "advanced_search_company" or tool_name == "advanced_search":
365
+ # Support both names for backward compatibility
366
+ result = financial_analyzer.search_company(arguments["company_input"])
367
+ if result.get("error"):
368
+ return {
369
+ "type": "text",
370
+ "text": json.dumps({
371
+ "error": result["error"]
372
+ }, ensure_ascii=False)
373
+ }
374
+ return {
375
+ "type": "text",
376
+ "text": json.dumps(result, ensure_ascii=False)
377
+ }
378
+
379
+ else:
380
+ return {
381
+ "type": "text",
382
+ "text": json.dumps({
383
+ "error": f"Unknown tool: {tool_name}"
384
+ }, ensure_ascii=False)
385
+ }
386
+
387
+ except Exception as e:
388
+ return {
389
+ "type": "text",
390
+ "text": json.dumps({
391
+ "error": f"Error executing {tool_name}: {str(e)}"
392
+ }, ensure_ascii=False)
393
+ }
394
+
395
+
396
+ # ==================== MCP Endpoints ====================
397
+
398
+ @app.post("/message")
399
+ async def handle_mcp_message(request: MCPRequest):
400
+ """
401
+ Main MCP message handler
402
+ Supports: initialize, tools/list, tools/call
403
+ """
404
+ method = request.method
405
+ params = request.params or {}
406
+
407
+ # Handle initialize
408
+ if method == "initialize":
409
+ return MCPResponse(
410
+ id=request.id,
411
+ result={
412
+ "protocolVersion": "2024-11-05",
413
+ "capabilities": {
414
+ "tools": {}
415
+ },
416
+ "serverInfo": {
417
+ "name": "sec-financial-data",
418
+ "version": "2.3.1"
419
+ }
420
+ }
421
+ ).dict()
422
+
423
+ # Handle tools/list
424
+ elif method == "tools/list":
425
+ return MCPResponse(
426
+ id=request.id,
427
+ result={
428
+ "tools": MCP_TOOLS
429
+ }
430
+ ).dict()
431
+
432
+ # Handle tools/call
433
+ elif method == "tools/call":
434
+ tool_name = params.get("name")
435
+ arguments = params.get("arguments", {})
436
+
437
+ if not tool_name:
438
+ return MCPResponse(
439
+ id=request.id,
440
+ error={
441
+ "code": -32602,
442
+ "message": "Missing tool name"
443
+ }
444
+ ).dict()
445
+
446
+ result = execute_tool(tool_name, arguments)
447
+
448
+ return MCPResponse(
449
+ id=request.id,
450
+ result={
451
+ "content": [result]
452
+ }
453
+ ).dict()
454
+
455
+ else:
456
+ return MCPResponse(
457
+ id=request.id,
458
+ error={
459
+ "code": -32601,
460
+ "message": f"Method not found: {method}"
461
+ }
462
+ ).dict()
463
+
464
+
465
+ @app.get("/sse")
466
+ async def sse_endpoint(request: Request):
467
+ """
468
+ Server-Sent Events endpoint for MCP transport
469
+ Keeps connection alive and handles MCP messages
470
+ """
471
+ async def event_stream():
472
+ # Send initial endpoint message
473
+ init_message = {
474
+ "jsonrpc": "2.0",
475
+ "method": "endpoint",
476
+ "params": {
477
+ "endpoint": "/message"
478
+ }
479
+ }
480
+ yield f"data: {json.dumps(init_message)}\n\n"
481
+
482
+ # Keep connection alive with optimized ping interval
483
+ try:
484
+ while True:
485
+ await asyncio.sleep(20) # 20 seconds ping interval for stability
486
+ # Send ping to keep connection alive
487
+ ping_message = {
488
+ "jsonrpc": "2.0",
489
+ "method": "ping",
490
+ "timestamp": datetime.now().isoformat()
491
+ }
492
+ yield f"data: {json.dumps(ping_message)}\n\n"
493
+ except asyncio.CancelledError:
494
+ pass
495
+
496
+ return StreamingResponse(
497
+ event_stream(),
498
+ media_type="text/event-stream",
499
+ headers={
500
+ "Cache-Control": "no-cache, no-transform",
501
+ "Connection": "keep-alive",
502
+ "X-Accel-Buffering": "no",
503
+ "Content-Type": "text/event-stream"
504
+ }
505
+ )
506
+
507
+
508
+ @app.get("/", response_class=HTMLResponse)
509
+ async def root():
510
+ """Interactive landing page with tool testing functionality"""
511
+ # Get the path to the templates directory
512
+ current_dir = Path(__file__).parent
513
+ template_path = current_dir / "templates" / "index.html"
514
+
515
+ # Read and return the HTML file
516
+ try:
517
+ with open(template_path, "r", encoding="utf-8") as f:
518
+ html_content = f.read()
519
+ return HTMLResponse(content=html_content)
520
+ except FileNotFoundError:
521
+ # Enhanced error message with debugging information
522
+ import os
523
+ debug_info = f"""
524
+ <h1>Error: Template not found</h1>
525
+ <h2>Debugging Information:</h2>
526
+ <p><strong>Looking for:</strong> {template_path.absolute()}</p>
527
+ <p><strong>Exists:</strong> {template_path.exists()}</p>
528
+ <p><strong>Current dir:</strong> {current_dir.absolute()}</p>
529
+ <h3>Directory contents:</h3>
530
+ <pre>{chr(10).join(os.listdir(current_dir))}</pre>
531
+ <h3>Fix:</h3>
532
+ <p>Ensure Dockerfile contains: <code>COPY templates/ templates/</code></p>
533
+ """
534
+ return HTMLResponse(content=debug_info, status_code=500)
535
+
536
+
537
+ @app.get("/tools")
538
+ async def list_tools():
539
+ """List all available MCP tools (for documentation)"""
540
+ return {"tools": MCP_TOOLS}
541
+
542
+
543
+ @app.get("/health")
544
+ async def health_check():
545
+ """Enhanced health check endpoint with diagnostics"""
546
+ uptime_seconds = time.time() - server_start_time
547
+ return {
548
+ "status": "healthy",
549
+ "server": "sec-financial-data",
550
+ "version": "2.3.1",
551
+ "protocol": "MCP",
552
+ "transport": "SSE",
553
+ "tools_count": len(MCP_TOOLS),
554
+ "uptime_seconds": round(uptime_seconds, 2),
555
+ "python_version": sys.version,
556
+ "request_count": request_count,
557
+ "error_count": error_count,
558
+ "error_rate": round(error_count / max(request_count, 1) * 100, 2),
559
+ "timestamp": datetime.now().isoformat()
560
+ }
561
+
562
+
563
+ @app.get("/ready")
564
+ async def readiness_check():
565
+ """Readiness check for load balancers"""
566
+ try:
567
+ # Quick validation that clients are initialized
568
+ if edgar_client and financial_analyzer:
569
+ return {"status": "ready", "timestamp": datetime.now().isoformat()}
570
+ else:
571
+ raise HTTPException(status_code=503, detail="Services not initialized")
572
+ except Exception as e:
573
+ raise HTTPException(status_code=503, detail=f"Not ready: {str(e)}")
574
+
575
+
576
+ @app.api_route("/api/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
577
+ async def redirect_old_api(path: str):
578
+ """Handle old REST API endpoints with helpful message"""
579
+ return JSONResponse(
580
+ status_code=404,
581
+ content={
582
+ "error": "API endpoint not found",
583
+ "message": "This server now uses the MCP (Model Context Protocol).",
584
+ "migration_guide": {
585
+ "old_endpoint": f"/api/{path}",
586
+ "new_method": "Use MCP protocol via POST /message",
587
+ "documentation": "Visit / for setup instructions",
588
+ "example": {
589
+ "method": "POST",
590
+ "url": "/message",
591
+ "body": {
592
+ "jsonrpc": "2.0",
593
+ "id": 1,
594
+ "method": "tools/call",
595
+ "params": {
596
+ "name": "advanced_search_company",
597
+ "arguments": {"company_input": "Microsoft"}
598
+ }
599
+ }
600
+ }
601
+ },
602
+ "available_tools": "/tools",
603
+ "health_check": "/health"
604
+ }
605
+ )
606
+
607
+
608
+ if __name__ == "__main__":
609
+ import uvicorn
610
+ uvicorn.run(
611
+ "mcp_server_sse:app",
612
+ host="0.0.0.0",
613
+ port=7860,
614
+ reload=True
615
+ )
{service → EasyReportDataMCP}/report_mcp.py RENAMED
File without changes
MarketandStockMCP/__pycache__/news_quote_mcp.cpython-311.pyc ADDED
Binary file (19.7 kB). View file
 
{service → MarketandStockMCP}/news_quote_mcp.py RENAMED
File without changes
app copy.py ADDED
@@ -0,0 +1,1922 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import datetime
4
+ import re
5
+ import pandas as pd
6
+ from sqlalchemy import true
7
+ from chatbot.chat_main import respond
8
+ import globals as g
9
+ from service.mysql_service import get_companys, insert_company, get_company_by_name
10
+ from service.chat_service import get_analysis_report, get_stock_price_from_bailian, search_company, search_news, get_invest_suggest, chat_bot
11
+ from service.company import check_company_exists
12
+ from service.hf_upload import get_hf_files_with_links
13
+ from MarketandStockMCP.news_quote_mcp import get_company_news, get_quote
14
+ from EasyReportDataMCP.report_mcp import query_financial_data
15
+ from service.three_year_table_tool import build_table_format
16
+ from service.three_year_tool import process_financial_data_with_metadata
17
+ from service.tool_processor import get_stock_price
18
+
19
+ get_companys_state = True
20
+ # JavaScript代码用于读取和存储数据
21
+ js_code = """
22
+ function handleStorage(operation, key, value) {
23
+ if (operation === 'set') {
24
+ localStorage.setItem(key, value);
25
+ return `已存储: ${key} = ${value}`;
26
+ } else if (operation === 'get') {
27
+ let storedValue = localStorage.getItem(key);
28
+ if (storedValue === null) {
29
+ return `未找到键: ${key}`;
30
+ }
31
+ return `读取到: ${key} = ${storedValue}`;
32
+ } else if (operation === 'clear') {
33
+ localStorage.removeItem(key);
34
+ return `已清除: ${key}`;
35
+ } else if (operation === 'clearAll') {
36
+ localStorage.clear();
37
+ return '已清除所有数据';
38
+ }
39
+ }
40
+ """
41
+ custom_css = """
42
+ /* 匹配所有以 gradio-container- 开头的类 */
43
+ div[class^="gradio-container-"],
44
+ div[class*=" gradio-container-"] {
45
+ -webkit-text-size-adjust: 100% !important;
46
+ line-height: 1.5 !important;
47
+ font-family: unset !important;
48
+ -moz-tab-size: 4 !important;
49
+ tab-size: 4 !important;
50
+ }
51
+
52
+ .company-list-container {
53
+ background-color: white;
54
+ border-radius: 0.5rem;
55
+ padding: 0.75rem;
56
+ margin-bottom: 0.75rem;
57
+ border: 1px solid #e5e7eb;
58
+ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
59
+ width: 100%;
60
+ }
61
+
62
+ /* 隐藏单选框 */
63
+ .company-list-container input[type="radio"] {
64
+ display: none;
65
+ }
66
+
67
+ /* 自定义选项样式 */
68
+ .company-list-container label {
69
+ display: block;
70
+ padding: 0.75rem 1rem;
71
+ margin: 0.25rem 0;
72
+ border-radius: 0.375rem;
73
+ cursor: pointer;
74
+ transition: all 0.2s ease;
75
+ background-color: #f9fafb;
76
+ border: 1px solid #e5e7eb;
77
+ font-size: 1rem;
78
+ text-align: left;
79
+ width: 100%;
80
+ box-sizing: border-box;
81
+ }
82
+
83
+ /* 悬停效果 */
84
+ .company-list-container label:hover {
85
+ background-color: #f3f4f6;
86
+ border-color: #d1d5db;
87
+ }
88
+
89
+ /* 选中效果 - 确保背景色充满整个选项 */
90
+ .company-list-container input[type="radio"]:checked + span {
91
+ # background: #3b82f6 !important;
92
+ color: white !important;
93
+ font-weight: 600 !important;
94
+ display: block;
95
+ width: 100%;
96
+ height: 100%;
97
+ padding: 0.75rem 1rem;
98
+ margin: -0.75rem -1rem;
99
+ border-radius: 0.375rem;
100
+ }
101
+
102
+ .company-list-container span {
103
+ display: block;
104
+ padding: 0;
105
+ border-radius: 0.375rem;
106
+ width: 100%;
107
+ }
108
+
109
+ /* 确保每行只有一个选项 */
110
+ .company-list-container .wrap {
111
+ display: block !important;
112
+ }
113
+
114
+ .company-list-container .wrap li {
115
+ display: block !important;
116
+ width: 100% !important;
117
+ }
118
+ label.selected {
119
+ background: #3b82f6 !important;
120
+ color: white !important;
121
+ }
122
+ """
123
+
124
+ # 全局变量用于存储公司映射关系
125
+ companies_map = {}
126
+
127
+ # 根据公司名称获取股票代码的函数
128
+ def get_stock_code_by_company_name(company_name):
129
+ """根据公司名称获取股票代码"""
130
+ if company_name in companies_map and "CODE" in companies_map[company_name]:
131
+ return companies_map[company_name]["CODE"]
132
+ return "" # 默认返回
133
+
134
+ # 创建一个简单的函数来获取公司列表
135
+ def get_company_list_choices():
136
+ choices = []
137
+ print(f"Getting init add company list choices...{get_companys_state}")
138
+ if not get_companys_state:
139
+ return gr.update(choices=choices)
140
+ try:
141
+ companies_data = get_companys()
142
+ print(f"Getting init add company list choices...companies_data: {companies_data}")
143
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
144
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
145
+ else:
146
+ choices = []
147
+ except:
148
+ choices = []
149
+
150
+ return gr.update(choices=choices)
151
+
152
+ # Sidebar service functions
153
+
154
+ # 处理公司点击事件的函数
155
+ def handle_company_click(company_name):
156
+ """处理公司点击事件,先判断是否已经入库,如果没有则进行入库操作,然后刷新公司列表"""
157
+ print(f"Handling click for company: {company_name}")
158
+
159
+ # 1. 判断是否已经入库
160
+ if not check_company_exists(company_name):
161
+ # 2. 如果没有入库,则进行入库操作
162
+ # 获取股票���码(如果有的话)
163
+ stock_code = companies_map.get(company_name, {}).get("CODE", "Unknown")
164
+ print(f"Inserting company {company_name} with code {stock_code}")
165
+
166
+ # 插入公司到数据库
167
+ success = insert_company(company_name, stock_code)
168
+ if success:
169
+ print(f"Successfully inserted company: {company_name}")
170
+ # 直接更新companies_map,而不是重新加载整个映射
171
+ companies_map[company_name] = {"NAME": company_name, "CODE": stock_code}
172
+ # 使用Gradio的成功提示
173
+ gr.Info(f"Successfully added company: {company_name}")
174
+ # 返回True表示添加成功,需要刷新列表
175
+ return True
176
+ else:
177
+ print(f"Failed to insert company: {company_name}")
178
+ # 使用Gradio的错误提示
179
+ gr.Error(f"Failed to insert company: {company_name}")
180
+ return False
181
+ else:
182
+ print(f"Company {company_name} already exists in database")
183
+ # 使用Gradio的警告提示
184
+ gr.Warning(f"Company '{company_name}' already exists")
185
+
186
+ # 3. 返回成功响应
187
+ return None
188
+
189
+ def get_company_list_html(selected_company=""):
190
+ try:
191
+ # 从数据库获取所有公司
192
+ companies_data = get_companys()
193
+ # 检查是否为错误信息
194
+ if isinstance(companies_data, str):
195
+ if "查询执行失败" in companies_data:
196
+ return "<div class='text-red-500'>获取公司列表失败</div>"
197
+ else:
198
+ # 如果是字符串但不是错误信息,可能需要特殊处理
199
+ return ""
200
+
201
+ # 检查是否为DataFrame且为空
202
+ if not isinstance(companies_data, pd.DataFrame) or companies_data.empty:
203
+ return ""
204
+
205
+ # 生成HTML列表
206
+ html_items = []
207
+ for _, row in companies_data.iterrows():
208
+ company_name = row.get('company_name', 'Unknown')
209
+ # 根据是否选中添加不同的样式类
210
+ css_class = "company-item"
211
+ if company_name == selected_company:
212
+ css_class += " selected-company"
213
+ # 使用button元素来确保可点击性
214
+ html_items.append(f'<button class="{css_class}" data-company="{company_name}" style="width:100%; text-align:left; border:none; background:none;">{company_name}</button>')
215
+
216
+ return "\n".join(html_items)
217
+ except Exception as e:
218
+ return f"<div class='text-red-500'>生成公司列表失败: {str(e)}</div>"
219
+
220
+ def initialize_company_list(selected_company=""):
221
+ return get_company_list_html(selected_company)
222
+
223
+ def refresh_company_list(selected_company=""):
224
+ """刷新公司列表,返回最新的HTML内容,带loading效果"""
225
+ # 先返回loading状态
226
+ loading_html = '''
227
+ <div style="display: flex; justify-content: center; align-items: center; height: 100px;">
228
+ <div class="loading-spinner" style="width: 24px; height: 24px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite;"></div>
229
+ <style>
230
+ @keyframes spin {
231
+ 0% { transform: rotate(0deg); }
232
+ 100% { transform: rotate(360deg); }
233
+ }
234
+ </style>
235
+ </div>
236
+ '''
237
+ yield loading_html
238
+
239
+ # 然后返回实际的数据
240
+ yield get_company_list_html(selected_company)
241
+
242
+ # 新增函数:处理公司选择事件
243
+ def select_company(company_name):
244
+ """处理公司选择事件,更新全局状态并返回更新后的公司列表"""
245
+ # 更新全局变量
246
+ g.SELECT_COMPANY = company_name if company_name else ""
247
+ # 对于Radio组件,我们只需要返回更新后的选项列表
248
+ try:
249
+ companies_data = get_companys()
250
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
251
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
252
+ else:
253
+ choices = []
254
+ except:
255
+ choices = []
256
+ return gr.update(choices=choices, value=company_name)
257
+
258
+ def initialize_companies_map():
259
+ """初始化 companies_map 字典"""
260
+ global companies_map
261
+ companies_map = {} # 清空之前的映射
262
+
263
+ print("Initializing companies map...")
264
+
265
+ try:
266
+ # 获取预定义的公司列表
267
+ predefined_companies = [
268
+ { "NAME": "Alibaba", "CODE": "BABA" },
269
+ { "NAME": "阿里巴巴-W", "CODE": "09988" },
270
+ { "NAME": "NVIDIA", "CODE": "NVDA" },
271
+ { "NAME": "Amazon", "CODE": "AMZN" },
272
+ { "NAME": "Intel", "CODE": "INTC" },
273
+ { "NAME": "Meta", "CODE": "META" },
274
+ { "NAME": "Google", "CODE": "GOOGL" },
275
+ { "NAME": "Apple", "CODE": "AAPL" },
276
+ { "NAME": "Tesla", "CODE": "TSLA" },
277
+ { "NAME": "AMD", "CODE": "AMD" },
278
+ { "NAME": "Microsoft", "CODE": "MSFT" },
279
+ { "NAME": "ASML", "CODE": "ASML" }
280
+ ]
281
+
282
+ # 将预定义公司添加到映射中
283
+ for company in predefined_companies:
284
+ companies_map[company["NAME"]] = {"NAME": company["NAME"], "CODE": company["CODE"]}
285
+
286
+ print(f"Predefined companies added: {len(predefined_companies)}")
287
+
288
+ # 从数据库获取公司数据
289
+ companies_data = get_companys()
290
+ # companies_data = window.cachedCompanies or []
291
+
292
+
293
+ print(f"Companies data from DB: {companies_data}")
294
+
295
+ # 如果数据库中有公司数据,则添加到映射中(去重)
296
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
297
+ print(f"Adding {len(companies_data)} companies from database")
298
+ for _, row in companies_data.iterrows():
299
+ company_name = row.get('company_name', 'Unknown')
300
+ stock_code = row.get('stock_code', '')
301
+
302
+ # 确保company_name和stock_code都是字符串类型
303
+ company_name = str(company_name) if company_name is not None else 'Unknown'
304
+ stock_code = str(stock_code) if stock_code is not None else ''
305
+
306
+ # 检查是否已存在于映射中(通过股票代码判断)
307
+ is_duplicate = False
308
+ for existing_company in companies_map.values():
309
+ if existing_company["CODE"] == stock_code:
310
+ is_duplicate = True
311
+ break
312
+
313
+ # 如果不重复,则添加到映射中
314
+ if not is_duplicate:
315
+ companies_map[company_name] = {"NAME": company_name, "CODE": stock_code}
316
+ # print(f"Added company: {company_name}")
317
+ else:
318
+ print("No companies found in database")
319
+
320
+ print(f"Final companies map: {companies_map}")
321
+ except Exception as e:
322
+ # 错误处理
323
+ print(f"Error initializing companies map: {str(e)}")
324
+ pass
325
+
326
+ # Sidebar company selector functions
327
+ def update_company_choices(user_input: str):
328
+ """更新公司选择列表"""
329
+ # 第一次 yield:立即显示 modal + loading 提示
330
+ yield gr.update(
331
+ choices=["Searching..."],
332
+ visible=True
333
+ ), gr.update(visible=False, value="") # 添加第二个返回值
334
+
335
+ # 第二次:执行耗时操作(调用 LLM)
336
+ choices = search_company(user_input) # 这是你原来的同步函数
337
+
338
+ # 检查choices是否为错误信息
339
+ if len(choices) > 0 and isinstance(choices[0], str) and not choices[0].startswith("Searching"):
340
+ # 如果是错误信息或非正常格式,显示提示消息
341
+ error_message = choices[0] if len(choices) > 0 else "未知错误"
342
+ # 使用Ant Design风格的错误提示
343
+ error_html = f'''
344
+ <div class="ant-message ant-message-error" style="
345
+ position: fixed;
346
+ top: 20px;
347
+ left: 50%;
348
+ transform: translateX(-50%);
349
+ z-index: 10000;
350
+ padding: 10px 16px;
351
+ border-radius: 4px;
352
+ background: #fff;
353
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
354
+ display: flex;
355
+ align-items: center;
356
+ pointer-events: all;
357
+ animation: messageFadeIn 0.3s ease-in-out;
358
+ ">
359
+ <div style="
360
+ width: 16px;
361
+ height: 16px;
362
+ background: #ff4d4f;
363
+ border-radius: 50%;
364
+ position: relative;
365
+ margin-right: 8px;
366
+ "></div>
367
+ <span>{error_message}</span>
368
+ </div>
369
+ <script>
370
+ setTimeout(function() {{
371
+ var msg = document.querySelector('.ant-message-error');
372
+ if (msg) {{
373
+ msg.style.animation = 'messageFadeOut 0.3s ease-in-out';
374
+ setTimeout(function() {{ msg.remove(); }}, 3000);
375
+ }}
376
+ }}, 3000);
377
+ </script>
378
+ '''
379
+ yield gr.update(choices=["No results found"], visible=True), gr.update(visible=True, value=error_html)
380
+ else:
381
+ # 第三次:更新为真实结果
382
+ yield gr.update(
383
+ choices=choices,
384
+ visible=len(choices) > 0
385
+ ), gr.update(visible=False, value="")
386
+
387
+ def add_company(selected, current_list):
388
+ """添加选中的公司"""
389
+ if selected == "No results found":
390
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
391
+ if selected:
392
+ # print(f"Selected company====: {selected}")
393
+ # 从选择的文本中提取公司名称和股票代码
394
+ # 假设格式为 "公司名称 (股票代码)"
395
+ selected_clean = selected.strip()
396
+ match = re.match(r"^(.+?)\s*\(([^)]+)\)$", selected_clean)
397
+ if match:
398
+ company_name = match.group(1)
399
+ stock_code = match.group(2)
400
+ elif companies_map.get(selected_clean):
401
+ company_name = selected_clean
402
+ stock_code = companies_map[selected_clean]["CODE"]
403
+ else:
404
+ company_name = selected_clean
405
+ stock_code = "Unknown"
406
+
407
+ # print(f"Company name: {company_name}, Stock code: {stock_code}")
408
+ # print(f"Company exists: {check_company_exists(company_name)}")
409
+
410
+ if not check_company_exists(company_name):
411
+ # 入库
412
+ success = insert_company(company_name, stock_code)
413
+ if success:
414
+ # 从数据库获取更新后的公司列表
415
+ try:
416
+ companies_data = get_companys()
417
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
418
+ updated_list = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
419
+ else:
420
+ updated_list = []
421
+ except:
422
+ updated_list = []
423
+
424
+ # 添加默认公司选项
425
+ if not updated_list:
426
+ updated_list = ['Alibaba', '腾讯控股', 'Tencent', '阿里巴巴-W', 'Apple']
427
+
428
+ # 成功插入后清除状态消息,并更新Radio组件的选项,同时默认选中刚添加的公司
429
+ # 通过设置value参数,会自动触发change事件来加载数据
430
+ return gr.update(visible=False), gr.update(choices=updated_list, value=company_name), gr.update(visible=False, value="")
431
+ else:
432
+ # 插入失败显示错误消息,使用Gradio内置的错误提示
433
+ gr.Error("插入公司失败")
434
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
435
+ else:
436
+ # 公司已存在,使用Gradio内置的警告消息
437
+ gr.Warning(f"公司 '{company_name}' 已存在")
438
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
439
+
440
+ return gr.update(visible=False), current_list, gr.update(visible=False, value="")
441
+
442
+ # Sidebar report section functions
443
+ # 创建一个全局变量来存储公司按钮组件
444
+ company_buttons = {}
445
+
446
+ def create_company_buttons():
447
+ """创建公司按钮组件"""
448
+ # 确保companies_map已被初始化
449
+ if not companies_map:
450
+ initialize_companies_map()
451
+
452
+ # 显示companies_map中的公司列表
453
+ companies = list(companies_map.keys())
454
+
455
+ # 添加调试信息
456
+ print(f"Companies in map: {companies}")
457
+
458
+ # 清空之前的按钮
459
+ company_buttons.clear()
460
+
461
+ if not companies:
462
+ # 如果没有公司,返回一个空的列
463
+ with gr.Column():
464
+ gr.Markdown("暂无公司数据")
465
+ else:
466
+ # 使用Gradio按钮组件创建公司列表
467
+ with gr.Column(elem_classes=["home-company-list"]):
468
+ # 按每行两个公司进行分组
469
+ for i in range(0, len(companies), 2):
470
+ # 检查是否是最后一行且只有一个元素
471
+ if i + 1 < len(companies):
472
+ # 有两个元素
473
+ with gr.Row(elem_classes=["home-company-item-box"]):
474
+ btn1 = gr.Button(companies[i], elem_classes=["home-company-item", "gradio-button"])
475
+ btn2 = gr.Button(companies[i + 1], elem_classes=["home-company-item", "gradio-button"])
476
+ # 保存按钮引用
477
+ company_buttons[companies[i]] = btn1
478
+ company_buttons[companies[i + 1]] = btn2
479
+ else:
480
+ # 只有一个元素
481
+ with gr.Row(elem_classes=["home-company-item-box", "single-item"]):
482
+ btn = gr.Button(companies[i], elem_classes=["home-company-item", "gradio-button"])
483
+ # 保存按钮引用
484
+ company_buttons[companies[i]] = btn
485
+
486
+ # 返回按钮字典
487
+ return company_buttons
488
+ def update_report_section(selected_company, report_data, stock_code):
489
+ """根据选中的公司更新报告部分"""
490
+ print(f"Updating report (报告部分): {selected_company}") # 添加调试信息
491
+
492
+ if selected_company == "" or selected_company is None or selected_company == "Unknown":
493
+ # 没有选中的公司,显示公司列表
494
+ # html_content = get_initial_company_list_content()
495
+ # 暂时返回空内容,稍后会用Gradio组件替换
496
+ html_content = ""
497
+ return gr.update(value=html_content, visible=True)
498
+ else:
499
+ # 有选中的公司,显示相关报告
500
+ # try:
501
+ # # 尝试从Hugging Face获取文件列表
502
+ # report_data = get_hf_files_with_links("JC321/files-world")
503
+ # except Exception as e:
504
+ # # 如果获取失败,使用模拟数据并显示错误消息
505
+ # print(f"获取Hugging Face文件列表失败: {str(e)}")
506
+ # report_data = []
507
+ stock_code = get_stock_code_by_company_name(selected_company)
508
+ report_data = query_financial_data(stock_code, "5-Year")
509
+ # report_data = process_financial_data_with_metadata(financial_metrics_pre)
510
+
511
+ html_content = '<div class="report-list-box bg-white">'
512
+ html_content += '<div class="report-list-title bg-white card-title left-card-title" style="border-bottom: 1px solid #e3e3e6 !important;"><h3>Financial Reports</h3></div>'
513
+ for report in report_data:
514
+ html_content += f'''
515
+ <div class="report-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{report['source_url']}', '_blank')">
516
+ <div class="report-item-content">
517
+ <span class="text-gray-800">{report['period']}-{stock_code}-{report['source_form']}</span>
518
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" class="text-blue-500" viewBox="0 0 20 20" fill="currentColor">
519
+ <path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 10-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l-1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd" />
520
+ </svg>
521
+ </div>
522
+ </div>
523
+ '''
524
+
525
+ html_content += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}份报告</span></div>'
526
+ html_content += '</div>'
527
+
528
+ return gr.update(value=html_content, visible=True)
529
+ def update_news_section(selected_company):
530
+ """根据选中的公司更新报告部分"""
531
+ html_content = ""
532
+ if selected_company == "" or selected_company is None:
533
+ # 没有选中的公司,显示公司列表
534
+ # html_content = get_initial_company_list_content()
535
+ # 暂时返回空内容,稍后会用Gradio组件替换
536
+ return gr.update(value=html_content, visible=True)
537
+ else:
538
+ try:
539
+ stock_code = get_stock_code_by_company_name(selected_company)
540
+ report_data = get_company_news(stock_code, None, None)
541
+ # print(f"新闻列表: {report_data['articles']}")
542
+ # report_data = search_news(selected_company)
543
+ if (report_data['articles']):
544
+ report_data = report_data['articles']
545
+ news_html = "<div class='news-list-box bg-white'>"
546
+ news_html += '<div class="report-list-title bg-white card-title left-card-title" style="border-bottom: 1px solid #e3e3e6 !important;"><h3>News</h3></div>'
547
+ from datetime import datetime
548
+
549
+ for news in report_data:
550
+ published_at = news['published']
551
+
552
+ # 解析 ISO 8601 时间字符串(注意:strptime 不直接支持 'Z',需替换或使用 fromisoformat)
553
+ dt = datetime.fromisoformat(published_at.replace("Z", "+00:00"))
554
+
555
+ # 格式化为 YYYY.MM.DD
556
+ formatted_date = dt.strftime("%Y.%m.%d")
557
+ news_html += f'''
558
+ <div class="news-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{news['url']}', '_blank')">
559
+ <div class="news-item-content">
560
+ <span class="text-xs text-gray-500">[{formatted_date}]</span>
561
+ <span class="text-gray-800">{news['headline']}</span>
562
+ </div>
563
+ </div>
564
+ '''
565
+ news_html += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}条新闻</span></div>'
566
+ news_html += '</div>'
567
+ html_content += news_html
568
+ except Exception as e:
569
+ print(f"Error updating report section: {str(e)}")
570
+
571
+ return gr.update(value=html_content, visible=True)
572
+
573
+ # Component creation functions
574
+ def create_header():
575
+ """创建头部组件"""
576
+ # 获取当前时间
577
+ current_time = datetime.datetime.now().strftime("%B %d, %Y - Market Data Updated Today")
578
+
579
+ with gr.Row(elem_classes=["header"]):
580
+ # 左侧:图标和标题
581
+ with gr.Column(scale=8):
582
+ # 使用圆柱体SVG图标表示数据库
583
+ gr.HTML('''
584
+ <div class="top-logo-box">
585
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 48 48">
586
+ <g fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="4">
587
+ <path d="M44 11v27c0 3.314-8.954 6-20 6S4 41.314 4 38V11"></path>
588
+ <path d="M44 29c0 3.314-8.954 6-20 6S4 32.314 4 29m40-9c0 3.314-8.954 6-20 6S4 23.314 4 20"></path>
589
+ <ellipse cx="24" cy="10" rx="20" ry="6"></ellipse>
590
+ </g>
591
+ </svg>
592
+ <span class="logo-title">Easy Financial Report Dashboard</span>
593
+ </div>
594
+ ''', elem_classes=["text-2xl"])
595
+
596
+ # 右侧:时间信息
597
+ with gr.Column(scale=2):
598
+ gr.Markdown(current_time, elem_classes=["text-sm-top-time"])
599
+
600
+ def create_company_list(get_companys_state):
601
+ company_list = gr.Radio(
602
+ choices=[],
603
+ label="",
604
+ interactive=True,
605
+ elem_classes=["company-list-container"],
606
+ container=False, # 不显示外部容器边框
607
+ visible=True
608
+ )
609
+ if (get_companys_state == False):
610
+ return company_list
611
+ else:
612
+ """创建公司列表组件"""
613
+ # 获取公司列表数据
614
+ try:
615
+ companies_data = get_companys()
616
+ print(f"创建公司列表组件 - Companies data: {companies_data}")
617
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
618
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
619
+ else:
620
+ choices = []
621
+ except:
622
+ choices = []
623
+
624
+ # 添加默认公司选项
625
+ if not choices:
626
+ choices = []
627
+
628
+ # 使用Radio组件显示公司列表,不显示标签
629
+ company_list = gr.Radio(
630
+ choices=choices,
631
+ label="",
632
+ interactive=True,
633
+ elem_classes=["company-list-container"],
634
+ container=False, # 不显示外部容器边框
635
+ visible=True
636
+ )
637
+
638
+ return company_list
639
+
640
+ def create_company_selector():
641
+ """创建公司选择器组件"""
642
+ company_input = gr.Textbox(
643
+ show_label=False,
644
+ placeholder="Add Company",
645
+ elem_classes=["company-input-box"],
646
+ lines=1,
647
+ max_lines=1,
648
+ # container=False
649
+ )
650
+
651
+ # 状态消息显示区域
652
+ status_message = gr.HTML(
653
+ "",
654
+ elem_classes=["status-message"],
655
+ visible=False
656
+ )
657
+
658
+ # 弹窗选择列表
659
+ company_modal = gr.Radio(
660
+ show_label=False,
661
+ choices=[],
662
+ visible=False,
663
+ elem_classes=["company-modal"]
664
+ )
665
+
666
+ return company_input, status_message, company_modal
667
+
668
+ def create_report_section():
669
+ """创建报告部分组件"""
670
+ # 创建一个用于显示报告列表的组件,初始显示公司列表
671
+ # initial_content = get_initial_company_list_content()
672
+ # 暂时返回空内容,稍后会用Gradio组件替换
673
+ initial_content = ""
674
+ # print(f"Initial content: {initial_content}") # 添加调试信息
675
+
676
+ report_display = gr.HTML(initial_content)
677
+ return report_display
678
+
679
+ def create_news_section():
680
+ """创建新闻部分组件"""
681
+ initial_content = ""
682
+ news_display = gr.HTML(initial_content)
683
+ return news_display
684
+
685
+ def format_financial_metrics(data: dict, prev_data: dict = None) -> list: # pyright: ignore[reportArgumentType]
686
+ """
687
+ 将原始财务数据转换为 financial_metrics 格式。
688
+
689
+ Args:
690
+ data (dict): 当前财年数据(必须包含 total_revenue, net_income 等字段)
691
+ prev_data (dict, optional): 上一财年数据,用于计算 change。若未提供,change 设为 "--"
692
+
693
+ Returns:
694
+ list[dict]: 符合 financial_metrics 格式的列表
695
+ """
696
+
697
+ def format_currency(value: float) -> str:
698
+ """将数字格式化为 $XB / $XM / $XK"""
699
+ if value >= 1e9:
700
+ return f"${value / 1e9:.2f}B"
701
+ elif value >= 1e6:
702
+ return f"${value / 1e6:.2f}M"
703
+ elif value >= 1e3:
704
+ return f"${value / 1e3:.2f}K"
705
+ else:
706
+ return f"${value:.2f}"
707
+
708
+ def calculate_change(current: float, previous: float) -> tuple:
709
+ """计算变化百分比和颜色"""
710
+ if previous == 0:
711
+ return "--", "gray"
712
+ change_pct = (current - previous) / abs(previous) * 100
713
+ sign = "+" if change_pct >= 0 else ""
714
+ color = "green" if change_pct >= 0 else "red"
715
+ return f"{sign}{change_pct:.1f}%", color
716
+
717
+ # 定义指标映射
718
+ metrics_config = [
719
+ {
720
+ "key": "total_revenue",
721
+ "label": "Total Revenue",
722
+ "is_currency": True,
723
+ "eps_like": False
724
+ },
725
+ {
726
+ "key": "net_income",
727
+ "label": "Net Income",
728
+ "is_currency": True,
729
+ "eps_like": False
730
+ },
731
+ {
732
+ "key": "earnings_per_share",
733
+ "label": "Earnings Per Share",
734
+ "is_currency": False, # EPS 不用 B/M 单位
735
+ "eps_like": True
736
+ },
737
+ {
738
+ "key": "operating_expenses",
739
+ "label": "Operating Expenses",
740
+ "is_currency": True,
741
+ "eps_like": False
742
+ },
743
+ {
744
+ "key": "operating_cash_flow",
745
+ "label": "Cash Flow",
746
+ "is_currency": True,
747
+ "eps_like": False
748
+ }
749
+ ]
750
+
751
+ result = []
752
+ for item in metrics_config:
753
+ key = item["key"]
754
+ current_val = data.get(key)
755
+ if current_val is None:
756
+ continue
757
+
758
+ # 格式化 value
759
+ if item["eps_like"]:
760
+ value_str = f"${current_val:.2f}"
761
+ elif item["is_currency"]:
762
+ value_str = format_currency(current_val)
763
+ else:
764
+ value_str = str(current_val)
765
+
766
+ # 计算 change(如果有上期数据)
767
+ if prev_data and key in prev_data:
768
+ prev_val = prev_data[key]
769
+ change_str, color = calculate_change(current_val, prev_val)
770
+ else:
771
+ change_str = "--"
772
+ color = "gray"
773
+
774
+ result.append({
775
+ "label": item["label"],
776
+ "value": value_str,
777
+ "change": change_str,
778
+ "color": color
779
+ })
780
+
781
+ return result
782
+
783
+
784
+ def create_sidebar():
785
+ """创建侧边栏组件"""
786
+ # 初始化 companies_map
787
+ initialize_companies_map()
788
+
789
+ with gr.Column(elem_classes=["sidebar"]):
790
+ # 公司选择
791
+ with gr.Group(elem_classes=["card"]):
792
+ gr.Markdown("### Select Company", elem_classes=["card-title", "left-card-title"])
793
+ with gr.Column():
794
+ company_list = create_company_list(get_companys_state)
795
+
796
+ # 创建公司列表
797
+ # if not get_companys_state:
798
+ # getCompanyFromStorage = gr.Button("读取")
799
+ # getCompanyFromStorage.click(
800
+ # fn=create_company_list(True),
801
+ # inputs=[],
802
+ # outputs=[company_list, status_message]
803
+ # )
804
+
805
+ # 创建公司选择器
806
+ company_input, status_message, company_modal = create_company_selector()
807
+
808
+ # 绑定事件
809
+ company_input.submit(
810
+ fn=update_company_choices,
811
+ inputs=[company_input],
812
+ outputs=[company_modal, status_message]
813
+ )
814
+
815
+ company_modal.change(
816
+ fn=add_company,
817
+ inputs=[company_modal, company_list],
818
+ outputs=[company_modal, company_list, status_message]
819
+ )
820
+
821
+ # 创建公司按钮组件
822
+ company_buttons = create_company_buttons()
823
+
824
+ # 为每个公司按钮绑定点击事件
825
+ def make_click_handler(company_name):
826
+ def handler():
827
+ result = handle_company_click(company_name)
828
+ # 如果添加成功,刷新Select Company列表并默认选中刚添加的公司
829
+ if result is True:
830
+ # 正确地刷新通过create_company_list()创建的Radio组件
831
+ try:
832
+ companies_data = get_companys()
833
+ if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
834
+ updated_choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
835
+ else:
836
+ updated_choices = []
837
+ except:
838
+ updated_choices = []
839
+ # 使用gr.update来正确更新Radio组件,并默认选中刚添加的公司
840
+ # 同时触发change事件来加载数据
841
+ return gr.update(choices=updated_choices, value=company_name)
842
+ return None
843
+ return handler
844
+
845
+ for company_name, button in company_buttons.items():
846
+ button.click(
847
+ fn=make_click_handler(company_name),
848
+ inputs=[],
849
+ outputs=[company_list]
850
+ )
851
+
852
+ # 创建一个容器来容纳报告部分,初始时隐藏
853
+ with gr.Group(elem_classes=["report-news-box"]) as report_section_group:
854
+ # gr.Markdown("### Financial Reports", elem_classes=["card-title", "left-card-title"])
855
+ report_display = create_report_section()
856
+ news_display = create_news_section()
857
+
858
+
859
+ # 处理公司选择事件
860
+ def select_company_handler(company_name):
861
+ """处理公司选择事件的处理器"""
862
+ # 更新全局变量
863
+ g.SELECT_COMPANY = company_name if company_name else ""
864
+
865
+ # 更新报告部分的内容
866
+ updated_report_display = update_report_section(company_name, None, None)
867
+
868
+ updated_news_display = update_news_section(company_name)
869
+ # 根据是否选择了公司来决定显示/隐藏报告部分
870
+ if company_name:
871
+ # 有选中的公司,显示报告部分
872
+ return gr.update(visible=True), updated_report_display, updated_news_display
873
+ else:
874
+ # 没有选中的公司,隐藏报告部分
875
+ return gr.update(visible=False), updated_report_display, updated_news_display
876
+
877
+ company_list.change(
878
+ fn=select_company_handler,
879
+ inputs=[company_list],
880
+ outputs=[report_section_group, report_display, news_display]
881
+ )
882
+
883
+ # 返回公司列表组件和报告部分组件
884
+ return company_list, report_section_group, report_display, news_display
885
+
886
+ def build_income_table(table_data):
887
+ # 兼容两种数据结构:
888
+ # 1. 新结构:包含 list_data 和 yoy_rates 的字典
889
+ # 2. 旧结构:直接是二维数组
890
+ if isinstance(table_data, dict) and "list_data" in table_data:
891
+ # 新结构
892
+ income_statement = table_data["list_data"]
893
+ yoy_rates = table_data["yoy_rates"] or []
894
+ else:
895
+ # 旧结构,直接使用传入的数据
896
+ income_statement = table_data
897
+ yoy_rates = []
898
+
899
+ # 创建一个映射,将年份列索引映射到增长率
900
+ yoy_map = {}
901
+ if len(yoy_rates) > 1 and len(yoy_rates[0]) > 1:
902
+ # 获取增长率表头(跳过第一列"Category")
903
+ yoy_headers = yoy_rates[0][1:]
904
+
905
+ # 为每个指标行创建增长率映射
906
+ for i, yoy_row in enumerate(yoy_rates[1:], 1): # 跳过标题行
907
+ category = yoy_row[0]
908
+ yoy_map[category] = {}
909
+ for j, rate in enumerate(yoy_row[1:]):
910
+ if j < len(yoy_headers):
911
+ yoy_map[category][yoy_headers[j]] = rate
912
+
913
+ table_rows = ""
914
+ header_row = income_statement[0]
915
+
916
+ for i, row in enumerate(income_statement):
917
+ if i == 0:
918
+ row_style = "background-color: #f5f5f5; font-weight: 500;"
919
+ else:
920
+ row_style = "background-color: #f9f9f9;"
921
+ cells = ""
922
+
923
+ for j, cell in enumerate(row):
924
+ if j == 0:
925
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
926
+ else:
927
+ # 添加增长率箭头(如果有的话)
928
+ growth = None
929
+ category = row[0]
930
+ # j是当前单元格索引,0是类别列,1,2,3...是数据列
931
+ # yoy_map的键是年份,例如"2024/FY"
932
+ if i > 0 and category in yoy_map and j > 0 and j < len(header_row):
933
+ year_header = header_row[j]
934
+ if year_header in yoy_map[category]:
935
+ growth = yoy_map[category][year_header]
936
+
937
+ if growth and growth != "N/A":
938
+ arrow = "▲" if growth.startswith("+") else "▼"
939
+ color = "green" if growth.startswith("+") else "red"
940
+ cells += f"""<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px; position: relative;'>
941
+ <div>{cell}</div>
942
+ <div style='position: absolute; bottom: -5px; right: 5px; font-size: 10px; color: {color};'>{arrow}{growth}</div>
943
+ </td>"""
944
+ else:
945
+ cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
946
+ table_rows += f"<tr style='{row_style}'>{cells}</tr>"
947
+
948
+ html = f"""
949
+ <div style="min-width: 400px;max-width: 600px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
950
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
951
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
952
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
953
+ </svg>
954
+ <div style="font-size: 18px; font-weight: 600;">Income Statement and Cash Flow</div>
955
+ </div>
956
+ <table style="width: 100%; border-collapse: collapse; font-size: 14px;">
957
+ {table_rows}
958
+ </table>
959
+ </div>
960
+ """
961
+ return html
962
+ def create_metrics_dashboard():
963
+ """创建指标仪表板组件"""
964
+ with gr.Row(elem_classes=["metrics-dashboard"]):
965
+ card_custom_style = '''
966
+ background-color: white;
967
+ border-radius: 0.5rem;
968
+ box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.1) 0px 1px 2px -1px;
969
+ padding: 1.25rem;
970
+ min-height: 250px !important;
971
+ text-align: center;
972
+ '''
973
+
974
+ # 模拟数据
975
+ company_info = {
976
+ "name": "N/A",
977
+ "symbol": "NYSE:N/A",
978
+ "price": 0,
979
+ "change": 0,
980
+ "change_percent": 0.41,
981
+ "open": 165.20,
982
+ "high": 166.37,
983
+ "low": 156.15,
984
+ "prev_close": 157.01,
985
+ "volume": "27.10M"
986
+ }
987
+
988
+ # financial_metrics = query_financial_data("NVDA", "最新财务数据")
989
+ # print(f"最新财务数据: {financial_metrics}")
990
+ financial_metrics = [
991
+ {"label": "Total Revenue", "value": "N/A", "change": "N/A", "color": "grey"},
992
+ {"label": "Net Income", "value": "N/A", "change": "N/A", "color": "grey"},
993
+ {"label": "Earnings Per Share", "value": "N/A", "change": "N/A", "color": "grey"},
994
+ {"label": "Operating Expenses", "value": "N/A", "change": "N/A", "color": "grey"},
995
+ {"label": "Cash Flow", "value": "N/A", "change": "N/A", "color": "grey"}
996
+ ]
997
+ # income_statement = [
998
+ # ["Category", "2024/FY", "2023/FY", "2022/FY"],
999
+ # ["Total", "130350M", "126491M", "134567M"],
1000
+ # ["Net Income", "11081", "10598M", "9818.4M"],
1001
+ # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1002
+ # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1003
+ # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1004
+ # ]
1005
+ income_statement = {
1006
+ "list_data": [
1007
+ ["Category", "N/A/FY", "N/A/FY", "N/A/FY"],
1008
+ ["Total", "N/A", "N/A", "N/A"],
1009
+ ["Net Income", "N/A", "N/A", "N/A.4M"],
1010
+ ["Earnings Per Share", "N/A", "N/A", "N/A"],
1011
+ ["Operating Expenses", "N/A", "N/A", "N/A"],
1012
+ ["Cash Flow", "N/A", "N/A", "N/A"]
1013
+ ],
1014
+ "yoy_rates": []
1015
+ # "yoy_rates": [
1016
+ # ["Category", "N/A/FY", "N/A/FY"],
1017
+ # ["Total", "N/A", "N/A"],
1018
+ # ["Net Income", "+3.05%", "-6.00%"],
1019
+ # ["Earnings Per Share", "+3.05%", "-6.00%"],
1020
+ # ["Operating Expenses", "+29.17%", "-6.00%"],
1021
+ # ["Cash Flow", "-13.05%", "-6.00%"]
1022
+ # ]
1023
+ }
1024
+ yearly_data = 'N/A'
1025
+ # 增长变化的 HTML 字符(箭头+百分比)
1026
+ def render_change(change: str, color: str):
1027
+ if change.startswith("+"):
1028
+ return f'<span style="color:{color};">▲{change}</span>'
1029
+ else:
1030
+ return f'<span style="color:{color};">▼{change}</span>'
1031
+
1032
+ # 构建左侧卡片
1033
+ def build_stock_card():
1034
+ html = f"""
1035
+ <div style="width: 250px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1036
+ <div style="font-size: 14px; color: #555;">N/A</div>
1037
+ <div style="font-size: 12px; color: #888;">N/A</div>
1038
+ <div style="font-size: 32px; font-weight: bold; margin: 8px 0;">N/A</div>
1039
+ <div style="font-size: 14px; margin: 8px 0;">N/A</div>
1040
+ <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
1041
+ <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1042
+ <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1043
+ <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1044
+ <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1045
+
1046
+ </div>
1047
+ </div>
1048
+ """
1049
+ return html
1050
+ # <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1051
+ # 构建中间卡片
1052
+ def build_financial_metrics():
1053
+ metrics_html = ""
1054
+ for item in financial_metrics:
1055
+ change_html = render_change(item["change"], item["color"])
1056
+ metrics_html += f"""
1057
+ <div style="display: flex; justify-content: space-between; padding: 8px 0; font-family: 'Segoe UI', sans-serif;">
1058
+ <div style="font-size: 14px; color: #555;">{item['label']}</div>
1059
+ <div style="font-size: 16px; font-weight: 500; color: #333;">{item['value']} {change_html}</div>
1060
+ </div>
1061
+ """
1062
+
1063
+ html = f"""
1064
+ <div style="min-width: 300px;max-width: 450px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1065
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;justify-content: space-between;">
1066
+ <div style="font-size: 18px; font-weight: 600;display: flex;align-items: center;">
1067
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1068
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1069
+ </svg>
1070
+ <span style="margin-left: 10px;">{yearly_data} Financial Metrics</span>
1071
+ </div>
1072
+ <div style="font-size: 16px; color: #8f8f8f;">
1073
+ YTD data
1074
+ </div>
1075
+ </div>
1076
+ {metrics_html}
1077
+ </div>
1078
+ """
1079
+ return html
1080
+
1081
+
1082
+ # 主函数:返回所有 HTML 片段
1083
+ def get_dashboard():
1084
+ with gr.Row():
1085
+ with gr.Column(scale=1, min_width=250, elem_classes=["metric-card-col-left"]):
1086
+ stock_card_html = gr.HTML(build_stock_card(), elem_classes=["metric-card-left"])
1087
+ with gr.Column(scale=1, min_width=300, elem_classes=["metric-card-col-middle"]):
1088
+ financial_metrics_html = gr.HTML(build_financial_metrics(), elem_classes=["metric-card-middle"])
1089
+ with gr.Column(scale=1, min_width=450, elem_classes=["metric-card-col-right"]):
1090
+ # 传递income_statement参数
1091
+ income_table_html = gr.HTML(build_income_table(income_statement), elem_classes=["metric-card-right"])
1092
+ return stock_card_html, financial_metrics_html, income_table_html
1093
+
1094
+ # 创建指标仪表板并保存引用
1095
+ stock_card_component, financial_metrics_component, income_table_component = get_dashboard()
1096
+
1097
+ # 将组件引用保存到全局变量,以便在其他地方使用
1098
+ global metrics_dashboard_components
1099
+ metrics_dashboard_components = (stock_card_component, financial_metrics_component, income_table_component)
1100
+
1101
+ # 更新指标仪表板的函数
1102
+ def update_metrics_dashboard(company_name):
1103
+ """根据选择的公司更新指标仪表板"""
1104
+ # 模拟数据
1105
+ # company_info = {
1106
+ # "name": company_name,
1107
+ # "symbol": "NYSE:BABA",
1108
+ # "price": 157.65,
1109
+ # "change": 0.64,
1110
+ # "change_percent": 0.41,
1111
+ # "open": 165.20,
1112
+ # "high": 166.37,
1113
+ # "low": 156.15,
1114
+ # "prev_close": 157.01,
1115
+ # "volume": "27.10M"
1116
+ # }
1117
+ company_info = {}
1118
+ # 尝试获取股票价格数据,但不中断程序执行
1119
+ stock_code = ""
1120
+ try:
1121
+ # 根据选择的公司获取股票代码
1122
+ stock_code = get_stock_code_by_company_name(company_name)
1123
+ # result = get_quote(company_name.strip())
1124
+
1125
+ # company_info2 = get_stock_price(stock_code)
1126
+ # company_info2 = get_stock_price_from_bailian(stock_code)
1127
+ # print(f"股票价格数据: {company_info2}")
1128
+ company_info = get_quote(stock_code.strip())
1129
+ company_info['company'] = company_name
1130
+ print(f"股票价格数据====: {company_info}")
1131
+ # 查询结果:{
1132
+ # "company": "阿里巴巴",
1133
+ # "symbol": "BABA",
1134
+ # "open": "159.09",
1135
+ # "high": "161.46",
1136
+ # "low": "150.00",
1137
+ # "price": "157.60",
1138
+ # "volume": "21453064",
1139
+ # "latest trading day": "2025-11-27",
1140
+ # "previous close": "157.01",
1141
+ # "change": "+0.59",
1142
+ # "change_percent": "+0.38%"
1143
+ # }BABA
1144
+ # 如果成功获取数据,则用实际数据替换模拟数据
1145
+ # if company_info2 and "content" in company_info2 and len(company_info2["content"]) > 0:
1146
+ # import json
1147
+ # # 解析返回的JSON数据
1148
+ # data_text = company_info2["content"][0]["text"]
1149
+ # stock_data = json.loads(data_text)
1150
+
1151
+ # # 提取数据
1152
+ # quote = stock_data["Global Quote"]
1153
+
1154
+ # # 转换交易量单位
1155
+ # volume = int(quote['06. volume'])
1156
+ # if volume >= 1000000:
1157
+ # volume_str = f"{volume / 1000000:.2f}M"
1158
+ # elif volume >= 1000:
1159
+ # volume_str = f"{volume / 1000:.2f}K"
1160
+ # else:
1161
+ # volume_str = str(volume)
1162
+
1163
+ # company_info = {
1164
+ # "name": company_name,
1165
+ # "symbol": f"NYSE���{quote['01. symbol']}",
1166
+ # "price": float(quote['05. price']),
1167
+ # "change": float(quote['09. change']),
1168
+ # "change_percent": float(quote['10. change percent'].rstrip('%')),
1169
+ # "open": float(quote['02. open']),
1170
+ # "high": float(quote['03. high']),
1171
+ # "low": float(quote['04. low']),
1172
+ # "prev_close": float(quote['08. previous close']),
1173
+ # "volume": volume_str
1174
+ # }
1175
+ except Exception as e:
1176
+ print(f"获取股票价格数据失败: {e}")
1177
+ company_info2 = None
1178
+
1179
+ # financial_metrics = [
1180
+ # {"label": "Total Revenue", "value": "$2.84B", "change": "+12.4%", "color": "green"},
1181
+ # {"label": "Net Income", "value": "$685M", "change": "-3.2%", "color": "red"},
1182
+ # {"label": "Earnings Per Share", "value": "$2.15", "change": "-3.2%", "color": "red"},
1183
+ # {"label": "Operating Expenses", "value": "$1.2B", "change": "+5.1%", "color": "green"},
1184
+ # {"label": "Cash Flow", "value": "$982M", "change": "+8.7%", "color": "green"}
1185
+ # ]
1186
+ financial_metrics_pre = query_financial_data(stock_code, "5-Year")
1187
+ # financial_metrics_pre = query_financial_data(company_name, "5年趋势")
1188
+ # print(f"最新财务数据: {financial_metrics_pre}")
1189
+ # financial_metrics = format_financial_metrics(financial_metrics_pre)
1190
+
1191
+
1192
+ # financial_metrics_pre_2 = extract_last_three_with_fallback(financial_metrics_pre)
1193
+ # print(f"提取的3年数据: {financial_metrics_pre_2}")
1194
+ # financial_metrics_pre = {
1195
+ # "metrics": financial_metrics_pre_2
1196
+ # }
1197
+ financial_metrics = []
1198
+ # try:
1199
+ # # financial_metrics = calculate_yoy_comparison(financial_metrics_pre)
1200
+ # financial_metrics = build_financial_metrics_three_year_data(financial_metrics_pre)
1201
+ # print(f"格式化后的财务数据: {financial_metrics}")
1202
+ # except Exception as e:
1203
+ # print(f"Error calculating YOY comparison: {e}")
1204
+ year_data = None
1205
+ three_year_data = None
1206
+ try:
1207
+ # financial_metrics = process_financial_data_with_metadata(financial_metrics_pre)
1208
+ result = process_financial_data_with_metadata(financial_metrics_pre)
1209
+
1210
+ # 按需提取字段
1211
+ financial_metrics = result["financial_metrics"]
1212
+ year_data = result["year_data"]
1213
+ three_year_data = result["three_year_data"]
1214
+ print(f"格式化后的财务数据: {financial_metrics}")
1215
+ # 拿report数据
1216
+ # try:
1217
+ # # 从 result 中获取报告数据
1218
+ # if 'report_data' in result: # 假设 result 中包含 report_data 键
1219
+ # report_data = result['report_data']
1220
+ # else:
1221
+ # # 如果 result 中没有直接包含 report_data,则从其他键中获取
1222
+ # # 这需要根据实际的 result 数据结构来调整
1223
+ # report_data = result.get('reports', []) # 示例:假设数据在 'reports' 键下
1224
+
1225
+ # 更新报告部分的内容
1226
+ # 这里需要调用 update_report_section 函数并传入 report_data
1227
+ # 注意:update_report_section 可能需要修改以接受 report_data 参数
1228
+ # updated_report_content = update_report_section(company_name, report_data, stock_code)
1229
+
1230
+ # 然后将 updated_report_content 返回,以便在 UI 中更新
1231
+ # 这需要修改函数的返回值以包含报告内容
1232
+
1233
+ # except Exception as e:
1234
+ # print(f"Error updating report section with result data: {e}")
1235
+ # updated_report_content = "<div>Failed to load report data</div>"
1236
+ except Exception as e:
1237
+ print(f"Error process_financial_data: {e}")
1238
+
1239
+
1240
+ # income_statement = [
1241
+ # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1242
+ # ["Total", "130350M", "126491M", "134567M"],
1243
+ # ["Net Income", "11081", "10598M", "9818.4M"],
1244
+ # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1245
+ # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1246
+ # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1247
+ # ]
1248
+
1249
+ # table_data = None
1250
+ # try:
1251
+ # table_data = extract_financial_table(financial_metrics_pre)
1252
+ # print(table_data)
1253
+ # except Exception as e:
1254
+ # print(f"Error extract_financial_table: {e}")
1255
+ # yearly_data = None
1256
+ # try:
1257
+ # yearly_data = get_yearly_data(financial_metrics_pre)
1258
+ # except Exception as e:
1259
+ # print(f"Error get_yearly_data: {e}")
1260
+
1261
+ # ======
1262
+ # table_data = [
1263
+ # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1264
+ # ["Total", "130350M", "126491M", "134567M"],
1265
+ # ["Net Income", "11081", "10598M", "9818.4M"],
1266
+ # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1267
+ # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1268
+ # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1269
+ # ]
1270
+ yearly_data = year_data
1271
+ table_data = build_table_format(three_year_data)
1272
+ # print(f"table_data: {table_data}")
1273
+ # yearly_data = None
1274
+ # try:
1275
+ # yearly_data = get_yearly_data(financial_metrics_pre)
1276
+ # except Exception as e:
1277
+ # print(f"Error get_yearly_data: {e}")
1278
+ #=======
1279
+
1280
+ # exp = {
1281
+ # "list_data": [
1282
+ # ["Category", "2024/FY", "2023/FY", "2022/FY"],
1283
+ # ["Total", "130350M", "126491M", "134567M"],
1284
+ # ["Net Income", "11081", "10598M", "9818.4M"],
1285
+ # ["Earnings Per Share", "4.38", "4.03", "3.62"],
1286
+ # ["Operating Expenses", "31990.9M", "31439.6M", "34516.2M"],
1287
+ # ["Cash Flow", "25289.9M", "29086M", "22517.2M"]
1288
+ # ],
1289
+ # "yoy_rates": [
1290
+ # ["Category", "2024/FY", "2023/FY"],
1291
+ # ["Total", "+3.05%", "-6.00%"],
1292
+ # ["Net Income", "+3.05%", "-6.00%"],
1293
+ # ["Earnings Per Share", "+3.05%", "-6.00%"],
1294
+ # ["Operating Expenses", "+29.17%", "-6.00%"],
1295
+ # ["Cash Flow", "-13.05%", "-6.00%"]
1296
+ # ]
1297
+ # }
1298
+
1299
+ # 增长变化的 HTML 字符(箭头+百分比)
1300
+ def render_change(change: str, color: str):
1301
+ if change.startswith("+"):
1302
+ return f'<span style="color:{color};">▲{change}</span>'
1303
+ else:
1304
+ return f'<span style="color:{color};">▼{change}</span>'
1305
+
1306
+ # 构建左侧卡片
1307
+ def build_stock_card(company_info):
1308
+ try:
1309
+ if not company_info or not isinstance(company_info, dict):
1310
+ company_name = "N/A"
1311
+ symbol = "N/A"
1312
+ price = "N/A"
1313
+ change_html = '<span style="color:#888;">N/A</span>'
1314
+ open_val = high_val = low_val = prev_close_val = volume_display = "N/A"
1315
+ else:
1316
+ company_name = company_info.get("company", "N/A")
1317
+ symbol = company_info.get("symbol", "N/A")
1318
+ price = company_info.get("current_price", "N/A")
1319
+
1320
+ # 解析 change
1321
+ change_str = company_info.get("change", "0")
1322
+ try:
1323
+ change = float(change_str)
1324
+ except (ValueError, TypeError):
1325
+ change = 0.0
1326
+
1327
+ # 解析 change_percent
1328
+ change_percent = company_info.get("percent_change", "0%")
1329
+ # try:
1330
+ # change_percent = float(change_percent_str.rstrip('%'))
1331
+ # except (ValueError, TypeError):
1332
+ # change_percent = 0.0
1333
+
1334
+ change_color = "green" if change >= 0 else "red"
1335
+ sign = "+" if change >= 0 else ""
1336
+ change_html = f'<span style="color:{change_color};">{sign}{change:.2f} ({change_percent:+.2f}%)</span>'
1337
+
1338
+ # 其他价格字段(可选:也可格式化为 2 位小数)
1339
+ open_val = company_info.get("open", "N/A")
1340
+ high_val = company_info.get("high", "N/A")
1341
+ low_val = company_info.get("low", "N/A")
1342
+ prev_close_val = company_info.get("previous_close", "N/A")
1343
+ # raw_volume = company_info.get("volume", "N/A")
1344
+ # volume_display = format_volume(raw_volume)
1345
+
1346
+ html = f"""
1347
+ <div style="width: 250px; height: 300px !important; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1348
+ <div style="font-size: 16px; color: #555; font-weight: 500;">{company_name}</div>
1349
+ <div style="font-size: 12px; color: #888;">NYSE:{symbol}</div>
1350
+ <div style="display: flex; align-items: center; gap: 10px; margin: 8px 0;">
1351
+ <div style="font-size: 32px; font-weight: bold;">{price}</div>
1352
+ <div style="font-size: 14px;">{change_html}</div>
1353
+ </div>
1354
+ <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
1355
+ <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{open_val}</div>
1356
+ <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{high_val}</div>
1357
+ <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{low_val}</div>
1358
+ <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{prev_close_val}</div>
1359
+ </div>
1360
+ </div>
1361
+ """
1362
+ # <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{volume_display}</div>
1363
+
1364
+ return html
1365
+
1366
+ except Exception as e:
1367
+ print(f"Error building stock card: {e}")
1368
+ return '<div style="width:250px; padding:16px; color:red;">Error loading stock data</div>'
1369
+ # 构建中间卡片
1370
+ def build_financial_metrics(yearly_data):
1371
+ metrics_html = ""
1372
+ for item in financial_metrics:
1373
+ change_html = render_change(item["change"], item["color"])
1374
+ metrics_html += f"""
1375
+ <div style="display: flex; justify-content: space-between; padding: 8px 0; font-family: 'Segoe UI', sans-serif;">
1376
+ <div style="font-size: 14px; color: #555;">{item['label']}</div>
1377
+ <div style="font-size: 16px; font-weight: 500; color: #333;">{item['value']} {change_html}</div>
1378
+ </div>
1379
+ """
1380
+
1381
+ html = f"""
1382
+ <div style="width: 450px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1383
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;justify-content: space-between;">
1384
+ <div style="font-size: 18px; font-weight: 600;display: flex;align-items: center;">
1385
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1386
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1387
+ </svg>
1388
+ <span style="margin-left: 10px;">{yearly_data} Financial Metrics</span>
1389
+ </div>
1390
+ <div style="font-size: 16px; color: #8f8f8f;">
1391
+ YTD data
1392
+ </div>
1393
+ </div>
1394
+ {metrics_html}
1395
+ </div>
1396
+ """
1397
+ return html
1398
+
1399
+ # 构建右侧表格
1400
+ # def build_income_table(income_statement):
1401
+ # table_rows = ""
1402
+ # for i, row in enumerate(income_statement):
1403
+ # if i == 0:
1404
+ # row_style = "background-color: #f5f5f5; font-weight: 500;"
1405
+ # else:
1406
+ # row_style = "background-color: #f9f9f9;"
1407
+ # cells = ""
1408
+ # for j, cell in enumerate(row):
1409
+ # if j == 0:
1410
+ # cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
1411
+ # else:
1412
+ # # 添加增长箭头(模拟数据)
1413
+ # growth = None
1414
+ # if i == 1 and j == 1: growth = "+3.05%"
1415
+ # elif i == 1 and j == 2: growth = "-6.00%"
1416
+ # elif i == 2 and j == 1: growth = "+3.05%"
1417
+ # elif i == 2 and j == 2: growth = "-6.00%"
1418
+ # elif i == 3 and j == 1: growth = "+3.05%"
1419
+ # elif i == 3 and j == 2: growth = "-6.00%"
1420
+ # elif i == 4 and j == 1: growth = "+29.17%"
1421
+ # elif i == 4 and j == 2: growth = "+29.17%"
1422
+ # elif i == 5 and j == 1: growth = "-13.05%"
1423
+ # elif i == 5 and j == 2: growth = "+29.17%"
1424
+
1425
+ # if growth:
1426
+ # arrow = "▲" if growth.startswith("+") else "▼"
1427
+ # color = "green" if growth.startswith("+") else "red"
1428
+ # cells += f"""<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px; position: relative;'>
1429
+ # <div>{cell}</div>
1430
+ # <div style='position: absolute; bottom: -5px; right: 5px; font-size: 10px; color: {color};'>{arrow}{growth}</div>
1431
+ # </td>"""
1432
+ # else:
1433
+ # cells += f"<td style='padding: 8px; border: 1px solid #ddd; text-align: center; font-size: 14px;'>{cell}</td>"
1434
+ # table_rows += f"<tr style='{row_style}'>{cells}</tr>"
1435
+
1436
+ # html = f"""
1437
+ # <div style="width: 600px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1438
+ # <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
1439
+ # <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1440
+ # <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1441
+ # </svg>
1442
+ # <div style="font-size: 18px; font-weight: 600;">Income Statement and Cash Flow</div>
1443
+ # </div>
1444
+ # <table style="width: 100%; border-collapse: collapse; font-size: 14px;">
1445
+ # {table_rows}
1446
+ # </table>
1447
+ # </div>
1448
+ # """
1449
+ # return html
1450
+
1451
+ # 返回三个HTML组件的内容
1452
+ return build_stock_card(company_info), build_financial_metrics(yearly_data), build_income_table(table_data)
1453
+
1454
+ def create_tab_content(tab_name, company_name):
1455
+ """创建Tab内容组件"""
1456
+ if tab_name == "summary":
1457
+ print(f"company_name: {company_name}")
1458
+ # content = get_invest_suggest(company_name)
1459
+ gr.Markdown("# 11111", elem_classes=["invest-suggest-md-box"])
1460
+ # gr.Markdown(content, elem_classes=["invest-suggest-md-box"])
1461
+ # gr.Markdown("""
1462
+ # ## Investment Suggestions
1463
+
1464
+ # ### Company Overview
1465
+
1466
+ # GlobalTech inc. is a leading technology company with strong performance in the Q3 2025 period. The companyshows consistent revenue growth and maintains a healthy fnancial position.
1467
+
1468
+ # ### Key Strengths
1469
+
1470
+ # - Revenue Growth: 12.4% year-over-year increase demonstrates strong market demandDiversifed Portfolio: Multiple revenue streams reduce business risk
1471
+
1472
+ # - Innovation Focus: Continued investment in R&D drives future growth potential
1473
+
1474
+ # ### Financial Health Indicators
1475
+
1476
+ # - Liquidity: Current ratio of 1.82 indicates good short-term fnancial health
1477
+
1478
+ # - Proftability: Net income of $685M, though down slightly quarter-over-quarter0
1479
+ # - Cash Flow: Strong operating cash flow of $982M supports operations and growth initiatives
1480
+
1481
+ # ### Investment Recommendation
1482
+
1483
+ # BUY - GlobalTech Inc. presents a solid investment opportunity with:
1484
+ # - Consistent revenue growth trajectory
1485
+ # - Strong market position in key technology segments
1486
+ # - Healthy balance sheet and cash flow generation
1487
+
1488
+ # ### Risk Considerations
1489
+
1490
+ # Quarterly net income decline warrants monitoring
1491
+ # | Category | Q3 2025 | Q2 2025 | YoY % |
1492
+ # |--------------------|-----------|-----------|----------|
1493
+ # | Total Revenue | $2,842M | $2,712M | +12.4% |
1494
+ # | Gross Profit | $1,203M | $1,124M | +7.0% |
1495
+ # | Operating Income | $742M | $798M | -7.0% |
1496
+ # | Net Income | $685M | $708M | -3.2% |
1497
+ # | Earnings Per Share | $2.15 | $2.22 | -3.2% |
1498
+ # """, elem_classes=["invest-suggest-md-box"])
1499
+
1500
+
1501
+ elif tab_name == "detailed":
1502
+ with gr.Column(elem_classes=["tab-content"]):
1503
+ gr.Markdown("Financial Statements", elem_classes=["text-xl", "font-semibold", "text-gray-900", "mb-6"])
1504
+
1505
+ with gr.Row(elem_classes=["gap-6"]):
1506
+ # 收入报表 (3/5宽度)
1507
+ with gr.Column(elem_classes=["w-3/5", "bg-gray-50", "rounded-xl", "p-4"]):
1508
+ gr.Markdown("Income Statement", elem_classes=["font-medium", "mb-3"])
1509
+ # 这里将显示收入报表表格
1510
+
1511
+ # 资产负债表和现金流量表 (2/5宽度)
1512
+ with gr.Column(elem_classes=["w-2/5", "flex", "flex-col", "gap-6"]):
1513
+ # 资产负债表
1514
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1515
+ gr.Markdown("Balance Sheet Summary", elem_classes=["font-medium", "mb-3"])
1516
+ # 这里将显示资产负债表图表
1517
+
1518
+ # 现金流量表
1519
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1520
+ with gr.Row(elem_classes=["justify-between", "items-start"]):
1521
+ gr.Markdown("Cash Flow Statement", elem_classes=["font-medium"])
1522
+ gr.Markdown("View Detailed", elem_classes=["text-xs", "text-blue-600", "font-medium"])
1523
+
1524
+ with gr.Column(elem_classes=["mt-4", "space-y-3"]):
1525
+ # 经营现金流
1526
+ with gr.Column():
1527
+ with gr.Row(elem_classes=["justify-between"]):
1528
+ gr.Markdown("Operating Cash Flow")
1529
+ gr.Markdown("$982M", elem_classes=["font-medium"])
1530
+ with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]):
1531
+ with gr.Column(elem_classes=["bg-green-500", "h-1.5", "rounded-full"], scale=85):
1532
+ gr.Markdown("")
1533
+
1534
+ # 投资现金流
1535
+ with gr.Column():
1536
+ with gr.Row(elem_classes=["justify-between"]):
1537
+ gr.Markdown("Investing Cash Flow")
1538
+ gr.Markdown("-$415M", elem_classes=["font-medium"])
1539
+ with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]):
1540
+ with gr.Column(elem_classes=["bg-blue-500", "h-1.5", "rounded-full"], scale=42):
1541
+ gr.Markdown("")
1542
+
1543
+ # 融资现金流
1544
+ with gr.Column():
1545
+ with gr.Row(elem_classes=["justify-between"]):
1546
+ gr.Markdown("Financing Cash Flow")
1547
+ gr.Markdown("-$212M", elem_classes=["font-medium"])
1548
+ with gr.Row(elem_classes=["w-full", "bg-gray-200", "rounded-full", "h-1.5", "mt-1"]):
1549
+ with gr.Column(elem_classes=["bg-red-500", "h-1.5", "rounded-full"], scale=25):
1550
+ gr.Markdown("")
1551
+
1552
+ elif tab_name == "comparative":
1553
+ with gr.Column(elem_classes=["tab-content"]):
1554
+ gr.Markdown("Industry Benchmarking", elem_classes=["text-xl", "font-semibold", "text-gray-900", "mb-6"])
1555
+
1556
+ # 收入增长对比
1557
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4", "mb-6"]):
1558
+ gr.Markdown("Revenue Growth - Peer Comparison", elem_classes=["font-medium", "mb-3"])
1559
+ # 这里将显示对比图表
1560
+
1561
+ # 利润率和报告预览网格
1562
+ with gr.Row(elem_classes=["grid-cols-2", "gap-6"]):
1563
+ # 利润率表格
1564
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1565
+ gr.Markdown("Profitability Ratios", elem_classes=["font-medium", "mb-3"])
1566
+ # 这里将显示利润率表格
1567
+
1568
+ # 报告预览
1569
+ with gr.Column(elem_classes=["bg-gray-50", "rounded-xl", "p-4"]):
1570
+ gr.Markdown("Report Preview", elem_classes=["font-medium", "mb-3"])
1571
+ # 这里将显示报告预览
1572
+
1573
+ def create_chat_panel():
1574
+ """创建聊天面板组件"""
1575
+ # with gr.Column(elem_classes=["chat-panel"]):
1576
+ # 聊天头部
1577
+ # with gr.Row(elem_classes=["p-4", "border-b", "border-gray-200", "items-center", "gap-2"]):
1578
+ # gr.Markdown("🤖", elem_classes=["text-xl", "text-blue-600"])
1579
+ # gr.Markdown("Financial Assistant", elem_classes=["font-medium"])
1580
+
1581
+ # 聊天区域
1582
+ # 一行代码嵌入!
1583
+ # chat_component = create_financial_chatbot()
1584
+ # chat_component.render()
1585
+ # create_financial_chatbot()
1586
+ # gr.LoginButton()
1587
+ # chatbot = gr.Chatbot(
1588
+ # value=[
1589
+ # {"role": "assistant", "content": "I'm your financial assistant, how can I help you today?"},
1590
+
1591
+ # # {"role": "assistant", "content": "Hello! I can help you analyze financial data. Ask questions like \"Show revenue trends\" or \"Compare profitability ratios\""},
1592
+ # # {"role": "user", "content": "Show revenue trends for last 4 quarters"},
1593
+ # # {"role": "assistant", "content": "Revenue trend for GlobalTech Inc.:\n\nQ4 2024: $2.53B (+8.2%)\nQ1 2025: $2.61B (+9.8%)\nQ2 2025: $2.71B (+11.6%)\nQ3 2025: $2.84B (+12.4%)"},
1594
+ # # {"role": "assistant", "content": "Revenue trend for GlobalTech Inc.:\n\nQ4 2024: $2.53B (+8.2%)\nQ1 2025: $2.61B (+9.8%)\nQ2 2025: $2.71B (+11.6%)\nQ3 2025: $2.84B (+12.4%)"},
1595
+ # # {"role": "assistant", "content": "Revenue trend for GlobalTech Inc.:\n\nQ4 2024: $2.53B (+8.2%)\nQ1 2025: $2.61B (+9.8%)\nQ2 2025: $2.71B (+11.6%)\nQ3 2025: $2.84B (+12.4%)"},
1596
+ # # {"role": "assistant", "content": "Revenue trend for GlobalTech Inc.:\n\nQ4 2024: $2.53B (+8.2%)\nQ1 2025: $2.61B (+9.8%)\nQ2 2025: $2.71B (+11.6%)\nQ3 2025: $2.84B (+12.4%)"}
1597
+ # ],
1598
+ # type="messages",
1599
+ # # elem_classes=["min-h-0", "overflow-y-auto", "space-y-4", "chat-content-box"],
1600
+ # show_label=False,
1601
+ # autoscroll=True,
1602
+ # show_copy_button=True,
1603
+ # height=400,
1604
+ # container=False,
1605
+ # )
1606
+
1607
+ # # 输入区域
1608
+ # with gr.Row(elem_classes=["border-t", "border-gray-200", "gap-2"]):
1609
+ # msg = gr.Textbox(
1610
+ # placeholder="Ask a financial question...",
1611
+ # elem_classes=["flex-1", "border", "border-gray-300", "rounded-lg", "px-4", "py-2", "focus:border-blue-500"],
1612
+ # show_label=False,
1613
+ # lines=1,
1614
+ # submit_btn=True,
1615
+ # container=False,
1616
+ # )
1617
+ # msg.submit(
1618
+ # chat_bot,
1619
+ # [msg, chatbot],
1620
+ # [msg, chatbot],
1621
+ # queue=True,
1622
+ # )
1623
+
1624
+ # def load_css_files(css_dir, filenames):
1625
+ # css_content = ""
1626
+ # for filename in filenames:
1627
+ # path = os.path.join(css_dir, filename)
1628
+ # if os.path.exists(path):
1629
+ # with open(path, "r", encoding="utf-8") as f:
1630
+ # css_content += f.read() + "\n"
1631
+ # else:
1632
+ # print(f"⚠️ CSS file not found: {path}")
1633
+ # return css_content
1634
+ def main():
1635
+ # 获取当前目录
1636
+ current_dir = os.path.dirname(os.path.abspath(__file__))
1637
+ css_dir = os.path.join(current_dir, "css")
1638
+
1639
+ # def load_css_files(css_dir, filenames):
1640
+ # """读取多个 CSS 文件并合并为一个字符串"""
1641
+ # css_content = ""
1642
+ # for filename in filenames:
1643
+ # path = os.path.join(css_dir, filename)
1644
+ # if os.path.exists(path):
1645
+ # with open(path, "r", encoding="utf-8") as f:
1646
+ # css_content += f.read() + "\n"
1647
+ # else:
1648
+ # print(f"Warning: CSS file not found: {path}")
1649
+ # return css_content
1650
+ # 设置CSS路径
1651
+ css_paths = [
1652
+ os.path.join(css_dir, "main.css"),
1653
+ os.path.join(css_dir, "components.css"),
1654
+ os.path.join(css_dir, "layout.css")
1655
+ ]
1656
+ # css_dir = "path/to/your/css/folder" # 替换为你的实际路径
1657
+ # 自动定位 css 文件夹(与 app.py 同级)
1658
+ # BASE_DIR = os.path.dirname(os.path.abspath(__file__))
1659
+ # CSS_DIR = os.path.join(BASE_DIR, "css")
1660
+
1661
+ # css_files = ["main.css", "components.css", "layout.css"]
1662
+ # combined_css = load_css_files(CSS_DIR, css_files)
1663
+ # print(combined_css)
1664
+
1665
+ with gr.Blocks(
1666
+ title="Financial Analysis Dashboard",
1667
+ css_paths=css_paths,
1668
+ css=custom_css,
1669
+ # css=combined_css
1670
+ ) as demo:
1671
+
1672
+ # 添加处理公司点击事件的路由
1673
+ # 创建一个状态组件来跟踪选中的公司
1674
+ selected_company_state = gr.State("")
1675
+
1676
+ with gr.Column(elem_classes=["container", "container-h"]):
1677
+ # 头部
1678
+ create_header()
1679
+
1680
+ # 创建主布局
1681
+ with gr.Row(elem_classes=["main-content-box"]):
1682
+ # 左侧边栏
1683
+ with gr.Column(scale=1, min_width=350):
1684
+ # 获取company_list组件的引用
1685
+ company_list_component, report_section_component, report_display_component, news_display_component = create_sidebar()
1686
+
1687
+ # 主内容区域
1688
+ with gr.Column(scale=9):
1689
+
1690
+ # 指标仪表板
1691
+ create_metrics_dashboard()
1692
+
1693
+ with gr.Row(elem_classes=["main-content-box"]):
1694
+ with gr.Column(scale=8):
1695
+ # Tab内容
1696
+ with gr.Tabs():
1697
+ with gr.TabItem("Invest Suggest", elem_classes=["tab-item"]):
1698
+ # 创建一个用于显示公司名称的组件
1699
+ # company_display = gr.Markdown("# Please select a company")
1700
+ # 创建一个占位符用于显示tab内容
1701
+ tab_content = gr.Markdown(elem_classes=["invest-suggest-md-box"])
1702
+
1703
+ # 当选中的公司改变时,更新显示
1704
+ # selected_company_state.change(
1705
+ # fn=lambda company: f"# Investment Suggestions for {company}" if company else "# Please select a company",
1706
+ # inputs=[selected_company_state],
1707
+ # outputs=[company_display]
1708
+ # )
1709
+
1710
+ # 当选中的公司改变时,重新加载tab内容
1711
+ def update_tab_content(company):
1712
+ if company:
1713
+ # 显示loading状态
1714
+ loading_html = f'''
1715
+ <div style="display: flex; justify-content: center; align-items: center; height: 200px;">
1716
+ <div style="text-align: center;">
1717
+ <div class="loading-spinner" style="width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
1718
+ <p style="margin-top: 20px; color: #666;">Loading investment suggestions for {company}...</p>
1719
+ <style>
1720
+ @keyframes spin {{
1721
+ 0% {{ transform: rotate(0deg); }}
1722
+ 100% {{ transform: rotate(360deg); }}
1723
+ }}
1724
+ </style>
1725
+ </div>
1726
+ </div>
1727
+ '''
1728
+ yield loading_html
1729
+
1730
+ # 获取投资建议数据
1731
+ try:
1732
+ content = get_invest_suggest(company)
1733
+ yield content
1734
+ except Exception as e:
1735
+ error_html = f'''
1736
+ <div style="padding: 20px; text-align: center; color: #666;">
1737
+ <p>Error loading investment suggestions: {str(e)}</p>
1738
+ <p>Please try again later.</p>
1739
+ </div>
1740
+ '''
1741
+ yield error_html
1742
+ else:
1743
+ yield "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>"
1744
+
1745
+ selected_company_state.change(
1746
+ fn=update_tab_content,
1747
+ inputs=[selected_company_state],
1748
+ outputs=[tab_content],
1749
+ )
1750
+ with gr.TabItem("Analysis Report", elem_classes=["tab-item"]):
1751
+ # 创建一个用于显示公司名称的组件
1752
+ # analysis_company_display = gr.Markdown("# Please select a company")
1753
+ # 创建一个占位符用于显示tab内容
1754
+ analysis_tab_content = gr.Markdown(elem_classes=["analysis-report-md-box"])
1755
+
1756
+ # 当选中的公司改变时,更新显示
1757
+ # selected_company_state.change(
1758
+ # fn=lambda company: f"# Analysis Report for {company}" if company else "# Please select a company",
1759
+ # inputs=[selected_company_state],
1760
+ # outputs=[analysis_company_display]
1761
+ # )
1762
+
1763
+ # 当选中的公司改变时,重新加载tab内容
1764
+ def update_analysis_tab_content(company):
1765
+ if company:
1766
+ # 显示loading状态
1767
+ loading_html = f'''
1768
+ <div style="display: flex; justify-content: center; align-items: center; height: 200px;">
1769
+ <div style="text-align: center;">
1770
+ <div class="loading-spinner" style="width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
1771
+ <p style="margin-top: 20px; color: #666;">Loading analysis report for {company}...</p>
1772
+ <style>
1773
+ @keyframes spin {{
1774
+ 0% {{ transform: rotate(0deg); }}
1775
+ 100% {{ transform: rotate(360deg); }}
1776
+ }}
1777
+ </style>
1778
+ </div>
1779
+ </div>
1780
+ '''
1781
+ yield loading_html
1782
+
1783
+ # 获取分析报告数据
1784
+ try:
1785
+ # 这里应该调用获取详细分析报告的函数
1786
+ # 暂时使用占位内容,您需要替换为实际的函数调用
1787
+ # content = f"# Analysis Report for {company}\n\nDetailed financial analysis for {company} will be displayed here."
1788
+ yield get_analysis_report(company)
1789
+ except Exception as e:
1790
+ error_html = f'''
1791
+ <div style="padding: 20px; text-align: center; color: #666;">
1792
+ <p>Error loading analysis report: {str(e)}</p>
1793
+ <p>Please try again later.</p>
1794
+ </div>
1795
+ '''
1796
+ yield error_html
1797
+ else:
1798
+ yield "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>"
1799
+
1800
+ selected_company_state.change(
1801
+ fn=update_analysis_tab_content,
1802
+ inputs=[selected_company_state],
1803
+ outputs=[analysis_tab_content]
1804
+ )
1805
+ # with gr.TabItem("Comparison", elem_classes=["tab-item"]):
1806
+ # create_tab_content("comparison")
1807
+ with gr.Column(scale=2, min_width=400):
1808
+ # 聊天面板
1809
+ # gr.ChatInterface(
1810
+ # respond,
1811
+ # title="Easy Financial Report",
1812
+ # # label="Easy Financial Report",
1813
+ # additional_inputs=[
1814
+ # # gr.Textbox(value="You are a financial analysis assistant. Provide concise investment insights from company financial reports.", label="System message"),
1815
+ # # gr.Slider(minimum=1, maximum=4096, value=1024, step=1, label="Max new tokens"),
1816
+ # # gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
1817
+ # # gr.Slider(
1818
+ # # minimum=0.1,
1819
+ # # maximum=1.0,
1820
+ # # value=0.95,
1821
+ # # step=0.05,
1822
+ # # label="Top-p (nucleus sampling)",
1823
+ # # ),
1824
+ # gr.State(value="") # CRITICAL: Add State to store session URL across turns
1825
+ # ],
1826
+ # )
1827
+ # chatbot.render()
1828
+ # gr.LoginButton()
1829
+ gr.ChatInterface(
1830
+ respond,
1831
+ title="Easy Financial Report",
1832
+ additional_inputs=[
1833
+ gr.State(value=""), # CRITICAL: Store session URL across turns (hidden from UI)
1834
+ gr.State(value={}) # CRITICAL: Store agent context across turns (hidden from UI)
1835
+ ],
1836
+ additional_inputs_accordion=gr.Accordion(label="Settings", open=False, visible=False), # Hide the accordion completely
1837
+ )
1838
+ # chatbot.render()
1839
+
1840
+ # with gr.Blocks() as demo:
1841
+ # # Add custom CSS for Agent Plan styling
1842
+ # gr.Markdown("""
1843
+ # <style>
1844
+ # .agent-plan {
1845
+ # background-color: #f8f9fa;
1846
+ # border-left: 4px solid #6c757d;
1847
+ # padding: 10px;
1848
+ # margin: 10px 0;
1849
+ # border-radius: 4px;
1850
+ # font-family: monospace;
1851
+ # color: #495057;
1852
+ # }
1853
+ # </style>
1854
+ # """)
1855
+ # chatbot.render()
1856
+
1857
+ # 在页面加载时自动刷新公司列表,确保显示最新的数据
1858
+ # demo.load(
1859
+ # fn=get_company_list_choices,
1860
+ # inputs=[],
1861
+ # outputs=[company_list_component],
1862
+ # concurrency_limit=None,
1863
+ # )
1864
+
1865
+ # 绑定公司选择事件到状态更新
1866
+ # 注意:这里需要确保create_sidebar中没有重复绑定相同的事件
1867
+ company_list_component.change(
1868
+ fn=lambda x: x, # 直接返回选中的公司名称
1869
+ inputs=[company_list_component],
1870
+ outputs=[selected_company_state],
1871
+ concurrency_limit=None
1872
+ )
1873
+
1874
+ # 绑定公司选择事件到指标仪表板更新
1875
+ def update_metrics_dashboard_wrapper(company_name):
1876
+ if company_name:
1877
+ # 显示loading状态
1878
+ loading_html = f'''
1879
+ <div style="display: flex; justify-content: center; align-items: center; height: 300px;">
1880
+ <div style="text-align: center;">
1881
+ <div class="loading-spinner" style="width: 40px; height: 40px; border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto;"></div>
1882
+ <p style="margin-top: 20px; color: #666;">Loading financial data for {company_name}...</p>
1883
+ <style>
1884
+ @keyframes spin {{
1885
+ 0% {{ transform: rotate(0deg); }}
1886
+ 100% {{ transform: rotate(360deg); }}
1887
+ }}
1888
+ </style>
1889
+ </div>
1890
+ </div>
1891
+ '''
1892
+ yield loading_html, loading_html, loading_html
1893
+
1894
+ # 获取更新后的数据
1895
+ try:
1896
+ stock_card_html, financial_metrics_html, income_table_html = update_metrics_dashboard(company_name)
1897
+ yield stock_card_html, financial_metrics_html, income_table_html
1898
+ except Exception as e:
1899
+ error_html = f'''
1900
+ <div style="padding: 20px; text-align: center; color: #666;">
1901
+ <p>Error loading financial data: {str(e)}</p>
1902
+ <p>Please try again later.</p>
1903
+ </div>
1904
+ '''
1905
+ yield error_html, error_html, error_html
1906
+ else:
1907
+ # 如果没有选择公司,返回空内容
1908
+ empty_html = "<div style=\"padding: 20px; text-align: center; color: #666;\">Please select a company</div>"
1909
+ yield empty_html, empty_html, empty_html
1910
+
1911
+ selected_company_state.change(
1912
+ fn=update_metrics_dashboard_wrapper,
1913
+ inputs=[selected_company_state],
1914
+ outputs=list(metrics_dashboard_components),
1915
+ concurrency_limit=None
1916
+ )
1917
+
1918
+ return demo
1919
+
1920
+ if __name__ == "__main__":
1921
+ demo = main()
1922
+ demo.launch(share=True)
app.py CHANGED
@@ -4,20 +4,24 @@ import datetime
4
  import re
5
  import pandas as pd
6
  from sqlalchemy import true
 
7
  from chatbot.chat_main import respond
8
  import globals as g
9
  from service.mysql_service import get_companys, insert_company, get_company_by_name
10
  from service.chat_service import get_analysis_report, get_stock_price_from_bailian, search_company, search_news, get_invest_suggest, chat_bot
11
  from service.company import check_company_exists
12
  from service.hf_upload import get_hf_files_with_links
13
- from service.news_quote_mcp import get_company_news, get_quote
14
- from service.report_mcp import query_financial_data
 
15
  from service.report_tools import build_financial_metrics_three_year_data, calculate_yoy_comparison, extract_financial_table, extract_last_three_with_fallback, get_yearly_data
16
  from service.three_year_table_tool import build_table_format
17
  from service.three_year_tool import process_financial_data_with_metadata
18
  from service.tool_processor import get_stock_price
19
 
 
20
  get_companys_state = True
 
21
  # JavaScript代码用于读取和存储数据
22
  js_code = """
23
  function handleStorage(operation, key, value) {
@@ -139,7 +143,8 @@ def get_company_list_choices():
139
  if not get_companys_state:
140
  return gr.update(choices=choices)
141
  try:
142
- companies_data = get_companys()
 
143
  print(f"Getting init add company list choices...companies_data: {companies_data}")
144
  if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
145
  choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
@@ -158,27 +163,22 @@ def handle_company_click(company_name):
158
  print(f"Handling click for company: {company_name}")
159
 
160
  # 1. 判断是否已经入库
161
- if not check_company_exists(company_name):
162
  # 2. 如果没有入库,则进行入库操作
163
  # 获取股票代码(如果有的话)
164
  stock_code = companies_map.get(company_name, {}).get("CODE", "Unknown")
165
  print(f"Inserting company {company_name} with code {stock_code}")
166
 
167
  # 插入公司到数据库
168
- success = insert_company(company_name, stock_code)
169
- if success:
170
- print(f"Successfully inserted company: {company_name}")
171
- # 直接更新companies_map,而不是重新加载整个映射
172
- companies_map[company_name] = {"NAME": company_name, "CODE": stock_code}
173
- # 使用Gradio的成功提示
174
- gr.Info(f"Successfully added company: {company_name}")
175
- # 返回True表示添加成功,需要刷新列表
176
- return True
177
- else:
178
- print(f"Failed to insert company: {company_name}")
179
- # 使用Gradio的错误提示
180
- gr.Error(f"Failed to insert company: {company_name}")
181
- return False
182
  else:
183
  print(f"Company {company_name} already exists in database")
184
  # 使用Gradio的警告提示
@@ -190,7 +190,8 @@ def handle_company_click(company_name):
190
  def get_company_list_html(selected_company=""):
191
  try:
192
  # 从数据库获取所有公司
193
- companies_data = get_companys()
 
194
  # 检查是否为错误信息
195
  if isinstance(companies_data, str):
196
  if "查询执行失败" in companies_data:
@@ -247,8 +248,12 @@ def select_company(company_name):
247
  g.SELECT_COMPANY = company_name if company_name else ""
248
  # 对于Radio组件,我们只需要返回更新后的选项列表
249
  try:
250
- companies_data = get_companys()
251
- if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
 
 
 
 
252
  choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
253
  else:
254
  choices = []
@@ -287,7 +292,8 @@ def initialize_companies_map():
287
  print(f"Predefined companies added: {len(predefined_companies)}")
288
 
289
  # 从数据库获取公司数据
290
- companies_data = get_companys()
 
291
  # companies_data = window.cachedCompanies or []
292
 
293
 
@@ -408,31 +414,32 @@ def add_company(selected, current_list):
408
  # print(f"Company name: {company_name}, Stock code: {stock_code}")
409
  # print(f"Company exists: {check_company_exists(company_name)}")
410
 
411
- if not check_company_exists(company_name):
412
  # 入库
413
- success = insert_company(company_name, stock_code)
414
- if success:
415
- # 从数据库获取更新后的公司列表
416
- try:
417
- companies_data = get_companys()
418
- if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
419
- updated_list = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
420
- else:
421
- updated_list = []
422
- except:
 
 
 
 
423
  updated_list = []
424
 
425
- # 添加默认公司选项
426
- if not updated_list:
427
- updated_list = ['Alibaba', '腾讯控股', 'Tencent', '阿里巴巴-W', 'Apple']
428
 
429
- # 成功插入后清除状态消息,并更新Radio组件的选项,同时默认选中刚添加的公司
430
- # 通过设置value参数,会自动触发change事件来加载数据
431
- return gr.update(visible=False), gr.update(choices=updated_list, value=company_name), gr.update(visible=False, value="")
432
- else:
433
- # 插入失败显示错误消息,使用Gradio内置的错误提示
434
- gr.Error("插入公司失败")
435
- return gr.update(visible=False), current_list, gr.update(visible=False, value="")
436
  else:
437
  # 公司已存在,使用Gradio内置的警告消息
438
  gr.Warning(f"公司 '{company_name}' 已存在")
@@ -498,35 +505,48 @@ def update_report_section(selected_company, report_data, stock_code):
498
  return gr.update(value=html_content, visible=True)
499
  else:
500
  # 有选中的公司,显示相关报告
501
- # try:
502
- # # 尝试从Hugging Face获取文件列表
503
- # report_data = get_hf_files_with_links("JC321/files-world")
504
- # except Exception as e:
505
- # # 如果获取失败,使用模拟数据并显示错误消息
506
- # print(f"获取Hugging Face文件列表失败: {str(e)}")
507
- # report_data = []
508
- stock_code = get_stock_code_by_company_name(selected_company)
509
- report_data = query_financial_data(stock_code, "5-Year")
510
- # report_data = process_financial_data_with_metadata(financial_metrics_pre)
511
-
512
- html_content = '<div class="report-list-box bg-white">'
513
- html_content += '<div class="report-list-title bg-white card-title left-card-title" style="border-bottom: 1px solid #e3e3e6 !important;"><h3>Financial Reports</h3></div>'
514
- for report in report_data:
515
- html_content += f'''
516
- <div class="report-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{report['source_url']}', '_blank')">
 
 
 
 
 
 
 
 
 
 
517
  <div class="report-item-content">
518
- <span class="text-gray-800">{report['period']}-{stock_code}-{report['source_form']}</span>
519
  <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" class="text-blue-500" viewBox="0 0 20 20" fill="currentColor">
520
  <path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 10-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l-1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd" />
521
  </svg>
522
  </div>
523
  </div>
524
  '''
525
-
526
- html_content += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}份报告</span></div>'
527
- html_content += '</div>'
528
-
529
- return gr.update(value=html_content, visible=True)
 
 
 
530
  def update_news_section(selected_company):
531
  """根据选中的公司更新报告部分"""
532
  html_content = ""
@@ -599,44 +619,38 @@ def create_header():
599
  gr.Markdown(current_time, elem_classes=["text-sm-top-time"])
600
 
601
  def create_company_list(get_companys_state):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
  company_list = gr.Radio(
603
- choices=[],
604
  label="",
605
  interactive=True,
606
  elem_classes=["company-list-container"],
607
  container=False, # 不显示外部容器边框
608
  visible=True
609
- )
610
- if (get_companys_state == False):
611
- return company_list
612
- else:
613
- """创建公司列表组件"""
614
- # 获取公司列表数据
615
- try:
616
- companies_data = get_companys()
617
- print(f"创建公司列表组件 - Companies data: {companies_data}")
618
- if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
619
- choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
620
- else:
621
- choices = []
622
- except:
623
- choices = []
624
-
625
- # 添加默认公司选项
626
- if not choices:
627
- choices = []
628
-
629
- # 使用Radio组件显示公司列表,不显示标签
630
- company_list = gr.Radio(
631
- choices=choices,
632
- label="",
633
- interactive=True,
634
- elem_classes=["company-list-container"],
635
- container=False, # 不显示外部容器边框
636
- visible=True
637
- )
638
-
639
- return company_list
640
 
641
  def create_company_selector():
642
  """创建公司选择器组件"""
@@ -830,8 +844,12 @@ def create_sidebar():
830
  if result is True:
831
  # 正确地刷新通过create_company_list()创建的Radio组件
832
  try:
833
- companies_data = get_companys()
834
- if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
 
 
 
 
835
  updated_choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
836
  else:
837
  updated_choices = []
@@ -1786,6 +1804,9 @@ def main():
1786
  # 这里应该调用获取详细分析报告的函数
1787
  # 暂时使用占位内容,您需要替换为实际的函数调用
1788
  # content = f"# Analysis Report for {company}\n\nDetailed financial analysis for {company} will be displayed here."
 
 
 
1789
  yield get_analysis_report(company)
1790
  except Exception as e:
1791
  error_html = f'''
@@ -1805,55 +1826,20 @@ def main():
1805
  )
1806
  # with gr.TabItem("Comparison", elem_classes=["tab-item"]):
1807
  # create_tab_content("comparison")
1808
- with gr.Column(scale=2, min_width=400):
1809
  # 聊天面板
 
 
 
1810
  # gr.ChatInterface(
1811
  # respond,
1812
  # title="Easy Financial Report",
1813
- # # label="Easy Financial Report",
1814
  # additional_inputs=[
1815
- # # gr.Textbox(value="You are a financial analysis assistant. Provide concise investment insights from company financial reports.", label="System message"),
1816
- # # gr.Slider(minimum=1, maximum=4096, value=1024, step=1, label="Max new tokens"),
1817
- # # gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
1818
- # # gr.Slider(
1819
- # # minimum=0.1,
1820
- # # maximum=1.0,
1821
- # # value=0.95,
1822
- # # step=0.05,
1823
- # # label="Top-p (nucleus sampling)",
1824
- # # ),
1825
- # gr.State(value="") # CRITICAL: Add State to store session URL across turns
1826
  # ],
 
1827
  # )
1828
- # chatbot.render()
1829
- # gr.LoginButton()
1830
- gr.ChatInterface(
1831
- respond,
1832
- title="Easy Financial Report",
1833
- additional_inputs=[
1834
- gr.State(value=""), # CRITICAL: Store session URL across turns (hidden from UI)
1835
- gr.State(value={}) # CRITICAL: Store agent context across turns (hidden from UI)
1836
- ],
1837
- additional_inputs_accordion=gr.Accordion(label="Settings", open=False, visible=False), # Hide the accordion completely
1838
- )
1839
- # chatbot.render()
1840
-
1841
- # with gr.Blocks() as demo:
1842
- # # Add custom CSS for Agent Plan styling
1843
- # gr.Markdown("""
1844
- # <style>
1845
- # .agent-plan {
1846
- # background-color: #f8f9fa;
1847
- # border-left: 4px solid #6c757d;
1848
- # padding: 10px;
1849
- # margin: 10px 0;
1850
- # border-radius: 4px;
1851
- # font-family: monospace;
1852
- # color: #495057;
1853
- # }
1854
- # </style>
1855
- # """)
1856
- # chatbot.render()
1857
 
1858
  # 在页面加载时自动刷新公司列表,确保显示最新的数据
1859
  # demo.load(
 
4
  import re
5
  import pandas as pd
6
  from sqlalchemy import true
7
+ # from EasyFinancialAgent.chat import query_company
8
  from chatbot.chat_main import respond
9
  import globals as g
10
  from service.mysql_service import get_companys, insert_company, get_company_by_name
11
  from service.chat_service import get_analysis_report, get_stock_price_from_bailian, search_company, search_news, get_invest_suggest, chat_bot
12
  from service.company import check_company_exists
13
  from service.hf_upload import get_hf_files_with_links
14
+ from MarketandStockMCP.news_quote_mcp import get_company_news, get_quote
15
+ from EasyReportDataMCP.report_mcp import query_financial_data
16
+ from service.report_service import get_report_data, query_company_advanced
17
  from service.report_tools import build_financial_metrics_three_year_data, calculate_yoy_comparison, extract_financial_table, extract_last_three_with_fallback, get_yearly_data
18
  from service.three_year_table_tool import build_table_format
19
  from service.three_year_tool import process_financial_data_with_metadata
20
  from service.tool_processor import get_stock_price
21
 
22
+
23
  get_companys_state = True
24
+ my_companies = []
25
  # JavaScript代码用于读取和存储数据
26
  js_code = """
27
  function handleStorage(operation, key, value) {
 
143
  if not get_companys_state:
144
  return gr.update(choices=choices)
145
  try:
146
+ # companies_data = get_companys()
147
+ companies_data = my_companies
148
  print(f"Getting init add company list choices...companies_data: {companies_data}")
149
  if isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
150
  choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
 
163
  print(f"Handling click for company: {company_name}")
164
 
165
  # 1. 判断是否已经入库
166
+ if not check_company_exists(my_companies, company_name):
167
  # 2. 如果没有入库,则进行入库操作
168
  # 获取股票代码(如果有的话)
169
  stock_code = companies_map.get(company_name, {}).get("CODE", "Unknown")
170
  print(f"Inserting company {company_name} with code {stock_code}")
171
 
172
  # 插入公司到数据库
173
+ # success = insert_company(company_name, stock_code)
174
+ my_companies.append({"company_name": company_name, "stock_code": stock_code})
175
+ print(f"Successfully inserted company: {company_name}") # 直接更新companies_map,而不是重新加载整个映射
176
+ # 直接更新companies_map,而不是重新加载整个映射
177
+ companies_map[company_name] = {"NAME": company_name, "CODE": stock_code}
178
+ # 使用Gradio的成功提示
179
+ gr.Info(f"Successfully added company: {company_name}")
180
+ # 返回True表示添加成功,需要刷新列表
181
+ return True
 
 
 
 
 
182
  else:
183
  print(f"Company {company_name} already exists in database")
184
  # 使用Gradio的警告提示
 
190
  def get_company_list_html(selected_company=""):
191
  try:
192
  # 从数据库获取所有公司
193
+ # companies_data = get_companys()
194
+ companies_data = my_companies
195
  # 检查是否为错误信息
196
  if isinstance(companies_data, str):
197
  if "查询执行失败" in companies_data:
 
248
  g.SELECT_COMPANY = company_name if company_name else ""
249
  # 对于Radio组件,我们只需要返回更新后的选项列表
250
  try:
251
+ # companies_data = get_companys()
252
+ companies_data = my_companies
253
+ if isinstance(companies_data, list) and len(companies_data) > 0:
254
+ # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
255
+ choices = [str(item.get('company_name', 'Unknown')) for item in companies_data]
256
+ elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
257
  choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
258
  else:
259
  choices = []
 
292
  print(f"Predefined companies added: {len(predefined_companies)}")
293
 
294
  # 从数据库获取公司数据
295
+ # companies_data = get_companys()
296
+ companies_data = my_companies
297
  # companies_data = window.cachedCompanies or []
298
 
299
 
 
414
  # print(f"Company name: {company_name}, Stock code: {stock_code}")
415
  # print(f"Company exists: {check_company_exists(company_name)}")
416
 
417
+ if not check_company_exists(my_companies, company_name):
418
  # 入库
419
+ # success = insert_company(company_name, stock_code)
420
+ my_companies.append({"company_name": company_name, "stock_code": stock_code})
421
+ # 从数据库获取更新后的公司列表
422
+ try:
423
+ # companies_data = get_companys()
424
+ companies_data = my_companies
425
+ if isinstance(companies_data, list) and len(companies_data) > 0:
426
+ # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
427
+ updated_list = [str(item.get('company_name', 'Unknown')) for item in companies_data]
428
+ elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
429
+ updated_list = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
430
+ else:
431
+ updated_list = []
432
+ except:
433
  updated_list = []
434
 
435
+ # 添加默认公司选项
436
+ if not updated_list:
437
+ updated_list = ['Alibaba', '腾讯控股', 'Tencent', '阿里巴巴-W', 'Apple']
438
 
439
+ # 成功插入后清除状态消息,并更新Radio组件的选项,同时默认选中刚添加的公司
440
+ # 通过设置value参数,会自动触发change事件来加载数据
441
+ return gr.update(visible=False), gr.update(choices=updated_list, value=company_name), gr.update(visible=False, value="")
442
+
 
 
 
443
  else:
444
  # 公司已存在,使用Gradio内置的警告消息
445
  gr.Warning(f"公司 '{company_name}' 已存在")
 
505
  return gr.update(value=html_content, visible=True)
506
  else:
507
  # 有选中的公司,显示相关报告
508
+ try:
509
+ # prmpt = f"""
510
+
511
+ # """
512
+ stock_code = get_stock_code_by_company_name(selected_company)
513
+ result = get_report_data(stock_code)
514
+ print(f"get_report_data=====================: {result}")
515
+ report_data = query_financial_data(stock_code, "5-Year")
516
+ # report_data = process_financial_data_with_metadata(financial_metrics_pre)
517
+
518
+ # 检查 report_data 是否是列表且第一个元素是字典
519
+ if not isinstance(report_data, list) or len(report_data) == 0:
520
+ return gr.update(value="<div>暂无报告数据</div>", visible=True)
521
+
522
+ # 检查第一个元素是否是字典
523
+ if not isinstance(report_data[0], dict):
524
+ return gr.update(value="<div>数据格式不正常</div>", visible=True)
525
+
526
+ html_content = '<div class="report-list-box bg-white">'
527
+ html_content += '<div class="report-list-title bg-white card-title left-card-title" style="border-bottom: 1px solid #e3e3e6 !important;"><h3>Financial Reports</h3></div>'
528
+ for report in report_data:
529
+ source_url = report.get('source_url', '#')
530
+ period = report.get('period', 'N/A')
531
+ source_form = report.get('source_form', 'N/A')
532
+ html_content += f'''
533
+ <div class="report-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{source_url}', '_blank')">
534
  <div class="report-item-content">
535
+ <span class="text-gray-800">{period}-{stock_code}-{source_form}</span>
536
  <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" class="text-blue-500" viewBox="0 0 20 20" fill="currentColor">
537
  <path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 10-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l-1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd" />
538
  </svg>
539
  </div>
540
  </div>
541
  '''
542
+
543
+ html_content += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}份报告</span></div>'
544
+ html_content += '</div>'
545
+
546
+ return gr.update(value=html_content, visible=True)
547
+ except Exception as e:
548
+ print(f"Error in update_report_section: {str(e)}")
549
+ return gr.update(value=f"<div>报告载入失败: {str(e)}</div>", visible=True)
550
  def update_news_section(selected_company):
551
  """根据选中的公司更新报告部分"""
552
  html_content = ""
 
619
  gr.Markdown(current_time, elem_classes=["text-sm-top-time"])
620
 
621
  def create_company_list(get_companys_state):
622
+ """创建公司列表组件"""
623
+ try:
624
+ # 获取公司列表数据
625
+ # companies_data = get_companys()
626
+ companies_data = my_companies
627
+ print(f"创建公司列表组件 - Companies data: {companies_data}")
628
+ if isinstance(companies_data, list) and len(companies_data) > 0:
629
+ # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
630
+ choices = [str(item.get('company_name', 'Unknown')) for item in companies_data]
631
+ elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
632
+ choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
633
+ else:
634
+ choices = []
635
+ except Exception as e:
636
+ print(f"Error creating company list: {str(e)}")
637
+ choices = []
638
+
639
+ # 添加默认公司选项
640
+ if not choices:
641
+ choices = []
642
+
643
+ # 使用Radio组件显示公司列表,不显示标签
644
  company_list = gr.Radio(
645
+ choices=choices,
646
  label="",
647
  interactive=True,
648
  elem_classes=["company-list-container"],
649
  container=False, # 不显示外部容器边框
650
  visible=True
651
+ )
652
+
653
+ return company_list
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
 
655
  def create_company_selector():
656
  """创建公司选择器组件"""
 
844
  if result is True:
845
  # 正确地刷新通过create_company_list()创建的Radio组件
846
  try:
847
+ # companies_data = get_companys()
848
+ companies_data = my_companies
849
+ if isinstance(companies_data, list) and len(companies_data) > 0:
850
+ # my_companies 是对象列表 [{company_name: '', stock_code: ''}, ...]
851
+ updated_choices = [str(item.get('company_name', 'Unknown')) for item in companies_data]
852
+ elif isinstance(companies_data, pd.DataFrame) and not companies_data.empty:
853
  updated_choices = [str(row.get('company_name', 'Unknown')) for _, row in companies_data.iterrows()]
854
  else:
855
  updated_choices = []
 
1804
  # 这里应该调用获取详细分析报告的函数
1805
  # 暂时使用占位内容,您需要替换为实际的函数调用
1806
  # content = f"# Analysis Report for {company}\n\nDetailed financial analysis for {company} will be displayed here."
1807
+ stock_code = get_stock_code_by_company_name(company)
1808
+ result = query_company_advanced(stock_code)
1809
+ print(f"Result=====================: {result}")
1810
  yield get_analysis_report(company)
1811
  except Exception as e:
1812
  error_html = f'''
 
1826
  )
1827
  # with gr.TabItem("Comparison", elem_classes=["tab-item"]):
1828
  # create_tab_content("comparison")
1829
+ # with gr.Column(scale=2, min_width=400):
1830
  # 聊天面板
1831
+
1832
+ # chatbot.render()
1833
+ # gr.LoginButton()
1834
  # gr.ChatInterface(
1835
  # respond,
1836
  # title="Easy Financial Report",
 
1837
  # additional_inputs=[
1838
+ # gr.State(value=""), # CRITICAL: Store session URL across turns (hidden from UI)
1839
+ # gr.State(value={}) # CRITICAL: Store agent context across turns (hidden from UI)
 
 
 
 
 
 
 
 
 
1840
  # ],
1841
+ # additional_inputs_accordion=gr.Accordion(label="Settings", open=False, visible=False), # Hide the accordion completely
1842
  # )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1843
 
1844
  # 在页面加载时自动刷新公司列表,确保显示最新的数据
1845
  # demo.load(
requirements.txt CHANGED
@@ -14,4 +14,9 @@ httpx>=0.23.0
14
  pymysql
15
  sse-starlette>=1.6.5
16
  starlette>=0.27.0
17
- mcp>=1.0.0
 
 
 
 
 
 
14
  pymysql
15
  sse-starlette>=1.6.5
16
  starlette>=0.27.0
17
+ mcp>=1.0.0
18
+ uvicorn[standard]==0.27.0
19
+ pydantic==2.5.3
20
+
21
+ # SEC EDGAR API
22
+ sec-edgar-api==1.1.0
service/news_quote_mcp_server.py DELETED
@@ -1,321 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Finnhub MCP Server - Standard MCP Protocol (JSON-RPC over SSE)
4
-
5
- This is a standalone MCP server that implements the standard Model Context Protocol.
6
- It provides three tools for accessing Finnhub financial data:
7
- - get_quote: Real-time stock quotes
8
- - get_market_news: Latest market news by category
9
- - get_company_news: Company-specific news
10
-
11
- Usage:
12
- python mcp_server.py
13
-
14
- Environment Variables:
15
- FINNHUB_API_KEY: Your Finnhub API key (required)
16
- """
17
-
18
- import os
19
- import requests
20
- from typing import Optional, Dict, Any
21
- from datetime import datetime, timedelta
22
- from mcp.server.fastmcp import FastMCP
23
-
24
- # Initialize FastMCP server with standard MCP protocol
25
- mcp = FastMCP("finnhub-market-info")
26
-
27
-
28
- def get_api_key() -> Optional[str]:
29
- """Get the Finnhub API key from environment"""
30
- return os.getenv("FINNHUB_API_KEY")
31
-
32
-
33
- def make_finnhub_request(endpoint: str, params: Dict[str, Any]) -> Dict[str, Any]:
34
- """
35
- Make a request to Finnhub API
36
-
37
- Args:
38
- endpoint: API endpoint path
39
- params: Query parameters
40
-
41
- Returns:
42
- API response as dictionary
43
- """
44
- api_key = get_api_key()
45
- if not api_key:
46
- return {"error": "API key not configured. Please set FINNHUB_API_KEY environment variable."}
47
-
48
- params["token"] = api_key
49
- base_url = "https://finnhub.io/api/v1"
50
- url = f"{base_url}/{endpoint}"
51
-
52
- try:
53
- response = requests.get(url, params=params, timeout=10)
54
- response.raise_for_status()
55
- return response.json()
56
- except requests.exceptions.RequestException as e:
57
- return {"error": f"API request failed: {str(e)}"}
58
-
59
-
60
- @mcp.tool()
61
- def get_quote(symbol: str) -> dict:
62
- """
63
- Get real-time quote data for US stocks. Use this tool when you need current stock price
64
- information and market performance metrics for any US-listed stock.
65
-
66
- When to use:
67
- - User asks "What's the current price of [stock]?"
68
- - Need real-time stock quote data
69
- - User mentions "stock price", "current value", "how is [stock] trading?"
70
- - Want to check latest market price and daily changes
71
-
72
- Examples:
73
- - "What's Apple's stock price?" → get_quote(symbol="AAPL")
74
- - "How is Tesla trading today?" → get_quote(symbol="TSLA")
75
- - "Show me Microsoft's current quote" → get_quote(symbol="MSFT")
76
-
77
- Args:
78
- symbol: Stock ticker symbol (e.g., 'AAPL', 'MSFT', 'TSLA', 'GOOGL')
79
-
80
- Returns:
81
- dict: Real-time quote data containing:
82
- - symbol: Stock ticker symbol
83
- - current_price: Current trading price (c)
84
- - change: Price change in dollars (d)
85
- - percent_change: Price change in percentage (dp)
86
- - high: Today's high price (h)
87
- - low: Today's low price (l)
88
- - open: Opening price (o)
89
- - previous_close: Previous trading day's closing price (pc)
90
- - timestamp: Quote timestamp
91
- """
92
- result = make_finnhub_request("quote", {"symbol": symbol.upper()})
93
-
94
- if "error" in result:
95
- return {
96
- "error": result["error"],
97
- "symbol": symbol.upper()
98
- }
99
-
100
- # Return structured data
101
- return {
102
- "symbol": symbol.upper(),
103
- "current_price": result.get('c'),
104
- "change": result.get('d'),
105
- "percent_change": result.get('dp'),
106
- "high": result.get('h'),
107
- "low": result.get('l'),
108
- "open": result.get('o'),
109
- "previous_close": result.get('pc'),
110
- "timestamp": datetime.fromtimestamp(result.get('t', 0)).strftime('%Y-%m-%d %H:%M:%S') if result.get('t') else None
111
- }
112
-
113
-
114
- @mcp.tool()
115
- def get_market_news(category: str = "general", min_id: int = 0) -> dict:
116
- """
117
- Get latest market news across different categories. Use this tool when you need current market
118
- news, trends, and developments in general markets, forex, cryptocurrency, or mergers.
119
-
120
- When to use:
121
- - User asks "What's the latest market news?"
122
- - Need current financial news and market updates
123
- - User mentions "news", "market trends", "what's happening in the market?"
124
- - Want to get news for specific categories (forex, crypto, M&A)
125
-
126
- Categories explained:
127
- - general: General market news, stocks, economy, major companies
128
- - forex: Foreign exchange and currency market news
129
- - crypto: Cryptocurrency and blockchain news
130
- - merger: Mergers & acquisitions, corporate deals
131
-
132
- Examples:
133
- - "What's the latest market news?" → get_market_news(category="general")
134
- - "Show me crypto news" → get_market_news(category="crypto")
135
- - "Any forex updates?" → get_market_news(category="forex")
136
- - "Recent merger news" → get_market_news(category="merger")
137
-
138
- Args:
139
- category: News category - "general", "forex", "crypto", or "merger" (default: "general")
140
- min_id: Minimum news ID for pagination (default: 0, use 0 to get latest news)
141
-
142
- Returns:
143
- dict: Market news data containing:
144
- - category: News category requested
145
- - total_articles: Total number of articles returned
146
- - articles: List of news articles (max 10), each with:
147
- * id: Article ID
148
- * headline: News headline
149
- * summary: Brief summary of the article
150
- * source: News source
151
- * url: Link to full article
152
- * published: Publication timestamp
153
- * image: Article image URL (if available)
154
- """
155
- params = {"category": category}
156
- if min_id > 0:
157
- params["minId"] = str(min_id)
158
-
159
- result = make_finnhub_request("news", params)
160
-
161
- if isinstance(result, dict) and "error" in result:
162
- return {
163
- "error": result["error"],
164
- "category": category
165
- }
166
-
167
- if not result or len(result) == 0:
168
- return {
169
- "category": category,
170
- "total_articles": 0,
171
- "articles": [],
172
- "message": "No news articles found for this category"
173
- }
174
-
175
- # Format the news articles
176
- articles = []
177
- for article in result[:10]: # Limit to 10 articles # pyright: ignore[reportArgumentType]
178
- articles.append({
179
- "id": article.get('id'),
180
- "headline": article.get('headline', 'No headline'),
181
- "summary": article.get('summary', ''),
182
- "source": article.get('source', 'Unknown'),
183
- "url": article.get('url'),
184
- "published": datetime.fromtimestamp(article.get('datetime', 0)).strftime('%Y-%m-%d %H:%M:%S') if article.get('datetime') else None,
185
- "image": article.get('image')
186
- })
187
-
188
- return {
189
- "category": category,
190
- "total_articles": len(articles),
191
- "articles": articles
192
- }
193
-
194
-
195
- @mcp.tool()
196
- def get_company_news(symbol: str, from_date: Optional[str] = None, to_date: Optional[str] = None) -> dict:
197
- """
198
- Get latest news for a specific company by stock symbol. This endpoint provides company-specific
199
- news, press releases, and announcements. Only available for North American companies.
200
-
201
- When to use:
202
- - User asks about a specific company's news (e.g., "Apple news", "Tesla updates")
203
- - Need company-specific announcements or press releases
204
- - User mentions "[company name] news", "recent [company] developments"
205
- - Want to filter news by date range
206
-
207
- Date range tips:
208
- - Default: Last 7 days if no dates specified
209
- - Can go back up to several years
210
- - Use YYYY-MM-DD format (e.g., "2024-01-01")
211
-
212
- Examples:
213
- - "What's the latest Apple news?" → get_company_news(symbol="AAPL")
214
- - "Tesla news from last month" → get_company_news(symbol="TSLA", from_date="2024-10-01", to_date="2024-10-31")
215
- - "Microsoft announcements this week" → get_company_news(symbol="MSFT")
216
- - "Show me Amazon news from January 2024" → get_company_news(symbol="AMZN", from_date="2024-01-01", to_date="2024-01-31")
217
-
218
- Args:
219
- symbol: Company stock ticker symbol (e.g., 'AAPL', 'MSFT', 'TSLA', 'GOOGL')
220
- Must be a North American (US/Canada) listed company
221
- from_date: Start date in YYYY-MM-DD format (default: 7 days ago)
222
- to_date: End date in YYYY-MM-DD format (default: today)
223
-
224
- Returns:
225
- dict: Company news data containing:
226
- - symbol: Company stock symbol
227
- - from_date: Start date of news range
228
- - to_date: End date of news range
229
- - total_articles: Total number of articles returned
230
- - articles: List of news articles (max 10), each with:
231
- * headline: News headline
232
- * summary: Brief summary
233
- * source: News source
234
- * url: Link to full article
235
- * published: Publication timestamp
236
- * related: Related stock symbols (if any)
237
- """
238
- # Set default date range if not provided
239
- if not to_date:
240
- to_date = datetime.now().strftime('%Y-%m-%d')
241
-
242
- if not from_date:
243
- from_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
244
-
245
- params = {
246
- "symbol": symbol.upper(),
247
- "from": from_date,
248
- "to": to_date
249
- }
250
-
251
- result = make_finnhub_request("company-news", params)
252
-
253
- if isinstance(result, dict) and "error" in result:
254
- return {
255
- "error": result["error"],
256
- "symbol": symbol.upper(),
257
- "from_date": from_date,
258
- "to_date": to_date
259
- }
260
-
261
- if not result or len(result) == 0:
262
- # Provide helpful suggestions based on the symbol
263
- suggestion = "Try expanding the date range or check if the symbol is correct."
264
- if symbol.upper() in ["BABA", "BIDU", "JD", "PDD", "NIO"]:
265
- suggestion = "Note: Chinese ADRs may have limited news coverage. Try US companies like AAPL, MSFT, TSLA, or GOOGL for better results."
266
-
267
- return {
268
- "symbol": symbol.upper(),
269
- "from_date": from_date,
270
- "to_date": to_date,
271
- "total_articles": 0,
272
- "articles": [],
273
- "message": f"No news found for {symbol.upper()} between {from_date} and {to_date}.",
274
- "suggestion": suggestion,
275
- "note": "Company news is only available for North American companies. Some companies may have limited news coverage during certain periods."
276
- }
277
-
278
- # Format the news articles
279
- articles = []
280
- for article in result[:10]: # Limit to 10 articles # pyright: ignore[reportArgumentType]
281
- articles.append({
282
- "headline": article.get('headline', 'No headline'),
283
- "summary": article.get('summary', ''),
284
- "source": article.get('source', 'Unknown'),
285
- "url": article.get('url'),
286
- "published": datetime.fromtimestamp(article.get('datetime', 0)).strftime('%Y-%m-%d %H:%M:%S') if article.get('datetime') else None,
287
- "related": article.get('related', '')
288
- })
289
-
290
- return {
291
- "symbol": symbol.upper(),
292
- "from_date": from_date,
293
- "to_date": to_date,
294
- "total_articles": len(articles),
295
- "articles": articles
296
- }
297
-
298
-
299
- if __name__ == "__main__":
300
- # Check for API key
301
- api_key = get_api_key()
302
- if not api_key:
303
- print("❌ Error: FINNHUB_API_KEY environment variable is not set")
304
- print("Please set your Finnhub API key:")
305
- print(" export FINNHUB_API_KEY='your_api_key_here'")
306
- exit(1)
307
-
308
- print("✅ API Key loaded from environment variable")
309
- print("▶️ Starting Finnhub MCP Server (JSON-RPC over SSE)...")
310
- print("📡 Server name: finnhub-market-info")
311
- print("🔧 Protocol: Model Context Protocol (MCP)")
312
- print("🌐 Transport: SSE (Server-Sent Events)")
313
- print("")
314
- print("Available tools:")
315
- print(" • get_quote(symbol)")
316
- print(" • get_market_news(category, min_id)")
317
- print(" • get_company_news(symbol, from_date, to_date)")
318
- print("")
319
-
320
- # Run the MCP server with SSE transport
321
- mcp.run(transport="sse")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
service/report_service.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Report Service - 财务报告数据服务
3
+ 集成 EasyFinancialAgent 的 MCP 工具,提供完整的数据查询方法
4
+ """
5
+
6
+ from EasyFinancialAgent.chat_direct import (
7
+ search_company_direct,
8
+ get_company_info_direct,
9
+ extract_financial_metrics_direct,
10
+ get_company_filings_direct,
11
+ get_latest_financial_data_direct,
12
+ query_company_direct
13
+ )
14
+ from datetime import datetime
15
+ from typing import Optional
16
+
17
+
18
+ def get_report_data(symbol_or_name: str, years: int = 5):
19
+ """
20
+ 获取公司完整财务报告数据
21
+
22
+ Args:
23
+ symbol_or_name: 公司代码或名称 (e.g., "Apple", "AAPL", "0000320193")
24
+ years: 财务年限 (默认 5 年)
25
+
26
+ Returns:
27
+ 完整的报告数据字典,包含公司信息、财务指标、最新数据等
28
+
29
+ Example:
30
+ report = get_report_data("Apple", years=5)
31
+ """
32
+ result = {
33
+ "timestamp": datetime.now().isoformat(),
34
+ "query_input": symbol_or_name,
35
+ "status": "success",
36
+ "data": {
37
+ "company_search": None,
38
+ "company_info": None,
39
+ "latest_financial": None,
40
+ "financial_metrics": None,
41
+ "filings": None
42
+ },
43
+ "errors": []
44
+ }
45
+
46
+ try:
47
+ # 1. 搜索公司
48
+ print(f"[Report Service] 搜索公司: {symbol_or_name}")
49
+ search_result = search_company_direct(symbol_or_name)
50
+ print(f"[Report Service] 搜索结果: {search_result}")
51
+ if "error" in search_result:
52
+ result["errors"].append(f"Search failed: {search_result['error']}")
53
+ result["status"] = "error"
54
+ return result
55
+
56
+ result["data"]["company_search"] = search_result
57
+
58
+ # 从搜索结果提取 CIK
59
+ cik = None
60
+ if isinstance(search_result, dict):
61
+ cik = search_result.get("cik")
62
+ elif isinstance(search_result, (list, tuple)) and len(search_result) > 0:
63
+ try:
64
+ first = search_result[0] if isinstance(search_result, (list, tuple)) else None
65
+ if isinstance(first, dict):
66
+ cik = first.get("cik")
67
+ except (IndexError, TypeError):
68
+ pass
69
+
70
+ if not cik:
71
+ result["errors"].append("Could not extract CIK from search result")
72
+ result["status"] = "error"
73
+ return result
74
+
75
+ print(f"[Report Service] 找到公司 CIK: {cik}")
76
+
77
+ # 2. 获取公司详细信息
78
+ # print(f"[Report Service] 获取公司详细信息")
79
+ # company_info = get_company_info_direct(cik)
80
+ # if "error" not in company_info:
81
+ # result["data"]["company_info"] = company_info
82
+ # else:
83
+ # result["errors"].append(f"Failed to get company info: {company_info.get('error')}")
84
+
85
+ # 3. 获取最新财务数据
86
+ # print(f"[Report Service] 获取最新财务数据")
87
+ # latest_data = get_latest_financial_data_direct(cik)
88
+ # if "error" not in latest_data:
89
+ # result["data"]["latest_financial"] = latest_data
90
+ # else:
91
+ # result["errors"].append(f"Failed to get latest financial data: {latest_data.get('error')}")
92
+
93
+ # 4. 获取多年财务指标
94
+ print(f"[Report Service] 获取 {years} 年财务指标")
95
+ metrics = extract_financial_metrics_direct(cik, years=years)
96
+ if "error" not in metrics:
97
+ result["data"]["financial_metrics"] = metrics
98
+ else:
99
+ result["errors"].append(f"Failed to get financial metrics: {metrics.get('error')}")
100
+
101
+ # 5. 获取公司文件列表
102
+ # print(f"[Report Service] 获取公司 SEC 文件列表")
103
+ # filings = get_company_filings_direct(cik)
104
+ # if "error" not in filings:
105
+ # result["data"]["filings"] = filings
106
+ # else:
107
+ # result["errors"].append(f"Failed to get filings: {filings.get('error')}")
108
+
109
+ print(f"[Report Service] 报告数据获取完成")
110
+
111
+ except Exception as e:
112
+ result["status"] = "error"
113
+ result["errors"].append(f"Exception: {str(e)}")
114
+ import traceback
115
+ result["errors"].append(traceback.format_exc())
116
+
117
+ return result
118
+
119
+
120
+ def get_company_summary(symbol_or_name: str):
121
+ """
122
+ 获取公司摘要信息(轻量级查询)
123
+
124
+ Args:
125
+ symbol_or_name: 公司代码或名称
126
+
127
+ Returns:
128
+ 公司摘要数据字典
129
+
130
+ Example:
131
+ summary = get_company_summary("Tesla")
132
+ """
133
+ result = {
134
+ "timestamp": datetime.now().isoformat(),
135
+ "query_input": symbol_or_name,
136
+ "status": "success",
137
+ "data": {
138
+ "company_search": None,
139
+ "company_info": None,
140
+ "latest_financial": None
141
+ },
142
+ "errors": []
143
+ }
144
+
145
+ try:
146
+ # 搜索公司
147
+ search_result = search_company_direct(symbol_or_name)
148
+ if "error" in search_result:
149
+ result["errors"].append(f"Search failed: {search_result['error']}")
150
+ result["status"] = "error"
151
+ return result
152
+
153
+ result["data"]["company_search"] = search_result
154
+
155
+ # 提取 CIK
156
+ cik = None
157
+ if isinstance(search_result, dict):
158
+ cik = search_result.get("cik")
159
+ elif isinstance(search_result, (list, tuple)) and len(search_result) > 0:
160
+ try:
161
+ first = search_result[0] if isinstance(search_result, (list, tuple)) else None
162
+ if isinstance(first, dict):
163
+ cik = first.get("cik")
164
+ except (IndexError, TypeError):
165
+ pass
166
+
167
+ if not cik:
168
+ result["errors"].append("Could not extract CIK")
169
+ result["status"] = "error"
170
+ return result
171
+
172
+ # 获取公司信息
173
+ company_info = get_company_info_direct(cik)
174
+ if "error" not in company_info:
175
+ result["data"]["company_info"] = company_info
176
+
177
+ # 获取最新财务数据
178
+ latest_data = get_latest_financial_data_direct(cik)
179
+ if "error" not in latest_data:
180
+ result["data"]["latest_financial"] = latest_data
181
+
182
+ except Exception as e:
183
+ result["status"] = "error"
184
+ result["errors"].append(str(e))
185
+
186
+ return result
187
+
188
+
189
+ def get_financial_metrics(symbol_or_name: str, years: int = 5):
190
+ """
191
+ 获取财务指标趋势数据
192
+
193
+ Args:
194
+ symbol_or_name: 公司代码或名称
195
+ years: 年数(默认 5 年)
196
+
197
+ Returns:
198
+ 财务指标数据字典
199
+
200
+ Example:
201
+ metrics = get_financial_metrics("Microsoft", years=10)
202
+ """
203
+ result = {
204
+ "timestamp": datetime.now().isoformat(),
205
+ "query_input": symbol_or_name,
206
+ "years": years,
207
+ "status": "success",
208
+ "data": None,
209
+ "errors": []
210
+ }
211
+
212
+ try:
213
+ # 搜索公司
214
+ search_result = search_company_direct(symbol_or_name)
215
+ if "error" in search_result:
216
+ result["errors"].append(f"Search failed: {search_result['error']}")
217
+ result["status"] = "error"
218
+ return result
219
+
220
+ # 提取 CIK
221
+ cik = None
222
+ if isinstance(search_result, dict):
223
+ cik = search_result.get("cik")
224
+ elif isinstance(search_result, (list, tuple)) and len(search_result) > 0:
225
+ try:
226
+ first = search_result[0] if isinstance(search_result, (list, tuple)) else None
227
+ if isinstance(first, dict):
228
+ cik = first.get("cik")
229
+ except (IndexError, TypeError):
230
+ pass
231
+
232
+ if not cik:
233
+ result["errors"].append("Could not extract CIK")
234
+ result["status"] = "error"
235
+ return result
236
+
237
+ # 获取财务指标
238
+ metrics = extract_financial_metrics_direct(cik, years=years)
239
+ if "error" in metrics:
240
+ result["errors"].append(f"Failed to get metrics: {metrics['error']}")
241
+ result["status"] = "error"
242
+ else:
243
+ result["data"] = metrics
244
+
245
+ except Exception as e:
246
+ result["status"] = "error"
247
+ result["errors"].append(str(e))
248
+
249
+ return result
250
+
251
+
252
+ def get_service_health():
253
+ """
254
+ 检查财务数据服务健康状态
255
+
256
+ Returns:
257
+ 服务状态字典
258
+
259
+ Example:
260
+ health = get_service_health()
261
+ """
262
+ return {
263
+ "status": "ok",
264
+ "message": "Using direct MCP functions (no HTTP service required)"
265
+ }
266
+
267
+
268
+ def query_company_advanced(company_input: str, get_filings: bool = True, get_metrics: bool = True):
269
+ """
270
+ 综合查询公司信息 (直接调用 chat_direct 的高级方法)
271
+ 包括搜索、基本信息、文件列表和财务指标
272
+
273
+ Args:
274
+ company_input: 公司名称或代码
275
+ get_filings: 是否获取文件列表
276
+ get_metrics: 是否获取财务指标
277
+
278
+ Returns:
279
+ 综合结果字典,包含 company_search, company_info, filings, metrics
280
+
281
+ Example:
282
+ result = query_company_advanced("Apple", get_filings=True, get_metrics=True)
283
+ """
284
+ prompt = f"""
285
+ Role:
286
+
287
+ You are an expert investment advisor who provides data-driven recommendations based on a company’s financials, news, and market data.
288
+
289
+ Task:
290
+
291
+ Analyze the following for {company_input}:
292
+
293
+ Financial metrics – revenue, profit, debt, cash flow, etc.
294
+ Recent news – assess risks and opportunities.
295
+ Stock data – price trend, volume, etc.
296
+ Then provide clear, objective investment guidance in this format:
297
+
298
+ Investment Recommendation:
299
+
300
+ Recommendation: [Buy / Hold / Avoid]
301
+ Entry & Exit Price: [Specify]
302
+ Stop-Loss & Take-Profit Levels: [Specify]
303
+ Holding Horizon: Short-term (<1 month) or Long-term (>1 month)
304
+ Rules:
305
+ Base all advice strictly on factual data—no speculation.
306
+ Be concise, professional, and actionable.
307
+ Always include a risk disclaimer acknowledging market uncertainty.
308
+ Output must be in English only.
309
+
310
+ """
311
+ return query_company_direct(prompt, get_filings=get_filings, get_metrics=get_metrics)