phacker 0.1.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.
- phacker-0.1.0/.gitignore +218 -0
- phacker-0.1.0/CMakeLists.txt +39 -0
- phacker-0.1.0/LICENSE +21 -0
- phacker-0.1.0/PKG-INFO +231 -0
- phacker-0.1.0/README.md +216 -0
- phacker-0.1.0/pyproject.toml +44 -0
- phacker-0.1.0/src/cpp/bindings.cpp +40 -0
- phacker-0.1.0/src/cpp/block_stats.cpp +62 -0
- phacker-0.1.0/src/cpp/cscv.hpp +28 -0
- phacker-0.1.0/src/cpp/cscv_kernel.cpp +159 -0
- phacker-0.1.0/src/phacker/__init__.py +8 -0
- phacker-0.1.0/src/phacker/cscv.py +51 -0
- phacker-0.1.0/src/phacker/grid.py +174 -0
- phacker-0.1.0/src/phacker/indicators.py +107 -0
- phacker-0.1.0/src/phacker/stats.py +44 -0
- phacker-0.1.0/tests/conftest.py +8 -0
- phacker-0.1.0/tests/test_algorithm.py +254 -0
- phacker-0.1.0/tests/test_ground_truth.py +139 -0
- phacker-0.1.0/tests/test_parity.py +77 -0
phacker-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.18...3.31)
|
|
2
|
+
project(${SKBUILD_PROJECT_NAME} LANGUAGES NONE)
|
|
3
|
+
|
|
4
|
+
# The kernel is optional: with no compiler the wheel ships the NumPy reference alone.
|
|
5
|
+
include(CheckLanguage)
|
|
6
|
+
check_language(CXX)
|
|
7
|
+
if(NOT CMAKE_CXX_COMPILER)
|
|
8
|
+
message(WARNING "No C++ compiler found, building phacker without the C++ kernel")
|
|
9
|
+
return()
|
|
10
|
+
endif()
|
|
11
|
+
enable_language(CXX)
|
|
12
|
+
|
|
13
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
14
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
15
|
+
|
|
16
|
+
find_package(Python 3.11 REQUIRED COMPONENTS Interpreter Development.Module)
|
|
17
|
+
find_package(nanobind CONFIG REQUIRED)
|
|
18
|
+
|
|
19
|
+
nanobind_add_module(_core NB_STATIC
|
|
20
|
+
src/cpp/bindings.cpp
|
|
21
|
+
src/cpp/block_stats.cpp
|
|
22
|
+
src/cpp/cscv_kernel.cpp)
|
|
23
|
+
|
|
24
|
+
if(MSVC)
|
|
25
|
+
# plain /openmp is OpenMP 2.0 and rejects `omp simd`
|
|
26
|
+
target_compile_options(_core PRIVATE /O2 /openmp:experimental)
|
|
27
|
+
else()
|
|
28
|
+
find_package(OpenMP COMPONENTS CXX) # absent on Apple clang; the kernel then runs serially
|
|
29
|
+
if(OpenMP_CXX_FOUND)
|
|
30
|
+
target_link_libraries(_core PRIVATE OpenMP::OpenMP_CXX)
|
|
31
|
+
endif()
|
|
32
|
+
target_compile_options(_core PRIVATE -O3 -fno-math-errno -Wno-unknown-pragmas)
|
|
33
|
+
# never -march=native in a wheel (SIGILL on older CPUs); x86-64-v2 is the portable baseline
|
|
34
|
+
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64")
|
|
35
|
+
target_compile_options(_core PRIVATE -march=x86-64-v2)
|
|
36
|
+
endif()
|
|
37
|
+
endif()
|
|
38
|
+
|
|
39
|
+
install(TARGETS _core LIBRARY DESTINATION phacker)
|
phacker-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ryousuf569
|
|
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.
|
phacker-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phacker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Probability of backtest overfitting via CSCV (Bailey et al. 2016), with a C++ kernel
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Project-URL: Source, https://github.com/ryousuf569/phacker
|
|
8
|
+
Project-URL: Paper, https://doi.org/10.21314/JCF.2016.322
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: numpy>=1.24
|
|
11
|
+
Requires-Dist: pandas>=2.0
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest; extra == "test"
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# phacker
|
|
17
|
+
|
|
18
|
+
Measure how likely it is that your best backtest is overfit.
|
|
19
|
+
|
|
20
|
+
If you try many strategy configurations and keep the one with the best in-sample
|
|
21
|
+
Sharpe ratio, part of that Sharpe is selection luck. `phacker` estimates the
|
|
22
|
+
**Probability of Backtest Overfitting (PBO)**: the probability that the configuration
|
|
23
|
+
you picked as best in-sample ranks below the median out-of-sample. It implements
|
|
24
|
+
combinatorially symmetric cross-validation (CSCV) from
|
|
25
|
+
|
|
26
|
+
> Bailey, D., Borwein, J., López de Prado, M., & Zhu, Q. J. (2016).
|
|
27
|
+
> *The Probability of Backtest Overfitting.* Journal of Computational Finance, 20(4), 39–69.
|
|
28
|
+
> [doi:10.21314/JCF.2016.322](https://doi.org/10.21314/JCF.2016.322)
|
|
29
|
+
|
|
30
|
+
CSCV is deterministic: there is no Monte Carlo, and the same input always gives the same
|
|
31
|
+
answer. The core is a C++ kernel (OpenMP, SIMD) with a pure-NumPy fallback.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install phacker
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Wheels are built for CPython 3.11–3.13 on Linux (x86_64, aarch64), macOS (Intel,
|
|
40
|
+
Apple Silicon) and Windows (x64). On other platforms pip builds from source, and
|
|
41
|
+
if no C++ compiler is available you get the NumPy implementation alone. Both give
|
|
42
|
+
the same results; only the speed differs.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import phacker
|
|
46
|
+
phacker.backend() # "cpp" if the compiled kernel is available, else "numpy"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
You need a T × N matrix of P&L: one column per configuration you tried, one row per
|
|
52
|
+
period, with all columns on the same time index.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import numpy as np
|
|
56
|
+
from phacker.cscv import cscv
|
|
57
|
+
|
|
58
|
+
pnl = np.random.default_rng(0).standard_normal((2000, 500)) # 500 pure-noise strategies
|
|
59
|
+
pbo, logits = cscv(pnl, S=16)
|
|
60
|
+
|
|
61
|
+
pbo # 0.317; noise averages 0.5, but one family can land well away from it
|
|
62
|
+
logits.shape # (12870,) = C(16, 8)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Include every configuration you tried**, including the ones that did badly.
|
|
66
|
+
Leaving out failed trials biases PBO downward.
|
|
67
|
+
|
|
68
|
+
### Reading the result
|
|
69
|
+
|
|
70
|
+
- **PBO = 0.5** means selection carried no information. The in-sample winner is
|
|
71
|
+
equally likely to land above or below the out-of-sample median. This is what pure
|
|
72
|
+
noise gives, not 0.
|
|
73
|
+
- **PBO near 0** means the in-sample winner reliably stays above the median
|
|
74
|
+
out-of-sample.
|
|
75
|
+
- **PBO above 0.5** means the selection is worse than random. The in-sample winners
|
|
76
|
+
tend to underperform out-of-sample (mean reversion between halves, for example).
|
|
77
|
+
|
|
78
|
+
`logits` holds one value per train/test split, λ = ln(ω / (1 − ω)), where ω is the
|
|
79
|
+
in-sample winner's relative out-of-sample rank. PBO is the fraction with λ ≤ 0.
|
|
80
|
+
With no skill, ω is uniform and λ follows the standard logistic distribution exactly.
|
|
81
|
+
|
|
82
|
+
## Building a trial matrix from prices
|
|
83
|
+
|
|
84
|
+
`phacker` includes a grid backtester for threshold rules on technical indicators.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import pandas as pd
|
|
88
|
+
from phacker.indicators import indicators
|
|
89
|
+
from phacker.grid import Grid, backtest, linspace
|
|
90
|
+
from phacker.cscv import cscv
|
|
91
|
+
|
|
92
|
+
# a DataFrame with Adj_Close, High, Low, Volume columns on a DatetimeIndex
|
|
93
|
+
prices = pd.read_csv("NQ.csv", index_col=0, parse_dates=True)
|
|
94
|
+
prices = indicators(prices, ["RSI14", "Z20", "BOLL_B20"])
|
|
95
|
+
|
|
96
|
+
grid = Grid(
|
|
97
|
+
indicators=["RSI14", "Z20", "BOLL_B20"],
|
|
98
|
+
entry=linspace(-2, 2, 17), # z-score levels
|
|
99
|
+
band=[0.25, 0.5, 1.0, 2.0], # half-width around each level
|
|
100
|
+
hold=[1, 5, 10, 20], # bars to hold after a trigger
|
|
101
|
+
side="both", # "long", "short" or "both"
|
|
102
|
+
cost_bps=0.5, # per unit of position change
|
|
103
|
+
)
|
|
104
|
+
M = backtest(prices, grid, price_col="Adj_Close", min_periods=60) # TrialMatrix, 1,632 columns
|
|
105
|
+
|
|
106
|
+
pbo, logits = cscv(M, S=16)
|
|
107
|
+
M.names[0] # 'RSI14:entry=-2:band=0.25:hold=1:long'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
For each configuration (indicator, entry, band, hold, side) the rule goes long or short
|
|
111
|
+
whenever the indicator's z-score is within `band` of `entry`, and holds for `hold` bars.
|
|
112
|
+
Positions are lagged one bar, so there is no lookahead. Indicators are standardized
|
|
113
|
+
with an **expanding-window** z-score, so θ means the same thing for every indicator
|
|
114
|
+
and no future data leaks into the past. `min_periods` is the warm-up before the
|
|
115
|
+
z-score is defined.
|
|
116
|
+
|
|
117
|
+
Available indicators: `SMA5`, `SMA20`, `EMA5`, `EMA20`, `RSI14`, `ATR14`, `OBV`,
|
|
118
|
+
`BOLL_B20`, `ADX14`, `VOL20`, `Z20`.
|
|
119
|
+
|
|
120
|
+
To bring your own P&L with labels, wrap it in `TrialMatrix(pnl, names=[...])`.
|
|
121
|
+
|
|
122
|
+
## Diagnostics
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from phacker.stats import n_eff, logit_fit, sharpe_se
|
|
126
|
+
|
|
127
|
+
n_eff(M) # effective number of independent trials
|
|
128
|
+
logit_fit(logits) # (KS distance from the logistic null, mean logit)
|
|
129
|
+
sharpe_se(returns) # delta-method standard error of each column's Sharpe ratio
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- **`n_eff`**: the participation ratio of the centred trial matrix's singular values.
|
|
133
|
+
A grid of 1,632 configurations that mostly trade the same signal can have an
|
|
134
|
+
`n_eff` near 1. High PBO on a family with a small `n_eff` may say more about
|
|
135
|
+
redundancy than about overfitting.
|
|
136
|
+
- **`logit_fit`** compares the logits to the exact logistic null. It's descriptive
|
|
137
|
+
only: the C(S, S/2) splits share blocks, so the logits are dependent and a
|
|
138
|
+
classical KS p-value would be wrong. None is reported.
|
|
139
|
+
- **`sharpe_se`** is √((1 − γ₃·SR + (γ₄ − 1)/4·SR²) / (T − 1)), with skewness γ₃
|
|
140
|
+
and non-excess kurtosis γ₄. Use it to check that Sharpe estimates on
|
|
141
|
+
T/2 rows are reliable enough for your choice of S.
|
|
142
|
+
|
|
143
|
+
## Choosing S
|
|
144
|
+
|
|
145
|
+
`S` is the number of blocks the rows are split into. It must be even and at most 32.
|
|
146
|
+
CSCV evaluates C(S, S/2) splits, each with T/2 rows on each side:
|
|
147
|
+
|
|
148
|
+
| S | splits |
|
|
149
|
+
|---|---|
|
|
150
|
+
| 8 | 70 |
|
|
151
|
+
| 12 | 924 |
|
|
152
|
+
| 16 | 12,870 |
|
|
153
|
+
| 20 | 184,756 |
|
|
154
|
+
|
|
155
|
+
The paper uses S = 16. A larger S gives more splits but shorter blocks. If T isn't a
|
|
156
|
+
multiple of S, the last `T % S` rows are dropped.
|
|
157
|
+
|
|
158
|
+
## Custom metrics
|
|
159
|
+
|
|
160
|
+
`cscv` accepts any performance function that maps a `(rows, N)` array to N scores.
|
|
161
|
+
Rows arrive in their original time order, so path-dependent metrics such as drawdown
|
|
162
|
+
work too:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
pbo, logits = cscv(M, S=16, metric=lambda x: x.mean(axis=0))
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The C++ kernel computes the Sharpe ratio only. Any other metric runs the NumPy
|
|
169
|
+
implementation (also available directly as `phacker.cscv.cscv_reference`), which is
|
|
170
|
+
much slower at large N.
|
|
171
|
+
|
|
172
|
+
## Performance
|
|
173
|
+
|
|
174
|
+
With the Sharpe metric, the kernel sums each block once and then reuses those sums.
|
|
175
|
+
Neighbouring splits are visited in revolving-door (Gray code) order, so each split
|
|
176
|
+
needs one block added and one removed, and the out-of-sample sums are the totals minus
|
|
177
|
+
the in-sample ones. Splits are processed in parallel with OpenMP.
|
|
178
|
+
|
|
179
|
+
N = 1,000 strategies, T = 2,000 rows, S = 16, Intel i7-1365U (10 cores), Windows:
|
|
180
|
+
|
|
181
|
+
| backend | time |
|
|
182
|
+
|---|---|
|
|
183
|
+
| NumPy reference | ~250 s |
|
|
184
|
+
| C++ kernel, 1 thread | ~0.3 s |
|
|
185
|
+
| C++ kernel, OpenMP | ~0.05–0.07 s |
|
|
186
|
+
|
|
187
|
+
macOS wheels are built without OpenMP and run single-threaded.
|
|
188
|
+
|
|
189
|
+
## Behaviour details
|
|
190
|
+
|
|
191
|
+
- **Flat columns.** A configuration with no nonzero returns in a half (it never traded
|
|
192
|
+
there) gets a Sharpe of 0 in that half rather than NaN.
|
|
193
|
+
- **Ties.** The out-of-sample rank is 1 + the number of strategies strictly worse,
|
|
194
|
+
so ties rank low.
|
|
195
|
+
- **First winner.** If several strategies share the best in-sample score, the first
|
|
196
|
+
column wins.
|
|
197
|
+
- Both backends follow these rules and agree to within 1e-12 on the logits.
|
|
198
|
+
|
|
199
|
+
## Limitations
|
|
200
|
+
|
|
201
|
+
1. **PBO measures the selection process, not a strategy.** A family where every
|
|
202
|
+
configuration is genuinely good and roughly equal also gives a high PBO (paper
|
|
203
|
+
§5.2). `n_eff` helps tell these cases apart but doesn't settle it.
|
|
204
|
+
2. **The answer is only as honest as the trial list.** Omitting configurations you
|
|
205
|
+
tried biases PBO down, and so does padding the family with configurations
|
|
206
|
+
designed to fail.
|
|
207
|
+
3. **Never optimize for low PBO.** Using CSCV as an objective function overfits PBO
|
|
208
|
+
itself.
|
|
209
|
+
4. **The splits are dependent**, so there's no calibrated goodness-of-fit p-value on
|
|
210
|
+
the logits.
|
|
211
|
+
5. **Regimes outside the sample are invisible.** PBO can't account for structural
|
|
212
|
+
breaks it never saw.
|
|
213
|
+
6. **Order-dependent metrics** (max drawdown, Calmar) have no additive block
|
|
214
|
+
statistics, so they run on the slow NumPy path only.
|
|
215
|
+
|
|
216
|
+
## Development
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
git clone https://github.com/ryousuf569/phacker.git
|
|
220
|
+
cd phacker
|
|
221
|
+
pip install -e ".[test]" # builds the C++ kernel; needs a C++20 compiler
|
|
222
|
+
pytest
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`pytest` without installing runs against the source tree on the NumPy backend, and
|
|
226
|
+
the C++ parity tests are skipped. On Windows with MinGW on `PATH`, set
|
|
227
|
+
`CMAKE_GENERATOR="Visual Studio 17 2022"` so the build uses MSVC.
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
MIT. See [LICENSE](LICENSE).
|