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.
- __init__.py +1 -0
- baostock_data_source.py +689 -0
- data_source_interface.py +126 -0
- formatting/__init__.py +1 -0
- formatting/markdown_formatter.py +65 -0
- mseep_a_share_mcp-0.1.1.dist-info/METADATA +46 -0
- mseep_a_share_mcp-0.1.1.dist-info/RECORD +20 -0
- mseep_a_share_mcp-0.1.1.dist-info/WHEEL +5 -0
- mseep_a_share_mcp-0.1.1.dist-info/licenses/LICENSE +21 -0
- mseep_a_share_mcp-0.1.1.dist-info/top_level.txt +6 -0
- tools/__init__.py +1 -0
- tools/analysis.py +169 -0
- tools/base.py +161 -0
- tools/date_utils.py +180 -0
- tools/financial_reports.py +201 -0
- tools/indices.py +103 -0
- tools/macroeconomic.py +146 -0
- tools/market_overview.py +99 -0
- tools/stock_market.py +242 -0
- utils.py +69 -0
data_source_interface.py
ADDED
@@ -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,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.
|
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}"
|