Decisions
Copied at M1 from the owner's DECISIONS.md (recorded 2026-10-31), which overrides the spec. Agent decisions on ambiguous points follow. API keys never go in this file.
Owner decisions that apply here
- Project: #2
backtest-overfitting-lab, in the first batch with #3crypto-lob-alphaand #4vol-surface-modeling. Scope is all 15 projects, for researcher and developer seats, a few at a time, with no fixed date. - Merging: agents push feature branches and open PRs. PRs merge automatically once CI is green. Branch protection and auto-merge get turned on per repo once its CI exists (M1).
- GitHub: the
ghactive account isavmhdt, with admin on the repo. Actions is enabled. - WRDS: no access. CRSP, Compustat, OptionMetrics and Capital IQ paths stay off.
- Sharadar: Full History Bundle bought directly from Sharadar, not through Nasdaq Data Link.
- Base URL
https://api.sharadar.com/v1.0, key inSHARADAR_API_KEY, sent in thex-api-keyheader so it stays out of URLs and logs. - Tables
stocks(SEP),funds(SFP),fundamentals(SF1),tickers,actions. Bulk zips withyears=full. - This project's spec was rewritten on that basis.
- Licence: Personal Use License. Sharadar replied in writing on 2026-10-31 that public code, aggregate results, job applications, aggregate cross-checks and hosted coding tools are allowed. Publishing stays aggregate-only, with no security-level data.
- Money: Sharadar is an existing subscription. Nothing else is approved for this project (USD 0).
- Storage: stay on
D:. Free space is checked at each milestone. Project data never goes onC:or into git. - Waiting on the owner: put the Sharadar key in
.env. - Accepted: every other owner decision in section 3 of the spec with a default stands as written. None of the changes from spec defaults in
DECISIONS.mdapply to this project.
Spec decisions
| ID | Decision | Status |
|---|---|---|
| D-01 | Package and import name overfitlab |
Accepted. Checked 2026-11-01: pypi.org/simple/overfitlab/, test.pypi.org/simple/overfitlab/ and both JSON APIs return 404. The HTML project pages return 200 to scripts because they serve a bot-challenge shell, so the simple index is the reliable check. Re-check before the first release. |
| D-02 | Publish to PyPI | Default: (a) TestPyPI then PyPI on v0.1.0 once P-12 to P-14 are done, otherwise (c). |
| D-03 | Licence | MIT, code only. Licensed data is never redistributed. |
| D-04 | Paid data spend | No new spend. |
| D-05 | Universe | Default stands. |
| D-06 | Verdict thresholds | Default stands: dsr_min = 0.95, pbo_max = 0.20, snoop_alpha = 0.05, BHY haircut Sharpe above 0. |
| D-07 | Monte Carlo budget | Default stands. |
| D-08 | Trials in the headline DSR | Default stands: raw N in the headline, clustered N alongside. |
| D-09 | Kurtosis convention | Non-excess (normal = 3) everywhere in the public API. |
| D-10 | Annualisation | 252, 52 and 12. Golden tests pass the paper's own value. A log's x_periods_per_year overrides the inferred value. |
| D-11 | Contract freeze | Default stands: freeze v1.0 at the end of M2. |
| D-12 | Docs host | Default stands: GitHub Pages. |
| D-13 | Python versions | 3.12 and 3.13 in CI, requires-python = ">=3.12". |
Owner rulings, 2026-11-01
- AC-W-07 stays open. The owner asked for a stricter check. The raw-against-raw comparison with
archis kept as evidence and does not close the criterion. The limit is unchanged. An independent reference that really studentises is needed; the candidates are in validation. - G-03c is a documented exception to AC-W-03. The tolerance is unchanged and the case still reports
pass=false. It misses by 5e-6 because the paper feeds an input printed to three decimals, so the exception covers this one case and no other. The AC-W-03 row of the local spec records it. - G-07 is sourced (2026-11-02): Advances in Financial Machine Learning, chapter 12, section 12.4.1, p. 164, with Figures 12.1 and 12.2, checked in the owner's copy. Only the two counts and the citation are recorded.
-
Deviation from P-02: the repo's default workflow permission stays at
read, not read/write as P-02 asks. Workflows declare their ownpermissions:, so M1 to M4 do not need the default raised. -
AC-W-02: the DSR miss is kept as a finding (2026-11-02). The
dsrrows ofpower_check.csvkeep reporting the miss, and CI keeps the strict expected failure. The fixed-V diagnostic is post hoc and does not count toward the criterion. M5 proceeds on that basis, and the writeup must explain the cause. - AC-W-07: Octave and R are approved (2026-11-02).
bsds.munder GNU Octave is the studentised reference for SPA and the Reality Check, and R'sStepwiseTestfor StepM. Case 2's miss is documented with its cause, the limit does not change, and no paid licence is approved.
Agent decisions
- M1,
DATA_ROOTon Windows:.env.exampleis fixed by the spec and setsDATA_ROOTto a~path meant for WSL. On native Windows the settings loader ignores aDATA_ROOTthat starts with~, so a copied.envcannot put project data onC:. - M1, study in the dev group:
overfitlab-studyis a dev dependency of the root project, souv syncfollowed byuv run olab-studyworks from the repo root. The published wheel still contains onlyoverfitlab. - M1, line endings:
.gitattributesforces LF, because the trial-log dialect andruff formatboth need LF on Windows too. - M1,
.gitignore: besidesresults/logs/, addedresults/sims/and tool caches, plus a negation forstudy/src/overfitlab_study/data/, because the existingdata/rule also matched that code package. No existing rule was removed. - M1, action pins:
astral-sh/setup-uvpublishes no moving major tag, soci.ymlpins an exact release. - M2, flat series:
sharpe_ratioreturns 0.0 for a series with zero variance, so a flat factory variant does not abort a matrix evaluation.return_momentsstill raises, because skew and kurtosis are undefined there. - M2, skewness and kurtosis: moment ratios without a small-sample correction, matching
scipy.stats.skewandkurtosis(fisher=False)defaults, which is what the PSR code in Bailey and López de Prado's appendix expects. - M2, haircut conventions: follows
Haircut_SR.m, not the paper's Exhibit 7, where they differ; see validation.haircut_sharpereturns the autocorrelation-corrected Sharpe ratio insr_annual, andhaircut_pctis relative to it, as in Exhibit 5. - M2, simulated golden tolerances: the spec says Holm and BHY match "within the printed precision because they are simulated". The authors' values come from 2,000 unseeded runs, so half a unit of the third decimal is below their own noise. Tolerances are 3 standard deviations of a 2,000-run median, measured before any comparison with the printed values.
- M2, G-03c: a known miss of 5e-6 past tolerance. The tolerance was left alone; see validation and the owner's ruling above.
- M2, contract details settled before the freeze (D-11):
E_SHARPEwas added, because the spec's error list had no code for an unreadablesharpe_annual.W_SUMMARY_ONLYwas added. The Parquet files are git-ignored, sooverfitlab trials validate results/trial_log.csvin a sibling's CI sees none. Such a log passes on its CSV checks with this warning. If any series exists, a missing one isE_RETURNS_MISSING.--no-strictandstrict=Falselist errors without failing.--strictis the default.- Rows with
n_obs = 0may leavestartandendempty. An emptyx_periods_per_yearcell means absent. E_FREQ_MIXapplies per family in the file validator, and across the whole scope in the evaluator, so families at different frequencies may share a log.TrialLoggerwritesx_periods_per_yearandx_notein every new log. An existing log keeps its header, since the file is append-only.- The
-dirtycheck ignores the log file itself, otherwise every run after the first would be marked dirty. - M2,
evaluate_trial_log: ships in M2 with N, V, SR₀, DSR and the haircut. PBO, the snooping tests and the verdict are added in M4, and until then the verdict isINCONCLUSIVEwith a reason.trials reportandtrials add-manualalso ship in M2, because the frozen contract refers to them. - M3, embargo: the window of
ceil(embargo_pct * T)rows starts after the last row purged behind a test fold, not right after the fold. Right after the fold it would sit inside the purge whenever labels span several days and remove nothing. With point labels the two readings coincide. - M3,
t0: the default of row positions works only with integert1, so datetimet1needs an explicitt0. - M3, factory: breakout and mean-reversion run a long-short state machine and long-only clips it at zero, the same rule as the moving-average family. With
exit_z = 0.0the spec's rule|z| < exit_znever fires, so those variants hold until the opposite entry; this is kept as written. Rolling standard deviations useddof=1.exit_lengthuses Python'sround. - M3,
run_strategies:T + 1closes give aT x Nmatrix, because the first close has no return. - M3,
planted_signal_prices: the trend oracle isma_cross10/60 and the mean-reversion oracle ismean_reversion20, 1.0, 0.0, both long-short. The amplitude is calibrated by bisection on one seeded 100,000-bar path, before costs. - M3,
bootstrap.py: arrives in M3 instead of M4, becauseblock_bootstrap_returnsneeds it. - M3, generators:
gaussian_noiseandstudent_t_noisealways returnT x N, including whenN = 1.gbm_pricesreturnsn_obsprices, so a run ofTreturns needsT + 1prices. - M3, G-07: see the owner's rulings above.
tests/unit/test_cv.pyalso asserts 15 splits and 5 paths against the formula in FR-09. - M4,
archcomparison:arch8 does not studentise, so the 0.02 and 1e-8 checks compare the Reality Check andspa_test(studentize=False)witharch. This is evidence and does not close AC-W-07; see the owner's ruling above. The studentised gap is reported asmax_studentised_gap.studentizeis an added keyword onspa_test, and its default is Hansen's statistic. - M4, PBO: rows beyond the last whole block are dropped, as in the paper. A logit of exactly zero counts toward PBO, as the spec's
λ ≤ 0says. - M4, block length: the default is the median Politis-White length over the columns. The gate uses one set of resamples for SPA and the stepdown.
- M4,
evaluateneeds at least nine observations for the snooping tests: with fewer,evaluate_trial_logreports them as not computed. - M4,
TrialLogReport: gainsspa_pvalueandstepm_rejects_selected. - M4, clustered N: average linkage, the number of clusters is scanned on up to 40 geometric steps, and a best mean silhouette below 0.05 counts as no structure, so every trial stays independent.
- M4, noise table:
noise_experiment.csvholds one summary row per setting. The per-seed values stay in the git-ignoredresults/sims/noise/. - M5, AC-W-02: the three DSR rows with N > 1 miss the reference, which the owner kept as a finding; see the rulings above.
dsrestimates V from the same trials, and the reference assumes V is known.dsr_known_vfixes V at 1/(T − 1). It is post hoc: it was added after the failure, so it does not count. - M5, power tests: a test detects when it rejects on the best trial, whichever trial that is, because the reference power is the probability that the maximum clears the bar. PSR is the naive PSR of the best trial. The haircut rejects when the BHY adjusted p-value is at most α, with 1,000 simulated sets per replication. PBO "rejects" at most at 0.20, the D-06 limit.
- M5, power bootstrap: block length 1, because the simulated returns are IID. The closed-form tests run 1,000 replications and the rest 500, per D-07. The PBO trend uses the SR grid at 5 years and N = 100.
- M5,
^spx: not in the Stooq snapshot, so^ndxis the nearest available index (D-05). - M5, ETF history: Stooq's U.S. ETF files start in 2005, and Sharadar's reach further back. The case study uses the dates all 12 series share, from 2006.
- M5, Sharadar:
closeinprices_dailyiscloseadj, and the other prices are scaled by the same ratio, so returns include distributions. The key is sent inx-api-key, as the owner decided, though Sharadar's docs show a query parameter. - M5,
STOOQ_DIR: the owner's.envdoes not set it, sodata stooqtakes--stooq-dir. - M5, factory case: a trial is one variant across the whole universe, equal weighted. One trial per symbol and variant would give 12,408 rows, over the 1 MB limit on tracked files, and would multiply the count without adding independent ideas. The first 250 bars are dropped, so every variant is live throughout. ETFs and the index are excess of the French risk-free rate. FX is already excess (DQ-07).
- M5, holdout:
x_is_holdout = truein a separate family, because its dates do not overlap the in-sample trials. A rerun reads the holdout back from the log and never recomputes it. - M5, factors case:
F-F_Research_Data_Factorsplus the four AQR datasets.RFis excluded. Columns needing a window shorter than the common one are reported as dropped. - M5, OSAP: the owner supplied release 2026.10 of the OP set, so the
openassetpricingpackage was not needed and nothing was downloaded twice.cases.osapinstudy.yamlnow readsrelease: "2026.10", weighting: op, because that set uses each original paper's weighting, not value weighting throughout. Value weighting is not needed: the question is whether the published results survive, and the OP set is the published construction. - M5, OSAP windows: in-sample is the paper's sample years, and post-publication is after the publication year. For StepM a predictor's months with no portfolio are 0.0, which leaves its t-statistic unchanged. Predictors need 60 months in a window.
- M5,
git_sha: a study run records the commit it started from, andresults/andreports/do not mark it dirty. validate studentised: a command added to section 10 of the spec for the owner's AC-W-07 ruling. It runs locally because it needs Octave and R, and CI keeps thearchcomparison.bsds.mis pinned to commit9622b6c546bcand kept out of the repo, because the toolbox has no licence file.StepwiseTestinstalls into the WSL user library throughscripts/r/install_stepwise.R..env:STOOQ_DIRwas added on 2026-11-02, as the owner approved, sodata stooqno longer needs--stooq-dir. The key was not read.- M6, CI: the coverage job took 44 minutes because it ran the
xchecktests through pure-Python loops. It now leaves them tostudy-quick, which runs them with Numba on. -
M6,
all: runs the study commands on the tables already indata/. Ingest stays separate, because Sharadar and French need the network, and a missing table skips the case studies with a message.--quickalso skipsvalidate arch, which CI runs at full size, andbench, because timings differ on every run.validate studentisedis not part ofall, because it needs Octave and R. -
M6, docs: theory pages quote the committed tables, and
tests/unit/test_docs.pychecks the API page against__all__, the docstring sections, the word limit and the getting-started examples.import overfitlabis tested not to load pyarrow, typer or Polars, and its time stays inolab-study bench. - M6, haircut:
haircut_sharpereturnedinfonce the adjusted p-value fell below about 1e-16, because1 - p/2rounded to 1. It now inverts the lower tail and caps the result at the t-statistic. One row ofcase_factors(AQR's Intl Stock Selection Multi-style, t = 9.8) had showninf. - M6, calculator:
npm testrunsnode --test "js/test/*.test.mjs", because Node 22 does not accept the spec's directory form,node --test js/test/. MinTRL is against a benchmark of 0, MinBTL takes the entered Sharpe ratio as the target, and autocorrelation enters only the haircut, as in the CLI. - M6, FR-34: the Holm and BHY lookup is a Could and is deferred. The page gives the Bonferroni haircut, which is exact, and points to the library for the other two.
- M6, release:
release.ymlalso runs by hand, to prove Trusted Publishing and the install without a tag. A manual run builds<version>.dev<run>, so the real version stays free on TestPyPI, and the PyPI job runs only on a pushedv*tag. The tag must matchpyproject.toml.scripts/smoke_install.pyruns the README quickstart from a fresh venv. - M6, full run: the AC-W-09 run started from
1e40df2. Every table matched the commit except benchmark times,case_factors(the haircut fix) andown_projects(below). Rows fromsim leakageon carry-dirtybecausesim powerrewrotedocs/assets/power_curves.htmlwith its first pinned id, and nothing else in the tree had changed. The rows at8021ed2and2760f4fare a dry run made while the docs were being edited. - M6, own projects:
equity-factor-pipelinenow logs 517 trials in eight families, up from 20 in one. FR-29 evaluates each family alone, and three pass. That is a statement about each family's own search: with N = 517 the bar forinvestmentrises from 0.70 to 1.02, above its Sharpe ratio of 1.00. - M7, own projects: the
equity-factor-pipelinelog moved from 517 to 613 trials between runs while it was being rebuilt, so the table and the prose that quotes it are provisional until that rebuild lands.own_projectsnow also reports each family's DSR at the project's whole trial count (n_raw_project,sr0_annual_project_n,dsr_project_n). These columns add to FR-29 and do not change its verdicts. - M7, README: rewritten as the research memo. The checklists stay, with their ids, under the heading "Depth" in place of "Impresses hirers", because the README is also the PyPI page.
tests/unit/test_docs.pyfails if the README or memo stops matching the committed tables. - M7, final run: the M7 exit run started from
e7566b6, after the reboot. Every table and figure matched the commit except benchmark times. Theequity-factor-pipelinelog still had 613 trials, so the README and memo numbers stand. Rows fromsim leakageon carry-dirtybecause Plotly wrotepower_curves.htmlwith Windows line endings. The content was identical, and the chart is now written with\nendings. - M7, own projects final: the owner confirmed on 2026-11-03 that the
equity-factor-pipelinenumbers are final. Its PR 9 rebuilt the project from a fresh clone and added no trials, and regeneratingown_projectsfrom its merged log gives the committed table byte for byte. The README and memo no longer mark the table provisional. - M7, AQR permission: AQR's terms of use bar publishing information from its site without written consent. On 2026-11-03 AQR gave written permission to publish aggregate statistics and charts with citation, with no redistribution of its files or raw series. The owner holds the email. AQR is cited wherever its results appear, and its files stay in the git-ignored
data/raw/aqr/. - M7, superseded French-only run: before the permission arrived, PR #17 withheld AQR results and reran the factors case on French data alone. It was closed without merging. That run's experiment-log row (
c6fbb18a, commitdaa6f39on branchaqr-withheld) is kept, because the log is append-only, and is superseded by the permission. Its results are not published. - v0.1.0, AC-H-03:
v0.1.0was tagged onc612356and published to PyPI on 2026-11-03, with a wheel and an sdist, and is listed inCHANGELOG.mdafter the owner approved thepypienvironment.pip install overfitlab==0.1.0from PyPI into a fresh venv then passedscripts/smoke_install.py, which runs the README quickstart, the CLI and the light-import check. It passed on Windows 11 with Python 3.12.10, and on Ubuntu 22.04 under WSL with Python 3.12.15 fromuv venv --seed, because the system Python is 3.10. AC-H-03 is met. - 0.1.1, README: the owner ruled on 2026-11-03 that the README, which is the PyPI page, should read as a package description. The per-family trial-log table moved to
docs/projects.md, and the requirement and success-criteria checklists moved todocs/criteria.md. This departs from the spec's repository layout, which keeps the checklists in the README. The release is README only, as version 0.1.1.