vike
Market making

Settlement

The resolution-aware risk heuristics for a binary market — a linear force-flatten schedule that shifts the reservation toward flat as time runs out, and a time-accelerated pull that trips the fill-rate breaker sooner.

crates/vike-mm/src/settlement.rs holds the pure τ-driven risk heuristics of the spread maker on a binary [0,1] prediction market. The header states the hazard: holding inventory to resolution on the wrong side is a total loss, so as τ = T − t runs to zero the maker must (1) actively unwind toward flat and (2) tighten its circuit breaker on the toxic side faster than symmetric time decay would. Everything here keys off one anchor, AsParams::resolution_ts — the resolution timestamp T in epoch milliseconds, which for Polymarket is GammaMarket.end_date parsed at mount. SpreadMaker::from_params's reader says every settlement feature reads it: the force-flatten τ, the blackout, the terminal penalty, the underlying anchor and the pull acceleration.

What the pricing layer already does

The header separates this module from its neighbour. The Avellaneda–Stoikov layer already skews harder and widens through a settlement-variance term — AsParams::terminal_penalty_gamma, folded in over the last terminal_ramp_ms, so the reservation skew strengthens and the half-spread widens at p ≈ 0.5 as t → T — and blacks out near resolution: within resolution_blackout_ms of T it quotes maximally wide, pushing fills out to the [tick, 1 − tick] walls, to dodge the resolution-news vol spike the Bernoulli cap does not catch. That doc also draws a line the operator should know: the blackout is the pricing layer's stance, while an active mass-cancel pull is the mount's on_feed_status job. What this module adds is the thing the pricing layer lacks — an active force-flatten schedule that ramps the target inventory toward zero.

These are practitioner heuristics

The header says so in as many words. There is no canonical closed-form f(τ) for a force-flatten schedule: the literature — Avellaneda–Stoikov and its settlement variants — prices the reservation and the spread but does not prescribe an active-liquidation ramp. The shapes here are deliberately the simplest defensible ones, linear in τ; a caller wanting a convex late-panic curve composes these, and the linear primitives are the pinned, testable baseline.

The force-flatten schedule

flatten_weight(tau_ms, flatten_by_ms) answers how much of the current position should already be unwound, in [0, 1]. flatten_by_ms is the τ at which the maker wants to be fully flat, measured as time-to-resolution — 30_000 means "flat by 30 s before resolution". A non-positive window is disabled and returns 0.0; a τ at or beyond the window is 0.0, nothing to unwind yet; inside the window the weight ramps linearly, 1 − tau_ms / flatten_by_ms; and a τ at or below zero is 1.0, the whole position wanted gone. It is monotone non-increasing in τ and always clamped.

force_flatten_skew(q_norm, flatten_w, strength) turns that weight into an additive reservation shift: −strength · flatten_w · q_norm. A long book yields a negative shift — a lower reservation price, so the maker leans to sell and flatten — and a short book mirrors. The lean grows with the weight, so the pressure to be flat rises as τ runs out, and it is exactly 0.0 when the weight is zero or strength is zero, the opt-out. The call site is the A-S reservation in crates/vike-mm/src/avellaneda.rs's AsState: τ is resolution_ts − ts, or i64::MAX without a known resolution — outside any window, weight zero, inert — and the shift is added to the reservation after the Cartea–Jaimungal short-alpha term. The keys are flatten_by_ms and flatten_strength, both defaulting to off; AsParams::flatten_strength's doc notes that 0.0 is byte-identical even with a window set.

The accelerated pull

accelerated_pull(tau_ms, ramp_ms, max_accel) is the factor the per-side fill-rate breaker divides its effective net-fill threshold by, so it trips sooner as τ runs out. A non-positive ramp, or a τ outside it, returns 1.0; inside, the multiplier ramps linearly from 1.0 to 1 + max_accel; at or after resolution it is 1 + max_accel. It is always at least 1.0 — floored, so a negative max_accel can never weaken the breaker — and monotone non-decreasing toward resolution. Because it is 1.0 bit-for-bit when off, dividing the threshold by it is byte-identical on the default path: x / 1.0 == x.

The wiring is in two places. SpreadMaker::pull_accel_mult reads 1.0 unless the A-S layer is on with a known resolution_ts and pull_accel_ramp_ms > 0 — the two knobs live on AsParams, so a breaker-only maker with no A-S layer reads 1.0 and is byte-identical. Strategy::on_fill in crates/vike-mm/src/strategy_impl.rs then compares the netted fills against net_fill_threshold / pull_accel_mult(fill.ts); the pure net_signed_fills is untouched, and the multiplier is applied only at that comparison. The keys are pull_accel_ramp_ms and pull_accel_max, defaulting to off.

The inert-when-off invariant

Every function here returns its neutral value — a 0.0 shift, a 1.0 multiplier — when its window parameter is zero or non-positive, so a maker that has not opted in is byte-identical to the pre-settlement path. The in-file tests pin the disabled, far, at-resolution and mid-ramp values for all three functions, including accelerated_pull's 1.0 being bit-exact so the divided threshold is unchanged.

Two things this page does not cover. The active unwind that crosses the book is a different leg — taker flatten — and both the schedule here and the skew are passive. And the venue side of resolution — what the Polymarket bridge does when a market resolves, auto-redeem included — is on Polymarket operations; the reward-band fold's own blackout rule is on liquidity rewards.

On this page