---
title: "Signal-Driven Market Maker"
description: "Combine multiple signals with adaptive spread and Kelly sizing."
icon: "wave-pulse"
---

<Note>**Pro Feature.** Requires a Pro or Ultra subscription. [Get started at api.mathematicalcompany.com](https://api.mathematicalcompany.com)</Note>

A market maker that combines 5 built-in signals plus a custom signal to drive fair value estimation, then applies adaptive spread and Kelly-based sizing.

## Full Code

```python
"""Signal-driven market maker with 5 signals + custom."""

import horizon as hz
from horizon.context import FeedData


def expiry_pressure(ctx: hz.Context) -> float:
    """Custom signal: markets approaching expiry tend to converge."""
    days = ctx.params.get("days_to_expiry", 30)
    if days < 7:
        return 0.8   # strong convergence pressure
    elif days < 14:
        return 0.3
    return 0.0


# Combine 5 built-in signals + custom
combiner = hz.signal_combiner(
    signals=[
        hz.price_signal(feed_name="polymarket"),
        hz.imbalance_signal(feed_name="polymarket"),
        hz.spread_signal(feed_name="polymarket"),
        hz.momentum_signal(feed_name="polymarket", lookback=20),
        hz.flow_signal(feed_name="polymarket"),
        hz.Signal(name="expiry", fn=expiry_pressure, weight=0.10),
    ],
    method="weighted_avg",
)


def quoter(ctx: hz.Context, fair: float) -> list[hz.Quote]:
    """Adaptive spread based on signal confidence."""
    # Signal confidence is stored by the combiner
    confidence = ctx.params.get("signal_confidence", 0.5)

    # High confidence -> tight spread, low confidence -> wide spread
    base_spread = 0.02
    spread = base_spread + (1.0 - confidence) * 0.06

    # Kelly sizing from the combined signal
    book = ctx.feeds.get("polymarket", FeedData())
    market_price = book.price if book.price > 0 else 0.50

    size = hz.kelly_size(
        prob=fair,
        market_price=market_price,
        bankroll=5000.0,
        fraction=0.25,
        max_size=30.0,
    )

    if size <= 0:
        return []
    return hz.quotes(fair, spread, size=size)


hz.run(
    name="signal_driven_mm",
    markets=["election-winner"],
    feeds={
        "polymarket": hz.PolymarketBook("election-winner"),
    },
    pipeline=[combiner, quoter],
    risk=hz.Risk(max_position=100, max_drawdown_pct=5),
    params={"days_to_expiry": 21},
    interval=1.0,
    mode="paper",
)
```

## How It Works

1. **5 built-in signals** extract features from the live feed:
  - `price_signal` - mid-price level
  - `imbalance_signal` - bid/ask size imbalance
  - `spread_signal` - bid-ask spread width (liquidity proxy)
  - `momentum_signal` - short-term price trend
  - `flow_signal` - net trade flow direction
2. **Custom signal** (`expiry_pressure`) is wrapped in an `hz.Signal` with a name, callable, and weight
3. **`signal_combiner()`** aggregates all signals into a single fair value using weighted average
4. **Quoter** adjusts spread inversely to confidence and sizes via Kelly

## Combination Methods

The `method` parameter controls how signals are merged:

```python
# Weighted average (default): weighted sum of signal values
combiner_avg = hz.signal_combiner(signals=[...], method="weighted_avg", weights=[...])

# Rank-based: convert signals to ranks, then average
combiner_rank = hz.signal_combiner(signals=[...], method="rank")

# Z-score: standardize each signal, then combine
combiner_zscore = hz.signal_combiner(signals=[...], method="zscore")
```

## Run It

```bash
python examples/signal_driven_mm.py
```

## Extending

Add the Binance BTC feed as an additional signal source:

```python
hz.run(
    name="signal_mm_btc",
    markets=["btc-100k"],
    feeds={
        "polymarket": hz.PolymarketBook("btc-100k"),
        "btc": hz.BinanceWS("btcusdt"),
    },
    pipeline=[
        hz.signal_combiner(
            signals=[
                hz.price_signal(feed_name="polymarket"),
                hz.price_signal(feed_name="btc"),
                hz.momentum_signal(feed_name="btc", lookback=50),
            ],
            method="weighted_avg",
            weights=[0.4, 0.4, 0.2],
        ),
        quoter,
    ],
    risk=hz.Risk(max_position=100),
    interval=0.5,
    mode="paper",
)
```

See [Signals](/signals) for the full signal reference.
