The Pipeline

What happens on every tick, from data to venue

Every tick follows the same path. This page walks through one concrete tick so you can see exactly what each layer receives, what it returns, and how it feeds the next one.

The chain

Data → Features → Strategy → Portfolio → Execution → Risk → Venue

Each layer takes the output of the previous one and produces input for the next. Here’s one tick for AAPL, traced end to end.


Step-by-step example

Data source emits a bar

The data source yields one price bar per market per tick.
python
# What the data source produces:
bar = Bar(market_id="AAPL", open=185.10, high=185.50, low=184.80, close=185.30, timestamp=t)

Output → next layer gets: a price snapshot for each market in the universe.

FeatureStore computes features

The store appends the new price to AAPL's rolling history, then computes whatever features the strategy requested.
python
# The strategy declared these features:
features = {
    "z": Zscore(window=20),
    "vol": RealizedVol(window=60),
}

# After this tick, the store returns:
f.z["AAPL"]   = -2.4    # z-score of 20-day log returns
f.vol["AAPL"] = 0.18    # annualized realized vol

Input: price bar. Output → next layer gets: a namespace of computed feature values per market.

Strategy reads features, returns Signals

Your strategy receives the features and the universe. It decides: is there an opinion worth expressing?
python
def evaluate(self, f, universe):
    signals = []
    for market in universe:
        z = f.z[market.id]          # -2.4 for AAPL
        vol = f.vol[market.id]      # 0.18
        if abs(z) > 2.0:
            signals.append(
                Signal.from_score(
                    market=market,
                    score=-z,                    # +2.4 → go long
                    edge_per_stdev=20,           # 20 bps edge per σ
                    horizon="3d",
                    expected_expected_vol_bps=vol * 100,  # 18 bps
                )
            )
    return signals
python
# What the strategy returns for this tick:
[Signal(
    market_id="AAPL",
    direction=Direction.Increase,    # long
    confidence=0.72,                 # derived from |z|
    expected_edge_bps=48.0,          # 2.4 × 20
    expected_expected_vol_bps=18.0,
    horizon=timedelta(days=3),
)]

Input: feature values + universe. Output → next layer gets: a list of Signal objects (can be empty).

Portfolio sizer converts Signals → TargetPositions

The sizer takes all active signals, the current portfolio state, and cash. It decides how many dollars to allocate.
python
# Kelly formula: f* = edge / vol²
# f* = 48 bps / (18 bps)² = 48 / 324 = 0.148
# Scaled: 0.148 × kelly_fraction(0.25) × confidence(0.72) = 0.0267
# Target notional: 0.0267 × equity($100,000) = $2,670

# What the sizer returns:
[TargetPosition(
    market_id="AAPL",
    target_notional_usd=2670.0,      # go long $2,670 of AAPL
    urgency=Urgency.Patient,
)]

Input: list of Signal + current positions + cash + constraints. Output → next layer gets: a list of TargetPosition objects (USD notional, not shares).

Executor converts TargetPositions → OrderActions

The executor translates dollar targets into concrete orders. It knows the asset class, the current position, and the current price.
python
# Current position in AAPL: 0 shares
# Target: $2,670 long
# Current price: $185.30
# Shares needed: $2,670 / $185.30 = 14 shares
# Delta vs current: 14 - 0 = 14 shares to buy

# What the executor returns:
[OrderAction.place(
    market_id="AAPL",
    side="buy",
    quantity=14,
    price=185.30,           # limit at last price
    order_type="limit",
)]

Input: TargetPosition + current position + current price. Output → next layer gets: a list of OrderAction objects (concrete orders with side, quantity, price).

Risk engine checks each OrderAction → Decision

Every order passes through 8 pre-trade checks. Each check returns a `Decision`: pass, reject, or resize.
python
# Check 1: Kill switch?        → pass (not triggered)
# Check 2: Drawdown halt?      → pass (equity is at peak)
# Check 3: NaN/Inf sanity?     → pass (price=185.30, qty=14)
# Check 4: Price in range?     → pass (0.01 ≤ 185.30 ≤ 100,000)
# Check 5: Notional under cap? → pass ($2,594 < $50,000 cap)
# Check 6: Rate limit?         → pass (first order this second)
# Check 7: Max orders/tick?    → pass (1 < 50)
# Check 8: Stop-loss cooldown? → pass (no recent stop on AAPL)

# Result:
Decision.pass_()   # order goes through unchanged

If any check fails, the order is either rejected (Decision.reject("reason")) or resized (Decision.resize(new_qty)).

Input: OrderAction + portfolio state + risk config. Output → next layer gets: the same OrderAction if passed, or nothing if rejected.

Venue receives the order

The passed order goes to the paper venue (in backtest) or a real broker (in live).
python
# Paper venue receives:
#   buy 14 AAPL @ 185.30 limit
#
# Since this is a limit order at the current price,
# it fills immediately:
Fill(market_id="AAPL", side="buy", quantity=14, price=185.30, fee=0.13)

The fill flows back to the ledger, which updates the position and cash:

python
# After fill:
#   Position: AAPL long 14 shares @ $185.30 avg
#   Cash: $100,000 - $2,594.20 - $0.13 fee = $97,405.67

Input: OrderAction. Output: Fill → updates ledger → updates equity for next tick.

Metrics snapshot

End of tick. The metrics collector records the current state.
python
# Recorded:
#   equity = $100,000.00 (no price move yet, so unrealized = 0)
#   cash = $97,405.67
#   positions = 1
#   drawdown = 0.0%

The full type chain

StepTakesReturns
Data-Bar(market_id, price, timestamp)
Featuresprice historyf.z["AAPL"] = -2.4, f.vol["AAPL"] = 0.18
Strategyfeatures + universelist[Signal] (direction, confidence, edge, vol, horizon)
Portfoliosignals + positions + cashlist[TargetPosition] (market_id, target USD notional)
Executiontarget position + current position + pricelist[OrderAction] (side, quantity, price, order type)
Riskorder action + state + configDecision (pass / reject / resize)
Venueorder actionFill (side, quantity, price, fee) → ledger

Each type is a frozen dataclass. You can inspect any of them at any point.

Where you plug in

Most of the time you only touch one layer:

  • Write a strategy: implement evaluate(f, universe) → list[Signal]
  • Custom sizing: implement optimize(signals, positions, cash) → list[TargetPosition]
  • Custom risk: add StopLoss, DrawdownGuard, or write a custom check
  • Custom features: subclass Feature with a compute() method

The rest of the pipeline stays the same.

Mode switching

The pipeline has one code path. Only two things change between modes:

ModeData sourceVenue
backtestHistorical / syntheticPaper
paperLive feedPaper
shadowLive feedReal (log-only)
liveLive feedReal

If your backtest works, paper works. If paper works, live works. No code forks.

Next