Inside Kestrel: The Per-Bar Pipeline, Derived Bars and Expiry

One bar arrives. What happens next, and in what order? In this episode we walk through Kestrel's handleBar pipeline in a C++20 trading engine, and see why the order of steps prevents lookahead bugs.

Intro

One Bar, Many Decisions

A single one-minute bar arrives at the engine. Within a few microseconds, orders may fill, an option price may be born from nothing, an expiring contract may be settled, and a strategy may decide to trade. The question is: in what order? Get that order wrong, and your backtest quietly cheats.

  • One bar in
  • Many things must happen
  • Order is everything

Where We Are

In the previous episode we saw the event bus, the k-way merge, and why the engine replays the same bars in the same order every time. Today we zoom into what the engine does with each bar once the merge hands it over. This function is called handleBar, and it is the heart of Kestrel.

  • Episode 5: the merge
  • Episode 6: handleBar
  • The heart of Kestrel

What We Will Learn

By the end you will know the order of steps in handleBar and the reason for each one. You will understand the venue pull model, how option and future bars are derived from an underlying bar, how expiry settlement works, and how the paper trading loop differs from a backtest. Educational only, and any numbers are illustrative.

  • Step order and why
  • Pull model and pump
  • Derived bars and expiry
  • The paper loop

The Eight Steps

The Pipeline Stations

Picture the pipeline as a vertical line of stations. Each bar enters at the top and lights up every station in turn. Settle expiries. Handle the instrument and update the market. Derive option and future bars. Advance the clock and let the venue fill. Check equity. Square off. Run the alphas. Record an equity point.

  • Settle expiries
  • Update market, derive bars
  • Venue fills, marks
  • Equity, square-off, alphas

Step A: Settle Expiries First

Step one. If the clock has advanced since the last bar, the engine settles anything that expired up to the instant just before this bar began, that is bar start minus one, and then notifies equity. Settling first means a dead contract can never be traded, marked, or priced by the bar that follows.

  • Only if time advanced
  • settleExpired(bar.start - 1)
  • Then notify equity

Step B: Filter the Bar

Step two handles bad input. If the instrument is unknown, invalid, or already settled, the bar is dropped. Otherwise the market state is updated with this bar. Only after that does anything else look at prices, so every later station sees a consistent market.

  • Unknown, invalid, settled
  • Drop or update market
  • Consistent view for later steps

Step C: Derived Bars Arrive

Step three. Derived bars are computed right now, synchronously, breadth first. They are never posted to the event bus. That detail matters: an option priced off this underlying bar exists in the very same step, not one event later, so the venue and the alphas see them together.

  • Computed synchronously
  • Breadth-first
  • Never posted to the bus

Step D: Clock and Venue

Step four moves the clock: now becomes the larger of its old value and the bar's end time. Then the gateway receives onBars. The venue fills working orders using earlier bars, marks are updated, and execution reports are pumped through the system. Notice the phrase earlier bars. It is the key to the whole design.

  • now_ = max(now_, bar.end)
  • Venue fills from EARLIER bars
  • Marks update, reports pumped

Steps E and F: Guards

Step five runs onEquity, which drives the daily-loss kill switch. Step six is the square-off tick, which closes positions at the configured time of day, unless the run is still in warmup. These guards run before any strategy code. A strategy never gets to act on a day the risk layer has already halted.

  • onEquity: daily-loss kill switch
  • Square-off tick
  • Skipped during warmup

Steps G and H: Alphas Last

Only now, in step seven, do the alphas receive onBar. After they run, step eight records an equity point. Alphas are last on purpose. By the time a strategy sees a bar, fills, marks, risk and square-off for that bar are all settled, so the strategy sees the world exactly as it is.

  • Alphas' onBar runs last
  • Then an equity point
  • World is settled first

Why This Order

The Lookahead Trap

Why does the order matter so much? Imagine alphas ran before fills. A strategy sees the bar's close, submits a buy, and the venue fills it at a price from that same bar. That trade used information that did not exist when the order was placed. It is lookahead bias, and it flatters every backtest it touches.

  • Alpha sees close
  • Order fills on same bar
  • That is lookahead

Worked Example: Bar by Bar

Here is the correct behaviour with illustrative times. At nine twenty, a bar ending at nine twenty-one reaches the alphas. The alpha decides to buy. In step four, this bar's fills have already run, so the order cannot fill here. The order sits in the venue. When the next bar, ending nine twenty-two, arrives, step four finally considers it.

  • Bar ends 09:21: alpha decides
  • Order rests in the venue
  • Bar ends 09:22: order can fill

The Consequence

So the rule is simple. An alpha decides on a bar that has already been used for fills, and its order can only fill on a later bar. How the fill price is chosen is a separate topic, covered in episode ten. Here we only care that the pipeline order makes the wrong answer impossible.

  • Decide on bar t
  • Fill on bar t+1 or later
  • Price rules: episode 10

Faults Are Contained

Strategy code can throw. So callAlpha wraps every callback in a try and catch. If an alpha throws, it is faulted: disabled, with its open orders cancelled. Then pumpVenue runs, so the resulting reports apply immediately. One broken alpha cannot crash the run or leave orphan orders behind.

  • try/catch around callbacks
  • Exception: alpha disabled
  • Orders cancelled, then pumpVenue()

The Venue Pull Model

The Bug We Want to Kill

Now the second big idea. Suppose a venue called back into the engine whenever an order filled. The alpha's onFill handler might submit a new order, while the venue is still looping over its working orders. The container changes under the iterator. That bug class is nasty, because it depends on timing and data.

  • Venue calls back into engine
  • Alpha submits inside onFill
  • Venue mid-iteration: trouble

Reports Wait in a Queue

Kestrel's answer is a pull model. Venues never call back into the engine. They only queue execution reports, each one either an OrderUpdate or a Fill. The execution core later drains that queue by calling poll. The venue finishes its own work first, and only then does anyone react.

  • Venues never call back
  • They queue ExecutionReports
  • Core drains via poll()

The Fixed Order of Effects

When a report is drained, it is applied in a fixed order. First the order management system. Then positions. Then risk counters. Then the journal. Finally the listener, which notifies observers and the owning alpha's onFill or onOrderUpdate. By the time any strategy code runs, all the books already agree.

  • 1. OMS
  • 2. Positions
  • 3. Risk counters
  • 4. Journal
  • 5. Listener, alpha callbacks

Code: ExecutionCore pump

Here is the real pump. The first line returns early if we are already pumping, or there is no venue. Then the flag is set, and a tiny struct named Reset clears it in its destructor. That is RAII: even if a handler throws, the flag cannot stay stuck on.

  • Early return if re-entered
  • pumping_ = true
  • RAII Reset clears the flag

Code: Loop Until Quiet

Next comes an endless loop. Clear the local reports, ask the venue to poll, and if nothing came back, break. Otherwise visit each report with std visit over the variant of OrderUpdate and Fill, calling handleOrderUpdate or handleFill. Why loop again? Handling a report can cause new reports, so we drain until the venue is quiet.

  • for (;;) poll until empty
  • std::visit with Overloaded
  • OrderUpdate or Fill

Re-entrancy Guard in Action

What if a handler, say an alpha's onFill, submits an order and that triggers another pump call? The flag is already set, so the nested call returns immediately. The new reports are not lost, because the outer loop will poll again. No nested iteration, no mutation under an iterator, one clean drain.

  • Nested pump() returns at once
  • Outer loop polls again
  • No nested iteration

Derived Bars

Options Have No History

Now step three in detail. The store holds bars for underlyings, but a backtest also needs bars for options and futures. Kestrel derives them, with the source tag derived. Every derived bar is computed from an underlying bar, using the time remaining to expiry. If the bar is at or after expiry, the derivation is skipped.

  • Source tag: derived
  • Computed from underlying bar
  • Skip at or after expiry

Futures: Cost of Carry

For a future, first compute T, the year fraction from the bar's end to expiry. Then each of open, high, low and close is transformed as F equals S times e to the r T. Illustrative numbers: S of twenty-four thousand, r of seven percent, thirty days. T is about 0.082, and F comes to roughly twenty-four thousand one hundred and thirty-eight.

  • T = yearFraction(bar.end, expiry)
  • F = S e^(rT)
  • Applied to O, H, L, C

Options: Four Prices, Four Pricings

For an option, Black-Scholes is evaluated at each of the underlying's open, high, low and close, with volatility taken from pricing dot volatility. The formula itself is episode twelve. What matters here is the mapping. Four underlying prices produce four option prices, which then become the option bar's own open, high, low and close.

  • Price at each underlying O/H/L/C
  • Vol from pricing.volatility
  • Formula: episode 12

The Put Twist

Here is a subtle trap. A put gains value when the underlying falls. So a put's high does not come from the underlying high. It comes from the underlying low. Take an illustrative underlying bar: open twenty-four thousand, high twenty-four twenty, low twenty-three eighty-five, close twenty-four ten. The put's high is priced at twenty-three eighty-five.

  • Put value rises as S falls
  • Put high uses underlying LOW
  • Call high uses underlying HIGH

Tick Rounding and Clamping

Raw model prices are not tradable prices. Open and close are rounded to the nearest tick. The high is rounded up and the low is rounded down, so the rounded bar always contains the real range. Everything is clamped to be at least zero, and volume is set to zero, since a derived bar has no real trading behind it.

  • O and C: nearest tick
  • H up, L down
  • Clamp at zero; volume 0

Code: A Growing List

Derived bars can themselves have derived children, so the list grows while we walk it. This simplified sketch uses an index loop, not a range-for. A range-for over a vector you push back into invalidates iterators when it reallocates, which is undefined behaviour. The index loop stays valid, and the walk is naturally breadth first.

  • Simplified sketch
  • Index loop, not range-for
  • push_back may reallocate

Expiry Settlement

What Happens at Expiry

Contracts end, and the books must end with them. When settleExpired runs, the engine first cancels the contract's working orders. Then it finds a settlement price. For an option, that is intrinsic value computed from the underlying's last price. For a future, the underlying's last price, else its own last price, else its mark.

  • Cancel working orders
  • Option: intrinsic from underlying last
  • Future: underlying last, own last, mark

Synthetic Fills

Next, for every strategy holding a non-flat position, the engine generates a synthetic Fill at the settlement price. Its order id is kNoOrder, because no real order exists, and its commission is zero. Illustrative: a long call, settlement price intrinsic of forty. The position closes through the same fill path as any trade, so positions and P and L stay consistent.

  • One Fill per non-flat strategy
  • orderId = kNoOrder
  • Commission 0

A Known Limitation

Be honest about the edge. Settlement uses the first bar at or after expiry. If the data is sparse around that moment, the underlying price used may be slightly stale or later than the true expiry instant. It is a deliberate simplification of the design, and worth knowing when you read an expiry-day result.

  • Uses first bar at or after expiry
  • Sparse data: price may lag
  • Simplification, not exact

Curve and Paper Loop

The Equity Curve Budget

Step eight records equity points, one per simulated second, but a long run would make millions of points. So the curve is capped at four thousand. When it fills, the engine keeps only the even-indexed points and doubles the sampling interval. The curve always spans the whole run, at a resolution that adapts to its length.

  • One point per sim second
  • Cap: 4000 points
  • Keep even indices, double interval

Paper Mode: Same Pipeline, New Driver

Paper trading reuses the same pipeline, but time comes from the world, not from a file. First the engine replays warmup bars from the store. Then one SourceRunner thread per source posts BarEvents onto the bus. The engine thread loops on waitAndDrain with a fifty millisecond timeout, so it wakes both for bars and for the clock.

  • Replay warmup bars from the store
  • One SourceRunner thread per source
  • waitAndDrain(50ms)

Each Loop Turn

After each drain, the loop pumps the venue, runs squareOffTick with the larger of simulated now and the wall clock, and settles expiries about once a second. Using the wall clock matters: if no bar arrives, square-off time still comes. Without it, a quiet feed could leave positions open past the cutoff.

  • Pump the venue
  • squareOffTick(max(now, wall))
  • Settle about once a second

The Warmup Gate

One more safeguard. The submitGate rejects orders with the reason warmup until the instrument has a bar that ended after the run started. Replayed history is for building indicators, not for trading. An alpha cannot trade on a price that is hours old and was only loaded to prime its state.

  • Reject reason: warmup
  • Needs a bar ending after run start
  • History primes, never trades

Pitfalls and Recap

Pitfalls to Remember

Two pitfalls stand out. First, a range-for over a vector that you push back into, which is undefined behaviour when it reallocates. Second, letting alphas see a bar before fills have run, which is the lookahead trap. Both look harmless in code review, and both are prevented here by structure, not by discipline.

  • No range-for on a growing vector
  • Alphas never before fills
  • Structure beats discipline

Recap

Let us recap. Handlebar runs: settle expiries, filter and update the market, derive bars, advance the clock and let the venue fill, check equity, square off, run alphas, record equity. Venues queue reports and the pump drains them with a re-entrancy guard. Derived bars and expiry settlement keep options and futures consistent.

  • Eight steps, fixed order
  • Pull model with pump()
  • Derived bars, expiry fills

Next Episode and Subscribe

Everything an alpha does plugs into step seven of this pipeline. In the next episode we open the alpha framework: its interfaces, its gates, and how target positions become orders.

  • Next: the alpha framework
  • Interfaces, gates, targets
  • Subscribe for episode 7