# Partial Bar Graft

> 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."

- Skill: `traderspost/partial-bar-graft` (Agent Skill)
- Install (CLI): `npx skillmds@latest add traderspost/partial-bar-graft`
- Raw SKILL.md: https://api.skillmd.com/api/skills/traderspost/partial-bar-graft/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: traderspost (https://skillmd.com/u/traderspost)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/traderspost/partial-bar-graft

---


# 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.

```python
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

    # Staleness guard: if the last historical bar is from a stale snapshot
    # (backtest window from years ago, expired contract), grafting a
    # "now" bar would lie about the timeline. Only graft when we're at
    # most one bucket behind the current one.
    last = bars[-1] if bars else None
    if last is not None and bucket_start - int(last["time"]) > bucket_sec:
        return

    # Fetch 1m bars from bucket_start through now (REST tail).
    minute_df = await client.bars(
        symbol, resolution="1min",
        start=bucket_start_date, end=bucket_start_date + 1day,
        limit=bucket_sec // 60 + 5,
    )

    # Merge live in-progress 1m bar from Redis.
    live = await read_live_bar(symbol)
    if live and bucket_start <= live.time < bucket_start + bucket_sec:
        bucket_rows.append(live)

    # Aggregate OHLCV: open=first, high=max, low=min, close=last, volume=sum.
    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),
    }

    # Append or replace the last bar.
    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:

```tsx
const initialLiveBar = useMemo(() => {
  if (bars.length === 0) return null;
  const last = bars[bars.length - 1];
  const bucketSec = granularityToSeconds(granularity);
  const now = Date.now() / 1000;
  // Alignment-agnostic guard: does "now" fall within [last.time,
  // last.time + bucketSec)? Works for any bucket alignment (UTC,
  // CME 18:00 ET, ISO-week, etc.) — see Gotchas below.
  if (now < last.time || now >= last.time + bucketSec) return null;
  return last;
}, [bars, granularity]);

useTickStream(symbol, granularity, onBar, initialLiveBar);

// Inside the hook:
if (!current) {
  const seed = initialBarRef.current;
  initialBarRef.current = null;
  if (seed && seed.time === barTime) {
    // MERGE: preserve seed's OHL, update close + maxes from tick
    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 {
    // No seed → fresh bar (the bug branch when seed is null)
    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

