马科维茨组合优化在QMT和ptrade中的量化交易实战
2026/9/10 6:13:29 网站建设 项目流程

周末在菜市场买菜的时候,我经常看到一个大妈蹲在水果摊前精挑细选,苹果拿两个、橘子拿三个、香蕉必须捏一捏才决定要不要。她说这样买,家里谁想吃什么都有,坏一筐也不心疼。我当时就站在菜市场出口笑了一下——这不就是资产组合分散风险吗?如果这个大妈手里拿的不是水果而是股票账户,她可能会成为菜市场里最懂马科维茨的人。这个想法其实正是我写QMT和ptrade量化交易小白入门系列第115篇的初衷:把看似高深的组合优化理论,落到普通散户每天能碰到的选股、配仓、回测动作上。今天这篇,我就把马科维茨这套东西如何嵌入量化交易、如何在QMT和ptrade里真正落地,一次性讲透。

作为入门系列里偏“配置层”的一篇,它解决的核心问题其实很朴素:你手里有几只股票、几个策略信号,到底该各买多少?重仓谁、轻仓谁、空仓谁,凭什么?绝大部分散户根本没想过这个问题,只看“哪个涨得好就买哪个”,结果一顿操作猛如虎,账户亏成二百五。马科维茨提供的是一套数学上的答案:在给定收益目标下,让整个组合的风险最小;在给定风险承受力下,让整个组合的收益最大。它不保证你买到最牛的股,但保证你不在某一个坑里摔死。这篇内容适合所有刚开始用QMT或ptrade、还在手动乱买一气的朋友,也适合已经写了几个策略但不知道怎么组装成一个完整账户的进阶者。

1. 一个菜市场场景,揭开资产配置的底层逻辑

1.1 马科维茨在量化交易里到底解决什么问题

很多人第一次听说马科维茨,是在大学金融课本上,然后立刻被“均值-方差”“协方差矩阵”“有效前沿”这几个词劝退了。我理解这种恐惧,因为教科书的问题在于它一上来就讲数学,而不是讲场景。换个方式说就简单多了:马科维茨做的事情,本质就是帮你在几个选项之间分配“下注比例”。

放到量化交易里,场景更清晰。假设你写了一个小市值轮动策略,每天选出5只股票;你又写了一个红利低波策略,每周选3只股票;你还在ptrade里挂了一个可转债打新任务。这三路信号是并行存在的,问题来了:账户里的钱怎么分?以前我也是拍脑袋,50%给小市值、30%给红利、20%打新,后来发现这不是策略问题,是权重问题——权重没定好,任何一个策略回测再漂亮,合成账户照样过山车。

马科维茨给出的解法是:不要只看每个策略单独的收益和风险,还要看策略和策略之间会不会“同涨同跌”。如果小市值策略和红利策略在统计上相关性很低,两者组合在一起,就能在保持收益基本不变的情况下,把整个账户的波动压低。这就像菜场大妈既买苹果又买橘子:苹果价格崩了,橘子价格还可能稳着,整体支出波动就小了。现代投资组合理论的核心不是选“最好的资产”,而是选“搭配起来最好的资产组合”。

1.2 “5年六倍”的数学真相

标题里“直接5年六倍”这个说法,我必须先泼一盆冷水。5年六倍,翻译成数学语言是:1万元变成6万元,总收益率500%,换算成年化复合收益率是多少?用公式算一下:

[ 年化收益率 = 6^{1/5} - 1 \approx 0.431 ]

也就是年化要稳定做到约43.1%,连续5年不中断。这是什么概念?股神级别的长期年化收益率大概在20%到25%左右,43%的年化意味着几乎每两年就要翻一倍,而且5年中间不能有任何一个年头出现大幅回撤,否则复利链条一断,后面根本补不回来。说实话,这种收益水平放在真实市场里,要么是幸存者偏差,要么是加了高杠杆,要么是后视镜里看出来的“最优组合”。

我写这个标题并不是鼓励你做“5年六倍”的梦,而是想借马科维茨的框架做一个理性拆解:马科维茨能帮你的,是在给定风险水平下拿到最高的风险调整后收益,而不是凭空创造收益。它提高的是你收益的“质量”和“稳定性”,不是收益的“绝对值”。理解了这一点,你才算真正入门资产配置,而不是被标题党带偏。

2. 从均值-方差到有效前沿:小白必须吃透的三个概念

2.1 期望收益与风险不是拍脑袋,是矩阵计算

马科维茨整个理论的地基是两个数字:期望收益和风险。期望收益很好理解,就是你持有一篮子资产,未来一段时间预期能赚多少,通常用历史收益的均值来估计。风险则用收益率的波动程度——标准差来衡量,标准差越大,说明涨跌越没谱,风险越高。

但真正的关键不在单个资产的这两个数字,而在它们之间的关系。这就引出了协方差矩阵。用一个通俗的例子:你手里有两只股票,A是白酒,B是医药。它们大多数时候同涨同跌,那么它们之间的协方差就是正的;如果你换成A是白酒、B是黄金ETF,那当白酒大跌时黄金往往上涨,协方差可能是负数。协方差为负的组合,放在一起才有“对冲”效果,账户整体波动会被明显压低。

在QMT和ptrade里做这件事,你不需要手算协方差,用pandas一行就能搞定:

import pandas as pd import numpy as np # df是各资产日收益率,列名为资产代码,行名为日期 cov_daily = df.cov() cov_annual = cov_daily * 252 # 年化协方差矩阵

这里乘252是A股一年的交易日数量,因为日收益率波动需要考虑年化才能和年化收益对比。这个矩阵就是后续一切组合优化的原材料。很多新手在这里会犯一个错误:只盯着单只股票的涨幅榜选权重,完全忽略资产之间的相关性,结果买来买去发现持仓全是同一个风格,市场一回调整个账户一起跌,跟没分散一模一样。

2.2 有效前沿与夏普比率:选择风险收益的性价比

当你能算出任意一个权重组合的期望收益和标准差之后,把所有可能的组合都画在一张图上,横轴是风险(标准差),纵轴是收益(年化收益率),你会得到一片散点。这些散点里,同一风险水平下收益最高的点连成一条曲线,这就是“有效前沿”。有效前沿上的任何一个组合,都是在“不降低收益的前提下无法降低风险”的最优解。

有效前沿上有两个点特别值得关注。一个是“最小方差组合”,整个组合里风险最低的那个点,适合极度保守的人;另一个是“最大夏普比率组合”,夏普比率的公式是:

[ 夏普比率 = \frac{组合年化收益 - 无风险利率}{组合年化波动率} ]

它衡量的是每承担一单位风险能换来多少超额收益。最大夏普组合就是“性价比最高”的组合,也是我在实际做配置时最常用的目标。A股的无风险利率通常用十年期国债收益率约2.5%到3%来近似,具体数值会随时间变化,代码里用常量即可。

理解有效前沿的价值在于:它给了你一个客观的“参照系”。当你建好一个组合,发现它的风险和收益落在有效前沿下方很远的地方,说明你的资金分配效率很低;如果你能在同样的风险水平下通过调权重把收益抬到前沿上,相当于不增加风险就白捡收益。这种收益不是来自预测行情,而是来自数学结构,这就是马科维茨最迷人的地方。

2.3 为什么相关性能让组合“1+1>2”

我见过不少人质疑:“既然马科维茨只是配权重,那我干脆全买收益最高的那个不就行了?”这正是没有理解相关性作用的典型误区。全买收益最高的资产,收益可能不错,但波动率会非常大,一旦回撤超过心理承受范围,大概率在底部割肉离场,最终收益根本拿不到。

举一个用真实逻辑推演的例子:假设资产A年化收益10%、年化波动率20%,资产B年化收益8%、年化波动率15%,A和B的相关系数是0.2。如果你把50%资金给A、50%给B,组合的期望收益大约等于9%。组合的方差不是简单的20%和15%加权,而是还要加上两项协方差贡献。因为相关系数只有0.2,组合波动率可能只有13.5%左右,低于B的15%。低相关资产拼接后,组合的风险比任何单一资产都低,但收益却是两者的加权平均——这就是“1+1>2”的数学来源。

回到量化交易场景,这个原理可以直接用在“多策略账户”上。我在ptrade里跑趋势追踪,同时挂一个网格交易,两者本身就是天然低相关的,因为趋势策略在震荡市里亏钱,网格却在震荡市里赚钱;趋势策略在单边行情里赚钱,网格却容易踏空。把它们组合起来,账户净值曲线会平滑很多,回撤变小,持有体验大幅提升。马科维茨不是教你选谁,而是教你如何让它们共存。

3. QMT和ptrade里的落地路径:数据、优化与执行

3.1 先搞清楚QMT和ptrade的差别

在做任何组合优化之前,你得先知道自己手里的工具能干什么。QMT和ptrade是国内散户能接触到的两个主流量化交易终端,都提供Python接口,但侧重点完全不同,很多新手两个都用过却说不清差别。

QMT是迅投体系的产品,特色是本地化、开放度高。它支持完整的xtquant库,你可以直接下载历史K线、财务数据、板块成分,也可以在本地写任意Python代码做研究,自由度很高。特别是miniQMT模式,可以脱离图形界面直接通过Python API操作,数据落地到本地,很适合做研究型回测和复杂策略。缺点是上手门槛高一点,环境配置麻烦,而且部分券商版本还有单线程下载慢的问题。

ptrade是恒生电子推出的量化平台,走的是“云端策略+托管运行”路线,策略写完后提交到券商服务器运行,不依赖你本地电脑开机。它对新手友好得多,内置了自动打新、条件单、定时任务等功能,新股新债自动申购这类操作以前要天天手动点,现在直接平台搞定。缺点是灵活性不如QMT,自定义数据源和底层库受限,复杂的统计分析还是得先在本地做完再传结果。

至于选哪个,我的建议很直接:如果你主要做研究、想自由折腾Python生态,选QMT;如果你想在券商云端稳定跑策略、顺便把打新这种琐事自动化,选ptrade。两者不冲突,我自己的实际工作流是QMT做数据分析和历史回测,ptrade做实盘托管执行。表格对比如下:

对比维度QMTptrade
运行方式本地客户端/miniQMT,可离线研究云端托管,策略上传后持续运行
开放性高,可自由安装Python库、下载数据低,只能在平台内置函数范围内调用
数据能力历史K线、财务、板块等均可本地落地以get_history/get_fundamentals为主
自动打新支持,需自行开发内置,配置后自动新股新债申购
适合人群爱折腾、有编程基础的研究型玩家新手、希望省心的自动化交易用户

3.2 在QMT中下载因子数据并构造收益序列

做马科维茨优化,第一步永远是把收益率序列准备好。这里我用QMT的xtquant库演示标准流程。假设你要给6个行业ETF做组合配置,先下载它们的日线数据:

from xtquant import xtdata import pandas as pd stock_list = [ "510300.SH", # 沪深300ETF "510500.SH", # 中证500ETF "159915.SZ", # 创业板ETF "512010.SH", # 医药ETF "512880.SH", # 证券ETF "518880.SH", # 黄金ETF ] # 下载历史日线,时间范围可以根据需要调整 xtdata.download_history_data2(stock_list, period="1d", start_time="20200101", end_time="20251231") # 读取收盘价,构建价格宽表 close_data = {} for code in stock_list: data = xtdata.get_market_data_ex([], [code], period="1d", start_time="20200101", end_time="20251231") df = data[code] close_data[code] = df["close"] price_df = pd.DataFrame(close_data) # 缺失值处理:统一按前值填充,实在没有的剔除 price_df = price_df.fillna(method="ffill").dropna() # 收益率 ret_df = price_df.pct_change().dropna()

这段代码看起来简单,实际有两个细节是新手最容易翻车的。第一,download_history_data2如果放在循环里逐个股票下载,在QMT某些版本上速度会极慢,因为底层下载是单线程的。我自己的做法是传整个股票列表一次下载,或者干脆在miniQMT里用多进程并行下载到本地,速度能提高好几倍。第二,不同ETF上市时间不同,价格数据会在早期出现大量NaN,一定要先对齐再算收益,否则协方差矩阵里全是缺失值,后续优化结果完全失真。

3.3 ptrade与QMT的代码异同

在ptrade里做同样的取数操作,代码风格完全不同。ptrade不提供本地数据下载,只能通过内置API在线取,常用的是get_history:

from ptrade import get_history # 获取最近250个交易日的收盘价 df = get_history(250, '1d', 'close', stock_list, fq='pre')

注意这里的几个点。第一,get_history一次性只能取有限根数,想拿五年数据得分段拼接,或者直接用get_price等接口配合日期分段。第二,fq参数建议用pre前复权,否则遇到除权除息,价格序列会突然跳空,收益率计算直接失真。第三,ptrade的数据接口拿到的DataFrame结构和QMT完全不同,列在前、股票代码在列上,后续做pct_change()之前先确认方向。

我的经验是:不要在ptrade里做重活,它内置的计算能力有限,大规模协方差矩阵运算会卡。正确姿势是在本地或QMT里算好权重,把最终的目标权重写成一个配置文件或直接硬编码进策略,ptrade只负责按权重下单交易。这样各用所长,研究能力归QMT,执行稳定性归ptrade,配合起来非常顺手。

4. Python实现一个最小可行组合优化

4.1 数据准备与协方差矩阵计算

理论知识讲再多,不如跑一个完整的例子。下面我用一个可以直接复制的Python脚本,演示如何实现“最小方差组合”和“最大夏普组合”的求解。整个脚本不依赖QMT或ptrade,只要你有日线数据的DataFrame就能跑。

import numpy as np import pandas as pd from scipy.optimize import minimize # 假设你已经准备好多资产日收益率矩阵 # 列:资产代码,行:交易日期,值为日收益率 # ret_df = pd.read_csv("your_returns.csv", index_col=0, parse_dates=True) # 年化期望收益 mu = ret_df.mean() * 252 # 年化协方差矩阵 cov = ret_df.cov() * 252 # 资产数量 n = len(mu) # 定义目标函数:最小化组合方差 def portfolio_var(w): w = np.array(w) return w @ cov @ w # 约束:权重之和等于1 cons = ({'type': 'eq', 'fun': lambda w: np.sum(w) - 1}) # 边界:允许做空?备选:每只资产权重在0到0.4之间,避免过度集中 bounds = tuple((0, 0.4) for _ in range(n)) # 初始权重:等权 init_w = np.ones(n) / n # 优化求解最小方差组合 res_minvar = minimize(portfolio_var, init_w, method='SLSQP', bounds=bounds, constraints=cons) minvar_w = res_minvar.x minvar_ret = mu @ minvar_w minvar_vol = np.sqrt(minvar_w @ cov @ minvar_w) minvar_sharpe = (minvar_ret - 0.025) / minvar_vol print("最小方差组合权重:", dict(zip(ret_df.columns, np.round(minvar_w, 4)))) print("年化收益:", round(minvar_ret, 4)) print("年化波动:", round(minvar_vol, 4)) print("夏普比率:", round(minvar_sharpe, 4))

这段代码是组合优化的骨架,几个细节值得展开。第一,权重上界设0.4而不是1,是为了防止优化器把仓位全压到某一两个资产上,这在实盘里非常危险,因为历史规律可能失效。第二,SLSQP是scipy里处理带约束非线性优化的常用算法,对小规模组合问题足够稳定;如果资产数量很大,可以考虑换用cvxpy做凸优化,速度和稳定性更好。第三,无风险利率0.025(2.5%)只是示例,实际用当前的十年期国债收益率,不同券商终端里可以直接查。

4.2 用scipy求解最大夏普组合

最大夏普组合不能直接求最小化,因为夏普比率的分母里有组合波动率,目标函数不是纯粹的二次型。但可以换一个等价思路:最小化负夏普比率。代码如下:

def neg_sharpe(w): w = np.array(w) ret = mu @ w vol = np.sqrt(w @ cov @ w) return -(ret - 0.025) / vol # 同样的约束和边界 res_sharpe = minimize(neg_sharpe, init_w, method='SLSQP', bounds=bounds, constraints=cons) best_w = res_sharpe.x best_ret = mu @ best_w best_vol = np.sqrt(best_w @ cov @ best_w) best_sharpe = (best_ret - 0.025) / best_vol print("最大夏普组合权重:", dict(zip(ret_df.columns, np.round(best_w, 4)))) print("年化收益:", round(best_ret, 4)) print("年化波动:", round(best_vol, 4)) print("夏普比率:", round(best_sharpe, 4))

跑完之后你会看到,最大夏普组合通常会在相关性较低的资产上分配更多权重,而不是全押在收益率最高的资产上。这就是马科维茨的“分散化”在实际权重上的体现。我在第一次跑通这个代码的时候,最大夏普组合给黄金ETF分了不少权重,当时还有点不服气,因为黄金那几年涨幅平庸;但经历过一次权益市场大回调后,账户回撤确实比单一满仓宽基小了一大截,我才真的信了。

4.3 画出有效前沿

只给两个最优组合不够直观,把有效前沿画出来,才能真正理解风险收益的取舍。方法很简单:在最小方差组合和最大夏普组合之间,设定一系列目标收益,逐个求“给定收益下最小方差”的组合,连成一条曲线。

import matplotlib.pyplot as plt def min_var_for_target_ret(target_ret): cons = [ {'type': 'eq', 'fun': lambda w: np.sum(w) - 1}, {'type': 'eq', 'fun': lambda w: mu @ w - target_ret} ] res = minimize(portfolio_var, init_w, method='SLSQP', bounds=bounds, constraints=cons) return res.x target_rets = np.linspace(mu @ minvar_w, mu @ best_w, 30) frontier_vols = [] frontier_rets = [] for tr in target_rets: w = min_var_for_target_ret(tr) vol = np.sqrt(w @ cov @ w) frontier_vols.append(vol) frontier_rets.append(tr) plt.figure(figsize=(10, 6)) plt.scatter(frontier_vols, frontier_rets, c='b', label='有效前沿') plt.scatter(minvar_vol, minvar_ret, c='g', marker='*', s=200, label='最小方差组合') plt.scatter(best_vol, best_ret, c='r', marker='*', s=200, label='最大夏普组合') plt.xlabel('年化波动率') plt.ylabel('年化收益率') plt.title('马科维茨有效前沿示例') plt.legend() plt.grid(True) plt.show()

这张图画出来以后,你一眼就能看到最大夏普组合并不在收益最高点,而是在“风险收益最划算”的位置。有效前沿的曲率越大,说明分散化带来的好处越明显;如果曲线很平甚至接近直线,说明这些资产之间的相关性太高,分散基本没效果。这也是我用它来做“预检”的原因:正式跑策略之前,先看资产池的相关性结构,如果所有资产相关系数都高于0.8,这个池子本身就不适合做马科维茨优化,不如直接换标的。

5. 常见问题与排查技巧实录

5.1 QMT和ptrade运行环境问题速查

做量化的人一大半时间不是在写策略,而是在折腾环境。我在这个系列里收到过最多的问题集中在三个点:QMT的Python库下载失败、大QMT单线程下载慢、ptrade自动打新配置。逐个说。

QMT的Python库下载失败,最常见的原因有两个。一是Python版本不匹配,xtquant要求特定版本范围,有些用户电脑里装的是最新版Python反而装不上,建议严格按券商文档推荐版本建虚拟环境。二是下载源问题,国内直连部分海外源慢或超时,换成阿里云镜像或清华镜像基本能解决,命令就是:

pip install xtquant -i https://pypi.tuna.tsinghua.edu.cn/simple

大QMT单线程的问题是老用户都知道的痛。下载历史数据时,界面版QMT经常一个股票一个股票地跑,几百只股票能下载半天。我的解法是用miniQMT模式,绕过图形界面,直接用xtdata.download_history_data2传整个列表,实测速度能快几倍;如果股票数量特别多,再叠加concurrent.futures做多进程并行,但要注意券商对并发连接数有限制,并发太猛会被限流。

ptrade自动打新(新股新债)则是平台自带能力,不需要写复杂代码。在策略配置里开启自动打新功能,再配合定时任务设置成每天固定时间检查,就能自动申购沪深两市的新股和可转债。它和马科维茨组合优化是两条腿走路:打新负责低风险补充收益,组合优化负责管好存量底仓。

问题常见原因解决办法
QMT装Python库失败Python版本不匹配或下载源慢建虚拟环境、换清华/阿里镜像
大QMT下载数据极慢界面版单线程下载用miniQMT或并发下载
ptrade报错“无权限”券商未开通对应接口权限联系客户经理开通,别自己硬试
协方差矩阵有NaN上市日期不同导致数据缺失先对齐日期再算收益

5.2 马科维茨在A股的失灵时刻

我必须诚实地说,马科维茨的框架放在A股不是万能的,尤其在2015年、2018年那种系统性下跌里,几乎所有资产的相关系数会同时飙升到0.9以上,分散化瞬间失效。这不是理论错了,而是极端行情下“风险”的定义变了:平时是波动率,危机时是流动性,市场里所有风险资产都被当成同一个“风险资产”抛售。

所以在实盘里,我通常会在马科维茨的权重基础上做两件事。第一,叠加风控约束,比如单一行业上限不超过30%、总仓位根据市场波动率动态调整等,这些约束需要在优化代码里以不等式条件加入。第二,不做过于频繁的调仓,组合优化的结果是基于历史协方差矩阵的,而这个矩阵本身不稳定,月度或季度调仓一次足够,天天调只会不断追着噪音跑,手续费还把收益吃得干干净净。

还有一个A股特有的坑:历史收益用得太长反而不好。A股风格轮动非常快,三年前的行业收益对当下没有太多参考意义,直接用长期均值会把权重分配导向“过去涨得好的资产”,容易在风格切换时踩坑。我自己的做法是用滚动两年的日收益数据估计均值与协方差,按季度滚动更新,比一次性用五年长数据稳健得多。

5.3 小白最容易被骗的收益幻觉

最后说点大实话。量化和组合优化被很多课程讲得神乎其神,什么“稳赚不赔”“年化翻倍”,全是话术。任何优化器,包括马科维茨这个拿了诺贝尔经济学奖的理论,喂进去的是历史数据的估计值,出来的权重就是“历史的总结”,而不是“未来的预言”。你能控制的是风险结构,不能控制的是行情本身。

“5年六倍”这类数字,最大的作用不是作为目标,而是作为体检标准:如果一个策略回测里的收益好到不真实,先检查是不是用了未来函数,是不是在收益率里偷偷包含了幸存者偏差,是不是把手续费和滑点设成了零。我在QMT和ptrade里调试过的策略,至少有一半是死于这些暗坑,而不是死于策略逻辑本身。

如果你是个刚开始学量化的小白,我的建议是:别一上来就追求收益,先用马科维茨做一个最简单的两只ETF组合,跑通数据、优化、下单的完整链路,体验一下“配置”和“猜测”之间的区别。配置是你可以持续复制的技能,猜测是运气,长期来看,前者才是账户能拿得住收益的真正原因。

我个人实际操作下来最大的体会是:马科维茨这套东西,靠一次两次优化是看不出威力的,它的价值要放在一个完整牛熊周期里才能体现。市场好的时候它可能跑不过你闭眼买的单只大牛股,但市场一转弱,分散化带来的回撤控制会让你少很多焦虑,至少不会在底部恐慌清仓。想进阶的话,下一步可以研究Black-Litterman模型,在马科维茨的框架里加入你自己对行业的观点,让权重不完全是历史数据的奴隶,那样做出来的组合,才真正算得上是你自己的策略。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询