Options Pricing in a Real-Time App: Black-Scholes, Volatility and Strikes by Delta
How we added Black-Scholes pricing, volatility estimators and strike-by-delta selection to a live market app, plus the edge cases that break naive code.
This week we added options pricing to Signal Desk, the market-signals platform we built and run ourselves for Indian equities and F&O. The commit message was short: *price options with Black-Scholes; volatility estimators and strike by delta*. The work behind it was a good reminder that textbook formulas only get you about 30% of the way in a production system.
This post covers how we approached it and the parts that need the most care. It is written for engineering leads building anything finance-adjacent. It should also help founders who need to judge whether "we'll just add options pricing" is a two-day task or a two-week one.
A quick note on scope. Signal Desk is an engineering example. Nothing here is a claim about trading performance, and pricing models describe options. They do not predict them.
Why an alerts app needs a pricing model at all
Signal Desk streams the Zerodha Kite websocket, builds multi-timeframe candles and raises entry and exit alerts on stocks and index futures. An alert on an underlying is only half the picture for F&O users. The natural next questions are:
- Which strike would express this view?
- Roughly what should that option cost?
- How sensitive is it to the underlying moving?
Answering these needs three pieces: a pricing model, a volatility input, and a way to turn "I want this much sensitivity" into an actual listed strike.
Piece 1: Black-Scholes, the honest version
Black-Scholes gives a closed-form price for a European option from five inputs: spot price, strike, time to expiry, risk-free rate and volatility. It is cheap to compute, which matters when you might reprice on every tick.
The core fits in a few lines:
function bs(S, K, T, r, sigma, isCall) {
const sqrtT = Math.sqrt(T);
const d1 = (Math.log(S / K) + (r + 0.5 * sigma * sigma) * T) / (sigma * sqrtT);
const d2 = d1 - sigma * sqrtT;
const df = Math.exp(-r * T);
if (isCall) {
return { price: S * N(d1) - K * df * N(d2), delta: N(d1) };
}
return { price: K * df * N(-d2) - S * N(-d1), delta: N(d1) - 1 };
}The formula is the easy part. These are the things that actually take time.
You need your own normal CDF
JavaScript has no built-in N(x). You will implement it from an erf approximation or a rational approximation. Write unit tests against known values, including the tails. A small error in N shows up as a delta that never quite reaches 0 or 1.
Time to expiry is a modelling decision
T is in years, but *which* years? You can use calendar time, or you can use trading time, which counts only market hours. With weekly expiries, the two choices give visibly different numbers on the last couple of days. Pick one, document it, and use it everywhere: in pricing, in volatility annualisation and in the UI.
Expiry day breaks the math
As T approaches zero, sigma * sqrtT approaches zero and d1 heads towards infinity. Delta collapses into a step function, and at exactly zero you divide by zero. Production code needs an explicit rule for this:
- Clamp
Tto a small minimum. - Or switch to intrinsic value when you are inside a threshold.
Do not let a NaN flow into a dashboard or an alert.
It is a model, not a quote
Black-Scholes assumes constant volatility and a European exercise style. Real option prices carry a volatility smile, a skew and a bid-ask spread. We present the output as a *model estimate* with its inputs visible. We do not present it as "the price".
Piece 2: Volatility is the input that matters most
Every other Black-Scholes input is observable. Volatility is not, and the price is very sensitive to it. Two broad sources exist:
- Implied volatility: back it out from market option prices. This is what the market currently believes, but you need clean option quotes to get it.
- Historical (realised) volatility: estimate it from the underlying's past price movement. You can compute it from data you already have.
Signal Desk already builds OHLC candles at several timeframes, so historical estimators were a natural fit. Building it taught us that "historical volatility" is not one number. The common estimators include:
- Close-to-close: the standard deviation of log returns. Simple, but it ignores everything that happened inside each bar.
- Parkinson: uses the high-low range. It extracts more information per bar, but it assumes no drift and no gaps.
- Garman-Klass: uses open, high, low and close. It makes even better use of each candle.
- Yang-Zhang: designed to handle overnight gaps. This matters for equities, where the market is closed for most of the day.
Lessons from wiring estimators into a live system
Annualise with the right bar count. If you estimate from 5-minute candles, you scale by the number of 5-minute bars in a trading year, not by 365 days. Getting this wrong produces volatility that is off by a large factor, and the bug is silent.
Make the estimator and the window explicit. "20-day Parkinson" and "60-day close-to-close" can disagree substantially. Show the user which one fed the price.
Guard against bad candles. A single bad tick that creates a huge high-low range will distort a range-based estimator. Validate candles before they reach the estimator, not after.
Piece 3: Choosing a strike by delta
Traders often think in delta, not in strike prices. "Give me roughly a 0.30-delta call" is a request about sensitivity and moneyness that stays meaningful whether the index is at one level or another. So we added strike selection by delta.
The trick is that Black-Scholes delta can be inverted analytically for the strike. For a call, delta is N(d1). Apply the inverse normal to the target delta to get d1, then solve the d1 equation for K:
K = S * exp(-(Ninv(delta) * sigma * sqrt(T)) + (r + sigma^2 / 2) * T)
That gives a theoretical strike. Getting from there to something usable takes three more steps.
Snap to listed strikes
Exchanges list strikes at fixed intervals, and the interval differs by underlying. The theoretical strike will almost never be a listed one. Snap to the nearest listed strike, then recompute the delta at that strike and show the user the actual delta they would get, not the one they asked for.
Implement the inverse normal carefully
Like N(x), the inverse Ninv(p) is not built in. Rational approximations work well. Test them near 0 and 1, and reject target deltas outside a sensible range before you call them.
Puts need their own sign handling
Put delta runs from -1 to 0. Convert it explicitly (N(d1) = delta + 1) instead of reusing the call path with an absolute value. That shortcut is a classic source of strikes that land on the wrong side of the spot price.
Performance: closed-form is your friend
A common worry is that pricing on every tick will be expensive. With closed-form Black-Scholes it is not. A few logs, exps and square roots per option is trivial work. The real costs are elsewhere:
- Recomputing volatility too often. Volatility from candles only changes when a candle closes. Compute it then and cache it. Do not recompute it per tick.
- Pushing every repriced value to every client. We had already learned this one on the dashboard's live feed, where batching price-only updates and compressing them cut bandwidth dramatically. Derived values like option prices should follow the same discipline: batch them and send only what changed.
- Iterative solvers. If you later add implied volatility, which requires a root-finder, that is where CPU time goes. Bound the iterations and handle non-convergence explicitly.
What we would tell a team starting this
If your product touches options, structured products or anything else priced by a model, these are the parts teams tend to underestimate:
- Edge cases are the work. These include expiry day, zero or missing volatility, deep in- or out-of-the-money strikes, and bad candles. Write tests for each before you touch the UI.
- Every input is a decision. The time convention, the volatility estimator, the window and the rate all affect the output. Make them visible and configurable.
- Show the inputs next to the output. A model price without its assumptions invites users to treat it as a quote.
- Keep the math pure. Pricing functions should take numbers in and return numbers out, with no I/O. That makes them easy to test, easy to benchmark and easy to reuse in a statistics lab or a backtest.
Closing thought
None of this is exotic. The math is decades old. What makes it production-grade is the boring part: conventions, clamps, validation, caching, and honest presentation of what a model can and cannot tell you.
That is the kind of work we like doing at Codemone. If you are adding data-heavy or real-time features to your product and want a second pair of senior eyes, we offer a one-week fixed-price sprint. It can be an architecture or performance review with the fixes done, or a first working slice of the feature, so you see results before committing to anything bigger.