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.