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.
@@ -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).