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.