quantcli 0.1.1__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quantcli-0.1.1/quantcli.egg-info → quantcli-0.1.2}/PKG-INFO +2 -2
- {quantcli-0.1.1 → quantcli-0.1.2}/pyproject.toml +2 -2
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/cli.py +103 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/parser/constants.py +1 -1
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/parser/formula.py +7 -2
- {quantcli-0.1.1 → quantcli-0.1.2/quantcli.egg-info}/PKG-INFO +2 -2
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli.egg-info/SOURCES.txt +1 -0
- quantcli-0.1.2/tests/test_builtin_factors.py +799 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/LICENSE +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/README.md +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/core/__init__.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/core/backtest.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/core/data.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/core/factor.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/datasources/__init__.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/datasources/akshare.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/datasources/baostock.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/datasources/base.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/datasources/cache.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/datasources/mixed.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/datasources/mysql.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/datasources/sync/__init__.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/datasources/sync/akshare.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/datasources/sync/base.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/datasources/sync/gm.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/factors/__init__.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/factors/base.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/factors/compute.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/factors/loader.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/factors/pipeline.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/factors/ranking.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/factors/screening.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/parser/__init__.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/utils/__init__.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/utils/logger.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/utils/path.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/utils/time.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli/utils/validate.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli.egg-info/dependency_links.txt +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli.egg-info/entry_points.txt +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli.egg-info/requires.txt +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/quantcli.egg-info/top_level.txt +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/setup.cfg +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/tests/test_akshare_integration.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/tests/test_cli.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/tests/test_datasources.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/tests/test_factors.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/tests/test_mixed_datasource.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/tests/test_multi_factor.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/tests/test_pipeline_integration.py +0 -0
- {quantcli-0.1.1 → quantcli-0.1.2}/tests/test_time.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quantcli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: 面向AI的多因子量化选股策略挖掘工具,AI Agent 友好 CLI
|
|
5
5
|
Author-email: QuantCLI Team <quantcli@example.com>
|
|
6
|
-
Project-URL: repository, https://
|
|
6
|
+
Project-URL: repository, https://github.com/wumu2013/quantcli
|
|
7
7
|
Classifier: Development Status :: 3 - Alpha
|
|
8
8
|
Classifier: Intended Audience :: Developers
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quantcli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "面向AI的多因子量化选股策略挖掘工具,AI Agent 友好 CLI"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
authors = [
|
|
12
12
|
{name = "QuantCLI Team", email = "quantcli@example.com"}
|
|
13
13
|
]
|
|
14
|
-
urls = {repository = "https://
|
|
14
|
+
urls = {repository = "https://github.com/wumu2013/quantcli"}
|
|
15
15
|
classifiers = [
|
|
16
16
|
"Development Status :: 3 - Alpha",
|
|
17
17
|
"Intended Audience :: Developers",
|
|
@@ -1338,6 +1338,109 @@ def config_set(ctx, key, value):
|
|
|
1338
1338
|
click.echo("(Configuration persistence not implemented yet)")
|
|
1339
1339
|
|
|
1340
1340
|
|
|
1341
|
+
# =============================================================================
|
|
1342
|
+
# Expr 命令 - 列出可用表达式
|
|
1343
|
+
# =============================================================================
|
|
1344
|
+
|
|
1345
|
+
@quantcli.group()
|
|
1346
|
+
def expr():
|
|
1347
|
+
"""可用表达式列表"""
|
|
1348
|
+
pass
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
@expr.command("functions")
|
|
1352
|
+
@click.option("--json", is_flag=True, help="Output as JSON")
|
|
1353
|
+
@click.pass_context
|
|
1354
|
+
def expr_functions(ctx, json):
|
|
1355
|
+
"""列出所有内置函数"""
|
|
1356
|
+
from .parser.constants import BUILTIN_FUNCTIONS
|
|
1357
|
+
|
|
1358
|
+
funcs = sorted(BUILTIN_FUNCTIONS)
|
|
1359
|
+
|
|
1360
|
+
if json:
|
|
1361
|
+
import json
|
|
1362
|
+
click.echo(json.dumps({"status": "success", "count": len(funcs), "functions": funcs}, ensure_ascii=False, indent=2))
|
|
1363
|
+
return
|
|
1364
|
+
|
|
1365
|
+
click.echo(f"Built-in Functions ({len(funcs)} total):\n")
|
|
1366
|
+
# 分列显示
|
|
1367
|
+
cols = 4
|
|
1368
|
+
for i in range(0, len(funcs), cols):
|
|
1369
|
+
row = funcs[i:i + cols]
|
|
1370
|
+
click.echo(" " + " ".join(f"{f:<14}" for f in row))
|
|
1371
|
+
|
|
1372
|
+
click.echo("\nUsage examples:")
|
|
1373
|
+
click.echo(" delay(close, 5) # 5日前收盘价")
|
|
1374
|
+
click.echo(" ma(close, 10) # 10日均价")
|
|
1375
|
+
click.echo(" zscore(close) # Z分数标准化")
|
|
1376
|
+
click.echo(" rank(close) # 截面排名 (0-1)")
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
@expr.command("columns")
|
|
1380
|
+
@click.option("--json", is_flag=True, help="Output as JSON")
|
|
1381
|
+
@click.pass_context
|
|
1382
|
+
def expr_columns(ctx, json):
|
|
1383
|
+
"""列出所有可用字段别名"""
|
|
1384
|
+
from .parser.constants import COLUMN_ALIASES
|
|
1385
|
+
|
|
1386
|
+
aliases = list(COLUMN_ALIASES.items())
|
|
1387
|
+
|
|
1388
|
+
if json:
|
|
1389
|
+
import json
|
|
1390
|
+
click.echo(json.dumps({"status": "success", "count": len(aliases), "columns": [{"alias": k, "actual": v} for k, v in aliases]}, ensure_ascii=False, indent=2))
|
|
1391
|
+
return
|
|
1392
|
+
|
|
1393
|
+
click.echo(f"Column Aliases ({len(aliases)} total):\n")
|
|
1394
|
+
click.echo(f"{'Alias':<20} {'Actual Column':<20}")
|
|
1395
|
+
click.echo("-" * 40)
|
|
1396
|
+
for alias, actual in sorted(aliases):
|
|
1397
|
+
click.echo(f"{alias:<20} {actual:<20}")
|
|
1398
|
+
|
|
1399
|
+
click.echo("\nUsage examples:")
|
|
1400
|
+
click.echo(" 'pe < 20' # 市盈率小于20")
|
|
1401
|
+
click.echo(" 'roe > 0.1' # ROE大于10%")
|
|
1402
|
+
click.echo(" 'netprofitmargin > 0.05' # 净利润率大于5%")
|
|
1403
|
+
|
|
1404
|
+
|
|
1405
|
+
@expr.command("list")
|
|
1406
|
+
@click.option("--json", is_flag=True, help="Output as JSON")
|
|
1407
|
+
@click.pass_context
|
|
1408
|
+
def expr_list(ctx, json):
|
|
1409
|
+
"""列出所有可用表达式(函数 + 字段)"""
|
|
1410
|
+
from .parser.constants import BUILTIN_FUNCTIONS, COLUMN_ALIASES
|
|
1411
|
+
|
|
1412
|
+
functions = sorted(BUILTIN_FUNCTIONS)
|
|
1413
|
+
columns = sorted(COLUMN_ALIASES.keys())
|
|
1414
|
+
|
|
1415
|
+
if json:
|
|
1416
|
+
import json
|
|
1417
|
+
click.echo(json.dumps({
|
|
1418
|
+
"status": "success",
|
|
1419
|
+
"functions": {"count": len(functions), "items": functions},
|
|
1420
|
+
"columns": {"count": len(columns), "items": columns}
|
|
1421
|
+
}, ensure_ascii=False, indent=2))
|
|
1422
|
+
return
|
|
1423
|
+
|
|
1424
|
+
click.echo("=" * 50)
|
|
1425
|
+
click.echo("Available Expressions")
|
|
1426
|
+
click.echo("=" * 50)
|
|
1427
|
+
|
|
1428
|
+
click.echo(f"\nFunctions ({len(functions)}):")
|
|
1429
|
+
cols = 4
|
|
1430
|
+
for i in range(0, len(functions), cols):
|
|
1431
|
+
row = functions[i:i + cols]
|
|
1432
|
+
click.echo(" " + " ".join(f"{f:<14}" for f in row))
|
|
1433
|
+
|
|
1434
|
+
click.echo(f"\nColumns ({len(columns)}):")
|
|
1435
|
+
for col in columns:
|
|
1436
|
+
click.echo(f" - {col}")
|
|
1437
|
+
|
|
1438
|
+
click.echo("\n" + "=" * 50)
|
|
1439
|
+
click.echo("Use 'quantcli expr functions' for detailed function list")
|
|
1440
|
+
click.echo("Use 'quantcli expr columns' for detailed column list")
|
|
1441
|
+
click.echo("=" * 50)
|
|
1442
|
+
|
|
1443
|
+
|
|
1341
1444
|
# =============================================================================
|
|
1342
1445
|
# 辅助函数
|
|
1343
1446
|
# =============================================================================
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Builtin functions used in formula expressions
|
|
4
4
|
BUILTIN_FUNCTIONS = frozenset({
|
|
5
5
|
'ma', 'ema', 'delay', 'zscore', 'rank', 'where', 'if',
|
|
6
|
-
'abs', 'sign', 'clamp', 'rolling_std', 'rolling_sum',
|
|
6
|
+
'abs', 'sign', 'clamp', 'max', 'rolling_std', 'rolling_sum',
|
|
7
7
|
'correlation', 'cross_up', 'cross_down', 'sma', 'sgn',
|
|
8
8
|
'rolling_max', 'rolling_min', 'rolling_mean',
|
|
9
9
|
'ts_argmax', 'ts_argmin',
|
|
@@ -326,6 +326,11 @@ def clamp(x: Array, min_val: float, max_val: float) -> Array:
|
|
|
326
326
|
return x.clip(lower=min_val, upper=max_val)
|
|
327
327
|
|
|
328
328
|
|
|
329
|
+
def max_val(x: Array) -> Array:
|
|
330
|
+
"""返回序列最大值(单值Series)"""
|
|
331
|
+
return pd.Series([x.max()], index=[x.index[-1]])
|
|
332
|
+
|
|
333
|
+
|
|
329
334
|
# =============================================================================
|
|
330
335
|
# 内置函数映射
|
|
331
336
|
# =============================================================================
|
|
@@ -361,7 +366,7 @@ BUILTIN_FUNCTIONS: Dict[str, Callable] = {
|
|
|
361
366
|
|
|
362
367
|
# 数学函数
|
|
363
368
|
"abs": abs_val, "sign": sign, "clamp": clamp,
|
|
364
|
-
"max":
|
|
369
|
+
"max": max_val,
|
|
365
370
|
}
|
|
366
371
|
|
|
367
372
|
|
|
@@ -431,7 +436,7 @@ class Formula:
|
|
|
431
436
|
ctx = {
|
|
432
437
|
'ma': ma, 'ema': ema, 'delay': delay, 'zscore': zscore,
|
|
433
438
|
'rank': rank, 'where': where, 'if': if_,
|
|
434
|
-
'abs': abs_val, 'sign': sign, 'clamp': clamp,
|
|
439
|
+
'abs': abs_val, 'sign': sign, 'clamp': clamp, 'max': max_val,
|
|
435
440
|
'rolling_std': rolling_std, 'rolling_sum': rolling_sum,
|
|
436
441
|
'correlation': correlation, 'cross_up': cross_up,
|
|
437
442
|
'cross_down': cross_down,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quantcli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: 面向AI的多因子量化选股策略挖掘工具,AI Agent 友好 CLI
|
|
5
5
|
Author-email: QuantCLI Team <quantcli@example.com>
|
|
6
|
-
Project-URL: repository, https://
|
|
6
|
+
Project-URL: repository, https://github.com/wumu2013/quantcli
|
|
7
7
|
Classifier: Development Status :: 3 - Alpha
|
|
8
8
|
Classifier: Intended Audience :: Developers
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -0,0 +1,799 @@
|
|
|
1
|
+
"""内置因子和高级功能测试
|
|
2
|
+
|
|
3
|
+
测试 cli_guide.md 中描述的功能:
|
|
4
|
+
- quantcli factors list
|
|
5
|
+
- quantcli analyze ic/batch (核心逻辑)
|
|
6
|
+
- quantcli filter run (核心逻辑)
|
|
7
|
+
- 内置 Alpha101 因子引用
|
|
8
|
+
- Pipeline 多阶段筛选
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import tempfile
|
|
13
|
+
import json
|
|
14
|
+
from unittest.mock import MagicMock, patch
|
|
15
|
+
import pandas as pd
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from click.testing import CliRunner
|
|
19
|
+
|
|
20
|
+
from quantcli import cli
|
|
21
|
+
from quantcli.factors.loader import load_strategy, load_all_factors, load_factor_from_ref
|
|
22
|
+
from quantcli.factors.pipeline import FactorPipeline
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def runner():
|
|
27
|
+
"""Create CliRunner for testing CLI commands"""
|
|
28
|
+
return CliRunner()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def sample_price_data():
|
|
33
|
+
"""生成合成价格数据"""
|
|
34
|
+
np.random.seed(42)
|
|
35
|
+
dates = pd.date_range("2023-01-01", periods=100, freq="B")
|
|
36
|
+
|
|
37
|
+
close = 100 + np.cumsum(np.random.randn(100) * 0.5)
|
|
38
|
+
open_ = close * (1 + np.random.randn(100) * 0.01)
|
|
39
|
+
volume = np.random.randint(1000000, 10000000, 100)
|
|
40
|
+
|
|
41
|
+
return pd.DataFrame({
|
|
42
|
+
"date": dates,
|
|
43
|
+
"symbol": ["600519"] * 100,
|
|
44
|
+
"open": open_,
|
|
45
|
+
"high": close * 1.02,
|
|
46
|
+
"low": close * 0.98,
|
|
47
|
+
"close": close,
|
|
48
|
+
"volume": volume,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def multi_symbol_price_data():
|
|
54
|
+
"""生成多只股票的价格数据"""
|
|
55
|
+
np.random.seed(42)
|
|
56
|
+
dates = pd.date_range("2023-01-01", periods=60, freq="B")
|
|
57
|
+
|
|
58
|
+
data = {}
|
|
59
|
+
symbols = ["600519", "000001", "600036", "000002", "600000"]
|
|
60
|
+
|
|
61
|
+
for symbol in symbols:
|
|
62
|
+
close = 100 + np.cumsum(np.random.randn(60) * 0.5)
|
|
63
|
+
data[symbol] = pd.DataFrame({
|
|
64
|
+
"date": dates,
|
|
65
|
+
"symbol": [symbol] * 60,
|
|
66
|
+
"open": close * (1 + np.random.randn(60) * 0.01),
|
|
67
|
+
"high": close * 1.02,
|
|
68
|
+
"low": close * 0.98,
|
|
69
|
+
"close": close,
|
|
70
|
+
"volume": np.random.randint(1000000, 10000000, 60),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestFactorsListCommand:
|
|
77
|
+
"""quantcli factors list 命令测试"""
|
|
78
|
+
|
|
79
|
+
def test_factors_list_basic(self, runner):
|
|
80
|
+
"""测试 factors list 基本功能"""
|
|
81
|
+
result = runner.invoke(cli.factors_list, ["--json"])
|
|
82
|
+
|
|
83
|
+
assert result.exit_code == 0
|
|
84
|
+
output = json.loads(result.output)
|
|
85
|
+
|
|
86
|
+
assert output["status"] == "success"
|
|
87
|
+
assert output["count"] == 40
|
|
88
|
+
assert len(output["factors"]) == 40
|
|
89
|
+
|
|
90
|
+
def test_factors_list_shows_alpha101(self, runner):
|
|
91
|
+
"""测试 factors list 显示 Alpha101 因子"""
|
|
92
|
+
result = runner.invoke(cli.factors_list, ["--json"])
|
|
93
|
+
|
|
94
|
+
assert result.exit_code == 0
|
|
95
|
+
output = json.loads(result.output)
|
|
96
|
+
|
|
97
|
+
# 检查包含 alpha_001
|
|
98
|
+
factor_files = [f["file"] for f in output["factors"]]
|
|
99
|
+
assert "alpha101/alpha_001.yaml" in factor_files
|
|
100
|
+
|
|
101
|
+
def test_factors_list_human_format(self, runner):
|
|
102
|
+
"""测试 factors list 人类友好格式"""
|
|
103
|
+
result = runner.invoke(cli.factors_list, [])
|
|
104
|
+
|
|
105
|
+
assert result.exit_code == 0
|
|
106
|
+
assert "Built-in Alpha101 Factors" in result.output
|
|
107
|
+
assert "alpha101/alpha_001.yaml" in result.output
|
|
108
|
+
assert "technical" in result.output
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestBuiltinFactors:
|
|
112
|
+
"""内置 Alpha101 因子测试"""
|
|
113
|
+
|
|
114
|
+
def test_load_single_builtin_factor(self):
|
|
115
|
+
"""测试加载单个内置因子"""
|
|
116
|
+
factor = load_factor_from_ref("/Users/apple/quantcli/examples/strategies", "alpha101/alpha_001")
|
|
117
|
+
|
|
118
|
+
assert factor is not None
|
|
119
|
+
assert "alpha_001" in factor.name.lower() or "反转" in factor.description
|
|
120
|
+
|
|
121
|
+
def test_load_multiple_builtin_factors(self):
|
|
122
|
+
"""测试加载多个内置因子"""
|
|
123
|
+
weights = {
|
|
124
|
+
"alpha101/alpha_001": 0.3,
|
|
125
|
+
"alpha101/alpha_008": 0.4,
|
|
126
|
+
"alpha101/alpha_029": 0.3,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
factors = load_all_factors(weights, "/Users/apple/quantcli/examples/strategies")
|
|
130
|
+
|
|
131
|
+
assert len(factors) == 3
|
|
132
|
+
assert "alpha101/alpha_001" in factors
|
|
133
|
+
assert "alpha101/alpha_008" in factors
|
|
134
|
+
assert "alpha101/alpha_029" in factors
|
|
135
|
+
|
|
136
|
+
def test_builtin_factor_fields(self):
|
|
137
|
+
"""测试内置因子字段完整"""
|
|
138
|
+
factor = load_factor_from_ref("/Users/apple/quantcli/examples/strategies", "alpha101/alpha_008")
|
|
139
|
+
|
|
140
|
+
assert factor is not None
|
|
141
|
+
assert factor.name is not None
|
|
142
|
+
assert factor.expr is not None
|
|
143
|
+
assert factor.type is not None
|
|
144
|
+
assert factor.direction is not None
|
|
145
|
+
|
|
146
|
+
def test_all_40_builtin_factors_loadable(self):
|
|
147
|
+
"""测试全部 40 个内置因子都可加载"""
|
|
148
|
+
from pathlib import Path
|
|
149
|
+
from quantcli.utils import builtin_factors_dir
|
|
150
|
+
|
|
151
|
+
builtin_dir = builtin_factors_dir() / "alpha101"
|
|
152
|
+
yaml_files = sorted(builtin_dir.glob("alpha_*.yaml"))
|
|
153
|
+
|
|
154
|
+
assert len(yaml_files) == 40
|
|
155
|
+
|
|
156
|
+
# 测试每个因子都能加载
|
|
157
|
+
for yaml_file in yaml_files:
|
|
158
|
+
alpha_name = yaml_file.stem # alpha_001
|
|
159
|
+
factor = load_factor_from_ref("/Users/apple/quantcli/examples/strategies", f"alpha101/{alpha_name}")
|
|
160
|
+
assert factor is not None, f"Failed to load {yaml_file.name}"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class TestStrategyWithBuiltinFactors:
|
|
164
|
+
"""使用内置因子的策略测试"""
|
|
165
|
+
|
|
166
|
+
def test_strategy_with_builtin_factors(self, tmp_path, multi_symbol_price_data):
|
|
167
|
+
"""测试使用内置因子的策略"""
|
|
168
|
+
# 创建策略文件
|
|
169
|
+
yaml_content = """
|
|
170
|
+
name: 内置因子测试策略
|
|
171
|
+
version: 1.0.0
|
|
172
|
+
|
|
173
|
+
factors:
|
|
174
|
+
- alpha101/alpha_001
|
|
175
|
+
- alpha101/alpha_008
|
|
176
|
+
- alpha101/alpha_029
|
|
177
|
+
|
|
178
|
+
ranking:
|
|
179
|
+
weights:
|
|
180
|
+
alpha101/alpha_001: 0.4
|
|
181
|
+
alpha101/alpha_008: 0.3
|
|
182
|
+
alpha101/alpha_029: 0.3
|
|
183
|
+
normalize: zscore
|
|
184
|
+
|
|
185
|
+
output:
|
|
186
|
+
limit: 10
|
|
187
|
+
"""
|
|
188
|
+
config_file = tmp_path / "builtin_test.yaml"
|
|
189
|
+
config_file.write_text(yaml_content)
|
|
190
|
+
|
|
191
|
+
config = load_strategy(str(config_file))
|
|
192
|
+
|
|
193
|
+
# 验证配置加载
|
|
194
|
+
assert config.name == "内置因子测试策略"
|
|
195
|
+
assert len(config.ranking.get("weights", {})) == 3
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class TestAnalyzeCommands:
|
|
199
|
+
"""quantcli analyze 命令核心逻辑测试
|
|
200
|
+
|
|
201
|
+
Note: CLI 测试依赖 FormulaParser,但当前实现使用不存在的类。
|
|
202
|
+
这里测试 IC/IR 计算的核心逻辑。
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def test_compute_ic_core_logic(self, sample_price_data):
|
|
206
|
+
"""测试 IC 计算核心逻辑"""
|
|
207
|
+
pytest.importorskip("scipy")
|
|
208
|
+
|
|
209
|
+
from scipy.stats import spearmanr
|
|
210
|
+
|
|
211
|
+
# 计算因子值
|
|
212
|
+
close = sample_price_data["close"]
|
|
213
|
+
factor = (close / close.shift(20)) - 1
|
|
214
|
+
|
|
215
|
+
# 计算未来收益
|
|
216
|
+
forward_returns = close.shift(-5) / close - 1
|
|
217
|
+
|
|
218
|
+
# 去除 NaN
|
|
219
|
+
valid_mask = ~(factor.isna() | forward_returns.isna())
|
|
220
|
+
ic, _ = spearmanr(factor[valid_mask], forward_returns[valid_mask])
|
|
221
|
+
|
|
222
|
+
# IC 值应该在 -1 到 1 之间
|
|
223
|
+
assert -1 <= ic <= 1
|
|
224
|
+
|
|
225
|
+
def test_ir_computation(self):
|
|
226
|
+
"""测试 IR 计算"""
|
|
227
|
+
ic_rolling = np.array([0.05, 0.03, 0.07, 0.02, 0.04])
|
|
228
|
+
ic_mean = np.mean(ic_rolling)
|
|
229
|
+
ic_std = np.std(ic_rolling)
|
|
230
|
+
ir = ic_mean / ic_std if ic_std > 0 else 0
|
|
231
|
+
|
|
232
|
+
assert ir > 0 # 正 IR
|
|
233
|
+
assert ic_mean > 0 # 正 IC 均值
|
|
234
|
+
|
|
235
|
+
def test_rolling_ic_computation(self, sample_price_data):
|
|
236
|
+
"""测试滚动 IC 计算"""
|
|
237
|
+
pytest.importorskip("scipy")
|
|
238
|
+
|
|
239
|
+
from scipy.stats import spearmanr
|
|
240
|
+
|
|
241
|
+
close = sample_price_data["close"]
|
|
242
|
+
|
|
243
|
+
# 计算因子
|
|
244
|
+
factor = (close / close.shift(20)) - 1
|
|
245
|
+
forward_returns = close.shift(-5) / close - 1
|
|
246
|
+
|
|
247
|
+
# 滚动 IC
|
|
248
|
+
window = 20
|
|
249
|
+
ic_rolling = []
|
|
250
|
+
for i in range(window, len(factor)):
|
|
251
|
+
f = factor.iloc[i-window:i]
|
|
252
|
+
r = forward_returns.iloc[i-window:i]
|
|
253
|
+
valid_mask = ~(f.isna() | r.isna())
|
|
254
|
+
if valid_mask.sum() > 5:
|
|
255
|
+
ic, _ = spearmanr(f[valid_mask], r[valid_mask])
|
|
256
|
+
ic_rolling.append(ic)
|
|
257
|
+
|
|
258
|
+
assert len(ic_rolling) > 0
|
|
259
|
+
ic_array = np.array(ic_rolling)
|
|
260
|
+
assert -1 <= ic_array.mean() <= 1
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class TestFilterRunCommand:
|
|
264
|
+
"""quantcli filter run 核心逻辑测试
|
|
265
|
+
|
|
266
|
+
测试筛选功能的配置加载和核心计算逻辑。
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
def test_filter_config_loading(self, tmp_path):
|
|
270
|
+
"""测试 filter 配置加载"""
|
|
271
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
272
|
+
f.write("""
|
|
273
|
+
name: 测试策略
|
|
274
|
+
version: 1.0.0
|
|
275
|
+
|
|
276
|
+
factors:
|
|
277
|
+
- name: close_price
|
|
278
|
+
expr: "close"
|
|
279
|
+
|
|
280
|
+
ranking:
|
|
281
|
+
weights:
|
|
282
|
+
close_price: 1.0
|
|
283
|
+
normalize: zscore
|
|
284
|
+
|
|
285
|
+
output:
|
|
286
|
+
limit: 10
|
|
287
|
+
""")
|
|
288
|
+
config_path = f.name
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
config = load_strategy(config_path)
|
|
292
|
+
assert config is not None
|
|
293
|
+
assert config.name == "测试策略"
|
|
294
|
+
finally:
|
|
295
|
+
import os
|
|
296
|
+
os.unlink(config_path)
|
|
297
|
+
|
|
298
|
+
def test_filter_with_weights(self, tmp_path, multi_symbol_price_data):
|
|
299
|
+
"""测试带权重的筛选"""
|
|
300
|
+
from quantcli.factors.compute import FactorComputer
|
|
301
|
+
from quantcli.factors.ranking import ScoringEngine
|
|
302
|
+
|
|
303
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
304
|
+
f.write("""
|
|
305
|
+
name: 测试策略
|
|
306
|
+
version: 1.0.0
|
|
307
|
+
|
|
308
|
+
factors:
|
|
309
|
+
- name: close_price
|
|
310
|
+
expr: "close"
|
|
311
|
+
- name: volume_val
|
|
312
|
+
expr: "volume"
|
|
313
|
+
|
|
314
|
+
ranking:
|
|
315
|
+
weights:
|
|
316
|
+
close_price: 0.6
|
|
317
|
+
volume_val: 0.4
|
|
318
|
+
normalize: zscore
|
|
319
|
+
|
|
320
|
+
output:
|
|
321
|
+
limit: 10
|
|
322
|
+
""")
|
|
323
|
+
config_path = f.name
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
config = load_strategy(config_path)
|
|
327
|
+
factors = {f.name: f for f in config.ranking.get("inline_factors", [])}
|
|
328
|
+
weights = config.ranking.get("weights", {})
|
|
329
|
+
|
|
330
|
+
computer = FactorComputer()
|
|
331
|
+
factor_df = computer.compute_all_factors(
|
|
332
|
+
factors,
|
|
333
|
+
multi_symbol_price_data,
|
|
334
|
+
{},
|
|
335
|
+
list(multi_symbol_price_data.keys())
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
assert "close_price" in factor_df.columns
|
|
339
|
+
assert "volume_val" in factor_df.columns
|
|
340
|
+
|
|
341
|
+
scorer = ScoringEngine(normalize="zscore")
|
|
342
|
+
result = scorer.compute(factors, weights, factor_df)
|
|
343
|
+
|
|
344
|
+
assert "score" in result.columns
|
|
345
|
+
assert len(result) == len(multi_symbol_price_data)
|
|
346
|
+
finally:
|
|
347
|
+
import os
|
|
348
|
+
os.unlink(config_path)
|
|
349
|
+
|
|
350
|
+
def test_filter_with_conditions(self, tmp_path, multi_symbol_price_data):
|
|
351
|
+
"""测试带条件的筛选"""
|
|
352
|
+
from quantcli.factors.compute import FactorComputer
|
|
353
|
+
from quantcli.factors.ranking import ScoringEngine
|
|
354
|
+
|
|
355
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
356
|
+
f.write("""
|
|
357
|
+
name: 测试条件筛选
|
|
358
|
+
version: 1.0.0
|
|
359
|
+
|
|
360
|
+
factors:
|
|
361
|
+
- name: close_price
|
|
362
|
+
expr: "close"
|
|
363
|
+
- name: is_up
|
|
364
|
+
expr: "where(close > open, 1, 0)"
|
|
365
|
+
|
|
366
|
+
ranking:
|
|
367
|
+
weights:
|
|
368
|
+
close_price: 0.7
|
|
369
|
+
is_up: 0.3
|
|
370
|
+
conditions:
|
|
371
|
+
is_up: true
|
|
372
|
+
normalize: zscore
|
|
373
|
+
|
|
374
|
+
output:
|
|
375
|
+
limit: 10
|
|
376
|
+
""")
|
|
377
|
+
config_path = f.name
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
config = load_strategy(config_path)
|
|
381
|
+
factors = {f.name: f for f in config.ranking.get("inline_factors", [])}
|
|
382
|
+
weights = config.ranking.get("weights", {})
|
|
383
|
+
conditions = config.ranking.get("conditions", {})
|
|
384
|
+
|
|
385
|
+
computer = FactorComputer()
|
|
386
|
+
factor_df = computer.compute_all_factors(
|
|
387
|
+
factors,
|
|
388
|
+
multi_symbol_price_data,
|
|
389
|
+
{},
|
|
390
|
+
list(multi_symbol_price_data.keys())
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
scorer = ScoringEngine(normalize="zscore")
|
|
394
|
+
result = scorer.compute(factors, weights, factor_df, conditions=conditions)
|
|
395
|
+
|
|
396
|
+
# 验证条件生效
|
|
397
|
+
assert "score" in result.columns
|
|
398
|
+
finally:
|
|
399
|
+
import os
|
|
400
|
+
os.unlink(config_path)
|
|
401
|
+
|
|
402
|
+
def test_filter_json_output_format(self, tmp_path, multi_symbol_price_data):
|
|
403
|
+
"""测试 JSON 输出格式"""
|
|
404
|
+
import json
|
|
405
|
+
|
|
406
|
+
from quantcli.factors.compute import FactorComputer
|
|
407
|
+
from quantcli.factors.ranking import ScoringEngine
|
|
408
|
+
|
|
409
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
|
|
410
|
+
f.write("""
|
|
411
|
+
name: 测试策略
|
|
412
|
+
version: 1.0.0
|
|
413
|
+
|
|
414
|
+
factors:
|
|
415
|
+
- name: close_price
|
|
416
|
+
expr: "close"
|
|
417
|
+
|
|
418
|
+
ranking:
|
|
419
|
+
weights:
|
|
420
|
+
close_price: 1.0
|
|
421
|
+
normalize: zscore
|
|
422
|
+
|
|
423
|
+
output:
|
|
424
|
+
limit: 10
|
|
425
|
+
""")
|
|
426
|
+
config_path = f.name
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
config = load_strategy(config_path)
|
|
430
|
+
factors = {f.name: f for f in config.ranking.get("inline_factors", [])}
|
|
431
|
+
weights = config.ranking.get("weights", {})
|
|
432
|
+
|
|
433
|
+
computer = FactorComputer()
|
|
434
|
+
factor_df = computer.compute_all_factors(
|
|
435
|
+
factors,
|
|
436
|
+
multi_symbol_price_data,
|
|
437
|
+
{},
|
|
438
|
+
list(multi_symbol_price_data.keys())
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
scorer = ScoringEngine(normalize="zscore")
|
|
442
|
+
result = scorer.compute(factors, weights, factor_df)
|
|
443
|
+
|
|
444
|
+
# 模拟 JSON 输出
|
|
445
|
+
output = {
|
|
446
|
+
"status": "success",
|
|
447
|
+
"count": len(result),
|
|
448
|
+
"results": result.to_dict(orient="records")
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
assert output["status"] == "success"
|
|
452
|
+
assert output["count"] == len(multi_symbol_price_data)
|
|
453
|
+
parsed = json.dumps(output)
|
|
454
|
+
assert len(parsed) > 0
|
|
455
|
+
finally:
|
|
456
|
+
import os
|
|
457
|
+
os.unlink(config_path)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class TestPipelineIntegration:
|
|
461
|
+
"""Pipeline 多阶段筛选测试"""
|
|
462
|
+
|
|
463
|
+
def test_pipeline_load_config(self, tmp_path):
|
|
464
|
+
"""测试 Pipeline 加载配置"""
|
|
465
|
+
yaml_content = """
|
|
466
|
+
name: Pipeline 测试策略
|
|
467
|
+
version: 1.0.0
|
|
468
|
+
|
|
469
|
+
factors:
|
|
470
|
+
- name: ma10_deviation
|
|
471
|
+
expr: "(close - ma(close, 10)) / ma(close, 10)"
|
|
472
|
+
- name: volume_ratio
|
|
473
|
+
expr: "volume / ma(volume, 5)"
|
|
474
|
+
|
|
475
|
+
ranking:
|
|
476
|
+
weights:
|
|
477
|
+
ma10_deviation: 0.6
|
|
478
|
+
volume_ratio: 0.4
|
|
479
|
+
normalize: zscore
|
|
480
|
+
|
|
481
|
+
output:
|
|
482
|
+
limit: 10
|
|
483
|
+
"""
|
|
484
|
+
config_file = tmp_path / "pipeline_test.yaml"
|
|
485
|
+
config_file.write_text(yaml_content)
|
|
486
|
+
|
|
487
|
+
pipeline = FactorPipeline(str(config_file))
|
|
488
|
+
|
|
489
|
+
assert pipeline is not None
|
|
490
|
+
assert pipeline.config.name == "Pipeline 测试策略"
|
|
491
|
+
|
|
492
|
+
def test_pipeline_with_builtin_factors(self, tmp_path, multi_symbol_price_data):
|
|
493
|
+
"""测试 Pipeline 使用内置因子"""
|
|
494
|
+
yaml_content = """
|
|
495
|
+
name: 内置因子 Pipeline
|
|
496
|
+
version: 1.0.0
|
|
497
|
+
|
|
498
|
+
factors:
|
|
499
|
+
- alpha101/alpha_001
|
|
500
|
+
- alpha101/alpha_008
|
|
501
|
+
|
|
502
|
+
ranking:
|
|
503
|
+
weights:
|
|
504
|
+
alpha101/alpha_001: 0.5
|
|
505
|
+
alpha101/alpha_008: 0.5
|
|
506
|
+
normalize: zscore
|
|
507
|
+
|
|
508
|
+
output:
|
|
509
|
+
limit: 10
|
|
510
|
+
"""
|
|
511
|
+
config_file = tmp_path / "builtin_pipeline.yaml"
|
|
512
|
+
config_file.write_text(yaml_content)
|
|
513
|
+
|
|
514
|
+
pipeline = FactorPipeline(str(config_file))
|
|
515
|
+
|
|
516
|
+
assert pipeline is not None
|
|
517
|
+
assert len(pipeline.config.ranking.get("weights", {})) == 2
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class TestStrategyConfigExamples:
|
|
521
|
+
"""cli_guide.md 中的策略配置示例测试"""
|
|
522
|
+
|
|
523
|
+
def test_example1_basic_multi_factor(self, tmp_path, multi_symbol_price_data):
|
|
524
|
+
"""测试示例1: 基础多因子策略"""
|
|
525
|
+
yaml_content = """
|
|
526
|
+
name: 我的多因子策略
|
|
527
|
+
version: 1.0.0
|
|
528
|
+
|
|
529
|
+
factors:
|
|
530
|
+
- name: momentum_20
|
|
531
|
+
type: technical
|
|
532
|
+
expr: "(close / delay(close, 20)) - 1"
|
|
533
|
+
direction: positive
|
|
534
|
+
|
|
535
|
+
- name: rsi_14
|
|
536
|
+
type: technical
|
|
537
|
+
expr: "rsi(close, 14)"
|
|
538
|
+
direction: negative
|
|
539
|
+
|
|
540
|
+
- name: volume_ratio
|
|
541
|
+
type: technical
|
|
542
|
+
expr: "volume / ma(volume, 5)"
|
|
543
|
+
direction: negative
|
|
544
|
+
|
|
545
|
+
ranking:
|
|
546
|
+
weights:
|
|
547
|
+
momentum_20: 0.4
|
|
548
|
+
rsi_14: 0.3
|
|
549
|
+
volume_ratio: 0.3
|
|
550
|
+
normalize: zscore
|
|
551
|
+
|
|
552
|
+
output:
|
|
553
|
+
limit: 20
|
|
554
|
+
"""
|
|
555
|
+
config_file = tmp_path / "example1.yaml"
|
|
556
|
+
config_file.write_text(yaml_content)
|
|
557
|
+
|
|
558
|
+
config = load_strategy(str(config_file))
|
|
559
|
+
|
|
560
|
+
# 验证配置
|
|
561
|
+
assert config.name == "我的多因子策略"
|
|
562
|
+
assert config.version == "1.0.0"
|
|
563
|
+
|
|
564
|
+
inline_factors = config.ranking.get("inline_factors", [])
|
|
565
|
+
assert len(inline_factors) == 3
|
|
566
|
+
|
|
567
|
+
weights = config.ranking.get("weights", {})
|
|
568
|
+
assert weights["momentum_20"] == 0.4
|
|
569
|
+
assert weights["rsi_14"] == 0.3
|
|
570
|
+
assert weights["volume_ratio"] == 0.3
|
|
571
|
+
|
|
572
|
+
def test_example2_with_conditions(self, tmp_path):
|
|
573
|
+
"""测试示例2: 带条件筛选的策略"""
|
|
574
|
+
yaml_content = """
|
|
575
|
+
name: 精选低估成长
|
|
576
|
+
version: 1.0.0
|
|
577
|
+
|
|
578
|
+
factors:
|
|
579
|
+
- name: roe
|
|
580
|
+
type: fundamental
|
|
581
|
+
expr: "roe"
|
|
582
|
+
direction: positive
|
|
583
|
+
|
|
584
|
+
- name: pe
|
|
585
|
+
type: fundamental
|
|
586
|
+
expr: "pe"
|
|
587
|
+
direction: negative
|
|
588
|
+
|
|
589
|
+
- name: revenue_growth
|
|
590
|
+
type: fundamental
|
|
591
|
+
expr: "revenue_yoy"
|
|
592
|
+
direction: positive
|
|
593
|
+
|
|
594
|
+
ranking:
|
|
595
|
+
weights:
|
|
596
|
+
roe: 0.4
|
|
597
|
+
pe: 0.3
|
|
598
|
+
revenue_growth: 0.3
|
|
599
|
+
conditions:
|
|
600
|
+
roe: {min: 0.1}
|
|
601
|
+
pe: {max: 30}
|
|
602
|
+
revenue_growth: {min: 0.05}
|
|
603
|
+
|
|
604
|
+
output:
|
|
605
|
+
limit: 50
|
|
606
|
+
"""
|
|
607
|
+
config_file = tmp_path / "example2.yaml"
|
|
608
|
+
config_file.write_text(yaml_content)
|
|
609
|
+
|
|
610
|
+
config = load_strategy(str(config_file))
|
|
611
|
+
|
|
612
|
+
conditions = config.ranking.get("conditions", {})
|
|
613
|
+
assert "roe" in conditions
|
|
614
|
+
assert conditions["roe"]["min"] == 0.1
|
|
615
|
+
assert "pe" in conditions
|
|
616
|
+
assert conditions["pe"]["max"] == 30
|
|
617
|
+
|
|
618
|
+
def test_example3_with_bonuses(self, tmp_path):
|
|
619
|
+
"""测试示例3: 评分+加分项策略"""
|
|
620
|
+
yaml_content = """
|
|
621
|
+
name: 强势股回调策略
|
|
622
|
+
version: 1.0.0
|
|
623
|
+
|
|
624
|
+
factors:
|
|
625
|
+
- name: ma10_deviation
|
|
626
|
+
type: technical
|
|
627
|
+
expr: "(close - ma(close, 10)) / ma(close, 10)"
|
|
628
|
+
direction: negative
|
|
629
|
+
|
|
630
|
+
- name: is_yinliang
|
|
631
|
+
type: technical
|
|
632
|
+
expr: "close < open"
|
|
633
|
+
direction: positive
|
|
634
|
+
|
|
635
|
+
- name: volume_ratio
|
|
636
|
+
type: technical
|
|
637
|
+
expr: "volume / ma(volume, 5)"
|
|
638
|
+
direction: negative
|
|
639
|
+
|
|
640
|
+
ranking:
|
|
641
|
+
weights:
|
|
642
|
+
ma10_deviation: 0.5
|
|
643
|
+
is_yinliang: 0
|
|
644
|
+
volume_ratio: 0.2
|
|
645
|
+
conditions:
|
|
646
|
+
is_yinliang: true
|
|
647
|
+
bonuses:
|
|
648
|
+
- condition: "volume_ratio < 0.8"
|
|
649
|
+
weight: 1.0
|
|
650
|
+
description: 缩量
|
|
651
|
+
- condition: "ma10_deviation > -0.05 and ma10_deviation < 0"
|
|
652
|
+
weight: 2.0
|
|
653
|
+
description: 回调到10日线附近
|
|
654
|
+
|
|
655
|
+
output:
|
|
656
|
+
limit: 30
|
|
657
|
+
"""
|
|
658
|
+
config_file = tmp_path / "example3.yaml"
|
|
659
|
+
config_file.write_text(yaml_content)
|
|
660
|
+
|
|
661
|
+
config = load_strategy(str(config_file))
|
|
662
|
+
|
|
663
|
+
bonuses = config.ranking.get("bonuses", [])
|
|
664
|
+
assert len(bonuses) == 2
|
|
665
|
+
assert bonuses[0].condition == "volume_ratio < 0.8"
|
|
666
|
+
assert bonuses[1].condition == "ma10_deviation > -0.05 and ma10_deviation < 0"
|
|
667
|
+
|
|
668
|
+
def test_example4_builtin_factors(self, tmp_path):
|
|
669
|
+
"""测试示例4: 使用内置因子"""
|
|
670
|
+
yaml_content = """
|
|
671
|
+
name: 内置因子策略
|
|
672
|
+
version: 1.0.0
|
|
673
|
+
|
|
674
|
+
factors:
|
|
675
|
+
- alpha101/alpha_001
|
|
676
|
+
- alpha101/alpha_008
|
|
677
|
+
- alpha101/alpha_029
|
|
678
|
+
|
|
679
|
+
ranking:
|
|
680
|
+
weights:
|
|
681
|
+
alpha101/alpha_001: 0.4
|
|
682
|
+
alpha101/alpha_008: 0.3
|
|
683
|
+
alpha101/alpha_029: 0.3
|
|
684
|
+
normalize: zscore
|
|
685
|
+
|
|
686
|
+
output:
|
|
687
|
+
limit: 30
|
|
688
|
+
"""
|
|
689
|
+
config_file = tmp_path / "example4.yaml"
|
|
690
|
+
config_file.write_text(yaml_content)
|
|
691
|
+
|
|
692
|
+
config = load_strategy(str(config_file))
|
|
693
|
+
|
|
694
|
+
weights = config.ranking.get("weights", {})
|
|
695
|
+
assert weights["alpha101/alpha_001"] == 0.4
|
|
696
|
+
assert weights["alpha101/alpha_008"] == 0.3
|
|
697
|
+
assert weights["alpha101/alpha_029"] == 0.3
|
|
698
|
+
|
|
699
|
+
# 验证内置因子可加载
|
|
700
|
+
all_factors = load_all_factors(weights, str(config_file))
|
|
701
|
+
assert len(all_factors) == 3
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
class TestFormulaSyntax:
|
|
705
|
+
"""公式语法测试"""
|
|
706
|
+
|
|
707
|
+
def test_time_series_functions(self, sample_price_data):
|
|
708
|
+
"""测试时间序列函数"""
|
|
709
|
+
from quantcli.factors.compute import FactorComputer
|
|
710
|
+
from quantcli.factors.base import FactorDefinition
|
|
711
|
+
|
|
712
|
+
computer = FactorComputer()
|
|
713
|
+
|
|
714
|
+
factors = {
|
|
715
|
+
"ma20": FactorDefinition(name="ma20", type="technical", expr="ma(close, 20)"),
|
|
716
|
+
"ema12": FactorDefinition(name="ema12", type="technical", expr="ema(close, 12)"),
|
|
717
|
+
"delay5": FactorDefinition(name="delay5", type="technical", expr="delay(close, 5)"),
|
|
718
|
+
"std20": FactorDefinition(name="std20", type="technical", expr="rolling_std(close, 20)"),
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
result = computer.compute_all_factors(
|
|
722
|
+
factors,
|
|
723
|
+
{"600519": sample_price_data},
|
|
724
|
+
{},
|
|
725
|
+
["600519"]
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
assert "ma20" in result.columns
|
|
729
|
+
assert "ema12" in result.columns
|
|
730
|
+
assert "delay5" in result.columns
|
|
731
|
+
assert "std20" in result.columns
|
|
732
|
+
|
|
733
|
+
def test_statistical_functions(self, sample_price_data):
|
|
734
|
+
"""测试统计函数"""
|
|
735
|
+
from quantcli.factors.compute import FactorComputer
|
|
736
|
+
from quantcli.factors.base import FactorDefinition
|
|
737
|
+
|
|
738
|
+
computer = FactorComputer()
|
|
739
|
+
|
|
740
|
+
factors = {
|
|
741
|
+
"rank": FactorDefinition(name="rank", type="technical", expr="rank(close)"),
|
|
742
|
+
"zscore": FactorDefinition(name="zscore", type="technical", expr="zscore(close)"),
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
result = computer.compute_all_factors(
|
|
746
|
+
factors,
|
|
747
|
+
{"600519": sample_price_data},
|
|
748
|
+
{},
|
|
749
|
+
["600519"]
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
assert "rank" in result.columns
|
|
753
|
+
assert "zscore" in result.columns
|
|
754
|
+
|
|
755
|
+
def test_technical_indicators(self, sample_price_data):
|
|
756
|
+
"""测试技术指标函数 (correlation)"""
|
|
757
|
+
from quantcli.factors.compute import FactorComputer
|
|
758
|
+
from quantcli.factors.base import FactorDefinition
|
|
759
|
+
|
|
760
|
+
computer = FactorComputer()
|
|
761
|
+
|
|
762
|
+
# correlation 计算 - 价格与成交量的相关性
|
|
763
|
+
factors = {
|
|
764
|
+
"corr": FactorDefinition(name="corr", type="technical", expr="correlation(close, volume, 10)"),
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
result = computer.compute_all_factors(
|
|
768
|
+
factors,
|
|
769
|
+
{"600519": sample_price_data},
|
|
770
|
+
{},
|
|
771
|
+
["600519"]
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
assert "corr" in result.columns
|
|
775
|
+
|
|
776
|
+
def test_conditional_expression(self, sample_price_data):
|
|
777
|
+
"""测试条件表达式"""
|
|
778
|
+
from quantcli.factors.compute import FactorComputer
|
|
779
|
+
from quantcli.factors.base import FactorDefinition
|
|
780
|
+
|
|
781
|
+
computer = FactorComputer()
|
|
782
|
+
|
|
783
|
+
factors = {
|
|
784
|
+
"is_up": FactorDefinition(name="is_up", type="technical", expr="where(close > open, 1, 0)"),
|
|
785
|
+
"is_yinliang": FactorDefinition(name="is_yinliang", type="technical", expr="where(close < open, 1, 0)"),
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
result = computer.compute_all_factors(
|
|
789
|
+
factors,
|
|
790
|
+
{"600519": sample_price_data},
|
|
791
|
+
{},
|
|
792
|
+
["600519"]
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
assert "is_up" in result.columns
|
|
796
|
+
assert "is_yinliang" in result.columns
|
|
797
|
+
# 值应该为 0 或 1
|
|
798
|
+
assert result["is_up"].iloc[0] in [0, 1]
|
|
799
|
+
assert result["is_yinliang"].iloc[0] in [0, 1]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|