quantcli 0.1.17__tar.gz → 0.2.1__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 (102) hide show
  1. {quantcli-0.1.17/quantcli.egg-info → quantcli-0.2.1}/PKG-INFO +1 -1
  2. {quantcli-0.1.17 → quantcli-0.2.1}/pyproject.toml +1 -1
  3. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/core/data.py +15 -7
  4. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/akshare.py +30 -1
  5. {quantcli-0.1.17 → quantcli-0.2.1/quantcli.egg-info}/PKG-INFO +1 -1
  6. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli.egg-info/SOURCES.txt +1 -0
  7. {quantcli-0.1.17 → quantcli-0.2.1}/tests/test_cli.py +188 -122
  8. quantcli-0.2.1/tests/test_data_manager.py +270 -0
  9. {quantcli-0.1.17 → quantcli-0.2.1}/LICENSE +0 -0
  10. {quantcli-0.1.17 → quantcli-0.2.1}/README.md +0 -0
  11. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/cli.py +0 -0
  12. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/core/__init__.py +0 -0
  13. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/core/backtest.py +0 -0
  14. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/core/factor.py +0 -0
  15. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/__init__.py +0 -0
  16. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/baostock.py +0 -0
  17. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/base.py +0 -0
  18. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/cache.py +0 -0
  19. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/fundamentals/__init__.py +0 -0
  20. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/fundamentals/provider.py +0 -0
  21. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/mixed.py +0 -0
  22. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/mysql.py +0 -0
  23. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/sync/__init__.py +0 -0
  24. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/sync/akshare.py +0 -0
  25. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/sync/base.py +0 -0
  26. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/sync/gm.py +0 -0
  27. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/datasources/sync/gm_fundamental.py +0 -0
  28. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/__init__.py +0 -0
  29. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_001.yaml +0 -0
  30. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_002.yaml +0 -0
  31. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_003.yaml +0 -0
  32. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_004.yaml +0 -0
  33. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_005.yaml +0 -0
  34. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_006.yaml +0 -0
  35. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_007.yaml +0 -0
  36. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_008.yaml +0 -0
  37. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_009.yaml +0 -0
  38. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_010.yaml +0 -0
  39. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_011.yaml +0 -0
  40. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_012.yaml +0 -0
  41. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_013.yaml +0 -0
  42. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_014.yaml +0 -0
  43. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_015.yaml +0 -0
  44. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_016.yaml +0 -0
  45. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_017.yaml +0 -0
  46. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_018.yaml +0 -0
  47. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_019.yaml +0 -0
  48. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_020.yaml +0 -0
  49. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_021.yaml +0 -0
  50. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_022.yaml +0 -0
  51. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_023.yaml +0 -0
  52. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_024.yaml +0 -0
  53. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_025.yaml +0 -0
  54. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_026.yaml +0 -0
  55. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_027.yaml +0 -0
  56. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_028.yaml +0 -0
  57. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_029.yaml +0 -0
  58. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_030.yaml +0 -0
  59. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_031.yaml +0 -0
  60. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_032.yaml +0 -0
  61. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_033.yaml +0 -0
  62. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_034.yaml +0 -0
  63. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_035.yaml +0 -0
  64. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_036.yaml +0 -0
  65. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_037.yaml +0 -0
  66. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_038.yaml +0 -0
  67. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_039.yaml +0 -0
  68. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/alpha101/alpha_040.yaml +0 -0
  69. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/base.py +0 -0
  70. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/compute.py +0 -0
  71. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/loader.py +0 -0
  72. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/pipeline.py +0 -0
  73. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/ranking.py +0 -0
  74. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/ranking_executor.py +0 -0
  75. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/screening.py +0 -0
  76. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/factors/screening_executor.py +0 -0
  77. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/models/bar.py +0 -0
  78. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/parser/__init__.py +0 -0
  79. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/parser/constants.py +0 -0
  80. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/parser/formula.py +0 -0
  81. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/utils/__init__.py +0 -0
  82. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/utils/env.py +0 -0
  83. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/utils/logger.py +0 -0
  84. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/utils/path.py +0 -0
  85. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/utils/symbol_utils.py +0 -0
  86. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/utils/time.py +0 -0
  87. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli/utils/validate.py +0 -0
  88. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli.egg-info/dependency_links.txt +0 -0
  89. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli.egg-info/entry_points.txt +0 -0
  90. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli.egg-info/requires.txt +0 -0
  91. {quantcli-0.1.17 → quantcli-0.2.1}/quantcli.egg-info/top_level.txt +0 -0
  92. {quantcli-0.1.17 → quantcli-0.2.1}/setup.cfg +0 -0
  93. {quantcli-0.1.17 → quantcli-0.2.1}/tests/test_akshare_integration.py +0 -0
  94. {quantcli-0.1.17 → quantcli-0.2.1}/tests/test_builtin_factors.py +0 -0
  95. {quantcli-0.1.17 → quantcli-0.2.1}/tests/test_datasources.py +0 -0
  96. {quantcli-0.1.17 → quantcli-0.2.1}/tests/test_factors.py +0 -0
  97. {quantcli-0.1.17 → quantcli-0.2.1}/tests/test_gm_executors.py +0 -0
  98. {quantcli-0.1.17 → quantcli-0.2.1}/tests/test_mixed_datasource.py +0 -0
  99. {quantcli-0.1.17 → quantcli-0.2.1}/tests/test_multi_factor.py +0 -0
  100. {quantcli-0.1.17 → quantcli-0.2.1}/tests/test_pipeline_integration.py +0 -0
  101. {quantcli-0.1.17 → quantcli-0.2.1}/tests/test_symbol_utils.py +0 -0
  102. {quantcli-0.1.17 → quantcli-0.2.1}/tests/test_time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantcli
3
- Version: 0.1.17
3
+ Version: 0.2.1
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.17"
7
+ version = "0.2.1"
8
8
  description = "面向AI的多因子量化选股策略挖掘工具,AI Agent 友好 CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -117,16 +117,24 @@ class DataManager:
117
117
  """获取单只股票日线数据"""
118
118
  cache_file = self.cache_dir / "raw" / "stock_daily" / f"{symbol}.parquet"
119
119
 
120
- # 检查缓存
121
- if use_cache and cache_file.exists():
120
+ # 尝试获取交易日历,计算预期交易日数
121
+ expected_count = 0
122
+ has_calendar = False
123
+ try:
124
+ trading_days = self.get_trading_calendar()
125
+ expected_count = sum(1 for d in trading_days if start_date <= d <= end_date)
126
+ has_calendar = True
127
+ except Exception:
128
+ pass
129
+
130
+ # 检查缓存完整性
131
+ if use_cache and cache_file.exists() and has_calendar:
122
132
  cached = self._load_cached_range(cache_file)
123
- if cached is not None:
133
+ if cached is not None and len(cached) >= expected_count:
124
134
  mask = (cached["date"] >= start_date) & (cached["date"] <= end_date)
125
- result = cached[mask].copy()
126
- if not result.empty:
127
- return result
135
+ return cached[mask].copy()
128
136
 
129
- # 从数据源获取
137
+ # 全量拉取
130
138
  df = self.datasource.get_daily(symbol, start_date, end_date, **kwargs)
131
139
 
132
140
  if df.empty:
@@ -235,13 +235,16 @@ class AkshareDataSource(DataSource):
235
235
  """获取交易日历"""
236
236
  if self.config.use_cache:
237
237
  cached = self._calendar_cache.get_calendar(exchange)
238
- if cached is not None:
238
+ if cached is not None and self._validate_calendar(cached):
239
239
  return sorted(pd.to_datetime(cached["trade_date"]).dt.date.tolist())
240
240
 
241
241
  df = self._ak.tool_trade_date_hist_sina()
242
242
  if df.empty:
243
243
  return []
244
244
 
245
+ if not self._validate_calendar(df):
246
+ return []
247
+
245
248
  trading_dates = pd.to_datetime(df["trade_date"]).dt.date.tolist()
246
249
 
247
250
  if self.config.use_cache:
@@ -249,6 +252,32 @@ class AkshareDataSource(DataSource):
249
252
 
250
253
  return sorted(trading_dates)
251
254
 
255
+ def _validate_calendar(self, df: pd.DataFrame) -> bool:
256
+ """验证交易日历是否完整"""
257
+ if df.empty:
258
+ return False
259
+
260
+ dates = pd.to_datetime(df["trade_date"])
261
+ min_date = dates.min()
262
+ max_date = dates.max()
263
+
264
+ # 检查日期范围(默认应该覆盖近1年)
265
+ from datetime import timedelta
266
+
267
+ one_year_ago = pd.Timestamp.now() - timedelta(days=365)
268
+
269
+ if min_date > one_year_ago:
270
+ return False
271
+
272
+ # 检查数量(1年应该有约250个交易日)
273
+ expected_min = 200
274
+ actual = len(dates)
275
+
276
+ if actual < expected_min:
277
+ return False
278
+
279
+ return True
280
+
252
281
  # ==================== 辅助方法 ====================
253
282
 
254
283
  def _filter_fields(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantcli
3
- Version: 0.1.17
3
+ Version: 0.2.1
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
@@ -89,6 +89,7 @@ quantcli/utils/validate.py
89
89
  tests/test_akshare_integration.py
90
90
  tests/test_builtin_factors.py
91
91
  tests/test_cli.py
92
+ tests/test_data_manager.py
92
93
  tests/test_datasources.py
93
94
  tests/test_factors.py
94
95
  tests/test_gm_executors.py
@@ -31,15 +31,17 @@ def mock_price_data():
31
31
  dates = pd.date_range(start=start_date, periods=100, freq="B")
32
32
 
33
33
  close = 100 + np.cumsum(np.random.randn(100) * 0.5)
34
- return pd.DataFrame({
35
- "date": dates,
36
- "symbol": ["600519"] * 100,
37
- "open": close * (1 + np.random.randn(100) * 0.005),
38
- "high": close * (1 + np.abs(np.random.randn(100) * 0.01)),
39
- "low": close * (1 - np.abs(np.random.randn(100) * 0.01)),
40
- "close": close,
41
- "volume": np.random.randint(1000000, 10000000, 100),
42
- })
34
+ return pd.DataFrame(
35
+ {
36
+ "date": dates,
37
+ "symbol": ["600519"] * 100,
38
+ "open": close * (1 + np.random.randn(100) * 0.005),
39
+ "high": close * (1 + np.abs(np.random.randn(100) * 0.01)),
40
+ "low": close * (1 - np.abs(np.random.randn(100) * 0.01)),
41
+ "close": close,
42
+ "volume": np.random.randint(1000000, 10000000, 100),
43
+ }
44
+ )
43
45
 
44
46
 
45
47
  @pytest.fixture
@@ -58,17 +60,27 @@ class TestQuant3Main:
58
60
  result = runner.invoke(cli.quantcli, ["--help"])
59
61
  assert result.exit_code == 0
60
62
  assert "QuantCLI" in result.output
61
- assert "quantitative" in result.output.lower() or "quant" in result.output.lower()
63
+ assert (
64
+ "quantitative" in result.output.lower() or "quant" in result.output.lower()
65
+ )
62
66
 
63
67
  def test_version(self, runner):
64
68
  """Test quantcli --version"""
65
69
  result = runner.invoke(cli.quantcli, ["--version"])
66
70
  assert result.exit_code == 0
67
- assert "0.1.0" in result.output
71
+ assert "quantcli" in result.output.lower()
72
+ import re
73
+
74
+ match = re.search(r"version (\d+\.\d+\.\d+)", result.output)
75
+ assert match is not None
76
+ version = match.group(1)
77
+ assert re.match(r"\d+\.\d+\.\d+", version)
68
78
 
69
79
  def test_verbose_flag(self, runner):
70
80
  """Test verbose flag sets debug level"""
71
- result = runner.invoke(cli.quantcli, ["--verbose", "data", "health"], catch_exceptions=False)
81
+ result = runner.invoke(
82
+ cli.quantcli, ["--verbose", "data", "health"], catch_exceptions=False
83
+ )
72
84
  # Should execute without error even if verbose mode
73
85
  assert result.exit_code == 0 or "Error" in result.output
74
86
 
@@ -81,8 +93,8 @@ class TestQuant3Main:
81
93
  class TestDataCommands:
82
94
  """Tests for data commands"""
83
95
 
84
- @patch.object(cli.DataManager, '__init__')
85
- @patch.object(cli.DataManager, 'get_daily')
96
+ @patch.object(cli.DataManager, "__init__")
97
+ @patch.object(cli.DataManager, "get_daily")
86
98
  def test_data_fetch_basic(self, mock_get_daily, mock_init, runner, mock_price_data):
87
99
  """Test data fetch command"""
88
100
  mock_init.return_value = None
@@ -93,42 +105,43 @@ class TestDataCommands:
93
105
  assert result.exit_code == 0
94
106
  assert "600519" in result.output
95
107
 
96
- @patch.object(cli.DataManager, '__init__')
97
- @patch.object(cli.DataManager, 'get_daily')
98
- def test_data_fetch_with_dates(self, mock_get_daily, mock_init, runner, mock_price_data):
108
+ @patch.object(cli.DataManager, "__init__")
109
+ @patch.object(cli.DataManager, "get_daily")
110
+ def test_data_fetch_with_dates(
111
+ self, mock_get_daily, mock_init, runner, mock_price_data
112
+ ):
99
113
  """Test data fetch with start and end dates"""
100
114
  mock_init.return_value = None
101
115
  mock_get_daily.return_value = mock_price_data
102
116
 
103
- result = runner.invoke(cli.data_fetch, [
104
- "600519",
105
- "--start", "2023-01-01",
106
- "--end", "2023-06-01"
107
- ])
117
+ result = runner.invoke(
118
+ cli.data_fetch, ["600519", "--start", "2023-01-01", "--end", "2023-06-01"]
119
+ )
108
120
 
109
121
  assert result.exit_code == 0
110
122
  assert "2023-01-01" in result.output or "2023-06-01" in result.output
111
123
 
112
- @patch.object(cli.DataManager, '__init__')
113
- @patch.object(cli.DataManager, 'get_daily')
114
- def test_data_fetch_with_output_csv(self, mock_get_daily, mock_init, runner, mock_price_data, tmp_path):
124
+ @patch.object(cli.DataManager, "__init__")
125
+ @patch.object(cli.DataManager, "get_daily")
126
+ def test_data_fetch_with_output_csv(
127
+ self, mock_get_daily, mock_init, runner, mock_price_data, tmp_path
128
+ ):
115
129
  """Test data fetch with CSV output"""
116
130
  mock_init.return_value = None
117
131
  mock_get_daily.return_value = mock_price_data
118
132
 
119
133
  output_file = tmp_path / "output.csv"
120
134
 
121
- result = runner.invoke(cli.data_fetch, [
122
- "600519",
123
- "--start", "2023-01-01",
124
- "--output", str(output_file)
125
- ])
135
+ result = runner.invoke(
136
+ cli.data_fetch,
137
+ ["600519", "--start", "2023-01-01", "--output", str(output_file)],
138
+ )
126
139
 
127
140
  assert result.exit_code == 0
128
141
  assert "Saved to" in result.output
129
142
 
130
- @patch.object(cli.DataManager, '__init__')
131
- @patch.object(cli.DataManager, 'get_daily')
143
+ @patch.object(cli.DataManager, "__init__")
144
+ @patch.object(cli.DataManager, "get_daily")
132
145
  def test_data_fetch_empty_result(self, mock_get_daily, mock_init, runner):
133
146
  """Test data fetch with no data"""
134
147
  mock_init.return_value = None
@@ -146,15 +159,15 @@ class TestDataCommands:
146
159
  assert result.exit_code != 0
147
160
  assert "Invalid date" in result.output or "Error" in result.output
148
161
 
149
- @patch.object(cli.DataManager, '__init__')
150
- @patch.object(cli.DataManager, 'get_cache_size')
162
+ @patch.object(cli.DataManager, "__init__")
163
+ @patch.object(cli.DataManager, "get_cache_size")
151
164
  def test_data_cache_ls(self, mock_get_cache_size, mock_init, runner):
152
165
  """Test data cache list command"""
153
166
  mock_init.return_value = None
154
167
  mock_get_cache_size.return_value = {
155
168
  "600519.csv": "1.2MB",
156
169
  "000001.csv": "0.8MB",
157
- "_total": "2.0MB"
170
+ "_total": "2.0MB",
158
171
  }
159
172
 
160
173
  result = runner.invoke(cli.data_cache_ls, [])
@@ -162,8 +175,8 @@ class TestDataCommands:
162
175
  assert result.exit_code == 0
163
176
  assert "Cached files" in result.output or "600519" in result.output
164
177
 
165
- @patch.object(cli.DataManager, '__init__')
166
- @patch.object(cli.DataManager, 'clear_cache')
178
+ @patch.object(cli.DataManager, "__init__")
179
+ @patch.object(cli.DataManager, "clear_cache")
167
180
  def test_data_cache_clean(self, mock_clear_cache, mock_init, runner):
168
181
  """Test data cache clean command"""
169
182
  mock_init.return_value = None
@@ -174,15 +187,15 @@ class TestDataCommands:
174
187
  assert result.exit_code == 0
175
188
  assert "5" in result.output or "Cleaned" in result.output
176
189
 
177
- @patch.object(cli.DataManager, '__init__')
178
- @patch.object(cli.DataManager, 'health_check')
190
+ @patch.object(cli.DataManager, "__init__")
191
+ @patch.object(cli.DataManager, "health_check")
179
192
  def test_data_health(self, mock_health_check, mock_init, runner):
180
193
  """Test data health check command"""
181
194
  mock_init.return_value = None
182
195
  mock_health_check.return_value = {
183
196
  "status": "ok",
184
197
  "cache_dir": "/tmp/quantcli_cache",
185
- "source": "akshare"
198
+ "source": "akshare",
186
199
  }
187
200
 
188
201
  result = runner.invoke(cli.data_health, [])
@@ -194,87 +207,117 @@ class TestDataCommands:
194
207
  class TestFactorCommands:
195
208
  """Tests for factor commands"""
196
209
 
197
- @patch.object(cli.DataManager, '__init__')
198
- @patch.object(cli.DataManager, 'get_daily')
210
+ @patch.object(cli.DataManager, "__init__")
211
+ @patch.object(cli.DataManager, "get_daily")
199
212
  def test_factor_run_basic(self, mock_get_daily, mock_init, runner, mock_price_data):
200
213
  """Test factor run command with basic formula"""
201
214
  mock_init.return_value = None
202
215
  mock_get_daily.return_value = mock_price_data
203
216
 
204
- result = runner.invoke(cli.factor_run, [
205
- "--name", "momentum",
206
- "--expr", "(close / delay(close, 20)) - 1",
207
- "--symbol", "600519",
208
- "--start", "2023-01-01"
209
- ])
217
+ result = runner.invoke(
218
+ cli.factor_run,
219
+ [
220
+ "--name",
221
+ "momentum",
222
+ "--expr",
223
+ "(close / delay(close, 20)) - 1",
224
+ "--symbol",
225
+ "600519",
226
+ "--start",
227
+ "2023-01-01",
228
+ ],
229
+ )
210
230
 
211
231
  assert result.exit_code == 0
212
232
  assert "momentum" in result.output or "computed" in result.output.lower()
213
233
 
214
- @patch.object(cli.DataManager, '__init__')
215
- @patch.object(cli.DataManager, 'get_daily')
216
- def test_factor_run_with_ma(self, mock_get_daily, mock_init, runner, mock_price_data):
234
+ @patch.object(cli.DataManager, "__init__")
235
+ @patch.object(cli.DataManager, "get_daily")
236
+ def test_factor_run_with_ma(
237
+ self, mock_get_daily, mock_init, runner, mock_price_data
238
+ ):
217
239
  """Test factor run with moving average formula"""
218
240
  mock_init.return_value = None
219
241
  mock_get_daily.return_value = mock_price_data
220
242
 
221
- result = runner.invoke(cli.factor_run, [
222
- "--name", "ma_20",
223
- "--expr", "ma(close, 20)",
224
- "--symbol", "600519",
225
- "--start", "2023-01-01"
226
- ])
243
+ result = runner.invoke(
244
+ cli.factor_run,
245
+ [
246
+ "--name",
247
+ "ma_20",
248
+ "--expr",
249
+ "ma(close, 20)",
250
+ "--symbol",
251
+ "600519",
252
+ "--start",
253
+ "2023-01-01",
254
+ ],
255
+ )
227
256
 
228
257
  assert result.exit_code == 0
229
258
  assert "ma_20" in result.output or "computed" in result.output.lower()
230
259
 
231
- @patch.object(cli.DataManager, '__init__')
232
- @patch.object(cli.DataManager, 'get_daily')
233
- def test_factor_run_with_output(self, mock_get_daily, mock_init, runner, mock_price_data, tmp_path):
260
+ @patch.object(cli.DataManager, "__init__")
261
+ @patch.object(cli.DataManager, "get_daily")
262
+ def test_factor_run_with_output(
263
+ self, mock_get_daily, mock_init, runner, mock_price_data, tmp_path
264
+ ):
234
265
  """Test factor run with CSV output"""
235
266
  mock_init.return_value = None
236
267
  mock_get_daily.return_value = mock_price_data
237
268
 
238
269
  output_file = tmp_path / "factor_output.csv"
239
270
 
240
- result = runner.invoke(cli.factor_run, [
241
- "--name", "momentum",
242
- "--expr", "close - close",
243
- "--symbol", "600519",
244
- "--start", "2023-01-01",
245
- "--output", str(output_file)
246
- ])
271
+ result = runner.invoke(
272
+ cli.factor_run,
273
+ [
274
+ "--name",
275
+ "momentum",
276
+ "--expr",
277
+ "close - close",
278
+ "--symbol",
279
+ "600519",
280
+ "--start",
281
+ "2023-01-01",
282
+ "--output",
283
+ str(output_file),
284
+ ],
285
+ )
247
286
 
248
287
  assert result.exit_code == 0
249
288
  assert "Saved to" in result.output
250
289
 
251
- @patch.object(cli.DataManager, '__init__')
252
- @patch.object(cli.DataManager, 'get_daily')
253
- @patch.object(cli.FactorEngine, 'evaluate_ic')
254
- def test_factor_eval_ic(self, mock_eval_ic, mock_get_daily, mock_init, runner, mock_price_data):
290
+ @patch.object(cli.DataManager, "__init__")
291
+ @patch.object(cli.DataManager, "get_daily")
292
+ @patch.object(cli.FactorEngine, "evaluate_ic")
293
+ def test_factor_eval_ic(
294
+ self, mock_eval_ic, mock_get_daily, mock_init, runner, mock_price_data
295
+ ):
255
296
  """Test factor eval with IC method"""
256
297
  mock_init.return_value = None
257
298
  mock_get_daily.return_value = mock_price_data
258
299
  mock_eval_ic.return_value = {
259
- "ic_stats": {
260
- "ic_mean": 0.05,
261
- "ic_std": 0.15,
262
- "ic_ir": 0.33
263
- }
300
+ "ic_stats": {"ic_mean": 0.05, "ic_std": 0.15, "ic_ir": 0.33}
264
301
  }
265
302
 
266
- result = runner.invoke(cli.factor_eval, [
267
- "momentum",
268
- "--symbol", "600519",
269
- "--start", "2023-01-01",
270
- "--method", "ic"
271
- ])
303
+ result = runner.invoke(
304
+ cli.factor_eval,
305
+ [
306
+ "momentum",
307
+ "--symbol",
308
+ "600519",
309
+ "--start",
310
+ "2023-01-01",
311
+ "--method",
312
+ "ic",
313
+ ],
314
+ )
272
315
 
273
316
  assert result.exit_code == 0
274
317
  assert "IC" in result.output or "IC Mean" in result.output
275
318
 
276
- @patch.object(cli.DataManager, '__init__')
277
- @patch.object(cli.DataManager, 'get_daily')
319
+ @patch.object(cli.DataManager, "__init__")
320
+ @patch.object(cli.DataManager, "get_daily")
278
321
  def test_factor_list(self, mock_get_daily, mock_init, runner):
279
322
  """Test factor list command"""
280
323
  mock_init.return_value = None
@@ -289,9 +332,11 @@ class TestFactorCommands:
289
332
  class TestBacktestCommands:
290
333
  """Tests for backtest commands"""
291
334
 
292
- @patch.object(cli.DataManager, '__init__')
293
- @patch.object(cli.DataManager, 'get_daily')
294
- def test_backtest_run_ma_cross(self, mock_get_daily, mock_init, runner, mock_price_data):
335
+ @patch.object(cli.DataManager, "__init__")
336
+ @patch.object(cli.DataManager, "get_daily")
337
+ def test_backtest_run_ma_cross(
338
+ self, mock_get_daily, mock_init, runner, mock_price_data
339
+ ):
295
340
  """Test backtest run with built-in MA cross strategy
296
341
 
297
342
  Note: May fail due to existing backtest engine issues with order handling.
@@ -300,39 +345,47 @@ class TestBacktestCommands:
300
345
  mock_get_daily.return_value = mock_price_data
301
346
 
302
347
  # Use built-in strategy
303
- result = runner.invoke(cli.backtest_run, [
304
- "--strategy", "ma_cross",
305
- "--symbol", "600519",
306
- "--start", "2023-01-01"
307
- ])
348
+ result = runner.invoke(
349
+ cli.backtest_run,
350
+ ["--strategy", "ma_cross", "--symbol", "600519", "--start", "2023-01-01"],
351
+ )
308
352
 
309
353
  # Either succeeds or fails with known backtest engine issue
310
354
  # Check that it attempted to run backtest
311
355
  assert "Running backtest" in result.output or "Error" in result.output
312
356
 
313
- @patch.object(cli.DataManager, '__init__')
314
- @patch.object(cli.DataManager, 'get_daily')
315
- def test_backtest_run_with_custom_capital(self, mock_get_daily, mock_init, runner, mock_price_data):
357
+ @patch.object(cli.DataManager, "__init__")
358
+ @patch.object(cli.DataManager, "get_daily")
359
+ def test_backtest_run_with_custom_capital(
360
+ self, mock_get_daily, mock_init, runner, mock_price_data
361
+ ):
316
362
  """Test backtest run with custom capital"""
317
363
  mock_init.return_value = None
318
364
  mock_get_daily.return_value = mock_price_data
319
365
 
320
- result = runner.invoke(cli.backtest_run, [
321
- "--strategy", "ma_cross",
322
- "--symbol", "600519",
323
- "--start", "2023-01-01",
324
- "--capital", "500000"
325
- ])
366
+ result = runner.invoke(
367
+ cli.backtest_run,
368
+ [
369
+ "--strategy",
370
+ "ma_cross",
371
+ "--symbol",
372
+ "600519",
373
+ "--start",
374
+ "2023-01-01",
375
+ "--capital",
376
+ "500000",
377
+ ],
378
+ )
326
379
 
327
380
  # Either succeeds or fails with known backtest engine issue
328
381
  assert "Running backtest" in result.output or "Error" in result.output
329
382
 
330
383
  def test_backtest_run_invalid_strategy(self, runner):
331
384
  """Test backtest run with invalid strategy"""
332
- result = runner.invoke(cli.backtest_run, [
333
- "--strategy", "nonexistent_strategy",
334
- "--symbol", "600519"
335
- ])
385
+ result = runner.invoke(
386
+ cli.backtest_run,
387
+ ["--strategy", "nonexistent_strategy", "--symbol", "600519"],
388
+ )
336
389
 
337
390
  assert result.exit_code != 0
338
391
  assert "Error" in result.output or "Unknown strategy" in result.output
@@ -348,7 +401,7 @@ class TestBacktestCommands:
348
401
  class TestConfigCommands:
349
402
  """Tests for config commands"""
350
403
 
351
- @patch.object(cli.DataManager, '__init__')
404
+ @patch.object(cli.DataManager, "__init__")
352
405
  def test_config_show(self, mock_init, runner):
353
406
  """Test config show command"""
354
407
  mock_init.return_value = None
@@ -372,20 +425,26 @@ class TestDateType:
372
425
 
373
426
  def test_valid_date(self, runner):
374
427
  """Test valid date format"""
375
- result = runner.invoke(cli.quantcli, ["data", "fetch", "600519", "--start", "2023-12-31"])
428
+ result = runner.invoke(
429
+ cli.quantcli, ["data", "fetch", "600519", "--start", "2023-12-31"]
430
+ )
376
431
  # Should not fail on date parsing
377
432
  assert "Error" not in result.output or result.exit_code == 0
378
433
 
379
434
  def test_invalid_date_format(self, runner):
380
435
  """Test invalid date format"""
381
- result = runner.invoke(cli.quantcli, ["data", "fetch", "600519", "--start", "31-12-2023"])
436
+ result = runner.invoke(
437
+ cli.quantcli, ["data", "fetch", "600519", "--start", "31-12-2023"]
438
+ )
382
439
 
383
440
  assert result.exit_code != 0
384
441
  assert "Invalid date" in result.output or "Error" in result.output
385
442
 
386
443
  def test_invalid_date_value(self, runner):
387
444
  """Test invalid date value"""
388
- result = runner.invoke(cli.quantcli, ["data", "fetch", "600519", "--start", "2023-13-01"])
445
+ result = runner.invoke(
446
+ cli.quantcli, ["data", "fetch", "600519", "--start", "2023-13-01"]
447
+ )
389
448
 
390
449
  assert result.exit_code != 0
391
450
 
@@ -393,19 +452,26 @@ class TestDateType:
393
452
  class TestEdgeCases:
394
453
  """Tests for edge cases and error handling"""
395
454
 
396
- @patch.object(cli.DataManager, '__init__')
397
- @patch.object(cli.DataManager, 'get_daily')
455
+ @patch.object(cli.DataManager, "__init__")
456
+ @patch.object(cli.DataManager, "get_daily")
398
457
  def test_factor_run_empty_data(self, mock_get_daily, mock_init, runner):
399
458
  """Test factor run with empty data"""
400
459
  mock_init.return_value = None
401
460
  mock_get_daily.return_value = pd.DataFrame()
402
461
 
403
- result = runner.invoke(cli.factor_run, [
404
- "--name", "test_factor",
405
- "--expr", "close",
406
- "--symbol", "600519",
407
- "--start", "2023-01-01"
408
- ])
462
+ result = runner.invoke(
463
+ cli.factor_run,
464
+ [
465
+ "--name",
466
+ "test_factor",
467
+ "--expr",
468
+ "close",
469
+ "--symbol",
470
+ "600519",
471
+ "--start",
472
+ "2023-01-01",
473
+ ],
474
+ )
409
475
 
410
476
  assert result.exit_code == 0
411
477
  assert "No data" in result.output
@@ -420,12 +486,12 @@ screening:
420
486
  conditions: []
421
487
  """)
422
488
 
423
- with patch('quantcli.core.backtest.YAMLBacktestEngine.run') as mock_run:
489
+ with patch("quantcli.core.backtest.YAMLBacktestEngine.run") as mock_run:
424
490
  mock_run.side_effect = Exception("No data")
425
- result = runner.invoke(cli.backtest_run, [
426
- "--strategy", str(strategy_file),
427
- "--start", "2023-01-01"
428
- ])
491
+ result = runner.invoke(
492
+ cli.backtest_run,
493
+ ["--strategy", str(strategy_file), "--start", "2023-01-01"],
494
+ )
429
495
 
430
496
  # Should fail due to no data
431
497
  assert result.exit_code != 0