alpholio 0.4.0__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 (42) hide show
  1. alpholio-0.4.0/LICENSE +21 -0
  2. alpholio-0.4.0/MANIFEST.in +6 -0
  3. alpholio-0.4.0/PKG-INFO +173 -0
  4. alpholio-0.4.0/README.md +137 -0
  5. alpholio-0.4.0/pyproject.toml +62 -0
  6. alpholio-0.4.0/setup.cfg +4 -0
  7. alpholio-0.4.0/src/alpholio/__init__.py +44 -0
  8. alpholio-0.4.0/src/alpholio/analyzer/__init__.py +18 -0
  9. alpholio-0.4.0/src/alpholio/analyzer/analyzer.py +135 -0
  10. alpholio-0.4.0/src/alpholio/analyzer/curves.py +85 -0
  11. alpholio-0.4.0/src/alpholio/analyzer/diagnostics.py +72 -0
  12. alpholio-0.4.0/src/alpholio/analyzer/metrics.py +100 -0
  13. alpholio-0.4.0/src/alpholio/api.py +518 -0
  14. alpholio-0.4.0/src/alpholio/benchmark.py +109 -0
  15. alpholio-0.4.0/src/alpholio/cli.py +38 -0
  16. alpholio-0.4.0/src/alpholio/config_schema.py +439 -0
  17. alpholio-0.4.0/src/alpholio/contracts.py +163 -0
  18. alpholio-0.4.0/src/alpholio/engine/__init__.py +27 -0
  19. alpholio-0.4.0/src/alpholio/engine/alignment.py +157 -0
  20. alpholio-0.4.0/src/alpholio/engine/cross_section.py +84 -0
  21. alpholio-0.4.0/src/alpholio/engine/engine.py +245 -0
  22. alpholio-0.4.0/src/alpholio/engine/weighting.py +56 -0
  23. alpholio-0.4.0/src/alpholio/frequency.py +74 -0
  24. alpholio-0.4.0/src/alpholio/input/__init__.py +24 -0
  25. alpholio-0.4.0/src/alpholio/input/processor.py +182 -0
  26. alpholio-0.4.0/src/alpholio/input/sources.py +253 -0
  27. alpholio-0.4.0/src/alpholio/io.py +145 -0
  28. alpholio-0.4.0/src/alpholio/pipeline.py +43 -0
  29. alpholio-0.4.0/src/alpholio/presets.py +64 -0
  30. alpholio-0.4.0/src/alpholio/registry.py +43 -0
  31. alpholio-0.4.0/src/alpholio/settings.py +44 -0
  32. alpholio-0.4.0/src/alpholio/visualizer/__init__.py +16 -0
  33. alpholio-0.4.0/src/alpholio/visualizer/charts.py +204 -0
  34. alpholio-0.4.0/src/alpholio/visualizer/style.py +134 -0
  35. alpholio-0.4.0/src/alpholio/visualizer/tables.py +63 -0
  36. alpholio-0.4.0/src/alpholio/visualizer/visualizer.py +123 -0
  37. alpholio-0.4.0/src/alpholio.egg-info/PKG-INFO +173 -0
  38. alpholio-0.4.0/src/alpholio.egg-info/SOURCES.txt +40 -0
  39. alpholio-0.4.0/src/alpholio.egg-info/dependency_links.txt +1 -0
  40. alpholio-0.4.0/src/alpholio.egg-info/entry_points.txt +2 -0
  41. alpholio-0.4.0/src/alpholio.egg-info/requires.txt +9 -0
  42. alpholio-0.4.0/src/alpholio.egg-info/top_level.txt +1 -0
alpholio-0.4.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 myang682
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # src/alpholio/data/ 下的 S&P 500 序列受数据使用权限制,不得对外分发。
2
+ #
3
+ # pyproject.toml 的 include-package-data=false 只挡住 wheel;sdist 的文件清单由本文件
4
+ # 决定,默认会把包目录内的全部文件一并打包,因此必须在此显式排除。
5
+ # 两处缺一,构建产物就会重新带上该数据。
6
+ prune src/alpholio/data
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.4
2
+ Name: alpholio
3
+ Version: 0.4.0
4
+ Summary: Config-driven cross-sectional portfolio backtesting toolkit
5
+ Author: myang682
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/mzyang-code/alpholio
8
+ Project-URL: Documentation, https://mzyang-code.github.io/alpholio/
9
+ Project-URL: Source, https://github.com/mzyang-code/alpholio
10
+ Project-URL: Issues, https://github.com/mzyang-code/alpholio/issues
11
+ Keywords: quant,portfolio,backtest,cross-section,finance
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Financial and Insurance Industry
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Office/Business :: Financial :: Investment
23
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: numpy>=1.23
28
+ Requires-Dist: pandas>=1.5
29
+ Requires-Dist: pyarrow>=10.0
30
+ Requires-Dist: matplotlib>=3.6
31
+ Provides-Extra: docs
32
+ Requires-Dist: mkdocs>=1.6; extra == "docs"
33
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
34
+ Requires-Dist: jieba>=0.42; extra == "docs"
35
+ Dynamic: license-file
36
+
37
+ # alpholio
38
+
39
+ Cross-sectional portfolio backtesting. The toolkit consumes alpha and handles bucketing,
40
+ weighting, metrics and charts.
41
+
42
+ **📖 [Documentation](https://mzyang-code.github.io/alpholio/)** (Chinese) | [简体中文 README](https://github.com/mzyang-code/alpholio/blob/main/README.zh-CN.md)
43
+
44
+ Four modules, one direction of data flow. Each module owns a single public entry point,
45
+ so any stage can be swapped without touching the others.
46
+
47
+ ```
48
+ InputProcessor ──InputBundle──▶ PortfolioEngine ──EngineResult──▶ Analyzer ──AnalysisResult──▶ Visualizer ──▶ PNG / CSV
49
+ ```
50
+
51
+ The toolkit consumes alpha only — it never generates signals. Price-based factors such as
52
+ MOM / STR / WSTR are ordinary inputs; see the
53
+ [daily MOM example](https://mzyang-code.github.io/alpholio/examples/daily-mom/) for one
54
+ worked end to end.
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ pip install alpholio
60
+ ```
61
+
62
+ Working from a clone instead:
63
+
64
+ ```bash
65
+ pip install -e . # compatible ranges
66
+ pip install -e ".[docs]" # adds the documentation site toolchain
67
+ ```
68
+
69
+ To reproduce the exact verified combination (Python 3.12.2):
70
+
71
+ ```bash
72
+ conda env create -f environment.yml
73
+ conda activate alpholio
74
+ pip install -e .
75
+ ```
76
+
77
+ ## Quick start
78
+
79
+ Factor and price tables go in as DataFrames — no need to write them to disk first:
80
+
81
+ ```python
82
+ import alpholio as alp
83
+
84
+ bt = alp.backtest(signals=alpha_df, prices=price_df, horizon=5)
85
+
86
+ bt.summary() # metrics per (signal_model, bucket, weight)
87
+ bt.plot("long_short") # long-short equity curve, returns a matplotlib Figure
88
+ bt.plot("deciles") # per-quantile gradient chart
89
+ bt.save("outputs/") # PNG charts and CSV metrics to disk
90
+
91
+ bt.returns # per-period portfolio returns, long format
92
+ bt.curves # equity and cumulative log-return curves
93
+ ```
94
+
95
+ `horizon` is the measurement window of each period's realised return and is required.
96
+ Column mapping is inferred when the source names already match the contract
97
+ (`date` / `id` / `alpha`); the rebalance interval defaults to `horizon`; and value
98
+ weighting is added automatically when the price table carries a `cap` column.
99
+
100
+ Monthly panels only need a `frequency`: `horizon` and the rebalance interval then count
101
+ calendar months, and metrics annualise at 12 periods per year.
102
+
103
+ ```python
104
+ bt = alp.backtest(signals=alpha_df, prices=panel_df, horizon=1, frequency="monthly")
105
+ ```
106
+
107
+ File paths work interchangeably with in-memory tables:
108
+
109
+ ```python
110
+ bt = alp.backtest(signals="alpha.feather", prices="prices.feather", horizon=5)
111
+ bt = alp.backtest(signals={"MOM": mom_df, "REV": rev_df}, prices=price_df, horizon=5)
112
+ ```
113
+
114
+ ### Batch runs and reproducible archives
115
+
116
+ The JSON config directory remains a first-class entry point, suited to batch execution
117
+ on a server and to shipping alongside a paper:
118
+
119
+ ```bash
120
+ alpholio run --config-dir configs/
121
+ ```
122
+
123
+ ```python
124
+ result = alp.run_pipeline("configs/")
125
+ ```
126
+
127
+ Both paths share the same validation and computation and agree value for value.
128
+ Parameters settled in a notebook export back out into a config directory:
129
+
130
+ ```python
131
+ bt.to_config("paper/configs/", data_dir="paper/data/")
132
+ ```
133
+
134
+ ## Documentation
135
+
136
+ The full documentation is written in Chinese. Direct links:
137
+
138
+ | Page | Contents |
139
+ |---|---|
140
+ | [Quick start](https://mzyang-code.github.io/alpholio/guide/quickstart/) | Install through the first equity curve |
141
+ | [Concepts](https://mzyang-code.github.io/alpholio/guide/concepts/) | Four modules, three data contracts, extension points |
142
+ | [Preparing input](https://mzyang-code.github.io/alpholio/guide/prepare-data/) | Signal table format, column mapping, forward-return sources |
143
+ | [Data frequency](https://mzyang-code.github.io/alpholio/guide/frequency/) | Daily vs monthly: units, alignment rules, annualisation base |
144
+ | [Multiple signals](https://mzyang-code.github.io/alpholio/guide/multi-signal/) | Running several alphas at once, and how charts split |
145
+ | [Outputs](https://mzyang-code.github.io/alpholio/guide/outputs/) | File listing, naming rules, long-table structure |
146
+ | [Math contract](https://mzyang-code.github.io/alpholio/guide/math/) | The exact formula behind every metric |
147
+ | [Daily example](https://mzyang-code.github.io/alpholio/examples/daily-mom/) | MOM 12-2 end to end: input shape, call, measured results |
148
+ | [Monthly example](https://mzyang-code.github.io/alpholio/examples/monthly-panel/) | Three alphas off one monthly panel, `frequency="monthly"` |
149
+ | [Python API](https://mzyang-code.github.io/alpholio/reference/api/) | `backtest()` parameters and the result object |
150
+ | [Config reference](https://mzyang-code.github.io/alpholio/reference/config-input/) | Per-field types, defaults and constraints |
151
+
152
+ ## Benchmarks
153
+
154
+ No market index data is bundled. Benchmark series are supplied by the user through
155
+ `input.references`, enter the result table under bucket `REF`, and appear on charts once
156
+ `"REF"` is listed in a chart's `buckets`.
157
+
158
+ Equal-weighted portfolios should be paired with an equal-weighted index and cap-weighted
159
+ with a cap-weighted index — only like-for-like comparisons mean anything. A single
160
+ `references` entry is replicated across every weighting scheme, so pairing one index per
161
+ scheme takes two entries plus two chart configs constraining `weights` and `signals`. See
162
+ [Multiple signals](https://mzyang-code.github.io/alpholio/guide/multi-signal/).
163
+
164
+ ## Building the docs locally
165
+
166
+ ```bash
167
+ pip install -e ".[docs]"
168
+ mkdocs serve
169
+ ```
170
+
171
+ ## License
172
+
173
+ MIT
@@ -0,0 +1,137 @@
1
+ # alpholio
2
+
3
+ Cross-sectional portfolio backtesting. The toolkit consumes alpha and handles bucketing,
4
+ weighting, metrics and charts.
5
+
6
+ **📖 [Documentation](https://mzyang-code.github.io/alpholio/)** (Chinese) | [简体中文 README](https://github.com/mzyang-code/alpholio/blob/main/README.zh-CN.md)
7
+
8
+ Four modules, one direction of data flow. Each module owns a single public entry point,
9
+ so any stage can be swapped without touching the others.
10
+
11
+ ```
12
+ InputProcessor ──InputBundle──▶ PortfolioEngine ──EngineResult──▶ Analyzer ──AnalysisResult──▶ Visualizer ──▶ PNG / CSV
13
+ ```
14
+
15
+ The toolkit consumes alpha only — it never generates signals. Price-based factors such as
16
+ MOM / STR / WSTR are ordinary inputs; see the
17
+ [daily MOM example](https://mzyang-code.github.io/alpholio/examples/daily-mom/) for one
18
+ worked end to end.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ pip install alpholio
24
+ ```
25
+
26
+ Working from a clone instead:
27
+
28
+ ```bash
29
+ pip install -e . # compatible ranges
30
+ pip install -e ".[docs]" # adds the documentation site toolchain
31
+ ```
32
+
33
+ To reproduce the exact verified combination (Python 3.12.2):
34
+
35
+ ```bash
36
+ conda env create -f environment.yml
37
+ conda activate alpholio
38
+ pip install -e .
39
+ ```
40
+
41
+ ## Quick start
42
+
43
+ Factor and price tables go in as DataFrames — no need to write them to disk first:
44
+
45
+ ```python
46
+ import alpholio as alp
47
+
48
+ bt = alp.backtest(signals=alpha_df, prices=price_df, horizon=5)
49
+
50
+ bt.summary() # metrics per (signal_model, bucket, weight)
51
+ bt.plot("long_short") # long-short equity curve, returns a matplotlib Figure
52
+ bt.plot("deciles") # per-quantile gradient chart
53
+ bt.save("outputs/") # PNG charts and CSV metrics to disk
54
+
55
+ bt.returns # per-period portfolio returns, long format
56
+ bt.curves # equity and cumulative log-return curves
57
+ ```
58
+
59
+ `horizon` is the measurement window of each period's realised return and is required.
60
+ Column mapping is inferred when the source names already match the contract
61
+ (`date` / `id` / `alpha`); the rebalance interval defaults to `horizon`; and value
62
+ weighting is added automatically when the price table carries a `cap` column.
63
+
64
+ Monthly panels only need a `frequency`: `horizon` and the rebalance interval then count
65
+ calendar months, and metrics annualise at 12 periods per year.
66
+
67
+ ```python
68
+ bt = alp.backtest(signals=alpha_df, prices=panel_df, horizon=1, frequency="monthly")
69
+ ```
70
+
71
+ File paths work interchangeably with in-memory tables:
72
+
73
+ ```python
74
+ bt = alp.backtest(signals="alpha.feather", prices="prices.feather", horizon=5)
75
+ bt = alp.backtest(signals={"MOM": mom_df, "REV": rev_df}, prices=price_df, horizon=5)
76
+ ```
77
+
78
+ ### Batch runs and reproducible archives
79
+
80
+ The JSON config directory remains a first-class entry point, suited to batch execution
81
+ on a server and to shipping alongside a paper:
82
+
83
+ ```bash
84
+ alpholio run --config-dir configs/
85
+ ```
86
+
87
+ ```python
88
+ result = alp.run_pipeline("configs/")
89
+ ```
90
+
91
+ Both paths share the same validation and computation and agree value for value.
92
+ Parameters settled in a notebook export back out into a config directory:
93
+
94
+ ```python
95
+ bt.to_config("paper/configs/", data_dir="paper/data/")
96
+ ```
97
+
98
+ ## Documentation
99
+
100
+ The full documentation is written in Chinese. Direct links:
101
+
102
+ | Page | Contents |
103
+ |---|---|
104
+ | [Quick start](https://mzyang-code.github.io/alpholio/guide/quickstart/) | Install through the first equity curve |
105
+ | [Concepts](https://mzyang-code.github.io/alpholio/guide/concepts/) | Four modules, three data contracts, extension points |
106
+ | [Preparing input](https://mzyang-code.github.io/alpholio/guide/prepare-data/) | Signal table format, column mapping, forward-return sources |
107
+ | [Data frequency](https://mzyang-code.github.io/alpholio/guide/frequency/) | Daily vs monthly: units, alignment rules, annualisation base |
108
+ | [Multiple signals](https://mzyang-code.github.io/alpholio/guide/multi-signal/) | Running several alphas at once, and how charts split |
109
+ | [Outputs](https://mzyang-code.github.io/alpholio/guide/outputs/) | File listing, naming rules, long-table structure |
110
+ | [Math contract](https://mzyang-code.github.io/alpholio/guide/math/) | The exact formula behind every metric |
111
+ | [Daily example](https://mzyang-code.github.io/alpholio/examples/daily-mom/) | MOM 12-2 end to end: input shape, call, measured results |
112
+ | [Monthly example](https://mzyang-code.github.io/alpholio/examples/monthly-panel/) | Three alphas off one monthly panel, `frequency="monthly"` |
113
+ | [Python API](https://mzyang-code.github.io/alpholio/reference/api/) | `backtest()` parameters and the result object |
114
+ | [Config reference](https://mzyang-code.github.io/alpholio/reference/config-input/) | Per-field types, defaults and constraints |
115
+
116
+ ## Benchmarks
117
+
118
+ No market index data is bundled. Benchmark series are supplied by the user through
119
+ `input.references`, enter the result table under bucket `REF`, and appear on charts once
120
+ `"REF"` is listed in a chart's `buckets`.
121
+
122
+ Equal-weighted portfolios should be paired with an equal-weighted index and cap-weighted
123
+ with a cap-weighted index — only like-for-like comparisons mean anything. A single
124
+ `references` entry is replicated across every weighting scheme, so pairing one index per
125
+ scheme takes two entries plus two chart configs constraining `weights` and `signals`. See
126
+ [Multiple signals](https://mzyang-code.github.io/alpholio/guide/multi-signal/).
127
+
128
+ ## Building the docs locally
129
+
130
+ ```bash
131
+ pip install -e ".[docs]"
132
+ mkdocs serve
133
+ ```
134
+
135
+ ## License
136
+
137
+ MIT
@@ -0,0 +1,62 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "alpholio"
7
+ # 版本号的唯一真源是 src/alpholio/__init__.py 的 __version__,见 tool.setuptools.dynamic
8
+ dynamic = ["version"]
9
+ description = "Config-driven cross-sectional portfolio backtesting toolkit"
10
+ readme = "README.md"
11
+ requires-python = ">=3.10"
12
+ license = { text = "MIT" }
13
+ authors = [{ name = "myang682" }]
14
+ keywords = ["quant", "portfolio", "backtest", "cross-section", "finance"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Financial and Insurance Industry",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Office/Business :: Financial :: Investment",
27
+ "Topic :: Scientific/Engineering :: Information Analysis",
28
+ ]
29
+ # 兼容区间。确切的已验证版本组合见 environment.yml
30
+ dependencies = [
31
+ "numpy>=1.23",
32
+ "pandas>=1.5",
33
+ "pyarrow>=10.0",
34
+ "matplotlib>=3.6", # 需要 mpl.colormaps 注册表
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ # 构建 docs/ 下的文档站用。jieba 非可选:缺失时中文正文不分词,
39
+ # 搜索索引退化为整句匹配,站内检索基本失效
40
+ docs = ["mkdocs>=1.6", "mkdocs-material>=9.5", "jieba>=0.42"]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/mzyang-code/alpholio"
44
+ Documentation = "https://mzyang-code.github.io/alpholio/"
45
+ Source = "https://github.com/mzyang-code/alpholio"
46
+ Issues = "https://github.com/mzyang-code/alpholio/issues"
47
+
48
+ [project.scripts]
49
+ alpholio = "alpholio.cli:main"
50
+
51
+ [tool.setuptools]
52
+ # data/ 下的 S&P 500 序列受数据使用权限制,不得随 sdist 与 wheel 分发。
53
+ # 该项在 pyproject.toml 下默认为 true,会把包目录内的非 .py 文件一并收进构建产物,
54
+ # 因此必须显式关闭;同时不声明任何 package-data。
55
+ # 基准数据由使用者自行准备,见 input.references。
56
+ include-package-data = false
57
+
58
+ [tool.setuptools.dynamic]
59
+ version = { attr = "alpholio.__version__" }
60
+
61
+ [tool.setuptools.packages.find]
62
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,44 @@
1
+ # 跨截面组合回测工具包:Input → Engine → Analyzer → Visualizer 单向数据流
2
+ from .analyzer import Analyzer
3
+ from .api import BacktestResult, backtest
4
+ from .config_schema import AnalyzerConfig, EngineConfig, InputConfig, PipelineConfig, VisualizerConfig
5
+ from .contracts import AnalysisResult, EngineResult, InputBundle, to_legacy_wide
6
+ from .engine import PortfolioEngine
7
+ from .input import InputProcessor
8
+ from .pipeline import PipelineResult, run_pipeline
9
+ from .settings import settings
10
+ from .visualizer import Visualizer
11
+
12
+ __version__ = "0.4.0"
13
+ __all__ = [
14
+ # 一行跑通的门面入口
15
+ "backtest",
16
+ "BacktestResult",
17
+ "settings",
18
+
19
+ # 核心类
20
+ "InputProcessor",
21
+ "PortfolioEngine",
22
+ "Analyzer",
23
+ "Visualizer",
24
+
25
+ # 辅助函数和数据类
26
+ "run_pipeline",
27
+ "PipelineResult",
28
+ "InputConfig",
29
+ "EngineConfig",
30
+ "AnalyzerConfig",
31
+ "VisualizerConfig",
32
+ "PipelineConfig",
33
+
34
+ # 用于流转的中间产物
35
+ "InputBundle",
36
+ "EngineResult",
37
+ "AnalysisResult",
38
+
39
+ # 格式转换函数
40
+ "to_legacy_wide",
41
+
42
+ # 版本号
43
+ "__version__",
44
+ ]
@@ -0,0 +1,18 @@
1
+ # ③ Analyzer:组合收益 → 指标 / 曲线 / 诊断
2
+ from .analyzer import Analyzer
3
+ from .curves import build_curves, shared_origin, vol_rescale_to_reference
4
+ from .diagnostics import compute_ic, compute_turnover
5
+ from .metrics import METRICS, Metric, MetricContext, build_metric
6
+
7
+ __all__ = [
8
+ "Analyzer",
9
+ "Metric",
10
+ "METRICS",
11
+ "MetricContext",
12
+ "build_metric",
13
+ "compute_turnover",
14
+ "compute_ic",
15
+ "build_curves",
16
+ "shared_origin",
17
+ "vol_rescale_to_reference",
18
+ ]
@@ -0,0 +1,135 @@
1
+ # ③ Analyzer:组合收益 → 指标汇总 + 曲线 + 诊断
2
+ from __future__ import annotations
3
+
4
+ from typing import Dict, List
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+ from ..config_schema import AnalyzerConfig
10
+ from ..contracts import (
11
+ BUCKET,
12
+ DATE,
13
+ IC,
14
+ N_NAMES,
15
+ RET,
16
+ SIGNAL,
17
+ TURNOVER,
18
+ WEIGHT,
19
+ AnalysisResult,
20
+ ContractError,
21
+ EngineResult,
22
+ sort_by_bucket,
23
+ )
24
+ from ..frequency import DAILY, resolve
25
+ from .curves import build_curves, shared_origin, vol_rescale_to_reference
26
+ from .diagnostics import compute_ic, compute_turnover
27
+ from .metrics import MetricContext, build_metric
28
+
29
+
30
+ class Analyzer:
31
+ def __init__(self, cfg: AnalyzerConfig) -> None:
32
+ self.cfg = cfg
33
+ self.metrics = [build_metric(name) for name in cfg.metrics]
34
+
35
+ # 唯一出口
36
+ def run(self, result: EngineResult) -> AnalysisResult:
37
+ cfg = self.cfg
38
+ ppy = self._periods_per_year(result.meta)
39
+ ctx = MetricContext(periods_per_year=ppy, risk_free_rate=cfg.risk_free_rate)
40
+
41
+ turnover = (
42
+ compute_turnover(
43
+ result.members,
44
+ long_short_label=result.meta.get("long_short_label"),
45
+ long_bucket=result.meta.get("long_bucket"),
46
+ short_bucket=result.meta.get("short_bucket"),
47
+ )
48
+ if cfg.diagnostics.turnover
49
+ else _empty(SIGNAL, BUCKET, DATE, TURNOVER)
50
+ )
51
+ ic = (
52
+ compute_ic(result.aligned, cfg.diagnostics.ic_min_names)
53
+ if cfg.diagnostics.ic
54
+ else _empty(SIGNAL, DATE, IC, N_NAMES)
55
+ )
56
+
57
+ returns = self._rescale(result.returns)
58
+ origin = shared_origin(returns) if cfg.curves.shared_origin else None
59
+
60
+ return AnalysisResult(
61
+ summary=self._summarize(returns, ctx, turnover, ic),
62
+ curves=build_curves(returns, cfg.curves.clip_lower, origin),
63
+ turnover=turnover,
64
+ ic=ic,
65
+ meta={
66
+ "periods_per_year": ppy,
67
+ "metrics": [m.name for m in self.metrics],
68
+ "vol_rescaled": bool(cfg.vol_rescale.enabled),
69
+ "vol_rescale_reference": cfg.vol_rescale.reference if cfg.vol_rescale.enabled else None,
70
+ **{
71
+ k: result.meta[k]
72
+ for k in ("frequency", "holding_days", "n_buckets")
73
+ if k in result.meta
74
+ },
75
+ },
76
+ )
77
+
78
+ # 开关打开时整体替换为缩放后的收益,不再并列输出两套口径
79
+ def _rescale(self, returns: pd.DataFrame) -> pd.DataFrame:
80
+ spec = self.cfg.vol_rescale
81
+ if not spec.enabled:
82
+ return returns
83
+ if not spec.reference:
84
+ raise ContractError(
85
+ "analyzer.vol_rescale.enabled=true 时必须指定 reference(缩放到哪个基准)"
86
+ )
87
+ return vol_rescale_to_reference(returns, spec.reference, spec.min_periods)
88
+
89
+ # 年化因子:显式配置优先,否则由「年化基数 / 持有期」推导。
90
+ # 基数随面板频率:日频取 analyzer.trading_days_per_year(默认 252),月频取 12。
91
+ # 频率沿 InputBundle → EngineResult.meta 传来,缺省按日频处理。
92
+ def _periods_per_year(self, engine_meta: Dict) -> float:
93
+ if self.cfg.periods_per_year:
94
+ return float(self.cfg.periods_per_year)
95
+ frequency = resolve(engine_meta.get("frequency", DAILY))
96
+ base = (
97
+ frequency.bars_per_year
98
+ if frequency.is_monthly
99
+ else float(self.cfg.trading_days_per_year)
100
+ )
101
+ holding = int(engine_meta.get("holding_days", 1))
102
+ return base / max(holding, 1)
103
+
104
+ def _summarize(
105
+ self, returns: pd.DataFrame, ctx: MetricContext, turnover: pd.DataFrame, ic: pd.DataFrame
106
+ ) -> pd.DataFrame:
107
+ turnover_mean = _mean_by(turnover, [SIGNAL, BUCKET], TURNOVER)
108
+ ic_mean = _mean_by(ic, [SIGNAL], IC)
109
+ rows: List[Dict] = []
110
+ for (signal, bucket, weight), grp in returns.groupby(
111
+ [SIGNAL, BUCKET, WEIGHT], sort=True
112
+ ):
113
+ series = grp[RET].dropna().to_numpy(dtype=np.float64)
114
+ if len(series) == 0:
115
+ continue
116
+ row = {
117
+ SIGNAL: signal, BUCKET: bucket, WEIGHT: weight,
118
+ "n_periods": int(len(series)),
119
+ }
120
+ row.update({m.name: m.compute(series, ctx) for m in self.metrics})
121
+ row[TURNOVER] = turnover_mean.get((signal, bucket), np.nan)
122
+ row["ic_mean"] = ic_mean.get(signal, np.nan)
123
+ rows.append(row)
124
+ return sort_by_bucket(pd.DataFrame(rows), [SIGNAL, WEIGHT])
125
+
126
+
127
+ def _empty(*columns: str) -> pd.DataFrame:
128
+ return pd.DataFrame(columns=list(columns))
129
+
130
+
131
+ def _mean_by(frame: pd.DataFrame, keys: List[str], column: str) -> Dict:
132
+ if frame.empty or column not in frame.columns:
133
+ return {}
134
+ grouped = frame.groupby(keys[0] if len(keys) == 1 else keys, sort=False)[column].mean()
135
+ return grouped.to_dict()