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.
Files changed (101) hide show
  1. {quantcli-0.1.13/quantcli.egg-info → quantcli-0.1.14}/PKG-INFO +1 -1
  2. {quantcli-0.1.13 → quantcli-0.1.14}/pyproject.toml +1 -1
  3. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/cli.py +1 -1
  4. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/akshare.py +36 -61
  5. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/baostock.py +102 -97
  6. {quantcli-0.1.13 → quantcli-0.1.14/quantcli.egg-info}/PKG-INFO +1 -1
  7. {quantcli-0.1.13 → quantcli-0.1.14}/LICENSE +0 -0
  8. {quantcli-0.1.13 → quantcli-0.1.14}/README.md +0 -0
  9. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/core/__init__.py +0 -0
  10. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/core/backtest.py +0 -0
  11. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/core/data.py +0 -0
  12. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/core/factor.py +0 -0
  13. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/__init__.py +0 -0
  14. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/base.py +0 -0
  15. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/cache.py +0 -0
  16. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/fundamentals/__init__.py +0 -0
  17. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/fundamentals/provider.py +0 -0
  18. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/mixed.py +0 -0
  19. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/mysql.py +0 -0
  20. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/sync/__init__.py +0 -0
  21. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/sync/akshare.py +0 -0
  22. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/sync/base.py +0 -0
  23. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/sync/gm.py +0 -0
  24. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/datasources/sync/gm_fundamental.py +0 -0
  25. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/__init__.py +0 -0
  26. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_001.yaml +0 -0
  27. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_002.yaml +0 -0
  28. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_003.yaml +0 -0
  29. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_004.yaml +0 -0
  30. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_005.yaml +0 -0
  31. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_006.yaml +0 -0
  32. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_007.yaml +0 -0
  33. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_008.yaml +0 -0
  34. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_009.yaml +0 -0
  35. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_010.yaml +0 -0
  36. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_011.yaml +0 -0
  37. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_012.yaml +0 -0
  38. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_013.yaml +0 -0
  39. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_014.yaml +0 -0
  40. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_015.yaml +0 -0
  41. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_016.yaml +0 -0
  42. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_017.yaml +0 -0
  43. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_018.yaml +0 -0
  44. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_019.yaml +0 -0
  45. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_020.yaml +0 -0
  46. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_021.yaml +0 -0
  47. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_022.yaml +0 -0
  48. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_023.yaml +0 -0
  49. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_024.yaml +0 -0
  50. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_025.yaml +0 -0
  51. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_026.yaml +0 -0
  52. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_027.yaml +0 -0
  53. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_028.yaml +0 -0
  54. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_029.yaml +0 -0
  55. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_030.yaml +0 -0
  56. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_031.yaml +0 -0
  57. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_032.yaml +0 -0
  58. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_033.yaml +0 -0
  59. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_034.yaml +0 -0
  60. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_035.yaml +0 -0
  61. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_036.yaml +0 -0
  62. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_037.yaml +0 -0
  63. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_038.yaml +0 -0
  64. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_039.yaml +0 -0
  65. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/alpha101/alpha_040.yaml +0 -0
  66. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/base.py +0 -0
  67. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/compute.py +0 -0
  68. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/loader.py +0 -0
  69. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/pipeline.py +0 -0
  70. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/ranking.py +0 -0
  71. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/ranking_executor.py +0 -0
  72. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/screening.py +0 -0
  73. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/factors/screening_executor.py +0 -0
  74. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/models/bar.py +0 -0
  75. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/parser/__init__.py +0 -0
  76. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/parser/constants.py +0 -0
  77. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/parser/formula.py +0 -0
  78. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/__init__.py +0 -0
  79. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/env.py +0 -0
  80. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/logger.py +0 -0
  81. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/path.py +0 -0
  82. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/symbol_utils.py +0 -0
  83. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/time.py +0 -0
  84. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli/utils/validate.py +0 -0
  85. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli.egg-info/SOURCES.txt +0 -0
  86. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli.egg-info/dependency_links.txt +0 -0
  87. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli.egg-info/entry_points.txt +0 -0
  88. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli.egg-info/requires.txt +0 -0
  89. {quantcli-0.1.13 → quantcli-0.1.14}/quantcli.egg-info/top_level.txt +0 -0
  90. {quantcli-0.1.13 → quantcli-0.1.14}/setup.cfg +0 -0
  91. {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_akshare_integration.py +0 -0
  92. {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_builtin_factors.py +0 -0
  93. {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_cli.py +0 -0
  94. {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_datasources.py +0 -0
  95. {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_factors.py +0 -0
  96. {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_gm_executors.py +0 -0
  97. {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_mixed_datasource.py +0 -0
  98. {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_multi_factor.py +0 -0
  99. {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_pipeline_integration.py +0 -0
  100. {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_symbol_utils.py +0 -0
  101. {quantcli-0.1.13 → quantcli-0.1.14}/tests/test_time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantcli
3
- Version: 0.1.13
3
+ Version: 0.1.14
4
4
  Summary: 面向AI的多因子量化选股策略挖掘工具,AI Agent 友好 CLI
5
5
  Author-email: QuantCLI Team <quantcli@example.com>
6
6
  Project-URL: repository, https://github.com/wumu2013/quantcli
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "quantcli"
7
- version = "0.1.13"
7
+ version = "0.1.14"
8
8
  description = "面向AI的多因子量化选股策略挖掘工具,AI Agent 友好 CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -174,7 +174,7 @@ def data_fetch(ctx, symbol, start, end, source, use_cache, output):
174
174
 
175
175
  @data.group("cache")
176
176
  @click.pass_context
177
- def data_cache():
177
+ def data_cache(ctx):
178
178
  """缓存管理"""
179
179
  pass
180
180
 
@@ -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"idx_{symbol}_{start_str}_{end_str}"
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(columns=['date', 'open', 'high', 'low', 'close', 'volume'])
167
+ return pd.DataFrame(
168
+ columns=["date", "open", "high", "low", "close", "volume"]
169
+ )
180
170
 
181
171
  # 标准化列名
182
- df = df.rename(columns={
183
- "时间": "date",
184
- "开盘": "open",
185
- "最高": "high",
186
- "最低": "low",
187
- "收盘": "close",
188
- "成交量": "volume",
189
- "成交额": "amount"
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 = ['open', 'high', 'low', 'close', 'volume', 'amount']
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='coerce')
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(columns=['date', 'open', 'high', 'low', 'close', 'volume'])
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=['symbol', 'name', 'exchange', 'market'])
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
- self,
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, '_bs'):
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 != '0':
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 = symbol.replace(".SH", "").replace(".SZ", "").replace("SH", "").replace("SZ", "")
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 != '0':
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 != '0':
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(StockInfo(
192
- symbol=symbol,
193
- name=name,
194
- exchange=exchange,
195
- market=market_type,
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("Baostock 不提供交易日历,请使用 akshare 的 tool_trade_date_hist_sina")
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 == '0':
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 != '0':
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
- "symbol": symbol,
264
- "year": year,
265
- "quarter": quarter,
266
- "roe": float(row[3]) if row[3] else None,
267
- "netprofitmargin": float(row[7]) if row[7] else None,
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"dupont_{symbol}_{start_year}_{end_year}"
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 != '0':
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
- "symbol": symbol,
324
- "pub_date": row[1],
325
- "stat_date": row[2],
326
- "roe": float(row[3]) if row[3] else None,
327
- "asset_equity_ratio": float(row[4]) if row[4] else None,
328
- "asset_turnover": float(row[5]) if row[5] else None,
329
- "net_profit_margin": float(row[6]) if row[6] else None,
330
- "gross_profit_margin": float(row[7]) if row[7] else None,
331
- "tax_burden": float(row[8]) if row[8] else None,
332
- "interest_burden": float(row[9]) if row[9] else None,
333
- "ebit_to_nprofit": float(row[10]) if row[10] else None,
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"profit_{symbol}_{start_year}_{end_year}"
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 != '0':
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
- "symbol": symbol,
386
- "pub_date": row[1],
387
- "net_profits": float(row[2]) if row[2] else None,
388
- "net_profits_yr": float(row[3]) if row[3] else None,
389
- "dt_net_profits": float(row[4]) if row[4] else None,
390
- "total_revenue": float(row[5]) if row[5] else None,
391
- "revenue_yr": float(row[6]) if row[6] else None,
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"growth_{symbol}_{start_year}_{end_year}"
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 != '0':
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
- "symbol": symbol,
442
- "pub_date": row[1],
443
- "net_profits_growth": float(row[2]) if row[2] else None,
444
- "revenue_growth": float(row[3]) if row[3] else None,
445
- "total_assets_growth": float(row[4]) if row[4] else None,
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
  # 批量下载杜邦分析数据
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantcli
3
- Version: 0.1.13
3
+ Version: 0.1.14
4
4
  Summary: 面向AI的多因子量化选股策略挖掘工具,AI Agent 友好 CLI
5
5
  Author-email: QuantCLI Team <quantcli@example.com>
6
6
  Project-URL: repository, https://github.com/wumu2013/quantcli
File without changes
File without changes
File without changes
File without changes
File without changes