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 |
magnitudeis model-specific in interpretation but must stay within [0.0, 1.0].confidenceexpresses certainty in thedirectionprediction.close_atmust always equalgenerated_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 raisesACL_DENIED. - Publishing to
platform.oms.*orplatform.ems.*topics is rejected withACL_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¶
- The sidecar replays historical data through
on_datawithdata_window.is_warmup = True. - Any insights generated during warmup are not published to the bus.
- The plugin must not transition to
RUNNINGuntil warmup completes. - 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 |