Signal Combiner
Horizon includes a signal combination system for building composite alpha signals from multiple data sources. Core math is implemented in Rust for performance, with Python pipeline factories for ergonomic use in hz.run().
Signals are functions that extract a single float value (typically 0 to 1) from the current market context. The signal combiner merges multiple signals into a single composite score that can drive downstream quoting or sizing decisions.
Overview
Weighted Average hz.combine_signals() with "weighted_avg" method for importance-weighted combination.
Built-in Extractors 5 ready-to-use signal extractors: price, spread, momentum, flow, and imbalance.
EMA Smoothing hz.ema() for exponential smoothing of noisy signal values.
Time Decay hz.decay_weight() for exponential half-life weighting of stale data.
Core Functions
hz.combine_signals
Combine multiple weighted signal values into a single score.
import horizon as hz
score = hz.combine_signals(
signals = [( 0.6 , 1.0 ), ( 0.8 , 2.0 )], # (value, weight) pairs
method = "weighted_avg" ,
)
print ( f "Combined: { score :.4f} " ) # 0.7333 (weighted average)
Parameter Type Description signalslist[tuple[float, float]]List of (value, weight) pairs methodstrCombination method: "weighted_avg", "rank", or "zscore"
Methods:
"weighted_avg" - Weighted mean: sum(v*w) / sum(w). Zero-weight signals are filtered out.
"rank" - Rank-based: converts values to percentile ranks, then weighted average.
"zscore" - Z-score normalization: standardizes values, then maps back to 0-1 via sigmoid.
Returns 0.0 for empty input.
hz.ema
Compute the exponential moving average of a series.
result = hz.ema( values = [ 1.0 , 2.0 , 3.0 , 4.0 ], span = 3 )
print ( f "EMA: { result :.4f} " ) # 3.125
Parameter Type Description valueslist[float]Time series values spanintEMA span (alpha = 2/(span+1))
Returns 0.0 for empty input or zero span.
hz.zscore
Compute the z-score of a value given mean and standard deviation.
z = hz.zscore( value = 10.0 , mean = 5.0 , std = 2.0 )
print ( f "Z-score: { z :.4f} " ) # 2.5
Returns 0.0 if std is zero or value is NaN.
hz.decay_weight
Compute an exponential decay weight based on age and half-life.
w = hz.decay_weight( age_secs = 60.0 , half_life_secs = 60.0 )
print ( f "Weight: { w :.4f} " ) # 0.5 (one half-life = 50% weight)
Parameter Type Description age_secsfloatAge of the data point in seconds half_life_secsfloatHalf-life in seconds
Returns 0.0 for negative age or zero half-life.
Pipeline Factory: hz.signal_combiner
The signal_combiner() factory returns a pipeline function that evaluates and combines signals on each cycle.
import horizon as hz
combiner = hz.signal_combiner(
signals = [
hz.price_signal( "btc" , weight = 0.4 ),
hz.spread_signal( "book" , weight = 0.3 ),
hz.momentum_signal( "btc" , lookback = 20 , weight = 0.2 ),
hz.flow_signal( "btc" , window = 50 , weight = 0.1 ),
],
method = "weighted_avg" ,
smoothing = 10 , # EMA smoothing span (0 = disabled)
clip = ( 0.0 , 1.0 ), # Clamp output to this range
)
Parameters
Parameter Type Default Description signalslist[Signal]required Signals to combine methodstr"weighted_avg"Combination method smoothingint0EMA smoothing span (0 = disabled) cliptuple[float, float](0.0, 1.0)Output clamp range
If a signal raises an exception during evaluation, it is skipped (not propagated). This ensures robustness when feeds are temporarily unavailable.
Each extractor returns a Signal object with a name, fn, and weight.
hz.price_signal
Extracts the mid price from a feed. Returns the raw price (typically 0 to 1 for prediction markets).
sig = hz.price_signal( "btc" , weight = 0.4 )
Returns 0.0 if the feed is missing.
hz.spread_signal
Measures bid-ask tightness. Tighter spreads produce higher values (closer to 1.0).
sig = hz.spread_signal( "book" , weight = 0.3 )
Formula: 1.0 - min(spread / 0.20, 1.0) where spread = ask - bid.
hz.momentum_signal
Tracks rolling price momentum. Values above 0.5 indicate an uptrend, below 0.5 indicate a downtrend.
sig = hz.momentum_signal( "btc" , lookback = 20 , weight = 0.2 )
Maintains an internal price history. Returns 0.5 when insufficient data is available.
hz.flow_signal
Estimates buy/sell flow direction from price tick movements. Values above 0.5 indicate net buying pressure.
sig = hz.flow_signal( "btc" , window = 50 , weight = 0.1 )
Classifies each price tick as a buy (up-tick) or sell (down-tick) and computes the rolling ratio.
hz.imbalance_signal
Estimates orderbook imbalance from bid/ask prices. Values above 0.5 indicate buy-side pressure.
sig = hz.imbalance_signal( "book" , levels = 5 , weight = 0.3 )
Returns 0.5 when no bid/ask data is available.
Examples
Basic Signal Combination
import horizon as hz
def quoter ( ctx : hz.Context, score : float ) -> list[hz.Quote]:
"""Use the combined signal as fair value."""
spread = 0.04 + ( 1.0 - score) * 0.04 # Wider spread when uncertain
return hz.quotes( fair = score, spread = spread, size = 5 )
hz.run(
name = "signal_strategy" ,
markets = [ "election-winner" ],
feeds = { "book" : hz.PolymarketBook( "election-winner" )},
pipeline = [
hz.signal_combiner([
hz.price_signal( "book" , weight = 0.5 ),
hz.spread_signal( "book" , weight = 0.3 ),
hz.momentum_signal( "book" , lookback = 20 , weight = 0.2 ),
]),
quoter,
],
risk = hz.Risk( max_position = 100 ),
)
Custom Signal with Built-in Combiner
import horizon as hz
from horizon.signals import Signal
def my_model ( ctx ):
"""Custom probability model."""
price = ctx.feeds.get( "btc" , hz.context.FeedData()).price or 0.5
return min ( max (price * 1.1 - 0.05 , 0.0 ), 1.0 )
custom = Signal( name = "my_model" , fn = my_model, weight = 2.0 )
combiner = hz.signal_combiner([
custom,
hz.price_signal( "book" , weight = 1.0 ),
hz.spread_signal( "book" , weight = 0.5 ),
])
Signal + Kelly Sizing
import horizon as hz
hz.run(
name = "signal_kelly" ,
markets = [ "election-winner" ],
feeds = { "book" : hz.PolymarketBook( "election-winner" )},
pipeline = [
hz.signal_combiner([
hz.price_signal( "book" , weight = 0.4 ),
hz.momentum_signal( "book" , lookback = 20 , weight = 0.3 ),
hz.spread_signal( "book" , weight = 0.3 ),
], smoothing = 10 ),
hz.kelly_sizer( fraction = 0.25 , bankroll = 1000 ),
quoter,
],
)
Signal + Market Maker
import horizon as hz
hz.run(
name = "signal_mm" ,
markets = [ "election-winner" ],
feeds = { "book" : hz.PolymarketBook( "election-winner" )},
pipeline = [
hz.signal_combiner([
hz.price_signal( "book" , weight = 0.5 ),
hz.imbalance_signal( "book" , levels = 5 , weight = 0.3 ),
hz.flow_signal( "book" , window = 30 , weight = 0.2 ),
]),
hz.market_maker( feed_name = "book" , gamma = 0.5 , size = 5.0 ),
],
risk = hz.Risk( max_position = 100 ),
)
Signal Dataclass
The Signal dataclass is used to define custom signals:
from horizon.signals import Signal
Signal(
name = "my_signal" , # Display name
fn = my_function, # Callable[[Context], float]
weight = 1.0 , # Combination weight
)
Field Type Description namestrSignal identifier (for logging) fnCallable[[Context], float]Function that extracts a float from context weightfloatRelative weight in combination