Quant Python 面试实战:从 NumPy 到 Backtest 的 30 道代码题精讲
量化面试的 Python 编程题与普通 SWE 面试有本质区别——它考察的是你用代码解决金融数学问题的能力。本文精选 30 道 Two Sigma、Citadel、Jane Street 等机构的 Python 面试真题,覆盖 NumPy 矩阵运算、统计建模、Monte Carlo 模拟和策略回测,每题附完整代码和解析。
## Quant Python 面试 vs. SWE Python 面试:核心差异
许多候选人用准备 LeetCode 的方式来备考 Quant Python 面试,结果往往表现不佳。理解两者的核心差异,是制定正确备考策略的前提。 | 维度 | SWE Python 面试 | Quant Python 面试 | |------|----------------|------------------| | 核心考察 | 算法复杂度、数据结构 | 数值计算、统计建模 | | 主要库 | 标准库、collections | NumPy、pandas、scipy | | 题目类型 | 字符串处理、图算法 | 矩阵运算、蒙特卡洛、回测 | | 代码风格 | 面向对象、设计模式 | 向量化计算、函数式 | | 评估标准 | 时间/空间复杂度 | 数值精度、计算效率 | **Quant Python 面试的核心能力**: 1. **向量化思维**:用 NumPy 向量操作替代 Python 循环,提升计算效率 100–1000 倍 2. **统计直觉**:理解统计量的含义,能够快速实现统计检验 3. **金融建模**:能够将金融概念(期望收益、风险、相关性)转化为代码 4. **调试能力**:能够识别数值计算中的常见陷阱(浮点精度、矩阵奇异性)
## 第一部分:NumPy 核心操作(题目 1–10)
**题目 1:向量化 vs. 循环** *题目*:计算两个 10,000 维向量的点积,比较循环方式和 NumPy 方式的性能差异。 ```python import numpy as np import time # 生成测试数据 a = np.random.randn(10000) b = np.random.randn(10000) # 方法 1:Python 循环(慢) start = time.time() dot_loop = sum(a[i] * b[i] for i in range(len(a))) loop_time = time.time() - start # 方法 2:NumPy 向量化(快) start = time.time() dot_numpy = np.dot(a, b) numpy_time = time.time() - start print(f"Loop: {loop_time*1000:.2f}ms, NumPy: {numpy_time*1000:.2f}ms") print(f"Speedup: {loop_time/numpy_time:.0f}x") # 典型输出:Loop: 3.5ms, NumPy: 0.02ms, Speedup: 175x ``` **题目 2:协方差矩阵计算** *题目*:给定 5 支股票的日收益率矩阵(252×5),计算年化协方差矩阵和相关系数矩阵。 ```python import numpy as np # 模拟日收益率数据(252 个交易日,5 支股票) np.random.seed(42) returns = np.random.randn(252, 5) * 0.01 # 1% 日波动率 # 计算协方差矩阵(年化:乘以 252) cov_matrix = np.cov(returns.T) * 252 print("年化协方差矩阵形状:", cov_matrix.shape) # (5, 5) # 计算相关系数矩阵 corr_matrix = np.corrcoef(returns.T) print("相关系数矩阵(对角线应为 1):") print(np.round(corr_matrix, 3)) # 验证:从协方差矩阵计算相关系数 std_devs = np.sqrt(np.diag(cov_matrix)) corr_from_cov = cov_matrix / np.outer(std_devs, std_devs) print("两种方法结果一致:", np.allclose(corr_matrix, corr_from_cov)) ``` **题目 3:矩阵分解与 PCA** *题目*:对股票收益率矩阵进行 PCA,提取前 3 个主成分,解释总方差的比例。 ```python import numpy as np np.random.seed(42) returns = np.random.randn(252, 10) * 0.01 # 标准化(零均值) returns_centered = returns - returns.mean(axis=0) # SVD 分解(PCA 的核心) U, S, Vt = np.linalg.svd(returns_centered, full_matrices=False) # 解释方差比例 explained_variance = S**2 / (len(returns) - 1) total_variance = explained_variance.sum() explained_ratio = explained_variance / total_variance print("前 3 个主成分解释方差比例:") for i in range(3): print(f" PC{i+1}: {explained_ratio[i]:.1%}") print(f" 累计: {explained_ratio[:3].sum():.1%}") # 投影到前 3 个主成分 pc_scores = returns_centered @ Vt[:3].T print("主成分得分形状:", pc_scores.shape) # (252, 3) ``` **题目 4:滚动窗口计算(高频考点)** *题目*:计算股价序列的 20 日滚动均值和滚动标准差,不使用 pandas。 ```python import numpy as np # 模拟股价数据 np.random.seed(42) prices = 100 * np.exp(np.cumsum(np.random.randn(252) * 0.01)) def rolling_stats(arr, window): """计算滚动均值和标准差(纯 NumPy 实现)""" n = len(arr) means = np.full(n, np.nan) stds = np.full(n, np.nan) for i in range(window - 1, n): window_data = arr[i - window + 1:i + 1] means[i] = np.mean(window_data) stds[i] = np.std(window_data, ddof=1) # ddof=1 使用样本标准差 return means, stds # 更高效的向量化实现(使用 stride tricks) def rolling_mean_vectorized(arr, window): """向量化滚动均值""" cumsum = np.cumsum(arr) cumsum[window:] = cumsum[window:] - cumsum[:-window] result = np.full(len(arr), np.nan) result[window-1:] = cumsum[window-1:] / window return result means, stds = rolling_stats(prices, 20) print(f"第 20–25 日的滚动均值: {means[19:25].round(2)}") ```
## 第二部分:统计建模与假设检验(题目 11–20)
**题目 11:线性回归从零实现** *题目*:不使用 sklearn,用 NumPy 实现 OLS 线性回归,计算系数、R² 和 t 统计量。 ```python import numpy as np def ols_regression(X, y): """ OLS 线性回归 X: (n, p) 特征矩阵(不含截距列) y: (n,) 目标向量 返回: coefficients, r_squared, t_stats, p_values """ n, p = X.shape # 添加截距列 X_with_intercept = np.column_stack([np.ones(n), X]) # OLS 估计:β = (X'X)^{-1} X'y XtX = X_with_intercept.T @ X_with_intercept Xty = X_with_intercept.T @ y beta = np.linalg.solve(XtX, Xty) # 比 inv 更稳定 # 残差和 R² y_pred = X_with_intercept @ beta residuals = y - y_pred ss_res = np.sum(residuals**2) ss_tot = np.sum((y - y.mean())**2) r_squared = 1 - ss_res / ss_tot # 标准误差和 t 统计量 sigma_sq = ss_res / (n - p - 1) # 无偏估计 var_beta = sigma_sq * np.linalg.inv(XtX) se_beta = np.sqrt(np.diag(var_beta)) t_stats = beta / se_beta return beta, r_squared, t_stats # 测试:Fama-French 单因子模型(CAPM) np.random.seed(42) n = 252 market_returns = np.random.randn(n) * 0.01 stock_returns = 0.5 + 1.2 * market_returns + np.random.randn(n) * 0.005 beta, r2, t_stats = ols_regression(market_returns.reshape(-1, 1), stock_returns) print(f"Alpha: {beta[0]:.4f}, Beta: {beta[1]:.4f}") print(f"R²: {r2:.4f}") print(f"t-stats: Alpha={t_stats[0]:.2f}, Beta={t_stats[1]:.2f}") ``` **题目 12:蒙特卡洛期权定价** *题目*:用 Monte Carlo 方法计算欧式看涨期权价格,并与 Black-Scholes 解析解比较。 ```python import numpy as np from scipy.stats import norm def black_scholes_call(S, K, T, r, sigma): """Black-Scholes 解析解""" 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 monte_carlo_call(S, K, T, r, sigma, n_simulations=100000): """Monte Carlo 期权定价""" np.random.seed(42) # 模拟到期时股价(GBM) Z = np.random.standard_normal(n_simulations) S_T = S * np.exp((r - 0.5*sigma**2)*T + sigma*np.sqrt(T)*Z) # 计算期权收益 payoffs = np.maximum(S_T - K, 0) # 折现期望收益 price = np.exp(-r*T) * np.mean(payoffs) std_error = np.exp(-r*T) * np.std(payoffs) / np.sqrt(n_simulations) return price, std_error # 参数 S, K, T, r, sigma = 100, 100, 1, 0.05, 0.20 bs_price = black_scholes_call(S, K, T, r, sigma) mc_price, mc_se = monte_carlo_call(S, K, T, r, sigma) print(f"Black-Scholes 价格: £{bs_price:.4f}") print(f"Monte Carlo 价格: £{mc_price:.4f} ± £{mc_se:.4f}") print(f"误差: {abs(mc_price - bs_price):.4f}") ``` **题目 13:时间序列平稳性检验(ADF 检验)** ```python import numpy as np from statsmodels.tsa.stattools import adfuller # 模拟两种时间序列 np.random.seed(42) n = 500 # 非平稳序列(随机游走) random_walk = np.cumsum(np.random.randn(n)) # 平稳序列(AR(1) with |φ| < 1) ar1 = np.zeros(n) for t in range(1, n): ar1[t] = 0.7 * ar1[t-1] + np.random.randn() # ADF 检验 for name, series in [("随机游走", random_walk), ("AR(1)", ar1)]: result = adfuller(series) print(f"\n{name}:") print(f" ADF 统计量: {result[0]:.4f}") print(f" p-value: {result[1]:.4f}") is_stationary = result[1] < 0.05; print(" 结论: " + ("平稳" if is_stationary else "非平稳")) ```
## 第三部分:策略回测框架(题目 21–30)
**题目 21:简单动量策略回测** *题目*:实现一个基于 12 个月动量的股票选择策略,计算年化收益、夏普比率和最大回撤。 ```python import numpy as np import pandas as pd def momentum_backtest(prices_df, lookback=252, holding=21): """ 动量策略回测 prices_df: DataFrame,行为日期,列为股票 lookback: 动量计算窗口(交易日) holding: 持仓周期(交易日) """ returns = prices_df.pct_change() # 计算动量信号(过去 lookback 日收益率) momentum = prices_df.pct_change(lookback) # 每 holding 天重新平衡 portfolio_returns = [] for t in range(lookback, len(prices_df) - holding, holding): # 选择动量最强的前 20% 股票(做多) signal = momentum.iloc[t] n_stocks = len(signal.dropna()) n_long = max(1, n_stocks // 5) long_stocks = signal.nlargest(n_long).index # 等权重持仓 period_returns = returns.iloc[t:t+holding][long_stocks].mean(axis=1) portfolio_returns.extend(period_returns.tolist()) portfolio_returns = np.array(portfolio_returns) # 计算绩效指标 annual_return = np.mean(portfolio_returns) * 252 annual_vol = np.std(portfolio_returns) * np.sqrt(252) sharpe = annual_return / annual_vol # 最大回撤 cumulative = np.cumprod(1 + portfolio_returns) running_max = np.maximum.accumulate(cumulative) drawdowns = (cumulative - running_max) / running_max max_drawdown = drawdowns.min() return { 'annual_return': annual_return, 'annual_vol': annual_vol, 'sharpe_ratio': sharpe, 'max_drawdown': max_drawdown } # 模拟测试 np.random.seed(42) n_days, n_stocks = 1260, 50 # 5 年数据,50 支股票 prices = pd.DataFrame( 100 * np.exp(np.cumsum(np.random.randn(n_days, n_stocks) * 0.01, axis=0)), columns=[f'Stock_{i}' for i in range(n_stocks)] ) results = momentum_backtest(prices) for key, val in results.items(): print(f"{key}: {val:.2%}") ``` **题目 22:配对交易(Pairs Trading)** ```python import numpy as np from statsmodels.regression.linear_model import OLS from statsmodels.tsa.stattools import coint def pairs_trading_signal(price_a, price_b, window=60, entry_z=2.0, exit_z=0.5): """ 配对交易信号生成 返回:+1(做多价差),-1(做空价差),0(无仓位) """ n = len(price_a) signals = np.zeros(n) for t in range(window, n): # 在滚动窗口内估计对冲比率 y = price_a[t-window:t] x = price_b[t-window:t] # OLS 回归:price_a = β × price_b + ε beta = np.cov(y, x)[0, 1] / np.var(x) spread = y - beta * x # 计算当前价差的 z-score spread_mean = np.mean(spread) spread_std = np.std(spread) current_spread = price_a[t] - beta * price_b[t] z_score = (current_spread - spread_mean) / spread_std # 生成信号 if z_score > entry_z: signals[t] = -1 # 价差过高,做空价差(卖 A 买 B) elif z_score < -entry_z: signals[t] = 1 # 价差过低,做多价差(买 A 卖 B) elif abs(z_score) < exit_z: signals[t] = 0 # 价差回归,平仓 else: signals[t] = signals[t-1] # 保持前一仓位 return signals # 测试 np.random.seed(42) n = 500 common_factor = np.cumsum(np.random.randn(n) * 0.01) price_a = 100 * np.exp(common_factor + np.cumsum(np.random.randn(n) * 0.005)) price_b = 100 * np.exp(common_factor + np.cumsum(np.random.randn(n) * 0.005)) signals = pairs_trading_signal(price_a, price_b) print(f"做多信号次数: {(signals == 1).sum()}") print(f"做空信号次数: {(signals == -1).sum()}") ``` ### 想要完整的 30 题代码题库? **AT&T Career Quant Track 提供包含 100+ Python 量化编程题的完整题库,以及 1 对 1 代码审查服务。** 我们的导师来自 Two Sigma、Citadel 等顶级机构,能够提供最接近真实面试的代码题训练。 **预约免费 Python 量化编程诊断 →**
Quant 面试中最常用的 Python 库有哪些?
核心库:NumPy(数值计算)、pandas(数据处理)、scipy(统计函数)、statsmodels(统计建模)。可视化:matplotlib、seaborn。机器学习:scikit-learn。金融专用:QuantLib(衍生品定价)、zipline(回测)、pyfolio(绩效分析)。建议重点掌握 NumPy 和 pandas,这是 Quant 面试中最高频的工具。
Jane Street 的 Python 面试和 Two Sigma 的有什么区别?
Jane Street 的编程面试更注重算法和数学问题的实现,题目通常更抽象(如实现一个随机游走模拟器)。Two Sigma 的编程面试更注重数据分析和统计建模,通常会给你一个真实数据集,要求你进行探索性分析和建模。建议根据目标机构调整备考重点。
向量化计算为什么比循环快这么多?
NumPy 的向量化操作底层使用 C 语言实现,并利用 CPU 的 SIMD(单指令多数据)指令集进行并行计算。Python 循环每次迭代都有解释器开销,而 NumPy 将整个数组操作编译为单个 C 函数调用。对于大型数组,向量化通常快 100–1000 倍。
回测中最常见的陷阱是什么?
最常见的回测陷阱:(1) 前视偏差(Look-ahead Bias):使用了未来数据;(2) 幸存者偏差(Survivorship Bias):只使用了现存股票的历史数据;(3) 过拟合(Overfitting):策略参数在样本内表现很好,但样本外失效;(4) 交易成本忽略:没有考虑买卖价差和市场冲击;(5) 流动性假设:假设可以以任意价格成交任意数量。
AT&T Career