3. Fundamentals: when they were really published
Level 1: Where they come from and what they carry inside
With prices, the problem is which number they give you. With fundamentals, the problem is when. Every accounting figure carries two different dates, and confusing them is the mistake that most often turns a mediocre strategy into an impossible marvel.
The two dates on any fundamental
- Period date. The day the figure refers to: December 31, 2019, June 30, 2024. It is the accounting label.
- Publication date. The day that figure became known to the public, because the company announced it or filed it with the regulator.
Weeks or months pass between the two. And here is the detail that almost every cheap source hides from you: the CSV usually carries only the first one. You get a table indexed by quarter-end and you are left to believe the data already existed on that day. It did not. On December 31 nobody yet knows fourth-quarter earnings, not even the company itself.
How long it really takes: the US regulator's deadlines
The SEC sets maximum deadlines by issuer size, and they serve as a reference for the typical lag:
- Quarterly report (10-Q): 40 calendar days after quarter-end for the largest filers, and 45 days for the small ones.
- Annual report (10-K): 60 days for the large ones, 75 for the intermediate category and 90 for the small ones.
There is also a nuance that trips a lot of people up: the earnings press release usually comes out before the full report, announced to the market in an 8-K. In other words, there are two distinct moments at which the market learns different things about the same quarter. The audited, detailed figure arrives later.
Translated into round numbers: for an annual report, two or three months can pass between the end of the financial year and its full filing. If your backtest assumes that on January 1 you could already screen on the earnings of the year that had just ended, you have moved the clock forward by an entire quarter.
The first thing, then, is to check whether your source even carries the second date, and what the lag looks like:
import pandas as pd
fund = pd.read_csv("fundamentales.csv", parse_dates=["fin_periodo", "fecha_publicacion"])
print("Filas sin fecha de publicacion:", fund["fecha_publicacion"].isna().sum())
fund["desfase_dias"] = (fund["fecha_publicacion"] - fund["fin_periodo"]).dt.days
print(fund["desfase_dias"].describe())
# Anomalies: publications before the period ends, or absurd lags.
raras = fund[(fund["desfase_dias"] < 0) | (fund["desfase_dias"] > 200)]
print(raras[["cik", "fin_periodo", "fecha_publicacion", "desfase_dias"]].head())If the publication column does not exist, or if describe hands you a median of zero days, your source is unusable for anything that depends on the calendar: it is giving you accounting as though it had been instantaneous. Working through exactly what that does to a backtest is the business of the next level; here it is enough to know that it is a property of the source, not a minor detail.
The 2019 figure you see today is not the 2019 figure
The second half of the problem is that figures change after being published, and databases tend to overwrite them without leaving a trace:
- Ordinary revisions. A preliminary figure from the press release is refined in the full report. Small changes, very frequent.
- Restatements. The company acknowledges that the published accounts contained material errors and republishes the statements for earlier periods. In the United States, a serious correction obliges the company to warn that the earlier statements should no longer be relied upon.
- Recasts for a change of accounting standard or of perimeter. When a new standard comes in or a division is sold, prior years are restated so that they remain comparable. Nobody lied, but the number in the table has changed.
A point-in-time database keeps every version and lets you ask "what was known on March 3, 2020". A normal database keeps only the latest. If all you have is snapshots, at least keep them and compare them:
viejo = pd.read_parquet("fundamentales_2021_06.parquet")
nuevo = pd.read_parquet("fundamentales_2026_06.parquet")
clave = ["cik", "fin_periodo", "concepto"]
comp = viejo.merge(nuevo, on=clave, suffixes=("_viejo", "_nuevo"))
comp["cambio_pct"] = (comp["valor_nuevo"] / comp["valor_viejo"] - 1) * 100
revisados = comp[comp["cambio_pct"].abs() > 0.5]
print("Puntos revisados:", len(revisados), "de", len(comp))
print(revisados.groupby("concepto")["cambio_pct"].agg(["count", "median"]).sort_values("count", ascending=False).head(10))The result is usually a surprise: flow items and lines that involve estimates (provisions, impairments, deferred taxes) get revised far more than sales do. If your signal leans on a highly revisable line, a good part of what you are measuring in the past is information that was not available in that form at the time.
Three practical rules follow from all of the above:
- Keep dated snapshots. Even if your vendor has no version history, you can build one: download the full panel every quarter and archive it with the download date in the filename. In two years you will have something that today does not come cheap.
- Treat the publication date as a mandatory column. If a source does not carry it, or carries it empty on part of the rows, that part of the panel is unusable for anything that depends on the calendar, however pretty the figures are.
- Be suspicious of lags that are too tidy. An identical lag for every issuer, exactly 90 days say, almost always means somebody estimated it by adding a fixed number instead of reading the real date off the filing.
A real case: Wirecard, June 2020
Wirecard had at one point been a member of Germany's large-cap index. In June 2020 its auditors refused to sign off the 2019 accounts because they could not locate some €1.9 billion sitting in trustee accounts. The company ended up admitting that the balance probably did not exist and withdrawing the validity of financial statements for earlier years. Days later it filed for insolvency.
Think about what that means for a database. For years, any quantitative screen looking at Wirecard's cash read a figure that was published, audited and perfectly formatted. Today that row may show up corrected, empty or flagged; and a download made in 2019 and one made now do not agree. Neither of them is wrong: they are two different moments of the same fact. Recording when you knew each thing is not bureaucracy, it is what makes the past reconstructible. Education, not financial advice.