| name | partial-bar-graft |
| description | Use when building a charting UI that needs to show the current in-progress candle correctly on page-load. Data providers return only closed bars, leaving a gap from bucket-start to "now" until live ticks arrive. Server must graft the partial bucket; client must seed the tick handler with it. Missing EITHER layer manifests as "the rightmost candle shows wrong OHLC" or "the chart appears stuck for the first few seconds after page-load." |
| metadata | {"category":"charting"} |
Partial-bar graft: showing the in-progress candle on page-load
OHLC data providers (massive.com, polygon, IBKR market data, etc.) return only closed bars in their REST responses. On a 5min chart at 13:02 ET, you'll get bars up to the 12:55 bar (which closed at 13:00). The 13:00→13:02 in-progress bucket is absent from the response. Naive integrations either show a gap until the next live tick arrives, or have the live tick reset the bar to (p, p, p, p) from the tick price — destroying the partial bar's OHL history.
Fixing this requires two coordinated layers: a server-side graft that fabricates the in-progress bar from finer-grained data, AND a client-side seed handoff that preserves it across the first WebSocket tick.
When to use
Reach for this skill if you're:
- Shipping a chart that displays a real-time in-progress candle
- Seeing the rightmost candle's open/high/low jump on every page reload
- Tracing a "gap between historical and live data" issue on a 5min+ chart
- Confused why the partial bar appears for a fraction of a second then disappears
The pattern — both layers, in order
Layer 1: server graft
On the bars endpoint, after returning historical (closed) bars, aggregate finer-grained data (1m REST + your live-bar key in Redis or equivalent) into a single partial bar for the current bucket, and append/replace the last entry.
async def graft_partial_bucket(bars, symbol, granularity, client):
bucket_sec = NATIVE_GRANULARITY_SECONDS.get(granularity)
if bucket_sec is None or bucket_sec <= 60:
return
now_sec = int(datetime.now(UTC).timestamp())
bucket_start = (now_sec // bucket_sec) * bucket_sec
last = bars[-1] if bars else None
if last is not None and bucket_start - int(last["time"]) > bucket_sec:
return
minute_df = await client.bars(
symbol, resolution="1min",
start=bucket_start_date, end=bucket_start_date + 1day,
limit=bucket_sec // 60 + 5,
)
live = await read_live_bar(symbol)
if live and bucket_start <= live.time < bucket_start + bucket_sec:
bucket_rows.append(live)
partial = {
"time": bucket_start,
"open": bucket_rows[0]["open"],
"high": max(r["high"] for r in bucket_rows),
"low": min(r["low"] for r in bucket_rows),
"close": bucket_rows[-1]["close"],
"volume": sum(r["volume"] for r in bucket_rows),
}
if last and int(last["time"]) == bucket_start:
bars[-1] = partial
else:
bars.append(partial)
Layer 2: client seed handoff
Without this layer, even with the server's grafted partial bar, the very first WebSocket tick after page-load resets the bar's OHLC to (p, p, p, p). The tick-stream's "no previous state → start fresh" branch fires and erases the server's hard work.
Pass the grafted bar to the tick stream as a seed. On the first tick, if the seed's time matches the tick's bucket, MERGE OHL from the seed rather than reset:
const initialLiveBar = useMemo(() => {
if (bars.length === 0) return null;
const last = bars[bars.length - 1];
const bucketSec = granularityToSeconds(granularity);
const now = Date.now() / 1000;
if (now < last.time || now >= last.time + bucketSec) return null;
return last;
}, [bars, granularity]);
useTickStream(symbol, granularity, onBar, initialLiveBar);
if (!current) {
const seed = initialBarRef.current;
initialBarRef.current = null;
if (seed && seed.time === barTime) {
current = {
time: barTime,
open: seed.open,
high: Math.max(seed.high, tick.p),
low: Math.min(seed.low, tick.p),
close: tick.p,
volume: seed.volume + tick.s,
};
} else {
current = { time: barTime, open: tick.p, high: tick.p, low: tick.p, close: tick.p, volume: tick.s };
}
}
Gotchas
-
The "is the last bar the in-progress one?" check has to be alignment-agnostic. Comparing last.time === floor(now / bucketSec) * bucketSec only works for UTC-aligned buckets (1m, 5min, 15min, 1hour). For CME-aligned 4h/8h or weekly/monthly, the bucket starts at a NON-UTC-multiple time, so the equality always fails and the seed never fires. Use a range check: now ∈ [last.time, last.time + bucketSec).
-
Stale-snapshot guard. If the operator opens a backtest window from years ago or an expired contract, the historical fetch returns old bars. DON'T graft a "now" partial bar onto that — it lies about the timeline. Skip the graft if bucket_start - last.time > bucket_sec.
-
Race condition with client-fetched bars. If the chart fetches bars via fetch() (not as a server-rendered prop), the WS subscription may fire its first tick BEFORE bars resolve. The seed handoff via the prop won't help because the prop is null at that point. See react-canvas-race-conditions for the ref-based fix.
-
Resampled granularities. If 4h bars come from a server-side resampler that calls 1h-bars internally, the underlying 1h endpoint's graft will surface — make sure your resampler's path also benefits, OR add an explicit graft for resampled targets.
-
Volume accuracy. The grafted bar's volume = REST 1m bars' volume sum + live bar's volume. The live bar's volume is "this minute so far" — some implementations double-count if a 1m bar overlaps the live bar. Dedupe by time.
Reference implementation
This pattern came from a charting platform whose data layer had:
- A bars endpoint (
/api/bars) that returns historical OHLC, with a graft_partial_bucket helper called for native granularities > 1m
- A live worker that publishes the current 1m bar to Redis (key shape
live_bar_1m:{symbol})
- Two consumer charts (main + picture-in-picture); both implemented an
initialLiveBar memo that derives the seed bar from props/state and passes it into a shared useTickStream hook
- A
useTickStream hook owning the WebSocket subscription, with the merge-vs-reset branch keyed on the seed
The split between "server has the data" and "client preserves it across the first tick" was the load-bearing insight.
Related skills
react-canvas-race-conditions — the ref-lifting fix for the WS-before-bars race
cme-futures-time-buckets — the bucket math your server-side graft needs to respect
lightweight-charts-integration — the chart-side primitives the seeded ticks update