Quant500 Academy

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

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:

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:

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:

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.

The rest of Quant500

What you are learning, applied to the index every day:

The model portfolio, weekly in your inbox

Every Saturday, its week in one minute: how it went, the best and the worst, and which insiders bought with their own money. Plus, the day the model is rebuilt, which stocks come in and which go out. A simulation, not advice. At most one email a week, no ads, no account; one-click unsubscribe. Made by one person: Pol Gracia.

A model portfolio with simulated results, not a personalized recommendation. Your email is used for this only: privacy.

Behind the Academy and the rest of Quant500 there is one person: Pol Gracia Serrahima.