Python for Quant Work
The NumPy and SciPy patterns that price, simulate and test under interview time pressure.
Quant take-homes reward code that is correct, vectorised and easy to check not clever. This guide covers the handful of NumPy/SciPy patterns that show up again and again: vectorised pricing, Monte Carlo with error bars, working with distributions, and the small habits (seeding, sanity checks) that make a submission look like it came from someone who has done this before.
Vectorise everything that touches a payoff
A loop over simulation paths is the single most common thing that makes a Monte Carlo answer too slow and harder to read. Generate all your shocks at once and let NumPy broadcast. The terminal price of a geometric Brownian motion is a one-liner.
import numpy as np z = rng.standard_normal(n) ST = S * np.exp((r - 0.5 * sigma**2) * T + sigma * np.sqrt(T) * z) payoff = np.exp(-r * T) * np.maximum(ST - K, 0.0) price, se = payoff.mean(), payoff.std(ddof=1) / np.sqrt(n)
Always report a standard error
A Monte Carlo price without an error bar can't be judged the interviewer can't tell whether a 0.3% gap to the closed form is noise or a bug. Report mean ± std/√n. The error shrinks like 1/√n, so to halve it you need 4× the paths; that fact alone is worth saying out loud.
- Use ddof=1 for the sample standard deviation.
- For antithetic variates, average each z/−z pair first, then take the SE over the n/2 pair-means not naively over all n payoffs.
- Seed your generator (np.random.default_rng(seed)) so results are reproducible.
Reach for SciPy for distributions and root-finding
Don't hand-roll a normal CDF or a solver. scipy.stats.norm gives you cdf/pdf/ppf; scipy.optimize.brentq gives you a bracketed root-finder that won't diverge exactly what you want when inverting Black-Scholes for an implied vol.
from scipy.stats import norm
from scipy.optimize import brentq
def implied_vol(price, S, K, r, T):
f = lambda s: bs_call(S, K, r, s, T) - price
return brentq(f, 1e-4, 5.0) # bracketed: robust, no divergenceMake it checkable
The fastest way to build trust in a numerical answer is to show it agreeing with something you already know. Benchmark Monte Carlo against the closed form. Assert put-call parity. Print a limiting case. A submission that checks itself reads as senior.
- Benchmark MC against Black-Scholes on a vanilla before trusting it on an exotic.
- Assert identities (put-call parity) rather than eyeballing numbers.
- Keep functions pure and small so each can be tested in isolation.
Structure a pricer so every piece is testable
Have one source of truth for d1 and d2 and build price, Greeks and implied vol on top of it. Keep each function pure parameters in, a number out, no globals or hidden state so you can test it in isolation and an interviewer can read it top to bottom. The layout that reads as senior: small closed-form functions, a separate Greeks block, then a few asserts at the bottom that the file checks on itself.
def d1(S,K,r,sig,T): ... # one definition, reused everywhere
def call(S,K,r,sig,T): ... # built from N(d1), N(d2)
def greeks(S,K,r,sig,T): ... # delta/gamma/vega/theta in one dict
if __name__ == '__main__':
c = call(100,100,.02,.2,1); p = put(100,100,.02,.2,1)
assert abs((c - p) - (100 - 100*exp(-.02))) < 1e-8 # put-call parityVariance reduction that pays for itself
Brute-force Monte Carlo is fine, but two cheap tricks cut the error for free and signal that you've done this before. Antithetic variates pair each draw z with −z, halving variance for payoffs that are monotone in z. A control variate subtracts something you can price exactly (the underlying, or a vanilla) and adds back its known mean powerful when the control is highly correlated with the payoff.
- Antithetic: average the z and −z payoffs into pair-means, then take the SE over the n/2 pairs not over all n.
- Control variate: price = mean(payoff − beta·(control − E[control])); beta≈1 when the control tracks the payoff.
- Report the SE before and after showing the error shrank is more convincing than claiming it did.
Diagnose convergence, don't assert it
A number on its own proves nothing. Plot the running estimate against the number of paths with a ±2·SE band and show it settling onto the closed form. A histogram of discounted payoffs explains the variance heavily skewed payoffs need more paths. These plots are quick and they are exactly what a Monte Carlo discussion is fishing for.
import matplotlib.pyplot as plt, numpy as np run = np.cumsum(payoff) / np.arange(1, n+1) se = payoff.std(ddof=1) / np.sqrt(np.arange(1, n+1)) plt.plot(run); plt.fill_between(range(n), run-2*se, run+2*se, alpha=.2) plt.axhline(bs_closed_form, ls='--') # should sit inside the band
How to approach a pricing take-home
- Read the spec and write the closed form first it's your benchmark for everything else.
- Then simulate, and validate the simulation against the closed form before trusting it on anything exotic.
- Always report a standard error; quote put-call parity and a limiting case (vol→0 → intrinsic) as checks.
- Comment on how the Greeks behave (delta→1 deep ITM, gamma peaks ATM) interviewers want the intuition, not just the numbers.
- Keep it runnable end to end with a fixed seed; a submission that reproduces is worth more than a faster one that doesn't.
Study alongside
© 2026 DeskPrep. All rights reserved. Licensed for personal interview-preparation use only. Not for redistribution, resale, publication or sharing. Terms & licence.