BANGER
Product status: Banger’s public beta is paper-only. References to live execution are future-looking. Venue rules and legal claims can change; verify current primary sources for your jurisdiction.
2026-07-30 · 4 min read · reviewed 2026-10-14

Building a Mean-Reversion Bot Step by Step

A working prototype should separate a measurable feature from the claim that the feature predicts anything. This walkthrough implements an intentionally simple paper-only rule using Banger 0.1.7. It supplies actual callback, admission and simulated-close code; its chosen window, threshold and size are illustrative fixture inputs, not recommended trading settings.

Mean reversion asks whether a deviation from a defined baseline tends to shrink over a later window. A lower quote can also reflect durable new information. The old average is not fair value, and a signal threshold cannot guarantee a profitable or available exit.

1. Choose the exact instrument and units

Select one current open Kalshi binary ticker and read its payout and determination rules. The hosted binary path should not be assumed to admit a scalar player market. Replace the placeholder before a run and preserve the selected identifier with the strategy version.

The code uses valid bid and ask observations to form a midpoint in dollar units. Missing, crossed, non-finite, wrong-market and out-of-order inputs are rejected. A midpoint is a feature input, not an executable entry or sale price. Three successive observations are not three equal-duration periods; record the actual sampling procedure.

2. Make the earlier baseline explicit

The previous three eligible midpoints form the toy baseline. The latest observation is excluded until after its deviation is calculated. There is no universal best lookback, two-standard-deviation threshold, spread cutoff or half-life exit. Investigate the chosen procedure on appropriate data instead of importing those numbers as facts.

3. Implement a paper-only bounded prototype

The class attempts one $1-notional YES acquisition when its illustrative deviation condition holds, then one close if the chosen return condition occurs. The request ceiling prevents repeated entry cycles. It does not set worker lifetime or guarantee that a holding is flattened. Actual position records, not an assumed local balance, determine whether a held YES exists.

from collections import deque
from decimal import Decimal
from statistics import mean
from banger import Strategy, RiskEnvelope, signals
from banger_adapter_kalshi import Kalshi


class ToyMeanReversionPaper(Strategy):
    name = "toy-mean-reversion-paper"
    venues = [Kalshi()]
    universe = signals.kalshi_markets(
        status="open", tickers=["REPLACE_WITH_CURRENT_OPEN_TICKER"]
    )
    risk = RiskEnvelope(max_position_usd=Decimal("2"),
                        max_daily_loss_usd=Decimal("1"),
                        max_open_positions=1, max_orders_per_minute=2)

    def on_start(self, ctx):
        self._history = deque(maxlen=3)
        self._last_time = None
        self._market_id = None
        self._entered = False
        self._requests = 0
        self._logs = 0

    async def on_market_tick(self, market, tick, ctx):
        if not ctx.is_paper:
            raise RuntimeError("This illustrative prototype is paper-only")
        if market.kind.value != "binary" or market.market_id != tick.market_id:
            return
        venue = self.venues[0]
        if market.venue != venue.name or tick.venue != venue.name:
            return
        if self._market_id is not None and market.market_id != self._market_id:
            return
        if any(p is None or not p.is_finite() or not 0 <= p <= 1
               for p in (tick.bid, tick.ask)) or tick.bid > tick.ask:
            return
        if tick.timestamp.tzinfo is None:
            return
        if self._last_time is not None and tick.timestamp <= self._last_time:
            return
        self._market_id, self._last_time = market.market_id, tick.timestamp
        mid = (tick.bid + tick.ask) / 2
        delta = mid - mean(self._history) if len(self._history) == 3 else None
        self._history.append(mid)  # Latest point excluded from earlier baseline.
        if self._logs < 10:
            ctx.log("toy_deviation", market_id=market.market_id,
                    deviation_usd=str(delta) if delta is not None else None)
            self._logs += 1
        if delta is None or self._requests >= 2:
            return
        held = next((p for p in ctx.positions
                     if p.venue == venue.name and p.market_id == market.market_id
                     and p.side.value == "yes"), None)
        if held is not None and delta >= 0:
            self._requests += 1
            await ctx.close(venue, market, side="yes")
        elif held is None and not self._entered and delta <= Decimal("-0.05"):
            if ctx.can_open_position():
                self._entered = True
                self._requests += 1
                await ctx.place(venue, market, side="yes", size=Decimal("1"))
        # At most two requests. No guaranteed close or worker-time bound.

The envelope's position limit concerns cumulative cost basis. Its UTC-day mark-to-market loss admission can reject new exposure; it does not liquidate an existing holding. The close path needs actual simulated exit evidence. Exceptions, partial quantities or an untriggered close are outcomes to retain, not reasons to weaken the limits.

4. Exercise the rule offline first

This exact class was exercised against the public package's PaperContext with synthetic ticks. The fixtures cover warm-up, a dip producing one paper entry, a return producing a close, no repeat entry, missing/crossed/duplicate inputs and refusal of a non-paper context. That is implementation evidence for the fixtures, not a production journey or a validated mean-reversion effect.

5. Keep a supervised run within its scope

Complete the Banger account setup in the CLI documentation, save the class as strategy.py and use banger run strategy.py --paper for a deliberately short supervised test. Stop with Ctrl+C and inspect its retained record. The ten-log ceiling limits output; it does not stop the worker. Account and infrastructure admission still apply.

Inspect delivered data, decisions, requests, positions, fees and resource shutdown separately. A remaining simulated holding means the round-trip evidence is incomplete, even when the process stopped normally. Preserve that result.

6. Interpret the simulator honestly

The packaged forward-paper path can assume a non-crossing-limit fill and uses a reported-volume cap when available. Its named Kalshi fee model rounds to cents, rather than exactly reproducing every current venue charge. This example uses that model's evidence without calling it real execution.

Banger's public beta remains paper-only. A successful toy round trip proves that the supplied implementation handled its fixture; it does not establish a profitable signal, continuous market data, true probability estimates or public live readiness.

Sources

Keep reading

Run your first strategy free

Paper-trade on Polymarket and Kalshi market data without venue keys.

Start free