Quant Interview Questions & Solutions
Real questions from quant interviews at London hedge funds and investment banks. With Python verification code for every answer.
Probability Puzzles
"Two dice. What is the probability the sum is 7?"
There are 36 equally likely outcomes (6 × 6). The pairs that sum to 7 are: (1,6), (2,5), (3,4), (4,3), (5,2), (6,1). That is 6 outcomes. So the probability is 6/36 = 1/6 ≈ 16.7%.
from itertools import product
# Q: Two dice, probability of sum = 7?
outcomes = list(product(range(1, 7), range(1, 7)))
sum_7 = [o for o in outcomes if sum(o) == 7]
print(f"Pairs summing to 7: {sum_7}")
print(f"Probability: {len(sum_7)}/{len(outcomes)} = {len(sum_7)/len(outcomes):.4f}")
print(f"= 1/6
")"Pick 2 cards from a deck. Probability both are aces?"
First card is an ace: 4/52. Given the first was an ace, second is an ace: 3/51. Multiply: (4/52) × (3/51) = 12/2652 = 1/221 ≈ 0.45%.
# Q: Pick 2 cards, probability both aces?
# First ace: 4/52, second ace: 3/51
p = (4/52) * (3/51)
print(f"P(both aces) = (4/52) x (3/51) = {p:.6f}")
print(f"= 1/{1/p:.0f} = 1/221
")
# Verify by simulation
import numpy as np
n_sims = 1000000
deck = list(range(52)) # 0-3 are aces
count = 0
for _ in range(n_sims):
draw = np.random.choice(deck, 2, replace=False)
if draw[0] < 4 and draw[1] < 4:
count += 1
print(f"Simulated: {count/n_sims:.6f} (should be ~0.004525)")"Explain the Monty Hall problem."
Three doors. One hides a car, two hide goats. You pick a door. The host (who knows where the car is) opens a different door revealing a goat. Should you switch? Yes — switching wins 2/3 of the time. Your initial pick had a 1/3 chance. The remaining door now carries the other 2/3 probability because the host's reveal is not random.
# Monty Hall Problem Simulation
import numpy as np
def monty_hall(n_games=100000):
"""Simulate the Monty Hall problem."""
wins_stay = 0
wins_switch = 0
for _ in range(n_games):
# Car is behind one of 3 doors
car = np.random.randint(0, 3)
# Player picks a door
choice = np.random.randint(0, 3)
# Host opens a door with a goat (not player's, not car)
# If player switches, they win iff original choice was wrong
if choice == car:
wins_stay += 1
else:
wins_switch += 1
print(f"Stay: {wins_stay/n_games:.4f} (should be ~0.3333)")
print(f"Switch: {wins_switch/n_games:.4f} (should be ~0.6667)")
print(f"
Switching wins 2/3 of the time!")
monty_hall()Statistics Questions
"Variance of the sum of independent random variables?"
Var(X + Y) = Var(X) + Var(Y) when X and Y are independent. The covariance term vanishes. If they are not independent, add 2·Cov(X,Y).
"Expected value of the maximum of two Uniform[0,1] random variables?"
E[max(X,Y)] = 2/3. The CDF of max(X,Y) is P(max ≤ z) = z² (both must be ≤ z). The PDF is 2z. So E[max] = ∫z·2z dz from 0 to 1 = 2/3.
"Central Limit Theorem in one sentence?"
The sum (or average) of a large number of independent random variables, regardless of their individual distributions, converges to a normal distribution.
# Variance of sum of independent RVs
# Var(X + Y) = Var(X) + Var(Y) when independent
import numpy as np
# Verify: X ~ Uniform(0,1), Y ~ Uniform(0,1)
n = 1000000
X = np.random.uniform(0, 1, n)
Y = np.random.uniform(0, 1, n)
print(f"Var(X) = {np.var(X):.4f} (theory: 1/12 = {1/12:.4f})")
print(f"Var(Y) = {np.var(Y):.4f}")
print(f"Var(X+Y) = {np.var(X+Y):.4f} (theory: 1/6 = {1/6:.4f})")
print(f"Var(X) + Var(Y) = {np.var(X) + np.var(Y):.4f}
")
# Expected value of max of two Uniform[0,1]
Z = np.maximum(X, Y)
print(f"E[max(X,Y)] = {np.mean(Z):.4f} (theory: 2/3 = {2/3:.4f})")
print(f"Derivation: integral of 2x*x dx from 0 to 1 = 2/3
")
# CLT in one sentence:
print("CLT: The sum of n independent random variables,")
print("regardless of distribution, approaches a normal")
print("distribution as n approaches infinity.")Brainteasers
"12 coins, one is a different weight. Find it in 3 weighings."
This is a classic 3.67-star difficulty problem. The key insight is information-theoretic: 3 weighings on a balance scale give 3³ = 27 possible outcomes (left heavy, balanced, right heavy for each weighing). You need to distinguish 24 possibilities (12 coins × heavier or lighter). Since 27 ≥ 24, it is theoretically possible. The challenge is constructing the actual scheme.
# 12 Coins Puzzle Strategy
# You have 12 coins. One is a different weight (heavier or lighter).
# Find it in 3 weighings.
def solve_12_coins():
"""
Strategy for the 12 coins problem.
Weighing 1: Compare coins [1,2,3,4] vs [5,6,7,8]
Case A: They balance -> odd coin is in [9,10,11,12]
Case B: Left heavy -> odd coin makes left heavy or right light
Case C: Left light -> odd coin makes left light or right heavy
Each case uses 2 more weighings to identify the coin
AND whether it is heavier or lighter.
Key insight: 3 weighings give 3^3 = 27 outcomes.
We need to distinguish 24 possibilities (12 coins x 2 states).
27 >= 24, so it is information-theoretically possible.
"""
print("12 Coins Solution:")
print(" Weighing 1: [1,2,3,4] vs [5,6,7,8]")
print(" If balanced:")
print(" Weighing 2: [9,10,11] vs [1,2,3]")
print(" If balanced: coin 12 is odd, weigh vs any to find H/L")
print(" If 9,10,11 heavy: weigh 9 vs 10")
print(" Balanced -> 11 is heavy; else heavier one is odd")
print(" If [1,2,3,4] heavy:")
print(" Weighing 2: [1,2,5] vs [3,6,9]")
print(" This narrows suspects based on which side tips")
print(" Weighing 3: identifies exact coin")
print()
print(" Information theory: 3 weighings -> 27 outcomes")
print(" Need to distinguish: 12 coins x 2 (H or L) = 24 cases")
print(" 27 >= 24, so 3 weighings SUFFICE")
solve_12_coins()"How many piano tuners are there in London?"
This is a Fermi estimation question. The interviewer does not care about the exact answer. They want to see you decompose the problem into estimable components, make reasonable assumptions, and arrive at a ballpark figure with clear reasoning.
# Fermi Estimation: Piano Tuners in London
def piano_tuners_london():
"""
Structured Fermi estimation.
The interviewer wants to see your PROCESS, not the exact number.
"""
london_pop = 9_000_000
people_per_household = 2.5
households = london_pop / people_per_household # 3,600,000
pct_with_piano = 0.05 # 5% of households
pianos_home = households * pct_with_piano # 180,000
# Add churches, schools, concert halls, studios
institutional_pianos = 20_000
total_pianos = pianos_home + institutional_pianos # 200,000
tunings_per_year = 1 # Average piano tuned once a year
total_tunings = total_pianos * tunings_per_year # 200,000
tunings_per_day = 4 # A tuner does about 4 per day
working_days = 250
tunings_per_tuner = tunings_per_day * working_days # 1,000
n_tuners = total_tunings / tunings_per_tuner # 200
print(f"London population: {london_pop:,}")
print(f"Households: {households:,.0f}")
print(f"Pianos (homes): {pianos_home:,.0f}")
print(f"Pianos (institutional): {institutional_pianos:,}")
print(f"Total pianos: {total_pianos:,}")
print(f"Annual tunings: {total_tunings:,}")
print(f"Tunings per tuner/year: {tunings_per_tuner:,}")
print(f"
Estimated piano tuners in London: ~{n_tuners:.0f}")
piano_tuners_london()Finance Questions
"What is put-call parity?"
C - P = S - K·e^(-rT)
A call minus a put equals the forward price of the stock minus the present value of the strike.
This is a no-arbitrage relationship. If it is violated, you can construct a risk-free profit. It is one of the most fundamental results in options pricing and connects calls, puts, and forwards.
"What is the duration of a zero-coupon bond?"
Exactly equal to its maturity. A 5-year zero-coupon bond has a duration of 5 years. This is the maximum possible duration for any bond of that maturity, because coupon-paying bonds have durations shorter than their maturity (the coupons pull the weighted average cash flow forward).
"What happens to a call price when volatility increases?"
It increases. Always. Both calls and puts become more valuable when volatility rises because the expected payoff increases while the downside is capped at zero. This is captured by vega, which is always positive for long options.
# Put-Call Parity: C - P = S - K*exp(-rT)
import numpy as np
from scipy.stats import norm
def bs_call(S, K, T, r, sigma):
d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
d2 = d1 - sigma*np.sqrt(T)
return S*norm.cdf(d1) - K*np.exp(-r*T)*norm.cdf(d2)
def bs_put(S, K, T, r, sigma):
d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
d2 = d1 - sigma*np.sqrt(T)
return K*np.exp(-r*T)*norm.cdf(-d2) - S*norm.cdf(-d1)
S, K, T, r, sigma = 100, 100, 1, 0.05, 0.2
C = bs_call(S, K, T, r, sigma)
P = bs_put(S, K, T, r, sigma)
print("Put-Call Parity Verification:")
print(f" C - P = {C - P:.4f}")
print(f" S - K*exp(-rT) = {S - K*np.exp(-r*T):.4f}")
print(f" Equal? {abs((C-P) - (S - K*np.exp(-r*T))) < 1e-10}
")
# Duration of zero-coupon bond
print("Duration of a zero-coupon bond = T (time to maturity)")
print(" A 5-year zero has duration exactly 5 years.")
print(" This is the MAXIMUM duration for any bond of that maturity.
")
# Vol and call price
for vol in [0.10, 0.15, 0.20, 0.30, 0.40, 0.50]:
c = bs_call(100, 100, 1, 0.05, vol)
print(f" sigma={vol:.2f} Call={c:.2f}")
print(" Call price ALWAYS increases with volatility (positive vega)")Interview Tips
Think out loud. The interviewer wants to see your reasoning process, not just the answer.
State assumptions. For Fermi questions, explicitly state each assumption so the interviewer can adjust.
Sanity check. After getting an answer, verify it makes sense. Does the order of magnitude feel right?
Know your edge cases. What happens at the extremes? What if T goes to 0? What if volatility goes to infinity?
Code it. If you can verify your answer with a quick simulation, it demonstrates both mathematical understanding and programming skill. Many London firms now expect candidates to code during interviews.