Intraday Seasonality Patterns in Systematic Trading
Intraday seasonality is the recurring pattern of volume, volatility, spreads, and sometimes returns across the trading day. Equities show the familiar U-shape in volume and volatility — busy open, quiet midday, busy close. Ignoring it mis-sizes risk, mis-schedules TWAP/VWAP, and creates false alpha from time-of-day artifacts. This article shows how to estimate profiles and use them without overfitting.
What is seasonal vs what is noise
Stable patterns (many markets, many years):
- Volume/vol U-shape in equity RTH
- Spread wider at open, tighter midday
- Auction concentration at open/close
Unstable / often spurious:
- "Monday 10:35 long edge" from data mining
- Session effects that flip after a microstructure regime change
- Crypto patterns that shift with US/EU/Asia participation mix
Use deflated Sharpe thinking whenever you test many minute-of-day buckets.
Estimating a diurnal profile
import pandas as pd
import numpy as np
def diurnal_profile(df: pd.DataFrame, value_col: str,
minute_col: str = "minute_of_day") -> pd.Series:
"""Median value by minute-of-day (robust to outliers)."""
return df.groupby(minute_col)[value_col].median()
def normalize_by_profile(values: pd.Series, profile: pd.Series,
minutes: pd.Series) -> pd.Series:
"""De-seasonalize a series using the diurnal profile."""
expected = minutes.map(profile)
return values / expected.replace(0, np.nan)
Apply to volume, absolute returns, and spread separately. De-seasonalized residuals are what belong in feature engineering.
Uses in execution
VWAP algorithms need a volume curve forecast:
V̂(t) = f_diurnal(minute) × day_scale × event_adjust
Without a curve, "VWAP" is a random schedule. Adjust for:
- Half days / early closes
- FOMC / CPI days (different shape)
- Month-end / OPEX (pin risk,
Participation rates should be vs expected volume in that bucket, not vs full-day ADV alone (capacity).
Uses in risk and signals
- Vol scaling — size positions by time-of-day expected σ, not only daily σ
- Signal filters — disable fragile mean-reversion into the open auction mess
- Overnight vs intraday — link to overnight premium
- Crypto sessions — build profiles by UTC hour; re-estimate quarterly
def time_of_day_vol_scale(sigma_day: float, profile_vol: pd.Series,
minute: int) -> float:
"""Allocate daily vol budget across minutes using profile weights."""
w = profile_vol / profile_vol.sum()
return sigma_day * np.sqrt(w.loc[minute] * len(w))
Return seasonality: be skeptical
Academic and practitioner papers find small average return differences by hour. Most die after costs. If you trade them:
- Require OOS stability across years and regimes
- Include spread + impact at that hour (open is expensive)
- Forbid large multiple-testing grids over minutes × weekdays × months
Prefer using intraday patterns for risk and execution, not as primary alpha.
Crypto and FX
FX has session handoffs (Tokyo/London/NY) with vol spikes at opens — true seasonality tied to information flow (lead-lag).
Crypto is 24/7; "seasonality" is often:
- US equity hours spillover
- Funding timestamps
(funding)
- Weekend liquidity drop
Re-estimate often; do not freeze a 2021 UTC profile into 2026.
Key takeaways
- Volume and vol U-shapes are real; use them for execution and risk
- De-seasonalize features before calling residuals "alpha"
- VWAP curves must adjust for events and half-days
- Intraday return seasonality is usually too weak after costs
- Re-estimate crypto/FX session profiles — they drift
