一次惨痛的教训
2015年6月,A股经历了惊心动魄的股灾。
我同学的一位私募经理,在那之前他的策略年化收益超过40%,回撤控制在10%以内。但股灾来临后,他的策略在两周内亏损了35%。
他后来反思说:「我低估了极端行情的风险。」
策略再好,没有风控,一次黑天鹅就能归零。
这就是为什么风控模块是量化系统中最重要的一环——它决定了你能活多久。
今天我们用Rust构建一个完整的风控模块,包括:
风控的本质是在收益和风险之间找平衡。
三个核心指标:
指标 | 含义 | 目标 |
|---|---|---|
最大回撤 | 从高点到低点的最大跌幅 | < 20% |
夏普比率 | 每承担一单位风险获得的收益 | > 1.0 |
卡玛比率 | 年化收益/最大回撤 | > 1.5 |
风控模块的职责:在策略信号的基础上,计算合适的仓位,在回撤达到阈值时主动降仓。
固定仓位:每次交易投入固定比例的资金。
问题:不考虑市场波动,风险敞口不均匀。
动态仓位:根据市场状态调整仓位大小。
波动大时减仓,波动小时加仓。
凯利公式是仓位管理的经典方法:
1
2
3
4
5
最优仓位 f* = (p × b - q) / b
p = 胜率
q = 败率 = 1 - p
b = 盈亏比
Rust实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn kelly_fraction(win_rate: f64, win_loss_ratio: f64) -> f64 {
let p = win_rate;
let q = 1.0 - p;
let b = win_loss_ratio;
let kelly = (p * b - q) / b;
// 限制在合理范围
kelly.clamp(0.0, 1.0)
}
// 示例:胜率55%,盈亏比1.5
// kelly = (0.55 × 1.5 - 0.45) / 1.5 = 0.25
// 建议仓位25%
实际使用时,通常用「半凯利」或「四分之一凯利」,避免过度杠杆。
根据市场波动率动态调整仓位:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn volatility_adjusted_position(
base_position: f64,
current_volatility: f64,
target_volatility: f64,
) -> f64 {
// 波动大时减仓,波动小时加仓
let volatility_ratio = target_volatility / current_volatility;
// 限制调整范围
let adjusted = base_position * volatility_ratio;
adjusted.clamp(base_position * 0.5, base_position * 2.0)
}
// 示例:
// 基础仓位20%,当前波动率25%,目标波动率15%
// 调整后仓位 = 20% × (15%/25%) = 12%
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
pub struct PositionSizer {
base_position: f64, // 基础仓位
max_position: f64, // 最大仓位
min_position: f64, // 最小仓位
volatility_target: f64, // 目标波动率
kelly_multiplier: f64, // 凯利系数(通常0.25或0.5)
}
impl PositionSizer {
pub fn new() -> Self {
Self {
base_position: 0.2,
max_position: 0.3,
min_position: 0.05,
volatility_target: 0.15,
kelly_multiplier: 0.25,
}
}
pub fn calculate_position(
&self,
win_rate: f64,
win_loss_ratio: f64,
current_volatility: f64,
) -> f64 {
// 1. 凯利公式计算基础仓位
let kelly = kelly_fraction(win_rate, win_loss_ratio);
let kelly_position = kelly * self.kelly_multiplier;
// 2. 波动率调整
let vol_adjusted = volatility_adjusted_position(
kelly_position,
current_volatility,
self.volatility_target,
);
// 3. 限制范围
vol_adjusted.clamp(self.min_position, self.max_position)
}
}
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
pub struct DrawdownMonitor {
peak_value: f64, // 历史最高净值
current_value: f64, // 当前净值
warning_level: f64, // 预警线
reduce_level: f64, // 减仓线
stop_level: f64, // 清仓线
}
impl DrawdownMonitor {
pub fn new(initial_value: f64) -> Self {
Self {
peak_value: initial_value,
current_value: initial_value,
warning_level: 0.05, // 回撤5%预警
reduce_level: 0.10, // 回撤10%减仓
stop_level: 0.20, // 回撤20%清仓
}
}
pub fn update(&mut self, new_value: f64) -> DrawdownAction {
self.current_value = new_value;
// 更新历史高点
if new_value > self.peak_value {
self.peak_value = new_value;
}
// 计算回撤
let drawdown = (self.peak_value - self.current_value) / self.peak_value;
// 返回动作建议
if drawdown >= self.stop_level {
DrawdownAction::StopTrading
} else if drawdown >= self.reduce_level {
DrawdownAction::ReducePosition(0.5) // 减仓50%
} else if drawdown >= self.warning_level {
DrawdownAction::Warning
} else {
DrawdownAction::Normal
}
}
pub fn current_drawdown(&self) -> f64 {
(self.peak_value - self.current_value) / self.peak_value
}
}
#[derive(Debug, Clone)]
pub enum DrawdownAction {
Normal, // 正常运行
Warning, // 预警
ReducePosition(f64), // 减仓比例
StopTrading, // 停止交易
}
更精细的风控应该分层:
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
pub struct LayeredRiskControl {
// 单笔交易风控
max_single_loss: f64, // 单笔最大亏损比例
max_single_position: f64, // 单只股票最大仓位
// 组合风控
max_sector_exposure: f64, // 单行业最大敞口
max_total_position: f64, // 总仓位上限
// 系统风控
daily_loss_limit: f64, // 日亏损上限
weekly_loss_limit: f64, // 周亏损上限
}
impl LayeredRiskControl {
pub fn check_trade(
&self,
trade: &Trade,
portfolio: &Portfolio,
) -> Result<(), RiskError> {
// 检查单笔仓位
if trade.position_ratio > self.max_single_position {
return Err(RiskError::PositionTooLarge);
}
// 检查单笔潜在亏损
let potential_loss = trade.position_ratio * trade.stop_loss_ratio;
if potential_loss > self.max_single_loss {
return Err(RiskError::PotentialLossTooLarge);
}
// 检查行业敞口
let sector_exposure = portfolio.get_sector_exposure(&trade.sector);
if sector_exposure + trade.position_ratio > self.max_sector_exposure {
return Err(RiskError::SectorExposureExceeded);
}
Ok(())
}
pub fn check_daily_loss(
&self,
daily_pnl: f64,
portfolio_value: f64,
) -> Result<(), RiskError> {
let daily_loss_ratio = -daily_pnl / portfolio_value;
if daily_loss_ratio > self.daily_loss_limit {
return Err(RiskError::DailyLossLimitExceeded);
}
Ok(())
}
}
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
pub struct CircuitBreaker {
trigger_threshold: f64, // 触发阈值
cooldown_period: Duration, // 冷却期
triggered_at: Option<DateTime<Utc>>,
is_active: bool,
}
impl CircuitBreaker {
pub fn new(threshold: f64, cooldown_minutes: u64) -> Self {
Self {
trigger_threshold: threshold,
cooldown_period: Duration::from_secs(cooldown_minutes * 60),
triggered_at: None,
is_active: false,
}
}
pub fn check(&mut self, drawdown: f64) -> CircuitBreakerStatus {
if self.is_active {
// 检查冷却期是否结束
if let Some(triggered) = self.triggered_at {
if Utc::now() - triggered > self.cooldown_period {
self.is_active = false;
return CircuitBreakerStatus::Resumed;
}
}
return CircuitBreakerStatus::Triggered;
}
if drawdown >= self.trigger_threshold {
self.is_active = true;
self.triggered_at = Some(Utc::now());
return CircuitBreakerStatus::Triggered;
}
CircuitBreakerStatus::Normal
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum CircuitBreakerStatus {
Normal,
Triggered,
Resumed,
}
根据VIX指数调整风险敞口:
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
pub struct VixRiskControl {
vix_thresholds: Vec<(f64, f64)>, // (VIX阈值, 最大仓位比例)
}
impl VixRiskControl {
pub fn new() -> Self {
Self {
vix_thresholds: vec![
(15.0, 1.0), // VIX < 15: 正常仓位
(20.0, 0.8), // VIX < 20: 仓位80%
(25.0, 0.5), // VIX < 25: 仓位50%
(30.0, 0.3), // VIX < 30: 仓位30%
(f64::MAX, 0.1), // VIX >= 30: 仓位10%
],
}
}
pub fn get_max_position(&self, vix: f64) -> f64 {
for (threshold, position) in &self.vix_thresholds {
if vix < *threshold {
return *position;
}
}
0.1 // 默认最小仓位
}
}
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
70
71
72
73
pub struct RiskManager {
position_sizer: PositionSizer,
drawdown_monitor: DrawdownMonitor,
layered_control: LayeredRiskControl,
circuit_breaker: CircuitBreaker,
vix_control: VixRiskControl,
}
impl RiskManager {
pub fn new(initial_capital: f64) -> Self {
Self {
position_sizer: PositionSizer::new(),
drawdown_monitor: DrawdownMonitor::new(initial_capital),
layered_control: LayeredRiskControl {
max_single_loss: 0.02,
max_single_position: 0.1,
max_sector_exposure: 0.3,
max_total_position: 0.8,
daily_loss_limit: 0.02,
weekly_loss_limit: 0.05,
},
circuit_breaker: CircuitBreaker::new(0.15, 60),
vix_control: VixRiskControl::new(),
}
}
pub fn evaluate_trade(
&mut self,
trade: &Trade,
portfolio: &Portfolio,
market_state: &MarketState,
) -> RiskDecision {
// 1. 检查熔断
let drawdown = self.drawdown_monitor.current_drawdown();
if self.circuit_breaker.check(drawdown) == CircuitBreakerStatus::Triggered {
return RiskDecision::Reject("熔断中,暂停交易".to_string());
}
// 2. 检查分层风控
if let Err(e) = self.layered_control.check_trade(trade, portfolio) {
return RiskDecision::Reject(format!("风控限制: {:?}", e));
}
// 3. 检查VIX风控
let vix_limit = self.vix_control.get_max_position(market_state.vix);
if trade.position_ratio > vix_limit {
return RiskDecision::Modify(TradeModification {
new_position: vix_limit,
reason: "VIX风控限制".to_string(),
});
}
// 4. 计算最终仓位
let final_position = self.position_sizer.calculate_position(
trade.historical_win_rate,
trade.historical_win_loss_ratio,
market_state.volatility,
);
RiskDecision::Approve(final_position)
}
pub fn update_portfolio_value(&mut self, value: f64) -> DrawdownAction {
self.drawdown_monitor.update(value)
}
}
#[derive(Debug)]
pub enum RiskDecision {
Approve(f64), // 批准,返回仓位比例
Reject(String), // 拒绝,返回原因
Modify(TradeModification), // 修改建议
}
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
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let initial_capital = 1_000_000.0;
let mut risk_manager = RiskManager::new(initial_capital);
let mut portfolio = Portfolio::new(initial_capital);
loop {
// 获取市场数据
let market_state = fetch_market_state().await?;
// 更新组合净值
let current_value = portfolio.calculate_value(&market_state);
let action = risk_manager.update_portfolio_value(current_value);
match action {
DrawdownAction::StopTrading => {
println!("回撤超过20%,清仓止损");
portfolio.clear_all_positions();
}
DrawdownAction::ReducePosition(ratio) => {
println!("回撤超过10%,减仓{}%", ratio * 100.0);
portfolio.reduce_all_positions(ratio);
}
DrawdownAction::Warning => {
println!("回撤超过5%,进入预警状态");
}
DrawdownAction::Normal => {}
}
// 获取交易信号
if let Some(trade) = generate_signal(&market_state) {
let decision = risk_manager.evaluate_trade(
&trade,
&portfolio,
&market_state,
);
match decision {
RiskDecision::Approve(position) => {
println!("交易批准,仓位: {:.1}%", position * 100.0);
portfolio.execute_trade(&trade, position);
}
RiskDecision::Reject(reason) => {
println!("交易拒绝: {}", reason);
}
RiskDecision::Modify(modification) => {
println!("仓位调整: {:.1}%, 原因: {}",
modification.new_position * 100.0,
modification.reason
);
}
}
}
tokio::time::sleep(Duration::from_secs(60)).await;
}
}
指标 | 无风控 | 有风控 | 改善 |
|---|---|---|---|
年化收益 | 28.5% | 22.3% | -6.2% |
最大回撤 | -35.2% | -12.8% | +22.4% |
夏普比率 | 0.82 | 1.45 | +77% |
卡玛比率 | 0.81 | 1.74 | +115% |
结论:风控虽然牺牲了一点收益,但大幅降低了风险,提升了风险调整后收益。
写这篇文章,我想传达一个核心理念:
风控不是成本,而是投资的生命线。
在量化交易中,赚钱的机会永远有,但亏完本金就彻底出局了。
一个完善的系统,应该:
活下去,才有机会赢。
接下来我们将开启新系列:数据驱动策略。
下一篇,我们将聊聊Level2逐笔数据的Rust处理:
敬请期待。
(全文完)