> ## Documentation Index
> Fetch the complete documentation index at: https://mathematicalcompany.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Signal Combiner

> Combine multiple alpha signals with weighted averaging, rank, and z-score methods. Built-in extractors for price, spread, momentum, flow, and orderbook imbalance.

# 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()`.

<Note>
  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.
</Note>

## Overview

<CardGroup cols={2}>
  <Card title="Weighted Average" icon="scale-balanced">
    `hz.combine_signals()` with `"weighted_avg"` method for importance-weighted combination.
  </Card>

  <Card title="Built-in Extractors" icon="wand-magic-sparkles">
    5 ready-to-use signal extractors: price, spread, momentum, flow, and imbalance.
  </Card>

  <Card title="EMA Smoothing" icon="chart-line">
    `hz.ema()` for exponential smoothing of noisy signal values.
  </Card>

  <Card title="Time Decay" icon="clock">
    `hz.decay_weight()` for exponential half-life weighting of stale data.
  </Card>
</CardGroup>

***

## Core Functions

### hz.combine\_signals

Combine multiple weighted signal values into a single score.

```python theme={null}
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                                                   |
| --------- | --------------------------- | ------------------------------------------------------------- |
| `signals` | `list[tuple[float, float]]` | List of `(value, weight)` pairs                               |
| `method`  | `str`                       | Combination 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.

```python theme={null}
result = hz.ema(values=[1.0, 2.0, 3.0, 4.0], span=3)
print(f"EMA: {result:.4f}")  # 3.125
```

| Parameter | Type          | Description                   |
| --------- | ------------- | ----------------------------- |
| `values`  | `list[float]` | Time series values            |
| `span`    | `int`         | EMA 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.

```python theme={null}
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.

```python theme={null}
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_secs`       | `float` | Age of the data point in seconds |
| `half_life_secs` | `float` | Half-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.

```python theme={null}
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                       |
| ----------- | --------------------- | ---------------- | --------------------------------- |
| `signals`   | `list[Signal]`        | required         | Signals to combine                |
| `method`    | `str`                 | `"weighted_avg"` | Combination method                |
| `smoothing` | `int`                 | `0`              | EMA smoothing span (0 = disabled) |
| `clip`      | `tuple[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.

***

## Built-in Signal Extractors

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).

```python theme={null}
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).

```python theme={null}
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.

```python theme={null}
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.

```python theme={null}
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.

```python theme={null}
sig = hz.imbalance_signal("book", levels=5, weight=0.3)
```

Returns 0.5 when no bid/ask data is available.

***

## Examples

### Basic Signal Combination

```python theme={null}
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

```python theme={null}
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

```python theme={null}
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

```python theme={null}
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:

```python theme={null}
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                                 |
| -------- | ---------------------------- | ------------------------------------------- |
| `name`   | `str`                        | Signal identifier (for logging)             |
| `fn`     | `Callable[[Context], float]` | Function that extracts a float from context |
| `weight` | `float`                      | Relative weight in combination              |
