一个散户自学量化的 20 个月
一个散户自学量化的 20 个月
这两年,圈内传出来的量化薪资门槛大概是这样的:在上海,应届 Junior Quant 的 Base 加奖金一般在 60万-80万人民币;在香港,这个数字大概是 120万-150万港币;如果能进纽约的头部对冲基金,起薪基本是 30万-40万美元。
看到这些数字,很多人的第一反应是:“我也要去学 Python,搞个机器学习模型来炒股。”
我当初也是这么想的。作为一个在A股里交了不少学费的散户,我以为只要学了量化,学会了用代码看盘,就能精准预测明天的涨跌,从此财富自由。
直到我自己跌跌撞撞地摸索了 20 个月,把账户里的钱亏了又赚、赚了又亏之后,我才慢慢明白:量化的核心根本不是写代码,更不是预测未来。它更像是一个重新训练自己怎么理解市场的过程。
回头看,这 20 个月里真正有用的东西,大概就这几块。如果你也想从零开始自学量化,也许可以少走一点弯路。
目录:
- 从猜涨跌,到开始算概率
- 写策略之前,先学会怀疑自己
- 股票不是一只一只看的
- 算出来的最优解,实盘不一定能做
- 到了期权这一步,才知道波动本身也会吃钱
- 最后真正卡住我的,是估算误差
从猜涨跌,到开始算概率
做散户的时候,我最喜欢干的事就是看图形、猜涨跌。“这根K线收了长下影,明天肯定涨!”
这是典型的绝对思维。后来我学了量化,第一件事就是被概率论狠狠敲醒:在市场里,“肯定”这两个字是最贵的。
1. 散户看单点,量化看“条件”
我以前总觉得某只股票是个好公司,所以该涨。后来我懂了,孤立的事件是没有交易价值的,有价值的是条件概率:
$$ P(A \mid B)=\frac{P(A \cap B)}{P(B)} $$
我现在不问“它明天涨不涨”,我只问程序:“在过去 10 年里,当大盘跌破 30 日均线,但该股净流入超过 1 亿(条件B)时,它次日上涨(事件A)的概率是多少?” 加上了严苛的约束条件,我才发现以前自以为的“规律”全都是错觉。
2. 被套牢的本质,是不懂贝叶斯
散户最惨的死法是什么?死扛。买入后暴跌,到处找利好消息安慰自己。 贝叶斯公式治好了我的“头铁”:
$$ P(H \mid D)=\frac{P(D \mid H)P(H)}{P(D)} $$
不管我买入前多看好它(先验),只要它今天跌破了我的量化防线(新数据),我的看好程度就必须往下调(后验),该止损就止损。交易里最忌讳的就是头铁,新数据出来后,原来的判断就得跟着改。
3. 为什么看对了大趋势,账户还是亏光了?
$$ \mathbb{E}[X]=\sum_i x_iP(X=x_i) \quad \text{与} \quad \mathrm{Var}(X)=\mathbb{E}\left[(X-\mathbb{E}[X])^2\right] $$ 我曾经写过一个回测收益很高的策略(期望值为正),满仓干进去,结果碰上一次极端行情,账户回撤了 40%,我吓得割肉了。刚割完,策略就开始赚钱。我终于明白,期望值决定你能不能赚钱,而方差决定你能不能活到赚钱的那一天。
我的脱产自学清单(耗时约 1 个月):
- 核心读物: Sheldon Ross 的 A First Course in Probability(直接刷完前 7 章,把课后题当做面试题来做)。
- 实战任务 1: 用 Python 写一个蒙特卡洛模拟器,测算如果一个策略胜率是 55%,赔率是 1:1,而你的本金只够扛 10 次亏损,那么在 100 笔交易内的破产概率有多大。
- 实战任务 2: 写一个贝叶斯追踪脚本,模拟一个连亏 5 天的趋势策略,观察它的“后验有效性”是如何跌破 20% 警戒线的。
import numpy as np
# 测算:胜率55%,赔率1:1,初始资金10万,每次下注1万,100笔交易内的破产概率
def simulate_ruin(win_rate=0.55, initial_capital=10, bet_size=1, n_simulations=10000, n_trades=100):
ruin_count = 0
for _ in range(n_simulations):
capital = initial_capital
for _ in range(n_trades):
# 赢了+1万,输了-1万
capital += bet_size if np.random.rand() < win_rate else -bet_size
if capital <= 0:
ruin_count += 1
break
return ruin_count / n_simulations
ruin_prob = simulate_ruin()
print(f"正期望策略的破产概率(方差的惩罚):{ruin_prob*100:.2f}%")
# 哪怕期望收益为正,只要仓位管理不对,你依然可能死在半路上。
写策略之前,先学会怀疑自己
学了点代码后,我开始疯狂写策略。有一天,我调出了一个年化 50% 的完美参数,兴奋得一晚上没睡。上线实盘,半个月亏了 15%。
那一刻我意识到,人脑太容易骗自己了。我必须学统计学来给自己泼冷水。
1. 先假设自己是个骗子
现在我跑出好回测,第一反应不是高兴,而是做假设检验:假设这 50% 的收益全是我运气好蒙出来的(零假设 $H_0$)。纯靠运气蒙出这个结果的概率(p值)有多大?如果概率很大,说明这个策略就是个垃圾。
2. 调参就是自我欺骗
我以前喜欢试各种参数:均线 5 日不行换 10 日,总能试出一个赚钱的。 后来我知道这叫多重比较问题。试了 1000 次总会撞上一次好运,如果不做 Bonferroni 调整($\alpha_{\text{adj}}=\frac{\alpha}{n}$),我选出来的所谓“最强策略”,不过是完美拟合了历史的一段噪音而已。
3. 接受自己算不准
$$ \hat{\theta}{MLE} = \arg\max\theta \sum_{i=1}^n \log f(x_i\mid\theta) $$ 市场到底是怎么运转的?没人知道。极大似然估计(MLE)教会我务实:拿现有的这批数据,反推一组“最有可能生成这些数据”的参数凑合用。别追求完美预测,在当前样本下管用就行了。
我的脱产自学清单(耗时约 1.5 个月):
- 核心读物: Casella & Berger 的 Statistical Inference(非常硬核,专治各种对统计指标的想当然)。
- 实战任务 1: 拉取标普 500 过去 10 年的数据,写一个滚动窗口(Rolling Window),用 MLE 估算收益率的尖峰厚尾特征。
- 实战任务 2: 自己手写一个置换检验器:固定未来收益,把交易信号打乱重排 5,000 次,看原始策略的夏普率在这 5,000 个随机结果里排在什么位置。排不进前 5%,就直接废弃。
import numpy as np
# 用置换检验打碎你的“回测幻觉”
def permutation_test(signal, future_returns, n_permutations=5000):
strategy_returns = signal * future_returns
actual_sharpe = np.mean(strategy_returns) / np.std(strategy_returns) * np.sqrt(252)
random_sharpes = []
for _ in range(n_permutations):
# 固定未来收益,只打乱信号与收益的对应关系
shuffled_signal = np.random.permutation(signal)
shuffled_returns = shuffled_signal * future_returns
sharpe = np.mean(shuffled_returns) / np.std(shuffled_returns) * np.sqrt(252)
random_sharpes.append(sharpe)
p_value = np.mean(np.array(random_sharpes) >= actual_sharpe)
return actual_sharpe, p_value
np.random.seed(42)
future_returns = np.random.normal(0.0005, 0.015, 1000)
signal = np.where(np.random.randn(1000) > 0, 1, -1)
sharpe, p_val = permutation_test(signal, future_returns)
print(f"原策略夏普: {sharpe:.2f}")
print(f"随机打乱信号后仍然不弱于它的概率: {p_val:.4f}")
# 如果 p 值大于 0.05,这个策略大概率只是噪音。
股票不是一只一只看的
散户看盘,是一只股票一只股票地看。但量化不能这么玩,当你管理几十只股票时,你必须学会线性代数,看清它们背后的网。
1. 股票是互相牵扯的
我以前满仓干了 5 只股票,以为自己分散投资了,结果大盘一跌全崩了。因为它们的相关性太高。 后来我学会了用协方差矩阵 $\Sigma$。整个投资组合的风险,其实是一个矩阵运算:
$$ \sigma_p^2=\mathbf{w}'\Sigma\mathbf{w} $$
我买的不是孤立的股票,我买的是它们之间的关系。
2. 剥离掉没用的新闻
$$ \Sigma=Q\Lambda Q^\top $$ 每天看新闻炒股太累了。PCA(主成分分析)帮我降了维。它把几百只股票的杂乱涨跌过滤掉,直接抽出驱动大盘的那两三个核心主轴。协方差矩阵是对称矩阵,所以这里用的是正交分解。看透了主轴,我就不用再盯着个股噪音看了。
我的脱产自学清单(耗时约 1.5 个月):
- 核心读物: David C. Lay 的 Linear Algebra and Its Applications(这本书里有很多关于动力系统的精彩论述)。
- 必做推导: 在纸上用特征分解,一步步把多资产组合的方差公式 $\sigma_p^2=\mathbf{w}'\Sigma\mathbf{w}$ 拆解到最底层的正交向量上。
- 实战任务: 下载纳斯达克 100 指数的成分股日频数据,自己写一个基于 PCA 的统计套利(Statistical Arbitrage)回测:先提取前几个主成分,再研究剥离公共因子后的残差序列。
import numpy as np
# 模拟 50 只股票的收益率矩阵(带有一定的系统性相关)
np.random.seed(42)
n_stocks = 50
n_days = 1000
market_factor = np.random.normal(0, 0.01, n_days)
# 股票收益 = 市场因子暴露 + 个股特异性噪音
returns = np.outer(market_factor, np.random.uniform(0.5, 1.5, n_stocks)) + np.random.normal(0, 0.02, (n_days, n_stocks))
centered_returns = returns - returns.mean(axis=0, keepdims=True)
# PCA:协方差矩阵特征分解
cov_matrix = np.cov(centered_returns, rowvar=False)
eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)
# 对特征值降序排列
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
variance_explained = eigenvalues / np.sum(eigenvalues)
top_k = 3
factor_loadings = centered_returns @ eigenvectors[:, :top_k]
common_component = factor_loadings @ eigenvectors[:, :top_k].T
residuals = centered_returns - common_component
print(f"前三个主成分一共解释了总方差的: {variance_explained[:top_k].sum()*100:.2f}%")
print(f"去掉公共因子后,残差日波动率均值约为: {residuals.std(axis=0).mean():.4f}")
算出来的最优解,实盘不一定能做
算出了完美的持仓比例,但实盘买不进去怎么办?我开始学微积分与优化理论,学习怎么向现实妥协。
1. 别看太远,顾好眼下
$$ f(x)=f(a)+f'(a)(x-a)+\frac{f''(a)}{2}(x-a)^2+\cdots $$ 预测未来太难了。Taylor 展开教我的道理是:只要把眼下的这一小步风险(一阶导和二阶导)给对冲平了就行。不猜大趋势,只管当下的生存。
2. 戴着镣铐跳舞
$$ \min_{\mathbf{w}}\mathbf{w}'\Sigma\mathbf{w} \quad \text{s.t.} \quad \mu'\mathbf{w}\ge\bar{\mu},; \mathbf{1}'\mathbf{w}=1 $$ 现实交易有手续费、有滑点、有仓位限制。凸优化求解器就是干这个的:在这些死规矩的“镣铐”下,硬算出一个你能实际操作的、最不坏的方案。
我的脱产自学清单(耗时约 1 个月):
- 核心读物: Boyd & Vandenberghe 的 Convex Optimization(把 KKT 条件彻底吃透,这是优化问题里跟现实妥协的核心密码)。
- 推导任务: 证明拉格朗日乘子(Lagrange Multipliers)在投资组合资金限制约束下的经济学含义(即“影子价格”)。
- 实战任务: 使用
scipy.optimize构建一个多空组合模型,加入“最大杠杆率 = 1.5”和“单期换手率 < 10%”这两个约束,再额外加一个换手惩罚项,然后强行求解。
import numpy as np
from scipy.optimize import minimize
# 一个带有现实约束的“脏活”优化器
def constrained_portfolio_optimization(
expected_returns,
cov_matrix,
previous_weights,
target_return=0.03,
max_leverage=1.5,
max_turnover=0.10,
turnover_penalty=0.01,
):
n = len(expected_returns)
def objective(weights):
variance = np.dot(weights.T, np.dot(cov_matrix, weights))
turnover_cost = turnover_penalty * np.sum(np.abs(weights - previous_weights))
return variance + turnover_cost
# 限制1:权重和为1(不能无限借钱)
# 限制2:预期收益必须达到目标
# 限制3:总杠杆不能超过 max_leverage
# 限制4:单期换手率不能超过 max_turnover,这里按 0.5 * sum(|w_t - w_{t-1}|) 计算
constraints = [
{'type': 'eq', 'fun': lambda w: np.sum(w) - 1},
{'type': 'ineq', 'fun': lambda w: np.dot(w, expected_returns) - target_return},
{'type': 'ineq', 'fun': lambda w: max_leverage - np.sum(np.abs(w))},
{'type': 'ineq', 'fun': lambda w: max_turnover - 0.5 * np.sum(np.abs(w - previous_weights))}
]
# 现实的枷锁:单只股票仓位不能超过30%,可以做空但不能低于-10%
bounds = tuple((-0.1, 0.3) for _ in range(n))
initial_weights = previous_weights.copy()
result = minimize(objective, initial_weights, method='SLSQP', bounds=bounds, constraints=constraints)
return result.x
# 市场并不完美,求解器给你的往往也是一个满是“妥协”的解。
到了期权这一步,才知道波动本身也会吃钱
最后,我触碰到了量化的深水区:随机微积分。
1. 颠簸本身就会吃钱
设 $Y=f(X,t)$,并且 $X_t$ 服从布朗运动驱动的扩散过程,那么 Itô 引理会多出一个二阶项。很多人第一次学随机微积分,真正卡住的就是这一步:$(dW_t)^2=dt$。
$$ dY= \left( \frac{\partial f}{\partial t} + \mu\frac{\partial f}{\partial X} + \frac{1}{2}\sigma^2\frac{\partial^2 f}{\partial X^2} \right)dt + \sigma\frac{\partial f}{\partial X}dW_t $$
这个多出来的二阶项告诉我:对期权头寸和动态对冲来说,价格路径本身就会产生损益,哪怕你只盯着起点和终点,也可能漏掉中间真正重要的风险。
2. 放弃个人看法
$$ \frac{\partial V}{\partial t} + \frac{1}{2}\sigma^2S^2\frac{\partial^2 V}{\partial S^2} + rS\frac{\partial V}{\partial S} -rV=0 $$ 在 Black-Scholes 方程里,代表预期收益率的 $\mu$ 消失了。期权定价并不直接依赖你主观假设的股票长期收益率,而是更多取决于当前价格、执行价、期限、无风险利率和波动率。只要对冲做得好,我个人觉得明天会涨还是会跌,其实没那么重要。真正起作用的,是你怎么处理波动、怎么处理风险。
我的脱产自学清单(耗时约 2 个月):
- 终极圣经: Björk 的 Arbitrage Theory in Continuous Time(非常严密地讲解了鞅理论和测度变换)。
- 实战任务 1: 自己在纸上用二叉树(Binomial Tree)模型推导出期权定价,并证明当步长 $\Delta t \to 0$ 时,它收敛于 Black-Scholes 解析解。
- 实战任务 2: 写一个期权动态 Delta 对冲的模拟器(Delta Hedging Simulator)。给定一条带有跳跃(Jump)的股价路径,计算你每天对冲需要花费多少摩擦成本,再看看最后的对冲误差到底有多大。
import numpy as np
# 这是一个极其简化的 Delta 对冲模拟器:同时看看对冲误差和摩擦成本
def simulate_delta_hedging(S0=100, K=100, T=1.0, r=0.05, sigma=0.2, steps=252):
from scipy.stats import norm
dt = T / steps
# 模拟一条真实的带有跳跃的股价路径
prices = [S0]
for _ in range(steps):
jump = np.random.normal(0, sigma * np.sqrt(dt))
# 偶尔来个暴跌(模拟黑天鹅)
if np.random.rand() < 0.01: jump -= 0.05
prices.append(prices[-1] * np.exp((r - 0.5*sigma**2)*dt + jump))
def get_delta(S, t):
if t >= T: return 1.0 if S > K else 0.0
tau = max(T - t, 1e-12)
d1 = (np.log(S/K) + (r + sigma**2/2)*tau) / (sigma*np.sqrt(tau))
return norm.cdf(d1)
# 每天调整仓位,计算摩擦成本
hedging_cost = 0
current_delta = get_delta(S0, 0)
cash_account = -current_delta * prices[0]
for i in range(1, steps):
new_delta = get_delta(prices[i], i*dt)
# 假设每次调仓有万分之五的手续费
delta_change = new_delta - current_delta
trade_cost = abs(delta_change) * prices[i] * 0.0005
cash_account -= delta_change * prices[i] + trade_cost
hedging_cost += trade_cost
current_delta = new_delta
option_payoff = max(prices[-1] - K, 0)
hedge_value = current_delta * prices[-1] + cash_account
hedge_error = hedge_value - option_payoff
return hedging_cost, hedge_error
cost, hedge_error = simulate_delta_hedging()
print(f"一年下来,摩擦成本大约吃掉了 ${cost:.2f}")
print(f"最终对冲误差约为 ${hedge_error:.2f}")
最后真正卡住我的,是估算误差
20 个月后,我不再看K线图了,也不再去股吧里跟人争论明天的大盘走势了。
我终于明白,很多散户转型做量化,最后还是死路一条,是因为他们只学会了写代码,却没有学会敬畏估算误差(Estimation Error)。他们拿着充满噪音的历史数据,算出一个看似完美的参数,然后满仓冲进去,本质上还是在“赌博”。
现在的我,承认历史数据是脏的,承认模型是脆弱的。我不追求算准明天,我只做大概率的事,严格对冲风险,分散持仓,然后把剩下的交给时间。
如果你现在也在走这条路,不想把这些坑再从头踩一遍,可以先了解一下我们的《量化24课》,先把整个框架搭起来。等概率、统计和回测这些底子有了,再接着看我们的《因子挖掘与机器学习策略课程》,会更容易把因子研究、特征工程和策略迭代这些事情真正串起来。
这大概就是我这 20 个月里真正学到的东西。