falsealpha 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.
Files changed (49) hide show
  1. falsealpha-0.1.0/.github/workflows/ci.yml +33 -0
  2. falsealpha-0.1.0/.github/workflows/publish.yml +60 -0
  3. falsealpha-0.1.0/.gitignore +11 -0
  4. falsealpha-0.1.0/CMakeLists.txt +41 -0
  5. falsealpha-0.1.0/LICENSE +21 -0
  6. falsealpha-0.1.0/PKG-INFO +228 -0
  7. falsealpha-0.1.0/README.md +207 -0
  8. falsealpha-0.1.0/cpp/include/falsealpha/eval.hpp +14 -0
  9. falsealpha-0.1.0/cpp/include/falsealpha/expr.hpp +59 -0
  10. falsealpha-0.1.0/cpp/include/falsealpha/gp.hpp +65 -0
  11. falsealpha-0.1.0/cpp/include/falsealpha/mat.hpp +46 -0
  12. falsealpha-0.1.0/cpp/include/falsealpha/ops.hpp +25 -0
  13. falsealpha-0.1.0/cpp/include/falsealpha/score.hpp +38 -0
  14. falsealpha-0.1.0/cpp/include/falsealpha/stats.hpp +46 -0
  15. falsealpha-0.1.0/cpp/src/bindings.cpp +214 -0
  16. falsealpha-0.1.0/cpp/src/eval.cpp +66 -0
  17. falsealpha-0.1.0/cpp/src/expr.cpp +206 -0
  18. falsealpha-0.1.0/cpp/src/gp.cpp +298 -0
  19. falsealpha-0.1.0/cpp/src/ops.cpp +324 -0
  20. falsealpha-0.1.0/cpp/src/score.cpp +101 -0
  21. falsealpha-0.1.0/cpp/src/stats.cpp +239 -0
  22. falsealpha-0.1.0/cpp/tests/helpers.hpp +25 -0
  23. falsealpha-0.1.0/cpp/tests/test_expr.cpp +63 -0
  24. falsealpha-0.1.0/cpp/tests/test_gp.cpp +104 -0
  25. falsealpha-0.1.0/cpp/tests/test_ops.cpp +140 -0
  26. falsealpha-0.1.0/cpp/tests/test_score.cpp +31 -0
  27. falsealpha-0.1.0/cpp/tests/test_stats.cpp +102 -0
  28. falsealpha-0.1.0/docs/demo.gif +0 -0
  29. falsealpha-0.1.0/docs/demo.tape +27 -0
  30. falsealpha-0.1.0/docs/report.png +0 -0
  31. falsealpha-0.1.0/pyproject.toml +37 -0
  32. falsealpha-0.1.0/scripts/noise_calibration.py +27 -0
  33. falsealpha-0.1.0/scripts/snoop_size.py +26 -0
  34. falsealpha-0.1.0/src/falsealpha/__init__.py +32 -0
  35. falsealpha-0.1.0/src/falsealpha/bench.py +133 -0
  36. falsealpha-0.1.0/src/falsealpha/binance.py +229 -0
  37. falsealpha-0.1.0/src/falsealpha/cli.py +215 -0
  38. falsealpha-0.1.0/src/falsealpha/firewall.py +116 -0
  39. falsealpha-0.1.0/src/falsealpha/panel.py +80 -0
  40. falsealpha-0.1.0/src/falsealpha/report.py +320 -0
  41. falsealpha-0.1.0/src/falsealpha/run.py +198 -0
  42. falsealpha-0.1.0/src/falsealpha/synthetic.py +50 -0
  43. falsealpha-0.1.0/tests/conftest.py +25 -0
  44. falsealpha-0.1.0/tests/reference.py +121 -0
  45. falsealpha-0.1.0/tests/test_bench.py +19 -0
  46. falsealpha-0.1.0/tests/test_binance.py +95 -0
  47. falsealpha-0.1.0/tests/test_cli.py +39 -0
  48. falsealpha-0.1.0/tests/test_firewall.py +107 -0
  49. falsealpha-0.1.0/tests/test_reference.py +78 -0
@@ -0,0 +1,33 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ cpp:
10
+ strategy:
11
+ matrix:
12
+ os: [ubuntu-latest, macos-latest]
13
+ runs-on: ${{ matrix.os }}
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - name: Configure (Debug, address + undefined-behaviour sanitizers)
17
+ run: cmake -S . -B build -DFALSEALPHA_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all"
18
+ - run: cmake --build build -j 4
19
+ - run: ./build/core_tests
20
+
21
+ python:
22
+ strategy:
23
+ matrix:
24
+ os: [ubuntu-latest, macos-latest]
25
+ python: ["3.10", "3.13"]
26
+ runs-on: ${{ matrix.os }}
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: ${{ matrix.python }}
32
+ - run: pip install ".[dev]"
33
+ - run: pytest
@@ -0,0 +1,60 @@
1
+ # Push a version tag (e.g. v0.1.0) to build wheels, test them, and publish to PyPI
2
+ # via Trusted Publishing (no tokens). Finishes green only once pip can install it.
3
+ # Pull requests build and test the wheels too, without publishing.
4
+ name: Publish to PyPI
5
+
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+ pull_request:
10
+
11
+ jobs:
12
+ wheels:
13
+ strategy:
14
+ matrix:
15
+ os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest]
16
+ runs-on: ${{ matrix.os }}
17
+ steps:
18
+ - uses: actions/checkout@v5
19
+ - uses: pypa/cibuildwheel@v4.2.1
20
+ env:
21
+ CIBW_SKIP: "pp* *-musllinux_* cp3??t-*"
22
+ CIBW_ARCHS_MACOS: "arm64 x86_64" # Intel wheels cross-built on the Apple Silicon runner
23
+ MACOSX_DEPLOYMENT_TARGET: "11.0"
24
+ CIBW_TEST_EXTRAS: dev
25
+ CIBW_TEST_COMMAND: pytest -q {project}/tests
26
+ - uses: actions/upload-artifact@v4
27
+ with:
28
+ name: wheels-${{ matrix.os }}
29
+ path: wheelhouse/*.whl
30
+
31
+ sdist:
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@v5
35
+ - run: pipx run build --sdist
36
+ - uses: actions/upload-artifact@v4
37
+ with:
38
+ name: sdist
39
+ path: dist/*.tar.gz
40
+
41
+ publish:
42
+ needs: [wheels, sdist]
43
+ if: startsWith(github.ref, 'refs/tags/v')
44
+ runs-on: ubuntu-latest
45
+ environment: pypi
46
+ permissions:
47
+ id-token: write # lets PyPI verify this workflow instead of a stored token
48
+ steps:
49
+ - uses: actions/download-artifact@v4
50
+ with:
51
+ path: dist
52
+ merge-multiple: true
53
+ - uses: pypa/gh-action-pypi-publish@release/v1
54
+ - name: Wait until pip can install it # PyPI's CDN can serve the old index for a few minutes
55
+ run: |
56
+ for i in $(seq 30); do
57
+ pip download --no-deps --no-cache-dir --only-binary=:all: -q -d /tmp/check "falsealpha==${GITHUB_REF_NAME#v}" && exit 0
58
+ sleep 10
59
+ done
60
+ exit 1
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ build/
3
+ dist/
4
+ __pycache__/
5
+ *.egg-info/
6
+ *.so
7
+ data/
8
+ runs/
9
+ .pytest_cache/
10
+ .env
11
+ build.log
@@ -0,0 +1,41 @@
1
+ cmake_minimum_required(VERSION 3.20)
2
+ project(falsealpha LANGUAGES CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 20)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
7
+ if(NOT CMAKE_BUILD_TYPE)
8
+ set(CMAKE_BUILD_TYPE Release)
9
+ endif()
10
+
11
+ find_package(Threads REQUIRED)
12
+ file(GLOB CORE_SOURCES CONFIGURE_DEPENDS cpp/src/*.cpp)
13
+ list(REMOVE_ITEM CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/cpp/src/bindings.cpp)
14
+ add_library(falsealpha_core STATIC ${CORE_SOURCES})
15
+ target_include_directories(falsealpha_core PUBLIC cpp/include)
16
+ target_link_libraries(falsealpha_core PUBLIC Threads::Threads)
17
+ target_compile_options(falsealpha_core PRIVATE -Wall -Wextra)
18
+
19
+ option(FALSEALPHA_BUILD_TESTS "Build the Catch2 test suite" OFF)
20
+
21
+ if(SKBUILD)
22
+ set(PYBIND11_FINDPYTHON ON)
23
+ find_package(pybind11 CONFIG REQUIRED)
24
+ pybind11_add_module(_core cpp/src/bindings.cpp)
25
+ target_link_libraries(_core PRIVATE falsealpha_core)
26
+ install(TARGETS _core DESTINATION falsealpha)
27
+ endif()
28
+
29
+ if(FALSEALPHA_BUILD_TESTS)
30
+ include(FetchContent)
31
+ FetchContent_Declare(Catch2
32
+ GIT_REPOSITORY https://github.com/catchorg/Catch2.git
33
+ GIT_TAG v3.7.1)
34
+ FetchContent_MakeAvailable(Catch2)
35
+ file(GLOB TEST_SOURCES CONFIGURE_DEPENDS cpp/tests/*.cpp)
36
+ add_executable(core_tests ${TEST_SOURCES})
37
+ target_link_libraries(core_tests PRIVATE falsealpha_core Catch2::Catch2WithMain)
38
+ target_compile_options(core_tests PRIVATE -Wall -Wextra -Werror)
39
+ enable_testing()
40
+ add_test(NAME core_tests COMMAND core_tests)
41
+ endif()
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Owen Alderson
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,228 @@
1
+ Metadata-Version: 2.4
2
+ Name: falsealpha
3
+ Version: 0.1.0
4
+ Summary: A C++ alpha-mining engine that searches formulaic trading signals, then tells you which of its discoveries are fake.
5
+ Keywords: quant,alpha,genetic-programming,backtesting,overfitting,deflated-sharpe
6
+ Author: Owen Alderson
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: C++
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Office/Business :: Financial :: Investment
12
+ Classifier: Intended Audience :: Science/Research
13
+ Project-URL: Homepage, https://github.com/owen-alderson/falsealpha
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: numpy>=1.26
16
+ Requires-Dist: certifi
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=8; extra == "dev"
19
+ Requires-Dist: pandas>=2.0; extra == "dev"
20
+ Description-Content-Type: text/markdown
21
+
22
+ # falsealpha
23
+
24
+ [![CI](https://github.com/owen-alderson/falsealpha/actions/workflows/ci.yml/badge.svg)](https://github.com/owen-alderson/falsealpha/actions/workflows/ci.yml)
25
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
26
+
27
+ **A C++ engine that searches thousands of formulaic trading signals, then tells you which of its own discoveries are fake.**
28
+
29
+ ![falsealpha mining six years of Binance data](docs/demo.gif)
30
+
31
+ Search hard enough and you will always find a backtest that looks good. falsealpha does the search fast in C++20, records every formula it tried, and keeps a result only if it beats the luck it took to find it.
32
+
33
+ ```
34
+ $ falsealpha data noise --out noise.npz # 2,500 days x 60 assets of pure noise
35
+ $ falsealpha mine noise.npz runs/noise
36
+ 6002 formulas tried -> 50 finalists -> 0 survivors
37
+ luck bar (train): best of 6002 unskilled strategies ~ Sharpe +1.84 /yr
38
+ Reality Check p 0.942 (gate) SPA p 0.800
39
+ PBO 0.95 (share of splits where the in-sample best finalist lands in the bottom half)
40
+
41
+ $ falsealpha data planted --out planted.npz # same, plus one hidden alpha
42
+ $ falsealpha mine planted.npz runs/planted
43
+ 6397 formulas tried -> 50 finalists -> 3 survivors
44
+ luck bar (train): best of 6397 unskilled strategies ~ Sharpe +1.85 /yr
45
+ Reality Check p 0.000 (gate) SPA p 0.000
46
+ PBO 0.00 (share of splits where the in-sample best finalist lands in the bottom half)
47
+ [PASS] train +5.87 (DSR 1.00) val +4.94 (DSR 1.00) (decay_linear(ts_sum(volume, 3), 5) * ts_sum(volume, 5))
48
+ $ falsealpha unseal runs/planted
49
+ combined: holdout Sharpe +4.08 /yr, P(true Sharpe > 0) 1.00, 498 days
50
+ ```
51
+
52
+ Across 30 independent noise panels (1,500 days × 60 assets, population 200 × 6 generations; [`scripts/noise_calibration.py`](scripts/noise_calibration.py)), the firewall let through **0** formulas. The Reality Check gate never opened (0 of 30); the Deflated Sharpe alone would also have caught everything. Mean PBO was 0.49, a coin flip, which is what overfitting to noise looks like.
53
+
54
+ ## On real data: six years of crypto
55
+
56
+ The default search on every Binance USDT pair that was ever in the daily top 100 by dollar volume: 2,465 days × 570 assets, delisted coins included, 10 bps costs, trading one day after the signal.
57
+
58
+ ```
59
+ $ falsealpha data binance --out data/binance-top100.npz
60
+ $ falsealpha mine data/binance-top100.npz runs/crypto # 45 s on an M3 Pro
61
+ 7376 formulas tried -> 50 finalists -> 0 survivors
62
+ luck bar (train): best of 7376 unskilled strategies ~ Sharpe +1.88 /yr
63
+ Reality Check p 0.475 (gate) SPA p 0.185
64
+ PBO 0.73 (share of splits where the in-sample best finalist lands in the bottom half)
65
+ [fail] train +2.06 (DSR 0.64) val -1.08 (DSR 0.00) min(((quote_volume / ts_mean(ts_delta(open, 20), 5)) - high), -ts_sum(trades, 10))
66
+ [fail] train +2.01 (DSR 0.60) val -1.79 (DSR 0.00) min(((ts_mean(ts_delta(open, 20), 3) * ts_sum(volume, 10)) - ts_cov((open * open), low, 10)), -ts_sum(trades, 10))
67
+ [fail] train +1.86 (DSR 0.48) val +1.02 (DSR 0.14) ts_sum((-trades * ts_cov(close, quote_volume, 10)), 40)
68
+ ```
69
+
70
+ The best formula the search found earns a Sharpe of +2.06 a year over four years of training data, after costs. Shown alone, that is a backtest people trade. The firewall rejects it on the training data already: 7,376 tries at nothing would be expected to produce about +1.88 by luck, so its Deflated Sharpe is only 0.64. On the next 16 months, which the search never saw, it loses money (−1.08 a year), and so does the runner-up (−1.79). The few finalists that stay positive out of sample (+1.02 at best) are well within what luck explains over 493 days (Reality Check p = 0.48), and PBO is 0.73: picking the in-sample best finalist usually picks a below-median one.
71
+
72
+ So the honest result of this run is **no tradable alpha**, and the holdout (May 2025 to September 2026) stays sealed for a better search. That is the tool working: it is built to say "no" when the evidence is not there.
73
+
74
+ ![Run report](docs/report.png)
75
+
76
+
77
+ ## How it works
78
+
79
+ ```
80
+ panel ──► train ──────────────► validation ──────────► holdout (sealed)
81
+ GP search, every finalists scored opens once, after
82
+ formula logged on unseen days you stop searching
83
+ │ │ │
84
+ ▼ ▼ ▼
85
+ Deflated Sharpe vs Deflated Sharpe vs combined survivors:
86
+ ALL trials the finalists; Reality Sharpe, P(Sharpe > 0);
87
+ Check gate; PBO every look logged
88
+ ```
89
+
90
+ 1. **Search.** Genetic programming over an Alpha101-style formula language: ramped half-and-half initialisation, subtree crossover, point / subtree / hoist mutation, tournament selection, a parsimony penalty, and a novelty penalty against a hall of fame so it keeps finding *different* signals rather than near-copies (|corr| ≤ 0.7 between finalists). Fitness is the net long-short Sharpe on the train period only.
91
+ 2. **Ledger.** Every distinct formula the search scores is written to `ledger.csv.gz` (formulas are canonicalised, so `a + b` and `b + a` count once). This is the *N* in the multiple-testing correction, not just the winners.
92
+ 3. **Firewall.** A finalist survives only if it passes all of:
93
+ - **Train Deflated Sharpe ≥ 0.95** ([Bailey & López de Prado 2014](https://papers.ssrn.com/abstract=2460551)): the train Sharpe must beat the expected maximum of *N* unskilled strategies, where *N* is the whole ledger.
94
+ - **Validation Sharpe > 0 with Deflated Sharpe ≥ 0.95**, deflated for the number of finalists scored on the unseen validation days.
95
+ - **White's Reality Check, p < 0.05** ([White 2000](https://doi.org/10.1111/1468-0262.00152)), a global gate on validation returns: is the best finalist better than flat once you allow for having looked at all of them? Stationary bootstrap (mean block 10, 1,000 draws). Hansen's SPA test ([Hansen 2005](https://doi.org/10.1198/073500105000000063)), the studentised version with consistent recentring, is reported alongside but is not the gate: at this sample size its measured size is 8–9% at a nominal 5%, while the Reality Check holds 4.5–6% ([`scripts/snoop_size.py`](scripts/snoop_size.py), 1,000 null simulations per setting).
96
+ - **PBO** ([Bailey, Borwein, López de Prado & Zhu 2017](https://papers.ssrn.com/abstract=2326253)) is reported, computed over the finalists' train + validation returns: combinatorially symmetric cross-validation over 16 blocks (12,870 splits), the share where the in-sample best finalist lands in the bottom half out of sample.
97
+ 4. **Holdout.** The search and validation never receive the holdout days: the panel is physically truncated before they run. `falsealpha unseal` scores the survivors and their equal-risk combination on the holdout once; a second look needs `--force`, and the report shows every look.
98
+
99
+ **One deliberate deviation from the DSR paper.** The paper sets the luck bar from the variance of Sharpe ratios *across* trials. In a search that finds something real, dozens of variants of that signal inflate that variance and the test ends up punishing the discovery (on the planted data it rejects a true signal with a Sharpe of 9). falsealpha uses the null variance, 1/(T−1): the expected best of *N* strategies with no skill. Counting every distinct formula as a trial overstates the number of independent trials, so the bar is conservative. The paper's version is still computed and stored (`sr0_train_paper`).
100
+
101
+ ## Quick start
102
+
103
+ ```bash
104
+ pip install falsealpha
105
+
106
+ falsealpha data planted --out planted.npz # random data with one hidden alpha
107
+ falsealpha data noise --out noise.npz # random data, no alpha at all
108
+ falsealpha mine planted.npz runs/planted # search + firewall + runs/planted/report.html
109
+ falsealpha unseal runs/planted # look at the holdout, once
110
+
111
+ falsealpha data binance --out binance.npz # every USDT pair, delisted included (~10 min download the first time)
112
+ falsealpha info binance.npz
113
+ falsealpha eval binance.npz "-rank(ts_delta(close, 5))"
114
+ falsealpha mine binance.npz runs/crypto
115
+ ```
116
+
117
+ Each run writes `config.json`, `ledger.csv.gz`, `finalists.json`, `nets.npy`, `holdout.json` and a self-contained `report.html` (no network, light and dark mode).
118
+
119
+ ### Python
120
+
121
+ ```python
122
+ import falsealpha as fa
123
+ from falsealpha import run
124
+
125
+ panel = fa.Panel.load("binance.npz")
126
+ signal = fa.evaluate("rank(ts_corr(close, volume, 10))", panel.core()) # days x assets
127
+ bt = fa.backtest(signal, "returns", panel.core(), delay=1, cost_bps=10) # daily IC, gross, cost, net
128
+ fa.summarize(bt, 0, 1000) # Sharpe, IC, IC IR, turnover
129
+
130
+ verdict = run.mine("binance.npz", "runs/x", run.MineConfig(population=400, generations=25))
131
+ ```
132
+
133
+ The statistics are exposed on their own: `fa.expected_max_sharpe`, `fa.probabilistic_sharpe`, `fa.pbo`, `fa.snoop_tests`.
134
+
135
+ ## Formula language
136
+
137
+ Fields are whatever the panel holds; Binance panels have `open high low close volume quote_volume vwap returns trades`. Infix `+ - * /` and unary minus work, as do numeric constants.
138
+
139
+ | kind | operators |
140
+ |---|---|
141
+ | elementwise | `neg abs log sign add sub mul div max min` |
142
+ | cross-sectional (universe members, one day) | `rank zscore demean scale` |
143
+ | time-series (trailing *d* days, today included) | `ts_delay ts_delta ts_sum ts_mean ts_std ts_zscore ts_min ts_max ts_argmin ts_argmax ts_rank decay_linear ts_corr ts_cov` |
144
+
145
+ NaN rules: a time-series window must be full of real data; cross-sectional ops only see universe members with a value; a variance below 1e-20 of the mean square counts as zero.
146
+
147
+ **Backtest convention.** Weights are the cross-sectional rank of the signal, demeaned and scaled to gross 1 (dollar-neutral). A signal built from data up to close *t* trades at close *t+delay* (default 1) and earns the next day's return. Costs are charged per unit of turnover. A member with no next-day return (delisted, gap) earns 0, i.e. it is sold at its last price.
148
+
149
+ ## The engine
150
+
151
+ - **Time-major panels.** A *T × N* panel stores one contiguous row of assets per day, so time-series ops sweep rows with per-asset state (vectorised across assets) and cross-sectional ops work on one contiguous row.
152
+ - **O(1) rolling sums** with a periodic rebuild so floating-point drift can't build up; **van Herk / Gil-Werman** sliding min/max; `decay_linear` by recurrence.
153
+ - **Exact second moments.** `ts_std`, `ts_zscore`, `ts_corr` and `ts_cov` are computed two-pass over each window. Running-moment updates (sums of squares, even rolling Welford) lose relative precision when a quiet window follows a volatile history at a high price level; the test suite has the random walk that proves it.
154
+ - **Common subexpressions** within a formula are computed once; only repeated subtrees are cached, so memory stays at about one panel per tree level.
155
+ - **Parallel over formulas**, deterministic for a given seed regardless of thread count (on one platform: C++ standard libraries draw random numbers differently); the GIL is released.
156
+
157
+ **Verified, not assumed:**
158
+ - every operator matches an independent, loop-per-window numpy reference to 1e-9 (relative), including NaN holes, universe masks, constant columns and 3,000-day random walks at a price level of 5,000,000;
159
+ - a property test perturbs every input after day *k* and checks no operator's output on or before day *k* moves (no look-ahead, by construction);
160
+ - the Deflated Sharpe reproduces the worked example in Bailey & López de Prado (2014) (SR₀ = 0.1132, DSR = 0.9004);
161
+ - the Reality Check rejects at about its nominal 5% on noise (SPA over-rejects at this sample size, measured and documented above) and both have power on a real edge; PBO averages about 0.5 on noise and about 0 with a dominant strategy;
162
+ - the search recovers a planted alpha, the firewall passes it and rejects pure noise;
163
+ - C++ tests run under AddressSanitizer and UBSan in CI.
164
+
165
+ ## Speed
166
+
167
+ Apple M3 Pro (11 cores), 2,000 days × 500 assets, best of 3:
168
+
169
+ | | time | vs pandas |
170
+ |---|---:|---:|
171
+ | 12 Alpha101-style formulas, falsealpha, 1 thread | 501 ms | 6.6× faster |
172
+ | same, 11 threads | 131 ms | 25× faster |
173
+ | same, pandas | 3,307 ms | |
174
+
175
+ | operator (1 thread) | falsealpha | pandas | |
176
+ |---|---:|---:|---:|
177
+ | `rank(close)` | 29.7 ms | 37.0 ms | 1.2× |
178
+ | `ts_mean(close, 20)` | 1.9 ms | 11.5 ms | 6× |
179
+ | `ts_std(close, 20)` | 8.8 ms | 19.1 ms | 2× |
180
+ | `ts_corr(close, volume, 20)` | 20.2 ms | 77.6 ms | 4× |
181
+ | `ts_max(close, 20)` | 3.3 ms | 21.0 ms | 6× |
182
+ | `ts_argmax(close, 20)` | 28.0 ms | 774 ms | 28× |
183
+ | `ts_rank(close, 20)` | 10.3 ms | 1,076 ms | 104× |
184
+ | `decay_linear(close, 20)` | 3.6 ms | 520 ms | 145× |
185
+
186
+ The honest reading: on operators pandas implements in C (`rank`, rolling mean/std/corr) the gain is 1–6×, and `ts_std` / `ts_corr` deliberately pay for an exact two-pass computation. The large gains are on window operators pandas can only do through Python-level `rolling().apply()`. In a search, the parallelism is over formulas, so throughput scales with cores: the default noise run above scored 6,002 formulas over 2,500 days × 60 assets in about 5 seconds.
187
+
188
+ `falsealpha bench` reproduces this. The pandas side evaluates the *identical* formula text (canonical formulas are valid Python expressions) with the usual pandas idioms: `rolling()`, `rank(axis=1, pct=True)`, `rolling().apply()` where pandas has no built-in. Where the definitions coincide the two agree to 1e-8 (tested). Most of falsealpha's single-thread time is `rank`, a sort per day, which pandas also does in compiled code; the big gaps come from window operators pandas can only do through Python-level `apply`.
189
+
190
+ ## Data
191
+
192
+ **Crypto (built in).** Binance's public dump ([data.binance.vision](https://data.binance.vision)) keeps every spot pair's daily candles, including pairs that were delisted, which is what makes a survivorship-free universe possible. The loader:
193
+ - drops stablecoins, fiat pairs and leveraged tokens;
194
+ - splits a ticker into separate assets when it goes more than 7 days without candles (LUNAUSDT resumes 18 days after the May 2022 collapse as a different coin);
195
+ - handles the switch from millisecond to microsecond timestamps in 2025;
196
+ - builds the universe each day from data up to that day: the top *N* by trailing 30-day dollar volume, with at least 60 days of history;
197
+ - starts the panel in 2020 by default: before that Binance had fewer than 40 liquid USDT pairs on a typical day, too few for cross-sectional ranks (earlier data still counts as history).
198
+
199
+ **US equities (planned).** Free daily data with delisted names is scarce. The plan is point-in-time S&P 500 membership ([fja05680/sp500](https://github.com/fja05680/sp500)) plus Tiingo's free tier, which carries delisted tickers, with results reported three ways (survivors only, point-in-time, and a delisting-loss stress) so the remaining bias is visible. yfinance can't be used: it has no delisted tickers and returns a different company for reused ones.
200
+
201
+ ## Limitations
202
+
203
+ - Daily bars only; costs are a flat charge per unit of turnover, with no market impact or borrow costs.
204
+ - A delisted asset is assumed sold at its last close. Real exits can be worse.
205
+ - The holdout seal is an honour system with an audit trail (the look log and a fingerprint of the panel file), not cryptography.
206
+ - Counting distinct formulas as trials is conservative; clustering trials into independent groups would give a sharper luck bar.
207
+ - `falsealpha eval` shows validation results for any formula you type, and those looks are not counted. Hand-testing ideas on validation is a search the firewall cannot see; keep it to train, or count it yourself.
208
+
209
+ ## Development
210
+
211
+ ```bash
212
+ python -m venv .venv && .venv/bin/pip install scikit-build-core pybind11 numpy pandas pytest certifi
213
+ .venv/bin/pip install --no-build-isolation -e ".[dev]"
214
+ .venv/bin/pytest # Python suite
215
+ cmake -S . -B build -DFALSEALPHA_BUILD_TESTS=ON && cmake --build build -j && ./build/core_tests
216
+ ```
217
+
218
+ ## References
219
+
220
+ - D. H. Bailey, M. López de Prado (2014). The Deflated Sharpe Ratio: Correcting for Selection Bias, Backtest Overfitting and Non-Normality. *Journal of Portfolio Management* 40(5).
221
+ - D. H. Bailey, J. Borwein, M. López de Prado, Q. J. Zhu (2017). The Probability of Backtest Overfitting. *Journal of Computational Finance* 20(4).
222
+ - P. R. Hansen (2005). A Test for Superior Predictive Ability. *Journal of Business & Economic Statistics* 23(4).
223
+ - H. White (2000). A Reality Check for Data Snooping. *Econometrica* 68(5).
224
+ - D. N. Politis, J. P. Romano (1994). The Stationary Bootstrap. *JASA* 89(428).
225
+ - C. R. Harvey, Y. Liu, H. Zhu (2016). …and the Cross-Section of Expected Returns. *Review of Financial Studies* 29(1).
226
+ - Z. Kakushadze (2016). 101 Formulaic Alphas. *Wilmott* 2016(84). [arXiv:1601.00991](https://arxiv.org/abs/1601.00991)
227
+
228
+ MIT licence.
@@ -0,0 +1,207 @@
1
+ # falsealpha
2
+
3
+ [![CI](https://github.com/owen-alderson/falsealpha/actions/workflows/ci.yml/badge.svg)](https://github.com/owen-alderson/falsealpha/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+
6
+ **A C++ engine that searches thousands of formulaic trading signals, then tells you which of its own discoveries are fake.**
7
+
8
+ ![falsealpha mining six years of Binance data](docs/demo.gif)
9
+
10
+ Search hard enough and you will always find a backtest that looks good. falsealpha does the search fast in C++20, records every formula it tried, and keeps a result only if it beats the luck it took to find it.
11
+
12
+ ```
13
+ $ falsealpha data noise --out noise.npz # 2,500 days x 60 assets of pure noise
14
+ $ falsealpha mine noise.npz runs/noise
15
+ 6002 formulas tried -> 50 finalists -> 0 survivors
16
+ luck bar (train): best of 6002 unskilled strategies ~ Sharpe +1.84 /yr
17
+ Reality Check p 0.942 (gate) SPA p 0.800
18
+ PBO 0.95 (share of splits where the in-sample best finalist lands in the bottom half)
19
+
20
+ $ falsealpha data planted --out planted.npz # same, plus one hidden alpha
21
+ $ falsealpha mine planted.npz runs/planted
22
+ 6397 formulas tried -> 50 finalists -> 3 survivors
23
+ luck bar (train): best of 6397 unskilled strategies ~ Sharpe +1.85 /yr
24
+ Reality Check p 0.000 (gate) SPA p 0.000
25
+ PBO 0.00 (share of splits where the in-sample best finalist lands in the bottom half)
26
+ [PASS] train +5.87 (DSR 1.00) val +4.94 (DSR 1.00) (decay_linear(ts_sum(volume, 3), 5) * ts_sum(volume, 5))
27
+ $ falsealpha unseal runs/planted
28
+ combined: holdout Sharpe +4.08 /yr, P(true Sharpe > 0) 1.00, 498 days
29
+ ```
30
+
31
+ Across 30 independent noise panels (1,500 days × 60 assets, population 200 × 6 generations; [`scripts/noise_calibration.py`](scripts/noise_calibration.py)), the firewall let through **0** formulas. The Reality Check gate never opened (0 of 30); the Deflated Sharpe alone would also have caught everything. Mean PBO was 0.49, a coin flip, which is what overfitting to noise looks like.
32
+
33
+ ## On real data: six years of crypto
34
+
35
+ The default search on every Binance USDT pair that was ever in the daily top 100 by dollar volume: 2,465 days × 570 assets, delisted coins included, 10 bps costs, trading one day after the signal.
36
+
37
+ ```
38
+ $ falsealpha data binance --out data/binance-top100.npz
39
+ $ falsealpha mine data/binance-top100.npz runs/crypto # 45 s on an M3 Pro
40
+ 7376 formulas tried -> 50 finalists -> 0 survivors
41
+ luck bar (train): best of 7376 unskilled strategies ~ Sharpe +1.88 /yr
42
+ Reality Check p 0.475 (gate) SPA p 0.185
43
+ PBO 0.73 (share of splits where the in-sample best finalist lands in the bottom half)
44
+ [fail] train +2.06 (DSR 0.64) val -1.08 (DSR 0.00) min(((quote_volume / ts_mean(ts_delta(open, 20), 5)) - high), -ts_sum(trades, 10))
45
+ [fail] train +2.01 (DSR 0.60) val -1.79 (DSR 0.00) min(((ts_mean(ts_delta(open, 20), 3) * ts_sum(volume, 10)) - ts_cov((open * open), low, 10)), -ts_sum(trades, 10))
46
+ [fail] train +1.86 (DSR 0.48) val +1.02 (DSR 0.14) ts_sum((-trades * ts_cov(close, quote_volume, 10)), 40)
47
+ ```
48
+
49
+ The best formula the search found earns a Sharpe of +2.06 a year over four years of training data, after costs. Shown alone, that is a backtest people trade. The firewall rejects it on the training data already: 7,376 tries at nothing would be expected to produce about +1.88 by luck, so its Deflated Sharpe is only 0.64. On the next 16 months, which the search never saw, it loses money (−1.08 a year), and so does the runner-up (−1.79). The few finalists that stay positive out of sample (+1.02 at best) are well within what luck explains over 493 days (Reality Check p = 0.48), and PBO is 0.73: picking the in-sample best finalist usually picks a below-median one.
50
+
51
+ So the honest result of this run is **no tradable alpha**, and the holdout (May 2025 to September 2026) stays sealed for a better search. That is the tool working: it is built to say "no" when the evidence is not there.
52
+
53
+ ![Run report](docs/report.png)
54
+
55
+
56
+ ## How it works
57
+
58
+ ```
59
+ panel ──► train ──────────────► validation ──────────► holdout (sealed)
60
+ GP search, every finalists scored opens once, after
61
+ formula logged on unseen days you stop searching
62
+ │ │ │
63
+ ▼ ▼ ▼
64
+ Deflated Sharpe vs Deflated Sharpe vs combined survivors:
65
+ ALL trials the finalists; Reality Sharpe, P(Sharpe > 0);
66
+ Check gate; PBO every look logged
67
+ ```
68
+
69
+ 1. **Search.** Genetic programming over an Alpha101-style formula language: ramped half-and-half initialisation, subtree crossover, point / subtree / hoist mutation, tournament selection, a parsimony penalty, and a novelty penalty against a hall of fame so it keeps finding *different* signals rather than near-copies (|corr| ≤ 0.7 between finalists). Fitness is the net long-short Sharpe on the train period only.
70
+ 2. **Ledger.** Every distinct formula the search scores is written to `ledger.csv.gz` (formulas are canonicalised, so `a + b` and `b + a` count once). This is the *N* in the multiple-testing correction, not just the winners.
71
+ 3. **Firewall.** A finalist survives only if it passes all of:
72
+ - **Train Deflated Sharpe ≥ 0.95** ([Bailey & López de Prado 2014](https://papers.ssrn.com/abstract=2460551)): the train Sharpe must beat the expected maximum of *N* unskilled strategies, where *N* is the whole ledger.
73
+ - **Validation Sharpe > 0 with Deflated Sharpe ≥ 0.95**, deflated for the number of finalists scored on the unseen validation days.
74
+ - **White's Reality Check, p < 0.05** ([White 2000](https://doi.org/10.1111/1468-0262.00152)), a global gate on validation returns: is the best finalist better than flat once you allow for having looked at all of them? Stationary bootstrap (mean block 10, 1,000 draws). Hansen's SPA test ([Hansen 2005](https://doi.org/10.1198/073500105000000063)), the studentised version with consistent recentring, is reported alongside but is not the gate: at this sample size its measured size is 8–9% at a nominal 5%, while the Reality Check holds 4.5–6% ([`scripts/snoop_size.py`](scripts/snoop_size.py), 1,000 null simulations per setting).
75
+ - **PBO** ([Bailey, Borwein, López de Prado & Zhu 2017](https://papers.ssrn.com/abstract=2326253)) is reported, computed over the finalists' train + validation returns: combinatorially symmetric cross-validation over 16 blocks (12,870 splits), the share where the in-sample best finalist lands in the bottom half out of sample.
76
+ 4. **Holdout.** The search and validation never receive the holdout days: the panel is physically truncated before they run. `falsealpha unseal` scores the survivors and their equal-risk combination on the holdout once; a second look needs `--force`, and the report shows every look.
77
+
78
+ **One deliberate deviation from the DSR paper.** The paper sets the luck bar from the variance of Sharpe ratios *across* trials. In a search that finds something real, dozens of variants of that signal inflate that variance and the test ends up punishing the discovery (on the planted data it rejects a true signal with a Sharpe of 9). falsealpha uses the null variance, 1/(T−1): the expected best of *N* strategies with no skill. Counting every distinct formula as a trial overstates the number of independent trials, so the bar is conservative. The paper's version is still computed and stored (`sr0_train_paper`).
79
+
80
+ ## Quick start
81
+
82
+ ```bash
83
+ pip install falsealpha
84
+
85
+ falsealpha data planted --out planted.npz # random data with one hidden alpha
86
+ falsealpha data noise --out noise.npz # random data, no alpha at all
87
+ falsealpha mine planted.npz runs/planted # search + firewall + runs/planted/report.html
88
+ falsealpha unseal runs/planted # look at the holdout, once
89
+
90
+ falsealpha data binance --out binance.npz # every USDT pair, delisted included (~10 min download the first time)
91
+ falsealpha info binance.npz
92
+ falsealpha eval binance.npz "-rank(ts_delta(close, 5))"
93
+ falsealpha mine binance.npz runs/crypto
94
+ ```
95
+
96
+ Each run writes `config.json`, `ledger.csv.gz`, `finalists.json`, `nets.npy`, `holdout.json` and a self-contained `report.html` (no network, light and dark mode).
97
+
98
+ ### Python
99
+
100
+ ```python
101
+ import falsealpha as fa
102
+ from falsealpha import run
103
+
104
+ panel = fa.Panel.load("binance.npz")
105
+ signal = fa.evaluate("rank(ts_corr(close, volume, 10))", panel.core()) # days x assets
106
+ bt = fa.backtest(signal, "returns", panel.core(), delay=1, cost_bps=10) # daily IC, gross, cost, net
107
+ fa.summarize(bt, 0, 1000) # Sharpe, IC, IC IR, turnover
108
+
109
+ verdict = run.mine("binance.npz", "runs/x", run.MineConfig(population=400, generations=25))
110
+ ```
111
+
112
+ The statistics are exposed on their own: `fa.expected_max_sharpe`, `fa.probabilistic_sharpe`, `fa.pbo`, `fa.snoop_tests`.
113
+
114
+ ## Formula language
115
+
116
+ Fields are whatever the panel holds; Binance panels have `open high low close volume quote_volume vwap returns trades`. Infix `+ - * /` and unary minus work, as do numeric constants.
117
+
118
+ | kind | operators |
119
+ |---|---|
120
+ | elementwise | `neg abs log sign add sub mul div max min` |
121
+ | cross-sectional (universe members, one day) | `rank zscore demean scale` |
122
+ | time-series (trailing *d* days, today included) | `ts_delay ts_delta ts_sum ts_mean ts_std ts_zscore ts_min ts_max ts_argmin ts_argmax ts_rank decay_linear ts_corr ts_cov` |
123
+
124
+ NaN rules: a time-series window must be full of real data; cross-sectional ops only see universe members with a value; a variance below 1e-20 of the mean square counts as zero.
125
+
126
+ **Backtest convention.** Weights are the cross-sectional rank of the signal, demeaned and scaled to gross 1 (dollar-neutral). A signal built from data up to close *t* trades at close *t+delay* (default 1) and earns the next day's return. Costs are charged per unit of turnover. A member with no next-day return (delisted, gap) earns 0, i.e. it is sold at its last price.
127
+
128
+ ## The engine
129
+
130
+ - **Time-major panels.** A *T × N* panel stores one contiguous row of assets per day, so time-series ops sweep rows with per-asset state (vectorised across assets) and cross-sectional ops work on one contiguous row.
131
+ - **O(1) rolling sums** with a periodic rebuild so floating-point drift can't build up; **van Herk / Gil-Werman** sliding min/max; `decay_linear` by recurrence.
132
+ - **Exact second moments.** `ts_std`, `ts_zscore`, `ts_corr` and `ts_cov` are computed two-pass over each window. Running-moment updates (sums of squares, even rolling Welford) lose relative precision when a quiet window follows a volatile history at a high price level; the test suite has the random walk that proves it.
133
+ - **Common subexpressions** within a formula are computed once; only repeated subtrees are cached, so memory stays at about one panel per tree level.
134
+ - **Parallel over formulas**, deterministic for a given seed regardless of thread count (on one platform: C++ standard libraries draw random numbers differently); the GIL is released.
135
+
136
+ **Verified, not assumed:**
137
+ - every operator matches an independent, loop-per-window numpy reference to 1e-9 (relative), including NaN holes, universe masks, constant columns and 3,000-day random walks at a price level of 5,000,000;
138
+ - a property test perturbs every input after day *k* and checks no operator's output on or before day *k* moves (no look-ahead, by construction);
139
+ - the Deflated Sharpe reproduces the worked example in Bailey & López de Prado (2014) (SR₀ = 0.1132, DSR = 0.9004);
140
+ - the Reality Check rejects at about its nominal 5% on noise (SPA over-rejects at this sample size, measured and documented above) and both have power on a real edge; PBO averages about 0.5 on noise and about 0 with a dominant strategy;
141
+ - the search recovers a planted alpha, the firewall passes it and rejects pure noise;
142
+ - C++ tests run under AddressSanitizer and UBSan in CI.
143
+
144
+ ## Speed
145
+
146
+ Apple M3 Pro (11 cores), 2,000 days × 500 assets, best of 3:
147
+
148
+ | | time | vs pandas |
149
+ |---|---:|---:|
150
+ | 12 Alpha101-style formulas, falsealpha, 1 thread | 501 ms | 6.6× faster |
151
+ | same, 11 threads | 131 ms | 25× faster |
152
+ | same, pandas | 3,307 ms | |
153
+
154
+ | operator (1 thread) | falsealpha | pandas | |
155
+ |---|---:|---:|---:|
156
+ | `rank(close)` | 29.7 ms | 37.0 ms | 1.2× |
157
+ | `ts_mean(close, 20)` | 1.9 ms | 11.5 ms | 6× |
158
+ | `ts_std(close, 20)` | 8.8 ms | 19.1 ms | 2× |
159
+ | `ts_corr(close, volume, 20)` | 20.2 ms | 77.6 ms | 4× |
160
+ | `ts_max(close, 20)` | 3.3 ms | 21.0 ms | 6× |
161
+ | `ts_argmax(close, 20)` | 28.0 ms | 774 ms | 28× |
162
+ | `ts_rank(close, 20)` | 10.3 ms | 1,076 ms | 104× |
163
+ | `decay_linear(close, 20)` | 3.6 ms | 520 ms | 145× |
164
+
165
+ The honest reading: on operators pandas implements in C (`rank`, rolling mean/std/corr) the gain is 1–6×, and `ts_std` / `ts_corr` deliberately pay for an exact two-pass computation. The large gains are on window operators pandas can only do through Python-level `rolling().apply()`. In a search, the parallelism is over formulas, so throughput scales with cores: the default noise run above scored 6,002 formulas over 2,500 days × 60 assets in about 5 seconds.
166
+
167
+ `falsealpha bench` reproduces this. The pandas side evaluates the *identical* formula text (canonical formulas are valid Python expressions) with the usual pandas idioms: `rolling()`, `rank(axis=1, pct=True)`, `rolling().apply()` where pandas has no built-in. Where the definitions coincide the two agree to 1e-8 (tested). Most of falsealpha's single-thread time is `rank`, a sort per day, which pandas also does in compiled code; the big gaps come from window operators pandas can only do through Python-level `apply`.
168
+
169
+ ## Data
170
+
171
+ **Crypto (built in).** Binance's public dump ([data.binance.vision](https://data.binance.vision)) keeps every spot pair's daily candles, including pairs that were delisted, which is what makes a survivorship-free universe possible. The loader:
172
+ - drops stablecoins, fiat pairs and leveraged tokens;
173
+ - splits a ticker into separate assets when it goes more than 7 days without candles (LUNAUSDT resumes 18 days after the May 2022 collapse as a different coin);
174
+ - handles the switch from millisecond to microsecond timestamps in 2025;
175
+ - builds the universe each day from data up to that day: the top *N* by trailing 30-day dollar volume, with at least 60 days of history;
176
+ - starts the panel in 2020 by default: before that Binance had fewer than 40 liquid USDT pairs on a typical day, too few for cross-sectional ranks (earlier data still counts as history).
177
+
178
+ **US equities (planned).** Free daily data with delisted names is scarce. The plan is point-in-time S&P 500 membership ([fja05680/sp500](https://github.com/fja05680/sp500)) plus Tiingo's free tier, which carries delisted tickers, with results reported three ways (survivors only, point-in-time, and a delisting-loss stress) so the remaining bias is visible. yfinance can't be used: it has no delisted tickers and returns a different company for reused ones.
179
+
180
+ ## Limitations
181
+
182
+ - Daily bars only; costs are a flat charge per unit of turnover, with no market impact or borrow costs.
183
+ - A delisted asset is assumed sold at its last close. Real exits can be worse.
184
+ - The holdout seal is an honour system with an audit trail (the look log and a fingerprint of the panel file), not cryptography.
185
+ - Counting distinct formulas as trials is conservative; clustering trials into independent groups would give a sharper luck bar.
186
+ - `falsealpha eval` shows validation results for any formula you type, and those looks are not counted. Hand-testing ideas on validation is a search the firewall cannot see; keep it to train, or count it yourself.
187
+
188
+ ## Development
189
+
190
+ ```bash
191
+ python -m venv .venv && .venv/bin/pip install scikit-build-core pybind11 numpy pandas pytest certifi
192
+ .venv/bin/pip install --no-build-isolation -e ".[dev]"
193
+ .venv/bin/pytest # Python suite
194
+ cmake -S . -B build -DFALSEALPHA_BUILD_TESTS=ON && cmake --build build -j && ./build/core_tests
195
+ ```
196
+
197
+ ## References
198
+
199
+ - D. H. Bailey, M. López de Prado (2014). The Deflated Sharpe Ratio: Correcting for Selection Bias, Backtest Overfitting and Non-Normality. *Journal of Portfolio Management* 40(5).
200
+ - D. H. Bailey, J. Borwein, M. López de Prado, Q. J. Zhu (2017). The Probability of Backtest Overfitting. *Journal of Computational Finance* 20(4).
201
+ - P. R. Hansen (2005). A Test for Superior Predictive Ability. *Journal of Business & Economic Statistics* 23(4).
202
+ - H. White (2000). A Reality Check for Data Snooping. *Econometrica* 68(5).
203
+ - D. N. Politis, J. P. Romano (1994). The Stationary Bootstrap. *JASA* 89(428).
204
+ - C. R. Harvey, Y. Liu, H. Zhu (2016). …and the Cross-Section of Expected Returns. *Review of Financial Studies* 29(1).
205
+ - Z. Kakushadze (2016). 101 Formulaic Alphas. *Wilmott* 2016(84). [arXiv:1601.00991](https://arxiv.org/abs/1601.00991)
206
+
207
+ MIT licence.
@@ -0,0 +1,14 @@
1
+ #pragma once
2
+ #include "falsealpha/expr.hpp"
3
+ #include "falsealpha/mat.hpp"
4
+
5
+ namespace fa {
6
+
7
+ // Evaluates a formula over the whole panel. Repeated subtrees are computed once.
8
+ // The result is masked to the universe: NaN wherever the asset is not a member.
9
+ Mat evaluate(const Node& n, const Data& d);
10
+
11
+ // Throws std::invalid_argument if the formula uses a field the data lacks.
12
+ void check_fields(const Node& n, const Data& d);
13
+
14
+ } // namespace fa