因子挖掘+回测+RL交易:量化金融完整系统
一、引言
量化交易的核心公式:信号(Alpha因子) × 风险控制 × 执行效率 = 收益。传统多因子模型依赖人工特征,而机器学习(GBDT/RL)正在重塑这一领域。本文将实现一个完整的量化系统:因子挖掘→回测验证→RL交易策略。
二、Alpha因子挖掘
python
import pandas as pd
import numpy as np
from scipy import stats
class AlphaFactory:
"""Alpha因子工厂"""
def __init__(self, df):
self.df = df # OHLCV数据
self.factors = pd.DataFrame(index=df.index)
def generate_all(self):
# === 动量因子 ===
self.factors['momentum_5d'] = self.df['close'].pct_change(5)
self.factors['momentum_20d'] = self.df['close'].pct_change(20)
self.factors['rsi_14'] = self._rsi(14)
# === 均值回归因子 ===
ma20 = self.df['close'].rolling(20).mean()
self.factors['mean_reversion'] = (ma20 - self.df['close']) / self.df['close'].rolling(20).std()
# === 波动率因子 ===
self.factors['volatility_20d'] = self.df['close'].pct_change().rolling(20).std()
self.factors['volume_ratio'] = self.df['volume'] / self.df['volume'].rolling(20).mean()
# === 技术指标 ===
self.factors['macd'] = self._macd()
self.factors['bb_width'] = self._bollinger_band_width()
# === 资金流因子 ===
self.factors['money_flow'] = ((self.df['close'] - self.df['low']) -
(self.df['high'] - self.df['close'])) / \
(self.df['high'] - self.df['low']) * self.df['volume']
# === 截面因子(横截面排名) ===
self.factors['cross_sectional_rank'] = self.df['close'].pct_change(5).rank(pct=True)
return self.factors
def _rsi(self, period):
delta = self.df['close'].diff()
gain = delta.clip(lower=0).rolling(period).mean()
loss = (-delta.clip(upper=0)).rolling(period).mean()
rs = gain / loss
return 100 - (100 / (1 + rs))
def _macd(self):
ema12 = self.df['close'].ewm(span=12).mean()
ema26 = self.df['close'].ewm(span=26).mean()
macd = ema12 - ema26
signal = macd.ewm(span=9).mean()
return macd - signal # MACD柱
def _bollinger_band_width(self):
ma20 = self.df['close'].rolling(20).mean()
std20 = self.df['close'].rolling(20).std()
return 2 * std20 / ma20 # 带宽
# 因子IC分析(信息系数)
def evaluate_factors(factors, forward_returns, horizon=5):
"""计算每个因子对未来收益的预测能力"""
forward_ret = forward_returns.shift(-horizon)
ic_summary = {}
for col in factors.columns:
valid = factors[col].notna() & forward_ret.notna()
ic, p_value = stats.spearmanr(factors[col][valid], forward_ret[valid])
ic_summary[col] = {"IC": ic, "p_value": p_value, "valid_samples": valid.sum()}
return pd.DataFrame(ic_summary).T.sort_values('IC', ascending=False)
三、Backtrader回测
python
import backtrader as bt
class MLStrategy(bt.Strategy):
"""机器学习驱动的交易策略"""
params = (
('risk_per_trade', 0.02), # 每笔风险2%
('max_positions', 5), # 最多5个持仓
)
def __init__(self):
# ML模型(预训练的LightGBM)
import joblib
self.model = joblib.load('lgbm_model.pkl')
self.orders = {}
def next(self):
# 1. 计算当前因子
features = self._compute_features()
# 2. ML预测未来收益
predicted_return = self.model.predict(features.reshape(1, -1))[0]
# 3. 交易信号
current_positions = len(self.positions)
if predicted_return > 0.005 and current_positions < self.p.max_positions:
# 买入
size = self.broker.getcash() * self.p.risk_per_trade / self.data.close[0]
self.buy(size=size)
print(f"BUY @ {self.data.close[0]:.2f}, Pred: {predicted_return:.4f}")
elif predicted_return < -0.003:
# 卖出
self.close()
print(f"SELL @ {self.data.close[0]:.2f}")
def _compute_features(self):
return np.array([
self.data.close[0] / self.data.close[-5] - 1, # 5日动量
self.data.close[0] / self.data.close[-20] - 1, # 20日动量
self.data.volume[0] / self.data.volume.get(size=20).mean(), # 量比
# ... 更多因子
])
# 运行回测
cerebro = bt.Cerebro()
cerebro.addstrategy(MLStrategy)
data = bt.feeds.PandasData(dataname=df)
cerebro.adddata(data)
cerebro.broker.setcash(100000.0)
cerebro.broker.setcommission(commission=0.001) # 0.1%手续费
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe', riskfreerate=0.02)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
results = cerebro.run()
strat = results[0]
print(f"Sharpe Ratio: {strat.analyzers.sharpe.get_analysis()['sharperatio']:.3f}")
print(f"Max Drawdown: {strat.analyzers.drawdown.get_analysis()['max']['drawdown']:.2f}%")
print(f"Annual Return: {strat.analyzers.returns.get_analysis()['rnorm100']:.2f}%")
cerebro.plot()
四、PPO强化学习交易
python
import gymnasium as gym
from stable_baselines3 import PPO
import numpy as np
class TradingEnv(gym.Env):
"""交易环境"""
def __init__(self, df, initial_balance=100000, window=60):
super().__init__()
self.df = df
self.window = window
# 观测: [price, volume, 技术指标...] * window
self.observation_space = gym.spaces.Box(-np.inf, np.inf, (window * 10,))
# 动作: [-1(满仓卖), -0.5, 0(持有), 0.5, 1(满仓买)]
self.action_space = gym.spaces.Box(-1, 1, (1,))
self.reset()
def reset(self):
self.current_step = self.window
self.balance = self.initial_balance
self.shares = 0
self.portfolio_history = []
return self._get_obs(), {}
def step(self, action):
action = action[0] # 解包
# 执行交易
current_price = self.df.iloc[self.current_step]['close']
trade_size = abs(action) * self.balance * 0.1 # 最多用10%余额
if action > 0:
# 买入
shares_to_buy = trade_size / current_price
cost = shares_to_buy * current_price * (1 + 0.001) # 手续费
if cost <= self.balance:
self.shares += shares_to_buy
self.balance -= cost
elif action < 0:
# 卖出
shares_to_sell = min(self.shares, trade_size / current_price)
revenue = shares_to_sell * current_price * (1 - 0.001)
self.shares -= shares_to_sell
self.balance += revenue
self.current_step += 1
# 新组合价值
new_value = self.balance + self.shares * self.df.iloc[self.current_step]['close']
# 奖励 = 收益 - 波动惩罚
if len(self.portfolio_history) > 0:
returns = new_value / self.portfolio_history[-1] - 1
volatility = np.std([new_value / v - 1 for v in self.portfolio_history[-20:]]) if len(self.portfolio_history) >= 20 else 0
reward = returns - 0.5 * volatility # Sharpe-like reward
else:
reward = 0
self.portfolio_history.append(new_value)
done = self.current_step >= len(self.df) - 1
return self._get_obs(), reward, done, False, {"portfolio_value": new_value}
def _get_obs(self):
window_data = self.df.iloc[self.current_step - self.window:self.current_step]
obs = np.concatenate([
(window_data['close'] / window_data['close'].iloc[-1] - 1).values, # 归一化价格
(window_data['volume'] / window_data['volume'].mean() - 1).values, # 归一化量
np.array([self.balance / self.initial_balance]), # 余额占比
np.array([self.shares * window_data['close'].iloc[-1] / self.initial_balance]), # 持仓占比
])
# padding到固定大小
padded = np.zeros(self.observation_space.shape[0])
padded[:len(obs)] = obs
return padded
# 训练
env = TradingEnv(train_df)
model = PPO("MlpPolicy", env, n_steps=2048, learning_rate=3e-4, verbose=1)
model.learn(total_timesteps=200_000)
model.save("trading_ppo")
# 回测
env = TradingEnv(test_df)
obs, _ = env.reset()
while True:
action, _ = model.predict(obs)
obs, reward, done, _, info = env.step(action)
if done:
final_value = info['portfolio_value']
print(f"Final: {final_value:.0f} (Return: {(final_value/100000-1)*100:.1f}%)")
break
五、风险管理
python
class RiskManager:
def __init__(self, max_drawdown=0.15, var_limit=0.02, position_limit=0.2):
self.max_dd = max_drawdown
self.var_limit = var_limit
self.position_limit = position_limit
def check_order(self, order, portfolio):
checks = []
# 1. 最大回撤检查
if portfolio.drawdown > self.max_dd:
checks.append(("BLOCKED", f"Drawdown {portfolio.drawdown:.1%} > {self.max_dd:.1%}"))
return 0
# 2. VaR检查
var = self._calculate_var(portfolio)
if var > self.var_limit:
order_size = order.size * (self.var_limit / var)
checks.append(("REDUCED", f"VaR {var:.2%} → size {order_size:.1f}"))
# 3. 仓位限制
position_ratio = order.size * order.price / portfolio.total_value
if position_ratio > self.position_limit:
order_size = order.size * (self.position_limit / position_ratio)
checks.append(("REDUCED", f"Position limit → {order_size:.1f}"))
return order_size
def _calculate_var(self, portfolio, confidence=0.95):
returns = portfolio.daily_returns[-252:]
var = np.percentile(returns, (1 - confidence) * 100)
return abs(var)
六、总结
量化系统三大组件:
- 因子挖掘 → 动量/均值回归/波动率 → IC分析筛选
- 回测验证 → Backtrader → Sharpe/Drawdown评估
- RL交易 → PPO + 定制环境 → 自适应策略
关键警示:回测≠实盘!过拟合、滑点、流动性是量化最大的敌人。