---
title: "1.1.2 Sessions, auctions, holidays and calendars"
description: "When each exchange opens and how the open and close are set: sessions in UTC, auctions, holidays and half-day sessions with exchange_calendars."
url: "https://pythonparatrading.com/en/python-trading-course/level-1/trading-sessions-auctions-and-market-calendars/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-1/trading-sessions-auctions-and-market-calendars.png"
---

# 1.1.2 · 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.

- Course: [Level 1: Financial data, markets and analysis](https://pythonparatrading.com/en/python-trading-course/level-1/)
- Module: 1.1 · How markets work
- Duration: 55 min
- Prerequisites: [1.1.1 · Market participants and trading venues](https://pythonparatrading.com/en/python-trading-course/level-1/market-participants-and-trading-venues/), [0.4.4 · Files (CSV, JSON, YAML, TOML) and dates with time zones](https://pythonparatrading.com/en/python-trading-course/level-0/python-files-pathlib-time-zones/)

## By the end of this lesson

- Describe the phases of an equity trading session (pre-open, opening auction, continuous trading, volatility auction, closing auction) and when each one is used.
- Get with `exchange_calendars` and `pandas_market_calendars` an exchange's opening and closing times, breaks, holidays and half-day sessions, in UTC and in local time.
- Compute the price of an auction as the one that maximizes executable volume and check it against the actual opening trade in the data.
- Explain the U-shaped volume profile and the weight of the closing auction, and avoid the most common calendar mistakes in a backtest.

## 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
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 it[^1].
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.

[^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).

## 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.

```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("")
```
```output
            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:

```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}")
```
```output
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:

```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()
```
```output
-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
30[^2]. And there are **half-day sessions**: on the NYSE, July 3, the Friday after Thanksgiving and December 24 usually
close at 13:00.

[^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.

```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")))
```
```output
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:

```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")
```
```output
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 auction[^3]:

[^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.

```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"))
```
```output
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`):

```python
import pandas_market_calendars as mcal

nyse_m = mcal.get_calendar("NYSE")
nyse_m.schedule("2025-11-26", "2025-12-01")
```
```output
                         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:

```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())
```
```output
250 sessions in 2025
Same closes: True
```

<aside class="callout" data-kind="aviso">

An exchange's calendar is not that of its settlement nor that of another market in the same country. Index futures
can trade on a day when the cash exchange is closed, and the depository may not settle on a day when the exchange is
open. For each series, use the calendar of the market that produces it (lessons 1.1.5 and 1.2.3).

</aside>

## 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 $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)$ falls as the price rises; supply $S(p)$ rises. The auction price is the $p$ with the
largest $V(p)$, and the difference $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:

```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"])
```
```output
139 orders
                 size    sum
lado   tipo
compra limitada    63  27400
       mercado      8  17000
venta  limitada    61  30600
       mercado      7  14600
```

```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]
```
```output
    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.* Two step curves between 52.96 and 53.62 USD. Cumulative demand falls from 44,400 shares to 17,200 as the price rises; supply rises from 15,600 to 45,200. They cross at 53.35 USD (highlighted), where 33,900 shares can be executed, the maximum.

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:

```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"])
```
```output
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.

```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)
```
```output
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.* 78 bars from 9:30 to 16:00 New York time. The first holds 3.06% of the day's volume and the following ones fall to about 1% at midday; in the last half hour they rise again, and the last bar, at 15:55, which includes the closing auction, reaches 11.28% (highlighted).

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:

```python
vol_dia = op["tamano"].sum()
por_tipo = op.groupby("condicion")["tamano"].sum()
(100 * por_tipo / vol_dia).round(2)
```
```output
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.

<aside class="callout" data-kind="riesgo">

Deciding with the close and executing at that same close is look-ahead (lesson 0.1.1): the closing price is not known
until the auction is matched, and to take part in it the order had to be sent ten minutes earlier. An honest rule
decides with the 15:50 data and executes in the auction, or decides with the close and executes at the next open.

</aside>

## What this changes in your data

```mermaid
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.**

```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")
```
```output
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.

2. A market buy of 5,000 shares arrives at NOVA's opening auction. Recompute the price, the executable volume and the
   imbalance.

**Solution.**

```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()]
```
```output
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.

3. 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.

## Self-assessment

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

   - A. None: Madrid closes before New York opens
   - B. Six and a half hours
   - C. It depends on the broker
   - D. Two hours, from 13:30 to 15:30 UTC

   *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?

   - A. The price that maximizes the volume that can be executed between buys and sells
   - B. The price of the last trade of the previous day
   - C. The midpoint between the best bid and the best ask
   - D. The price of the largest order

   *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?

   - A. Because it does not support time zones
   - B. Because it counts holidays and unscheduled closures as business days and knows nothing about half-day sessions
   - C. Because it starts the week on Sunday
   - D. It is fine: it gives the same dates

   *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?

   - A. Because market makers withdraw before the close
   - B. Because it is a data provider error
   - C. Because it includes the closing auction, where index funds and many asset managers that are valued at the closing price execute
   - D. Because Europe opens at that time

   *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?

   - A. Nothing: the closing time has no effect
   - B. It only affects futures
   - C. The data adjusts itself
   - 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

   *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

### Research

- [Seasonality in Python. Searching for Intraday Patterns](https://pythonparatrading.com/en/seasonality-intraday-preprocessing/) · Research-0024 · 9 min

### Documentation

- [NYSE: Holidays & Trading Hours (sessions, auctions, holidays and half-day sessions)](https://www.nyse.com/markets/hours-calendars) · nyse.com
- [NYSE: Opening and Closing Auctions (fact sheet; MOC/LOC until 15:50)](https://www.nyse.com/publicdocs/nyse/markets/nyse/NYSE_Opening_and_Closing_Auctions_Fact_Sheet.pdf) · nyse.com
- [BME Exchange (Spanish stock exchange): market hours (SIBE, auctions and Trading At Last)](https://www.bolsasymercados.es/es/bme-exchange/horario-del-mercado.html) · bolsasymercados.es
- [JPX: Cash Market Trading Hours Extension Kicked Off Today (November 5, 2024)](https://www.jpx.co.jp/english/corporate/news/news-releases/1030/20241105-01.html) · jpx.co.jp
- [exchange_calendars (repository and documentation)](https://github.com/gerrymanoim/exchange_calendars) · github.com
- [pandas_market_calendars: documentation](https://pandas-market-calendars.readthedocs.io/en/latest/) · pandas-market-calendars.readthedocs.io

### Data

- [Level 1 data: NOVA opening auction book (CSV)](https://pythonparatrading.com/curso/datos/nivel-1/nova_subasta_apertura.csv) · Download · nova_subasta_apertura.csv

Previous lesson: [1.1.1 · Market participants and trading venues](https://pythonparatrading.com/en/python-trading-course/level-1/market-participants-and-trading-venues/)

Next lesson: [1.1.3 · Order book: bid, ask, spread and depth](https://pythonparatrading.com/en/python-trading-course/level-1/order-book-bid-ask-spread-depth/)
