Skip to content

Signal Engines

A signal engine consumes market data and generates trading insights. Signal engines operate in a strictly read-only capacity: they publish insights but are prohibited from submitting, modifying, or canceling orders.


Insight Schema

Every insight produced by a signal engine follows this schema:

Field Type Required Description
symbol string Yes Instrument identifier (kernel canonical ID)
direction enum Yes Up, Down, or Flat
magnitude float Yes Expected magnitude, normalized to [0.0, 1.0]
confidence float Yes Confidence in the direction prediction, [0.0, 1.0]
source_model string Yes Name of the generating model/strategy
period duration Yes Expected time horizon
weight float No Relative weight across signal engines (default: 1.0)
generated_at timestamp Yes Plugin wall-clock time when insight was generated
close_at timestamp Yes Expiration time. Must equal generated_at + period
  • magnitude is model-specific in interpretation but must stay within [0.0, 1.0].
  • confidence expresses certainty in the direction prediction.
  • close_at must always equal generated_at + period. Expired insights should be disregarded by downstream consumers.

Order Prohibition

Signal engines must not call any order-related sidecar methods. The sidecar rejects and logs violations.

  • Calling submit_order, cancel_order, modify_order, or any OMS/EMS method raises ACL_DENIED.
  • Publishing to platform.oms.* or platform.ems.* topics is rejected with ACL_DENIED.
  • Signal engines can only publish to platform.signal.* topics.

This separation ensures that signal generation and order execution remain decoupled. Portfolio optimizers consume insights and generate target allocations; the OMS translates those into orders.


Warmup Protocol

Signal engines typically need historical data to initialize indicators, moving averages, or model state before producing meaningful insights.

Declaration

Declare warmup requirements in the engine configuration:

class MomentumSignal(SignalEngine):
    warmup_bars = 200        # replay 200 bars before going live
    # OR
    warmup_period = "90d"    # replay 90 days of data before going live

If both warmup_bars and warmup_period are specified, the sidecar uses whichever produces a longer replay window.

Behavior During Warmup

  1. The sidecar replays historical data through on_data with data_window.is_warmup = True.
  2. Any insights generated during warmup are not published to the bus.
  3. The plugin must not transition to RUNNING until warmup completes.
  4. After warmup, the engine goes live and all subsequent insights are published.

DataWindow

The on_data callback receives a DataWindow that bundles all available data for the current bar.

Method / Property Returns Description
bars(symbol, lookback) list[Bar] Last N bars for a symbol
latest(symbol) Bar Most recent bar for a symbol
symbols list[str] All symbols with available data
is_warmup bool True during warmup replay
current_date date Current bar date
def on_data(self, data: DataWindow):
    for symbol in data.symbols:
        bars = data.bars(symbol, lookback=20)
        if len(bars) < 20:
            continue
        latest = data.latest(symbol)
        # ... compute signal

Local Mode

In local mode, the SignalEngine base class provides a run() method that replays loaded bar data through on_data. Use warmup=N to replay N bars before the engine starts generating publishable insights.

from meridian import SignalEngine, Insight

class MeanReversionSignal(SignalEngine):
    warmup_bars = 50

    def on_data(self, data):
        insights = []
        for symbol in data.symbols:
            bars = data.bars(symbol, lookback=50)
            if len(bars) < 50:
                continue

            closes = [b.close for b in bars]
            mean = sum(closes) / len(closes)
            std = (sum((c - mean) ** 2 for c in closes) / len(closes)) ** 0.5
            latest = data.latest(symbol)
            z_score = (latest.close - mean) / std if std > 0 else 0

            if abs(z_score) > 2.0:
                insights.append(Insight(
                    symbol=symbol,
                    direction="Down" if z_score > 0 else "Up",
                    magnitude=min(abs(z_score) / 4.0, 1.0),
                    confidence=min(abs(z_score) / 3.0, 1.0),
                    source_model="mean_reversion",
                    period="5d",
                ))
        return insights


engine = MeanReversionSignal()
engine.load_bars("AAPL", source="data/aapl_daily.csv")
engine.load_bars("MSFT", source="data/msft_daily.csv")
engine.run(warmup=50)

Output during run:

[warmup] Replaying 50 bars...
[live] 2026-03-25: 0 insights
[live] 2026-03-26: 1 insight  AAPL direction=Down magnitude=0.62 confidence=0.73
[live] 2026-03-27: 0 insights

Deployed Mode

When deployed, the signal engine registers with the sidecar and receives live data events. The sidecar manages subscriptions, warmup replay, and insight publication. Connected mode (real-time data and live execution) is coming in a future release.

class LiveMomentumSignal(SignalEngine):
    warmup_bars = 200
    subscribe_symbols = ["AAPL", "MSFT", "GOOGL", "AMZN"]

    def on_data(self, data):
        insights = []
        for symbol in data.symbols:
            bars = data.bars(symbol, lookback=200)
            if len(bars) < 200:
                continue
            # ... compute momentum signal
            insights.append(Insight(
                symbol=symbol,
                direction="Up",
                magnitude=0.7,
                confidence=0.85,
                source_model="momentum_200d",
                period="10d",
            ))
        return insights

The sidecar publishes insights to platform.signal.{instance_id}.alpha. The Signal Pool Manager aggregates insights from multiple engines before forwarding to Portfolio Optimizers.


Bus Topics

Topic Direction Description
platform.signal.{instance_id}.alpha Publish Insights from this engine
platform.data.eod.* Subscribe End-of-day price data
platform.data.ohlcv.* Subscribe Intraday OHLCV bars