返回 市场交易
市场交易
15 分钟阅读

量化交易模型篇:常见金融模型的 Python 实现

用 Python 实现并讲解 CAPM β 回归、多因子模型、Black-Scholes 期权定价、 动量与均值回归信号、GARCH 波动率、Markowitz 组合优化、Kelly 仓位与协整配对交易。

第零章

把公式写成可运行的代码

原理篇 讲了量化系统的闭环与研究流程。本文做模型篇:把常见金融模型翻译成 Python,让你能亲手跑出 β、期权价格、GARCH 波动率、最优权重和协整检验结果。

为保持可复现,示例统一使用合成数据numpy 随机生成)。迁移到真实行情时,只需把 returns 换成从 CSV 或 API 拉取的序列,拟合与优化逻辑不变

环境依赖:

pip install numpy pandas scipy statsmodels arch
用途
numpy / pandas矩阵运算、时间序列
scipy正态分布、数值优化
statsmodelsOLS 回归、协整检验
archGARCH 族模型拟合

公式告诉你「应该算什么」;代码告诉你「算出来的数长什么样」——二者缺一不可。

— 模型篇入口
第一章

CAPM:用回归估计 β

CAPM 把个股预期收益与市场风险挂钩:

E[R_i] = R_f + β_i ( E[R_m] − R_f )

实务中常用**超额收益**做 OLS:R_i − R_f = α + β (R_m − R_f) + ε。α 显著不为零时,说明相对 CAPM 存在超额(或模型设定不足)。

下面生成 500 个交易日的合成收益,用 statsmodels 估计 β 与 α:

import numpy as np
import pandas as pd
import statsmodels.api as sm
 
np.random.seed(42)
n = 500
# 市场超额收益
rm = np.random.normal(0.0004, 0.012, n)
# 真实 β=1.2,加个股噪音
beta_true = 1.2
alpha_true = 0.0001
ri = alpha_true + beta_true * rm + np.random.normal(0, 0.008, n)
 
df = pd.DataFrame({"r_stock": ri, "r_market": rm})
X = sm.add_constant(df["r_market"])  # 含截距 → 估计 α
model = sm.OLS(df["r_stock"], X).fit()
 
print(model.summary().tables[1])
beta_hat = model.params["r_market"]
alpha_hat = model.params["const"]
print(f"β 估计 = {beta_hat:.3f}(真实 {beta_true})")
print(f"α 估计 = {alpha_hat:.6f}(日频,年化约 {alpha_hat*252:.2%})")

解读β > 1 表示个股波动大于市场;组合风控里常用 β 做基准对冲——持有 1 元多头的同时,卖空 β 元的指数期货,可剥离市场方向敞口。

第二章

多因子模型:剥离风格后的残差

多因子模型将收益分解为系统性因子与 idiosyncratic 残差:

R_i = α_i + Σ_k β_{ik} F_k + ε_i

F_k 可为市场、规模(SMB)、价值(HML)、动量等。量化选股常在控制 β 后,在 ε 或 α 上寻找可预测成分。

用三个合成因子演示多元回归:

import numpy as np
import pandas as pd
import statsmodels.api as sm
 
np.random.seed(0)
n = 600
F_mkt = np.random.normal(0.0003, 0.01, n)
F_smb = np.random.normal(0.0001, 0.006, n)   # 规模因子
F_hml = np.random.normal(0.0000, 0.005, n)   # 价值因子
 
# 个股对三因子的真实暴露
b_mkt, b_smb, b_hml = 0.9, 0.4, -0.2
alpha_true = 0.00005
r_stock = (
    alpha_true
    + b_mkt * F_mkt + b_smb * F_smb + b_hml * F_hml
    + np.random.normal(0, 0.007, n)
)
 
X = sm.add_constant(pd.DataFrame({
    "MKT": F_mkt, "SMB": F_smb, "HML": F_hml,
}))
res = sm.OLS(r_stock, X).fit()
print(res.summary())
 
# 残差 = 剥离因子后的「个股特异」部分
residuals = res.resid
print(f"残差标准差: {residuals.std():.4f}")

实务要点:真实 Fama-French 因子可从 Kenneth French 数据库 下载月度/日度序列。回归后得到的 residuals 可用于事件研究、残差动量或配对交易的输入。

第三章

Black-Scholes:期权定价与 Greeks

欧式看涨期权闭式解:

C = S · N(d₁) − K · e^{−rT} · N(d₂)

d₁ = [ln(S/K) + (r + σ²/2)T] / (σ√T),d₂ = d₁ − σ√T。下文同时计算 Delta、Gamma、Vega、Theta。
import numpy as np
from scipy.stats import norm
 
def black_scholes_call(S, K, T, r, sigma):
    """欧式看涨期权价格与 Greeks"""
    if T <= 0:
        return {"price": max(S - K, 0), "delta": 1.0 if S > K else 0.0}
    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    price = S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
    delta = norm.cdf(d1)
    gamma = norm.pdf(d1) / (S * sigma * np.sqrt(T))
    vega  = S * norm.pdf(d1) * np.sqrt(T) / 100   # 波动率 +1% 的价格变化
    theta = (
        -S * norm.pdf(d1) * sigma / (2 * np.sqrt(T))
        - r * K * np.exp(-r * T) * norm.cdf(d2)
    ) / 365  # 每日时间衰减
    return {"price": price, "delta": delta, "gamma": gamma,
            "vega": vega, "theta": theta}
 
# 示例:S=100, K=100, 30天到期, r=3%, σ=25%
g = black_scholes_call(S=100, K=100, T=30/365, r=0.03, sigma=0.25)
for k, v in g.items():
    print(f"{k:6s}: {v:.4f}")

解读vega 除以 100 表示 IV 上升 1 个百分点时期权价格变化。卖方策略需重点关注 GammaTail risk——标的大幅跳跃时,恒定 σ 假设失效。

第四章

动量与均值回归:两个相反的因子

动量信号

过去 k 日累计收益作为排名因子——涨得多的继续持有(简化版):

import numpy as np
import pandas as pd
 
np.random.seed(1)
dates = pd.date_range("2020-01-01", periods=300, freq="B")
# 3 只股票合成价格
prices = pd.DataFrame(
    np.exp(np.cumsum(np.random.normal(0.0003, 0.015, (300, 3)), axis=0)) * 100,
    index=dates, columns=["A", "B", "C"],
)
 
lookback = 20
momentum = prices.pct_change(lookback).iloc[-1]  # 最新截面动量
rank = momentum.rank(ascending=False)
print("20日动量:", momentum.round(4).to_dict())
print("排名(1=最强):", rank.astype(int).to_dict())
# 做多排名第 1,做空排名第 3(示意,未含风控)
long, short = rank.idxmin(), rank.idxmax()
print(f"示意:做多 {long},做空 {short}")

均值回归:价差 z-score

对协整价差计算 z-score,偏离 ±2 时触发交易信号:

import numpy as np
import pandas as pd
 
np.random.seed(2)
n = 400
# 构造均值回归价差:AR(1) 系数 0.95
eps = np.random.normal(0, 1, n)
spread = np.zeros(n)
for t in range(1, n):
    spread[t] = 0.95 * spread[t - 1] + eps[t]
 
s = pd.Series(spread)
z = (s - s.rolling(60).mean()) / s.rolling(60).std()
signal = pd.Series(0, index=s.index)
signal[z > 2]  = -1   # 价差过高 → 做空价差
signal[z < -2] =  1   # 价差过低 → 做多价差
 
print(signal.value_counts())
print("最近 5 日 z-score:\n", z.tail().round(2))

动量与均值回归在不同时间尺度上可并存:同一标的在月频可能动量显著,在日频价差上却均值回归。

第五章

GARCH(1,1):波动率聚集

金融收益常见「大波动之后仍是大波动」。GARCH(1,1) 对条件方差建模:

σ_t² = ω + α ε_{t−1}² + β σ_{t−1}²

ε_t 为收益残差。拟合后可用 σ_t 做动态仓位缩放:波动升高时减仓。
import numpy as np
import pandas as pd
from arch import arch_model
 
np.random.seed(3)
n = 1000
# 用 arch 生成 GARCH(1,1) 序列(ω=0.05, α=0.1, β=0.85)
from arch.univariate import ConstantMean, GARCH, Normal
am = arch_model(None, mean="Zero", vol="Garch", p=1, q=1)
# 手工构造带波动率聚集的收益
sigma = np.zeros(n)
eps = np.zeros(n)
omega, alpha, beta = 0.05, 0.1, 0.85
for t in range(1, n):
    sigma[t] = np.sqrt(omega + alpha * eps[t-1]**2 + beta * sigma[t-1]**2)
    eps[t] = sigma[t] * np.random.standard_normal()
 
returns = pd.Series(eps * 100, name="returns")  # 放大为百分比便于拟合
 
model = arch_model(returns, vol="Garch", p=1, q=1, rescale=False)
fit = model.fit(disp="off")
print(fit.summary())
 
# 提取条件波动率,用于 VaR 或仓位
cond_vol = fit.conditional_volatility
print(f"最新条件日波动率: {cond_vol.iloc[-1]:.3f}%")
print(f"95% 单日 VaR (近似): {1.65 * cond_vol.iloc[-1]:.3f}%")

解读1.65 × σ 是正态假设下 95% 单侧 VaR 的快捷估算。极端行情下应改用 CVaR 或更厚的尾分布(t 分布 GARCH)。

第六章

Markowitz 组合与 Kelly 仓位

Markowitz 最小方差组合

min_w wᵀΣw s.t. Σ w_i = 1

下例用 scipy 二次规划求最小方差组合;亦可加入 wᵀμ ≥ μ_target 约束追求有效前沿。
import numpy as np
import pandas as pd
from scipy.optimize import minimize
 
np.random.seed(4)
n_days, n_assets = 250, 4
rets = pd.DataFrame(
    np.random.multivariate_normal(
        mean=[0.0005, 0.0003, 0.0004, 0.0002],
        cov=np.array([
            [0.0004, 0.0001, 0.0001, 0.0000],
            [0.0001, 0.0003, 0.0000, 0.0001],
            [0.0001, 0.0000, 0.0005, 0.0001],
            [0.0000, 0.0001, 0.0001, 0.0002],
        ]),
        size=n_days,
    ),
    columns=["股票1", "股票2", "股票3", "股票4"],
)
 
mu = rets.mean().values
Sigma = rets.cov().values
n = len(mu)
 
def port_var(w, Sigma):
    return w @ Sigma @ w
 
constraints = {"type": "eq", "fun": lambda w: np.sum(w) - 1}
bounds = [(0, 1)] * n  # 不允许做空
w0 = np.ones(n) / n
 
res = minimize(port_var, w0, args=(Sigma,), method="SLSQP",
               bounds=bounds, constraints=constraints)
w_opt = res.x
print("最小方差权重:", pd.Series(w_opt, index=rets.columns).round(3))
print(f"组合日波动: {np.sqrt(port_var(w_opt, Sigma)):.4f}")
print(f"组合预期日收益: {w_opt @ mu:.6f}")

样本协方差 Σ 估计误差会导致权重极端化;实务中常用 Ledoit-Wolf 收缩 或约束单资产权重上限。

Kelly 准则

f* = ( p · b − q ) / b

p 胜率,q=1−p,b 净赔率。金融中普遍使用「半 Kelly」降低估计误差与路径风险。
def kelly_fraction(p, b, fraction=0.5):
    """p: 胜率, b: 净赔率(赢1元净赚b), fraction: 半Kelly系数"""
    q = 1 - p
    f = (p * b - q) / b
    return max(0.0, f * fraction)
 
# 例:胜率 55%,盈亏比 1.2:1(b=1.2)
f = kelly_fraction(p=0.55, b=1.2, fraction=0.5)
print(f"半 Kelly 建议仓位比例: {f:.2%}")
第七章

协整检验与配对交易骨架

两价格序列各自随机游走,但线性组合平稳 → 协整。用 statsmodels 的 Engle-Granger 检验:

P_A = γ P_B + ε_t, ε_t 平稳

γ 用 OLS 估计。交易信号:ε_t 的 z-score 突破阈值时建仓,期待回归。
import numpy as np
import pandas as pd
import statsmodels.api as sm
from statsmodels.tsa.stattools import coint
 
np.random.seed(5)
n = 500
# B 随机游走
pb = 100 + np.cumsum(np.random.normal(0, 1, n))
# A = 1.5 * B + 平稳噪声 → 协整
pa = 1.5 * pb + np.random.normal(0, 2, n)
 
score, pvalue, _ = coint(pa, pb)
print(f"协整检验 p-value: {pvalue:.4f}(< 0.05 则拒绝「无协整」)")
 
# 估计对冲比率 γ
X = sm.add_constant(pb)
gamma = sm.OLS(pa, X).fit().params[1]
spread = pa - gamma * pb
z = (spread - spread.mean()) / spread.std()
 
print(f"对冲比率 γ ≈ {gamma:.3f}")
print(f"当前 z-score: {z.iloc[-1]:.2f}")
 
# 简易回测信号统计(不含 PnL 计算)
entry_long  = (z < -2).sum()
entry_short = (z > 2).sum()
print(f"历史 |z|>2 次数: 做多信号 {entry_long}, 做空信号 {entry_short}")

思想实验

协整关系的 γ 会漂移——并购、退市、行业政策都会让历史回归失效。实盘需设置:滚动重估 γ、止损、以及「协整 p-value 恶化时暂停策略」的规则。

第八章

迷你流水线:串起回归 → 信号 → 权重

把本章片段拼成一条日频研究管线示意(仍用合成数据):

import numpy as np
import pandas as pd
import statsmodels.api as sm
 
np.random.seed(7)
n = 300
rm = np.random.normal(0.0003, 0.01, n)
ri = 0.0001 + 1.1 * rm + np.random.normal(0, 0.006, n)
 
# 1) 估计 β,做市场中性残差
X = sm.add_constant(rm)
beta = sm.OLS(ri, X).fit().params[1]
resid = ri - beta * rm
 
# 2) 残差 z-score 作为均值回归信号(示意)
z = (resid - pd.Series(resid).rolling(20).mean()) / pd.Series(resid).rolling(20).std()
position = (-z).clip(-1, 1).fillna(0)  # z 高则做空
 
# 3) 用滚动波动率缩放仓位
vol = pd.Series(ri).rolling(20).std()
target_pos = (position / (vol * np.sqrt(252))).clip(-0.5, 0.5)
 
print("最近 5 日目标仓位(波动率缩放后):")
print(target_pos.tail().round(3).to_list())

这条管线体现了原理篇的闭环:因子剥离(回归)→ 信号(z-score)→ 仓位(波动率缩放)。接入实盘前还需加上交易成本、涨跌停、资金约束与样本外验证。

代码是模型的一阶近似

本章用不到 200 行 Python,覆盖了量化研究中最常遇到的模型族:回归(CAPM / 因子)、定价(Black-Scholes)、时序(动量 / GARCH)、优化(Markowitz / Kelly)、协整(配对交易)。

它们与 二十块之谜的数学版 的关系不变:模型估计的是 P(t) 各成分的统计结构,不能替代对浮盈与实盈、现金守恒的理解。在扣费之后仍存活的 edge,才值得接入 Today Stock Finance 那样的工程管线。

建议学习路径:先跑通每个单元格 → 换成真实数据 → 做样本外检验 → 再读 进阶篇(LightGBM、NLP、walk-forward)。原理见 姊妹篇

相关文章

市场交易
13 分钟
量化交易原理篇:从信号到成交的系统闭环
梳理量化交易的数据—信号—仓位—执行—风控闭环, 数学工具全景与研究流程,为模型篇的 Python 实现打好概念基础。
市场交易
19 分钟
量化交易进阶篇:非线性模型、另类数据与机构级 Alpha
超越 OLS 与 GARCH 的「经典力学」:树模型、深度学习、NLP 与图像另类数据、 走步验证与过拟合防御,勾勒 Two Sigma、幻方等机构 2020s 量化实战的知识地图。
市场交易
13 分钟
二十块之谜的数学模型:浮盈、零和与市值守恒
用形式化模型拆解「从 20 元到 40 元,多出来的钱从哪来」—— 盯市浮盈、成交守恒、集体市值与已实现盈亏的零和性质。
AI 项目
10 分钟
Today Stock Finance:Vibe Coding 驱动的股票分析平台
基于 TypeScript + Koa2 + React + DeepSeek 的本地化股票分析工具,记录从脚手架到真实行情与 AI 研报的全流程工程实践