cme-futures-time-buckets

star 3

Use when resampling intraday futures bars (e.g. 4h, 8h) or weekly/monthly bars to a coarser timeframe. CME globex futures sessions run Sunday 18:00 ET → Friday 17:00 ET, NOT calendar weeks; intraday buckets a futures trader expects align to the 18:00 ET session open, NOT UTC midnight. Naive resampling produces bars that look right but are wrong by 4-6 hours.

TradersPost By TradersPost schedule Updated 5/21/2026

name: cme-futures-time-buckets description: Use when resampling intraday futures bars (e.g. 4h, 8h) or weekly/monthly bars to a coarser timeframe. CME globex futures sessions run Sunday 18:00 ET → Friday 17:00 ET, NOT calendar weeks; intraday buckets a futures trader expects align to the 18:00 ET session open, NOT UTC midnight. Naive resampling produces bars that look right but are wrong by 4-6 hours. metadata: category: charting

CME futures time-bucket alignment

Most data tools default to UTC-aligned buckets ("a 4-hour bar starts at 00:00, 04:00, 08:00, …"). For a futures trader looking at a 4h NQ chart, that's wrong — they expect the 4h bar at 18:00 ET to mark the start of the new trading session, the same way it's drawn on TradingView and every other futures-aware platform. Same for weekly: a futures week is Sunday 18:00 → Friday 17:00 ET, not ISO Monday → Sunday.

When to use

Reach for this skill if you're:

  • Building a resampler that turns 1m or 1h bars into 4h, 8h, weekly, or monthly bars
  • Shipping a charting UI where the operator can pick any timeframe
  • Hearing "the bars don't line up with TradingView" from a futures trader
  • Implementing group_by_dynamic or any equivalent time-bucketing in Polars / pandas / DuckDB

The pattern — Polars group_by_dynamic with NY-local timezone + offset

The trick is convert to NY-local time, group with the right offset argument, convert back to UTC. NY-local handles DST transitions automatically; the offset shifts the bucket origin so 18:00 ET lands on a bucket boundary.

import polars as pl

# Intraday: 4h or 8h buckets, anchored to CME 18:00 ET
def resample_intraday(bars: pl.DataFrame, every: str) -> pl.DataFrame:
    # `every` is "4h", "8h", etc. Bucket origin: 02:00 NY-local → buckets at
    # 02, 06, 10, 14, 18, 22 NY-local. The 18:00 ET (= NY-local 18:00) bar
    # captures the first 4h of the futures trading day.
    return (
        bars.with_columns(
            pl.col("window_start").dt.convert_time_zone("America/New_York")
        )
        .group_by_dynamic(
            "window_start",
            every=every,
            offset="2h",          # 02:00 NY-local origin → 18:00 included
            closed="left",
            label="left",
        )
        .agg(
            pl.col("open").first().alias("open"),
            pl.col("high").max().alias("high"),
            pl.col("low").min().alias("low"),
            pl.col("close").last().alias("close"),
            pl.col("volume").sum().alias("volume"),
        )
        .with_columns(
            pl.col("window_start").dt.convert_time_zone("UTC")
        )
    )

# Weekly: Sunday 18:00 ET → Friday 17:00 ET. Polars' default 1w is Monday
# ISO-week; shift back 6 hours from Monday 00:00 to land on Sun 18:00.
WEEK_OFFSET_NY = "-6h"

Why the magic numbers

  • 4h, offset="2h": default 4h origin is 00:00. Adding 2h shifts to {02, 06, 10, 14, 18, 22} NY-local — 18:00 included.
  • 8h, offset="2h": default 8h origin is 00:00. Adding 2h shifts to {02, 10, 18} NY-local — 18:00 included.
  • 2h, offset="0h": default already at every even hour → 18:00 included naturally.
  • 30m, no shift needed: trader convention is :00/:30 boundaries.
  • 1w, offset="-6h" on NY-local: shifts Monday 00:00 → previous Sunday 18:00.

What about 1d?

A daily bar on CME spans 18:00 ET (prev day) → 17:00 ET (current day). Most data providers already key daily bars by the session date with the right boundary baked in. If yours doesn't, apply the same NY-local + offset trick with every="1d" and offset="-6h" (NY-local 00:00 − 6h = previous day 18:00). Verify against the data provider's docs.

Verifying alignment

Build a test that constructs a known-aligned bar series, resamples, and asserts the output bucket starts at 18:00 ET on a specific date:

def test_4h_aligns_to_cme_session_start_18et() -> None:
    # 2026-01-05 23:00 UTC = 18:00 EST (winter). Spawn 8 hourly bars.
    bars = _mk_bars(datetime(2026, 1, 5, 23, 0), n=8, step_minutes=60)
    out = resample_bars(bars, "4hour").sort("window_start")
    assert out.height == 2
    # First bucket aggregates the 23:00, 00:00, 01:00, 02:00 UTC bars
    # (= 18:00, 19:00, 20:00, 21:00 EST)
    row0 = out.row(0, named=True)
    assert row0["open"] == 0.0
    assert row0["close"] == 3.25

This kind of test catches the "off by N hours" bugs that visual inspection misses.

Gotchas

  • Polars every="1w" defaults to Monday-aligned ISO week. Use the -6h NY-local offset trick above for CME-week, OR document the deviation as a known limitation if ISO-week is acceptable.
  • DST. Don't try to bake "18:00 ET = 23:00 UTC" as a fixed offset — that's only true in EST. America/New_York handles DST automatically via convert_time_zone. Don't shortcut it.
  • offset is a string token, not seconds. Polars wants "2h", "-6h", "30m". Numeric seconds throw.
  • The closed="left", label="left" combo matters. Default closed="right" shifts which bar each timestamp falls into and produces off-by-one bucket counts. Always specify both.
  • Test data needs to live in a single CME session. Constructing bars spanning a DST transition or a weekend gap will produce surprising bucket counts that aren't bugs but test-data issues.

Reference implementation

This pattern was extracted from a working resampler module that translates 1m → {30m, 2h, 4h, 8h, weekly, monthly} for a CME futures charting platform. The structure looked like:

  • A resampler.py exposing a single resample_bars(df, target) function
  • An _INTRADAY_OFFSETS_NY dict mapping each intraday granularity to its NY-local offset string
  • A WEEK_OFFSET_NY constant for the CME-week alignment
  • A test file with ~12 parity tests covering every granularity with explicit "first bucket starts at X" assertions

Related skills

  • partial-bar-graft — once your resampler works, the in-progress bar still needs special handling
  • lightweight-charts-integration — the chart side is unaware of timezones; the resampler is the right place for alignment logic
Install via CLI
npx skills add https://github.com/TradersPost/traderspost-command-dash --skill cme-futures-time-buckets
Repository Details
star Stars 3
call_split Forks 3
navigation Branch main
article Path SKILL.md
More from Creator