Skip to content

Repository files navigation

The Cost of a Convention

Accounting data is conventionally lagged six months before use. Across 52 markets that single constant is far too cautious where the data are good and not cautious enough at the slow end. Separately, on United States returns, ignoring availability dates altogether inflates the measured long-short spread by about 1.6 percentage points a year outside micro-caps, and by up to 4.1 on the full universe, on two of the three signals tested. On the third it reverses, by more, equal-weighted.

Jarod van Eijsden · WRDS: Compustat North America, Compustat Global, CRSP, CRSP-Compustat Merged, Fama-French · US equities 1995–2024, availability 2015–2024

📄 Read the paper · 📊 Interactive exhibit (download and open locally, or view it on GitHub Pages if enabled) · 🔧 Process record and defect log

Median days to first availability by market, against the 182-day convention


Results

Question Answer
How much does reporting speed vary? Median 20 days (Oman) to 116 (Bangladesh). US 41, Japan 42, UK 79, Germany 86, France 91, Greece 100. 52 markets, 729,924 firm-halves. Outside Compustat North America the measure is first appearance in the vendor database, not publication.
Is the six-month lag safe? In the US, yes — 98.5% of firm-halves at the reported window, 98.1–99.0% across the four tested, and the US median is 41 days at every one. At the slow end it leaves 11.7–17.4% unavailable, so applying it there assumes an availability that had not yet occurred in the vendor database — an upper bound on look-ahead, since ingestion may lag publication. How many markets qualify depends on how database load stamps are excluded (2 / 7 / 15 / 22 at 300 / 400 / 500 / 730 days); which markets lead does not.
Is the return panel trustworthy? Reconciles to the Fama-French market return at 1.4 bp/month, ρ = 0.999987 over 419 months.
What does look-ahead cost? +3.21pp/yr gross profitability, +4.13pp book-to-market (equal-weighted, t = 9.32 and 6.40) — +1.65pp and +1.58pp excluding names below the NYSE 20th percentile, still significant at 1%. Four of six contrasts clear 1% on the full universe, five clear 5%. Four of the six tell the same story in both halves of the sample; value-weighted gross profitability is entirely pre-2010, and value-weighted book-to-market loses significance in the second half.
What does the convention itself cost? Nothing detectable at 1% — five of six contrasts insignificant there, four of six at 5%. Expensive in days, cheap in returns.
How much history is backfilled? Median firm arrives with 1.93 years pre-loaded; 16.2% of firm-quarters predate their firm's entry date (an approximation with biases both ways, not a bound). Worth 0.9–1.8pp/yr on three of the four series and −0.5 on the fourth, on the paired estimator — but only 1 of 4 seasoning contrasts clears |t| = 2, and the layer covers gross profitability and book-to-market only. The weakest of the three results, and labelled as such.
Anything unexplained? Yes. Asset growth reverses: −7.81pp/yr, t = −8.07 — concentrated in small firms, −10.90pp in the smallest NYSE-size tercile against −0.66pp and −0.76pp in the mid and large, neither significant. The explanation that would have killed it — honest timing holding 21.0% fourth-quarter data against the naive rule's 26.8% — was decomposed and bounded far below what it would need to be: composition is −0.13pp of −8.59pp, under 2%, and equal-weighted the reversal appears inside every fiscal quarter, significantly at 5% in all four. Reported open.

Running it

Requires a WRDS account with Compustat, CRSP and the Fama-French library. No vendor data is committed — redistributing it would breach the WRDS terms of use, and the panel alone is 44 MB. Everything regenerates from the scripts.

pip install -r requirements.txt
cd src

python fetch_wrds.py    --probe --user USERNAME   # what the subscription can read
python fetch_wrds.py    --user USERNAME --out .   # availability extracts
python coverage_check.py phase0_na.csv.gz phase0_global.csv.gz --out phase0_out
python diagnose.py       phase0_na.csv.gz phase0_global.csv.gz --out phase0_out
python country_lag.py    phase0_na.csv.gz phase0_global.csv.gz --out phase0_out
python country_lag.py    phase0_na.csv.gz phase0_global.csv.gz --out phase0_out --window-scan
python build_chart.py    phase0_out/country_lag_q2q4.csv --out ../reporting_lag_by_country.html
python phase1_link.py   --user USERNAME           # CCM link, coverage funnel
python phase1_panel.py  --user USERNAME           # panel + Fama-French reconciliation
python phase2_l1.py     --user USERNAME           # timing decomposition
python phase3_l3.py     --user USERNAME           # backfill layer + paired inference
python phase4_quarter.py --user USERNAME          # timing vs fiscal-quarter composition
python robustness.py    --dir phase2_out          # inference on the differences

# The scripts write to phase*_out/, which is git-ignored. results/ is the
# committed copy and holds only the aggregated tables the paper cites. It
# deliberately excludes the twenty-four monthly long-short return series, which
# the paper does not tabulate, and database_entry.csv, which is one row per firm.
$keep = "country_lag_q2q4","country_lag_window","l1_alphas","l1_decomposition",
        "lag_sensitivity","paired_differences","raw_vs_adjusted","subperiod_split",
        "l3_seasoning","l3_decomposition","quarter_decomposition","quarter_strata",
        "link_funnel","reconciliation_stats","study_universe_by_year","size_split",
        "manifest_country","manifest_phase1link","manifest_phase1panel",
        "manifest_phase2","manifest_phase3","manifest_phase4","manifest_robustness"
$keep | ForEach-Object { Copy-Item "phase*_out\$_.csv" ..\results }

# on macOS or Linux the same step is
#   for f in "${keep[@]}"; do cp phase*_out/$f.csv ../results/; done
# with keep as a bash array of the same names
python verify_paper.py  --paper ../PAPER.md --results ../results
python check_repo.py    --root .. --src . --results ../results
python audit_census.py  --paper ../PAPER.md --results ../results
python mutation_test.py

Twelve of the fifteen scripts accept --selftest and run offline, with no database connection and no vendor data; build_chart.py, which renders the exhibit, is one of the three that do not. Two scripts need nothing but results/: verify_paper.py checks every tabulated figure in the paper against it, and build_chart.py rebuilds the interactive exhibit from results/country_lag_q2q4.csv byte-for-byte. country_lag.py, coverage_check.py, diagnose.py and robustness.py also need no database connection, but they read WRDS-derived files that are deliberately not committed, so they run only after the earlier phases have.

Requires Python 3.10 or later. Only the six scripts that query WRDS need the wrds package; the checkers need pandas alone.

How the code defends itself

Twelve of the fifteen scripts attempt to falsify their own result before reporting it.

  • phase1_panel.py reconciles against a published series, with thresholds fixed in the file before running (ρ ≥ 0.999, MAD ≤ 10 bp/month). It exits non-zero on failure. If the panel cannot reproduce Fama-French, nothing built on it is worth reading.
  • phase2_l1.py plants a look-ahead and requires the harness to catch it. A signal equal to next month's return must price at a large alpha and a noise control at none: t = 182 on the plant, t = 0.3 on the control. It also verifies that Newey-West reduces to White's robust errors at zero lags.
  • It also plants a portfolio that is the market factor and requires it to price at zero alpha. A risk model whose factor months are misaligned with the return months by one period explains nothing and reports the raw mean as an alpha. That defect shipped here once and no other test could see it, because every other test asks about the level of a return rather than about whether the factors line up with it.
  • phase3_l3.py plants a backfill effect — an alpha given only to database-young firms — and requires seasoning to remove it.
  • robustness.py verifies its own estimator recovers a planted difference that a single-leg test cannot detect, and does not reject a null pair.
  • phase4_quarter.py plants a pure composition effect and a pure timing effect and requires the timing plant to land in timing exactly and the composition plant in composition to within 15%, with the two components adding back to the total to machine precision. It also pins the split to the symmetric form, so an asymmetric variant fails.
  • fetch_wrds.py builds its SQL against a fake catalogue and must degrade cleanly when rdq is absent, which is the Compustat Global case.
  • Universe identity is asserted, not assumed. phase2_l1.py reports cross-section size per rule and warns if they diverge.
  • country_lag.py --compare diffs its output against the published table and exits non-zero if any median or firm count has moved.
  • verify_paper.py re-derives every tabulated figure in the paper from results/ — bar two tables it names as uncommitted — and exits non-zero on any mismatch. 532 checks in all: cell-by-cell comparisons at the precision the paper displays, the prose that restates those figures, the counts the paper states in words — "five of six are insignificant at the 1% level", "three of the four series decline monotonically", "seven of its eight weight-rule cells" — each recomputed from results/ rather than read, both Bonferroni bars recomputed from the family sizes rather than parsed out of the sentence, the size table's bolding against the 1% threshold, the multiple-testing count, §2's row-count reconciliation, and §3's market ordering at all four window bounds. Run from the repository root, its own self-test plants a wrong digit and requires the check to catch it. The paper is not a claim about the data; it is checkable against it in one command.
  • audit_census.py enumerates every number in the paper and refuses any that nothing verifies. The other checkers answer "do the figures match their sources?"; only this one answers "is there a figure here that nothing checks?". 1,239 numeric claims: 700 checked against their own source cell, 83 traceable to a value in results/, 456 classified as a threshold, a citation, a section reference, a cardinality of the study counted from results/, or a statistic of the licensed extracts that the repository discloses rather than ships — and zero unaccounted for. It exits non-zero if that last number is not zero, so a new unverifiable figure cannot enter the paper silently. Numbers spelled as words are counted too — 273 of the 1,239 — and a spelled count is credited only where a check parsed that exact position, never because its value happens to match some other figure; that loophole let a planted "nineteen review passes" through every check this repository had. audit_census.py --mutate-counts changes each of the 134 checked word-counts in turn and requires a check to fail on it; all 134 do. Eight review passes each read the paper looking for errors; none of them enumerated the claims, which is why each pass kept finding more.
  • check_repo.py checks the repository's claims about itself — 497 of them: that every file the README links exists, that the script counts are right, that every section cross-reference resolves, that the bibliography closes in both directions, that the interactive exhibit matches its source table cell by cell, and that no retired phrasing has survived in it.
  • Phase 0, Phase 1, Phases 2 to 4 and robustness.py stamp the SHA-256 of the code that produced their tables into the seven results/manifest_*.csv, and check_repo.py compares those hashes against src/. A table published from a script that has since been edited is then a failing check rather than something to remember. This is the one defect class the other checkers cannot see on their own: stale results beside edited code are internally consistent everywhere you look.
  • mutation_test.py damages the code on purpose and requires the self-test to notice. Forty-five mutations across ten of the fifteen files in src/ — invert the industry screen, delete a term from the forward-return alignment, zero the Newey-West lag weights, make value weighting equal weighting — and all forty-five are caught. A self-test that passes is weak evidence; one that fails when you damage what it guards is not. Three of the defects found in review of this project were self-tests that could not fail, which is why this exists.

Repository

PAPER.md                       the study
APPENDIX_process.md            development history, defect log, internal review,
                               and the disposition of every review finding
reporting_lag_by_country.html  interactive exhibit (tooltips, table, dark mode)
reporting_lag_by_country.png   the same exhibit, rendered to an image so it
                               shows in this README; the HTML is the source and
                               build_chart.py regenerates it from results/
src/                           fifteen scripts. Twelve self-test;
                               build_chart.py renders the exhibit. Four of the
                               fifteen — verify_paper.py, check_repo.py,
                               audit_census.py, mutation_test.py — verify the
                               work rather than produce it; see below.
results/                       the aggregated tables behind every tabulated figure
                               in the paper bar two, plus seven manifest_*.csv
                               giving the SHA-256 of the code behind the committed
                               tables. No vendor records, so a reader can check the
                               numbers without a WRDS licence
requirements.txt               pinned; pandas <2.3 because the wrds client requires it
gitignore-RENAME-ME.txt        the ignore file, under a name Windows and the
                               GitHub web uploader will not hide. Rename it to
                               .gitignore on arrival — until you do, nothing
                               stops a WRDS extract being committed
LICENSE                        MIT for the code and the writing; the data is
                               licensed from WRDS and is not redistributed

Scope

US common equity on NYSE, AMEX and Nasdaq, share codes 10 and 11, linked through CRSP-Compustat Merged, with delisting returns applied. Financials (SIC 6000–6999) are excluded from the portfolios on CRSP's point-in-time siccd, not on the firm's current classification; the screen is applied downstream of the Fama-French reconciliation, which needs them. Returns end December 2024 on this CRSP subscription; availability data extends to July 2026. Country is place of incorporation; five incorporation domiciles are excluded from the market comparison and retained, flagged, in the data table. Limitations are collected in §7 of the paper; a few that attach to one estimate are stated where that estimate is reported, in §4.4 and §5.

About

Data availability and measured alpha in cross-sectional equity signals. 53 markets, 1995-2024, WRDS.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages