History & Bars
The History endpoint lets you retrieve historical OHLCV (Open, High, Low, Close, Volume) bar data for any available instrument. Use this to pre-load data for indicator calculations, analyze historical price patterns, or build lookback buffers before entering the replay loop.
POST History/retrieveBars
Retrieve historical bars for a contract within a time window. Bars are aggregated server-side to the unit/unitNumber you request.
URL: POST /api/topstep-sim/History/retrieveBars
Authentication: Bearer token required.
Request body
{ "contractId": "CON.F.US.ENQ.H25", "unit": 2, "unitNumber": 5, "startTime": "2025-01-15T00:00:00Z", "endTime": "2025-01-16T00:00:00Z", "limit": 500}| Field | Type | Required | Description |
|---|---|---|---|
contractId | string | Yes | The contract ID to retrieve data for |
unit | number | Yes | Bar aggregation unit. See AggregateBarUnit below. |
unitNumber | number | Yes | Number of units per bar (e.g., 5 with unit=2 gives 5-minute bars). Must be a positive integer. |
startTime | string or null | No | ISO 8601 start timestamp. Defaults to the beginning of the instrument’s available data. |
endTime | string or null | No | ISO 8601 end timestamp. Defaults to the end of the instrument’s available data. |
limit | number or null | No | Maximum bars to return (default and cap: 20,000) |
includePartialBar | boolean | No | Accepted for ProjectX schema compatibility; the trailing partial bar is always included |
Bars are returned from startTime forward — to fetch the most recent bars, compute startTime by counting back from the end of the data (see the example below).
AggregateBarUnit
| Value | Unit |
|---|---|
1 | Second |
2 | Minute |
3 | Hour |
4 | Day |
Common timeframe configurations
| Timeframe | unit | unitNumber |
|---|---|---|
| 1-second | 1 | 1 |
| 1-minute | 2 | 1 |
| 5-minute | 2 | 5 |
| 15-minute | 2 | 15 |
| 30-minute | 2 | 30 |
| 1-hour | 3 | 1 |
| 4-hour | 3 | 4 |
| Daily | 4 | 1 |
Response
{ "success": true, "errorCode": 0, "errorMessage": null, "bars": [ { "t": "2025-01-15T14:00:00Z", "o": 21480.50, "h": 21495.25, "l": 21478.00, "c": 21492.75, "v": 3420 }, { "t": "2025-01-15T14:05:00Z", "o": 21492.75, "h": 21505.50, "l": 21490.00, "c": 21503.25, "v": 2815 } ]}| Field | Type | Description |
|---|---|---|
bars | array | List of bar objects in chronological order (oldest first) |
bars[].t | string | ISO 8601 timestamp for the bar’s open time |
bars[].o | number | Open price |
bars[].h | number | High price |
bars[].l | number | Low price |
bars[].c | number | Close price |
bars[].v | number | Volume (number of contracts traded) |
Example: Look back N bars
Compute startTime by counting back N bar-widths from your reference point (e.g., the replay session date):
from datetime import datetime, timedelta, timezone
# Get the 200 5-minute bars leading up to the session startsession_start = datetime(2025, 1, 15, 14, 30, tzinfo=timezone.utc)lookback = timedelta(minutes=5 * 200)
result = api("History/retrieveBars", { "contractId": CONTRACT_ID, "unit": 2, # Minute "unitNumber": 5, # 5-minute bars "startTime": (session_start - lookback).isoformat(), "endTime": session_start.isoformat(), "limit": 200})
if result["success"]: bars = result.get("bars", []) print(f"Retrieved {len(bars)} bars")
# Calculate a simple moving average from historical data closes = [b["c"] for b in bars] sma_20 = sum(closes[-20:]) / 20 print(f"SMA(20) = {sma_20:.2f}")curl -X POST https://api.test-max.com/api/topstep-sim/History/retrieveBars \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "contractId": "CON.F.US.ENQ.H25", "unit": 2, "unitNumber": 5, "startTime": "2025-01-14T21:50:00Z", "endTime": "2025-01-15T14:30:00Z", "limit": 200 }'Example: Time window
# Get all 1-minute bars between 14:00 and 15:00 UTCresult = api("History/retrieveBars", { "contractId": CONTRACT_ID, "unit": 2, # Minute "unitNumber": 1, # 1-minute bars "startTime": "2025-01-15T14:00:00Z", "endTime": "2025-01-15T15:00:00Z"})
bars = result.get("bars", [])print(f"Retrieved {len(bars)} bars in the 14:00-15:00 window")Building a lookback buffer
Many strategies require historical data to calculate indicators (e.g., moving averages, RSI, Bollinger Bands) before entering the replay loop. Use History/retrieveBars to pre-load this data:
# Pre-load 50 5-minute bars for SMA calculationlookback = timedelta(minutes=5 * 50)history = api("History/retrieveBars", { "contractId": CONTRACT_ID, "unit": 2, "unitNumber": 5, "startTime": (session_start - lookback).isoformat(), "endTime": session_start.isoformat(), "limit": 50})
closes = [b["c"] for b in history.get("bars", [])]
# Now enter the replay loop with a warm indicatorfor i in range(TOTAL_BARS): bar = get_next_bar() if bar is None: break
closes.append(bar["c"])
# Need at least 20 bars for SMA(20) if len(closes) >= 20: sma = sum(closes[-20:]) / 20 if bar["c"] > sma: # Price above SMA — bullish trigger passMulti-timeframe analysis
You can retrieve bars at different timeframes for the same instrument. This is useful for strategies that use a higher timeframe for trend direction and a lower timeframe for entries.
# Get 20 daily bars for trend contextdaily = api("History/retrieveBars", { "contractId": CONTRACT_ID, "unit": 4, # Day "unitNumber": 1, # Daily "startTime": (session_start - timedelta(days=20)).isoformat(), "endTime": session_start.isoformat(), "limit": 20})
daily_bars = daily.get("bars", [])daily_closes = [b["c"] for b in daily_bars]daily_sma = sum(daily_closes[-10:]) / 10daily_trend = "bullish" if daily_closes[-1] > daily_sma else "bearish"
# Get hourly bars for recent structurehourly = api("History/retrieveBars", { "contractId": CONTRACT_ID, "unit": 3, # Hour "unitNumber": 1, # 1-hour bars "startTime": (session_start - timedelta(hours=24)).isoformat(), "endTime": session_start.isoformat(), "limit": 24})
hourly_bars = hourly.get("bars", [])recent_high = max(b["h"] for b in hourly_bars[-4:])recent_low = min(b["l"] for b in hourly_bars[-4:])
print(f"Daily trend: {daily_trend}")print(f"4-hour range: {recent_low} — {recent_high}")Usage notes
- Bars are returned in chronological order (oldest first), starting at
startTime. - If
startTime/endTimeare omitted, they default to the boundaries of the instrument’s available data. - At most 20,000 bars are returned per request, and each request scans a bounded time window measured from
startTime: 30 days for second/minute bars, 120 days for hour bars, and 365 days for day bars. A request spanning a longer range succeeds with the bars from the start of the range — it is not an error and does not mean you reached the end of the data. To fetch more, page forward by setting the next request’sstartTimeto just after the last returned bar’s timestamp. unitaccepts 1–4 (Second, Minute, Hour, Day). Week/Month aggregation is not supported.- Requests are rate-limited per account (20/minute). A
429response includes aRetry-Afterheader. - Historical bar data uses the same format as bars returned by
Replay/step. See Bar Data Format for full field documentation.
Error responses
| Scenario | errorCode | errorMessage |
|---|---|---|
Missing or unknown contractId | 1 | "Contract not found" |
Missing or unknown unit | 2 | "Unit invalid" |
Unparseable startTime/endTime | 2 | "Invalid time range" |
unitNumber missing, non-integer, < 1, or bar width exceeds the scan ceiling | 3 | "Unit number invalid" |
limit < 1 | 4 | "Limit invalid" |
| Rate limit exceeded | HTTP 429 | "Too many requests" (with Retry-After) |