Lesson 1.1.2 · 55 min

Sessions, auctions, holidays and calendars

A market opens and closes at times that change with the time zone, daylight saving time, holidays and half-day sessions. This lesson computes the sessions of six exchanges with exchange_calendars, rebuilds an opening auction order by order and measures why the close concentrates so much volume.

Fig. — Sessions of six exchanges on 2 June 2025 in UTC, with the Tokyo and Hong Kong lunch breaks
02/05
Module
1.1 · How markets work
Duration
55 min
Updated
Level 1 lessons
  1. 1.1 · How markets work

    1. 1.1.1 Market participants and trading venues
    2. 1.1.2 Sessions, auctions, holidays and calendars
    3. 1.1.3 Order book: bid, ask, spread and depth
    4. 1.1.4 Order types
    5. 1.1.5 Costs, settlement, leverage and margin
  2. 1.2 · Asset classes

    1. 1.2.1 Stocks: splits, dividends and corporate actions
    2. 1.2.2 ETFs: replication, tracking error, leveraged ETFs and UCITS
    3. 1.2.3 Futures: specifications, margin, roll and contango
    4. 1.2.4 Forex: pairs, pips, lots, rollover and CFDs
    5. 1.2.5 Options: calls, puts, intrinsic value and time value
    6. 1.2.6 Crypto: spot, perpetuals, funding and custody
    7. 1.2.7 Fixed income and interest rates; CFDs and leveraged products
  3. 1.3 · Market data sources

    1. 1.3.1 Types of market data: EOD, bars, ticks, order book and reference
    2. 1.3.2 Free market data sources
    3. 1.3.3 Retail and prosumer market data APIs
    4. 1.3.4 Professional market data providers
    5. 1.3.5 Crypto and on-chain data
    6. 1.3.6 Brokers as market data sources
    7. 1.3.7 Market data licensing and terms of use
    8. 1.3.8 How to choose a market data provider
  4. 1.4 · Financial data engineering

    1. 1.4.1 Downloading over REST and websockets: pagination, rate limits and retries
    2. 1.4.2 Data cleaning: missing values, bad ticks, gaps and time zones
    3. 1.4.3 Adjusting for splits and dividends: adjusted and unadjusted prices
    4. 1.4.4 Universes without survivorship bias: delistings, mergers and symbol changes
    5. 1.4.5 Building bars: time, tick, volume, dollar and imbalance
    6. 1.4.6 Continuous futures series: roll rules, difference adjustment and ratio adjustment
    7. 1.4.7 Storage: CSV, Parquet, partitioning, DuckDB, TimescaleDB and ArcticDB
    8. 1.4.8 Reproducible pipeline: download, validation, incremental updates and quality tests
  5. 1.5 · Fundamental and macro data

    1. 1.5.1 Financial statements, ratios, XBRL and SEC EDGAR
    2. 1.5.2 Macro data, economic calendars, estimates and transcripts
  6. 1.6 · Financial exploratory analysis

    1. 1.6.1 Returns, volatility, rolling correlations and drawdown
    2. 1.6.2 Stylized facts: fat tails, clustering, autocorrelation and leverage
    3. 1.6.3 Seasonality: day of the week, month, turn of the month and intraday sessions
  7. 1.7 · Programmed technical analysis

    1. 1.7.1 Technical indicators in Python
    2. 1.7.2 Your own implementation versus TA-Lib: validating results
    3. 1.7.3 Candlestick patterns and support and resistance
    4. 1.7.4 Multi-timeframe analysis without look-ahead

The phases of a session

On a stock exchange, trading does not work the same way at all hours. The session is divided into phases and each one has its own rules for matching orders. The two basic forms are continuous trading, in which an incoming order is executed immediately against those already waiting in the book (lesson 1.1.3), and the auction, in which orders accumulate for a while without being executed and at the end are all matched at a single price.

opening auctionprice out of rangeend of auctionclosing timeclosing pricePreOpenContinuousVolatilityClosePostClose
Mermaid source
stateDiagram-v2
  [*] --> PreOpen
  PreOpen --> Continuous: opening auction
  Continuous --> Volatility: price out of range
  Volatility --> Continuous: end of auction
  Continuous --> Close: closing time
  Close --> PostClose: closing price
  PostClose --> [*]
  • Pre-open and opening auction. Orders can be sent, modified and cancelled, but there are no trades. The system publishes a theoretical equilibrium price that moves with the orders. At the end, all compatible orders are matched at the same price: the opening price.
  • Continuous trading. The book is open: each aggressive order is executed against the best opposite order.
  • Volatility auction. If a trade were about to happen too far from the last price or from the reference price, the market suspends continuous trading in that security and opens a short auction. It keeps a piece of news or an error from turning into a cascade of trades at absurd prices. The thresholds and the duration are set by each exchange in its rulebook, and in a backtest they show up as gaps of a few minutes with no trades.
  • Closing auction. Like the opening one, at the end of the day. It sets the official closing price.
  • Post-close. On some exchanges, a period in which you can only trade at the closing price.

Two exchanges often used in the course, with their official schedules in local time:

PhaseNYSE (New York)BME, continuous market (Madrid)
Order entryfrom 6:30—
Opening auctionmatch at 9:30from 8:30 to 9:00
Continuous tradingfrom 9:30 to 16:00from 9:00 to 17:30
Closing imbalance periodfrom 15:50 to 16:00—
Closing auctionmatch at 16:00from 17:30 to 17:35
Trading at the closing price—Trading At Last, from 17:35 to 17:45

On the NYSE, on-close orders (MOC and LOC, lesson 1.1.4) can be sent, modified or cancelled until 15:50. From then on the exchange publishes the imbalance between buys and sells at the close and only accepts orders that reduce it1Note 1 · NYSE hours on its “Holidays & Trading Hours” page and in the “NYSE opening and closing auctions” fact sheet; BME hours on BME Exchange's “Horario Bolsa Española” (Spanish stock exchange hours) page (accessed on October 21, 2026).. On BME, Trading At Last lets you keep buying and selling for ten more minutes, but only at the price the closing auction has just set.

Exchange calendars in Python

Writing the hours by hand (“the NYSE opens at 15:30 Madrid time”) works until the first clock change. An exchange calendar knows, for each date, whether there is a session, when it opens and closes in UTC, whether there is a midday break and whether it is a half-day session. The two reference libraries in Python are exchange_calendars (ISO 10383 market codes, such as XNYS; 71 calendars not counting aliases in version 4.13.2) and pandas_market_calendars, which reuses those calendars and adds many more, especially for futures (211 names, aliases included, in version 5.5.0). Both are installed in the course environment.

IN [01]python
import exchange_calendars as xcals
import pandas as pd

bolsas = {"XTKS": "Tokyo", "XHKG": "Hong Kong", "XLON": "London",
          "XETR": "Xetra", "XMAD": "Madrid", "XNYS": "New York"}
dia = pd.Timestamp("2025-06-02")
filas = {}
for codigo, nombre in bolsas.items():
    cal = xcals.get_calendar(codigo)
    filas[nombre] = {
        "abre": cal.session_open(dia),
        "pausa": cal.session_break_start(dia),
        "reanuda": cal.session_break_end(dia),
        "cierra": cal.session_close(dia),
    }
sesiones = pd.DataFrame(filas).T
hhmm = lambda c: pd.to_datetime(c).dt.strftime("%H:%M")
sesiones.apply(hhmm).fillna("")
OUT [01]Sessions of six exchanges on June 2, 2025, in UTC
            abre  pausa reanuda cierra
Tokyo      00:00  02:30   03:30  06:30
Hong Kong  01:30  04:00   05:00  08:00
London     07:00                 15:30
Xetra      07:00                 15:30
Madrid     07:00                 15:30
New York   13:30                 20:00

All times come out in UTC, which is how they should be stored (lesson 0.4.4). Tokyo and Hong Kong have a midday break: an hour without continuous trading for which the calendar returns pausa (break start) and reanuda (break end). On exchanges without a break those columns stay empty. In local time things are easier to recognize:

IN [02]python
for codigo, nombre in bolsas.items():
    cal = xcals.get_calendar(codigo)
    abre = cal.session_open(dia).tz_convert(cal.tz)
    cierra = cal.session_close(dia).tz_convert(cal.tz)
    print(f"{nombre:<11} {abre:%H:%M}-{cierra:%H:%M}  {cal.tz}")
OUT [02]The same sessions in local time
Tokyo       09:00-15:30  Asia/Tokyo
Hong Kong   09:30-16:00  Asia/Hong_Kong
London      08:00-16:30  Europe/London
Xetra       09:00-17:30  Europe/Berlin
Madrid      09:00-17:30  Europe/Madrid
New York    09:30-16:00  America/New_York

The figure at the top draws the six sessions on the same UTC axis. Three things are visible at a glance. Tokyo has closed by the time Europe opens, and Hong Kong only overlaps with it for one hour. London, Xetra and Madrid open and close at the same UTC time that day. And between Europe and New York there are only two hours of overlap, from 13:30 to 15:30 UTC: the only window in which a strategy can buy in Madrid and sell in New York with both books open.

That overlap is not fixed. The United States and Europe change their clocks on different dates, so for a few weeks a year the difference between Madrid and New York is five hours instead of six (the figure in lesson 0.4.4 shows it with the 2026 opens). The calendar handles it without you having to think about it:

IN [03]python
mad = xcals.get_calendar("XMAD")
nys = xcals.get_calendar("XNYS")
rango = ("2025-01-02", "2025-12-31")
comunes = mad.sessions_in_range(*rango).intersection(
    nys.sessions_in_range(*rango))
solape = mad.closes[comunes] - nys.opens[comunes]
(solape.dt.total_seconds() / 3600).value_counts().sort_index()
OUT [03]Madrid-New York overlap hours in each 2025 session
-1.5      2
 2.0    225
 3.0     20
Name: count, dtype: int64

Of the 247 sessions shared in 2025, 225 have two hours of overlap, 20 have three (the weeks of daylight saving time mismatch, in March and at the end of October) and 2 have a negative value: they are Madrid's half-day sessions of December 24 and 31, in which the continuous market closes at 14:00, an hour and a half before the NYSE opens. A system that assumes “always two hours of overlap” is wrong on 22 sessions a year.

Holidays, unscheduled closures and half-day sessions

The holidays of each exchange match neither those of its country nor those of other exchanges, and they change every year: a holiday that falls on a Saturday may move to the Friday, or not. There are also closures nobody foresaw: the NYSE closed on January 9, 2025 for the national day of mourning for former President Carter, announced on December 302Note 2 · 2025 calendar: NYSE Group press release “NYSE Group Announces 2025, 2026 and 2027 Holiday and Early Closings Calendar”; January 9 closure, ICE press release of December 30, 2024. 2026 calendar: the NYSE “Holidays & Trading Hours” page.. And there are half-day sessions: on the NYSE, July 3, the Friday after Thanksgiving and December 24 usually close at 13:00.

IN [04]python
nyse = xcals.get_calendar("XNYS")

def cerrados(cal, desde, hasta):
    laborables = pd.bdate_range(desde, hasta)
    return laborables.difference(cal.sessions)

for anio in ("2025", "2026"):
    dias = cerrados(nyse, f"{anio}-01-01", f"{anio}-12-31")
    print(anio, len(dias), "days:", ", ".join(dias.strftime("%d/%m")))
OUT [04]Weekdays without a session on the NYSE
2025 11 days: 01/01, 09/01, 20/01, 17/02, 18/04, 26/05, 19/06, 04/07, 01/09, 27/11, 25/12
2026 10 days: 01/01, 19/01, 16/02, 03/04, 25/05, 19/06, 03/07, 07/09, 26/11, 25/12

Compared with what the NYSE publishes, the calendar is right. For 2026 it can be checked against the list on its holidays page, copied by hand:

IN [05]python
nyse_com = pd.to_datetime([
    "2026-01-01", "2026-01-19", "2026-02-16", "2026-04-03",
    "2026-05-25", "2026-06-19", "2026-07-03", "2026-09-07",
    "2026-11-26", "2026-12-25"])
medias_nyse_com = pd.to_datetime(["2026-11-27", "2026-12-24"])

libreria = cerrados(nyse, "2026-01-01", "2026-12-31")
medias = nyse.early_closes[nyse.early_closes.year == 2026]
print("Same holidays:", libreria.equals(pd.DatetimeIndex(nyse_com)))
print("Same half-day sessions:", medias.equals(medias_nyse_com))
nyse.closes[medias].dt.tz_convert("America/New_York").dt.strftime(
    "%Y-%m-%d %H:%M")
OUT [05]Check against nyse.com: 2026 holidays and half-day sessions
Same holidays: True
Same half-day sessions: True
2026-11-27    2026-11-27 13:00
2026-12-24    2026-12-24 13:00
Name: close, dtype: str

In 2026 July 4 falls on a Saturday and the holiday moves to Friday the 3rd, so that year there is no half-day session the day before the holiday. It is the kind of detail a hand-made calendar misses. The calendar is not eternal either: it is updated with each version of the library. An unscheduled closure like that of January 9, 2025 only appears in versions released after the announcement, so it is worth pinning the version in the project (lesson 0.2.2) and upgrading it on purpose.

Schedule changes are recorded too. The Tokyo Stock Exchange extended its afternoon session by half an hour on November 5, 2024, with a new closing auction3Note 3 · JPX, “Cash Market Trading Hours Extension Kicked Off Today”, November 5, 2024. The current schedule, from 9:00 to 11:30 and from 12:30 to 15:30, is on JPX's “Trading Hours” page.:

IN [06]python
tks = xcals.get_calendar("XTKS")
for d in ("2024-11-01", "2024-11-05"):
    cierre = tks.session_close(d).tz_convert("Asia/Tokyo")
    print(d, "closes at", cierre.strftime("%H:%M"))
OUT [06]The Tokyo close before and after November 5, 2024
2024-11-01 closes at 15:00
2024-11-05 closes at 15:30

pandas_market_calendars offers the same with another interface: a schedule with one row per session. It is handy for generating bar grids, and it ships separate calendars for each group of futures (CME_Equity, CME_Rate…), where exchange_calendars has one generic calendar per exchange (CMES):

IN [07]python
import pandas_market_calendars as mcal

nyse_m = mcal.get_calendar("NYSE")
nyse_m.schedule("2025-11-26", "2025-12-01")
OUT [07]pandas_market_calendars: Thanksgiving 2025
                         market_open              market_close
2025-11-26 2025-11-26 14:30:00+00:00 2025-11-26 21:00:00+00:00
2025-11-28 2025-11-28 14:30:00+00:00 2025-11-28 18:00:00+00:00
2025-12-01 2025-12-01 14:30:00+00:00 2025-12-01 21:00:00+00:00

Thursday the 27th does not appear and Friday the 28th closes at 18:00 UTC, 13:00 New York time. The two libraries should give the same sessions; checking it is cheap and catches outdated versions:

IN [08]python
sched = nyse_m.schedule("2025-01-02", "2025-12-31")
x_cierres = nyse.closes["2025-01-02":"2025-12-31"]
print(len(sched), "sessions in 2025")
print("Same closes:", (sched["market_close"] == x_cierres).all())
OUT [08]The two libraries agree in 2025
250 sessions in 2025
Same closes: True

The opening auction, order by order

In an auction there is no aggressor and no book to sweep: there is a list of orders and a single price for all of them. The general rule is to maximize executable volume. For each candidate price pp:

D(p)=∑buys with limit≥pqi,S(p)=∑sells with limit≤pqj,V(p)=min⁡(D(p),S(p))D(p) = \sum_{\text{buys with limit} \ge p} q_i, \qquad S(p) = \sum_{\text{sells with limit} \le p} q_j, \qquad V(p) = \min\big(D(p), S(p)\big)

Market orders count at any price: a market buy is willing to pay whatever it takes, and a market sell, to sell at whatever price. Demand D(p)D(p) falls as the price rises; supply S(p)S(p) rises. The auction price is the pp with the largest V(p)V(p), and the difference D(p)−S(p)D(p) - S(p) at that price is the imbalance: orders that remain unexecuted and move on to the continuous session. If several prices tie on volume, each exchange has its tie-breaking rules (smallest imbalance, closeness to the reference price…), published in its rulebook.

The file nova_subasta_apertura.csv is the book of NOVA's opening auction on June 2, 2025 at the moment of the match:

IN [09]python
import numpy as np

sub = pd.read_csv("nova_subasta_apertura.csv")
print(len(sub), "orders")
sub.groupby(["lado", "tipo"])["tamano"].agg(["size", "sum"])
OUT [09]The auction book
139 orders
                 size    sum
lado   tipo
compra limitada    63  27400
       mercado      8  17000
venta  limitada    61  30600
       mercado      7  14600
IN [10]python
compra = sub[sub["lado"] == "compra"]
venta = sub[sub["lado"] == "venta"]

def demanda(p):  # buys willing to pay p or more
    ok = compra["tipo"].eq("mercado") | compra["precio_limite"].ge(p)
    return compra.loc[ok, "tamano"].sum()

def oferta(p):   # sells willing to sell at p or less
    ok = venta["tipo"].eq("mercado") | venta["precio_limite"].le(p)
    return venta.loc[ok, "tamano"].sum()

curva = pd.DataFrame({"precio": np.sort(sub["precio_limite"].dropna()
                                        .unique())})
curva["demanda"] = curva["precio"].map(demanda)
curva["oferta"] = curva["precio"].map(oferta)
curva["ejecutable"] = curva[["demanda", "oferta"]].min(axis=1)
curva["deseq"] = curva["demanda"] - curva["oferta"]
mejor = curva["ejecutable"].idxmax()
curva.loc[mejor - 3:mejor + 3]
OUT [10]Supply and demand curves and equilibrium price
    precio  demanda  oferta  ejecutable  deseq
19   53.32    35200   30200       30200   5000
20   53.33    34200   32200       32200   2000
21   53.34    34200   33200       33200   1000
22   53.35    33900   34500       33900   -600
23   53.36    31400   34500       31400  -3100
24   53.37    29900   35800       29900  -5900
25   53.38    29900   36800       29900  -6900
Fig. — NOVA opening auction on 2 June 2025: cumulative demand and supply by price and the clearing price

The two curves cross between 53.34 and 53.35. At 53.34 there would be 34,200 shares of demand and 33,200 of supply: 33,200 would be executed. At 53.35 demand falls to 33,900 and supply rises to 34,500: 33,900 are executed, the maximum. At 53.36 demand drops to 31,400. The opening price is $53.35, with 33,900 shares and an imbalance of −600: 600 shares for sale are left over and move to the book of the continuous session. The final check is against the tape:

IN [11]python
op = pd.read_parquet("nova_operaciones.parquet")
apertura = op.iloc[0]
print(apertura[["ts", "precio", "tamano", "condicion"]].to_string())
print("Match:", apertura["precio"] == curva.loc[mejor, "precio"]
      and apertura["tamano"] == curva.loc[mejor, "ejecutable"])
OUT [11]The tape's open is the auction
ts           2025-06-02 13:30:00+00:00
precio                           53.35
tamano                           33900
condicion             subasta_apertura
Match: True

The first trade of the day is exactly the rebuilt auction: same price, same volume, at 13:30:00 UTC and with no aggressor. It is also the opening price that any daily data provider will give. The first trade of the continuous session, 0.3 seconds later, was already at 53.34 (lesson 1.1.1).

U-shaped volume and the closing auction

Volume is not spread evenly over the day. NOVA's 5-minute bars for 2024 and 2025 let you measure the typical shape: for each full session, what percentage of the day's volume falls in each slot, and then the average over all sessions.

IN [12]python
barras = pd.read_parquet("nova_barras_5min.parquet")
ny = barras["ts"].dt.tz_convert("America/New_York")
barras["sesion"] = ny.dt.date
barras["hora"] = ny.dt.strftime("%H:%M")

n_barras = barras.groupby("sesion")["ts"].size()
print(n_barras.value_counts().to_string())

normales = barras[barras["sesion"].map(n_barras) == 78].copy()
total = normales.groupby("sesion")["volumen"].transform("sum")
normales["cuota"] = 100 * normales["volumen"] / total
perfil = normales.groupby("hora")["cuota"].mean()
perfil.iloc[[0, 1, 2, 3, 38, 39, 74, 75, 76, 77]].round(2)
OUT [12]NOVA intraday volume profile, 2024-2025
ts
78    496
42      6
hora
09:30     3.06
09:35     2.43
09:40     2.08
09:45     1.79
12:40     1.04
12:45     1.04
15:40     1.40
15:45     1.56
15:50     1.82
15:55    11.28
Name: cuota, dtype: float64
Fig. — NOVA intraday volume profile: average share of daily volume in each 5-minute bar, 496 full sessions of 2024 and 2025

There are 496 full sessions of 78 bars and 6 half-day sessions of 42 (from 9:30 to 13:00), which are left out so as not to mix profiles. The first bar, from 9:30 to 9:35, gathers on average 3.06% of the day's volume; at midday each bar is around 1%; in the last half hour volume rises again, and the last bar, the 15:55 one, jumps to 11.28% because it includes the closing auction. It is the classic “U” of stocks, with the right leg much higher than the left.

Why the close? Because it is the price at which funds are valued. An index fund that has to buy the stocks entering its index, or that receives subscriptions, seeks to execute at exactly the closing price: that way it tracks the index without error. The way to do it is to send orders to the closing auction (MOC orders). Many asset managers measured against the close do the same. The June 2, 2025 session shows it with one figure:

IN [13]python
vol_dia = op["tamano"].sum()
por_tipo = op.groupby("condicion")["tamano"].sum()
(100 * por_tipo / vol_dia).round(2)
OUT [13]Weight of the auctions in the June 2, 2025 session
condicion
bloque               3.66
regular             86.00
subasta_apertura     0.38
subasta_cierre       9.96
Name: tamano, dtype: float64

Almost 10% of the day's volume is matched in a single instant, in the closing auction, and the opening one barely reaches 0.4%. For a trading system this has two sides. In favor: if the strategy trades at the close, the auction offers a great deal of liquidity at a price known in advance by everyone. Against: the price of the last continuous trade (15:59:59) is not the official close, and a backtest that “buys at the close” with the 15:55 bar has to decide whether it takes part in the auction (and then send the order before 15:50 on the NYSE) or in the preceding continuous session.

What this changes in your data

noyesnoyesyesnoRaw UTC dataIs there a session?Drop rowWithin hours?Pre or post-closeSession barHalf-day session?42 bars78 bars
Mermaid source
flowchart TB
  D[Raw UTC data] --> C{Is there a session?}
  C -- no --> X[Drop row]
  C -- yes --> H{Within hours?}
  H -- no --> E[Pre or post-close]
  H -- yes --> B[Session bar]
  B --> M{Half-day session?}
  M -- yes --> Q[42 bars]
  M -- no --> R[78 bars]
  • Daily bar dates. The daily bar of a session carries its local date, which is not always the UTC date of its bars. Tokyo opens at 00:00 UTC, right at the change of day, and in markets that open the previous evening (many futures) the first bars of a session have the UTC date of the day before. Group by calendar session, not by UTC date.
  • Missing bars. A half-day session has 42 five-minute bars, not 78. A 5-minute resample without a calendar fills the hours when the exchange was closed with empty bars (or skips them if they were holidays), and an “N bars” indicator mixes days.
  • Auctions inside the bars. Many providers put the closing auction in the last bar, as here. An “abnormal volume” filter will take it for a signal every day.
  • Volatility gaps. A few minutes without trades in the middle of the session may be a volatility auction, not a data fault (lesson 1.4.2).
  • Returns between sessions. A Monday's return includes the weekend and, after a holiday, two or three days. To compare days, count sessions with the calendar and not calendar days (lesson 1.6.3).

Exercises

  1. Compute the overlap hours between Madrid and New York on March 12 and June 2, 2025 and explain the difference.
Solution
IN [14]python
for d in ("2025-03-12", "2025-06-02"):
    h = (mad.session_close(d) - nys.session_open(d)).total_seconds()
    print(d, "NYSE opens", nys.session_open(d).strftime("%H:%M"),
          "UTC; Madrid closes", mad.session_close(d).strftime("%H:%M"),
          f"UTC; overlap {h / 3600:.0f} h")
OUT [14]Overlap on two dates in 2025
2025-03-12 NYSE opens 13:30 UTC; Madrid closes 16:30 UTC; overlap 3 h
2025-06-02 NYSE opens 13:30 UTC; Madrid closes 15:30 UTC; overlap 2 h

On March 12 the United States is already on summer time (since March 9) and Europe is not yet (until March 30): New York opens at 13:30 UTC and Madrid closes at 16:30 UTC. Three hours of overlap. On June 2 both are on summer time and Madrid closes an hour earlier in UTC: two hours.

  1. A market buy of 5,000 shares arrives at NOVA's opening auction. Recompute the price, the executable volume and the imbalance.
Solution
IN [15]python
extra = pd.DataFrame([{"lado": "compra", "tipo": "mercado",
                       "precio_limite": np.nan, "tamano": 5000}])
compra = pd.concat([compra, extra], ignore_index=True)
curva["demanda"] = curva["precio"].map(demanda)
curva["ejecutable"] = curva[["demanda", "oferta"]].min(axis=1)
curva["deseq"] = curva["demanda"] - curva["oferta"]
curva.loc[curva["ejecutable"].idxmax()]
OUT [15]The auction with one more market buy
precio           53.37
demanda       34900.00
oferta        35800.00
ejecutable    34900.00
deseq          -900.00
Name: 24, dtype: float64

Demand rises by 5,000 shares at any price, so the match shifts upward: higher prices are needed for enough supply to appear. The price moves to $53.37, two cents more, with 34,900 executable shares and an imbalance of −900. It is what happens when news from the night before fills the auction with buys: the open jumps above the previous close and leaves a gap on the chart.

  1. Explain why a strategy that computes its signal with the closing price and “buys at the close” of the same day has look-ahead bias, and propose two ways to fix it with the data from this lesson.
Solution

The closing price is that of the closing auction, which is matched at 16:00, and on the NYSE MOC orders can only be sent until 15:50. The signal uses a figure that did not exist when the order had to be sent. Fixes: compute the signal with the bar that ends at 15:50 (the 5-minute bars in nova_barras_5min.parquet allow it) and execute in the closing auction; or compute it with the close and execute in the next opening auction, whose price is rebuilt as in this lesson. The second pays the overnight gap; the first, the risk of the last ten minutes.

Footnotes

  1. NYSE hours on its “Holidays & Trading Hours” page and in the “NYSE opening and closing auctions” fact sheet; BME hours on BME Exchange's “Horario Bolsa Española” (Spanish stock exchange hours) page (accessed on October 21, 2026). ↩

  2. 2025 calendar: NYSE Group press release “NYSE Group Announces 2025, 2026 and 2027 Holiday and Early Closings Calendar”; January 9 closure, ICE press release of December 30, 2024. 2026 calendar: the NYSE “Holidays & Trading Hours” page. ↩

  3. JPX, “Cash Market Trading Hours Extension Kicked Off Today”, November 5, 2024. The current schedule, from 9:00 to 11:30 and from 12:30 to 15:30, is on JPX's “Trading Hours” page. ↩

Each answer is folded under its question

Self-assessment

  1. On June 2, 2025, for how many hours are the Madrid Stock Exchange and the NYSE open at the same time?

    • ANone: Madrid closes before New York opens
    • BSix and a half hours
    • CIt depends on the broker
    • DTwo hours, from 13:30 to 15:30 UTC
    Show answer

    Answer D. Two hours, from 13:30 to 15:30 UTC

    The NYSE opens at 9:30 New York time (13:30 UTC in summer time) and the Madrid continuous market closes at 17:30 Madrid time (15:30 UTC). In 2025, in the weeks when the United States has already changed its clocks and Europe has not, the overlap rises to three hours.

  2. In an auction, which price is chosen?

    • AThe price that maximizes the volume that can be executed between buys and sells
    • BThe price of the last trade of the previous day
    • CThe midpoint between the best bid and the best ask
    • DThe price of the largest order
    Show answer

    Answer A. The price that maximizes the volume that can be executed between buys and sells

    For each candidate price, you add up the buys willing to pay that price or more and the sells willing to sell at that price or less. The executable volume is the smaller of the two; the auction price is the one that maximizes it. If there is a tie, each exchange applies its own tie-breaking rules.

  3. Why is pd.bdate_range no good for generating NYSE sessions?

    • ABecause it does not support time zones
    • BBecause it counts holidays and unscheduled closures as business days and knows nothing about half-day sessions
    • CBecause it starts the week on Sunday
    • DIt is fine: it gives the same dates
    Show answer

    Answer B. Because it counts holidays and unscheduled closures as business days and knows nothing about half-day sessions

    bdate_range only removes Saturdays and Sundays. In 2025 the NYSE was closed on eleven weekdays (ten holidays and the unscheduled closure of January 9) and had three half-day sessions that end at 13:00. An exchange calendar includes them.

  4. Why does the last 5-minute bar of the NOVA session have about ten times more volume than the midday bars?

    • ABecause market makers withdraw before the close
    • BBecause it is a data provider error
    • CBecause it includes the closing auction, where index funds and many asset managers that are valued at the closing price execute
    • DBecause Europe opens at that time
    Show answer

    Answer C. Because it includes the closing auction, where index funds and many asset managers that are valued at the closing price execute

    A fund that tracks an index is valued at the close, so it wants to buy and sell at exactly that price. It achieves this by sending orders to the closing auction. For NOVA, on June 2, 2025, the closing auction matched 885,500 shares, 10% of the day's volume, at a single price.

  5. The Tokyo Stock Exchange used to close at 15:00 and since November 5, 2024 it closes at 15:30. What happens if a backtest uses the new time for the whole history?

    • ANothing: the closing time has no effect
    • BIt only affects futures
    • CThe data adjusts itself
    • DIn the old data it treats prices from half an hour earlier as the session close, or looks for bars that did not exist
    Show answer

    Answer D. In the old data it treats prices from half an hour earlier as the session close, or looks for bars that did not exist

    An exchange calendar keeps the history of trading hours. exchange_calendars gives 15:00 for November 1, 2024 and 15:30 for November 5. With a fixed schedule, the “at the close” signals in the old data would be computed at a time when the market was already closed.

Key points

  • A session has phases: pre-open with an auction, continuous trading, volatility auctions when the price moves too much and a closing auction. Each one produces different data.
  • Market hours are stored in UTC and computed with an exchange calendar: the time zone, daylight saving time, midday breaks and schedule changes make a fixed time useless.
  • Holidays, unscheduled closures and half-day sessions change from one year to the next. exchange_calendars and pandas_market_calendars include them; pd.bdate_range does not.
  • The price of an auction is the one that maximizes executable volume. For NOVA, $53.35 with 33,900 shares, identical to the first trade of the day.
  • Intraday volume is U-shaped, and the closing auction is the largest trade of the day because it sets the price at which funds are valued.

Resources

Data