Quant500 Academy

2. Prices: consolidated, delayed and official close

Level 1: Where they come from and what they carry inside

When you write datos["Close"] you think you are asking for "the price". There is no such thing. There are dozens of defensible numbers for the same stock at the same instant, and the column that reaches you is a decision somebody made on your behalf. This module is about knowing which one you got.

A stock trades in many places at once

In the United States there are more than a dozen registered exchanges and dozens of alternative venues, on top of the enormous volume executed off-exchange through wholesalers and internalisers. No single exchange holds the majority of the volume in a large stock, and the share executed away from the public markets has run, in recent years, at something like half of the total.

Out of that come two different products:

Last trade versus midpoint

The last trade is a fact: somebody bought and somebody sold at that price. But it is a contaminated fact, because a trade executes against the bid or against the offer, not in the middle. With a wide spread, the last trade bounces between the two extremes without the stock having moved at all: this is bid-ask bounce, and in illiquid names it manufactures measured volatility that does not exist.

The midpoint of the best bid and the best offer is not a price anyone traded at, but it is usually a better estimate of the consensus value at that instant.

import pandas as pd

libro = pd.DataFrame({
    "hora": pd.to_datetime(["2026-03-16 21:59:58", "2026-03-16 21:59:59"]),
    "bid": [101.10, 101.12],
    "ask": [101.30, 101.34],
    "ultimo": [101.11, 101.30],   # an aggressive sell and then an aggressive buy
})

libro["medio"] = (libro["bid"] + libro["ask"]) / 2
libro["horquilla_pb"] = (libro["ask"] - libro["bid"]) / libro["medio"] * 10000
libro["sesgo_ultimo_pb"] = (libro["ultimo"] - libro["medio"]) / libro["medio"] * 10000
print(libro[["hora", "medio", "horquilla_pb", "sesgo_ultimo_pb"]])

In one second, the midpoint has moved barely three cents and the last trade has jumped nineteen. If your price series is a sequence of last trades in a wide-spread name, you are measuring the sign of the orders, not the price.

The official close is not the last trade of the day

Here is the most expensive misunderstanding in the module. On modern exchanges, the close is formed in an auction: for a few minutes orders pile up without being crossed and, at the end, an algorithm computes the single price that maximizes the volume traded. That auction price is the official close, and it is the one used by indices, index funds and portfolio valuations.

Consequences worth internalizing:

The same happens at the open: there is an opening auction, and the "Open" you see may be that auction price or the first trade your vendor happened to see. They are not the same thing.

Why two serious vendors disagree

Before accusing anyone of having bad data, here is the list of legitimate reasons why two closes differ for the same stock on the same day:

The grown-up way to handle this is not to pick a vendor and believe them: it is to cross-check them and monitor the difference.

a = pd.read_csv("cierres_proveedor_a.csv", parse_dates=["fecha"])
b = pd.read_csv("cierres_proveedor_b.csv", parse_dates=["fecha"])

comp = a.merge(b, on=["fecha", "ticker"], suffixes=("_a", "_b"))
comp["dif_pb"] = (comp["cierre_a"] / comp["cierre_b"] - 1) * 10000

print(comp["dif_pb"].abs().describe())

# Pragmatic threshold: under 25 bp it is usually definition; above that, go and investigate.
sospechosos = comp.loc[comp["dif_pb"].abs() > 25, ["fecha", "ticker", "cierre_a", "cierre_b", "dif_pb"]]
print(sospechosos.sort_values("dif_pb", key=abs, ascending=False).head(20))

If the discrepancies cluster on a handful of dates, suspect a market event or an adjustment applied by only one of the two. If they are spread evenly across every day and every stock, what you have is a difference of definition, and you have to choose which one you want.

A real case: August 24, 2015

That morning US futures opened sharply lower and the cash open descended into disorder. More than a thousand volatility trading halts were recorded that day, spread across several hundred stocks and exchange-traded funds. Many ETFs traded far below the value of the assets they held, with intraday falls that in some cases exceeded 30% while their underlying portfolios fell a fraction of that.

The lesson for anyone building data: during those minutes, "the price" of a single fund was radically different depending on whether you looked at the last trade, the midpoint of an enormous spread, the consolidated tape or a single venue. A backtest that assumes it could have executed at those prices is fantasizing. 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.