quantcli 0.1.13__tar.gz → 0.1.15__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.15}/PKG-INFO +1 -1
  2. {quantcli-0.1.13 → quantcli-0.1.15}/pyproject.toml +1 -1
  3. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/cli.py +1 -1
  4. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/core/data.py +80 -48
  5. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/akshare.py +36 -61
  6. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/baostock.py +102 -97
  7. {quantcli-0.1.13 → quantcli-0.1.15/quantcli.egg-info}/PKG-INFO +1 -1
  8. {quantcli-0.1.13 → quantcli-0.1.15}/LICENSE +0 -0
  9. {quantcli-0.1.13 → quantcli-0.1.15}/README.md +0 -0
  10. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/core/__init__.py +0 -0
  11. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/core/backtest.py +0 -0
  12. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/core/factor.py +0 -0
  13. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/__init__.py +0 -0
  14. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/base.py +0 -0
  15. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/cache.py +0 -0
  16. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/fundamentals/__init__.py +0 -0
  17. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/fundamentals/provider.py +0 -0
  18. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/mixed.py +0 -0
  19. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/mysql.py +0 -0
  20. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/sync/__init__.py +0 -0
  21. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/sync/akshare.py +0 -0
  22. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/sync/base.py +0 -0
  23. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/sync/gm.py +0 -0
  24. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/datasources/sync/gm_fundamental.py +0 -0
  25. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/__init__.py +0 -0
  26. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_001.yaml +0 -0
  27. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_002.yaml +0 -0
  28. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_003.yaml +0 -0
  29. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_004.yaml +0 -0
  30. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_005.yaml +0 -0
  31. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_006.yaml +0 -0
  32. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_007.yaml +0 -0
  33. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_008.yaml +0 -0
  34. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_009.yaml +0 -0
  35. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_010.yaml +0 -0
  36. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_011.yaml +0 -0
  37. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_012.yaml +0 -0
  38. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_013.yaml +0 -0
  39. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_014.yaml +0 -0
  40. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_015.yaml +0 -0
  41. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_016.yaml +0 -0
  42. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_017.yaml +0 -0
  43. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_018.yaml +0 -0
  44. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_019.yaml +0 -0
  45. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_020.yaml +0 -0
  46. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_021.yaml +0 -0
  47. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_022.yaml +0 -0
  48. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_023.yaml +0 -0
  49. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_024.yaml +0 -0
  50. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_025.yaml +0 -0
  51. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_026.yaml +0 -0
  52. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_027.yaml +0 -0
  53. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_028.yaml +0 -0
  54. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_029.yaml +0 -0
  55. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_030.yaml +0 -0
  56. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_031.yaml +0 -0
  57. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_032.yaml +0 -0
  58. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_033.yaml +0 -0
  59. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_034.yaml +0 -0
  60. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_035.yaml +0 -0
  61. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_036.yaml +0 -0
  62. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_037.yaml +0 -0
  63. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_038.yaml +0 -0
  64. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_039.yaml +0 -0
  65. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/alpha101/alpha_040.yaml +0 -0
  66. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/base.py +0 -0
  67. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/compute.py +0 -0
  68. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/loader.py +0 -0
  69. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/pipeline.py +0 -0
  70. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/ranking.py +0 -0
  71. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/ranking_executor.py +0 -0
  72. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/screening.py +0 -0
  73. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/factors/screening_executor.py +0 -0
  74. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/models/bar.py +0 -0
  75. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/parser/__init__.py +0 -0
  76. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/parser/constants.py +0 -0
  77. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/parser/formula.py +0 -0
  78. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/utils/__init__.py +0 -0
  79. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/utils/env.py +0 -0
  80. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/utils/logger.py +0 -0
  81. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/utils/path.py +0 -0
  82. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/utils/symbol_utils.py +0 -0
  83. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/utils/time.py +0 -0
  84. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli/utils/validate.py +0 -0
  85. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli.egg-info/SOURCES.txt +0 -0
  86. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli.egg-info/dependency_links.txt +0 -0
  87. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli.egg-info/entry_points.txt +0 -0
  88. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli.egg-info/requires.txt +0 -0
  89. {quantcli-0.1.13 → quantcli-0.1.15}/quantcli.egg-info/top_level.txt +0 -0
  90. {quantcli-0.1.13 → quantcli-0.1.15}/setup.cfg +0 -0
  91. {quantcli-0.1.13 → quantcli-0.1.15}/tests/test_akshare_integration.py +0 -0
  92. {quantcli-0.1.13 → quantcli-0.1.15}/tests/test_builtin_factors.py +0 -0
  93. {quantcli-0.1.13 → quantcli-0.1.15}/tests/test_cli.py +0 -0
  94. {quantcli-0.1.13 → quantcli-0.1.15}/tests/test_datasources.py +0 -0
  95. {quantcli-0.1.13 → quantcli-0.1.15}/tests/test_factors.py +0 -0
  96. {quantcli-0.1.13 → quantcli-0.1.15}/tests/test_gm_executors.py +0 -0
  97. {quantcli-0.1.13 → quantcli-0.1.15}/tests/test_mixed_datasource.py +0 -0
  98. {quantcli-0.1.13 → quantcli-0.1.15}/tests/test_multi_factor.py +0 -0
  99. {quantcli-0.1.13 → quantcli-0.1.15}/tests/test_pipeline_integration.py +0 -0
  100. {quantcli-0.1.13 → quantcli-0.1.15}/tests/test_symbol_utils.py +0 -0
  101. {quantcli-0.1.13 → quantcli-0.1.15}/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.15
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.15"
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
 
@@ -21,10 +21,7 @@ from pathlib import Path
21
21
  import pandas as pd
22
22
 
23
23
  from ..datasources import create_datasource, DataSource, StockInfo
24
- from ..utils import (
25
- get_logger, project_root, ensure_dir, format_size,
26
- parse_date, today
27
- )
24
+ from ..utils import get_logger, project_root, ensure_dir, format_size, parse_date, today
28
25
 
29
26
  logger = get_logger(__name__)
30
27
 
@@ -32,6 +29,7 @@ logger = get_logger(__name__)
32
29
  @dataclass
33
30
  class DataConfig:
34
31
  """数据管理配置"""
32
+
35
33
  source: str = "akshare"
36
34
  cache_dir: str = "./data"
37
35
  parallel: int = 4
@@ -54,6 +52,7 @@ class DataConfig:
54
52
  @dataclass
55
53
  class DataQualityReport:
56
54
  """数据质量报告"""
55
+
57
56
  total_rows: int
58
57
  null_count: Dict[str, int]
59
58
  outlier_count: Dict[str, int]
@@ -113,12 +112,7 @@ class DataManager:
113
112
  # =============================================================================
114
113
 
115
114
  def get_daily(
116
- self,
117
- symbol: str,
118
- start_date,
119
- end_date,
120
- use_cache: bool = True,
121
- **kwargs
115
+ self, symbol: str, start_date, end_date, use_cache: bool = True, **kwargs
122
116
  ) -> pd.DataFrame:
123
117
  """获取单只股票日线数据"""
124
118
  cache_file = self.cache_dir / "raw" / "stock_daily" / f"{symbol}.parquet"
@@ -151,7 +145,7 @@ class DataManager:
151
145
  start_date,
152
146
  end_date,
153
147
  use_cache: bool = True,
154
- parallel: bool = False
148
+ parallel: bool = False,
155
149
  ) -> pd.DataFrame:
156
150
  """批量获取多只股票日线数据"""
157
151
  if not symbols:
@@ -161,6 +155,7 @@ class DataManager:
161
155
 
162
156
  if parallel and len(symbols) > 1:
163
157
  from concurrent.futures import ThreadPoolExecutor
158
+
164
159
  with ThreadPoolExecutor(max_workers=self.config.parallel) as executor:
165
160
  futures = [
166
161
  executor.submit(self.get_daily, s, start_date, end_date, use_cache)
@@ -192,10 +187,7 @@ class DataManager:
192
187
  return result
193
188
 
194
189
  def get_universe(
195
- self,
196
- universe: str = "all",
197
- market: str = "all",
198
- use_cache: bool = True
190
+ self, universe: str = "all", market: str = "all", use_cache: bool = True
199
191
  ) -> List[StockInfo]:
200
192
  """获取股票池"""
201
193
  cache_file = self.cache_dir / "cache" / f"stock_list_{market}.json"
@@ -221,9 +213,7 @@ class DataManager:
221
213
  return stocks
222
214
 
223
215
  def get_trading_calendar(
224
- self,
225
- exchange: str = "SSE",
226
- use_cache: bool = True
216
+ self, exchange: str = "SSE", use_cache: bool = True
227
217
  ) -> List:
228
218
  """获取交易日历"""
229
219
  cache_file = self.cache_dir / "cache" / f"trading_calendar_{exchange}.json"
@@ -244,11 +234,7 @@ class DataManager:
244
234
  return trading_days
245
235
 
246
236
  def get_index_daily(
247
- self,
248
- symbol: str,
249
- start_date,
250
- end_date,
251
- use_cache: bool = True
237
+ self, symbol: str, start_date, end_date, use_cache: bool = True
252
238
  ) -> pd.DataFrame:
253
239
  """获取指数日线数据"""
254
240
  cache_file = self.cache_dir / "raw" / "index_daily" / f"{symbol}.parquet"
@@ -268,7 +254,9 @@ class DataManager:
268
254
 
269
255
  return df
270
256
 
271
- def _filter_universe(self, stocks: List[StockInfo], universe: str) -> List[StockInfo]:
257
+ def _filter_universe(
258
+ self, stocks: List[StockInfo], universe: str
259
+ ) -> List[StockInfo]:
272
260
  """根据股票池名称过滤股票"""
273
261
  return stocks
274
262
 
@@ -303,8 +291,10 @@ class DataManager:
303
291
  try:
304
292
  existing = pd.read_parquet(cache_file)
305
293
  df_copy = pd.concat([existing, df_copy]).drop_duplicates(
306
- subset=["date"] if "symbol" not in df_copy.columns else ["date", "symbol"],
307
- keep="last"
294
+ subset=["date"]
295
+ if "symbol" not in df_copy.columns
296
+ else ["date", "symbol"],
297
+ keep="last",
308
298
  )
309
299
  except Exception:
310
300
  pass
@@ -313,35 +303,62 @@ class DataManager:
313
303
 
314
304
  self._cache_metadata[cache_key] = {
315
305
  "last_updated": datetime.now().isoformat(),
316
- "rows": len(df_copy)
306
+ "rows": len(df_copy),
317
307
  }
318
308
  self._save_metadata()
319
309
 
320
310
  logger.debug(f"Cached {len(df_copy)} rows to {cache_file}")
321
311
 
322
- def clear_cache(self, older_than: Optional[int] = None, pattern: Optional[str] = None) -> int:
312
+ def clear_cache(
313
+ self, older_than: Optional[int] = None, pattern: Optional[str] = None
314
+ ) -> int:
323
315
  """清理缓存"""
324
316
  count = 0
325
- raw_dir = self.cache_dir / "raw"
317
+
318
+ # 清理所有缓存子目录
319
+ cache_subdirs = ["raw", "prices", "stocklist", "calendar"]
326
320
 
327
321
  if older_than:
328
322
  from datetime import timedelta
323
+
329
324
  cutoff = today() - timedelta(days=older_than)
330
- for cache_file in raw_dir.rglob("*.parquet"):
331
- mtime = parse_date(cache_file.stat().st_mtime)
332
- if mtime < cutoff:
333
- cache_file.unlink()
334
- count += 1
325
+ for subdir in cache_subdirs:
326
+ dir_path = self.cache_dir / subdir
327
+ if dir_path.exists():
328
+ for cache_file in dir_path.rglob("*.parquet"):
329
+ try:
330
+ mtime = parse_date(cache_file.stat().st_mtime)
331
+ if mtime < cutoff:
332
+ cache_file.unlink()
333
+ count += 1
334
+ except OSError:
335
+ continue
335
336
  elif pattern:
336
- for cache_file in raw_dir.rglob(pattern):
337
- if cache_file.is_file():
338
- cache_file.unlink()
339
- count += 1
337
+ for subdir in cache_subdirs:
338
+ dir_path = self.cache_dir / subdir
339
+ if dir_path.exists():
340
+ for cache_file in dir_path.rglob(pattern):
341
+ if cache_file.is_file():
342
+ try:
343
+ cache_file.unlink()
344
+ count += 1
345
+ except OSError:
346
+ continue
340
347
  else:
341
- for cache_file in raw_dir.rglob("*.parquet"):
342
- cache_file.unlink()
343
- count += 1
344
- ensure_dir(self.cache_dir / "features")
348
+ # 清理所有缓存目录
349
+ for subdir in cache_subdirs:
350
+ dir_path = self.cache_dir / subdir
351
+ if dir_path.exists():
352
+ for cache_file in dir_path.rglob("*.parquet"):
353
+ try:
354
+ cache_file.unlink()
355
+ count += 1
356
+ except OSError:
357
+ continue
358
+
359
+ # 确保目录结构存在
360
+ for subdir in cache_subdirs:
361
+ ensure_dir(self.cache_dir / subdir)
345
362
 
346
363
  logger.info(f"Cleared {count} cache files")
347
364
  return count
@@ -354,7 +371,13 @@ class DataManager:
354
371
  relative = str(path.relative_to(self.cache_dir))
355
372
  sizes[relative] = format_size(path.stat().st_size)
356
373
 
357
- sizes["_total"] = format_size(sum(path.stat().st_size for path in self.cache_dir.rglob("*.parquet") if path.is_file()))
374
+ sizes["_total"] = format_size(
375
+ sum(
376
+ path.stat().st_size
377
+ for path in self.cache_dir.rglob("*.parquet")
378
+ if path.is_file()
379
+ )
380
+ )
358
381
  return sizes
359
382
 
360
383
  # =============================================================================
@@ -369,7 +392,7 @@ class DataManager:
369
392
  filter_st: Optional[bool] = None,
370
393
  filter_suspended: Optional[bool] = None,
371
394
  filter_new: Optional[int] = None,
372
- inplace: bool = False
395
+ inplace: bool = False,
373
396
  ) -> pd.DataFrame:
374
397
  """数据清洗"""
375
398
  if not inplace:
@@ -381,7 +404,7 @@ class DataManager:
381
404
  outlier_count={},
382
405
  st_filtered=0,
383
406
  suspended_filtered=0,
384
- new_stock_filtered=0
407
+ new_stock_filtered=0,
385
408
  )
386
409
 
387
410
  # 空值填充
@@ -404,14 +427,20 @@ class DataManager:
404
427
  report.st_filtered = original_len - len(df)
405
428
 
406
429
  # 过滤停牌
407
- filter_suspended_flag = filter_suspended if filter_suspended is not None else self.config.filter_suspended
430
+ filter_suspended_flag = (
431
+ filter_suspended
432
+ if filter_suspended is not None
433
+ else self.config.filter_suspended
434
+ )
408
435
  if filter_suspended_flag:
409
436
  original_len = len(df)
410
437
  df = self._filter_suspended(df)
411
438
  report.suspended_filtered = original_len - len(df)
412
439
 
413
440
  # 过滤新股
414
- filter_new_days = filter_new if filter_new is not None else self.config.filter_new
441
+ filter_new_days = (
442
+ filter_new if filter_new is not None else self.config.filter_new
443
+ )
415
444
  if filter_new_days and filter_new_days > 0:
416
445
  original_len = len(df)
417
446
  df = self._filter_new_stocks(df, filter_new_days)
@@ -506,7 +535,10 @@ class DataManager:
506
535
  return {
507
536
  **status,
508
537
  "cache": self.get_cache_size(),
509
- "config": {"source": self.config.source, "cache_dir": str(self.cache_dir)}
538
+ "config": {
539
+ "source": self.config.source,
540
+ "cache_dir": str(self.cache_dir),
541
+ },
510
542
  }
511
543
  except Exception as e:
512
544
  return {"status": "error", "error": str(e)}
@@ -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.15
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