Skip to main content
Pro Feature. Requires a Pro or Ultra subscription. Get started at api.mathematicalcompany.com
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

"""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
  1. Custom signal (expiry_pressure) is wrapped in an hz.Signal with a name, callable, and weight
  2. signal_combiner() aggregates all signals into a single fair value using weighted average
  3. Quoter adjusts spread inversely to confidence and sizes via Kelly

Combination Methods

The method parameter controls how signals are merged:
# 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

python examples/signal_driven_mm.py

Extending

Add the Binance BTC feed as an additional signal source:
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 for the full signal reference.