DeskPrep
Coding Guide· 14 min read

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.

vectorised terminal price
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.

implied vol via a bracketed solve
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 divergence

Make 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.

shape, not the answer
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 parity

Variance 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.

convergence band
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.

Python for Quant Work — Coding Guide | DeskPrep