Skip to content
Back to App

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
}
FieldTypeRequiredDescription
contractIdstringYesThe contract ID to retrieve data for
unitnumberYesBar aggregation unit. See AggregateBarUnit below.
unitNumbernumberYesNumber of units per bar (e.g., 5 with unit=2 gives 5-minute bars). Must be a positive integer.
startTimestring or nullNoISO 8601 start timestamp. Defaults to the beginning of the instrument’s available data.
endTimestring or nullNoISO 8601 end timestamp. Defaults to the end of the instrument’s available data.
limitnumber or nullNoMaximum bars to return (default and cap: 20,000)
includePartialBarbooleanNoAccepted 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

ValueUnit
1Second
2Minute
3Hour
4Day

Common timeframe configurations

TimeframeunitunitNumber
1-second11
1-minute21
5-minute25
15-minute215
30-minute230
1-hour31
4-hour34
Daily41

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
}
]
}
FieldTypeDescription
barsarrayList of bar objects in chronological order (oldest first)
bars[].tstringISO 8601 timestamp for the bar’s open time
bars[].onumberOpen price
bars[].hnumberHigh price
bars[].lnumberLow price
bars[].cnumberClose price
bars[].vnumberVolume (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 start
session_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}")

Example: Time window

# Get all 1-minute bars between 14:00 and 15:00 UTC
result = 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 calculation
lookback = 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 indicator
for 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
pass

Multi-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 context
daily = 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:]) / 10
daily_trend = "bullish" if daily_closes[-1] > daily_sma else "bearish"
# Get hourly bars for recent structure
hourly = 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/endTime are 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’s startTime to just after the last returned bar’s timestamp.
  • unit accepts 1–4 (Second, Minute, Hour, Day). Week/Month aggregation is not supported.
  • Requests are rate-limited per account (20/minute). A 429 response includes a Retry-After header.
  • Bars returned here also count against the account’s daily bar budget (1,000,000 bars per UTC day). Exceeding it returns 429 with "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 answers 429 with "Daily data-processing limit reached. Resets at 00:00 UTC.". While the contract’s data is being re-imported the call answers 503 with Retry-After: 30. An identical request (same contractId, unit, unitNumber, startTime, endTime and limit) 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

ScenarioerrorCodeerrorMessage
Missing or unknown contractId1"Contract not found"
Missing or unknown unit2"Unit invalid"
Unparseable startTime/endTime2"Invalid time range"
unit × unitNumber is not a whole multiple of the contract’s stored bar width2"Invalid timeframe for this instrument"
unit × unitNumber is a custom width while the release switch is off2"Custom timeframes are currently disabled"
unitNumber missing, non-integer, < 1, or bar width exceeds the scan ceiling3"Unit number invalid"
limit < 14"Limit invalid"
Rate limit exceededHTTP 429"Too many requests" (with Retry-After)
Daily bar budget exhaustedHTTP 429, 429"Daily data limit reached. Resets at 00:00 UTC." (with Retry-After, which can be hours)