Full-stack systematic market making

February 2026

I will assume the reader knows how market making in general works.

Abstract

In this project, we market make on Kalshi sports markets (and we stop when the corresponding event begins, to avoid adverse selection). Our theos are derived by aggregating sportsbooks’ odds and then quoting about them with HJB equations and engaging in dynamic behavior when necessary, i.e. when the orderbook is competitive. We employ various risk management techniques ranging from actual positional limits to redundancies in code to catch e.g. Kalshi’s webhooks going inactive.

Quoting as Monopsony

To check if we have competition, we simply quote top of book, sit back, and see if we get outbid. Conveniently, our initial quotes can be calculated as if the market is monopsonistic and then we activate competitive logic only if outbid. This baseline quoting mechanism is inspired by Avellaneda & Stoikov, 2008, using Hamilton-Jacobi-Bellman equations, as follows.

To motivate this math a little, HJB quoting is necessary in these monopsonistic markets since such markets are invariably high spread and low liquidity, with the preexisting CLOB often sitting at something absurd, e.g. 5@955@95. Clearly we receive very little flow if we quote top of book p+1p+1 for 6@946@94, but at the same time quoting, say, 48@5248@52 (suppose our theo is 5050) leaves a lot of meat on the bone. Thus we aim to find the optimal price, a process which entails approximating the demand curve.

We model this demand curve via Poisson arrivals: let λ(δ)\lambda(\delta) be the fill intensity when quoting spread δ\delta from theo. Tighter spreads receive more flow but have less edge:

expected profit rate=λ(δ)δ\text{expected profit rate} = \lambda(\delta) \cdot \delta

With the standard parameterisation λ(δ)=Aekδ\lambda(\delta) = Ae^{-k\delta}, our profit is maximised at δ=1/k\delta^* = 1/k. However, this ignores inventory exposure. So, we adapt the equation used in the paper to this end. Since we have a reliable theo from our sportsbooks, we can treat fair value as known and fixed (insofar as we refresh that fair value periodically). Then let qtq_t be our inventory. With CARA utility (this is a hobby project) and risk aversion γ\gamma, the HJB equation becomes:

tV+maxδa,δb[λ(δa)(Vq1V)+λ(δb)(Vq+1V)]=0\partial_t V + \max_{\delta^a, \delta^b}\left[\lambda(\delta^a)(V|_{q-1} - V) + \lambda(\delta^b)(V|_{q+1} - V)\right] = 0

Our value function then depends only on (t,q)(t, q), assuming a non-stochastic midprice. This is apt for sports markets since the fair price of a contract ought to only change when new information (like a player sitting out) is released, something that happens infrequently; otherwise, it should be fixed.

Then, to mitigate inventory exposure, we skew our bid-ask quotes based on the directional exposure: if long, tighten the ask; if short, tighten the bid. The magnitude of skew depends on γ\gamma—higher risk aversion means more aggressive mean-reversion.

There are other skewing formulae I looked at, the best of which seemed to be linear skewing: r=sθ(qq)r^* = s - \theta(q - q^*), where rr^* is a skewed theo, ss is the true theo, θ\theta is the skew intensity, qq is current inventory, and qq^* is target inventory. In practice, however, this ends up being basically the same as the HJB-implied skew since qq^* is always 00 for our intents and purposes.

So, we need two parameters: risk aversion γ\gamma and arrival intensity kk. We set γ\gamma by market type—niche sports (e.g. Euroleague, college lacrosse) get higher risk aversion than less niche ones (e.g. boxing, esports). We then estimate kk from observed liquidity on the CLOBs of the sport. Since prediction markets have discrete ticks (1¢), we then precompute a quote table indexed by qq and snap to the nearest valid price (per sport).

Quoting with Competition

However, we usually have competition. They eat into the share of orderflow we absorb, which is objectively bad, but competition does make the quoting logic much easier.

The logic is best explained with a very complicated flow chart (see like a third of it in the thumbnail), but it boils down to “quote top of book unless top of book exceeds theo”. There is some sophistication regarding how we actually engage in bidding, but after a few seconds, bidding always ends as ceilings are hit and so (1) we back off, (2) our competitors back off, or (3) we all sit at a shared ceiling. Unfortunately, (3) is very common and very boring.

Execution & Infrastructure

We globally pull odds from The Odds API every 30 minutes, aggregating lines from a few dozen sportsbooks to get a no-vig implied probability. In particular, we implement a weighted average about Pinnacle (a very sharp book). We also pull odds on a per event basis when inventory skews dramatically in order to mitigate adverse selection from, say, a player being announced as injured recently (theoretically, we should lose if and only if Vegas loses, with some asterisks of course).

With these theos and the aforementioned quoting calculations, we then have WebSocket connections to Kalshi for real-time orderbook deltas and fill notifications. Orders are placed/updated in parallel via REST with connection pooling and retries. Each order has an async lock to prevent inventory runaway. On orderbook change or fill, we recalculate all four sides (checking inventory and so on) and requote.

For redundancy (and due to an Incident…), WebSocket disconnects trigger an automatic reconnect protocol with exponential backoffs. The inventory syncs from REST every 10 seconds to make sure positions don’t run away, and very primitive kill switches flood the API for a minute straight if things go particularly awry, which is unfortunately necessary since the Kalshi API tends to lie about whether an order kill succeeded…

Dashboard
The dashboard. This one is deprecated, but it’s prettier than the other one, so...

To monitor all this, we have a nice dashboard hosted locally.

Risk Management

Risk management boils down to inventory management.

We don’t care about anything else (e.g. optimising the overall portfolio) since we are not in a situation where we have limited funds and need to decide how to best allocate them; on the contrary, I don’t mind depositing more money whenever positions tie up our cash, since I want to absorb all of the orderflow.

Assuming our theo is correct, every trade is necessarily plus EV, even if we aren’t market neutral. So if we accumulate great directional exposure at a great price, I am more than happy to take the gamble. However, if our theo is outdated, we could easily be adversely selected (hence various mechanisms which activate a rechecking of the odds). Directional exposure is really scary and our main concern.

To that end, we have a hard inventory limit per match derived from (1) a constant corresponding with the sport and (2) whatever the liquidity available in that market is. Generally, I am more risk-averse for super niche sports where insider trading is a concern (esports, college baseball) and less risk-averse for more "legitimate" sports. A notable exception is boxing, an unserious sort of sport where competitors sometimes miss weight and get disqualified, some threaten to skip the fight which voids it, there's lowkey rigging which goes on, et cetera.

We track a single signed inventory which equates yesAyes_{A} as noBno_{B} and yesByes_{B} as noAno_{A}. And when we hit these ceilings (or get close enough where we need to offer fewer contracts lest all are filled and we go over ceiling), we stop quoting (or limit quoting) the side which would increase exposure—fairly straightforward stuff.

We also enforce break-even ceilings when overexposed. These override the quoting logic, allowing us to take positions which are theoretically unprofitable EV-wise. For instance, if we’re overexposed to AA at an average cost of 45¢45¢, we are willing to bid up to ~54¢54¢ (we calculate fees too of course), even if our theo is 52¢52¢. In other words, being exposed increases our ceiling.

Results

February 2026 P&L breakdown
February 2026 PnL.

I unfortunately cannot report e.g. Sharpe; market making, particularly on prediction markets, renders these metrics pretty meaningless. Strictly binary contracts like yesAyes_{A} and noAno_{A} resolve immediately whereas yesAyes_A and yesByes_B are held until resolution even though mutually exclusive and mutually exhaustive (and theoretically equivalent to the other case), so even choosing a series of time intervals over which you'd calculate σp\sigma_{p} isn't meaningful. In particular, retail generally enjoys betting on their favorite team, as opposed to against the opponent, so a great deal of positions are held of the nono flavor.

Thus, PnL comprises a drip of binary contracts resolving one another and then a large wave at event resolution (e.g. February 22, a Sunday, had a bunch of games resolve). There are good solutions to this issue (live calculate position value), but most of them eat up the API limit and I didn’t care enough about documentation to implement them.

In the end, I guess that this strategy is necessarily high Sharpe, high Calmar, etc., but it's also not very scalable; I generally hold all four positions top of book (yesA,noA,yesB,noByes_{A}, no_{A}, yes_{B}, no_{B}), each offering a few dozen shares, and that's typically enough to capture all orderflow (of course, they are refreshed upon fill (assuming appropriate inventory exposure, etc.)). The profitability of the strategy relies on picking low-hanging fruit.

I doubt I'll do much more with this—the alpha is drying up pretty quickly. Also, as was alluded to earlier (per our qq table), discrete contract pricing renders the math very easy and therefore boring. Perhaps HFT market making e.g. BTC up/down markets would be fun but those spaces are oversaturated and well-priced, and I have bigger fish to fry.