quantcli 0.1.10__tar.gz → 0.1.12__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.10/quantcli.egg-info → quantcli-0.1.12}/PKG-INFO +1 -1
- {quantcli-0.1.10 → quantcli-0.1.12}/pyproject.toml +4 -1
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/cli.py +43 -73
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_001.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_002.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_003.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_004.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_005.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_006.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_007.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_008.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_009.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_010.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_011.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_012.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_013.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_014.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_015.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_016.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_017.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_018.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_019.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_020.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_021.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_022.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_023.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_024.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_025.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_026.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_027.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_028.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_029.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_030.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_031.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_032.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_033.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_034.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_035.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_036.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_037.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_038.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_039.yaml +6 -0
- quantcli-0.1.12/quantcli/factors/alpha101/alpha_040.yaml +6 -0
- {quantcli-0.1.10 → quantcli-0.1.12/quantcli.egg-info}/PKG-INFO +1 -1
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli.egg-info/SOURCES.txt +40 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/tests/test_builtin_factors.py +89 -68
- {quantcli-0.1.10 → quantcli-0.1.12}/LICENSE +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/README.md +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/core/__init__.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/core/backtest.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/core/data.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/core/factor.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/__init__.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/akshare.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/baostock.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/base.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/cache.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/fundamentals/__init__.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/fundamentals/provider.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/mixed.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/mysql.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/sync/__init__.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/sync/akshare.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/sync/base.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/sync/gm.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/datasources/sync/gm_fundamental.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/factors/__init__.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/factors/base.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/factors/compute.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/factors/loader.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/factors/pipeline.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/factors/ranking.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/factors/ranking_executor.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/factors/screening.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/factors/screening_executor.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/models/bar.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/parser/__init__.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/parser/constants.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/parser/formula.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/utils/__init__.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/utils/env.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/utils/logger.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/utils/path.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/utils/symbol_utils.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/utils/time.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli/utils/validate.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli.egg-info/dependency_links.txt +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli.egg-info/entry_points.txt +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli.egg-info/requires.txt +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/quantcli.egg-info/top_level.txt +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/setup.cfg +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/tests/test_akshare_integration.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/tests/test_cli.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/tests/test_datasources.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/tests/test_factors.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/tests/test_gm_executors.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/tests/test_mixed_datasource.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/tests/test_multi_factor.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/tests/test_pipeline_integration.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/tests/test_symbol_utils.py +0 -0
- {quantcli-0.1.10 → quantcli-0.1.12}/tests/test_time.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quantcli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.12"
|
|
8
8
|
description = "面向AI的多因子量化选股策略挖掘工具,AI Agent 友好 CLI"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -48,6 +48,9 @@ where = ["."]
|
|
|
48
48
|
include = ["quantcli*"]
|
|
49
49
|
exclude = ["tests*"]
|
|
50
50
|
|
|
51
|
+
[tool.setuptools.package-data]
|
|
52
|
+
quantcli = ["factors/alpha101/*.yaml", "factors/*.yaml"]
|
|
53
|
+
|
|
51
54
|
[tool.pytest.ini_options]
|
|
52
55
|
testpaths = ["tests"]
|
|
53
56
|
python_files = ["test_*.py"]
|
|
@@ -277,68 +277,6 @@ def data_sync_fundamentals(ctx, years):
|
|
|
277
277
|
sys.exit(1)
|
|
278
278
|
|
|
279
279
|
|
|
280
|
-
# =============================================================================
|
|
281
|
-
# Factors 命令 - 列出内置因子
|
|
282
|
-
# =============================================================================
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
@quantcli.group()
|
|
286
|
-
def factors():
|
|
287
|
-
"""内置因子管理"""
|
|
288
|
-
pass
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
@factors.command("list")
|
|
292
|
-
@click.option("--json", is_flag=True, help="Output as JSON")
|
|
293
|
-
@click.pass_context
|
|
294
|
-
def factors_list(ctx, json):
|
|
295
|
-
"""列出所有内置 Alpha101 因子"""
|
|
296
|
-
from .utils import builtin_factors_dir
|
|
297
|
-
|
|
298
|
-
builtin_dir = builtin_factors_dir() / "alpha101"
|
|
299
|
-
|
|
300
|
-
if not builtin_dir.exists():
|
|
301
|
-
click.echo("No built-in factors found")
|
|
302
|
-
return
|
|
303
|
-
|
|
304
|
-
# 加载所有因子
|
|
305
|
-
factors = []
|
|
306
|
-
for f in sorted(builtin_dir.glob("alpha_*.yaml")):
|
|
307
|
-
import yaml
|
|
308
|
-
|
|
309
|
-
with open(f, "r", encoding="utf-8") as fp:
|
|
310
|
-
data = yaml.safe_load(fp)
|
|
311
|
-
factors.append(
|
|
312
|
-
{
|
|
313
|
-
"name": data.get("name", f.stem),
|
|
314
|
-
"type": data.get("type", "technical"),
|
|
315
|
-
"direction": data.get("direction", "neutral"),
|
|
316
|
-
"description": data.get("description", ""),
|
|
317
|
-
"file": f"alpha101/{f.name}",
|
|
318
|
-
}
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
if json:
|
|
322
|
-
import json
|
|
323
|
-
|
|
324
|
-
click.echo(
|
|
325
|
-
json.dumps(
|
|
326
|
-
{"status": "success", "count": len(factors), "factors": factors},
|
|
327
|
-
ensure_ascii=False,
|
|
328
|
-
indent=2,
|
|
329
|
-
)
|
|
330
|
-
)
|
|
331
|
-
return
|
|
332
|
-
|
|
333
|
-
click.echo(f"Built-in Alpha101 Factors ({len(factors)} total):\n")
|
|
334
|
-
click.echo(f"{'File':<22} {'Type':<12} {'Direction':<10} Description")
|
|
335
|
-
click.echo("-" * 60)
|
|
336
|
-
for f in factors:
|
|
337
|
-
click.echo(
|
|
338
|
-
f"{f['file']:<22} {f['type']:<12} {f['direction']:<10} {f['description']}"
|
|
339
|
-
)
|
|
340
|
-
|
|
341
|
-
|
|
342
280
|
# =============================================================================
|
|
343
281
|
# Factor 命令
|
|
344
282
|
# =============================================================================
|
|
@@ -469,21 +407,53 @@ def factor_eval(ctx, name, symbol, start, end, method):
|
|
|
469
407
|
|
|
470
408
|
|
|
471
409
|
@factor.command("list")
|
|
410
|
+
@click.option("--json", is_flag=True, help="Output as JSON")
|
|
472
411
|
@click.pass_context
|
|
473
|
-
def factor_list(ctx):
|
|
474
|
-
"""
|
|
475
|
-
|
|
476
|
-
factors = engine.registry.list_all()
|
|
412
|
+
def factor_list(ctx, json):
|
|
413
|
+
"""列出所有内置 Alpha101 因子"""
|
|
414
|
+
from .utils import builtin_factors_dir
|
|
477
415
|
|
|
478
|
-
|
|
479
|
-
|
|
416
|
+
builtin_dir = builtin_factors_dir() / "alpha101"
|
|
417
|
+
|
|
418
|
+
if not builtin_dir.exists():
|
|
419
|
+
click.echo("No built-in factors found")
|
|
480
420
|
return
|
|
481
421
|
|
|
482
|
-
|
|
483
|
-
for
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
422
|
+
factors = []
|
|
423
|
+
for f in sorted(builtin_dir.glob("alpha_*.yaml")):
|
|
424
|
+
import yaml
|
|
425
|
+
|
|
426
|
+
with open(f, "r", encoding="utf-8") as fp:
|
|
427
|
+
data = yaml.safe_load(fp)
|
|
428
|
+
factors.append(
|
|
429
|
+
{
|
|
430
|
+
"name": data.get("name", f.stem),
|
|
431
|
+
"type": data.get("type", "technical"),
|
|
432
|
+
"direction": data.get("direction", "neutral"),
|
|
433
|
+
"description": data.get("description", ""),
|
|
434
|
+
"file": f"alpha101/{f.name}",
|
|
435
|
+
}
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
if json:
|
|
439
|
+
import json
|
|
440
|
+
|
|
441
|
+
click.echo(
|
|
442
|
+
json.dumps(
|
|
443
|
+
{"status": "success", "count": len(factors), "factors": factors},
|
|
444
|
+
ensure_ascii=False,
|
|
445
|
+
indent=2,
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
click.echo(f"Built-in Alpha101 Factors ({len(factors)} total):\n")
|
|
451
|
+
click.echo(f"{'File':<22} {'Type':<12} {'Direction':<10} Description")
|
|
452
|
+
click.echo("-" * 60)
|
|
453
|
+
for f in factors:
|
|
454
|
+
click.echo(
|
|
455
|
+
f"{f['file']:<22} {f['type']:<12} {f['direction']:<10} {f['description']}"
|
|
456
|
+
)
|
|
487
457
|
|
|
488
458
|
|
|
489
459
|
@factor.command("run-file")
|
|
@@ -35,6 +35,46 @@ quantcli/factors/ranking.py
|
|
|
35
35
|
quantcli/factors/ranking_executor.py
|
|
36
36
|
quantcli/factors/screening.py
|
|
37
37
|
quantcli/factors/screening_executor.py
|
|
38
|
+
quantcli/factors/alpha101/alpha_001.yaml
|
|
39
|
+
quantcli/factors/alpha101/alpha_002.yaml
|
|
40
|
+
quantcli/factors/alpha101/alpha_003.yaml
|
|
41
|
+
quantcli/factors/alpha101/alpha_004.yaml
|
|
42
|
+
quantcli/factors/alpha101/alpha_005.yaml
|
|
43
|
+
quantcli/factors/alpha101/alpha_006.yaml
|
|
44
|
+
quantcli/factors/alpha101/alpha_007.yaml
|
|
45
|
+
quantcli/factors/alpha101/alpha_008.yaml
|
|
46
|
+
quantcli/factors/alpha101/alpha_009.yaml
|
|
47
|
+
quantcli/factors/alpha101/alpha_010.yaml
|
|
48
|
+
quantcli/factors/alpha101/alpha_011.yaml
|
|
49
|
+
quantcli/factors/alpha101/alpha_012.yaml
|
|
50
|
+
quantcli/factors/alpha101/alpha_013.yaml
|
|
51
|
+
quantcli/factors/alpha101/alpha_014.yaml
|
|
52
|
+
quantcli/factors/alpha101/alpha_015.yaml
|
|
53
|
+
quantcli/factors/alpha101/alpha_016.yaml
|
|
54
|
+
quantcli/factors/alpha101/alpha_017.yaml
|
|
55
|
+
quantcli/factors/alpha101/alpha_018.yaml
|
|
56
|
+
quantcli/factors/alpha101/alpha_019.yaml
|
|
57
|
+
quantcli/factors/alpha101/alpha_020.yaml
|
|
58
|
+
quantcli/factors/alpha101/alpha_021.yaml
|
|
59
|
+
quantcli/factors/alpha101/alpha_022.yaml
|
|
60
|
+
quantcli/factors/alpha101/alpha_023.yaml
|
|
61
|
+
quantcli/factors/alpha101/alpha_024.yaml
|
|
62
|
+
quantcli/factors/alpha101/alpha_025.yaml
|
|
63
|
+
quantcli/factors/alpha101/alpha_026.yaml
|
|
64
|
+
quantcli/factors/alpha101/alpha_027.yaml
|
|
65
|
+
quantcli/factors/alpha101/alpha_028.yaml
|
|
66
|
+
quantcli/factors/alpha101/alpha_029.yaml
|
|
67
|
+
quantcli/factors/alpha101/alpha_030.yaml
|
|
68
|
+
quantcli/factors/alpha101/alpha_031.yaml
|
|
69
|
+
quantcli/factors/alpha101/alpha_032.yaml
|
|
70
|
+
quantcli/factors/alpha101/alpha_033.yaml
|
|
71
|
+
quantcli/factors/alpha101/alpha_034.yaml
|
|
72
|
+
quantcli/factors/alpha101/alpha_035.yaml
|
|
73
|
+
quantcli/factors/alpha101/alpha_036.yaml
|
|
74
|
+
quantcli/factors/alpha101/alpha_037.yaml
|
|
75
|
+
quantcli/factors/alpha101/alpha_038.yaml
|
|
76
|
+
quantcli/factors/alpha101/alpha_039.yaml
|
|
77
|
+
quantcli/factors/alpha101/alpha_040.yaml
|
|
38
78
|
quantcli/models/bar.py
|
|
39
79
|
quantcli/parser/__init__.py
|
|
40
80
|
quantcli/parser/constants.py
|
|
@@ -18,7 +18,11 @@ import numpy as np
|
|
|
18
18
|
from click.testing import CliRunner
|
|
19
19
|
|
|
20
20
|
from quantcli import cli
|
|
21
|
-
from quantcli.factors.loader import
|
|
21
|
+
from quantcli.factors.loader import (
|
|
22
|
+
load_strategy,
|
|
23
|
+
load_all_factors,
|
|
24
|
+
load_factor_from_ref,
|
|
25
|
+
)
|
|
22
26
|
from quantcli.factors.pipeline import FactorPipeline
|
|
23
27
|
|
|
24
28
|
|
|
@@ -38,15 +42,17 @@ def sample_price_data():
|
|
|
38
42
|
open_ = close * (1 + np.random.randn(100) * 0.01)
|
|
39
43
|
volume = np.random.randint(1000000, 10000000, 100)
|
|
40
44
|
|
|
41
|
-
return pd.DataFrame(
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
return pd.DataFrame(
|
|
46
|
+
{
|
|
47
|
+
"date": dates,
|
|
48
|
+
"symbol": ["600519"] * 100,
|
|
49
|
+
"open": open_,
|
|
50
|
+
"high": close * 1.02,
|
|
51
|
+
"low": close * 0.98,
|
|
52
|
+
"close": close,
|
|
53
|
+
"volume": volume,
|
|
54
|
+
}
|
|
55
|
+
)
|
|
50
56
|
|
|
51
57
|
|
|
52
58
|
@pytest.fixture
|
|
@@ -60,25 +66,27 @@ def multi_symbol_price_data():
|
|
|
60
66
|
|
|
61
67
|
for symbol in symbols:
|
|
62
68
|
close = 100 + np.cumsum(np.random.randn(60) * 0.5)
|
|
63
|
-
data[symbol] = pd.DataFrame(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
data[symbol] = pd.DataFrame(
|
|
70
|
+
{
|
|
71
|
+
"date": dates,
|
|
72
|
+
"symbol": [symbol] * 60,
|
|
73
|
+
"open": close * (1 + np.random.randn(60) * 0.01),
|
|
74
|
+
"high": close * 1.02,
|
|
75
|
+
"low": close * 0.98,
|
|
76
|
+
"close": close,
|
|
77
|
+
"volume": np.random.randint(1000000, 10000000, 60),
|
|
78
|
+
}
|
|
79
|
+
)
|
|
72
80
|
|
|
73
81
|
return data
|
|
74
82
|
|
|
75
83
|
|
|
76
|
-
class
|
|
77
|
-
"""quantcli
|
|
84
|
+
class TestFactorBuiltinListCommand:
|
|
85
|
+
"""quantcli factor list 命令测试"""
|
|
78
86
|
|
|
79
|
-
def
|
|
80
|
-
"""测试
|
|
81
|
-
result = runner.invoke(cli.
|
|
87
|
+
def test_factor_list_builtin_basic(self, runner):
|
|
88
|
+
"""测试 factor list 基本功能"""
|
|
89
|
+
result = runner.invoke(cli.factor_list, ["--json"])
|
|
82
90
|
|
|
83
91
|
assert result.exit_code == 0
|
|
84
92
|
output = json.loads(result.output)
|
|
@@ -87,20 +95,19 @@ class TestFactorsListCommand:
|
|
|
87
95
|
assert output["count"] == 40
|
|
88
96
|
assert len(output["factors"]) == 40
|
|
89
97
|
|
|
90
|
-
def
|
|
91
|
-
"""测试
|
|
92
|
-
result = runner.invoke(cli.
|
|
98
|
+
def test_factor_list_builtin_shows_alpha101(self, runner):
|
|
99
|
+
"""测试 factor list 显示 Alpha101 因子"""
|
|
100
|
+
result = runner.invoke(cli.factor_list, ["--json"])
|
|
93
101
|
|
|
94
102
|
assert result.exit_code == 0
|
|
95
103
|
output = json.loads(result.output)
|
|
96
104
|
|
|
97
|
-
# 检查包含 alpha_001
|
|
98
105
|
factor_files = [f["file"] for f in output["factors"]]
|
|
99
106
|
assert "alpha101/alpha_001.yaml" in factor_files
|
|
100
107
|
|
|
101
|
-
def
|
|
102
|
-
"""测试
|
|
103
|
-
result = runner.invoke(cli.
|
|
108
|
+
def test_factor_list_builtin_human_format(self, runner):
|
|
109
|
+
"""测试 factor list 人类友好格式"""
|
|
110
|
+
result = runner.invoke(cli.factor_list, [])
|
|
104
111
|
|
|
105
112
|
assert result.exit_code == 0
|
|
106
113
|
assert "Built-in Alpha101 Factors" in result.output
|
|
@@ -113,7 +120,9 @@ class TestBuiltinFactors:
|
|
|
113
120
|
|
|
114
121
|
def test_load_single_builtin_factor(self):
|
|
115
122
|
"""测试加载单个内置因子"""
|
|
116
|
-
factor = load_factor_from_ref(
|
|
123
|
+
factor = load_factor_from_ref(
|
|
124
|
+
"/Users/apple/quantcli/examples/strategies", "alpha101/alpha_001"
|
|
125
|
+
)
|
|
117
126
|
|
|
118
127
|
assert factor is not None
|
|
119
128
|
assert "alpha_001" in factor.name.lower() or "反转" in factor.description
|
|
@@ -135,7 +144,9 @@ class TestBuiltinFactors:
|
|
|
135
144
|
|
|
136
145
|
def test_builtin_factor_fields(self):
|
|
137
146
|
"""测试内置因子字段完整"""
|
|
138
|
-
factor = load_factor_from_ref(
|
|
147
|
+
factor = load_factor_from_ref(
|
|
148
|
+
"/Users/apple/quantcli/examples/strategies", "alpha101/alpha_008"
|
|
149
|
+
)
|
|
139
150
|
|
|
140
151
|
assert factor is not None
|
|
141
152
|
assert factor.name is not None
|
|
@@ -156,7 +167,9 @@ class TestBuiltinFactors:
|
|
|
156
167
|
# 测试每个因子都能加载
|
|
157
168
|
for yaml_file in yaml_files:
|
|
158
169
|
alpha_name = yaml_file.stem # alpha_001
|
|
159
|
-
factor = load_factor_from_ref(
|
|
170
|
+
factor = load_factor_from_ref(
|
|
171
|
+
"/Users/apple/quantcli/examples/strategies", f"alpha101/{alpha_name}"
|
|
172
|
+
)
|
|
160
173
|
assert factor is not None, f"Failed to load {yaml_file.name}"
|
|
161
174
|
|
|
162
175
|
|
|
@@ -248,8 +261,8 @@ class TestAnalyzeCommands:
|
|
|
248
261
|
window = 20
|
|
249
262
|
ic_rolling = []
|
|
250
263
|
for i in range(window, len(factor)):
|
|
251
|
-
f = factor.iloc[i-window:i]
|
|
252
|
-
r = forward_returns.iloc[i-window:i]
|
|
264
|
+
f = factor.iloc[i - window : i]
|
|
265
|
+
r = forward_returns.iloc[i - window : i]
|
|
253
266
|
valid_mask = ~(f.isna() | r.isna())
|
|
254
267
|
if valid_mask.sum() > 5:
|
|
255
268
|
ic, _ = spearmanr(f[valid_mask], r[valid_mask])
|
|
@@ -268,7 +281,7 @@ class TestFilterRunCommand:
|
|
|
268
281
|
|
|
269
282
|
def test_filter_config_loading(self, tmp_path):
|
|
270
283
|
"""测试 filter 配置加载"""
|
|
271
|
-
with tempfile.NamedTemporaryFile(mode=
|
|
284
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
|
272
285
|
f.write("""
|
|
273
286
|
name: 测试策略
|
|
274
287
|
version: 1.0.0
|
|
@@ -293,6 +306,7 @@ output:
|
|
|
293
306
|
assert config.name == "测试策略"
|
|
294
307
|
finally:
|
|
295
308
|
import os
|
|
309
|
+
|
|
296
310
|
os.unlink(config_path)
|
|
297
311
|
|
|
298
312
|
def test_filter_with_weights(self, tmp_path, multi_symbol_price_data):
|
|
@@ -300,7 +314,7 @@ output:
|
|
|
300
314
|
from quantcli.factors.compute import FactorComputer
|
|
301
315
|
from quantcli.factors.ranking import ScoringEngine
|
|
302
316
|
|
|
303
|
-
with tempfile.NamedTemporaryFile(mode=
|
|
317
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
|
304
318
|
f.write("""
|
|
305
319
|
name: 测试策略
|
|
306
320
|
version: 1.0.0
|
|
@@ -332,7 +346,7 @@ output:
|
|
|
332
346
|
factors,
|
|
333
347
|
multi_symbol_price_data,
|
|
334
348
|
{},
|
|
335
|
-
list(multi_symbol_price_data.keys())
|
|
349
|
+
list(multi_symbol_price_data.keys()),
|
|
336
350
|
)
|
|
337
351
|
|
|
338
352
|
assert "close_price" in factor_df.columns
|
|
@@ -345,6 +359,7 @@ output:
|
|
|
345
359
|
assert len(result) == len(multi_symbol_price_data)
|
|
346
360
|
finally:
|
|
347
361
|
import os
|
|
362
|
+
|
|
348
363
|
os.unlink(config_path)
|
|
349
364
|
|
|
350
365
|
def test_filter_with_conditions(self, tmp_path, multi_symbol_price_data):
|
|
@@ -352,7 +367,7 @@ output:
|
|
|
352
367
|
from quantcli.factors.compute import FactorComputer
|
|
353
368
|
from quantcli.factors.ranking import ScoringEngine
|
|
354
369
|
|
|
355
|
-
with tempfile.NamedTemporaryFile(mode=
|
|
370
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
|
356
371
|
f.write("""
|
|
357
372
|
name: 测试条件筛选
|
|
358
373
|
version: 1.0.0
|
|
@@ -387,7 +402,7 @@ output:
|
|
|
387
402
|
factors,
|
|
388
403
|
multi_symbol_price_data,
|
|
389
404
|
{},
|
|
390
|
-
list(multi_symbol_price_data.keys())
|
|
405
|
+
list(multi_symbol_price_data.keys()),
|
|
391
406
|
)
|
|
392
407
|
|
|
393
408
|
scorer = ScoringEngine(normalize="zscore")
|
|
@@ -397,6 +412,7 @@ output:
|
|
|
397
412
|
assert "score" in result.columns
|
|
398
413
|
finally:
|
|
399
414
|
import os
|
|
415
|
+
|
|
400
416
|
os.unlink(config_path)
|
|
401
417
|
|
|
402
418
|
def test_filter_json_output_format(self, tmp_path, multi_symbol_price_data):
|
|
@@ -406,7 +422,7 @@ output:
|
|
|
406
422
|
from quantcli.factors.compute import FactorComputer
|
|
407
423
|
from quantcli.factors.ranking import ScoringEngine
|
|
408
424
|
|
|
409
|
-
with tempfile.NamedTemporaryFile(mode=
|
|
425
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
|
410
426
|
f.write("""
|
|
411
427
|
name: 测试策略
|
|
412
428
|
version: 1.0.0
|
|
@@ -435,7 +451,7 @@ output:
|
|
|
435
451
|
factors,
|
|
436
452
|
multi_symbol_price_data,
|
|
437
453
|
{},
|
|
438
|
-
list(multi_symbol_price_data.keys())
|
|
454
|
+
list(multi_symbol_price_data.keys()),
|
|
439
455
|
)
|
|
440
456
|
|
|
441
457
|
scorer = ScoringEngine(normalize="zscore")
|
|
@@ -445,7 +461,7 @@ output:
|
|
|
445
461
|
output = {
|
|
446
462
|
"status": "success",
|
|
447
463
|
"count": len(result),
|
|
448
|
-
"results": result.to_dict(orient="records")
|
|
464
|
+
"results": result.to_dict(orient="records"),
|
|
449
465
|
}
|
|
450
466
|
|
|
451
467
|
assert output["status"] == "success"
|
|
@@ -454,6 +470,7 @@ output:
|
|
|
454
470
|
assert len(parsed) > 0
|
|
455
471
|
finally:
|
|
456
472
|
import os
|
|
473
|
+
|
|
457
474
|
os.unlink(config_path)
|
|
458
475
|
|
|
459
476
|
|
|
@@ -712,17 +729,22 @@ class TestFormulaSyntax:
|
|
|
712
729
|
computer = FactorComputer()
|
|
713
730
|
|
|
714
731
|
factors = {
|
|
715
|
-
"ma20": FactorDefinition(
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
"
|
|
732
|
+
"ma20": FactorDefinition(
|
|
733
|
+
name="ma20", type="technical", expr="ma(close, 20)"
|
|
734
|
+
),
|
|
735
|
+
"ema12": FactorDefinition(
|
|
736
|
+
name="ema12", type="technical", expr="ema(close, 12)"
|
|
737
|
+
),
|
|
738
|
+
"delay5": FactorDefinition(
|
|
739
|
+
name="delay5", type="technical", expr="delay(close, 5)"
|
|
740
|
+
),
|
|
741
|
+
"std20": FactorDefinition(
|
|
742
|
+
name="std20", type="technical", expr="rolling_std(close, 20)"
|
|
743
|
+
),
|
|
719
744
|
}
|
|
720
745
|
|
|
721
746
|
result = computer.compute_all_factors(
|
|
722
|
-
factors,
|
|
723
|
-
{"600519": sample_price_data},
|
|
724
|
-
{},
|
|
725
|
-
["600519"]
|
|
747
|
+
factors, {"600519": sample_price_data}, {}, ["600519"]
|
|
726
748
|
)
|
|
727
749
|
|
|
728
750
|
assert "ma20" in result.columns
|
|
@@ -739,14 +761,13 @@ class TestFormulaSyntax:
|
|
|
739
761
|
|
|
740
762
|
factors = {
|
|
741
763
|
"rank": FactorDefinition(name="rank", type="technical", expr="rank(close)"),
|
|
742
|
-
"zscore": FactorDefinition(
|
|
764
|
+
"zscore": FactorDefinition(
|
|
765
|
+
name="zscore", type="technical", expr="zscore(close)"
|
|
766
|
+
),
|
|
743
767
|
}
|
|
744
768
|
|
|
745
769
|
result = computer.compute_all_factors(
|
|
746
|
-
factors,
|
|
747
|
-
{"600519": sample_price_data},
|
|
748
|
-
{},
|
|
749
|
-
["600519"]
|
|
770
|
+
factors, {"600519": sample_price_data}, {}, ["600519"]
|
|
750
771
|
)
|
|
751
772
|
|
|
752
773
|
assert "rank" in result.columns
|
|
@@ -761,14 +782,13 @@ class TestFormulaSyntax:
|
|
|
761
782
|
|
|
762
783
|
# correlation 计算 - 价格与成交量的相关性
|
|
763
784
|
factors = {
|
|
764
|
-
"corr": FactorDefinition(
|
|
785
|
+
"corr": FactorDefinition(
|
|
786
|
+
name="corr", type="technical", expr="correlation(close, volume, 10)"
|
|
787
|
+
),
|
|
765
788
|
}
|
|
766
789
|
|
|
767
790
|
result = computer.compute_all_factors(
|
|
768
|
-
factors,
|
|
769
|
-
{"600519": sample_price_data},
|
|
770
|
-
{},
|
|
771
|
-
["600519"]
|
|
791
|
+
factors, {"600519": sample_price_data}, {}, ["600519"]
|
|
772
792
|
)
|
|
773
793
|
|
|
774
794
|
assert "corr" in result.columns
|
|
@@ -781,15 +801,16 @@ class TestFormulaSyntax:
|
|
|
781
801
|
computer = FactorComputer()
|
|
782
802
|
|
|
783
803
|
factors = {
|
|
784
|
-
"is_up": FactorDefinition(
|
|
785
|
-
|
|
804
|
+
"is_up": FactorDefinition(
|
|
805
|
+
name="is_up", type="technical", expr="where(close > open, 1, 0)"
|
|
806
|
+
),
|
|
807
|
+
"is_yinliang": FactorDefinition(
|
|
808
|
+
name="is_yinliang", type="technical", expr="where(close < open, 1, 0)"
|
|
809
|
+
),
|
|
786
810
|
}
|
|
787
811
|
|
|
788
812
|
result = computer.compute_all_factors(
|
|
789
|
-
factors,
|
|
790
|
-
{"600519": sample_price_data},
|
|
791
|
-
{},
|
|
792
|
-
["600519"]
|
|
813
|
+
factors, {"600519": sample_price_data}, {}, ["600519"]
|
|
793
814
|
)
|
|
794
815
|
|
|
795
816
|
assert "is_up" in result.columns
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|