The hard part
The surface fit was not the hard bit. The vols were.
Everything downstream depends on implied-volatility calculations: the smile, SVI fit, ATM volatility, skew, risk reversals, flies, surface patches, UI updates, and API state all begin at the volatility layer.
At first, pressure in a live market-data system looks like an I/O problem. There are websocket feeds, asyncio, live order books, heartbeats, reconnect logic, and bursty exchange updates. When updates arrive in waves or heartbeats are delayed, it is natural to blame the sockets first.

Async reality
WebSocket symptoms are not always WebSocket problems.
Often the issue is simpler: the event loop is being blocked by your own implied-volatility calculations.
BTC options are a good stress test for this. On Deribit alone, a BTC options feed can easily contain more than a thousand live instruments across expiries and strikes, each with multiple bid and ask updates arriving continuously.
Profiling
The profile separated socket lag from calculation lag.
The first useful measurement was not a faster solver. It was a timing trace for the full path from venue message to dashboard patch. Once each stage had its own timestamp, the failure mode became obvious: messages were arriving, but IV inversion and fit preparation were occupying enough event-loop time that heartbeats and fanout were delayed.
That changed the tuning target. The system did not need to recompute every volatility point faster. It needed to avoid entering the expensive path when the visible surface would not change.
- Measure receive latency, book update latency, IV inversion time, fit-preparation time, and frontend fanout separately.
- Group timings by currency, expiry, option type, and venue so one noisy wing does not hide the real bottleneck.
- Track event-loop lag beside market-data lag because they create similar user-facing symptoms.
- Keep the latest accepted SVI state available so the dashboard can degrade without publishing undefined values.
venue_ts -> received_at -> book_updated_at -> iv_started_at -> iv_finished_at -> smile_fit_at -> dashboard_patch_sent_at
Latency budget
The event-loop latency budget had to be assigned to product state.
A single average latency number was not useful. The dashboard could tolerate a slow update for a far wing more easily than a stale front-expiry ATM row or a delayed 25-delta risk reversal. The latency budget therefore had to be attached to visible surface state.
Derivasys treated event-loop latency as a surface-quality input. If the loop was busy, the system needed to know which work could be merged, which work could be reused, and which user-facing risk nodes were old enough to force a recalculation.
- Budget front-expiry ATM rows and visible risk nodes more tightly than far-wing diagnostics.
- Measure event-loop lag, IV queue age, fit queue age, and dashboard patch age separately.
- Promote stale displayed nodes ahead of lower-priority quote churn.
- Record the reason a recalculation was skipped, merged, reused, or forced.
priority = visible_node_age + front_expiry_weight + risk_node_weight + quote_quality_weight - far_wing_noise_penalty
BTC stress test
A meaningful BTC move can force the whole surface to move.
Once BTC moves meaningfully, everything follows. Spot shifts, forwards move, moneyness changes, implied volatilities need recalculating, smiles update, and the volatility surface updates.
The MVP became less about fitting the surface and more about avoiding recalculating the world on every tick. A few calculations can become tens of thousands of implied-volatility calculations in short bursts.
Quote gating
Bad or irrelevant quotes should be stopped before the IV solver.
The cheapest optimization is rejecting work that should never have reached the volatility layer. A stale book, crossed spread, missing forward, or far-wing mark with no practical effect on the displayed smile should not trigger the same path as a live at-the-money repricing.
This also improved analytical quality. Filtering before IV inversion prevented weak quotes from consuming CPU and reduced the chance that SVI would contort around a market-data artifact.
- Reject stale, crossed, one-sided, or negative-spread books before IV inversion.
- Skip updates where mark and forward movement are below the dashboard display threshold.
- Use expiry-level priority so front tenors and visible risk nodes are refreshed first.
- Carry skip reasons into monitoring so performance tuning remains auditable.
Practical fixes
Throughput improved by making recalculation more selective.
The useful optimisations were practical rather than exotic. The aim was to reserve full implied-volatility inversion for moments where it changed the analytical state enough to matter.
- Ignore tiny spot and ATM moves that would not realistically move volatility by even 1bp.
- Use linear approximations for small moves instead of full recalculation.
- Debounce refreshes during bursts of market-data updates.
- Cache calculations with rounded state buckets.
- Use a C++ implementation of Let's Be Rational for implied-volatility inversion.

Solver strategy
The solver path needs fast reuse, guarded native calls, and explicit fallbacks.
Implied-volatility inversion is numerically small but operationally expensive when it is repeated across thousands of live instruments. Derivasys treated the solver as a scarce resource: reuse prior values when safe, route liquid quotes through the fast path, and reserve slower fallbacks for weak-vega or boundary cases.
The important production rule is that every shortcut has to preserve diagnostics. If an IV value was reused because the input move was below threshold, the dashboard and monitoring layer should know that. If the native solver failed and bisection recovered a value, that path should be visible too.
- Cache by instrument, forward bucket, expiry, option type, strike, and rounded mark state.
- Use the previous accepted IV as a seed only when quote and forward movement are within tolerance.
- Route low-vega, deep-wing, and short-expiry cases through guarded fallbacks instead of letting Newton steps wander.
- Publish solver path, iteration count, and fallback reason with the IV point.
if reusable(previous_iv, quote, forward): publish_iv(path="reuse") elif fast_solver_safe(quote): publish_iv(path="native_fast") else: publish_iv(path="guarded_fallback")
IV cache
The hard cache problem was invalidation, not storage.
Caching IV outputs is easy until the forward moves, a quote source changes, or the option shifts enough in moneyness that the previous value no longer explains the displayed smile. The useful cache key had to include the pricing state that could change the fitted surface, not only the listed instrument.
The cache also needed a diagnostic contract. A reused IV is acceptable only when downstream consumers can see that it was reused because the mark, forward, and surface impact were inside tolerance.
- Key cache entries by instrument, expiry, option type, strike, forward bucket, mark bucket, and chosen price source.
- Invalidate when the forward remaps delta buckets or when quote quality changes from accepted to rejected.
- Carry cache-hit, reuse, approximation, native-fast, and fallback labels into the VolState output.
- Compare cached and fully recomputed outputs during replay before relaxing thresholds.
IvCacheKey {
instrument,
expiry,
option_type,
strike,
forward_bucket,
mark_bucket,
price_source,
quality_state
}Performance tuning
The tuning loop measured volatility-state change, not just CPU time.
The useful performance metric was not raw messages per second. It was whether the volatility state seen by the dashboard changed enough to justify a full recomputation. A tick that does not move an expiry smile, risk reversal, fly, or fixed-tenor row should not consume the same budget as a market-wide repricing.
Derivasys performance tuning therefore treated implied-volatility inversion as a guarded expensive path. The fast path keeps the book current, marks what changed, and promotes work to full inversion only when the surface output would become materially different.
- Measure event-loop lag separately from exchange WebSocket lag.
- Track IV inversion time by currency, expiry, option type, and venue.
- Bucket tiny spot, forward, and mark moves before triggering full recalculation.
- Collapse duplicate updates so one expiry is recalculated once per batch window.
- Keep risk-node freshness visible so tuning never hides stale output.
if quote.is_stale or quote.is_crossed: skip_iv_inversion(reason="bad_quote") elif move_below_display_threshold(quote, previous_state): reuse_previous_iv(reason="below_surface_tick") elif expiry_already_queued(quote.expiry): merge_into_pending_batch(quote) else: queue_full_iv_inversion(quote.expiry, priority="surface_visible")
Native boundary
Native solver speed helped only after the worker boundary was explicit.
Dropping a faster native solver into the middle of a coupled event loop would have hidden the real problem. The solver call needed a narrow boundary: consume a normalized quote and forward state, return IV plus diagnostics, and never decide whether the surface should publish.
That boundary made later Rust or C++ work safer. A native worker can be faster, but it still has to preserve solver path, tolerance, fallback reason, and output units so SVI calibration and dashboard diagnostics stay comparable.
- Keep pricing conventions and units outside the native solver call.
- Return iteration count, convergence status, fallback reason, and timing with every IV.
- Treat native output as one VolState producer behind the same contract as the Python path.
- Use shadow comparison before letting native output drive SVI fits or API snapshots.
QuoteState + ForwardState
-> native_iv_solver
-> VolState { iv, total_variance, solver_path, timing, diagnostics }Batching
Expiry-level batching turned update storms into one surface decision.
During a BTC move, many instruments in the same expiry can update before the first recalculation finishes. Repricing each instrument independently wastes work and can publish uneven smile state. Batching by expiry lets the worker collapse a burst into one coherent fit request.
Backpressure then becomes a product decision. If a batch is already pending for a far expiry, the next update can merge into it. If the front expiry or a displayed risk node is stale, it should jump the queue. The queue policy is part of surface quality, not only infrastructure.
- Keep one pending recalculation per expiry unless the new update changes priority.
- Merge quote changes into the pending batch instead of scheduling duplicate solver work.
- Prioritize visible tenors, ATM rows, risk reversals, and flies over hidden far-wing noise.
- Publish batch age and queue length so dashboard freshness remains inspectable.
Guardrails
Performance tuning is only safe if stale output stays visible.
Every optimization can hide a failure if the dashboard only shows the final number. Reused IV, skipped recalculation, stale quote rejection, approximation, and full inversion should all leave different traces in the diagnostics.
For Derivasys, the test was whether a user could still explain a changed risk reversal, fly, or fixed-tenor row from the underlying quote and fit state. If a speed improvement made that explanation harder, it was not a product improvement.
- Show whether a point was recomputed, reused, approximated, rejected, or carried forward.
- Tie risk-node freshness to the expiry smile that produced it.
- Compare SVI residuals before and after each gating rule is enabled.
- Alert when latency improves but accepted quote coverage or fit quality degrades.

Regression replay
Performance changes had to be replayed against market-facing outputs.
The final check for an optimization was not whether the benchmark was faster in isolation. It was whether a replayed market window produced the same accepted smiles, risk reversals, flies, fixed-tenor rows, and dashboard patches unless a difference was intended and explainable.
That replay check kept the tuning work tied to product behavior. A change that reduced CPU but increased stale-node reuse, changed wing residuals, or moved forward-volatility buckets without evidence was not a safe improvement.
- Replay burst windows with caching disabled and enabled.
- Compare accepted SVI parameters, residuals, risk nodes, and surface snapshot timestamps.
- Flag differences caused by solver fallback, cache reuse, quote rejection, or batch ordering.
- Promote the optimization only when dashboard-facing differences are explained.
Scaling direction
The hard problem became systems engineering.
With those changes in place, throughput moved from hundreds of vol calculations per second to roughly 5,000 per second on a relatively small machine.
That is useful for one currency and a couple of exchanges. Once the system adds more exchanges, more currencies, more expiries, and more downstream analytics, the design naturally moves toward distributed compute and partitioned market-data pipelines.
The hard problem stopped being the maths. It became deciding when to compute, where to compute, how to partition live state, and how to keep the dashboard responsive while the market is moving.
Links