Young's Xiaohongshu QR codeOpen on Xiaohongshu →Skip to content
Young · PA Research
Go back

16 Years of 1-Minute ES Data: Where I Got It and How I Cleaned It

Contents
  1. Summary
  2. Where the data comes from
  3. Why a “continuous contract”
  4. The prices themselves are clean
  5. The real problem: which days you can’t use
  6. 1. Around contract rolls (66 rolls)
  7. 2. Holidays and half days (128 days)
  8. 3. Days with bad data (14 days)
  9. Cleaning results
  10. Limitations
  11. How I use it
  12. How to reproduce it

Summary

Where the data comes from

The CME Globex MDP 3.0 dataset page on Databento

CME Globex MDP 3.0 is the Chicago Mercantile Exchange’s official market data feed. Every futures and options contract that trades on Globex comes through it. Databento has this data from June 6, 2010 onward and charges by usage. Downloading the 1-minute bars cost me about $20.

Here’s exactly what I downloaded:

ItemChoice
DatasetCME Globex MDP 3.0 (GLBX.MDP3)
InstrumentES, E-mini S&P 500 futures
ContractES.c.0, continuous contract (see below)
BarsOHLCV-1m: 1-minute open, high, low, close and volume
Date range2010-06 to 2026-09
FormatCSV

Why a “continuous contract”

ES isn’t one contract. There’s one for every quarter: ESZ6 expires in December 2026, ESH7 in March 2027, and so on. Each contract is the front month only for the few months before it expires.

To study 16 years of price action, you have to stitch these contracts together end to end. ES.c.0 is the continuous contract Databento stitches for you: it always follows the nearest expiry and rolls to the next one after expiration. That automatic roll turns out to be the biggest trap.

The prices themselves are clean

I checked all of the roughly 5.5 million 1-minute bars:

CheckResult
Missing values / duplicate timestamps0 / 0
High below open or close, low above open or close0
Prices off the 0.25 tick0
Extremely large barsSpot checks all match real events, like the 2020 circuit breakers and the 2025-04-09 tariff pause
Cross-check against SPY (2023–2024, about 8,000 bars)ES/SPY price ratio deviates by at most 0.23%, no bad prices found

The real problem: which days you can’t use

1. Around contract rolls (66 rolls)

ES.c.0 only switches to the new contract on the Sunday evening after expiration Friday. But the old contract stops trading at 9:30 a.m. on expiration Friday. So:

Thu Fri · expiry Sat Sun Mon Tue ES.c.0 Old contract New contract No RTH data Weekend Deleted Thu 16:15 → Mon 16:15 Spliced ETH Treated as one overnight session Old and new contracts differ by 10+ points, up to 224: the spliced Monday overnight high/low is fake
Roll week. Gray is the old contract, blue is the new one. Time is drawn to scale, 24 hours per day.

What I did: for every roll, delete everything from Thursday evening to Monday’s close after the roll. Monday is dropped entirely; Tuesday is kept in full.

One special case: in June 2026, expiration day fell on Juneteenth (Friday 6/19, market closed), so expiration moved up to Thursday and the contract rolled that same evening. I handled it separately.

2. Holidays and half days (128 days)

5 of these fall inside a roll’s deletion window and are already gone with the roll, so the funnel below counts only 123 for this group.

3. Days with bad data (14 days)

ProblemDaysExample
Big chunk missing from the feed32020-02-28: no data from 10:58 a.m. until Sunday evening
Overnight session stretched over several nights by a holiday8Good Friday (stocks closed, futures had a morning session); Hurricane Sandy closed stocks for two days; two presidential state funerals
First day after a fully missing day3The source data is missing 6 regular trading days entirely in 2014, so the day after each of them gets the wrong “yesterday”

Cleaning results

Days with RTH data in the raw file4,129 days

  • Kept3,924
  • Around contract rolls (66 rolls)−68
  • Holidays and half days−123
  • Bad data−14
Counted in RTH trading days; bar lengths are proportional to the number of days. The 5 holidays that overlap with a roll are counted once. Holidays and half days aren't full trading days to begin with. Counting only full trading days, 77 of 4,001 were dropped, about 2%.
Result
Usable trading days3,924 days, all covering RTH 09:30–16:15. 3,901 of them have the full 405 one-minute bars (81 five-minute bars); the other 23 are missing a few (see Limitations)
LossAbout 2% of full trading days
Overnight sessions mixing two contracts0
Overnight sessions spanning several nights0

Limitations

How I use it

Every study on this site is built on this cleaned data, and I won’t repeat the details in each post. The cleaning rules are in one script; when new data comes in, I just rerun it.

How to reproduce it

Prompt: download the data

Please write Python code that downloads historical 1-minute bars for ES futures (S&P 500 E-mini) from Databento. Requirements:
1. Use the official databento library; dataset GLBX.MDP3 (CME Globex MDP 3.0), symbol ES.c.0, stype continuous (continuous contract), schema ohlcv-1m.
2. Date range from 2010-06-06 to today.
3. First call metadata.get_cost to look up and print the cost of this download. Only download after I type y to confirm, so I don't get charged by mistake.
4. Ask me for the API key at runtime with getpass. Don't put it in the code or any file.
5. Save the result as CSV, keeping the original columns such as ts_event, instrument_id, open, high, low, close, volume, with timestamps in UTC.
6. After downloading, print the total row count and the timestamps of the first and last rows so I can check them.
7. Add comments to every step, and tell me what I need to install first (pip install databento).

Prompt: clean the data

I have a CSV of 1-minute ES futures bars (Databento's ES.c.0 continuous contract, timestamps in UTC, with an instrument_id column). Please write Python code to clean it, following these rules:
1. Convert timestamps to US Eastern time (US/Eastern), handling daylight saving time automatically. RTH = 09:30–16:15 (1-minute bars from 09:30 to 16:14, 405 bars); ETH = previous trading day 16:15 to 09:29 today.
2. First check data quality and print the results: missing values, duplicate timestamps, rows where the high is below the open/close or the low is above the open/close, and prices not on the 0.25 tick.
3. Remove data around contract rolls: find where instrument_id changes (the roll), and delete everything from 16:15 on the last trading day before expiration to 16:15 on the first trading day after the roll. Expiration is usually the third Friday; if that day is a holiday, expiration moves up a day, so handle that case separately.
4. Remove holidays and half days: days whose last RTH bar is earlier than 13:30 (stock market closed with only futures trading, or an early 13:00 close). Delete the previous evening's overnight session along with them.
5. Remove days whose overnight session spans two or more nights (for example Good Friday or a state funeral, when stocks are closed but futures trade), and the first day after any fully missing day.
6. Write out the cleaned 1-minute data, and also build 5-minute bars in US Eastern time (open = first bar's open, high = max, low = min, close = last bar's close, volume = sum).
7. Finally verify and print: how many trading days are left, whether every day has a complete RTH session (405 one-minute bars), and whether any day's overnight session mixes two contracts.
Please add comments to every step explaining why it's done that way.

(Note: in October 2026 I downloaded the April–September 2026 data as well. The date range and numbers in this post reflect the merged data.)


Share this post:

Previous Post
Do Institutions Target Retail Traders? Doing the Math on Apple's First Two Hours
Next Post
How Far Does ES Move in a Day? The Median Intraday Range Over 16 Years Is 0.90%