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/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 gateway-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 |
The requested bar width (unit × unitNumber) must be a whole multiple of the contract’s stored bar width — 1-second bars, for example, are only available on contracts whose data is stored at 1-second resolution. A combination the contract cannot build returns errorCode 2, "Invalid timeframe for this instrument".
A width outside the platform’s presets — 5s, 10s, 15s, 30s and the eight rows above — is a custom timeframe (unit=2/unitNumber=3, unit=3/unitNumber=2, unit=4/unitNumber=2, and so on). Customs sit behind the same release switch here as in the UI, and that switch is currently off: while it is off they answer errorCode 2, "Custom timeframes are currently disabled", whatever your plan.
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/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. - Bars returned here also count against the account’s daily bar budget (1,000,000 bars per UTC day). Exceeding it returns
429with"Daily data limit reached. Resets at 00:00 UTC.". Stepping a replay does not count against it (it has its own streaming allowance — see the API overview). Cold reads over a range not served recently also count against a daily data-processing budget; over it the call answers429with"Daily data-processing limit reached. Resets at 00:00 UTC.". While the contract’s data is being re-imported the call answers503withRetry-After: 30. An identical request (samecontractId,unit,unitNumber,startTime,endTimeandlimit) repeated within 5 minutes is served from cache and is not billed again. - 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" |
unit × unitNumber is not a whole multiple of the contract’s stored bar width | 2 | "Invalid timeframe for this instrument" |
unit × unitNumber is a custom width while the release switch is off | 2 | "Custom timeframes are currently disabled" |
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) |
| Daily bar budget exhausted | HTTP 429, 429 | "Daily data limit reached. Resets at 00:00 UTC." (with Retry-After, which can be hours) |