piotroski-liquidity-research asks whether the Piotroski F-score premium is explained by firm size or by stock liquidity, using point-in-time backtests. Its headline is that the premium tracks illiquidity, with a capacity of about $250,000. I re-ran its US scripts on 23 October 2026 against a US price panel whose last session is 21 October 2026 and SEC EDGAR filings filed up to 20 October 2026. Not investment advice. Educational and research use only; historical averages on short samples, and the repo's own numbers are provisional.
Open-Source Tools · How-To · Markets research
How to use piotroski-liquidity-research: a point-in-time test of the Piotroski F-score and liquidity
Revised
· v1.0.0 · what changed
Code: github.com/herrrickshaw/piotroski-liquidity-research
Watch the walkthrough
Watch on YouTube: youtu.be/m8uKeoLRaBo
Why it is useful
- Who benefits: students and independent researchers who want a worked, honest example of a point-in-time factor study, including the data bugs found along the way.
- What it replaces: guessing whether an F-score screen survives size and liquidity controls; the repo ships the scripts, the paper and the result tables.
- Evidence from the run: the US scripts (completeness audit, daily Darvas x F-score scan, weight-preset sweep, cost-versus-edge) all ran end to end on 23 October 2026 and regenerated their result files in under a minute each.
- Evidence from the run: in the regenerated daily signal file (16,333 signals, 1,802 tickers) the median 12-month forward return rose from -2.8% for the lowest-turnover tier to +8.9% for the highest, the same direction as the committed file (-3.1% to +8.2%). Almost all of those signals fall in 2023 to 2026, so it is a short, regime-dependent window.
What it is for
The repo studies one question: is the Piotroski F-score premium conditioned by firm size or by liquidity? It double-sorts liquidity within size, uses point-in-time market cap and SEC EDGAR filing dates, keeps delisted names, and reports medians rather than means.
Its headline table says the premium is largest in large but illiquid firms and comes from weak-F names falling, not strong-F names rising. It then estimates execution costs and puts a capacity ceiling near $250,000.
The README opens with a warning that its own figures are being regenerated after data defects were found, so treat every number as provisional. That candour is part of what makes it worth reading.
The scripts need an EDGAR filings table and a US price panel that the repo does not ship. For this post I re-ran the US scripts on a US price panel (last session 21 October 2026) and an EDGAR filings table (filed up to 20 October 2026), with the hard-coded file paths pointed at those copies in a scratch clone. The EDGAR table is dense only from fiscal 2021, so the re-run covers roughly 2023 to 2026, not the repo's 2016 to 2025. I did not use the India scripts or file, which derive from screener.in data.
Who it is for
- Students learning point-in-time backtesting, survivorship handling and why medians beat means
- Researchers who want a template for liquidity-conditioned factor tests
- Anyone sceptical of factor claims who wants to see the validation failures documented
- Investment advice or trading signals. Not investment advice.
- Reproducing the headline tables without the EDGAR and price panels the README lists
- Trusting any number as final: the README says the tables are provisional
What you need
| Requirement | Why | How to get it |
|---|---|---|
| Python 3 with pandas, numpy and duckdb | the research scripts read Parquet through DuckDB | python3 -m venv .venv && pip install pandas numpy duckdb |
| A US price panel and an EDGAR filings table, both Parquet | each script has FUND and PX constants at the top that point at the author's machine; edit them. PX needs Symbol, Date, Open, High, Low, Close, Volume; FUND needs ticker, fy_end, filed, net_income, cfo, total_assets, current_assets, current_liabilities, long_term_debt, shares, revenue, gross_profit, ebit, equity | build them from your own data; the repo does not ship them. Note the sweep script expects a column called equity |
| Reading time for the paper | reports/PIOTROSKI_LIQUIDITY_PAPER.md holds the method and limitations | open in any Markdown viewer |
Get the code and an AI assistant
You do not have to be a programmer. Get the repository, give it to an AI assistant, and ask in plain language. Any of these routes works:
| Tool | What you need | Best for |
|---|---|---|
| GitHub | Free account to fork or star; git to clone, or the green Code button, then Download ZIP, or open in Codespaces | Getting the code and keeping up with updates |
| Claude Code | Claude subscription or API key; run claude inside the cloned folder | Letting the AI read and run the whole repo |
| Gemini CLI | Google account; run gemini inside the cloned folder | The same hands-on workflow with Gemini |
| Claude.ai, Gemini or ChatGPT in the browser | A chat account; upload README.md and the files you care about (or paste the output of git ls-files) | Understanding the project and asking questions without installing anything |
| Other tools (Copilot Chat, Cursor, Codex CLI) | Open the cloned folder in the tool and point it at README.md first | Editing and extending the code |
git clone https://github.com/herrrickshaw/piotroski-liquidity-research.git
cd piotroski-liquidity-research
claude # or: gemini
Install
- Clone the repo and make a virtual environment.
git clone https://github.com/herrrickshaw/piotroski-liquidity-research.git cd piotroski-liquidity-research python3 -m venv .venv && source .venv/bin/activate pip install pandas numpy duckdb # then edit the FUND and PX constants at the top of each US script
Quick start: commands and what you should see
Check the sample-size claim before trusting anything (run this first, as the README says).
python3 data_completeness_audit.py
One line: fundamentals tickers, price tickers and their overlap. On 23 Oct 2026: 6,897 | 10,110 | 6,889 (100% / 68%).
Regenerate the daily Darvas x F-score signal file and the tier table.
python3 daily_breakout_combo_us.py
About 40 seconds. Prints signal counts, a tier table and a per-year table, and rewrites reports/daily_breakout_combo_us.csv.
Run the weight-preset sweep, then the cost test that reads its output.
python3 sweep_piotroski_plus_us.py && python3 cost_vs_edge.py
About 15 seconds each. The cost test prints net edge by tier at ten portfolio sizes.
Sample outputs
Real output from a run on the date shown in the sources line; trimmed to fit.
Data completeness audit (23 Oct 2026; US price panel last session 2026-10-21)
python3 data_completeness_audit.py
=== source reconciliation ===
fundamentals 6,897 | prices 10,110 | overlap 6,889 (100% / 68%)
The committed script comments quote 9,278 price symbols and 4,459 overlap (97%). The fundamentals table here has 6,897 tickers, but most fiscal years before 2021 are sparse, so tickers overlap does not mean years of history overlap.
Daily Darvas x F-score scan, by liquidity tier (regenerated; winsorised median minus same-tier baseline)
python3 daily_breakout_combo_us.py
raw first-cross breakouts: 471,700
after 10-bar cooldown: 201,749 signals, 7,960 symbols
signals WITH point-in-time fundamentals: 16,333 (1,802 symbols, 11 years)
tier base med darvas n darvas×F7 n darvas×F<=3 n
SMALL 0.1% -2.9% 5,445 +6.6% 850 -14.9% 1076
MID 2.9% +3.3% 5,444 +7.9% 1695 -7.7% 426
LARGE 6.8% +2.1% 5,444 +2.7% 1984 -6.5% 248
By signal year: 4 in 2016, 16 in 2017, 5 in 2018, 17 in 2019, 31 in 2020, 31 in 2021, 127 in 2022, 4,059 in 2023, 5,887 in 2024, 5,800 in 2025, 356 in 2026. Effectively a three-year sample, and signals cluster in rallies.
Median 12-month forward return by tier, then by tier and F-score band (re-aggregated from the regenerated file)
groupby('tier') and pd.cut(F9,[-1,3,6,9]) on reports/daily_breakout_combo_us.csv
n med mean sd
LARGE 5444 0.0894 0.2146 0.6170
MID 5444 0.0619 0.1774 0.6074
SMALL 5445 -0.0280 0.1438 0.7874
F0-3 F4-6 F7-9
LARGE 0.0036 0.0855 0.1018
MID -0.0487 0.0565 0.0882
SMALL -0.1480 -0.0452 0.0609
The committed file showed no clean F-score gradient (for example SMALL -1.2%, -3.8%, -2.1%). In this 2023 to 2026 window every tier rises from F0-3 to F7-9. Cells range from 248 to 3,149 rows; 252-day windows overlap.
Weight-preset sweep, US (regenerated; 9 rebalances, but only 4 with forward returns)
python3 sweep_piotroski_plus_us.py
fundamentals: 15,654 ticker-years | 3,865 tickers
scored panel: 7,548 stock-years | 2,926 symbols | 9 rebalances
vector mean median vs univ beat univ worst
[universe] 34.4% 15.0% -- -- -32.6%
canonical 29.4% 28.1% +14.8% 4/4 13.0%
quality 30.1% 28.1% +15.6% 4/4 16.0%
turnaround 24.2% 21.1% +9.7% 3/4 11.6%
safety 28.4% 28.1% +13.9% 4/4 9.4%
tier n F>=70 med F<40 med med edge
SMALL 2,518 1.2% -25.4% +26.5%
MID 2,516 5.5% 5.4% +0.1%
LARGE 2,514 13.9% 19.9% -6.0%
Not comparable with the committed sweep (which had 3,294 observations): different universe, and only four rebalance years. The universe mean (34%) far above its median (15%) points to outliers, probably unadjusted corporate actions in the price panel, so read medians only. The small-tier edge is the opposite sign to the large tier here, the pattern Piotroski's original paper predicts, per a comment in the script.
Does the small-cap edge survive costs? (regenerated cost test, 2,923 symbols; prices through 2026-10-21)
python3 cost_vs_edge.py
tier n turnover/day spread impact per $1M
SMALL 975 $0.2M 1.07% 47.312%
MID 974 $5.8M 0.48% 0.648%
LARGE 974 $75.7M 0.23% 0.026%
$ 100,000 cost 2.01%/yr ( 6.0% of ADV) best gross +13.3% -> net +11.3% SURVIVES
$ 250,000 cost 3.43%/yr ( 15.0% of ADV) best gross +13.3% -> net +9.9% SURVIVES
$ 500,000 cost 5.80%/yr ( 30.0% of ADV) best gross +13.3% -> net +7.5% SURVIVES
$ 1,000,000 cost 10.53%/yr ( 60.0% of ADV) best gross +13.3% -> net +2.8% SURVIVES
$ 2,000,000 cost 19.99%/yr (119.9% of ADV) best gross +13.3% -> net -6.7% DEAD
Two things. The README's capacity ceiling of about $250k is consistent with these costs (15% of daily volume at $250k). But the script defines a 20%-of-volume ceiling (ADV_CEILING_PCT) and never applies it, so it still prints SURVIVES at $500k and $1M, where positions are 30% and 60% of daily volume. I saved a patch (not pushed). The gross edges used are constants hard-coded in the script from the earlier sweep, not recomputed.
Ways to use it
| Method | How it works |
|---|---|
| Read the paper | reports/PIOTROSKI_LIQUIDITY_PAPER.md and CAVEMAN_SUMMARY.md give method, results and limitations. |
| Re-run on your own data | Point FUND and PX at your EDGAR and price Parquet files, run the audit first, then the daily scan, sweep and cost scripts. |
| Adapt the scoring | piotroski_plus.py holds a canonical 9-point F-score plus a 3-point ROCE block with tunable weights. |
| Colab | notebooks/colab_test.ipynb is linked from the README (not run). |
| Via an AI assistant | Ask an assistant to explain the method and audit the caveats before you extend it. |
Sample prompts for your AI assistant
Copy, paste and edit the parts in [brackets].
- Read README.md and the paper in reports/ and explain, in plain English, why the author says the premium tracks illiquidity rather than size.
- Using reports/daily_breakout_combo_us.csv, compute the median and win rate of wret by tier and by year and tell me which results are most regime-dependent.
- List every data defect the README says was found, and for each say whether it is fixed and which results it could change.
- Explain why the README treats median as the headline and what overlapping 252-day windows do to the effective sample size.
- Write a checklist I could apply to my own backtest from the 'Metrics that failed validation' table.
- Suggest what extra controls (beta, book-to-market) would be needed before treating this as evidence, and what data I would need.
Limits and honest caveats
- Not investment advice. Educational and research use only.
- The README states that numbers in reports/ are provisional and are being regenerated after eight defects were found; one (lender EBIT and ROCE) is documented but unfixed. My re-run does not settle any of them.
- Data freshness: US prices run to the 2026-10-21 session and EDGAR filings to 2026-10-20, but the price panel is patchy: on several recent days only 2,000 to 3,000 of roughly 7,000 US symbols have a bar (for example 15 and 16 October). Anything that needs a clean daily history should be checked with the audit first.
- The EDGAR table I used is dense only from fiscal 2021 (about 4,700 tickers in 2021 versus a few dozen per year before 2020). So the re-run is a 2023 to 2026 test with 98.6% of signals in those four years, not the repo's 2016 to 2025 sample, and the results are not comparable one-for-one with the committed tables.
- Power is low and windows overlap: the sweep has only four rebalance years with forward returns, no standard errors are reported, and outliers (mean 34% versus median 15% in the universe) suggest unadjusted corporate actions. The India scripts were not run and the India file is not shown because it derives from screener.in data.
- The cost test's gross edges are hard-coded constants, and its 20%-of-volume ceiling is defined but unused (patch saved, not pushed).
Sources: README, paper and scripts in github.com/herrrickshaw/piotroski-liquidity-research, re-run on 23 October 2026 in a throwaway virtual environment against a US price panel (last session 21 October 2026) and an SEC EDGAR filings table (filed to 20 October 2026). Underlying filings are SEC EDGAR public data; only derived aggregates shown; no vendor data.
- v1.0.0 — 23 October 2026 — first published.
About this article: Researched, written and edited by Umashankar Triplicane Dwarakanathan, with AI research assistance; every figure is meant to trace to the primary source cited. See the Editorial Policy for how sourcing, AI use and corrections work.