halo 的技术博客

返回

大多数投资组合方法都活在「批处理」的世界里:先拿一段历史,估一个协方差矩阵,解一次优化,得到一组权重,然后拿去用。但真实的交易是逐日到来的——今天收盘,你必须为明天定好仓位,而你手里只有到今天为止的数据。这就是在线投资组合选择(Online Portfolio Selection, OLPS) 要解决的问题:把组合更新写成一个每天迭代一次的规则,不回头重算全历史。

本文聊 OLPS 里最经典、也最实用的一支——OLMAR(On-Line Moving Average Reversion),由 Li 和 Hoi 于 2012 年提出。它的核心信仰只有一句话:价格会向移动平均线回复。 今天跌破均线的资产,明天大概率反弹;今天冲高的,明天大概率回落。OLMAR 把这个均值回复的信念,写成一条闭式的在线更新规则。

我们用 6 只资产、756 个交易日(3 年)、带均值回复结构和一次系统性崩盘的合成市场,把它跑成看得见的数据:OLMAR 总收益 41.9%,年化 12.38%,显著跑赢等权买入持有的 17.5% 和 UCRP(均匀恒定再平衡)的 30.0%;20 个种子里 12/20 跑赢 UCRP。但代价也很实:日均换手率高达 104%,最大回撤 −42.3%——比 UCRP 的 −22% 深了近一倍。这正是在线反转策略的两面性。

一、OLPS 的问题设定#

设有 M 只资产,第 t 天的价格相对(price relative) 向量为

x_t = p_t / p_{t-1}   (逐元素相除,x_{t,i} > 1 表示第 i 只资产当天上涨)
plaintext

组合用权重向量 b_t 表示,落在单纯形 Δ_m = {b : b_i ≥ 0, Σ b_i = 1} 上(只做多、满仓)。第 t 天的组合收益倍数是 b_t · x_t,累计财富

S_n = Π_{t=1}^{n} (b_t · x_t)
plaintext

OLPS 的目标:在不预知未来的前提下,逐日选 b_t,让最终 S_n 尽可能大。评价基准通常有三个:

  • UCRP(Uniform Constant Rebalanced Portfolio):每天再平衡回等权 b = 1/M
  • 等权买入持有(BAH):初始等权买入后不动,权重随价格漂移;
  • 事后最优单一资产(Best Stock):上帝视角选出 3 年涨最多的那只——这是任何不预知未来的算法都追不上的作弊上界。

二、核心思想:移动平均反转#

OLMAR 的灵魂是预测下一期价格相对。它不用复杂模型,只用一条移动平均:

x̃_{t+1,i} = MA_w(p)_i / p_{t,i} = (1/w · Σ_{k=0}^{w-1} p_{t-k,i}) / p_{t,i}
plaintext

直觉极简单:MA_w / p_t 就是「均线相对于当前价的位置」。

  • 当前价 p_t 低于均线 → x̃ > 1 → 预测反弹 → 加仓
  • 当前价 高于均线 → x̃ < 1 → 预测回落 → 减仓

这就是均值回复(mean reversion)在组合层面的翻译。它和动量策略正好相反——动量追涨杀跌,OLMAR 抄底止盈。

import numpy as np

WIN = 5   # 移动平均窗口

def predict(prices, t, window=WIN):
    """用 MA_w(p_t)/p_t 预测 t+1 的价格相对向量。"""
    if t < window:
        return np.ones(prices.shape[1])
    ma = prices[t - window + 1 : t + 1].mean(0)
    return ma / prices[t]
python

三、在线更新规则:被动-主动的闭式解#

有了预测 ,OLMAR 怎么把它变成新权重?它借用了在线学习里的被动-主动(Passive-Aggressive, PA) 框架:在「预测收益不低于阈值 ε」的约束下,找一个离当前权重最近的新权重。写成优化问题:

b_{t+1} = argmin_{b ∈ Δ_m}  (1/2)‖b − b_t‖²   s.t.  b · x̃ ≥ ε
plaintext

「离 b_t 最近」就是被动(passive)——不轻易动仓;「满足 b·x̃ ≥ ε」就是主动(aggressive)——一旦预测足够强就调仓。它有一个漂亮的闭式解:

λ_t = max(0, (ε − b_t · x̃) / ‖x̃ − x̄·1‖²)   (x̄ 是 x̃ 的均值)
b_{t+1} = b_t + λ_t · (x̃ − x̄·1)
plaintext

然后把 b_{t+1} 投影回单纯形 Δ_m(去掉负权重、重新归一)。λ_t 是自适应步长:当预测收益 b_t·x̃ 已经超过 ε 时 λ_t = 0(不动仓);差得越远,调仓越猛。

def project_simplex(v):
    """把向量投影到单纯形 {b>=0, sum b=1}(Duchi 2008)。"""
    u = np.sort(v)[::-1]
    css = np.cumsum(u)
    rho = np.nonzero(u * np.arange(1, len(v) + 1) > (css - 1))[0][-1]
    tau = (css[rho] - 1) / (rho + 1.0)
    return np.maximum(v - tau, 0)

def olmar_step(b, x_pred, eps=10.0):
    x_bar = x_pred.mean()
    denom = float(((x_pred - x_bar) ** 2).sum())
    lam = max(0.0, (eps - float(b @ x_pred)) / denom) if denom > 1e-12 else 0.0
    return project_simplex(b + lam * (x_pred - x_bar))
python

ε反转强度阈值:越大越激进(愿意为追预测偏离当前仓位越远),越小越保守。这是 OLMAR 唯一真正需要调的超参数(窗口 w 是第二个)。

四、回测结果:在均值回复市场里跑赢等权#

把逐日更新串起来,跑完整 3 年:

def olmar(X, prices, window=5, eps=10.0):
    T, M = X.shape
    b = np.ones(M) / M
    rets = np.zeros(T); W = np.zeros((T, M))
    for t in range(T):
        W[t] = b
        if t > 0:
            rets[t] = float(b @ X[t]) - 1.0   # 昨收后定的 b,今日实现
        x_pred = predict(prices, t, window)
        b = olmar_step(b, x_pred, eps)
    return rets, W
python

主要数字(canonical seed, ε=10, w=5):

策略总收益年化Sharpe波动最大回撤
OLMAR41.9%12.38%0.5037.4%−42.3%
UCRP 等权再平衡30.0%9.15%0.5022.1%−22.2%
等权买入持有17.5%5.52%0.3522.0%−22.5%
事后最优单资产(作弊上界)56.3%16.05%0.6629.0%−30.7%

OLMAR vs 基准:均值回复市场里,反转再平衡跑赢等权

三个关键观察:

  1. 总收益碾压等权:OLMAR 的 41.9% 是买入持有的 2.4 倍,接近「事后最优单资产」这个作弊上界的 74%——对一个不预知未来的在线算法,这个成绩相当可观。
  2. Sharpe 却和 UCRP 打平(都是 0.50):因为 OLMAR 的收益是靠加大波动换来的(波动 37.4% vs UCRP 22.1%)。风险调整后并没有免费的午餐。
  3. 回撤更深:−42.3% vs −22.2%。图里的崩盘窗口(阴影)清楚显示 OLMAR 在系统性下跌中因为「越跌越买」而吃了更大的回撤,但也正因如此在随后的反弹里赚回更多。均值回复策略的宿命:在真反转里封神,在单边趋势里被反复打脸。

五、超参数敏感性:ε 和窗口 w#

OLMAR 的表现对两个超参数敏感。先看反转强度 ε:

ε 敏感性:阈值太小不动、太大过度激进,中段最优

  • ε 太小(接近 1):约束 b·x̃ ≥ ε 几乎总是满足,λ_t ≈ 0,OLMAR 退化成「几乎不动仓」,收益接近 UCRP;
  • ε 太大(>20):每天都拼命调仓追预测,换手爆炸、噪声主导,Sharpe 反而下滑;
  • 中段(ε≈8~15) 取得收益和稳健的平衡。

再看移动平均窗口 w:

窗口敏感性:窗口过短追噪声、过长反转信号钝化

  • 窗口过短(w=2~3):均线贴着价格跑,MA/p ≈ 1,反转信号被日内噪声淹没;
  • 窗口过长(w>20):均线太钝,反转信号滞后,错过反弹拐点;
  • 中等窗口(w≈5~10)通常最优。原论文默认 w=5,与我们的合成结果一致。

六、权重演化:崩盘后主动加仓被错杀资产#

OLMAR 最有辨识度的行为,是在系统性崩盘后逆势加仓。看权重演化热力图:

OLMAR 组合权重演化:崩盘后主动加仓被错杀资产

崩盘窗口(两条虚线之间)里,被砸得最狠的资产价格远低于均线,MA/p 飙升,OLMAR 把权重猛砸向它们。如果随后是真反弹,这份逆势加仓就是超额收益的来源;如果是趋势性崩塌(价格继续跌、均线跟着下移),这份加仓就是灾难。这就是均值回复策略在实盘里最危险的地方——它无法区分「超跌反弹」和「基本面崩塌」。

七、鲁棒性:20 个种子#

单次结果可能侥幸。跑 20 个不同种子的合成市场确认:

  • OLMAR Sharpe 均值 0.85 ± 0.54,UCRP 0.51 ± 0.03
  • OLMAR > UCRP:12/20 个种子。

两个信息量很大的细节:其一,OLMAR 的 Sharpe 标准差(±0.54)远高于 UCRP(±0.03)——它是高方差策略,赢的时候赢很多,输的时候也输得难看。其二,12/20 的胜率说明「均值回复能否跑赢等权」高度依赖市场是否真的均值回复;在 8/20 的样本里,市场的趋势性让 OLMAR 反而吃亏。这和真实世界一致:OLMAR 在震荡市封神,在单边趋势市(尤其是持续下跌)会被反复止损打脸。

八、六大真实陷阱#

  1. 交易成本吞噬:这是头号坑。我们测出 OLMAR 日均换手率 104%——意味着几乎每天把组合翻个底朝天。纸面 41.9% 的收益,扣掉双边 10bps 的成本后会被腰斩。任何 OLMAR 实盘都必须把成本写进目标函数,或用 ε 和最小调仓阈值抑制换手。
  2. 趋势市失效:OLMAR 假设均值回复。遇到单边趋势(尤其是持续下跌的熊市),「越跌越买」会持续加仓下跌资产,回撤急剧放大。必须叠加趋势过滤(如价格跌破长期均线时禁止加仓)。
  3. 单纯形约束丢空头:标准 OLMAR 只做多(权重非负)。它无法做空「预测下跌」的资产,等于浪费了一半反转信号。要用空头需改用允许负权重的变体(如带杠杆约束的在线镜像下降)。
  4. 预测即真理的幻觉MA/p 只是一个极粗糙的价格预测。把它当成「真实的下期收益」会系统性高估策略能力。实盘中应把 视为信号而非预言,并叠加多种预测(OLMAR 的后继 RMR、WMAMR 就是在改进预测)。
  5. 投影引入的隐性再平衡:单纯形投影会把负权重截断为 0 再归一,这本身是一次「隐性交易」。回测里若不把投影前后的权重差计入换手,会低估真实成本。
  6. 窗口/ε 的过拟合:w 和 ε 很容易在历史上调到最优,样本外失效。必须用 walk-forward 或时间序列交叉验证选参,且警惕「在同一段历史上既选参又评估」的双重偷看。

九、结语#

OLMAR 的价值不在于它是「最赚钱的算法」——它显然不是,风险调整后甚至只和等权再平衡打平。它的价值在于范式:把投资组合从「批处理优化」变成「逐日在线更新」,用一条闭式规则(预测→PA 更新→单纯形投影)完成再平衡,天然适配流式数据和实盘节奏。

对量化研究者而言,OLMAR 最该记住的三件事:

  • 它是纯均值回复策略,只在震荡市有效,遇到趋势市会被反复打脸——上线前务必叠加 regime 判断或趋势过滤;
  • 换手率是它的阿喀琉斯之踵,104% 的日换手意味着不控成本就是给券商打工;
  • 它属于 OLPS 家族的起点,后续的 RMR(用中位数替代均值抗离群)、PAMR、CWMR、OLMAR-2(多窗口)都是在「预测」和「更新」两个环节上打补丁。理解了 OLMAR 的骨架,整个在线组合选择的谱系就通了。

它和之前聊过的均值回复配对交易、风险平价再平衡是同一套思想在不同层的投影:在震荡里赚回复的钱,在趋势里认亏离场。

注:本文价格序列为带均值回复结构 + 一次系统性崩盘的自洽合成数据,用于演示 OLMAR 算法机制;真实市场的均值回复强度时变、且常被趋势和跳空打断,落地需接入真实价格、扣真实交易成本、叠加趋势过滤与 regime 判断,并做样本外与 walk-forward 验证。

在线投资组合 OLMAR:跟随均值回复的在线再平衡算法
https://blog.halo26812.eu.org/blog/online-portfolio-olmar
Author halo
Published at 2026年7月14日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨