quantcli 0.1.13__tar.gz → 0.1.14__tar.gz
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.
- {quantcli-0.1.13/quantcli.egg-info → quantcli-0.1.14}/PKG-INFO +1 -1
- {quantcli-0.1.13 → quantcli-0.1.14}/pyproject.toml +1 -1
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/cli.py +1 -1
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/akshare.py +36 -61
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/baostock.py +102 -97
- {quantcli-0.1.13 → quantcli-0.1.14/quantcli.egg-info}/PKG-INFO +1 -1
- {quantcli-0.1.13 → quantcli-0.1.14}/LICENSE +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/README.md +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/core/__init__.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/core/backtest.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/core/data.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/core/factor.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/__init__.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/base.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/cache.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/fundamentals/__init__.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/fundamentals/provider.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/mixed.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/mysql.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/sync/__init__.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/sync/akshare.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/sync/base.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/sync/gm.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/sync/gm_fundamental.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/__init__.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_001.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_002.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_003.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_004.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_005.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_006.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_007.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_008.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_009.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_010.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_011.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_012.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_013.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_014.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_015.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_016.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_017.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_018.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_019.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_020.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_021.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_022.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_023.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_024.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_025.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_026.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_027.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_028.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_029.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_030.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_031.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_032.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_033.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_034.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_035.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_036.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_037.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_038.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_039.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_040.yaml +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/base.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/compute.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/loader.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/pipeline.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/ranking.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/ranking_executor.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/screening.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/screening_executor.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/models/bar.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/parser/__init__.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/parser/constants.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/parser/formula.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/__init__.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/env.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/logger.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/path.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/symbol_utils.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/time.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/validate.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli.egg-info/SOURCES.txt +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli.egg-info/dependency_links.txt +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli.egg-info/entry_points.txt +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli.egg-info/requires.txt +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/quantcli.egg-info/top_level.txt +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/setup.cfg +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_akshare_integration.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_builtin_factors.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_cli.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_datasources.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_factors.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_gm_executors.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_mixed_datasource.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_multi_factor.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_pipeline_integration.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_symbol_utils.py +0 -0
- {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_time.py +0 -0
|
@@ -35,6 +35,7 @@ class AkshareDataSource(DataSource):
|
|
|
35
35
|
|
|
36
36
|
try:
|
|
37
37
|
import akshare as ak
|
|
38
|
+
|
|
38
39
|
self._ak = ak
|
|
39
40
|
except ImportError:
|
|
40
41
|
raise ImportError("akshare not installed. Run: pip install akshare")
|
|
@@ -56,11 +57,7 @@ class AkshareDataSource(DataSource):
|
|
|
56
57
|
symbol = symbol.replace(".SH", "").replace(".SZ", "")
|
|
57
58
|
return f"sh{symbol}" if symbol.startswith(("6", "5", "9")) else f"sz{symbol}"
|
|
58
59
|
|
|
59
|
-
def _market_filter(
|
|
60
|
-
self,
|
|
61
|
-
df: pd.DataFrame,
|
|
62
|
-
market: str
|
|
63
|
-
) -> pd.DataFrame:
|
|
60
|
+
def _market_filter(self, df: pd.DataFrame, market: str) -> pd.DataFrame:
|
|
64
61
|
"""过滤股票市场"""
|
|
65
62
|
if market == "all":
|
|
66
63
|
return df
|
|
@@ -73,16 +70,12 @@ class AkshareDataSource(DataSource):
|
|
|
73
70
|
# ==================== 核心接口 ====================
|
|
74
71
|
|
|
75
72
|
def get_daily(
|
|
76
|
-
self,
|
|
77
|
-
symbol: str,
|
|
78
|
-
start_date,
|
|
79
|
-
end_date,
|
|
80
|
-
fields: Optional[List[str]] = None
|
|
73
|
+
self, symbol: str, start_date, end_date, fields: Optional[List[str]] = None
|
|
81
74
|
) -> pd.DataFrame:
|
|
82
75
|
"""获取A股日线数据(腾讯接口)"""
|
|
83
76
|
start_str = format_date(start_date, "%Y-%m-%d")
|
|
84
77
|
end_str = format_date(end_date, "%Y-%m-%d")
|
|
85
|
-
cache_key = f"{symbol}_{start_str}_{end_str}"
|
|
78
|
+
cache_key = f"akshare_{symbol}_{start_str}_{end_str}"
|
|
86
79
|
|
|
87
80
|
# 1. 尝试缓存
|
|
88
81
|
if self.config.use_cache:
|
|
@@ -95,7 +88,7 @@ class AkshareDataSource(DataSource):
|
|
|
95
88
|
df = self._ak.stock_zh_a_hist_tx(
|
|
96
89
|
symbol=tx_symbol,
|
|
97
90
|
start_date=format_date(start_date, "%Y%m%d"),
|
|
98
|
-
end_date=format_date(end_date, "%Y%m%d")
|
|
91
|
+
end_date=format_date(end_date, "%Y%m%d"),
|
|
99
92
|
)
|
|
100
93
|
|
|
101
94
|
if df.empty:
|
|
@@ -109,16 +102,11 @@ class AkshareDataSource(DataSource):
|
|
|
109
102
|
|
|
110
103
|
return self._filter_fields(df, fields) if fields else df
|
|
111
104
|
|
|
112
|
-
def get_index_daily(
|
|
113
|
-
self,
|
|
114
|
-
symbol: str,
|
|
115
|
-
start_date,
|
|
116
|
-
end_date
|
|
117
|
-
) -> pd.DataFrame:
|
|
105
|
+
def get_index_daily(self, symbol: str, start_date, end_date) -> pd.DataFrame:
|
|
118
106
|
"""获取指数日线数据(腾讯接口)"""
|
|
119
107
|
start_str = format_date(start_date, "%Y-%m-%d")
|
|
120
108
|
end_str = format_date(end_date, "%Y-%m-%d")
|
|
121
|
-
cache_key = f"
|
|
109
|
+
cache_key = f"akshare_idx_{symbol}_{start_str}_{end_str}"
|
|
122
110
|
|
|
123
111
|
if self.config.use_cache:
|
|
124
112
|
cached = self._price_cache.get(cache_key)
|
|
@@ -143,7 +131,7 @@ class AkshareDataSource(DataSource):
|
|
|
143
131
|
symbol: str,
|
|
144
132
|
start_date: Optional[date] = None,
|
|
145
133
|
end_date: Optional[date] = None,
|
|
146
|
-
period: str = "5"
|
|
134
|
+
period: str = "5",
|
|
147
135
|
) -> pd.DataFrame:
|
|
148
136
|
"""获取分钟级数据
|
|
149
137
|
|
|
@@ -172,37 +160,43 @@ class AkshareDataSource(DataSource):
|
|
|
172
160
|
period=period,
|
|
173
161
|
start_date=start_date.strftime("%Y%m%d"),
|
|
174
162
|
end_date=end_date.strftime("%Y%m%d"),
|
|
175
|
-
adjust="qfq"
|
|
163
|
+
adjust="qfq",
|
|
176
164
|
)
|
|
177
165
|
|
|
178
166
|
if df.empty:
|
|
179
|
-
return pd.DataFrame(
|
|
167
|
+
return pd.DataFrame(
|
|
168
|
+
columns=["date", "open", "high", "low", "close", "volume"]
|
|
169
|
+
)
|
|
180
170
|
|
|
181
171
|
# 标准化列名
|
|
182
|
-
df = df.rename(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
172
|
+
df = df.rename(
|
|
173
|
+
columns={
|
|
174
|
+
"时间": "date",
|
|
175
|
+
"开盘": "open",
|
|
176
|
+
"最高": "high",
|
|
177
|
+
"最低": "low",
|
|
178
|
+
"收盘": "close",
|
|
179
|
+
"成交量": "volume",
|
|
180
|
+
"成交额": "amount",
|
|
181
|
+
}
|
|
182
|
+
)
|
|
191
183
|
|
|
192
184
|
# 转换日期格式
|
|
193
185
|
df["date"] = pd.to_datetime(df["date"])
|
|
194
186
|
|
|
195
187
|
# 确保数值列是数值类型
|
|
196
|
-
numeric_cols = [
|
|
188
|
+
numeric_cols = ["open", "high", "low", "close", "volume", "amount"]
|
|
197
189
|
for col in numeric_cols:
|
|
198
190
|
if col in df.columns:
|
|
199
|
-
df[col] = pd.to_numeric(df[col], errors=
|
|
191
|
+
df[col] = pd.to_numeric(df[col], errors="coerce")
|
|
200
192
|
|
|
201
193
|
return df
|
|
202
194
|
|
|
203
195
|
except Exception as e:
|
|
204
196
|
logger.warning(f"Failed to get intraday data for {symbol}: {e}")
|
|
205
|
-
return pd.DataFrame(
|
|
197
|
+
return pd.DataFrame(
|
|
198
|
+
columns=["date", "open", "high", "low", "close", "volume"]
|
|
199
|
+
)
|
|
206
200
|
|
|
207
201
|
def get_stock_list(self, market: str = "all") -> pd.DataFrame:
|
|
208
202
|
"""获取A股股票列表
|
|
@@ -217,7 +211,7 @@ class AkshareDataSource(DataSource):
|
|
|
217
211
|
|
|
218
212
|
df = self._ak.stock_info_a_code_name()
|
|
219
213
|
if df.empty:
|
|
220
|
-
return pd.DataFrame(columns=[
|
|
214
|
+
return pd.DataFrame(columns=["symbol", "name", "exchange", "market"])
|
|
221
215
|
|
|
222
216
|
# 转换格式:akshare 可能返回中文列名
|
|
223
217
|
df = df.rename(columns={"代码": "symbol", "名称": "name"})
|
|
@@ -258,9 +252,7 @@ class AkshareDataSource(DataSource):
|
|
|
258
252
|
# ==================== 辅助方法 ====================
|
|
259
253
|
|
|
260
254
|
def _filter_fields(
|
|
261
|
-
self,
|
|
262
|
-
df: pd.DataFrame,
|
|
263
|
-
fields: Optional[List[str]]
|
|
255
|
+
self, df: pd.DataFrame, fields: Optional[List[str]]
|
|
264
256
|
) -> pd.DataFrame:
|
|
265
257
|
"""过滤字段"""
|
|
266
258
|
if not fields:
|
|
@@ -272,21 +264,14 @@ class AkshareDataSource(DataSource):
|
|
|
272
264
|
"""健康检查"""
|
|
273
265
|
try:
|
|
274
266
|
self._ak.tool_trade_date_hist_sina()
|
|
275
|
-
return {
|
|
276
|
-
"status": "ok",
|
|
277
|
-
"source": self.name,
|
|
278
|
-
"cache": self.config.use_cache
|
|
279
|
-
}
|
|
267
|
+
return {"status": "ok", "source": self.name, "cache": self.config.use_cache}
|
|
280
268
|
except Exception as e:
|
|
281
269
|
return {"status": "error", "source": self.name, "error": str(e)}
|
|
282
270
|
|
|
283
271
|
# ==================== 不支持的接口 ====================
|
|
284
272
|
|
|
285
273
|
def get_fundamental(
|
|
286
|
-
self,
|
|
287
|
-
symbols: List[str],
|
|
288
|
-
date,
|
|
289
|
-
indicators: Optional[List[str]] = None
|
|
274
|
+
self, symbols: List[str], date, indicators: Optional[List[str]] = None
|
|
290
275
|
) -> pd.DataFrame:
|
|
291
276
|
"""基本面数据请使用 baostock 数据源"""
|
|
292
277
|
raise NotImplementedError(
|
|
@@ -295,19 +280,9 @@ class AkshareDataSource(DataSource):
|
|
|
295
280
|
)
|
|
296
281
|
|
|
297
282
|
def get_fina_indicator(
|
|
298
|
-
self,
|
|
299
|
-
symbols: List[str],
|
|
300
|
-
report_type: str = "latest"
|
|
283
|
+
self, symbols: List[str], report_type: str = "latest"
|
|
301
284
|
) -> pd.DataFrame:
|
|
302
|
-
raise NotImplementedError(
|
|
303
|
-
"Use baostock datasource for financial indicators"
|
|
304
|
-
)
|
|
285
|
+
raise NotImplementedError("Use baostock datasource for financial indicators")
|
|
305
286
|
|
|
306
|
-
def get_valuation(
|
|
307
|
-
|
|
308
|
-
symbols: List[str],
|
|
309
|
-
date
|
|
310
|
-
) -> pd.DataFrame:
|
|
311
|
-
raise NotImplementedError(
|
|
312
|
-
"Use baostock datasource for valuation data"
|
|
313
|
-
)
|
|
287
|
+
def get_valuation(self, symbols: List[str], date) -> pd.DataFrame:
|
|
288
|
+
raise NotImplementedError("Use baostock datasource for valuation data")
|
|
@@ -39,12 +39,11 @@ class BaostockDataSource(DataSource):
|
|
|
39
39
|
"""
|
|
40
40
|
try:
|
|
41
41
|
import baostock as bs
|
|
42
|
+
|
|
42
43
|
self._bs = bs
|
|
43
44
|
self._login()
|
|
44
45
|
except ImportError:
|
|
45
|
-
raise ImportError(
|
|
46
|
-
"baostock not installed. Run: pip install baostock"
|
|
47
|
-
)
|
|
46
|
+
raise ImportError("baostock not installed. Run: pip install baostock")
|
|
48
47
|
|
|
49
48
|
# 缓存
|
|
50
49
|
self._cache = FundamentalsCache(enabled=use_cache)
|
|
@@ -52,7 +51,7 @@ class BaostockDataSource(DataSource):
|
|
|
52
51
|
def __del__(self):
|
|
53
52
|
"""登出"""
|
|
54
53
|
try:
|
|
55
|
-
if hasattr(self,
|
|
54
|
+
if hasattr(self, "_bs"):
|
|
56
55
|
self._bs.logout()
|
|
57
56
|
except Exception:
|
|
58
57
|
pass
|
|
@@ -60,7 +59,7 @@ class BaostockDataSource(DataSource):
|
|
|
60
59
|
def _login(self):
|
|
61
60
|
"""登录"""
|
|
62
61
|
lg = self._bs.login()
|
|
63
|
-
if lg.error_code !=
|
|
62
|
+
if lg.error_code != "0":
|
|
64
63
|
raise RuntimeError(f"Baostock 登录失败: {lg.error_msg}")
|
|
65
64
|
|
|
66
65
|
def _to_bs_symbol(self, symbol: str) -> str:
|
|
@@ -72,9 +71,14 @@ class BaostockDataSource(DataSource):
|
|
|
72
71
|
- "sh.600519" -> "sh.600519"
|
|
73
72
|
- "SZ000001" -> "sz.000001"
|
|
74
73
|
"""
|
|
75
|
-
|
|
74
|
+
# 转 统一大写,移除前缀
|
|
76
75
|
symbol = symbol.upper()
|
|
77
|
-
symbol =
|
|
76
|
+
symbol = (
|
|
77
|
+
symbol.replace(".SH", "")
|
|
78
|
+
.replace(".SZ", "")
|
|
79
|
+
.replace("SH", "")
|
|
80
|
+
.replace("SZ", "")
|
|
81
|
+
)
|
|
78
82
|
|
|
79
83
|
# 判断交易所
|
|
80
84
|
if symbol.startswith(("6", "5", "9")):
|
|
@@ -88,11 +92,7 @@ class BaostockDataSource(DataSource):
|
|
|
88
92
|
return str(d)
|
|
89
93
|
|
|
90
94
|
def get_daily(
|
|
91
|
-
self,
|
|
92
|
-
symbol: str,
|
|
93
|
-
start_date,
|
|
94
|
-
end_date,
|
|
95
|
-
fields: Optional[List[str]] = None
|
|
95
|
+
self, symbol: str, start_date, end_date, fields: Optional[List[str]] = None
|
|
96
96
|
) -> pd.DataFrame:
|
|
97
97
|
"""获取日线数据"""
|
|
98
98
|
bs_symbol = self._to_bs_symbol(symbol)
|
|
@@ -122,10 +122,10 @@ class BaostockDataSource(DataSource):
|
|
|
122
122
|
",".join(selected),
|
|
123
123
|
start_date=start_str,
|
|
124
124
|
end_date=end_str,
|
|
125
|
-
frequency="d"
|
|
125
|
+
frequency="d",
|
|
126
126
|
)
|
|
127
127
|
|
|
128
|
-
if rs.error_code !=
|
|
128
|
+
if rs.error_code != "0":
|
|
129
129
|
raise RuntimeError(f"查询失败: {rs.error_msg}")
|
|
130
130
|
|
|
131
131
|
data_list = []
|
|
@@ -155,7 +155,7 @@ class BaostockDataSource(DataSource):
|
|
|
155
155
|
try:
|
|
156
156
|
rs = self._bs.query_all_stock()
|
|
157
157
|
|
|
158
|
-
if rs.error_code !=
|
|
158
|
+
if rs.error_code != "0":
|
|
159
159
|
raise RuntimeError(f"查询失败: {rs.error_msg}")
|
|
160
160
|
|
|
161
161
|
data_list = []
|
|
@@ -188,12 +188,14 @@ class BaostockDataSource(DataSource):
|
|
|
188
188
|
# 提取纯代码
|
|
189
189
|
symbol = code.replace("sh.", "").replace("sz.", "")
|
|
190
190
|
|
|
191
|
-
stocks.append(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
191
|
+
stocks.append(
|
|
192
|
+
StockInfo(
|
|
193
|
+
symbol=symbol,
|
|
194
|
+
name=name,
|
|
195
|
+
exchange=exchange,
|
|
196
|
+
market=market_type,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
197
199
|
|
|
198
200
|
return stocks
|
|
199
201
|
|
|
@@ -204,15 +206,12 @@ class BaostockDataSource(DataSource):
|
|
|
204
206
|
"""获取交易日历"""
|
|
205
207
|
# Baostock 没有直接的交易日历,可以从日线数据推断
|
|
206
208
|
# 这里暂时返回空列表,由其他数据源提供
|
|
207
|
-
logger.warning(
|
|
209
|
+
logger.warning(
|
|
210
|
+
"Baostock 不提供交易日历,请使用 akshare 的 tool_trade_date_hist_sina"
|
|
211
|
+
)
|
|
208
212
|
return []
|
|
209
213
|
|
|
210
|
-
def get_index_daily(
|
|
211
|
-
self,
|
|
212
|
-
symbol: str,
|
|
213
|
-
start_date,
|
|
214
|
-
end_date
|
|
215
|
-
) -> pd.DataFrame:
|
|
214
|
+
def get_index_daily(self, symbol: str, start_date, end_date) -> pd.DataFrame:
|
|
216
215
|
"""获取指数数据"""
|
|
217
216
|
# Baostock 主要支持股票数据,指数数据有限
|
|
218
217
|
raise NotImplementedError(
|
|
@@ -228,9 +227,9 @@ class BaostockDataSource(DataSource):
|
|
|
228
227
|
"date,close",
|
|
229
228
|
start_date="2024-01-01",
|
|
230
229
|
end_date="2024-01-01",
|
|
231
|
-
frequency="d"
|
|
230
|
+
frequency="d",
|
|
232
231
|
)
|
|
233
|
-
if rs.error_code ==
|
|
232
|
+
if rs.error_code == "0":
|
|
234
233
|
return {"status": "ok", "source": self.name}
|
|
235
234
|
else:
|
|
236
235
|
return {"status": "error", "source": self.name, "error": rs.error_msg}
|
|
@@ -238,9 +237,7 @@ class BaostockDataSource(DataSource):
|
|
|
238
237
|
return {"status": "error", "source": self.name, "error": str(e)}
|
|
239
238
|
|
|
240
239
|
def get_fina_indicator(
|
|
241
|
-
self,
|
|
242
|
-
symbols: List[str],
|
|
243
|
-
report_type: str = "latest"
|
|
240
|
+
self, symbols: List[str], report_type: str = "latest"
|
|
244
241
|
) -> pd.DataFrame:
|
|
245
242
|
"""获取杜邦分析数据"""
|
|
246
243
|
results = []
|
|
@@ -254,18 +251,22 @@ class BaostockDataSource(DataSource):
|
|
|
254
251
|
for quarter in [1, 2, 3, 4]:
|
|
255
252
|
rs = self._bs.query_dupont_data(bs_symbol, year, quarter)
|
|
256
253
|
|
|
257
|
-
if rs.error_code !=
|
|
254
|
+
if rs.error_code != "0":
|
|
258
255
|
continue
|
|
259
256
|
|
|
260
257
|
while rs.next():
|
|
261
258
|
row = rs.get_row_data()
|
|
262
|
-
results.append(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
259
|
+
results.append(
|
|
260
|
+
{
|
|
261
|
+
"symbol": symbol,
|
|
262
|
+
"year": year,
|
|
263
|
+
"quarter": quarter,
|
|
264
|
+
"roe": float(row[3]) if row[3] else None,
|
|
265
|
+
"netprofitmargin": float(row[7])
|
|
266
|
+
if row[7]
|
|
267
|
+
else None,
|
|
268
|
+
}
|
|
269
|
+
)
|
|
269
270
|
|
|
270
271
|
except Exception as e:
|
|
271
272
|
logger.warning(f"获取 {symbol} 杜邦数据失败: {e}")
|
|
@@ -278,10 +279,7 @@ class BaostockDataSource(DataSource):
|
|
|
278
279
|
return df
|
|
279
280
|
|
|
280
281
|
def get_dupont_analysis(
|
|
281
|
-
self,
|
|
282
|
-
symbols: List[str],
|
|
283
|
-
start_year: int = 2022,
|
|
284
|
-
end_year: int = 2024
|
|
282
|
+
self, symbols: List[str], start_year: int = 2022, end_year: int = 2024
|
|
285
283
|
) -> pd.DataFrame:
|
|
286
284
|
"""批量获取杜邦分析数据(带缓存)
|
|
287
285
|
|
|
@@ -297,7 +295,7 @@ class BaostockDataSource(DataSource):
|
|
|
297
295
|
|
|
298
296
|
for symbol in symbols:
|
|
299
297
|
# 尝试从缓存读取
|
|
300
|
-
cache_key = f"
|
|
298
|
+
cache_key = f"baostock_dupont_{symbol}_{start_year}_{end_year}"
|
|
301
299
|
cached = self._cache.get(cache_key) if self._cache.enabled else None
|
|
302
300
|
|
|
303
301
|
if cached is not None:
|
|
@@ -314,24 +312,36 @@ class BaostockDataSource(DataSource):
|
|
|
314
312
|
try:
|
|
315
313
|
rs = self._bs.query_dupont_data(bs_symbol, year, quarter)
|
|
316
314
|
|
|
317
|
-
if rs.error_code !=
|
|
315
|
+
if rs.error_code != "0":
|
|
318
316
|
continue
|
|
319
317
|
|
|
320
318
|
while rs.next():
|
|
321
319
|
row = rs.get_row_data()
|
|
322
|
-
symbol_data.append(
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
320
|
+
symbol_data.append(
|
|
321
|
+
{
|
|
322
|
+
"symbol": symbol,
|
|
323
|
+
"pub_date": row[1],
|
|
324
|
+
"stat_date": row[2],
|
|
325
|
+
"roe": float(row[3]) if row[3] else None,
|
|
326
|
+
"asset_equity_ratio": float(row[4])
|
|
327
|
+
if row[4]
|
|
328
|
+
else None,
|
|
329
|
+
"asset_turnover": float(row[5]) if row[5] else None,
|
|
330
|
+
"net_profit_margin": float(row[6])
|
|
331
|
+
if row[6]
|
|
332
|
+
else None,
|
|
333
|
+
"gross_profit_margin": float(row[7])
|
|
334
|
+
if row[7]
|
|
335
|
+
else None,
|
|
336
|
+
"tax_burden": float(row[8]) if row[8] else None,
|
|
337
|
+
"interest_burden": float(row[9])
|
|
338
|
+
if row[9]
|
|
339
|
+
else None,
|
|
340
|
+
"ebit_to_nprofit": float(row[10])
|
|
341
|
+
if row[10]
|
|
342
|
+
else None,
|
|
343
|
+
}
|
|
344
|
+
)
|
|
335
345
|
|
|
336
346
|
except Exception as e:
|
|
337
347
|
logger.warning(f"获取 {symbol} {year}Q{quarter} 数据失败: {e}")
|
|
@@ -349,17 +359,14 @@ class BaostockDataSource(DataSource):
|
|
|
349
359
|
return pd.concat(all_data, ignore_index=True)
|
|
350
360
|
|
|
351
361
|
def get_profit_data(
|
|
352
|
-
self,
|
|
353
|
-
symbols: List[str],
|
|
354
|
-
start_year: int = 2022,
|
|
355
|
-
end_year: int = 2024
|
|
362
|
+
self, symbols: List[str], start_year: int = 2022, end_year: int = 2024
|
|
356
363
|
) -> pd.DataFrame:
|
|
357
364
|
"""批量获取利润表数据(带缓存)"""
|
|
358
365
|
all_data = []
|
|
359
366
|
|
|
360
367
|
for symbol in symbols:
|
|
361
368
|
# 尝试从缓存读取
|
|
362
|
-
cache_key = f"
|
|
369
|
+
cache_key = f"baostock_profit_{symbol}_{start_year}_{end_year}"
|
|
363
370
|
cached = self._cache.get(cache_key) if self._cache.enabled else None
|
|
364
371
|
|
|
365
372
|
if cached is not None:
|
|
@@ -376,20 +383,22 @@ class BaostockDataSource(DataSource):
|
|
|
376
383
|
try:
|
|
377
384
|
rs = self._bs.query_profit_data(bs_symbol, year, quarter)
|
|
378
385
|
|
|
379
|
-
if rs.error_code !=
|
|
386
|
+
if rs.error_code != "0":
|
|
380
387
|
continue
|
|
381
388
|
|
|
382
389
|
while rs.next():
|
|
383
390
|
row = rs.get_row_data()
|
|
384
|
-
symbol_data.append(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
391
|
+
symbol_data.append(
|
|
392
|
+
{
|
|
393
|
+
"symbol": symbol,
|
|
394
|
+
"pub_date": row[1],
|
|
395
|
+
"net_profits": float(row[2]) if row[2] else None,
|
|
396
|
+
"net_profits_yr": float(row[3]) if row[3] else None,
|
|
397
|
+
"dt_net_profits": float(row[4]) if row[4] else None,
|
|
398
|
+
"total_revenue": float(row[5]) if row[5] else None,
|
|
399
|
+
"revenue_yr": float(row[6]) if row[6] else None,
|
|
400
|
+
}
|
|
401
|
+
)
|
|
393
402
|
|
|
394
403
|
except Exception as e:
|
|
395
404
|
continue
|
|
@@ -405,17 +414,14 @@ class BaostockDataSource(DataSource):
|
|
|
405
414
|
return pd.concat(all_data, ignore_index=True)
|
|
406
415
|
|
|
407
416
|
def get_growth_data(
|
|
408
|
-
self,
|
|
409
|
-
symbols: List[str],
|
|
410
|
-
start_year: int = 2022,
|
|
411
|
-
end_year: int = 2024
|
|
417
|
+
self, symbols: List[str], start_year: int = 2022, end_year: int = 2024
|
|
412
418
|
) -> pd.DataFrame:
|
|
413
419
|
"""批量获取成长能力数据(带缓存)"""
|
|
414
420
|
all_data = []
|
|
415
421
|
|
|
416
422
|
for symbol in symbols:
|
|
417
423
|
# 尝试从缓存读取
|
|
418
|
-
cache_key = f"
|
|
424
|
+
cache_key = f"baostock_growth_{symbol}_{start_year}_{end_year}"
|
|
419
425
|
cached = self._cache.get(cache_key) if self._cache.enabled else None
|
|
420
426
|
|
|
421
427
|
if cached is not None:
|
|
@@ -432,18 +438,24 @@ class BaostockDataSource(DataSource):
|
|
|
432
438
|
try:
|
|
433
439
|
rs = self._bs.query_growth_data(bs_symbol, year, quarter)
|
|
434
440
|
|
|
435
|
-
if rs.error_code !=
|
|
441
|
+
if rs.error_code != "0":
|
|
436
442
|
continue
|
|
437
443
|
|
|
438
444
|
while rs.next():
|
|
439
445
|
row = rs.get_row_data()
|
|
440
|
-
symbol_data.append(
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
446
|
+
symbol_data.append(
|
|
447
|
+
{
|
|
448
|
+
"symbol": symbol,
|
|
449
|
+
"pub_date": row[1],
|
|
450
|
+
"net_profits_growth": float(row[2])
|
|
451
|
+
if row[2]
|
|
452
|
+
else None,
|
|
453
|
+
"revenue_growth": float(row[3]) if row[3] else None,
|
|
454
|
+
"total_assets_growth": float(row[4])
|
|
455
|
+
if row[4]
|
|
456
|
+
else None,
|
|
457
|
+
}
|
|
458
|
+
)
|
|
447
459
|
|
|
448
460
|
except Exception as e:
|
|
449
461
|
continue
|
|
@@ -458,11 +470,7 @@ class BaostockDataSource(DataSource):
|
|
|
458
470
|
|
|
459
471
|
return pd.concat(all_data, ignore_index=True)
|
|
460
472
|
|
|
461
|
-
def get_valuation(
|
|
462
|
-
self,
|
|
463
|
-
symbols: List[str],
|
|
464
|
-
date
|
|
465
|
-
) -> pd.DataFrame:
|
|
473
|
+
def get_valuation(self, symbols: List[str], date) -> pd.DataFrame:
|
|
466
474
|
"""获取估值数据(从日线数据计算)"""
|
|
467
475
|
# Baostock 没有直接的估值数据接口
|
|
468
476
|
# 可以从日线数据中提取最新收盘价
|
|
@@ -471,10 +479,7 @@ class BaostockDataSource(DataSource):
|
|
|
471
479
|
)
|
|
472
480
|
|
|
473
481
|
def get_fundamental(
|
|
474
|
-
self,
|
|
475
|
-
symbols: List[str],
|
|
476
|
-
date,
|
|
477
|
-
indicators: Optional[List[str]] = None
|
|
482
|
+
self, symbols: List[str], date, indicators: Optional[List[str]] = None
|
|
478
483
|
) -> pd.DataFrame:
|
|
479
484
|
"""获取合并基本面数据"""
|
|
480
485
|
# 批量下载杜邦分析数据
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|