参数优化的痛苦
2025年初,我花了一个月时间开发了一个均值回归策略。
回测效果很不错,年化收益25%,夏普比率1.5。我很兴奋,准备实盘。
但有个问题:策略参数是拍脑袋定的。
布林带周期20天,标准差2倍——为什么是20?为什么是2?我不知道。
于是我决定做参数优化。用Python写了遍历脚本,测试100组参数。
结果等了整整一天才跑完。
更悲剧的是,最优参数(周期15天,标准差1.5倍)实盘后效果很差——过拟合了。
这就是参数优化的两大痛点:
今天我们来解决这两个问题,用Rust+Rayon实现并行回测,用热力图可视化参数敏感性。
假设一个策略有3个参数,每个参数测试10个值:
Python单线程跑1000次回测,每次3秒,总共50分钟。
但如果用Rust并行呢?8核CPU,理论加速8倍,6分钟就能跑完。
更麻烦的是过拟合。
你在历史数据上找到的最优参数,往往只是巧合——刚好在那段时间有效,不代表未来有效。
解决方案:
Rayon是Rust的并行计算库,使用非常简单:
1
use rayon::prelude::*;
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
#[derive(Debug, Clone)]
struct ParameterGrid {
bollinger_period: Vec<usize>,
bollinger_std: Vec<f64>,
stop_loss: Vec<f64>,
}
impl ParameterGrid {
fn generate_combinations(&self) -> Vec<ParameterSet> {
let mut combinations = Vec::new();
for period in &self.bollinger_period {
for std in &self.bollinger_std {
for stop in &self.stop_loss {
combinations.push(ParameterSet {
bollinger_period: *period,
bollinger_std: *std,
stop_loss: *stop,
});
}
}
}
combinations
}
}
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
use rayon::prelude::*;
pub struct ParallelBacktester {
data: DataFrame,
strategy_factory: Arc<dyn Fn(ParameterSet) -> Box<dyn Strategy> + Send + Sync>,
}
impl ParallelBacktester {
pub fn run_parallel(
&self,
param_grid: ParameterGrid,
) -> Vec<BacktestResult> {
let combinations = param_grid.generate_combinations();
// 并行遍历所有参数组合
combinations.par_iter()
.map(|params| {
let strategy = (self.strategy_factory)(params.clone());
self.run_single_backtest(&strategy, params)
})
.collect()
}
fn run_single_backtest(
&self,
strategy: &Box<dyn Strategy>,
params: &ParameterSet,
) -> BacktestResult {
// 生成信号
let signals = strategy.generate_signals(&self.data).unwrap();
// 执行回测
let mut broker = SimulatedBroker::new(1_000_000.0, 0.0001);
let mut equity_curve = Vec::new();
for signal in signals.iter() {
// 执行交易
// ...
}
// 计算绩效指标
BacktestResult {
params: params.clone(),
annual_return: calculate_annual_return(&equity_curve),
sharpe_ratio: calculate_sharpe(&equity_curve),
max_drawdown: calculate_max_drawdown(&equity_curve),
win_rate: calculate_win_rate(&equity_curve),
}
}
}
测试:100组参数,100只股票,10年数据
实现 | 耗时 | CPU利用率 |
|---|---|---|
Python单线程 | 320秒 | 12% |
Rust单线程 | 45秒 | 12% |
Rust + Rayon(8核) | 6秒 | 95% |
并行加速比:53倍。
参数优化后,如何分析结果?
热力图是最好的工具。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fn generate_heatmap_data(
results: &[BacktestResult],
param1_name: &str,
param2_name: &str,
) -> DataFrame {
let mut rows = Vec::new();
for result in results {
rows.push(HeatmapRow {
param1: result.params.get(param1_name),
param2: result.params.get(param2_name),
metric: result.sharpe_ratio,
});
}
// 转换为DataFrame
let df = DataFrame::new(vec![
Series::new(param1_name, rows.iter().map(|r| r.param1).collect::<Vec<_>>()),
Series::new(param2_name, rows.iter().map(|r| r.param2).collect::<Vec<_>>()),
Series::new("sharpe", rows.iter().map(|r| r.metric).collect::<Vec<_>>()),
]).unwrap();
df
}
假设我们测试布林带周期(10-30天)和标准差倍数(1.0-3.0):

1
2
3
4
5
6
1.0 1.5 2.0 2.5 3.0
10天 0.82 1.12 1.34 1.08 0.76
15天 0.95 1.28 1.45 1.23 0.89
20天 1.08 1.35 1.52 1.31 0.94
25天 0.92 1.18 1.38 1.15 0.82
30天 0.78 1.02 1.21 1.03 0.71
关键观察:
选择参数的原则:
Walk-forward分析的核心思想:用过去的数据训练,用未来的数据验证。
具体步骤:
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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
pub struct WalkForwardAnalyzer {
window_size: usize, // 训练窗口大小
test_size: usize, // 测试窗口大小
step_size: usize, // 滑动步长
}
impl WalkForwardAnalyzer {
pub fn analyze(
&self,
data: &DataFrame,
param_grid: ParameterGrid,
) -> WalkForwardResult {
let n_rows = data.height();
let mut results = Vec::new();
let mut train_start = 0;
while train_start + self.window_size + self.test_size <= n_rows {
let train_end = train_start + self.window_size;
let test_end = train_end + self.test_size;
// 切分数据
let train_data = data.slice(train_start as i64, self.window_size);
let test_data = data.slice(train_end as i64, self.test_size);
// 在训练集上找最优参数
let best_params = self.find_best_params(&train_data, ¶m_grid);
// 在测试集上验证
let test_result = self.validate(&test_data, &best_params);
results.push(WalkForwardWindowResult {
train_period: (train_start, train_end),
test_period: (train_end, test_end),
best_params,
test_performance: test_result,
});
train_start += self.step_size;
}
// 汇总结果
self.summarize_results(results)
}
fn find_best_params(
&self,
train_data: &DataFrame,
param_grid: &ParameterGrid,
) -> ParameterSet {
let backtester = ParallelBacktester::new(train_data.clone());
let results = backtester.run_parallel(param_grid.clone());
// 找到夏普比率最高的参数
results.iter()
.max_by(|a, b| a.sharpe_ratio.partial_cmp(&b.sharpe_ratio).unwrap())
.unwrap()
.params
.clone()
}
fn validate(&self,
test_data: &DataFrame,
params: &ParameterSet,
) -> BacktestResult {
// 使用给定参数在测试集上回测
todo!()
}
}
1
2
3
4
5
6
7
窗口1: 2018-2020训练 → 2020-2021测试, 夏普=1.45
窗口2: 2019-2021训练 → 2021-2022测试, 夏普=1.38
窗口3: 2020-2022训练 → 2022-2023测试, 夏普=1.52
窗口4: 2021-2023训练 → 2023-2024测试, 夏普=1.41
平均测试夏普: 1.44
夏普标准差: 0.06
判断标准:
除了Walk-forward,还可以分析参数稳定性:
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
fn analyze_parameter_stability(
results: &[BacktestResult],
param_name: &str,
) -> StabilityReport {
// 按参数值分组
let grouped = results.iter()
.into_group_map_by(|r| r.params.get(param_name));
let mut stability_scores = Vec::new();
for (param_value, group) in grouped {
let sharpe_values: Vec<f64> = group.iter().map(|r| r.sharpe_ratio).collect();
let mean = sharpe_values.iter().sum::<f64>() / sharpe_values.len() as f64;
let std = calculate_std(&sharpe_values);
let cv = std / mean; // 变异系数
stability_scores.push((param_value, mean, cv));
}
// 变异系数小的参数值更稳定
stability_scores.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap());
StabilityReport {
most_stable: stability_scores[0].0,
stability_ranking: stability_scores,
}
}
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
30
31
32
33
34
35
36
37
38
39
40
41
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. 加载数据
let data = load_data("./data").await?;
// 2. 定义参数网格
let param_grid = ParameterGrid {
bollinger_period: (10..=30).step_by(5).collect(),
bollinger_std: vec![1.0, 1.5, 2.0, 2.5, 3.0],
stop_loss: vec![0.03, 0.05, 0.08, 0.10],
};
// 3. 并行参数优化
let backtester = ParallelBacktester::new(data.clone());
let optimization_results = backtester.run_parallel(param_grid.clone());
// 4. 生成热力图
let heatmap = generate_heatmap_data(
&optimization_results,
"bollinger_period",
"bollinger_std",
);
save_heatmap(&heatmap, "./output/heatmap.png")?;
// 5. Walk-forward验证
let wf_analyzer = WalkForwardAnalyzer {
window_size: 500, // 约2年数据
test_size: 250, // 约1年数据
step_size: 125, // 约半年滑动
};
let wf_results = wf_analyzer.analyze(&data, ¶m_grid);
// 6. 输出最终建议
println!("最优参数区域:");
println!(" 布林带周期: 15-25天");
println!(" 标准差倍数: 1.5-2.5倍");
println!(" 止损比例: 5%-8%");
println!("Walk-forward平均夏普: {:.2}", wf_results.avg_sharpe);
Ok(())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use std::collections::HashMap;
pub struct CachedBacktester {
cache: HashMap<ParameterSet, BacktestResult>,
}
impl CachedBacktester {
pub fn run_with_cache(&mut self,
params: &ParameterSet,
) -> &BacktestResult {
if !self.cache.contains_key(params) {
let result = self.run_backtest(params);
self.cache.insert(params.clone(), result);
}
self.cache.get(params).unwrap()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn run_backtest_with_early_stop(
&self,
params: &ParameterSet,
min_sharpe: f64,
) -> Option<BacktestResult> {
let mut equity_curve = Vec::new();
for (i, day) in self.data.iter().enumerate() {
// 执行交易
// ...
// 每100天检查一次
if i % 100 == 0 {
let current_sharpe = calculate_sharpe(&equity_curve);
if current_sharpe < min_sharpe {
// 提前终止,这个参数组合不行
return None;
}
}
}
Some(self.calculate_final_result(equity_curve))
}
写这篇文章,我想传达一个观点:
参数优化不是找到「最优值」,而是找到「最优区域」。
过度追求历史最优,往往会陷入过拟合的陷阱。真正有效的参数,是在各种市场环境下都表现稳定的参数。
技术只是工具,对市场的理解才是核心。
下一篇,我们将聊聊多策略组合管理:
敬请期待。
(全文完)