mseep-a-share-mcp 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,126 @@
1
+ # Defines the abstract interface for financial data sources
2
+ from abc import ABC, abstractmethod
3
+ import pandas as pd
4
+ from typing import Optional, List
5
+
6
+ class DataSourceError(Exception):
7
+ """Base exception for data source errors."""
8
+ pass
9
+
10
+
11
+ class LoginError(DataSourceError):
12
+ """Exception raised for login failures to the data source."""
13
+ pass
14
+
15
+
16
+ class NoDataFoundError(DataSourceError):
17
+ """Exception raised when no data is found for the given query."""
18
+ pass
19
+
20
+
21
+ class FinancialDataSource(ABC):
22
+ """
23
+ Abstract base class defining the interface for financial data sources.
24
+ Implementations of this class provide access to specific financial data APIs
25
+ (e.g., Baostock, Akshare).
26
+ """
27
+
28
+ @abstractmethod
29
+ def get_historical_k_data(
30
+ self,
31
+ code: str,
32
+ start_date: str,
33
+ end_date: str,
34
+ frequency: str = "d",
35
+ adjust_flag: str = "3",
36
+ fields: Optional[List[str]] = None,
37
+ ) -> pd.DataFrame:
38
+ """
39
+ Fetches historical K-line (OHLCV) data for a given stock code.
40
+
41
+ Args:
42
+ code: The stock code (e.g., 'sh.600000', 'sz.000001').
43
+ start_date: Start date in 'YYYY-MM-DD' format.
44
+ end_date: End date in 'YYYY-MM-DD' format.
45
+ frequency: Data frequency. Common values depend on the underlying
46
+ source (e.g., 'd' for daily, 'w' for weekly, 'm' for monthly,
47
+ '5', '15', '30', '60' for minutes). Defaults to 'd'.
48
+ adjust_flag: Adjustment flag for historical data. Common values
49
+ depend on the source (e.g., '1' for forward adjusted,
50
+ '2' for backward adjusted, '3' for non-adjusted).
51
+ Defaults to '3'.
52
+ fields: Optional list of specific fields to retrieve. If None,
53
+ retrieves default fields defined by the implementation.
54
+
55
+ Returns:
56
+ A pandas DataFrame containing the historical K-line data, with
57
+ columns corresponding to the requested fields.
58
+
59
+ Raises:
60
+ LoginError: If login to the data source fails.
61
+ NoDataFoundError: If no data is found for the query.
62
+ DataSourceError: For other data source related errors.
63
+ ValueError: If input parameters are invalid.
64
+ """
65
+ pass
66
+
67
+ @abstractmethod
68
+ def get_stock_basic_info(self, code: str) -> pd.DataFrame:
69
+ """
70
+ Fetches basic information for a given stock code.
71
+
72
+ Args:
73
+ code: The stock code (e.g., 'sh.600000', 'sz.000001').
74
+
75
+ Returns:
76
+ A pandas DataFrame containing the basic stock information.
77
+ The structure and columns depend on the underlying data source.
78
+ Typically contains info like name, industry, listing date, etc.
79
+
80
+ Raises:
81
+ LoginError: If login to the data source fails.
82
+ NoDataFoundError: If no data is found for the query.
83
+ DataSourceError: For other data source related errors.
84
+ ValueError: If the input code is invalid.
85
+ """
86
+ pass
87
+
88
+ @abstractmethod
89
+ def get_trade_dates(self, start_date: Optional[str] = None, end_date: Optional[str] = None) -> pd.DataFrame:
90
+ """Fetches trading dates information within a range."""
91
+ pass
92
+
93
+ @abstractmethod
94
+ def get_all_stock(self, date: Optional[str] = None) -> pd.DataFrame:
95
+ """Fetches list of all stocks and their trading status on a given date."""
96
+ pass
97
+
98
+ @abstractmethod
99
+ def get_deposit_rate_data(self, start_date: Optional[str] = None, end_date: Optional[str] = None) -> pd.DataFrame:
100
+ """Fetches benchmark deposit rates."""
101
+ pass
102
+
103
+ @abstractmethod
104
+ def get_loan_rate_data(self, start_date: Optional[str] = None, end_date: Optional[str] = None) -> pd.DataFrame:
105
+ """Fetches benchmark loan rates."""
106
+ pass
107
+
108
+ @abstractmethod
109
+ def get_required_reserve_ratio_data(self, start_date: Optional[str] = None, end_date: Optional[str] = None, year_type: str = '0') -> pd.DataFrame:
110
+ """Fetches required reserve ratio data."""
111
+ pass
112
+
113
+ @abstractmethod
114
+ def get_money_supply_data_month(self, start_date: Optional[str] = None, end_date: Optional[str] = None) -> pd.DataFrame:
115
+ """Fetches monthly money supply data (M0, M1, M2)."""
116
+ pass
117
+
118
+ @abstractmethod
119
+ def get_money_supply_data_year(self, start_date: Optional[str] = None, end_date: Optional[str] = None) -> pd.DataFrame:
120
+ """Fetches yearly money supply data (M0, M1, M2 - year end balance)."""
121
+ pass
122
+
123
+ @abstractmethod
124
+ def get_shibor_data(self, start_date: Optional[str] = None, end_date: Optional[str] = None) -> pd.DataFrame:
125
+ """Fetches SHIBOR (Shanghai Interbank Offered Rate) data."""
126
+ pass
formatting/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # Initialization file for formatting package
@@ -0,0 +1,65 @@
1
+ """
2
+ Markdown formatting utilities for A-Share MCP Server.
3
+ """
4
+ import pandas as pd
5
+ import logging
6
+ from datetime import datetime, timedelta
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ # Configuration
11
+ # Common number of trading days per year. Max rows to display in Markdown output
12
+ MAX_MARKDOWN_ROWS = 250
13
+
14
+
15
+ def format_df_to_markdown(df: pd.DataFrame, max_rows: int = None) -> str:
16
+ """Formats a Pandas DataFrame to a Markdown string with row truncation.
17
+
18
+ Args:
19
+ df: The DataFrame to format
20
+ max_rows: Maximum rows to include in output. Defaults to MAX_MARKDOWN_ROWS if None.
21
+
22
+ Returns:
23
+ A markdown formatted string representation of the DataFrame
24
+ """
25
+ if df.empty:
26
+ logger.warning("Attempted to format an empty DataFrame to Markdown.")
27
+ return "(No data available to display)"
28
+
29
+ # Default max_rows to the configured limit if not provided
30
+ if max_rows is None:
31
+ max_rows = MAX_MARKDOWN_ROWS
32
+ logger.debug(f"max_rows defaulted to MAX_MARKDOWN_ROWS: {max_rows}")
33
+
34
+ original_rows = df.shape[0] # Only need original_rows now
35
+ truncated = False
36
+ truncation_notes = []
37
+
38
+ # Determine the actual number of rows to display, capped by max_rows
39
+ rows_to_show = min(original_rows, max_rows)
40
+
41
+ # Always apply the row limit
42
+ df_display = df.head(rows_to_show)
43
+
44
+ # Check if actual row truncation occurred (only if original_rows > rows_to_show)
45
+ if original_rows > rows_to_show:
46
+ truncation_notes.append(
47
+ f"rows truncated to the limit of {rows_to_show} (from {original_rows})")
48
+ truncated = True
49
+
50
+ try:
51
+ markdown_table = df_display.to_markdown(index=False)
52
+ except Exception as e:
53
+ logger.error(
54
+ f"Error converting DataFrame to Markdown: {e}", exc_info=True)
55
+ return "Error: Could not format data into Markdown table."
56
+
57
+ if truncated:
58
+ # Note: 'truncated' is now only True if rows were truncated
59
+ notes = "; ".join(truncation_notes)
60
+ logger.debug(
61
+ f"Markdown table generated with truncation notes: {notes}")
62
+ return f"Note: Data truncated ({notes}).\n\n{markdown_table}"
63
+ else:
64
+ logger.debug("Markdown table generated without truncation.")
65
+ return markdown_table
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: mseep-a-share-mcp
3
+ Version: 0.1.1
4
+ Summary: Add your description here
5
+ Author-email: mseep <support@skydeck.ai>
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/plain
8
+ License-File: LICENSE
9
+ Requires-Dist: baostock>=0.8.9
10
+ Requires-Dist: httpx>=0.28.1
11
+ Requires-Dist: mcp[cli]>=1.2.0
12
+ Requires-Dist: pandas>=2.2.3
13
+ Requires-Dist: annotated-types>=0.7.0
14
+ Requires-Dist: anyio>=4.9.0
15
+ Requires-Dist: certifi>=2025.4.26
16
+ Requires-Dist: click>=8.1.8
17
+ Requires-Dist: colorama>=0.4.6
18
+ Requires-Dist: h11>=0.16.0
19
+ Requires-Dist: httpcore>=1.0.9
20
+ Requires-Dist: httpx-sse>=0.4.0
21
+ Requires-Dist: idna>=3.10
22
+ Requires-Dist: markdown-it-py>=3.0.0
23
+ Requires-Dist: mdurl>=0.1.2
24
+ Requires-Dist: numpy>=2.2.5
25
+ Requires-Dist: pydantic>=2.11.3
26
+ Requires-Dist: pydantic-core>=2.33.1
27
+ Requires-Dist: pydantic-settings>=2.9.1
28
+ Requires-Dist: pygments>=2.19.1
29
+ Requires-Dist: python-dateutil>=2.9.0
30
+ Requires-Dist: python-dotenv>=1.1.0
31
+ Requires-Dist: pytz>=2025.2
32
+ Requires-Dist: rich>=14.0.0
33
+ Requires-Dist: shellingham>=1.5.4
34
+ Requires-Dist: six>=1.17.0
35
+ Requires-Dist: sniffio>=1.3.1
36
+ Requires-Dist: sse-starlette>=2.3.3
37
+ Requires-Dist: starlette>=0.46.2
38
+ Requires-Dist: tabulate>=0.9.0
39
+ Requires-Dist: typer>=0.15.3
40
+ Requires-Dist: typing-extensions>=4.13.2
41
+ Requires-Dist: typing-inspection>=0.4.0
42
+ Requires-Dist: tzdata>=2025.2
43
+ Requires-Dist: uvicorn>=0.34.2
44
+ Dynamic: license-file
45
+
46
+ Package managed by MseeP.ai
@@ -0,0 +1,20 @@
1
+ __init__.py,sha256=AWl8YWEZfzOKWAeTp95xP8pKfd6qXK0gt45H0Cyp7-Q,39
2
+ baostock_data_source.py,sha256=8516O2cvDgqapdk-Ip0GLOIp1OG4BWy-j5DvtCvofOo,34530
3
+ data_source_interface.py,sha256=rzduDOcbwc2YIbaR4xfNHBzhjhpALShlU26YelCWgZg,4829
4
+ utils.py,sha256=kcIn7S6ValYMiclIMOP8d5yifNmIt8Y9YXZK3CtD-Hk,2359
5
+ formatting/__init__.py,sha256=Tyqwy1nJmF3OFPSCgxga44JwEhqenT9QB0j9orKSI1c,45
6
+ formatting/markdown_formatter.py,sha256=g9ppHZTetKpUxT3nCU3p4gJuA608rGHjz3IekiopWBk,2264
7
+ mseep_a_share_mcp-0.1.1.dist-info/licenses/LICENSE,sha256=3RDP-0gPJrroR5V1bjnuYWcdC3Evdo_LVAtm5xqFK9E,1062
8
+ tools/__init__.py,sha256=Zh7xp3j0jT32WPGAkCKAWkUFGK4mFkqcjOL6K9BWiSc,40
9
+ tools/analysis.py,sha256=i4jZPDQACcu1G_QQ_n8pzNUJGVZNUMDjkcsYYmEh7YA,8504
10
+ tools/base.py,sha256=CY7qEB66_nYE4n7gjte6B9IWthzNBniMuVyXKwqCFV8,6097
11
+ tools/date_utils.py,sha256=_LEyy2vBD-PqwninYwJoG6lId9MIKRBKqTecv17vAsY,7512
12
+ tools/financial_reports.py,sha256=clksV528suQZ9dSI4lHHhc6UNAyw4Ubar9150RAIqeg,7310
13
+ tools/indices.py,sha256=SeTew1eBb2k23UE8tGSUEU6BCOhYMljnbh0_G5KNK58,3543
14
+ tools/macroeconomic.py,sha256=wdTkXFzl14xAFddlPSS9CjuoKzpIx0ppie0YfcK215s,5339
15
+ tools/market_overview.py,sha256=VGEyibX3V48pCdUiF-pbDWWaEZC8LGUs-oDHPG8hvU4,4085
16
+ tools/stock_market.py,sha256=fvMDpaYS_J8KnmjnK3tUlR2vHSO39sR7dRAmNHrTN6c,11138
17
+ mseep_a_share_mcp-0.1.1.dist-info/METADATA,sha256=8PoHx2OzC0w2cAFBcqPscaUCUQ_xAbPRd5qu3-jS-1o,1392
18
+ mseep_a_share_mcp-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ mseep_a_share_mcp-0.1.1.dist-info/top_level.txt,sha256=rhSJLqDXlTNnV3YJTEQtQyJOJGXap8iZrFmcJ0vfuFg,75
20
+ mseep_a_share_mcp-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Devin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ __init__
2
+ baostock_data_source
3
+ data_source_interface
4
+ formatting
5
+ tools
6
+ utils
tools/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # Initialization file for tools package
tools/analysis.py ADDED
@@ -0,0 +1,169 @@
1
+ """
2
+ Analysis tools for MCP server.
3
+ Contains tools for generating stock analysis reports.
4
+ """
5
+ import logging
6
+ from datetime import datetime, timedelta
7
+
8
+ from mcp.server.fastmcp import FastMCP
9
+ from src.data_source_interface import FinancialDataSource
10
+ from src.formatting.markdown_formatter import format_df_to_markdown
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def register_analysis_tools(app: FastMCP, active_data_source: FinancialDataSource):
16
+ """
17
+ Register analysis tools with the MCP app.
18
+
19
+ Args:
20
+ app: The FastMCP app instance
21
+ active_data_source: The active financial data source
22
+ """
23
+
24
+ @app.tool()
25
+ def get_stock_analysis(code: str, analysis_type: str = "fundamental") -> str:
26
+ """
27
+ 提供基于数据的股票分析报告,而非投资建议。
28
+
29
+ Args:
30
+ code: 股票代码,如'sh.600000'
31
+ analysis_type: 分析类型,可选'fundamental'(基本面)、'technical'(技术面)或'comprehensive'(综合)
32
+
33
+ Returns:
34
+ 数据驱动的分析报告,包含关键财务指标、历史表现和同行业比较
35
+ """
36
+ logger.info(
37
+ f"Tool 'get_stock_analysis' called for {code}, type={analysis_type}")
38
+
39
+ # 收集多个维度的实际数据
40
+ try:
41
+ # 获取基本信息
42
+ basic_info = active_data_source.get_stock_basic_info(code=code)
43
+
44
+ # 根据分析类型获取不同数据
45
+ if analysis_type in ["fundamental", "comprehensive"]:
46
+ # 获取最近一个季度财务数据
47
+ recent_year = datetime.now().strftime("%Y")
48
+ recent_quarter = (datetime.now().month - 1) // 3 + 1
49
+ if recent_quarter < 1: # 处理年初可能出现的边界情况
50
+ recent_year = str(int(recent_year) - 1)
51
+ recent_quarter = 4
52
+
53
+ profit_data = active_data_source.get_profit_data(
54
+ code=code, year=recent_year, quarter=recent_quarter)
55
+ growth_data = active_data_source.get_growth_data(
56
+ code=code, year=recent_year, quarter=recent_quarter)
57
+ balance_data = active_data_source.get_balance_data(
58
+ code=code, year=recent_year, quarter=recent_quarter)
59
+ dupont_data = active_data_source.get_dupont_data(
60
+ code=code, year=recent_year, quarter=recent_quarter)
61
+
62
+ if analysis_type in ["technical", "comprehensive"]:
63
+ # 获取历史价格
64
+ end_date = datetime.now().strftime("%Y-%m-%d")
65
+ start_date = (datetime.now() - timedelta(days=180)
66
+ ).strftime("%Y-%m-%d")
67
+ price_data = active_data_source.get_historical_k_data(
68
+ code=code, start_date=start_date, end_date=end_date
69
+ )
70
+
71
+ # 构建客观的数据分析报告
72
+ report = f"# {basic_info['code_name'].values[0] if not basic_info.empty else code} 数据分析报告\n\n"
73
+ report += "## 免责声明\n本报告基于公开数据生成,仅供参考,不构成投资建议。投资决策需基于个人风险承受能力和研究。\n\n"
74
+
75
+ # 添加行业信息
76
+ if not basic_info.empty:
77
+ report += f"## 公司基本信息\n"
78
+ report += f"- 股票代码: {code}\n"
79
+ report += f"- 股票名称: {basic_info['code_name'].values[0]}\n"
80
+ report += f"- 所属行业: {basic_info['industry'].values[0] if 'industry' in basic_info.columns else '未知'}\n"
81
+ report += f"- 上市日期: {basic_info['ipoDate'].values[0] if 'ipoDate' in basic_info.columns else '未知'}\n\n"
82
+
83
+ # 添加基本面分析
84
+ if analysis_type in ["fundamental", "comprehensive"] and not profit_data.empty:
85
+ report += f"## 基本面指标分析 ({recent_year}年第{recent_quarter}季度)\n\n"
86
+
87
+ # 盈利能力
88
+ report += "### 盈利能力指标\n"
89
+ if not profit_data.empty and 'roeAvg' in profit_data.columns:
90
+ roe = profit_data['roeAvg'].values[0]
91
+ report += f"- ROE(净资产收益率): {roe}%\n"
92
+ if not profit_data.empty and 'npMargin' in profit_data.columns:
93
+ npm = profit_data['npMargin'].values[0]
94
+ report += f"- 销售净利率: {npm}%\n"
95
+
96
+ # 成长能力
97
+ if not growth_data.empty:
98
+ report += "\n### 成长能力指标\n"
99
+ if 'YOYEquity' in growth_data.columns:
100
+ equity_growth = growth_data['YOYEquity'].values[0]
101
+ report += f"- 净资产同比增长: {equity_growth}%\n"
102
+ if 'YOYAsset' in growth_data.columns:
103
+ asset_growth = growth_data['YOYAsset'].values[0]
104
+ report += f"- 总资产同比增长: {asset_growth}%\n"
105
+ if 'YOYNI' in growth_data.columns:
106
+ ni_growth = growth_data['YOYNI'].values[0]
107
+ report += f"- 净利润同比增长: {ni_growth}%\n"
108
+
109
+ # 偿债能力
110
+ if not balance_data.empty:
111
+ report += "\n### 偿债能力指标\n"
112
+ if 'currentRatio' in balance_data.columns:
113
+ current_ratio = balance_data['currentRatio'].values[0]
114
+ report += f"- 流动比率: {current_ratio}\n"
115
+ if 'assetLiabRatio' in balance_data.columns:
116
+ debt_ratio = balance_data['assetLiabRatio'].values[0]
117
+ report += f"- 资产负债率: {debt_ratio}%\n"
118
+
119
+ # 添加技术面分析
120
+ if analysis_type in ["technical", "comprehensive"] and not price_data.empty:
121
+ report += "## 技术面分析\n\n"
122
+
123
+ # 计算简单的技术指标
124
+ # 假设price_data已经按日期排序
125
+ if 'close' in price_data.columns and len(price_data) > 1:
126
+ latest_price = price_data['close'].iloc[-1]
127
+ start_price = price_data['close'].iloc[0]
128
+ price_change = (
129
+ (float(latest_price) / float(start_price)) - 1) * 100
130
+
131
+ report += f"- 最新收盘价: {latest_price}\n"
132
+ report += f"- 6个月价格变动: {price_change:.2f}%\n"
133
+
134
+ # 计算简单的均线
135
+ if len(price_data) >= 20:
136
+ ma20 = price_data['close'].astype(
137
+ float).tail(20).mean()
138
+ report += f"- 20日均价: {ma20:.2f}\n"
139
+ if float(latest_price) > ma20:
140
+ report += f" (当前价格高于20日均线 {((float(latest_price)/ma20)-1)*100:.2f}%)\n"
141
+ else:
142
+ report += f" (当前价格低于20日均线 {((ma20/float(latest_price))-1)*100:.2f}%)\n"
143
+
144
+ # 添加行业比较分析
145
+ try:
146
+ if not basic_info.empty and 'industry' in basic_info.columns:
147
+ industry = basic_info['industry'].values[0]
148
+ industry_stocks = active_data_source.get_stock_industry(
149
+ date=None)
150
+ if not industry_stocks.empty:
151
+ same_industry = industry_stocks[industry_stocks['industry'] == industry]
152
+ report += f"\n## 行业比较 ({industry})\n"
153
+ report += f"- 同行业股票数量: {len(same_industry)}\n"
154
+
155
+ # 这里可以添加更多行业比较数据
156
+ except Exception as e:
157
+ logger.warning(f"获取行业比较数据失败: {e}")
158
+
159
+ report += "\n## 数据解读建议\n"
160
+ report += "- 以上数据仅供参考,建议结合公司公告、行业趋势和宏观环境进行综合分析\n"
161
+ report += "- 个股表现受多种因素影响,历史数据不代表未来表现\n"
162
+ report += "- 投资决策应基于个人风险承受能力和投资目标\n"
163
+
164
+ logger.info(f"成功生成{code}的分析报告")
165
+ return report
166
+
167
+ except Exception as e:
168
+ logger.exception(f"分析生成失败 for {code}: {e}")
169
+ return f"分析生成失败: {e}"
tools/base.py ADDED
@@ -0,0 +1,161 @@
1
+ """
2
+ Base utilities for MCP tools.
3
+ Contains shared helper functions for calling data sources.
4
+ """
5
+ import logging
6
+ from typing import Callable, Optional
7
+ import pandas as pd
8
+
9
+ from src.formatting.markdown_formatter import format_df_to_markdown
10
+ from src.data_source_interface import NoDataFoundError, LoginError, DataSourceError
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def call_financial_data_tool(
16
+ tool_name: str,
17
+ # Pass the bound method like active_data_source.get_profit_data
18
+ data_source_method: Callable,
19
+ data_type_name: str,
20
+ code: str,
21
+ year: str,
22
+ quarter: int
23
+ ) -> str:
24
+ """
25
+ Helper function to reduce repetition for financial data tools
26
+
27
+ Args:
28
+ tool_name: Name of the tool for logging
29
+ data_source_method: Method to call on the data source
30
+ data_type_name: Type of financial data (for logging)
31
+ code: Stock code
32
+ year: Year to query
33
+ quarter: Quarter to query
34
+
35
+ Returns:
36
+ Markdown formatted string with results or error message
37
+ """
38
+ logger.info(f"Tool '{tool_name}' called for {code}, {year}Q{quarter}")
39
+ try:
40
+ # Basic validation
41
+ if not year.isdigit() or len(year) != 4:
42
+ logger.warning(f"Invalid year format requested: {year}")
43
+ return f"Error: Invalid year '{year}'. Please provide a 4-digit year."
44
+ if not 1 <= quarter <= 4:
45
+ logger.warning(f"Invalid quarter requested: {quarter}")
46
+ return f"Error: Invalid quarter '{quarter}'. Must be between 1 and 4."
47
+
48
+ # Call the appropriate method on the already instantiated active_data_source
49
+ df = data_source_method(code=code, year=year, quarter=quarter)
50
+ logger.info(
51
+ f"Successfully retrieved {data_type_name} data for {code}, {year}Q{quarter}.")
52
+ # Use smaller limits for financial tables?
53
+ return format_df_to_markdown(df)
54
+
55
+ except NoDataFoundError as e:
56
+ logger.warning(f"NoDataFoundError for {code}, {year}Q{quarter}: {e}")
57
+ return f"Error: {e}"
58
+ except LoginError as e:
59
+ logger.error(f"LoginError for {code}: {e}")
60
+ return f"Error: Could not connect to data source. {e}"
61
+ except DataSourceError as e:
62
+ logger.error(f"DataSourceError for {code}: {e}")
63
+ return f"Error: An error occurred while fetching data. {e}"
64
+ except ValueError as e:
65
+ logger.warning(f"ValueError processing request for {code}: {e}")
66
+ return f"Error: Invalid input parameter. {e}"
67
+ except Exception as e:
68
+ logger.exception(
69
+ f"Unexpected Exception processing {tool_name} for {code}: {e}")
70
+ return f"Error: An unexpected error occurred: {e}"
71
+
72
+
73
+ def call_macro_data_tool(
74
+ tool_name: str,
75
+ data_source_method: Callable,
76
+ data_type_name: str,
77
+ start_date: Optional[str] = None,
78
+ end_date: Optional[str] = None,
79
+ **kwargs # For extra params like year_type
80
+ ) -> str:
81
+ """
82
+ Helper function for macroeconomic data tools
83
+
84
+ Args:
85
+ tool_name: Name of the tool for logging
86
+ data_source_method: Method to call on the data source
87
+ data_type_name: Type of data (for logging)
88
+ start_date: Optional start date
89
+ end_date: Optional end date
90
+ **kwargs: Additional keyword arguments to pass to data_source_method
91
+
92
+ Returns:
93
+ Markdown formatted string with results or error message
94
+ """
95
+ date_range_log = f"from {start_date or 'default'} to {end_date or 'default'}"
96
+ kwargs_log = f", extra_args={kwargs}" if kwargs else ""
97
+ logger.info(f"Tool '{tool_name}' called {date_range_log}{kwargs_log}")
98
+ try:
99
+ # Call the appropriate method on the active_data_source
100
+ df = data_source_method(start_date=start_date,
101
+ end_date=end_date, **kwargs)
102
+ logger.info(f"Successfully retrieved {data_type_name} data.")
103
+ return format_df_to_markdown(df)
104
+ except NoDataFoundError as e:
105
+ logger.warning(f"NoDataFoundError: {e}")
106
+ return f"Error: {e}"
107
+ except LoginError as e:
108
+ logger.error(f"LoginError: {e}")
109
+ return f"Error: Could not connect to data source. {e}"
110
+ except DataSourceError as e:
111
+ logger.error(f"DataSourceError: {e}")
112
+ return f"Error: An error occurred while fetching data. {e}"
113
+ except ValueError as e:
114
+ logger.warning(f"ValueError: {e}")
115
+ return f"Error: Invalid input parameter. {e}"
116
+ except Exception as e:
117
+ logger.exception(f"Unexpected Exception processing {tool_name}: {e}")
118
+ return f"Error: An unexpected error occurred: {e}"
119
+
120
+
121
+ def call_index_constituent_tool(
122
+ tool_name: str,
123
+ data_source_method: Callable,
124
+ index_name: str,
125
+ date: Optional[str] = None
126
+ ) -> str:
127
+ """
128
+ Helper function for index constituent tools
129
+
130
+ Args:
131
+ tool_name: Name of the tool for logging
132
+ data_source_method: Method to call on the data source
133
+ index_name: Name of the index (for logging)
134
+ date: Optional date to query
135
+
136
+ Returns:
137
+ Markdown formatted string with results or error message
138
+ """
139
+ log_msg = f"Tool '{tool_name}' called for date={date or 'latest'}"
140
+ logger.info(log_msg)
141
+ try:
142
+ # Add date validation if desired
143
+ df = data_source_method(date=date)
144
+ logger.info(
145
+ f"Successfully retrieved {index_name} constituents for {date or 'latest'}.")
146
+ return format_df_to_markdown(df)
147
+ except NoDataFoundError as e:
148
+ logger.warning(f"NoDataFoundError: {e}")
149
+ return f"Error: {e}"
150
+ except LoginError as e:
151
+ logger.error(f"LoginError: {e}")
152
+ return f"Error: Could not connect to data source. {e}"
153
+ except DataSourceError as e:
154
+ logger.error(f"DataSourceError: {e}")
155
+ return f"Error: An error occurred while fetching data. {e}"
156
+ except ValueError as e:
157
+ logger.warning(f"ValueError: {e}")
158
+ return f"Error: Invalid input parameter. {e}"
159
+ except Exception as e:
160
+ logger.exception(f"Unexpected Exception processing {tool_name}: {e}")
161
+ return f"Error: An unexpected error occurred: {e}"