vike
Market making

Liquidity rewards

Reward-aware quoting for a venue that pays for resting liquidity near the midpoint — the pure scoring model, the band clamp and min-order-age hold that fold it into the quote, and the two safety authorities that outrank it.

crates/vike-mm/src/reward.rs is two things in one file: the pure reward model — how a sampling-reward program scores a resting order — and the spread maker's fold that uses that shape to place its quote. It is net-new Rust with no Python twin — the header says parity here means self-consistent reference-value pinning and neutral reduction, not oracle bits — and every fold is a naive f64 fold with no mul_add, per the crate rule.

The model

The header describes the shape it models — Polymarket's sampling reward. A rewardable market pays a proportional share of a per-market daily pool for resting liquidity near the midpoint, sampled per minute. Per order the closeness score is

S(v, s) = ((v − s) / v)²

where v is the market's max_spread from the midpoint in cents and s the order's spread from the size-cutoff-adjusted midpoint in cents. The score is quadratic — closer to the mid earns superlinearly more — and an order counts only when it is within the band (s ≤ v) and at least min_size shares. The per-side sum Σ S·size is combined two-sidedly: with the midpoint inside [0.10, 0.90] the market scores max(min(Q_one, Q_two), max(Q_one, Q_two) / c) with c = 3, so quoting one side alone earns a third of it; strictly outside that range it scores min(Q_one, Q_two) — both sides required, a missing side scores nothing. The absolute payout is irrelevant to the maker; only this shape drives its quote.

The three constants are public — REWARD_MIDPOINT_LO (0.10), REWARD_MIDPOINT_HI (0.90) and REWARD_SINGLE_SIDED_DIVISOR (3.0) — and so are the three scoring functions spread_score, side_score and q_min. The crate root explains why the module is pub: a Polymarket mount or an analysis can compute expected reward with them, while the maker's own fold helpers stay crate-private. spread_score returns 1.0 at the mid, 0.0 at the band edge and zero outside it, for a negative spread or for a non-positive band; the tests pin that the same one-cent tightening is worth more near the mid than near the edge.

The fold

clamp_into_band(mid, bid_px, ask_px, params) pulls a candidate two-sided quote into the band. With band = max_spread_cents / 100 in 0..1 price units, each side's spread from the mid is capped at s_target = band · (1 − weight). The doc states the two properties that matter: the maker never quotes wider than the Avellaneda–Stoikov candidate wanted — a tighter A-S side is honoured — but a too-wide side is pulled in to at most s_target, which is at most the band, so the side always qualifies. Because it never widens a side, it can only ever raise the reward score. A larger weight means a tighter cap, closer to the mid, more reward and more adverse selection: weight is the reward-versus-A-S dial. The pulled price is deliberately left un-snapped — the A-S grid-snap and wall-clamp already ran upstream and the venue mount re-snaps. Off (weight <= 0, or a non-positive band) returns the input unchanged bit-for-bit, the byte-identical default.

moas_holds is the min-order-age gate. It answers true — skip the re-price, let the quote keep ageing toward the reward floor — only while reward chasing is active with a positive min_order_age_ms, the side has rested less than that, and the resting quote is still safely in-band around the mid (spread in [0, band]). Out-of-band or crossed returns false, so a safety re-price always fires. It is never consulted on a place, pull or fill path; quote.rs's reward_moas_holds also answers false on a fill-stale side, and it composes with the refresh tolerance — either may skip — rather than replacing it.

Where it sits in the tick

crates/vike-mm/src/quote.rs's requote runs the fold at its stage (2b), immediately after pricing. Two details there are load-bearing. The band is scored off the book midpoint, computed from the touch, not off the A-S reservation the candidate is centred on. And the fold is skipped entirely inside the A-S near-resolution blackout: inside it the maker quotes at the walls, and reward-clamping it back into the band would farm rewards straight into the resolution vol spike. Stage (3b) then floors each reward-active side's size up to min_size — never down — after the inventory skew has shaped it, and that floored base flows into rung 0 of a ladder.

Two safety authorities outrank the fold

The header names them: the A-S blackout and the fill-rate breaker. The caller skips the fold inside a blackout, and a suppressed side is pulled regardless of any reward hold. A rewards-shaped quote never quotes through a blackout to farm rewards. The behaviour suite, crates/vike-mm/src/tests/liquidity_rewards.rs, tests exactly that pair alongside the band clamp and the age hold.

The knobs

crates/vike-model/src/strategy/maker_params.rs's RewardParams is nested as Option<RewardParams> in the maker's params; None, and any weight <= 0, is off. weight is the intensity in [0, 1], clamped outside it. max_spread_cents is the band half-width in cents — 3.0 is ±3¢, ±0.03 on a 0..1 market — and a non-positive value is off. min_size is the eligibility floor in shares; 0.0 imposes none. min_order_age_ms is the program's minimum order age, Polymarket's being about 30_000; a non-positive value disables the age gate. RewardParams::default is a Polymarket-shaped starting point that is still off — weight 0.0, 3.0 cents, no size floor, 30_000 ms — so a Some(RewardParams::default()) is inert until weight is raised. SpreadMaker::with_liquidity_rewards is the builder.

The type's doc draws the layering line: this is the strategy half. A Polymarket mount parses its live market config — max_spread, min_size, min_order_age/moas — and feeds it here as plain parameters, so vike-mm needs no venue dependency. The rest of quoting a bounded [0,1] token — the LMSR fair value and the spread sources — is on binary outcomes, and the resolution-side mechanics on settlement.

On this page