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
baostock_data_source.py
ADDED
@@ -0,0 +1,689 @@
|
|
1
|
+
# Implementation of the FinancialDataSource interface using Baostock
|
2
|
+
import baostock as bs
|
3
|
+
import pandas as pd
|
4
|
+
from typing import List, Optional
|
5
|
+
import logging
|
6
|
+
from .data_source_interface import FinancialDataSource, DataSourceError, NoDataFoundError, LoginError
|
7
|
+
from .utils import baostock_login_context
|
8
|
+
|
9
|
+
# Get a logger instance for this module
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
DEFAULT_K_FIELDS = [
|
13
|
+
"date", "code", "open", "high", "low", "close", "preclose",
|
14
|
+
"volume", "amount", "adjustflag", "turn", "tradestatus",
|
15
|
+
"pctChg", "peTTM", "pbMRQ", "psTTM", "pcfNcfTTM", "isST"
|
16
|
+
]
|
17
|
+
|
18
|
+
DEFAULT_BASIC_FIELDS = [
|
19
|
+
"code", "tradeStatus", "code_name"
|
20
|
+
# Add more default fields as needed, e.g., "industry", "listingDate"
|
21
|
+
]
|
22
|
+
|
23
|
+
# Helper function to reduce repetition in financial data fetching
|
24
|
+
|
25
|
+
|
26
|
+
def _fetch_financial_data(
|
27
|
+
bs_query_func,
|
28
|
+
data_type_name: str,
|
29
|
+
code: str,
|
30
|
+
year: str,
|
31
|
+
quarter: int
|
32
|
+
) -> pd.DataFrame:
|
33
|
+
logger.info(
|
34
|
+
f"Fetching {data_type_name} data for {code}, year={year}, quarter={quarter}")
|
35
|
+
try:
|
36
|
+
with baostock_login_context():
|
37
|
+
# Assuming all these functions take code, year, quarter
|
38
|
+
rs = bs_query_func(code=code, year=year, quarter=quarter)
|
39
|
+
|
40
|
+
if rs.error_code != '0':
|
41
|
+
logger.error(
|
42
|
+
f"Baostock API error ({data_type_name}) for {code}: {rs.error_msg} (code: {rs.error_code})")
|
43
|
+
if "no record found" in rs.error_msg.lower() or rs.error_code == '10002':
|
44
|
+
raise NoDataFoundError(
|
45
|
+
f"No {data_type_name} data found for {code}, {year}Q{quarter}. Baostock msg: {rs.error_msg}")
|
46
|
+
else:
|
47
|
+
raise DataSourceError(
|
48
|
+
f"Baostock API error fetching {data_type_name} data: {rs.error_msg} (code: {rs.error_code})")
|
49
|
+
|
50
|
+
data_list = []
|
51
|
+
while rs.next():
|
52
|
+
data_list.append(rs.get_row_data())
|
53
|
+
|
54
|
+
if not data_list:
|
55
|
+
logger.warning(
|
56
|
+
f"No {data_type_name} data found for {code}, {year}Q{quarter} (empty result set from Baostock).")
|
57
|
+
raise NoDataFoundError(
|
58
|
+
f"No {data_type_name} data found for {code}, {year}Q{quarter} (empty result set).")
|
59
|
+
|
60
|
+
result_df = pd.DataFrame(data_list, columns=rs.fields)
|
61
|
+
logger.info(
|
62
|
+
f"Retrieved {len(result_df)} {data_type_name} records for {code}, {year}Q{quarter}.")
|
63
|
+
return result_df
|
64
|
+
|
65
|
+
except (LoginError, NoDataFoundError, DataSourceError, ValueError) as e:
|
66
|
+
logger.warning(
|
67
|
+
f"Caught known error fetching {data_type_name} data for {code}: {type(e).__name__}")
|
68
|
+
raise e
|
69
|
+
except Exception as e:
|
70
|
+
logger.exception(
|
71
|
+
f"Unexpected error fetching {data_type_name} data for {code}: {e}")
|
72
|
+
raise DataSourceError(
|
73
|
+
f"Unexpected error fetching {data_type_name} data for {code}: {e}")
|
74
|
+
|
75
|
+
# Helper function to reduce repetition for index constituent data fetching
|
76
|
+
|
77
|
+
|
78
|
+
def _fetch_index_constituent_data(
|
79
|
+
bs_query_func,
|
80
|
+
index_name: str,
|
81
|
+
date: Optional[str] = None
|
82
|
+
) -> pd.DataFrame:
|
83
|
+
logger.info(
|
84
|
+
f"Fetching {index_name} constituents for date={date or 'latest'}")
|
85
|
+
try:
|
86
|
+
with baostock_login_context():
|
87
|
+
# date is optional, defaults to latest
|
88
|
+
rs = bs_query_func(date=date)
|
89
|
+
|
90
|
+
if rs.error_code != '0':
|
91
|
+
logger.error(
|
92
|
+
f"Baostock API error ({index_name} Constituents) for date {date}: {rs.error_msg} (code: {rs.error_code})")
|
93
|
+
if "no record found" in rs.error_msg.lower() or rs.error_code == '10002':
|
94
|
+
raise NoDataFoundError(
|
95
|
+
f"No {index_name} constituent data found for date {date}. Baostock msg: {rs.error_msg}")
|
96
|
+
else:
|
97
|
+
raise DataSourceError(
|
98
|
+
f"Baostock API error fetching {index_name} constituents: {rs.error_msg} (code: {rs.error_code})")
|
99
|
+
|
100
|
+
data_list = []
|
101
|
+
while rs.next():
|
102
|
+
data_list.append(rs.get_row_data())
|
103
|
+
|
104
|
+
if not data_list:
|
105
|
+
logger.warning(
|
106
|
+
f"No {index_name} constituent data found for date {date} (empty result set).")
|
107
|
+
raise NoDataFoundError(
|
108
|
+
f"No {index_name} constituent data found for date {date} (empty result set).")
|
109
|
+
|
110
|
+
result_df = pd.DataFrame(data_list, columns=rs.fields)
|
111
|
+
logger.info(
|
112
|
+
f"Retrieved {len(result_df)} {index_name} constituents for date {date or 'latest'}.")
|
113
|
+
return result_df
|
114
|
+
|
115
|
+
except (LoginError, NoDataFoundError, DataSourceError, ValueError) as e:
|
116
|
+
logger.warning(
|
117
|
+
f"Caught known error fetching {index_name} constituents for date {date}: {type(e).__name__}")
|
118
|
+
raise e
|
119
|
+
except Exception as e:
|
120
|
+
logger.exception(
|
121
|
+
f"Unexpected error fetching {index_name} constituents for date {date}: {e}")
|
122
|
+
raise DataSourceError(
|
123
|
+
f"Unexpected error fetching {index_name} constituents for date {date}: {e}")
|
124
|
+
|
125
|
+
# Helper function to reduce repetition for macroeconomic data fetching
|
126
|
+
|
127
|
+
|
128
|
+
def _fetch_macro_data(
|
129
|
+
bs_query_func,
|
130
|
+
data_type_name: str,
|
131
|
+
start_date: Optional[str] = None,
|
132
|
+
end_date: Optional[str] = None,
|
133
|
+
**kwargs # For extra params like yearType
|
134
|
+
) -> pd.DataFrame:
|
135
|
+
date_range_log = f"from {start_date or 'default'} to {end_date or 'default'}"
|
136
|
+
kwargs_log = f", extra_args={kwargs}" if kwargs else ""
|
137
|
+
logger.info(f"Fetching {data_type_name} data {date_range_log}{kwargs_log}")
|
138
|
+
try:
|
139
|
+
with baostock_login_context():
|
140
|
+
rs = bs_query_func(start_date=start_date,
|
141
|
+
end_date=end_date, **kwargs)
|
142
|
+
|
143
|
+
if rs.error_code != '0':
|
144
|
+
logger.error(
|
145
|
+
f"Baostock API error ({data_type_name}): {rs.error_msg} (code: {rs.error_code})")
|
146
|
+
if "no record found" in rs.error_msg.lower() or rs.error_code == '10002':
|
147
|
+
raise NoDataFoundError(
|
148
|
+
f"No {data_type_name} data found for the specified criteria. Baostock msg: {rs.error_msg}")
|
149
|
+
else:
|
150
|
+
raise DataSourceError(
|
151
|
+
f"Baostock API error fetching {data_type_name} data: {rs.error_msg} (code: {rs.error_code})")
|
152
|
+
|
153
|
+
data_list = []
|
154
|
+
while rs.next():
|
155
|
+
data_list.append(rs.get_row_data())
|
156
|
+
|
157
|
+
if not data_list:
|
158
|
+
logger.warning(
|
159
|
+
f"No {data_type_name} data found for the specified criteria (empty result set).")
|
160
|
+
raise NoDataFoundError(
|
161
|
+
f"No {data_type_name} data found for the specified criteria (empty result set).")
|
162
|
+
|
163
|
+
result_df = pd.DataFrame(data_list, columns=rs.fields)
|
164
|
+
logger.info(
|
165
|
+
f"Retrieved {len(result_df)} {data_type_name} records.")
|
166
|
+
return result_df
|
167
|
+
|
168
|
+
except (LoginError, NoDataFoundError, DataSourceError, ValueError) as e:
|
169
|
+
logger.warning(
|
170
|
+
f"Caught known error fetching {data_type_name} data: {type(e).__name__}")
|
171
|
+
raise e
|
172
|
+
except Exception as e:
|
173
|
+
logger.exception(
|
174
|
+
f"Unexpected error fetching {data_type_name} data: {e}")
|
175
|
+
raise DataSourceError(
|
176
|
+
f"Unexpected error fetching {data_type_name} data: {e}")
|
177
|
+
|
178
|
+
|
179
|
+
class BaostockDataSource(FinancialDataSource):
|
180
|
+
"""
|
181
|
+
Concrete implementation of FinancialDataSource using the Baostock library.
|
182
|
+
"""
|
183
|
+
|
184
|
+
def _format_fields(self, fields: Optional[List[str]], default_fields: List[str]) -> str:
|
185
|
+
"""Formats the list of fields into a comma-separated string for Baostock."""
|
186
|
+
if fields is None or not fields:
|
187
|
+
logger.debug(
|
188
|
+
f"No specific fields requested, using defaults: {default_fields}")
|
189
|
+
return ",".join(default_fields)
|
190
|
+
# Basic validation: ensure requested fields are strings
|
191
|
+
if not all(isinstance(f, str) for f in fields):
|
192
|
+
raise ValueError("All items in the fields list must be strings.")
|
193
|
+
logger.debug(f"Using requested fields: {fields}")
|
194
|
+
return ",".join(fields)
|
195
|
+
|
196
|
+
def get_historical_k_data(
|
197
|
+
self,
|
198
|
+
code: str,
|
199
|
+
start_date: str,
|
200
|
+
end_date: str,
|
201
|
+
frequency: str = "d",
|
202
|
+
adjust_flag: str = "3",
|
203
|
+
fields: Optional[List[str]] = None,
|
204
|
+
) -> pd.DataFrame:
|
205
|
+
"""Fetches historical K-line data using Baostock."""
|
206
|
+
logger.info(
|
207
|
+
f"Fetching K-data for {code} ({start_date} to {end_date}), freq={frequency}, adjust={adjust_flag}")
|
208
|
+
try:
|
209
|
+
formatted_fields = self._format_fields(fields, DEFAULT_K_FIELDS)
|
210
|
+
logger.debug(
|
211
|
+
f"Requesting fields from Baostock: {formatted_fields}")
|
212
|
+
|
213
|
+
with baostock_login_context():
|
214
|
+
rs = bs.query_history_k_data_plus(
|
215
|
+
code,
|
216
|
+
formatted_fields,
|
217
|
+
start_date=start_date,
|
218
|
+
end_date=end_date,
|
219
|
+
frequency=frequency,
|
220
|
+
adjustflag=adjust_flag
|
221
|
+
)
|
222
|
+
|
223
|
+
if rs.error_code != '0':
|
224
|
+
logger.error(
|
225
|
+
f"Baostock API error (K-data) for {code}: {rs.error_msg} (code: {rs.error_code})")
|
226
|
+
# Check common error codes, e.g., for no data
|
227
|
+
if "no record found" in rs.error_msg.lower() or rs.error_code == '10002': # Example error code
|
228
|
+
raise NoDataFoundError(
|
229
|
+
f"No historical data found for {code} in the specified range. Baostock msg: {rs.error_msg}")
|
230
|
+
else:
|
231
|
+
raise DataSourceError(
|
232
|
+
f"Baostock API error fetching K-data: {rs.error_msg} (code: {rs.error_code})")
|
233
|
+
|
234
|
+
data_list = []
|
235
|
+
while rs.next():
|
236
|
+
data_list.append(rs.get_row_data())
|
237
|
+
|
238
|
+
if not data_list:
|
239
|
+
logger.warning(
|
240
|
+
f"No historical data found for {code} in range (empty result set from Baostock).")
|
241
|
+
raise NoDataFoundError(
|
242
|
+
f"No historical data found for {code} in the specified range (empty result set).")
|
243
|
+
|
244
|
+
# Crucial: Use rs.fields for column names
|
245
|
+
result_df = pd.DataFrame(data_list, columns=rs.fields)
|
246
|
+
logger.info(f"Retrieved {len(result_df)} records for {code}.")
|
247
|
+
return result_df
|
248
|
+
|
249
|
+
except (LoginError, NoDataFoundError, DataSourceError, ValueError) as e:
|
250
|
+
# Re-raise known errors
|
251
|
+
logger.warning(
|
252
|
+
f"Caught known error fetching K-data for {code}: {type(e).__name__}")
|
253
|
+
raise e
|
254
|
+
except Exception as e:
|
255
|
+
# Wrap unexpected errors
|
256
|
+
# Use logger.exception to include traceback
|
257
|
+
logger.exception(
|
258
|
+
f"Unexpected error fetching K-data for {code}: {e}")
|
259
|
+
raise DataSourceError(
|
260
|
+
f"Unexpected error fetching K-data for {code}: {e}")
|
261
|
+
|
262
|
+
def get_stock_basic_info(self, code: str, fields: Optional[List[str]] = None) -> pd.DataFrame:
|
263
|
+
"""Fetches basic stock information using Baostock."""
|
264
|
+
logger.info(f"Fetching basic info for {code}")
|
265
|
+
try:
|
266
|
+
# Note: query_stock_basic doesn't seem to have a fields parameter in docs,
|
267
|
+
# but we keep the signature consistent. It returns a fixed set.
|
268
|
+
# We will use the `fields` argument post-query to select columns if needed.
|
269
|
+
logger.debug(
|
270
|
+
f"Requesting basic info for {code}. Optional fields requested: {fields}")
|
271
|
+
|
272
|
+
with baostock_login_context():
|
273
|
+
# Example: Fetch basic info; adjust API call if needed based on baostock docs
|
274
|
+
# rs = bs.query_stock_basic(code=code, code_name=code_name) # If supporting name lookup
|
275
|
+
rs = bs.query_stock_basic(code=code)
|
276
|
+
|
277
|
+
if rs.error_code != '0':
|
278
|
+
logger.error(
|
279
|
+
f"Baostock API error (Basic Info) for {code}: {rs.error_msg} (code: {rs.error_code})")
|
280
|
+
if "no record found" in rs.error_msg.lower() or rs.error_code == '10002':
|
281
|
+
raise NoDataFoundError(
|
282
|
+
f"No basic info found for {code}. Baostock msg: {rs.error_msg}")
|
283
|
+
else:
|
284
|
+
raise DataSourceError(
|
285
|
+
f"Baostock API error fetching basic info: {rs.error_msg} (code: {rs.error_code})")
|
286
|
+
|
287
|
+
data_list = []
|
288
|
+
while rs.next():
|
289
|
+
data_list.append(rs.get_row_data())
|
290
|
+
|
291
|
+
if not data_list:
|
292
|
+
logger.warning(
|
293
|
+
f"No basic info found for {code} (empty result set from Baostock).")
|
294
|
+
raise NoDataFoundError(
|
295
|
+
f"No basic info found for {code} (empty result set).")
|
296
|
+
|
297
|
+
# Crucial: Use rs.fields for column names
|
298
|
+
result_df = pd.DataFrame(data_list, columns=rs.fields)
|
299
|
+
logger.info(
|
300
|
+
f"Retrieved basic info for {code}. Columns: {result_df.columns.tolist()}")
|
301
|
+
|
302
|
+
# Optional: Select subset of columns if `fields` argument was provided
|
303
|
+
if fields:
|
304
|
+
available_cols = [
|
305
|
+
col for col in fields if col in result_df.columns]
|
306
|
+
if not available_cols:
|
307
|
+
raise ValueError(
|
308
|
+
f"None of the requested fields {fields} are available in the basic info result.")
|
309
|
+
logger.debug(
|
310
|
+
f"Selecting columns: {available_cols} from basic info for {code}")
|
311
|
+
result_df = result_df[available_cols]
|
312
|
+
|
313
|
+
return result_df
|
314
|
+
|
315
|
+
except (LoginError, NoDataFoundError, DataSourceError, ValueError) as e:
|
316
|
+
logger.warning(
|
317
|
+
f"Caught known error fetching basic info for {code}: {type(e).__name__}")
|
318
|
+
raise e
|
319
|
+
except Exception as e:
|
320
|
+
logger.exception(
|
321
|
+
f"Unexpected error fetching basic info for {code}: {e}")
|
322
|
+
raise DataSourceError(
|
323
|
+
f"Unexpected error fetching basic info for {code}: {e}")
|
324
|
+
|
325
|
+
def get_dividend_data(self, code: str, year: str, year_type: str = "report") -> pd.DataFrame:
|
326
|
+
"""Fetches dividend information using Baostock."""
|
327
|
+
logger.info(
|
328
|
+
f"Fetching dividend data for {code}, year={year}, year_type={year_type}")
|
329
|
+
try:
|
330
|
+
with baostock_login_context():
|
331
|
+
rs = bs.query_dividend_data(
|
332
|
+
code=code, year=year, yearType=year_type)
|
333
|
+
|
334
|
+
if rs.error_code != '0':
|
335
|
+
logger.error(
|
336
|
+
f"Baostock API error (Dividend) for {code}: {rs.error_msg} (code: {rs.error_code})")
|
337
|
+
if "no record found" in rs.error_msg.lower() or rs.error_code == '10002':
|
338
|
+
raise NoDataFoundError(
|
339
|
+
f"No dividend data found for {code} and year {year}. Baostock msg: {rs.error_msg}")
|
340
|
+
else:
|
341
|
+
raise DataSourceError(
|
342
|
+
f"Baostock API error fetching dividend data: {rs.error_msg} (code: {rs.error_code})")
|
343
|
+
|
344
|
+
data_list = []
|
345
|
+
while rs.next():
|
346
|
+
data_list.append(rs.get_row_data())
|
347
|
+
|
348
|
+
if not data_list:
|
349
|
+
logger.warning(
|
350
|
+
f"No dividend data found for {code}, year {year} (empty result set from Baostock).")
|
351
|
+
raise NoDataFoundError(
|
352
|
+
f"No dividend data found for {code}, year {year} (empty result set).")
|
353
|
+
|
354
|
+
result_df = pd.DataFrame(data_list, columns=rs.fields)
|
355
|
+
logger.info(
|
356
|
+
f"Retrieved {len(result_df)} dividend records for {code}, year {year}.")
|
357
|
+
return result_df
|
358
|
+
|
359
|
+
except (LoginError, NoDataFoundError, DataSourceError, ValueError) as e:
|
360
|
+
logger.warning(
|
361
|
+
f"Caught known error fetching dividend data for {code}: {type(e).__name__}")
|
362
|
+
raise e
|
363
|
+
except Exception as e:
|
364
|
+
logger.exception(
|
365
|
+
f"Unexpected error fetching dividend data for {code}: {e}")
|
366
|
+
raise DataSourceError(
|
367
|
+
f"Unexpected error fetching dividend data for {code}: {e}")
|
368
|
+
|
369
|
+
def get_adjust_factor_data(self, code: str, start_date: str, end_date: str) -> pd.DataFrame:
|
370
|
+
"""Fetches adjustment factor data using Baostock."""
|
371
|
+
logger.info(
|
372
|
+
f"Fetching adjustment factor data for {code} ({start_date} to {end_date})")
|
373
|
+
try:
|
374
|
+
with baostock_login_context():
|
375
|
+
rs = bs.query_adjust_factor(
|
376
|
+
code=code, start_date=start_date, end_date=end_date)
|
377
|
+
|
378
|
+
if rs.error_code != '0':
|
379
|
+
logger.error(
|
380
|
+
f"Baostock API error (Adjust Factor) for {code}: {rs.error_msg} (code: {rs.error_code})")
|
381
|
+
if "no record found" in rs.error_msg.lower() or rs.error_code == '10002':
|
382
|
+
raise NoDataFoundError(
|
383
|
+
f"No adjustment factor data found for {code} in the specified range. Baostock msg: {rs.error_msg}")
|
384
|
+
else:
|
385
|
+
raise DataSourceError(
|
386
|
+
f"Baostock API error fetching adjust factor data: {rs.error_msg} (code: {rs.error_code})")
|
387
|
+
|
388
|
+
data_list = []
|
389
|
+
while rs.next():
|
390
|
+
data_list.append(rs.get_row_data())
|
391
|
+
|
392
|
+
if not data_list:
|
393
|
+
logger.warning(
|
394
|
+
f"No adjustment factor data found for {code} in range (empty result set from Baostock).")
|
395
|
+
raise NoDataFoundError(
|
396
|
+
f"No adjustment factor data found for {code} in the specified range (empty result set).")
|
397
|
+
|
398
|
+
result_df = pd.DataFrame(data_list, columns=rs.fields)
|
399
|
+
logger.info(
|
400
|
+
f"Retrieved {len(result_df)} adjustment factor records for {code}.")
|
401
|
+
return result_df
|
402
|
+
|
403
|
+
except (LoginError, NoDataFoundError, DataSourceError, ValueError) as e:
|
404
|
+
logger.warning(
|
405
|
+
f"Caught known error fetching adjust factor data for {code}: {type(e).__name__}")
|
406
|
+
raise e
|
407
|
+
except Exception as e:
|
408
|
+
logger.exception(
|
409
|
+
f"Unexpected error fetching adjust factor data for {code}: {e}")
|
410
|
+
raise DataSourceError(
|
411
|
+
f"Unexpected error fetching adjust factor data for {code}: {e}")
|
412
|
+
|
413
|
+
def get_profit_data(self, code: str, year: str, quarter: int) -> pd.DataFrame:
|
414
|
+
"""Fetches quarterly profitability data using Baostock."""
|
415
|
+
return _fetch_financial_data(bs.query_profit_data, "Profitability", code, year, quarter)
|
416
|
+
|
417
|
+
def get_operation_data(self, code: str, year: str, quarter: int) -> pd.DataFrame:
|
418
|
+
"""Fetches quarterly operation capability data using Baostock."""
|
419
|
+
return _fetch_financial_data(bs.query_operation_data, "Operation Capability", code, year, quarter)
|
420
|
+
|
421
|
+
def get_growth_data(self, code: str, year: str, quarter: int) -> pd.DataFrame:
|
422
|
+
"""Fetches quarterly growth capability data using Baostock."""
|
423
|
+
return _fetch_financial_data(bs.query_growth_data, "Growth Capability", code, year, quarter)
|
424
|
+
|
425
|
+
def get_balance_data(self, code: str, year: str, quarter: int) -> pd.DataFrame:
|
426
|
+
"""Fetches quarterly balance sheet data (solvency) using Baostock."""
|
427
|
+
return _fetch_financial_data(bs.query_balance_data, "Balance Sheet", code, year, quarter)
|
428
|
+
|
429
|
+
def get_cash_flow_data(self, code: str, year: str, quarter: int) -> pd.DataFrame:
|
430
|
+
"""Fetches quarterly cash flow data using Baostock."""
|
431
|
+
return _fetch_financial_data(bs.query_cash_flow_data, "Cash Flow", code, year, quarter)
|
432
|
+
|
433
|
+
def get_dupont_data(self, code: str, year: str, quarter: int) -> pd.DataFrame:
|
434
|
+
"""Fetches quarterly DuPont analysis data using Baostock."""
|
435
|
+
return _fetch_financial_data(bs.query_dupont_data, "DuPont Analysis", code, year, quarter)
|
436
|
+
|
437
|
+
def get_performance_express_report(self, code: str, start_date: str, end_date: str) -> pd.DataFrame:
|
438
|
+
"""Fetches performance express reports (业绩快报) using Baostock."""
|
439
|
+
logger.info(
|
440
|
+
f"Fetching Performance Express Report for {code} ({start_date} to {end_date})")
|
441
|
+
try:
|
442
|
+
with baostock_login_context():
|
443
|
+
rs = bs.query_performance_express_report(
|
444
|
+
code=code, start_date=start_date, end_date=end_date)
|
445
|
+
|
446
|
+
if rs.error_code != '0':
|
447
|
+
logger.error(
|
448
|
+
f"Baostock API error (Perf Express) for {code}: {rs.error_msg} (code: {rs.error_code})")
|
449
|
+
if "no record found" in rs.error_msg.lower() or rs.error_code == '10002':
|
450
|
+
raise NoDataFoundError(
|
451
|
+
f"No performance express report found for {code} in range {start_date}-{end_date}. Baostock msg: {rs.error_msg}")
|
452
|
+
else:
|
453
|
+
raise DataSourceError(
|
454
|
+
f"Baostock API error fetching performance express report: {rs.error_msg} (code: {rs.error_code})")
|
455
|
+
|
456
|
+
data_list = []
|
457
|
+
while rs.next():
|
458
|
+
data_list.append(rs.get_row_data())
|
459
|
+
|
460
|
+
if not data_list:
|
461
|
+
logger.warning(
|
462
|
+
f"No performance express report found for {code} in range {start_date}-{end_date} (empty result set).")
|
463
|
+
raise NoDataFoundError(
|
464
|
+
f"No performance express report found for {code} in range {start_date}-{end_date} (empty result set).")
|
465
|
+
|
466
|
+
result_df = pd.DataFrame(data_list, columns=rs.fields)
|
467
|
+
logger.info(
|
468
|
+
f"Retrieved {len(result_df)} performance express report records for {code}.")
|
469
|
+
return result_df
|
470
|
+
|
471
|
+
except (LoginError, NoDataFoundError, DataSourceError, ValueError) as e:
|
472
|
+
logger.warning(
|
473
|
+
f"Caught known error fetching performance express report for {code}: {type(e).__name__}")
|
474
|
+
raise e
|
475
|
+
except Exception as e:
|
476
|
+
logger.exception(
|
477
|
+
f"Unexpected error fetching performance express report for {code}: {e}")
|
478
|
+
raise DataSourceError(
|
479
|
+
f"Unexpected error fetching performance express report for {code}: {e}")
|
480
|
+
|
481
|
+
def get_forecast_report(self, code: str, start_date: str, end_date: str) -> pd.DataFrame:
|
482
|
+
"""Fetches performance forecast reports (业绩预告) using Baostock."""
|
483
|
+
logger.info(
|
484
|
+
f"Fetching Performance Forecast Report for {code} ({start_date} to {end_date})")
|
485
|
+
try:
|
486
|
+
with baostock_login_context():
|
487
|
+
rs = bs.query_forecast_report(
|
488
|
+
code=code, start_date=start_date, end_date=end_date)
|
489
|
+
# Note: Baostock docs mention pagination for this, but the Python API doesn't seem to expose it directly.
|
490
|
+
# We fetch all available pages in the loop below.
|
491
|
+
|
492
|
+
if rs.error_code != '0':
|
493
|
+
logger.error(
|
494
|
+
f"Baostock API error (Forecast) for {code}: {rs.error_msg} (code: {rs.error_code})")
|
495
|
+
if "no record found" in rs.error_msg.lower() or rs.error_code == '10002':
|
496
|
+
raise NoDataFoundError(
|
497
|
+
f"No performance forecast report found for {code} in range {start_date}-{end_date}. Baostock msg: {rs.error_msg}")
|
498
|
+
else:
|
499
|
+
raise DataSourceError(
|
500
|
+
f"Baostock API error fetching performance forecast report: {rs.error_msg} (code: {rs.error_code})")
|
501
|
+
|
502
|
+
data_list = []
|
503
|
+
while rs.next(): # Loop should handle pagination implicitly if rs manages it
|
504
|
+
data_list.append(rs.get_row_data())
|
505
|
+
|
506
|
+
if not data_list:
|
507
|
+
logger.warning(
|
508
|
+
f"No performance forecast report found for {code} in range {start_date}-{end_date} (empty result set).")
|
509
|
+
raise NoDataFoundError(
|
510
|
+
f"No performance forecast report found for {code} in range {start_date}-{end_date} (empty result set).")
|
511
|
+
|
512
|
+
result_df = pd.DataFrame(data_list, columns=rs.fields)
|
513
|
+
logger.info(
|
514
|
+
f"Retrieved {len(result_df)} performance forecast report records for {code}.")
|
515
|
+
return result_df
|
516
|
+
|
517
|
+
except (LoginError, NoDataFoundError, DataSourceError, ValueError) as e:
|
518
|
+
logger.warning(
|
519
|
+
f"Caught known error fetching performance forecast report for {code}: {type(e).__name__}")
|
520
|
+
raise e
|
521
|
+
except Exception as e:
|
522
|
+
logger.exception(
|
523
|
+
f"Unexpected error fetching performance forecast report for {code}: {e}")
|
524
|
+
raise DataSourceError(
|
525
|
+
f"Unexpected error fetching performance forecast report for {code}: {e}")
|
526
|
+
|
527
|
+
def get_stock_industry(self, code: Optional[str] = None, date: Optional[str] = None) -> pd.DataFrame:
|
528
|
+
"""Fetches industry classification using Baostock."""
|
529
|
+
log_msg = f"Fetching industry data for code={code or 'all'}, date={date or 'latest'}"
|
530
|
+
logger.info(log_msg)
|
531
|
+
try:
|
532
|
+
with baostock_login_context():
|
533
|
+
rs = bs.query_stock_industry(code=code, date=date)
|
534
|
+
|
535
|
+
if rs.error_code != '0':
|
536
|
+
logger.error(
|
537
|
+
f"Baostock API error (Industry) for {code}, {date}: {rs.error_msg} (code: {rs.error_code})")
|
538
|
+
if "no record found" in rs.error_msg.lower() or rs.error_code == '10002':
|
539
|
+
raise NoDataFoundError(
|
540
|
+
f"No industry data found for {code}, {date}. Baostock msg: {rs.error_msg}")
|
541
|
+
else:
|
542
|
+
raise DataSourceError(
|
543
|
+
f"Baostock API error fetching industry data: {rs.error_msg} (code: {rs.error_code})")
|
544
|
+
|
545
|
+
data_list = []
|
546
|
+
while rs.next():
|
547
|
+
data_list.append(rs.get_row_data())
|
548
|
+
|
549
|
+
if not data_list:
|
550
|
+
logger.warning(
|
551
|
+
f"No industry data found for {code}, {date} (empty result set).")
|
552
|
+
raise NoDataFoundError(
|
553
|
+
f"No industry data found for {code}, {date} (empty result set).")
|
554
|
+
|
555
|
+
result_df = pd.DataFrame(data_list, columns=rs.fields)
|
556
|
+
logger.info(
|
557
|
+
f"Retrieved {len(result_df)} industry records for {code or 'all'}, {date or 'latest'}.")
|
558
|
+
return result_df
|
559
|
+
|
560
|
+
except (LoginError, NoDataFoundError, DataSourceError, ValueError) as e:
|
561
|
+
logger.warning(
|
562
|
+
f"Caught known error fetching industry data for {code}, {date}: {type(e).__name__}")
|
563
|
+
raise e
|
564
|
+
except Exception as e:
|
565
|
+
logger.exception(
|
566
|
+
f"Unexpected error fetching industry data for {code}, {date}: {e}")
|
567
|
+
raise DataSourceError(
|
568
|
+
f"Unexpected error fetching industry data for {code}, {date}: {e}")
|
569
|
+
|
570
|
+
def get_sz50_stocks(self, date: Optional[str] = None) -> pd.DataFrame:
|
571
|
+
"""Fetches SZSE 50 index constituents using Baostock."""
|
572
|
+
return _fetch_index_constituent_data(bs.query_sz50_stocks, "SZSE 50", date)
|
573
|
+
|
574
|
+
def get_hs300_stocks(self, date: Optional[str] = None) -> pd.DataFrame:
|
575
|
+
"""Fetches CSI 300 index constituents using Baostock."""
|
576
|
+
return _fetch_index_constituent_data(bs.query_hs300_stocks, "CSI 300", date)
|
577
|
+
|
578
|
+
def get_zz500_stocks(self, date: Optional[str] = None) -> pd.DataFrame:
|
579
|
+
"""Fetches CSI 500 index constituents using Baostock."""
|
580
|
+
return _fetch_index_constituent_data(bs.query_zz500_stocks, "CSI 500", date)
|
581
|
+
|
582
|
+
def get_trade_dates(self, start_date: Optional[str] = None, end_date: Optional[str] = None) -> pd.DataFrame:
|
583
|
+
"""Fetches trading dates using Baostock."""
|
584
|
+
logger.info(
|
585
|
+
f"Fetching trade dates from {start_date or 'default'} to {end_date or 'default'}")
|
586
|
+
try:
|
587
|
+
with baostock_login_context(): # Login might not be strictly needed for this, but keeping consistent
|
588
|
+
rs = bs.query_trade_dates(
|
589
|
+
start_date=start_date, end_date=end_date)
|
590
|
+
|
591
|
+
if rs.error_code != '0':
|
592
|
+
logger.error(
|
593
|
+
f"Baostock API error (Trade Dates): {rs.error_msg} (code: {rs.error_code})")
|
594
|
+
# Unlikely to have 'no record found' for dates, but handle API errors
|
595
|
+
raise DataSourceError(
|
596
|
+
f"Baostock API error fetching trade dates: {rs.error_msg} (code: {rs.error_code})")
|
597
|
+
|
598
|
+
data_list = []
|
599
|
+
while rs.next():
|
600
|
+
data_list.append(rs.get_row_data())
|
601
|
+
|
602
|
+
if not data_list:
|
603
|
+
# This case should ideally not happen if the API returns a valid range
|
604
|
+
logger.warning(
|
605
|
+
f"No trade dates returned for range {start_date}-{end_date} (empty result set).")
|
606
|
+
raise NoDataFoundError(
|
607
|
+
f"No trade dates found for range {start_date}-{end_date} (empty result set).")
|
608
|
+
|
609
|
+
result_df = pd.DataFrame(data_list, columns=rs.fields)
|
610
|
+
logger.info(f"Retrieved {len(result_df)} trade date records.")
|
611
|
+
return result_df
|
612
|
+
|
613
|
+
except (LoginError, NoDataFoundError, DataSourceError, ValueError) as e:
|
614
|
+
logger.warning(
|
615
|
+
f"Caught known error fetching trade dates: {type(e).__name__}")
|
616
|
+
raise e
|
617
|
+
except Exception as e:
|
618
|
+
logger.exception(f"Unexpected error fetching trade dates: {e}")
|
619
|
+
raise DataSourceError(
|
620
|
+
f"Unexpected error fetching trade dates: {e}")
|
621
|
+
|
622
|
+
def get_all_stock(self, date: Optional[str] = None) -> pd.DataFrame:
|
623
|
+
"""Fetches all stock list for a given date using Baostock."""
|
624
|
+
logger.info(f"Fetching all stock list for date={date or 'default'}")
|
625
|
+
try:
|
626
|
+
with baostock_login_context():
|
627
|
+
rs = bs.query_all_stock(day=date)
|
628
|
+
|
629
|
+
if rs.error_code != '0':
|
630
|
+
logger.error(
|
631
|
+
f"Baostock API error (All Stock) for date {date}: {rs.error_msg} (code: {rs.error_code})")
|
632
|
+
if "no record found" in rs.error_msg.lower() or rs.error_code == '10002': # Check if this applies
|
633
|
+
raise NoDataFoundError(
|
634
|
+
f"No stock data found for date {date}. Baostock msg: {rs.error_msg}")
|
635
|
+
else:
|
636
|
+
raise DataSourceError(
|
637
|
+
f"Baostock API error fetching all stock list: {rs.error_msg} (code: {rs.error_code})")
|
638
|
+
|
639
|
+
data_list = []
|
640
|
+
while rs.next():
|
641
|
+
data_list.append(rs.get_row_data())
|
642
|
+
|
643
|
+
if not data_list:
|
644
|
+
logger.warning(
|
645
|
+
f"No stock list returned for date {date} (empty result set).")
|
646
|
+
raise NoDataFoundError(
|
647
|
+
f"No stock list found for date {date} (empty result set).")
|
648
|
+
|
649
|
+
result_df = pd.DataFrame(data_list, columns=rs.fields)
|
650
|
+
logger.info(
|
651
|
+
f"Retrieved {len(result_df)} stock records for date {date or 'default'}.")
|
652
|
+
return result_df
|
653
|
+
|
654
|
+
except (LoginError, NoDataFoundError, DataSourceError, ValueError) as e:
|
655
|
+
logger.warning(
|
656
|
+
f"Caught known error fetching all stock list for date {date}: {type(e).__name__}")
|
657
|
+
raise e
|
658
|
+
except Exception as e:
|
659
|
+
logger.exception(
|
660
|
+
f"Unexpected error fetching all stock list for date {date}: {e}")
|
661
|
+
raise DataSourceError(
|
662
|
+
f"Unexpected error fetching all stock list for date {date}: {e}")
|
663
|
+
|
664
|
+
def get_deposit_rate_data(self, start_date: Optional[str] = None, end_date: Optional[str] = None) -> pd.DataFrame:
|
665
|
+
"""Fetches benchmark deposit rates using Baostock."""
|
666
|
+
return _fetch_macro_data(bs.query_deposit_rate_data, "Deposit Rate", start_date, end_date)
|
667
|
+
|
668
|
+
def get_loan_rate_data(self, start_date: Optional[str] = None, end_date: Optional[str] = None) -> pd.DataFrame:
|
669
|
+
"""Fetches benchmark loan rates using Baostock."""
|
670
|
+
return _fetch_macro_data(bs.query_loan_rate_data, "Loan Rate", start_date, end_date)
|
671
|
+
|
672
|
+
def get_required_reserve_ratio_data(self, start_date: Optional[str] = None, end_date: Optional[str] = None, year_type: str = '0') -> pd.DataFrame:
|
673
|
+
"""Fetches required reserve ratio data using Baostock."""
|
674
|
+
# Note the extra yearType parameter handled by kwargs
|
675
|
+
return _fetch_macro_data(bs.query_required_reserve_ratio_data, "Required Reserve Ratio", start_date, end_date, yearType=year_type)
|
676
|
+
|
677
|
+
def get_money_supply_data_month(self, start_date: Optional[str] = None, end_date: Optional[str] = None) -> pd.DataFrame:
|
678
|
+
"""Fetches monthly money supply data (M0, M1, M2) using Baostock."""
|
679
|
+
# Baostock expects YYYY-MM format for dates here
|
680
|
+
return _fetch_macro_data(bs.query_money_supply_data_month, "Monthly Money Supply", start_date, end_date)
|
681
|
+
|
682
|
+
def get_money_supply_data_year(self, start_date: Optional[str] = None, end_date: Optional[str] = None) -> pd.DataFrame:
|
683
|
+
"""Fetches yearly money supply data (M0, M1, M2 - year end balance) using Baostock."""
|
684
|
+
# Baostock expects YYYY format for dates here
|
685
|
+
return _fetch_macro_data(bs.query_money_supply_data_year, "Yearly Money Supply", start_date, end_date)
|
686
|
+
|
687
|
+
def get_shibor_data(self, start_date: Optional[str] = None, end_date: Optional[str] = None) -> pd.DataFrame:
|
688
|
+
"""Fetches SHIBOR (Shanghai Interbank Offered Rate) data using Baostock."""
|
689
|
+
return _fetch_macro_data(bs.query_shibor_data, "SHIBOR", start_date, end_date)
|