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.
- Module
- 1.1 · How markets work
- Duration
- 55 min
- Prerequisites
- 1.1.1 · Market participants and trading venues, 0.4.4 · Files (CSV, JSON, YAML, TOML) and dates with time zones
- Author
- Jesús Cuesta
- Updated
Level 1 lessons
1.1 · How markets work
1.2 · Asset classes
- 1.2.1 Stocks: splits, dividends and corporate actions
- 1.2.2 ETFs: replication, tracking error, leveraged ETFs and UCITS
- 1.2.3 Futures: specifications, margin, roll and contango
- 1.2.4 Forex: pairs, pips, lots, rollover and CFDs
- 1.2.5 Options: calls, puts, intrinsic value and time value
- 1.2.6 Crypto: spot, perpetuals, funding and custody
- 1.2.7 Fixed income and interest rates; CFDs and leveraged products
1.3 · Market data sources
- 1.3.1 Types of market data: EOD, bars, ticks, order book and reference
- 1.3.2 Free market data sources
- 1.3.3 Retail and prosumer market data APIs
- 1.3.4 Professional market data providers
- 1.3.5 Crypto and on-chain data
- 1.3.6 Brokers as market data sources
- 1.3.7 Market data licensing and terms of use
- 1.3.8 How to choose a market data provider
1.4 · Financial data engineering
- 1.4.1 Downloading over REST and websockets: pagination, rate limits and retries
- 1.4.2 Data cleaning: missing values, bad ticks, gaps and time zones
- 1.4.3 Adjusting for splits and dividends: adjusted and unadjusted prices
- 1.4.4 Universes without survivorship bias: delistings, mergers and symbol changes
- 1.4.5 Building bars: time, tick, volume, dollar and imbalance
- 1.4.6 Continuous futures series: roll rules, difference adjustment and ratio adjustment
- 1.4.7 Storage: CSV, Parquet, partitioning, DuckDB, TimescaleDB and ArcticDB
- 1.4.8 Reproducible pipeline: download, validation, incremental updates and quality tests
1.5 · Fundamental and macro data
1.6 · Financial exploratory analysis
1.7 · Programmed technical analysis
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.
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:
| Phase | NYSE (New York) | BME, continuous market (Madrid) |
|---|---|---|
| Order entry | from 6:30 | — |
| Opening auction | match at 9:30 | from 8:30 to 9:00 |
| Continuous trading | from 9:30 to 16:00 | from 9:00 to 17:30 |
| Closing imbalance period | from 15:50 to 16:00 | — |
| Closing auction | match at 16:00 | from 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.
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("") 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:00All 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:
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}")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_YorkThe 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:
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()-1.5 2
2.0 225
3.0 20
Name: count, dtype: int64Of 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.
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")))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/12Compared 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:
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")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: strIn 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.:
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"))2024-11-01 closes at 15:00
2024-11-05 closes at 15:30pandas_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):
import pandas_market_calendars as mcal
nyse_m = mcal.get_calendar("NYSE")
nyse_m.schedule("2025-11-26", "2025-12-01") 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:00Thursday 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:
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())250 sessions in 2025
Same closes: TrueThe 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 :
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 falls as the price rises; supply rises. The auction price is the with the largest , and the difference 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:
import numpy as np
sub = pd.read_csv("nova_subasta_apertura.csv")
print(len(sub), "orders")
sub.groupby(["lado", "tipo"])["tamano"].agg(["size", "sum"])139 orders
size sum
lado tipo
compra limitada 63 27400
mercado 8 17000
venta limitada 61 30600
mercado 7 14600compra = 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] 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 -6900The 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:
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"])ts 2025-06-02 13:30:00+00:00
precio 53.35
tamano 33900
condicion subasta_apertura
Match: TrueThe 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.
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)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: float64There 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:
vol_dia = op["tamano"].sum()
por_tipo = op.groupby("condicion")["tamano"].sum()
(100 * por_tipo / vol_dia).round(2)condicion
bloque 3.66
regular 86.00
subasta_apertura 0.38
subasta_cierre 9.96
Name: tamano, dtype: float64Almost 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
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
resamplewithout 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
- Compute the overlap hours between Madrid and New York on March 12 and June 2, 2025 and explain the difference.
Solution
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")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 hOn 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.
- A market buy of 5,000 shares arrives at NOVA's opening auction. Recompute the price, the executable volume and the imbalance.
Solution
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()]precio 53.37
demanda 34900.00
oferta 35800.00
ejecutable 34900.00
deseq -900.00
Name: 24, dtype: float64Demand 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.
- 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
-
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). ↩
-
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. ↩
-
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
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.
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.
Why is
pd.bdate_rangeno 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_rangeonly 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.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.
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_calendarsgives 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_calendarsandpandas_market_calendarsinclude them;pd.bdate_rangedoes 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
Documentation
- NYSE: Holidays & Trading Hours (sessions, auctions, holidays and half-day sessions)
- NYSE: Opening and Closing Auctions (fact sheet; MOC/LOC until 15:50)
- BME Exchange (Spanish stock exchange): market hours (SIBE, auctions and Trading At Last)
- JPX: Cash Market Trading Hours Extension Kicked Off Today (November 5, 2024)
- exchange_calendars (repository and documentation)
- pandas_market_calendars: documentation