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.
- alpholio-0.4.0/LICENSE +21 -0
- alpholio-0.4.0/MANIFEST.in +6 -0
- alpholio-0.4.0/PKG-INFO +173 -0
- alpholio-0.4.0/README.md +137 -0
- alpholio-0.4.0/pyproject.toml +62 -0
- alpholio-0.4.0/setup.cfg +4 -0
- alpholio-0.4.0/src/alpholio/__init__.py +44 -0
- alpholio-0.4.0/src/alpholio/analyzer/__init__.py +18 -0
- alpholio-0.4.0/src/alpholio/analyzer/analyzer.py +135 -0
- alpholio-0.4.0/src/alpholio/analyzer/curves.py +85 -0
- alpholio-0.4.0/src/alpholio/analyzer/diagnostics.py +72 -0
- alpholio-0.4.0/src/alpholio/analyzer/metrics.py +100 -0
- alpholio-0.4.0/src/alpholio/api.py +518 -0
- alpholio-0.4.0/src/alpholio/benchmark.py +109 -0
- alpholio-0.4.0/src/alpholio/cli.py +38 -0
- alpholio-0.4.0/src/alpholio/config_schema.py +439 -0
- alpholio-0.4.0/src/alpholio/contracts.py +163 -0
- alpholio-0.4.0/src/alpholio/engine/__init__.py +27 -0
- alpholio-0.4.0/src/alpholio/engine/alignment.py +157 -0
- alpholio-0.4.0/src/alpholio/engine/cross_section.py +84 -0
- alpholio-0.4.0/src/alpholio/engine/engine.py +245 -0
- alpholio-0.4.0/src/alpholio/engine/weighting.py +56 -0
- alpholio-0.4.0/src/alpholio/frequency.py +74 -0
- alpholio-0.4.0/src/alpholio/input/__init__.py +24 -0
- alpholio-0.4.0/src/alpholio/input/processor.py +182 -0
- alpholio-0.4.0/src/alpholio/input/sources.py +253 -0
- alpholio-0.4.0/src/alpholio/io.py +145 -0
- alpholio-0.4.0/src/alpholio/pipeline.py +43 -0
- alpholio-0.4.0/src/alpholio/presets.py +64 -0
- alpholio-0.4.0/src/alpholio/registry.py +43 -0
- alpholio-0.4.0/src/alpholio/settings.py +44 -0
- alpholio-0.4.0/src/alpholio/visualizer/__init__.py +16 -0
- alpholio-0.4.0/src/alpholio/visualizer/charts.py +204 -0
- alpholio-0.4.0/src/alpholio/visualizer/style.py +134 -0
- alpholio-0.4.0/src/alpholio/visualizer/tables.py +63 -0
- alpholio-0.4.0/src/alpholio/visualizer/visualizer.py +123 -0
- alpholio-0.4.0/src/alpholio.egg-info/PKG-INFO +173 -0
- alpholio-0.4.0/src/alpholio.egg-info/SOURCES.txt +40 -0
- alpholio-0.4.0/src/alpholio.egg-info/dependency_links.txt +1 -0
- alpholio-0.4.0/src/alpholio.egg-info/entry_points.txt +2 -0
- alpholio-0.4.0/src/alpholio.egg-info/requires.txt +9 -0
- 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.
|
alpholio-0.4.0/PKG-INFO
ADDED
|
@@ -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
|
alpholio-0.4.0/README.md
ADDED
|
@@ -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"]
|
alpholio-0.4.0/setup.cfg
ADDED
|
@@ -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()
|