Rebuilding a Massive aggregate bar from raw ticks and getting a different answer is the usual reason people read this page. The sale conditions on each US stock trade are why: they decide whether a trade may set the open or the close, extend the high or the low, or add to volume, and one trade can be allowed to do some of those and not the others.
Applies to
- Plans: any Stocks subscription. Other asset classes follow the same shape with their own eligibility rules.
- Endpoints: /v2/aggs/ticker/... and the grouped endpoint for the bars, /v3/reference/conditions for the rules, /v3/trades for the trades.
- Asset classes: US stocks above all; options, indices, forex, crypto and futures also aggregate.
How each timespan is assembled
Each timespan is assembled from trades at the smallest sizes and rolled up above that:
| Timespan | Built from |
|---|---|
| Per second | Eligible trades in that second, published about two seconds later to let late messages arrive |
| Per minute | Eligible trades in that minute, published as they arrive and revised if a late trade changes it |
| Per hour | The minute bars inside the hour, rolled up |
| Per day | Continuously recalculated through the session, so it absorbs even very late trades |
| Week, month, quarter, year | Rolled up from the daily bars |
Because the hour is a roll-up of minutes, an hour bar's volume is the sum of its minute bars' volume and never includes a trade that no minute bar counted.
Why your reconstruction differs
Your reconstruction differs for one of four reasons, in the order they show up:
- Eligibility. If you summed every trade, you included prints that our rules exclude from price, volume, or both. This is the big one.
- Which timestamp you bucketed on. Bucketing on the participant timestamp puts trades in different windows from bucketing on the SIP timestamp, and the two differ by the SIP's own latency.
- size against decimal_size. The integer size is rounded and can be zero on a fractional-share trade. Sum the decimal field.
- Corrections. A trade canceled or corrected after you pulled it still counts in your copy. Bars are revised for about 15 minutes on the stream and again at the end of the day.
Other vendors differ from us for the same reasons, applied differently. There is no universal source of truth for OHLC, which is why we follow the CTA and UTP guidelines rather than inventing our own rules.
How to check one trade's eligibility
Take the conditions array from the trade and ask what those codes do. The conditions endpoint answers per code, and separately for the consolidated tape and for a single market center:
curl -X GET "https://api.massive.com/v3/reference/conditions?asset_class=stocks&data_type=trade&id=2&apiKey=YOUR_API_KEY"
Response
{
"results": [
{
"id": 2, "type": "sale_condition", "name": "Average Price Trade",
"sip_mapping": { "CTA": "B", "UTP": "W", "FINRA_TDDS": "W" },
"update_rules": {
"consolidated": { "updates_high_low": false, "updates_open_close": false, "updates_volume": true }
}
}
],
"status": "OK"
}
Condition 2 adds to volume and touches no price, so a minute holding only average-price trades produces no bar at all.
If you see an error
A bar that differs from your own by a small amount is usually size against decimal_size.
A bar that differs a lot, or has a high or low you cannot find in the trades, is eligibility. Check the conditions on the trades at the extremes.
A bar that changed after you stored it is a correction, and the REST value is the current one.

