The Strategy interface
Every Banger strategy is a Python class that subclasses banger.Strategy. The class declares what the strategy is (venues, universe, risk envelope) and implements how it reacts to events.
Required class attributes
| Attribute | Type | What it does |
|---|---|---|
name | str | URL-safe slug. Identifies this strategy across CLI/dashboard/MCP. |
venues | list[Venue] | Execution venues. The runtime subscribes to ticks for each. |
universe | Universe | Declarative spec of which markets to subscribe to (use signals.*). |
risk | RiskEnvelope | Position-size, open-positions, daily-loss limits. Enforced before orders ship. |
Event handlers
Implement only the handlers you need. They can be sync or async — the runtime auto-awaits coroutines.
class MyStrategy(Strategy):
name = "my_strategy"
venues = [Polymarket()]
universe = signals.polymarket_markets()
risk = RiskEnvelope(max_position_usd=Decimal("100"))
def on_start(self, ctx):
"""Called once when the deployment starts."""
ctx.log("up and running")
def on_stop(self, ctx):
"""Called once when the deployment stops (manual or auto)."""
pass
async def on_market_tick(self, market, tick, ctx):
"""Called for every price/state update on every subscribed market."""
...
def on_event(self, event, ctx):
"""Catch-all for non-tick events (resolutions, news, etc.)."""
passThe Context object
Every handler receives a ctx with the runtime APIs.
Placing orders
# Market order — fills at the latest seen tick.
# size is the USD notional to stake, NOT a contract count.
# $10 at $0.45 buys ~22 contracts.
await ctx.place(self.venues[0], market, side="yes", size=Decimal("10"))
# Limit order — uses the explicit price
await ctx.place(
self.venues[0], market,
side="no", size=Decimal("10"), price=Decimal("0.45"),
)size is a USD notional stake, not a number of contracts. The runtime converts it to a contract quantity at the fill or limit price, so size=Decimal("10")means "spend up to $10" and buys roughly 22 contracts at $0.45. This matches ctx.kelly(), which also returns a USD stake, so you can pass its result straight to place(). Kalshi trades whole contracts, so the stake is rounded down to the nearest contract and a stake worth less than one contract is rejected rather than rounded up.
The side argument can be a string ("yes", "no", "buy", "sell") or a Side enum. For binary markets, the runtime auto-infers the YES price from NO (and vice versa) when only one side has ticked.
Risk gating
if ctx.can_open_position():
# the risk envelope permits a new position right now
...ctx.place() also enforces the envelope automatically — if an order would breach a limit, it raises RiskViolation and the runtime logs it.
Sizing helpers
# Quarter-Kelly stake sized against current account equity
size_usd = ctx.kelly(edge=0.05, fraction=0.25)Inspecting state
# All open positions on this deployment
for pos in ctx.positions:
print(pos.market_id, pos.side, pos.size, pos.avg_price)
# Are we paper or live?
ctx.is_paper # → True / FalseLogging
ctx.log("found edge", market_id=market.market_id, edge=0.07)
# -> surfaces to your dashboard and deployment log APISurfaced parameters
Class-level scalar attributes (int, float, str, bool) are automatically discovered as “declared parameters” and shown as form fields in the marketplace cloner UX. So users who clone your strategy can configure these values without editing code.
class MyStrategy(Strategy):
name = "my_strategy"
...
# These show up as configurable inputs in the cloner UI:
edge_threshold: float = 0.05
max_holding_hours: int = 24
only_political_markets: bool = FalseWhat you don’t do
- You don’t open websockets — the runtime does that.
- You don’t track positions yourself — the runtime does.
- You don’t spawn tasks — the runtime drives the event loop.
- You don’t handle reconnects — the runtime handles that.
- Paper mode uses public market data and does not require venue credentials.
Your job is the alpha; ours is the plumbing.