多策略组合系统的架构演进:从单策略到全天候
阶段一:单策略裸奔
最早的量化系统就是一根筋:一个策略,一个进程,定时拉数据、算信号、发单。
python
import time
import pandas as pd
from datetime import datetime
class SingleStrategyRunner:
def __init__(self, strategy, broker):
self.strategy = strategy
self.broker = broker
def run(self):
while True:
now = datetime.now()
# 收盘后计算信号
if now.hour == 15 and now.minute == 5:
data = self.broker.get_history(days=120)
signal = self.strategy.generate_signal(data)
if signal == 1:
self.broker.buy(target=0.95)
elif signal == -1:
self.broker.sell_all()
time.sleep(30)
问题清单:
- 策略逻辑与执行逻辑耦合,改一行代码要停整个进程
- 没有状态管理,进程挂了持仓状态全靠券商接口反查
- 单点故障,一次异常退出就彻底躺平
- 无法回测与实盘共用同一套代码,两边逻辑经常漂移
这个阶段的核心矛盾是耦合。策略、执行、调度、状态全混在一起,任何一处改动都可能引发连锁反应。
阶段二:多策略并行------解耦与隔离
当手里攒了三五个策略,第一反应是"多开几个进程"。但这只是把单点故障变成了多点故障。
真正的演进方向是事件驱动 + 策略隔离。核心思路:把每个策略封装成独立单元,通过统一的事件总线通信,状态由中央账本管理。
python
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Dict
import queue
import threading
@dataclass
class SignalEvent:
strategy_id: str
symbol: str
direction: int # 1 买, -1 卖, 0 平
weight: float
timestamp: float
class Strategy(ABC):
def __init__(self, strategy_id: str):
self.strategy_id = strategy_id
@abstractmethod
def on_data(self, data: pd.DataFrame) -> list[SignalEvent]:
...
class StrategyEngine:
"""每个策略跑在独立线程,产出信号事件"""
def __init__(self, bus: queue.Queue):
self.bus = bus
self.strategies: Dict[str, Strategy] = {}
self._stop = threading.Event()
def register(self, strategy: Strategy):
self.strategies[strategy.strategy_id] = strategy
def _run_one(self, strategy: Strategy):
while not self._stop.is_set():
data = self._fetch_data(strategy.strategy_id)
if data is None or data.empty:
time.sleep(1)
continue
for event in strategy.on_data(data):
self.bus.put(event)
time.sleep(5)
def start(self):
for s in self.strategies.values():
threading.Thread(target=self._run_one, args=(s,), daemon=True).start()
def _fetch_data(self, sid: str):
# 实际项目替换为行情源
return None
中央账本负责汇总所有策略的目标仓位,做净额处理:
python
class PortfolioLedger:
"""汇总多策略信号,输出净目标仓位"""
def __init__(self):
self.target: Dict[str, float] = {}
self.lock = threading.Lock()
def apply(self, event: SignalEvent):
with self.lock:
key = event.symbol
delta = event.direction * event.weight
self.target[key] = self.target.get(key, 0.0) + delta
# 单标的敞口上限
self.target[key] = max(-1.0, min(1.0, self.target[key]))
def snapshot(self) -> Dict[str, float]:
with self.lock:
return dict(self.target)
这一阶段解决的问题:
- 策略之间互不干扰,一个策略抛异常不影响其他策略
- 信号与执行分离,执行层只认净目标仓位
- 回测和实盘共用
Strategy接口,逻辑不再漂移
新暴露的问题:
- 多策略同时满仓时,总仓位可能超过 100%,杠杆失控
- 策略之间相关性高,回撤叠加,组合并没有真正分散
- 没有资金分配机制,谁先发信号谁占坑
阶段三:资金分配与风险预算
多策略并行不等于多策略组合。组合的关键在于资金如何在策略之间分配。
最朴素的做法是等权,但等权忽略了策略的波动率差异。更合理的是风险平价(Risk Parity):让每个策略对组合的风险贡献相等。
python
import numpy as np
def risk_parity_weights(returns: pd.DataFrame, lookback: int = 60) -> pd.Series:
"""
returns: 每列一个策略的日收益率
返回: 各策略权重,使风险贡献相等
"""
recent = returns.tail(lookback).dropna()
cov = recent.cov().values
n = cov.shape[0]
# 迭代求解风险平价权重
w = np.ones(n) / n
for _ in range(500):
port_var = w @ cov @ w
marginal = cov @ w
rc = w * marginal / np.sqrt(port_var) # 风险贡献
target = rc.mean()
w = w * (target / rc)
w = w / w.sum()
return pd.Series(w, index=returns.columns)
def apply_vol_target(weights: pd.Series, returns: pd.DataFrame,
target_vol: float = 0.15) -> pd.Series:
"""在风险平价基础上叠加波动率目标"""
port_ret = (returns.tail(60) * weights).sum(axis=1)
realized = port_ret.std() * np.sqrt(252)
scale = target_vol / realized if realized > 0 else 1.0
return weights * min(scale, 2.0) # 杠杆上限 2 倍
在 PortfolioLedger 里引入权重层:
python
class WeightedLedger(PortfolioLedger):
def __init__(self, weights: Dict[str, float]):
super().__init__()
self.weights = weights # strategy_id -> 资金权重
def apply(self, event: SignalEvent):
w = self.weights.get(event.strategy_id, 0.0)
scaled = SignalEvent(
strategy_id=event.strategy_id,
symbol=event.symbol,
direction=event.direction,
weight=event.weight * w,
timestamp=event.timestamp,
)
super().apply(scaled)
架构对比:
| 维度 | 阶段一 | 阶段二 | 阶段三 |
|---|---|---|---|
| 策略隔离 | 无 | 线程级 | 线程级 + 权重隔离 |
| 资金管理 | 全仓 | 净额封顶 | 风险平价 + 波动目标 |
| 故障影响 | 全局 | 单策略 | 单策略 |
| 回撤特征 | 单策略回撤 | 相关性叠加 | 风险分散 |
阶段四:全天候------状态机与动态切换
到了这一步,系统能跑多策略、能分配资金,但还有一个致命问题:策略有生命周期。
趋势策略在震荡市里连续亏损,套利策略在流动性枯竭时失效。如果一直给所有策略分配资金,等于在无效环境里持续送钱。
全天候的核心不是"永远满仓",而是根据市场状态动态调整策略权重。
python
from enum import Enum
class Regime(Enum):
TREND_UP = "trend_up"
TREND_DOWN = "trend_down"
RANGE = "range"
HIGH_VOL = "high_vol"
class RegimeDetector:
"""基于波动率和趋势强度识别市场状态"""
def __init__(self, lookback: int = 20):
self.lookback = lookback
def detect(self, prices: pd.Series) -> Regime:
ret = prices.pct_change().tail(self.lookback)
vol = ret.std() * np.sqrt(252)
# 趋势强度:ADX 简化版
ma_fast = prices.tail(5).mean()
ma_slow = prices.tail(20).mean()
trend_strength = abs(ma_fast - ma_slow) / ma_slow
if vol > 0.35:
return Regime.HIGH_VOL
if trend_strength > 0.03:
return Regime.TREND_UP if ma_fast > ma_slow else Regime.TREND_DOWN
return Regime.RANGE
# 策略在不同状态下的权重系数
REGIME_MULTIPLIER = {
Regime.TREND_UP: {"trend": 1.5, "mean_rev": 0.3, "arb": 1.0},
Regime.TREND_DOWN: {"trend": 1.5, "mean_rev": 0.3, "arb": 1.0},
Regime.RANGE: {"trend": 0.3, "mean_rev": 1.5, "arb": 1.0},
Regime.HIGH_VOL: {"trend": 0.5, "mean_rev": 0.5, "arb": 0.8},
}
把状态检测接入权重层,形成闭环:
python
class AllWeatherLedger(WeightedLedger):
def __init__(self, base_weights, strategy_types, detector):
super().__init__(base_weights)
self.strategy_types = strategy_types # strategy_id -> "trend"/"mean_rev"/"arb"
self.detector = detector
self.current_regime = Regime.RANGE
def update_regime(self, prices: pd.Series):
new_regime = self.detector.detect(prices)
if new_regime != self.current_regime:
self.current_regime = new_regime
self._rebalance_weights()
def _rebalance_weights(self):
mult = REGIME_MULTIPLIER[self.current_regime]
raw = {
sid: self.weights[sid] * mult.get(self.strategy_types[sid], 1.0)
for sid in self.weights
}
total = sum(raw.values())
self.weights = {k: v / total for k, v in raw.items()}
全天候架构的四个层次:
┌─────────────────────────────────────┐
│ 数据层:行情 + 状态检测 │
├─────────────────────────────────────┤
│ 策略层:独立线程,事件驱动输出信号 │
├─────────────────────────────────────┤
│ 组合层:风险平价 + 状态权重调整 │
├─────────────────────────────────────┤
│ 执行层:净额下单 + 状态持久化 │
└─────────────────────────────────────┘
工程上的几个坑
1. 状态持久化。 进程重启后必须恢复持仓、权重、当前市场状态。用 SQLite 或 Redis 都行,关键是要有 snapshot() 和 restore() 对称接口。
2. 时钟对齐。 多策略跑在不同线程,信号时间戳必须统一到同一时钟源,否则净额计算会出现时序错乱。
3. 回测与实盘的一致性。 状态检测、权重计算、净额处理这三层必须在回测里完整复现。很多组合回测好看实盘拉胯,就是因为回测只跑了策略层,没跑组合层。
4. 权重调整的频率。 状态检测太频繁会导致权重抖动,交易成本爆炸。实践中用日频检测、周频调权,配合最小变动阈值。
演进路线总结
从单策略到全天候,本质是不断把隐式假设显式化的过程:
- 阶段一假设"只有一个策略",阶段二把它变成"多策略隔离"
- 阶段二假设"资金平均分配",阶段三把它变成"风险预算"
- 阶段三假设"策略永远有效",阶段四把它变成"状态自适应"
每一步演进都不是为了架构好看,而是因为上一层的假设在实盘中被打脸了。架构演进的驱动力永远来自真实的亏损和故障,而不是设计模式。
代码层面,核心是保持 Strategy 接口稳定,让组合层和执行层可以独立演化。策略作者只关心 on_data 返回什么信号,不需要知道资金怎么分配、状态怎么切换。这种分层是系统能持续演进的前提。
更多内容请关注本站。