Skip to content

20.2 编程与算法题

编程是量化面试的核心考察维度——毕竟量化交易的本质是将交易思想转化为代码。编程面试通常分为三个层次:LeetCode 风格的算法题、Pandas/NumPy 数据处理题、以及系统设计题。本章覆盖各层次的高频考点和实战技巧。

LeetCode高频题在量化面试中的变形

量化面试中的算法题与科技公司面试有显著差异——更注重数值计算、概率相关算法和高效的数据处理,而不是纯粹的图论或动态规划。

典型高频题

1. 蓄水池抽样(Reservoir Sampling)

从一个未知大小的数据流中均匀随机选取 k 个样本。这在处理 tick 级行情数据时非常实用。

PYTHON68 行 · 2.2 KB
📄此处有展示代码68 行 · 2.2 KB展开 ▼
python
import random


def reservoir_sampling(stream, k: int) -> list:
    """
    蓄水池抽样算法
    从流式数据中均匀地随机抽取 k 个元素
    
    时间复杂度: O(n), 空间复杂度: O(k)
    """
    reservoir = []
    
    for i, item in enumerate(stream):
        if i < k:
            reservoir.append(item)
        else:
            # 以 k/(i+1) 的概率替换
            j = random.randint(0, i)
            if j < k:
                reservoir[j] = item
    
    return reservoir


# 验证均匀性
def verify_reservoir_sampling(n_trials: int = 10000, 
                                stream_size: int = 100,
                                k: int = 10) -> dict:
    """验证蓄水池抽样的均匀性"""
    counts = {i: 0 for i in range(stream_size)}
    
    for _ in range(n_trials):
        stream = range(stream_size)
        sampled = reservoir_sampling(stream, k)
        for item in sampled:
            counts[item] += 1
    
    expected = n_trials * k / stream_size
    chi_sq = sum((c - expected) ** 2 / expected for c in counts.values())
    
    return {'expected_count': expected, 'chi_squared': chi_sq, 'counts': counts}


random.seed(42)

# 1) 单次抽样看结果
sample = reservoir_sampling(range(1000), k=10)
print(f"从 0..999 的流中抽 10 个: {sorted(sample)}")

# 2) 均匀性验证
res = verify_reservoir_sampling(n_trials=10000, stream_size=100, k=10)
counts = res['counts']
expected = res['expected_count']
chi_sq = res['chi_squared']

print(f"\n均匀性验证 (10000 次试验, 流长 100, k=10):")
print(f"  每个元素的期望入选次数: {expected:.0f}")
print(f"  实际最少 / 最多:        {min(counts.values())} / {max(counts.values())}")
print(f"  卡方统计量:             {chi_sq:.2f}  (自由度 99)")

# 自由度 99 时 chi2 的 95% 临界值约 123.2;均匀分布下 chi_sq 应在 99 附近
verdict = "通过(无法拒绝均匀假设)" if chi_sq < 123.2 else "未通过(分布可疑)"
print(f"  95% 临界值 123.2 → {verdict}")

print("\n  头部/尾部元素入选次数抽查(应大致相同,无位置偏好):")
for i in [0, 1, 2, 49, 50, 97, 98, 99]:
    bar = '#' * int(counts[i] / expected * 30)
    print(f"    元素 {i:>3}: {counts[i]:>5}{bar}")
点击展开可浏览运行结果
从 0..999 的流中抽 10 个: [18, 81, 235, 288, 430, 538, 562, 599, 663, 989]

均匀性验证 (10000 次试验, 流长 100, k=10):
  每个元素的期望入选次数: 1000
  实际最少 / 最多:        916 / 1084
  卡方统计量:             72.09  (自由度 99)
  95% 临界值 123.2 → 通过(无法拒绝均匀假设)

  头部/尾部元素入选次数抽查(应大致相同,无位置偏好):
    元素   0:   947 次  ############################
    元素   1:  1032 次  ##############################
    元素   2:   978 次  #############################
    元素  49:  1084 次  ################################
    元素  50:  1005 次  ##############################
    元素  97:   999 次  #############################
    元素  98:  1012 次  ##############################
    元素  99:   965 次  ############################

2. 快速幂(Fast Exponentiation)

在期权定价和蒙特卡洛模拟中经常需要计算 (1+r)n 等复利。快速幂能将时间复杂度从 O(n) 降到 O(logn)

PYTHON44 行 · 1.2 KB
📄此处有展示代码44 行 · 1.2 KB展开 ▼
python
def fast_pow(base: float, exp: int) -> float:
    """
    二分快速幂算法
    时间复杂度: O(log n)
    """
    result = 1.0
    current = base
    
    while exp > 0:
        if exp & 1:  # 当前位为1
            result *= current
        current *= current  # 底数平方
        exp >>= 1  # 右移一位
    
    return result


def matrix_pow(matrix: list, exp: int) -> list:
    """矩阵快速幂(用于马尔可夫链状态转移等场景)"""
    n = len(matrix)
    
    # 单位矩阵
    result = [[1.0 if i == j else 0.0 for j in range(n)] for i in range(n)]
    base = [row[:] for row in matrix]
    
    while exp > 0:
        if exp & 1:
            # result = result * base
            new_result = [[0.0] * n for _ in range(n)]
            for i in range(n):
                for j in range(n):
                    new_result[i][j] = sum(result[i][k] * base[k][j] for k in range(n))
            result = new_result
        
        # base = base * base
        new_base = [[0.0] * n for _ in range(n)]
        for i in range(n):
            for j in range(n):
                new_base[i][j] = sum(base[i][k] * base[k][j] for k in range(n))
        base = new_base
        
        exp >>= 1
    
    return result
点击展开可浏览运行结果
📘 本段代码定义了 2 个函数/类:函数 `fast_pow`(二分快速幂算法)、函数 `matrix_pow`(矩阵快速幂(用于马尔可夫链状态转移等场景))。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

3. 在线中位数(数据流中位数)

在实时风控中,需要维护滑动窗口内的中位数来进行异常检测。使用两个堆可以实现 O(logn) 的插入和 O(1) 的查询。

PYTHON29 行 · 1.0 KB
📄此处有展示代码29 行 · 1.0 KB展开 ▼
python
import heapq


class MedianFinder:
    """数据流中位数维护器"""
    
    def __init__(self):
        self.max_heap = []  # 存储较小的一半(取负实现大根堆)
        self.min_heap = []  # 存储较大的一半(小根堆)
    
    def add(self, num: float):
        """添加一个数值"""
        if not self.max_heap or num <= -self.max_heap[0]:
            heapq.heappush(self.max_heap, -num)
        else:
            heapq.heappush(self.min_heap, num)
        
        # 平衡两个堆(大小差不超过1)
        if len(self.max_heap) > len(self.min_heap) + 1:
            heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))
        elif len(self.min_heap) > len(self.max_heap):
            heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))
    
    def median(self) -> float:
        """查询当前中位数"""
        if len(self.max_heap) > len(self.min_heap):
            return -self.max_heap[0]
        else:
            return (-self.max_heap[0] + self.min_heap[0]) / 2.0
点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:类 `MedianFinder`(数据流中位数维护器)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

数据处理题:Pandas/NumPy核心操作

这是量化面试中最具区分度的环节——考察你能否熟练地用 Python 处理真实的金融数据。

Pandas 核心技巧

1. 高效的多股票收益率计算

PYTHON31 行 · 920 B
📄此处有展示代码31 行 · 920 B展开 ▼
python
import pandas as pd
import numpy as np


def efficient_return_calculation(prices: pd.DataFrame, 
                                   periods: list = [1, 5, 20]) -> pd.DataFrame:
    """
    快速计算多个周期的收益率并处理异常值
    
    Parameters
    ----------
    prices : pd.DataFrame
        index=date, columns=stock_codes, values=close_price
    periods : list
        需要计算的收益周期列表
    """
    result = pd.DataFrame(index=prices.index)
    
    for p in periods:
        # 使用 pct_change 的 fill_method=None 避免前视偏差
        ret = prices.pct_change(p, fill_method=None)
        
        # 异常值处理:Winsorize
        for col in ret.columns:
            lower = ret[col].quantile(0.01)
            upper = ret[col].quantile(0.99)
            ret[col] = ret[col].clip(lower, upper)
        
        result[f'ret_{p}d'] = ret.stack()
    
    return result
点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:函数 `efficient_return_calculation`(快速计算多个周期的收益率并处理异常值)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

2. 滚动因子计算

PYTHON28 行 · 954 B
📄此处有展示代码28 行 · 954 B展开 ▼
python
def rolling_factor_calculation(data: pd.DataFrame) -> pd.DataFrame:
    """
    计算常用的滚动因子
    
    演示如何用 rolling + transform 高效计算
    """
    df = data.copy()
    
    # 动量因子:最近20日累计收益
    df['momentum_20d'] = df.groupby('code')['close'].transform(
        lambda x: x.pct_change(20, fill_method=None)
    )
    
    # 波动率因子:最近20日收益标准差
    df['volatility_20d'] = df.groupby('code')['close'].transform(
        lambda x: x.pct_change().rolling(20).std()
    )
    
    # 换手率加权因子:最近5日与20日成交量的比值
    df['volume_ratio'] = df.groupby('code')['volume'].transform(
        lambda x: x.rolling(5).mean() / x.rolling(20).mean()
    )
    
    # 截面排名(cross-sectional rank)
    for col in ['momentum_20d', 'volatility_20d', 'volume_ratio']:
        df[f'{col}_rank'] = df.groupby('date')[col].rank(pct=True)
    
    return df
点击展开可浏览运行结果
📘 本段代码定义了 1 个函数/类:函数 `rolling_factor_calculation`(计算常用的滚动因子)。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

3. DataFrame 合并与对齐的高级操作

PYTHON64 行 · 3.1 KB
📄此处有展示代码64 行 · 3.1 KB展开 ▼
python
import numpy as np
import pandas as pd


def advanced_merge_operations():
    """量化中最常被追问的三个 DataFrame 合并/对齐场景。"""

    # ---------- 场景1:(date, code) 双键对齐,行情与因子的并集 ----------
    quotes = pd.DataFrame({
        'date': pd.to_datetime(['2024-01-02', '2024-01-02', '2024-01-03', '2024-01-03']),
        'code': ['600519', '000001', '600519', '000001'],
        'close': [1680.0, 10.5, 1702.0, 10.4],
    })
    factors = pd.DataFrame({
        'date': pd.to_datetime(['2024-01-02', '2024-01-03', '2024-01-03']),
        'code': ['600519', '600519', '300750'],   # 000001 缺因子,300750 缺行情
        'value': [0.82, 0.91, -0.35],
    })
    merged = quotes.merge(factors, on=['date', 'code'], how='outer')
    print("【场景1】(date, code) 外连接 —— 缺口一目了然")
    print(merged.to_string(index=False))
    print(f"  close 缺失 {merged['close'].isna().sum()} 行,"
          f"value 缺失 {merged['value'].isna().sum()} 行")
    # 因子按代码前值填充(注意先排序,且绝不能跨代码填充)
    merged['value_ffill'] = merged.sort_values('date').groupby('code')['value'].ffill()
    print(f"  按 code 分组 ffill 后 value 缺失降至 {merged['value_ffill'].isna().sum()}\n")

    # ---------- 场景2:merge_asof —— 委托时间戳配最近一笔行情 ----------
    orders = pd.DataFrame({
        'timestamp': pd.to_datetime(['2024-01-02 09:30:05', '2024-01-02 09:30:12',
                                     '2024-01-02 09:30:31']),
        'code': ['600519', '600519', '600519'],
        'side': ['BUY', 'SELL', 'BUY'],
    })
    prices = pd.DataFrame({
        'timestamp': pd.to_datetime(['2024-01-02 09:30:00', '2024-01-02 09:30:10',
                                     '2024-01-02 09:30:20', '2024-01-02 09:30:30']),
        'code': ['600519'] * 4,
        'price': [1680.0, 1681.5, 1680.8, 1682.2],
    })
    asof = pd.merge_asof(orders, prices, on='timestamp', by='code', direction='backward')
    print("【场景2】merge_asof(direction='backward') —— 只用委托时刻已知的行情")
    print(asof.to_string(index=False))
    print("  用 direction='forward' 就变成未来函数:拿到了下一笔还没发生的价格\n")

    # ---------- 场景3:截面标准化,transform 优于 apply ----------
    np.random.seed(7)
    panel = pd.DataFrame({
        'date': np.repeat(pd.to_datetime(['2024-01-02', '2024-01-03']), 4),
        'code': ['600519', '000001', '300750', '601318'] * 2,
        'factor': np.random.randn(8).round(3),
    })
    panel['z'] = panel.groupby('date')['factor'].transform(lambda x: (x - x.mean()) / x.std())
    print("【场景3】groupby.transform 做截面 z-score(形状不变,可直接赋列)")
    print(panel.to_string(index=False))
    chk = panel.groupby('date')['z'].agg(['mean', 'std']).round(6)
    print("  每个截面 z 的均值/标准差:")
    print(chk.to_string())
    print("  apply 返回的是嵌套结构,需要 reset_index 才能贴回原表 —— 这是面试常考的差别")

    return merged, asof, panel


advanced_merge_operations()
点击展开可浏览运行结果
【场景1】(date, code) 外连接 —— 缺口一目了然
      date   code  close  value
2024-01-02 000001   10.5    NaN
2024-01-02 600519 1680.0   0.82
2024-01-03 000001   10.4    NaN
2024-01-03 300750    NaN  -0.35
2024-01-03 600519 1702.0   0.91
  close 缺失 1 行,value 缺失 2 行
  按 code 分组 ffill 后 value 缺失降至 2 行

【场景2】merge_asof(direction='backward') —— 只用委托时刻已知的行情
          timestamp   code side  price
2024-01-02 09:30:05 600519  BUY 1680.0
2024-01-02 09:30:12 600519 SELL 1681.5
2024-01-02 09:30:31 600519  BUY 1682.2
  用 direction='forward' 就变成未来函数:拿到了下一笔还没发生的价格

【场景3】groupby.transform 做截面 z-score(形状不变,可直接赋列)
      date   code  factor         z
2024-01-02 600519   1.691  1.382308
2024-01-02 000001  -0.466 -0.957149
2024-01-02 300750   0.033 -0.415940
2024-01-02 601318   0.408 -0.009219
2024-01-03 600519  -0.789 -0.183790
2024-01-03 000001   0.002  0.764840
2024-01-03 300750  -0.001  0.761242
2024-01-03 601318  -1.755 -1.342293
  每个截面 z 的均值/标准差:
            mean  std
date                 
2024-01-02   0.0  1.0
2024-01-03   0.0  1.0
  apply 返回的是嵌套结构,需要 reset_index 才能贴回原表 —— 这是面试常考的差别

NumPy 核心技巧

1. 向量化优于循环

PYTHON12 行 · 385 B
📄此处有展示代码12 行 · 385 B展开 ▼
python
# 错误做法
def slow_correlation_matrix(returns: np.ndarray) -> np.ndarray:
    n = returns.shape[1]
    corr = np.zeros((n, n))
    for i in range(n):
        for j in range(n):
            corr[i, j] = np.corrcoef(returns[:, i], returns[:, j])[0, 1]
    return corr

# 正确做法
def fast_correlation_matrix(returns: np.ndarray) -> np.ndarray:
    return np.corrcoef(returns.T)
点击展开可浏览运行结果
📘 本段代码定义了 2 个函数/类:函数 `slow_correlation_matrix`、函数 `fast_correlation_matrix`。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

2. 广播(Broadcasting)机制

PYTHON15 行 · 574 B
📄此处有展示代码15 行 · 574 B展开 ▼
python
def broadcasting_examples():
    """
    NumPy 广播机制是量化计算加速的关键
    """
    # 示例:计算所有股票对的价格比率
    prices = np.array([10, 20, 30, 40, 50])  # (5,)
    ratios = prices[:, np.newaxis] / prices[np.newaxis, :]  # (5, 5)
    
    # 示例:按行/列标准化
    returns = np.random.randn(100, 50)  # 100天, 50只股票
    row_mean = returns.mean(axis=1, keepdims=True)  # (100, 1)
    row_std = returns.std(axis=1, keepdims=True)    # (100, 1)
    normalized = (returns - row_mean) / row_std
    
    return normalized
点击展开可浏览运行结果
--- 结果 ---
--- broadcasting_examples() ---
ndarray array([[-1.49428528, -0.81031696,  1.53556644, ...,  0.85659982,
        -0.77393081,  0.61654908],
       [ 0.9614732 ,  0.48771949,  0.24524353, ..., -1.75775581,
        -0.36113835,  0.73517147],
       [ 1.66584358,  0.85300898, -0.16735648, ..., -0.8679565 ,
        -0.73868999, -0.98711315],

系统设计题

量化面试中的系统设计题聚焦于真实交易系统的架构:

实时行情系统设计

需求:设计一个能处理 5000+ 只股票、每秒数万条 tick 数据的实时行情系统。

关键考虑

  1. 数据接入:多路数据源冗余(防止单点故障),使用消息队列(Kafka/Pulsar)解耦。
  2. 数据存储
    • 热数据(最近数日):内存数据库(Redis)
    • 温数据(最近数月):列式存储(ClickHouse/DolphinDB)
    • 冷数据(历史存档):对象存储(S3/MinIO)
  3. 数据分发:发布-订阅模式,策略系统按需订阅。
  4. 延迟优化:采用零拷贝(zero-copy)、内存映射文件(mmap)、内核旁路(kernel bypass)技术。
  5. 容错:数据源自动切换、断线重连和回补机制。

回测系统设计

核心模块

┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  数据引擎     │    │  策略引擎     │    │  执行模拟     │
│  - 历史数据    │───▶│  - 信号生成    │───▶│  - 订单管理    │
│  - 复权处理    │    │  - 组合构建    │    │  - 成交模拟    │
│  - 数据对齐    │    │  - 风控约束    │    │  - 成本估算    │
└──────────────┘    └──────────────┘    └──────────────┘
        │                                      │
        └──────────  ┌──────────────┐ ─────────┘
                     │  分析引擎     │
                     │  - 绩效统计    │
                     │  - 风险分析    │
                     │  - 归因报告    │
                     └──────────────┘

面试要点

  • 事件驱动架构 vs 向量化架构的取舍
  • 如何处理前视偏差(Look-ahead Bias)
  • 生存偏差(Survivorship Bias)的修正方法
  • 如何在保证灵活性的同时控制计算复杂度

Python常用技巧与陷阱

高频技巧

PYTHON44 行 · 1.2 KB
📄此处有展示代码44 行 · 1.2 KB展开 ▼
python
# 1. 使用 collections.defaultdict 简化分组逻辑
from collections import defaultdict
by_sector = defaultdict(list)
for stock in stocks:
    by_sector[stock['sector']].append(stock)

# 2. 使用 itertools 进行高效迭代
from itertools import combinations, product, chain
# 所有股票的配对组合
pairs = list(combinations(stock_list, 2))

# 3. 使用 functools.lru_cache 缓存计算结果
from functools import lru_cache

@lru_cache(maxsize=128)
def compute_black_scholes(S, K, T, r, sigma, option_type):
    # 期权定价计算...
    pass

# 4. 使用 dataclass 管理策略参数
from dataclasses import dataclass, field

@dataclass
class StrategyConfig:
    name: str
    lookback_window: int = 60
    rebalance_frequency: str = 'monthly'
    max_position: float = 0.10
    stop_loss: float = 0.05
    parameters: dict = field(default_factory=dict)

# 5. finally 在资源管理中的应用
def process_market_data(filepath: str):
    connection = None
    try:
        connection = Database.connect(filepath)
        data = connection.query("SELECT * FROM trades")
        return process(data)
    except DatabaseError as e:
        logger.error(f"数据库错误: {e}")
        raise
    finally:
        if connection:
            connection.close()
点击展开可浏览运行结果
📘 本段为代码片段(依赖上文变量或外部输入,如 df/data/参数等),无法独立运行

常见陷阱

1. 浮点数精度问题

PYTHON10 行 · 226 B
📄此处有展示代码10 行 · 226 B展开 ▼
python
# 陷阱
0.1 + 0.2 == 0.3  # False!

# 解决方案
import math
math.isclose(0.1 + 0.2, 0.3)  # True

# 或在金融计算中使用 Decimal
from decimal import Decimal
Decimal('0.1') + Decimal('0.2') == Decimal('0.3')  # True
点击展开可浏览运行结果
(脚本执行完成,未产生可展示的模块级变量或函数定义)

2. 可变默认参数

PYTHON11 行 · 227 B
📄此处有展示代码11 行 · 227 B展开 ▼
python
# 陷阱:默认参数只计算一次
def bad_append(item, lst=[]):
    lst.append(item)
    return lst

# 解决方案
def good_append(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst
点击展开可浏览运行结果
📘 本段代码定义了 2 个函数/类:函数 `bad_append`(解决方案),以及 函数 `good_append`。该片段为教学展示(未包含独立运行的输入数据),可在实战练习中结合真实数据调用。

3. 闭包中的变量绑定

PYTHON6 行 · 198 B
📄此处有展示代码6 行 · 198 B展开 ▼
python
# 陷阱:lambda 中的 i 是引用,循环结束后的值
funcs = [lambda x: x + i for i in range(10)]
# 所有 funcs 都用 i=9

# 解决方案
funcs = [lambda x, i=i: x + i for i in range(10)]
点击展开可浏览运行结果
--- 结果 ---
funcs = [<function <lambda> at 0x000001AF647D13A0>, <function <lambda> at 0x000001AF647D1440>, <function <lambda> at 0x000001AF647D14E0>, <function <lambda> at 0x000001AF647D1580>, <function <lambda> at 0x000001AF647D1620>, <function <lambda> at 0x000001AF647D16C0>, <function <lambda> at 0x000001AF647D1760>, <function <lambda> at 0x000001AF647D1800>, <function <lambda> at 0x000001AF647D18A0>, <function <lambda> at 0x000001AF647D1940>]

时间复杂度速查

数据结构/操作Python 实现时间效率
list.append动态数组O(1) 均摊
list.insert(0)需要移动所有元素O(n)
set.add / dict key lookup哈希表O(1) 平均
list in (线性扫描)-O(n)
sorted()TimsortO(nlogn)
heapq.heappush/heappop二叉堆O(logn)
collections.deque.popleft双向链表O(1)
bisect (有序列表插入)二分查找O(logn)

面试中的编程题不仅考察你能不能写出正确的代码,更考察你是否理解这些代码在量化交易场景中的适用边界。代码的效率、可读性和鲁棒性,三个维度同样重要。