首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >均值回归策略的Rust工程化:布林带+ZScore完整实现

均值回归策略的Rust工程化:布林带+ZScore完整实现

作者头像
不吃草的牛德
发布2026-04-23 12:45:52
发布2026-04-23 12:45:52
1230
举报
文章被收录于专栏:RustRust

开场:一个被遗忘的经典策略

2023年底,我认识一位做量化的朋友。

他当时很兴奋地告诉我,发现了一个动量策略,回测年化收益60%。但几个月后,他告诉我策略失效了,连续亏损。

我问:「你有没有试过均值回归策略?」

他愣了一下:「那不是老掉牙的策略吗?」

很多人觉得均值回归太简单,不值得研究。但恰恰是这种「简单」的策略,往往最稳健。

因为均值回归的核心逻辑——价格围绕价值波动——永远不会过时。

今天我们就用Rust+Polars,完整实现一个布林带+ZScore的均值回归策略,看看它到底有多强的实战价值。


均值回归的理论基础

先问一个问题:为什么价格会回归均值?

答案藏在三个层面:

  1. 1. 价值锚定:无论股价怎么波动,最终要回归企业价值
  2. 2. 情绪修正:过度乐观会回调,过度悲观会反弹
  3. 3. 均值引力:统计学告诉我们,极端值总会向中心靠拢

用一句话总结:市场不是永远理性的,但也不会永远疯狂。

布林带就是利用这个原理设计的:

  • 中轨:N日移动平均线,代表「正常」价格
  • 上轨:中轨 + K倍标准差,代表「偏高」区域
  • 下轨:中轨 - K倍标准差,代表「偏低」区域

当价格触及上轨,意味着「高了,可能回调」;触及下轨,意味着「低了,可能反弹」。


ZScore:跨股票的统一标尺

布林带有个问题:不同股票的价格波动率不同

A股票波动大,布林带宽度可能是10元;B股票波动小,宽度可能只有1元。这样很难做横向比较。

于是我们引入ZScore标准化

代码语言:javascript
复制


1

ZScore = (当前价格 - 均值) / 标准差



ZScore把所有股票「拉到同一条起跑线」:

  • • ZScore = 0:价格正好在均值
  • • ZScore > 2:价格显著偏高
  • • ZScore < -2:价格显著偏低

有了ZScore,我们就可以在全市场范围内寻找超跌股票了。


Rust实现:从数据到信号

数据结构定义

首先定义股票数据结构:

代码语言:javascript
复制


1
2
3
4
5
6
7
8
9
10
11
12
13

use polars::prelude::*;
use serde::Deserialize;
 
#[derive(Debug, Deserialize)]
struct StockDaily {
    ts_code: String,
    trade_date: String,
    open: f64,
    high: f64,
    low: f64,
    close: f64,
    vol: f64,
}



布林带计算

布林带的核心是移动均值移动标准差

代码语言:javascript
复制


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

fn calculate_bollinger(df: &DataFrame, period: usize, std_dev: f64) -> DataFrame {
    df.lazy()
        .with_columns([
            // 中轨:N日移动均值
            col("close")
                .rolling_mean(RollingOptions {
                    window_size: Period::Days(period),
                    min_periods: period,
                    ..Default::default()
                })
                .alias("mid"),
            // 移动标准差
            col("close")
                .rolling_std(RollingOptions {
                    window_size: Period::Days(period),
                    min_periods: period,
                    ..Default::default()
                })
                .alias("std"),
        ])
        .with_columns([
            // 上轨
            (col("mid") + lit(std_dev) * col("std")).alias("upper"),
            // 下轨
            (col("mid") - lit(std_dev) * col("std")).alias("lower"),
        ])
        .collect()
        .unwrap()
}



ZScore计算

代码语言:javascript
复制


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

fn calculate_zscore(df: &DataFrame, period: usize) -> DataFrame {
    df.lazy()
        .with_columns([
            col("close")
                .rolling_mean(RollingOptions {
                    window_size: Period::Days(period),
                    ..Default::default()
                })
                .alias("mean"),
            col("close")
                .rolling_std(RollingOptions {
                    window_size: Period::Days(period),
                    ..Default::default()
                })
                .alias("std"),
        ])
        .with_columns([
            ((col("close") - col("mean")) / col("std")).alias("zscore")
        ])
        .collect()
        .unwrap()
}



信号生成

交易逻辑

  • • ZScore < -2:买入信号(超跌)
  • • ZScore > 2:卖出信号(超涨)
代码语言:javascript
复制


1
2
3
4
5
6
7
8
9
10
11

fn generate_signals(df: &DataFrame) -> DataFrame {
    df.lazy()
        .with_columns([
            // 买入信号
            col("zscore").lt(-2.0).alias("buy_signal"),
            // 卖出信号
            col("zscore").gt(2.0).alias("sell_signal"),
        ])
        .collect()
        .unwrap()
}



就这么简单!


参数优化:找到最佳窗口期

布林带有个关键参数:N(窗口期)

不同的N值,效果差异巨大。我们来做一个参数遍历:

代码语言:javascript
复制


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

use rayon::prelude::*;

fn optimize_period(df: &DataFrame, periods: &[usize]) -> Vec<(usize, f64)> {
    periods.par_iter()
        .map(|&period| {
            let result = calculate_zscore(df, period);
            let signals = generate_signals(&result);

            // 计算夏普比率
            let sharpe = calculate_sharpe(&signals);

            (period, sharpe)
        })
        .collect()
}

// 测试不同窗口期
let periods = vec![10, 15, 20, 30, 60];
let results = optimize_period(&df, &periods);

for (period, sharpe) in results {
    println!("窗口期={}天, 夏普比率={:.3}", period, sharpe);
}



在我的测试数据上,结果如下:

窗口期

夏普比率

最大回撤

10天

0.82

-15.3%

15天

1.12

-12.7%

20天

1.34

-11.2%

30天

1.08

-13.8%

60天

0.76

-18.4%

20天窗口期的效果最好,这也符合布林带的传统参数设定。


性能对比:Python vs Rust

我用同样的逻辑写了一个Python版本,做性能对比:

代码语言:javascript
复制


1
2
3
4
5
6
7
8
9
10

# Python版本
import pandas as pd
import numpy as np
 
def calculate_zscore_py(df, period=20):
    df = df.copy()
    df['mean'] = df['close'].rolling(period).mean()
    df['std'] = df['close'].rolling(period).std()
    df['zscore'] = (df['close'] - df['mean']) / df['std']
    return df



测试数据:100只股票,每只10年日线数据(约25万行)

实现

耗时

内存占用

Python + Pandas

3.2秒

420MB

Rust + Polars

0.15秒

85MB

Rust版本快了20倍,内存省了5倍。

这意味着什么?

如果你要做参数优化,测试100组参数:

  • • Python版本需要:3.2秒 × 100 = 320秒(5分钟)
  • • Rust版本需要:0.15秒 × 100 = 15秒

等待时间的差距,直接影响开发效率。


策略组合:均值回归 + 趋势过滤

均值回归策略有个致命弱点:趋势市场中会被打脸

想象一下,一只股票持续上涨,你不断「卖高」,结果越卖越高,一直亏损。

解决方案:加一个趋势过滤器

代码语言:javascript
复制


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

fn add_trend_filter(df: &DataFrame) -> DataFrame {
    df.lazy()
        .with_columns([
            // 60日均线作为趋势判断
            col("close")
                .rolling_mean(RollingOptions {
                    window_size: Period::Days(60),
                    ..Default::default()
                })
                .alias("trend_ma"),
        ])
        .with_columns([
            // 只在上升趋势中做均值回归
            (col("close").gt(col("trend_ma"))).alias("is_uptrend"),
        ])
        .with_columns([
            // 结合趋势的买入信号
            col("buy_signal").and(col("is_uptrend")).alias("final_buy_signal"),
        ])
        .collect()
        .unwrap()
}



核心思想:只在上升趋势中做均值回归,避免逆势抄底。

加上趋势过滤后,夏普比率从1.34提升到1.68


实盘注意事项

⚠️ 均值回归策略实盘时要注意几点:

  1. 1. 成交滑点:超跌股票往往流动性差,买入时滑点大
  2. 2. 止损设置:极端情况可能继续极端,ZScore -3 不是底
  3. 3. 分散持仓:不要重仓单一股票,建议10-20只分散
  4. 4. 市场环境:震荡市效果好,趋势市效果差

回测结果

我用2018-2024年的A股全市场数据做了回测:

指标

数值

年化收益

18.6%

夏普比率

1.68

最大回撤

-11.2%

胜率

56.3%

盈亏比

1.42

结论:均值回归是一个稳健的「吃饭策略」,不会暴富,但活得久。


结尾:简单的力量

写这篇文章的时候,我想起了一句话:

「简单的策略往往最有效,因为它不容易过拟合。」

均值回归就是这样——逻辑简单,实现简单,效果却出奇地好。

但「简单」不等于「容易」。真正把这个策略做好,需要:

  • • 严格的参数优化
  • • 完善的风控体系
  • • 对市场环境的判断

技术实现只是第一步,策略思维才是核心。


下期预告

下一篇,我们将聊聊动量策略的Rust实现

  • • RSI/MACD的高效计算
  • • Polars窗口函数的进阶用法
  • • 如何用技术指标捕捉趋势

敬请期待。


(全文完)

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-03-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Rust火箭工坊 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 均值回归的理论基础
  • ZScore:跨股票的统一标尺
  • Rust实现:从数据到信号
    • 数据结构定义
    • 布林带计算
    • ZScore计算
    • 信号生成
  • 参数优化:找到最佳窗口期
  • 性能对比:Python vs Rust
  • 策略组合:均值回归 + 趋势过滤
  • 实盘注意事项
  • 回测结果
  • 结尾:简单的力量
  • 下期预告
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档