量化编程测试三大工程能力与落地实现细节

Feng 29 阅读 开发者故事

量化岗的编程测试和互联网大厂那套纯算法题不是一路。它更像一次压缩版交付:给一个金融场景,看你能不能把数学模型落成跑得动、跑得稳、跑得快的代码。下面按数值、数据结构、系统韧性三条线拆,顺带说那些面试里不暴露、上生产才咬人的细节。

量化编程测试三大工程能力与落地实现细节

从随机过程到可执行定价

期权定价是标配题。面试官不关心你背不背 Black-Scholes,关心你能不能把连续时间的随机过程翻成离散数值计算。几何布朗运动下终端价格写成 S_T = S·exp((r – σ²/2)T + σ√T·Z),Z 取标准正态,蒙特卡洛就是重复抽样再取贴现期望。

朴素实现的毛病在收敛速度:误差按 1/√N 衰减,多要一位精度就得多抽一百倍样本。只写循环抽样基本会被追问怎么提速。对偶变量法是最便宜的答案,用 Z 与 -Z 成对抽样,两者负相关,方差明显下降,改动量极小。

随机数生成器这块值得单独说。老代码里 np.random.randn 走的是全局状态,多线程或多次调用互相污染,结果不可复现。NumPy 1.17 起提供了 default_rng,显式构造 Generator 才是安全写法:

import numpy as np

rng = np.random.default_rng(20240501)
Z = rng.standard_normal(n_paths)

多进程场景下别用同一个种子硬拷贝,用 SeedSequence.spawn(k) 派生子流,每个进程拿独立且互不相关的流,否则并行抽样会退化成相关样本。贴现因子是 exp(-rT),原文示例写成乘 exp(rT),方向反了,实盘里这会直接让价格虚高。这段只适用欧式看涨,美式期权得走最小二乘蒙特卡洛,别混用。

窗口统计:选对结构只是入场券

高频方向爱卡时间。典型要求是在十万到百万级 tick 上算滑动窗口波动率,窗口固定,数据流式到达,每次更新要 O(1),队列加增量维护是标准解。

增量维护平方和有个隐蔽的坑。价格量级在 10⁴ 以上、窗口内方差又很小时,sum(x²)/n - (sum(x)/n)² 会发生灾难性抵消,甚至算出负数再开根号变成 nan。价格数据恰好落在这个区间。给个可判定的经验线:当均值平方与方差之比超过约 10⁸(双精度有效位约 15-16 位十进制),相对误差就会逼近甚至超过结果本身,此时必须换算法。安全做法是 Welford 在线更新,或先对价格做去均值平移再累计。

边界条件也要交代。窗口没填满时返回什么——None、当前样本方差、还是不输出——三种约定业务含义完全不同,主动说明比默默选一个更能体现工程意识。Welford 维护均值与 M2 的写法:

class WelfordWindow:
 def __init__(self, size):
 self.size = size
 self.buf = collections.deque()
 self.n = 0
 self.mean = 0.0
 self.m2 = 0.0

 def push(self, x):
 self.buf.append(x)
 self.n += 1
 delta = x - self.mean
 self.mean += delta / self.n
 self.m2 += delta * (x - self.mean)
 if len(self.buf) > self.size:
 self._evict(self.buf.popleft())

 def _evict(self, x):
 if self.n <= 1:
 self.n, self.mean, self.m2 = 0, 0.0, 0.0
 return
 new_n = self.n - 1
 new_mean = (self.n * self.mean - x) / new_n
 self.m2 -= (x - self.mean) * (x - new_mean)
 self.n, self.mean = new_n, new_mean

出队回退用反向更新公式 M2 -= (x - mean_old) * (x - mean_new),这是原文没写全的部分,缺了它窗口滑动后方差会漂。面试时间紧,把这点讲出来比硬写完整更划算。另外 m2 理论上非负,浮点误差可能让它略小于零,开根号前 clamp 到 0。

断流、熔断与重试的真实边界

第三类考系统韧性。行情源会断,接口会超时,要求主备切换加告警。熔断器配指数退避是常见答案:连续失败超阈值就打开熔断,期间直接走备用源,重试间隔按 2 的幂次拉长。

这套模式有明确失效场景,写代码时一并说明。主备源若是同一云厂商同区域服务,故障同源,切换等于没切。退避没有上限会拖长恢复窗口,通常要封顶并加抖动,否则大量客户端同一时刻齐刷刷重连,形成重试风暴。熔断状态需要半开探测,否则永远不会自动恢复。

import time, random

class CircuitBreaker:
 def __init__(self, threshold=5, base=0.5, cap=30.0):
 self.threshold = threshold
 self.base = base
 self.cap = cap
 self.fails = 0
 self.state = "closed"
 self.opened_at = 0.0

 def call(self, primary, backup):
 if self.state == "open":
 if time.monotonic() - self.opened_at > self.cap:
 self.state = "half_open"
 else:
 return backup()
 try:
 r = primary()
 if self.state == "half_open":
 self.state, self.fails = "closed", 0
 return r
 except Exception:
 self.fails += 1
 if self.fails >= self.threshold:
 self.state = "open"
 self.opened_at = time.monotonic()
 delay = min(self.cap, self.base * (2 ** self.fails))
 time.sleep(random.uniform(0, delay)) # decorrelated jitter
 return backup()

抖动别用固定比例,decorrelated jitter(random.uniform(0, delay))比等比例抖动更能打散重连峰值。时钟源用 time.monotonic 而非 time.time,避免系统时钟回拨把熔断窗口算错,跨时区部署的交易系统里这是真实事故来源。更严格可读 CLOCK_MONOTONIC_RAW,它不受 NTP 频率调整影响。半开状态下探测成功要复位失败计数,只改状态不复位,下一次单次失败就会立刻重新熔断。

三条线背后的同一要求

定价题考数值直觉,窗口统计考复杂度与数值稳定,容错题考系统边界。三者指向同一件事:代码要能被别人接手、被生产检验。刷题时给每道题补一段“这段代码在什么情况下会错”,比单纯追 AC 更接近量化开发的日常。

ID:6619

Feng
这位作者很神秘,还没有填写简介。