Kestrel Alpha Framework: Interfaces, Gates and Target Positions

How does one C++ strategy object run unchanged in backtest, paper and live trading? Through a narrow interface, a chain of gates, and a habit of trading to targets instead of deltas.

Intro

One Object, Three Modes

Imagine a strategy object that has no idea whether it is running in a backtest, in paper trading, or against a real broker. It cannot find out, because it was never given the means. That is not an accident. It is the central design decision of the Kestrel alpha framework.

  • Same alpha everywhere
  • It cannot tell which mode

Today's Roadmap

In the previous episode we followed the per-bar pipeline, which ends with alphas being called. Today we open that last step. We will meet the IAlpha interface, the AlphaContext window, the registry, the chain of gates every order must pass, and a small function called tradeToTarget.

  • IAlpha and AlphaContext
  • Registry and gates
  • tradeToTarget

Framework, Not Strategy

One boundary first. This playlist teaches the framework, not the logic of any individual alpha. Whether a particular idea is any good belongs to the separate strategy playlist, and nothing here is financial advice or a claim that anything works.

  • Framework is in scope
  • Alpha logic is out of scope

The IAlpha Interface

The Isolation Problem

The problem is simple to state. A strategy needs data and needs to place orders, but if it can reach the broker, the clock or the positions table directly, it can never be tested offline and it can behave differently between modes. So the alpha is given a small interface and nothing else.

  • No direct broker access
  • No wall clock
  • One narrow window

IAlpha: Five Hooks

IAlpha has five methods. onStart and onBar are pure virtual, so every alpha must implement them. onFill, onOrderUpdate and onStop have empty default bodies, so an alpha only overrides what it cares about. And there is a virtual destructor, so the host can delete an alpha through the base pointer safely.

  • onStart, onBar are required
  • Three optional hooks
  • Virtual destructor

The Alpha Lifecycle

The lifecycle is easy to picture. onStart is called once, where the alpha resolves its symbol and subscribes. Then onBar is called for each bar, interleaved with onFill and onOrderUpdate events as the gateway reports back. Finally onStop is called at the end of the run.

  • onStart once
  • onBar every bar
  • onStop at the end

AlphaContext: The Only Window

The Only Window

AlphaContext is the only window an alpha has onto the world. Read the list again: it contains things to look at, and exactly two things to act with, submit and cancel.

  • Read: now, lastBar, position
  • Act: submit and cancel
  • Also: volatility, log

Defaults Protect Fakes

Some capabilities are not pure virtual. dailyHistory, quote, and the event calendar calls, events and isEventDay, have default implementations. The calendar covers things like RBI meetings, the budget, CPI releases, expiries and holidays. The reason for defaults is practical: when a new capability is added, every existing test fake still compiles and still works.

  • dailyHistory, quote, events
  • Event days: RBI, budget, CPI
  • New method never breaks fakes

Same Alpha, Any Context

Because AlphaContext is an interface, a unit test can hand an alpha a fake context, feed it scripted bars, and inspect the orders it submits. No engine, no files, no sockets. And the very same alpha object runs in backtest, paper and live, because each mode simply supplies a different implementation behind the same window.

  • Fake contexts for tests
  • Same object in all modes

Registry and the Reference Pattern

From JSON to Object

How does a config file turn into an alpha object? Through a registry. AlphaRegistry is an alias for FactoryRegistry of IAlpha, constructed from a const nlohmann json reference. The type string in alpha dot json selects a factory, and the factory receives the params.

  • FactoryRegistry of IAlpha
  • Type string selects factory

Built-ins and Errors

registerBuiltinAlphas adds three reference alphas: sma_crossover, zscore_mean_reversion and delta_hedged_option. After those, it calls one register function per research family. If you ask for a type that does not exist, the error lists every known type, so a typo in a config costs you seconds, not an afternoon.

  • Three reference alphas
  • One register call per family
  • Unknown type lists all types

The Four-Step Pattern

Every reference alpha follows the same four steps. Parse and validate parameters in the constructor, throwing invalid_argument, which the config layer turns into a ConfigError. Update indicators in onBar. Then compute a target position and call tradeToTarget.

  • Validate in constructor
  • Subscribe in onStart
  • Indicators in onBar
  • Target, then tradeToTarget

Fail Before Bar One

Why validate in the constructor? Because a bad parameter, say a slow moving average shorter than the fast one, should fail before the first bar, with a clear message. Finding that out at ten in the morning of a paper session, halfway through, is exactly the bug this prevents.

  • Bad params fail at startup
  • Not mid-session

The Gate Chain

Rules Live in the Host

Now the most important safety idea in the framework. If an alpha calls submit and the order goes straight to the gateway, then every alpha must independently remember to respect warmup, to respect square-off, and to respect position limits. Some will forget. So those rules live in the host, once, where no alpha can skip them.

  • Not in every alpha
  • Enforced exactly once

The Submit Gate Chain

Here is the chain inside AlphaHost submit. First: is this alpha active? Second: the engine submit gate, which checks that the engine is not stopping, that the instrument is not already settled, and that paper warmup is over. Third: the position gate. Fourth: gateway submit, where the OMS normalises the order and the RiskManager checks it.

  • Alpha active?
  • Engine submitGate
  • positionGate
  • gateway.submit

Bouncing Off Warmup

Watch an order during paper warmup. The alpha is active, so it passes door one. At door two, the engine gate sees that paper warmup is still running, and the order bounces back with a rejection. It never reaches the gateway, so the OMS and the risk manager never even see it.

  • Door one: passes
  • Door two: warmup blocks
  • Gateway never sees it

Inside positionGate

The position gate has several jobs. It stops a blocked alpha, it respects the square-off state, and it enforces the maxPositionLimit cap. The cap can optionally clip an order down to the limit instead of rejecting it. And one rule is crucial: orders that reduce a position always pass, so a limit can never trap you in a position you want to leave.

  • Blocked and square-off states
  • maxPositionLimit with clip option
  • Reducing orders always pass

Clipping Worked Example

A numeric example. Suppose the limit is 100 and the position is 80. An alpha wants to buy 50. Without clipping, the order is rejected. With clip to limit, it is reduced to buy 20, landing exactly at 100. Now suppose the position is 120 and the alpha sells 50. That reduces the position, so it passes untouched.

  • Limit 100, position 80
  • Buy 50 clipped to 20
  • Sell always passes

Cancel Touches Only Yours

Cancel follows a similar principle of ownership. An alpha can only cancel its own orders. Several alphas may share one strategy and one book, and one of them must not be able to pull another's working orders. Risk limits and the details of normalisation come after the gates, and belong to the next episode.

  • Own orders only
  • Risk rules come in ep 8

Target Positions

Deltas Pile Up

Now the idea that makes alphas robust. A beginner writes: signal says buy, so submit a buy of ten. Run that on every bar and the signal stays true for twenty bars, and you have bought two hundred. The code expresses an action, but what the strategy actually means is a state: I want to be long ten.

  • Signal persists across bars
  • Buying each bar stacks up
  • Intent is a state, not an action

Target vs Position

The fix is to compute a target position, then let a shared helper work out the difference. The target line is what the alpha wants. The position is what it has. The order is simply the gap between them. If nothing changes, the gap is zero and nothing is sent.

  • Alpha computes the target
  • Helper sends the gap
  • Zero gap means silence

tradeToTarget

Here is tradeToTarget, verbatim. It takes the context, an instrument id, a target quantity, and a minimum difference. It returns an optional SubmitResult: nothing if no order was needed. The side comes from the sign of the difference, the size from its absolute value, and the order is a market order, good till cancelled.

  • Returns optional SubmitResult
  • Side from sign of diff
  • Quantity is abs(diff)

Line Two Makes It Idempotent

Look at the very first line of the body. If there are open orders for this instrument, return nothing. That single line makes the function idempotent. While an order is working, calling it again does nothing, so it is safe to call on every bar, without the alpha tracking pending state itself.

  • Open order means return nullopt
  • Safe to fire every bar

minDiff and Epsilon

The next line computes diff as target minus current position, then returns nothing if the absolute difference is below minDiff, or below kQtyEpsilon. The minDiff is a dead band that avoids tiny rebalancing orders. The epsilon guards against floating point residue, because quantities are doubles, and a difference of one e minus fifteen should never become an order.

  • minDiff avoids tiny orders
  • kQtyEpsilon guards doubles

Worked Example

Bar One Setup

Let us run it. The alpha wants a target of ten. The current position is minus five, so the alpha is short five. Say minDiff is one. On this bar, openOrders is empty, so the first check passes.

  • Target 10
  • Position minus 5
  • No open orders

Buy 15

The difference is ten minus minus five, which is fifteen. Fifteen is larger than minDiff and larger than the epsilon, so we proceed. Diff is positive, so the side is Buy, and the quantity is the absolute value, fifteen. One market order to buy fifteen goes through the gates.

  • 10 minus minus 5 equals 15
  • Side Buy, quantity 15

Bar Two: Nothing

On the next bar, suppose the market order has not filled yet. The alpha computes the same target and calls tradeToTarget again. This time openOrders is not empty, so the function returns nothing at line two. Without that line, we would send another buy of fifteen and end up at plus twenty-five.

  • Order still working
  • Early return, no duplicate

Bar Three: Converged

On the third bar the fill has arrived. The position is now plus ten, and no orders are open. The difference is ten minus ten, zero, which is below the epsilon, so again nothing is sent. The system has converged and stays quiet until the target changes.

  • Position equals target
  • Diff zero, no order

Building Blocks

The Toolbox

Alphas should not each reimplement the same maths, so the framework ships shared building blocks. indicators dot hpp has SimpleMovingAverage and RollingStats. intraday dot hpp has the time-aware tools. option_legs dot hpp handles multi-leg option work, and spread_book dot hpp handles spreads. Let us tour them.

  • indicators.hpp
  • intraday.hpp
  • option_legs.hpp
  • spread_book.hpp

IST-Aligned Resampling

BarResampler turns one-minute bars into wider bars, using IST-aligned buckets. The bucket start is floorDiv of timestamp plus the IST offset, by the width, times the width, minus the IST offset. Why shift by five and a half hours? So that a fifteen-minute bucket starts at 09:15 Indian time rather than at a boundary in UTC. flushIfStale emits a bucket that would otherwise wait forever for its next bar.

  • Buckets align to IST
  • floorDiv handles negatives
  • flushIfStale emits late bucket

Killing Floating-Point Drift

Rolling statistics can be updated incrementally by adding the new value and subtracting the old one. But in floating point, tiny errors accumulate over millions of updates and drift. So WindowStats recomputes exactly from the window when the window is at most one thousand bars, which removes drift where it is cheap to do so.

  • Incremental sums drift
  • Exact recompute up to 1000

VWAP, EMA, ATR, ADX, OLS

The same file also provides SessionVwap, which resets each session, plus Ema, Atr, Adx, and olsFit, an ordinary least squares fit. These are plain, deterministic structures with no hidden clock, so they behave identically in a test and in a live process.

  • SessionVwap resets per session
  • Ema, Atr, Adx, olsFit
  • No hidden clock

syntheticFuture

option_legs dot hpp has OptionChain, which tracks freshness so that a stale quote is not treated as current. It also provides syntheticFuture: for each strike, compute K plus call minus put, then take the mean. With a strike of 22,000, a call at 310 and a put at 290, that strike implies a forward of 22,020. Averaging across strikes smooths noise.

  • K plus C minus P per strike
  • Mean across fresh strikes
  • Example: 22,020

LegBook Rolls Back

LegBook handles multi-leg orders. Suppose a hedged position needs three legs, and the first two are sent but the third is rejected. Leaving those two filled would leave a position that is only half-hedged. So LegBook closes the already-sent legs if a later leg is rejected. This is the pitfall of leaving one leg filled, handled in one place.

  • Legs sent in sequence
  • Rejection closes earlier legs
  • No half-hedged state

Session Windows

spread_book dot hpp offers SpreadBook, LegCostModel and SessionWindow. A SessionWindow encodes the daily schedule, for example entries allowed from 09:20 to 11:00, exit starting at 15:10, and everything flat by 15:12. Indian intraday sessions run in IST, and encoding the schedule once avoids every alpha hand-rolling its own clock logic.

  • Entry 09:20 to 11:00
  • Exit 15:10, flat 15:12

Pitfalls and Recap

Pitfalls and Their Fixes

Let us collect the pitfalls. First, computing deltas instead of targets, which stacks positions. Second, trading before warmup ends, which the engine gate now blocks. Third, leaving one leg filled when a later leg fails, which LegBook unwinds. Each is prevented by structure, not by remembering.

  • Deltas: use targets
  • Warmup: engine gate
  • Half-hedge: LegBook

Recap

To recap: an alpha implements IAlpha and sees the world only through AlphaContext, so the same object runs in tests, backtests, paper and live. The registry builds it from json. Every order passes four doors. And tradeToTarget turns a wanted position into at most one idempotent order. As always, this is education, not advice.

  • IAlpha plus AlphaContext
  • Four-door gate chain
  • Targets, idempotent orders

Next: OMS and Risk

We stopped at the last door, gateway submit. In episode 8 we go through it: how the OMS normalises an order and how the RiskManager checks it before anything is sent.

  • Episode 8: OMS and Risk
  • Subscribe for more