实战项目:多因子选股系统

# 实战项目:多因子选股系统

# 项目概述

构建一个完整的多因子选股系统,包含因子挖掘、测试、组合、回测的全流程。

# 学习目标

通过本项目,你将掌握:

  • 多因子模型的理论和实践
  • 因子有效性测试方法
  • 因子组合与权重优化
  • 完整的选股回测系统
  • 产品化思维

# 系统架构

数据层
├── 行情数据
├── 财务数据
├── 另类数据
└── 数据清洗

因子层
├── 技术因子
├── 基本面因子
├── 情绪因子
└── 因子标准化

测试层
├── 单因子测试
├── IC/IR分析
├── 分层回测
└── 因子相关性

组合层
├── 因子打分
├── 权重优化
├── 风险控制
└── 换手率控制

执行层
├── 选股输出
├── 仓位管理
├── 交易执行
└── 性能监控
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

# 核心理论

# 什么是多因子模型

多因子模型基于这样的假设:股票的超额收益可以由多个因子解释。

股票收益 = α + β1×因子1 + β2×因子2 + ... + βn×因子n + ε

其中:
- α:截距项
- βi:因子暴露度
- 因子i:影响股票收益的变量
- ε:残差项
1
2
3
4
5
6
7

# 经典因子

# 1. Fama-French三因子

收益 = α + β1×市场因子 + β2×规模因子 + β3×价值因子

- 市场因子(MKT):市场超额收益
- 规模因子(SMB):小盘股 - 大盘股
- 价值因子(HML):高账面市值比 - 低账面市值比
1
2
3
4
5

# 2. Fama-French五因子

在三因子基础上增加:
- 盈利因子(RMW):高盈利 - 低盈利
- 投资因子(CMA):保守投资 - 激进投资
1
2
3

# 完整代码实现

"""
多因子选股系统
"""

import pandas as pd
import numpy as np
import yfinance as yf
from datetime import datetime, timedelta
from scipy import stats
import matplotlib.pyplot as plt
import seaborn as sns

class MultiFactorSystem:
    """多因子选股系统"""

    def __init__(self, stock_universe):
        """
        初始化
        :param stock_universe: 股票池列表
        """
        self.stock_universe = stock_universe
        self.data = {}
        self.factors = pd.DataFrame()
        self.factor_returns = pd.DataFrame()
        self.selected_stocks = []

    def download_data(self, start_date, end_date):
        """下载数据"""
        print("下载股票数据...")

        for symbol in self.stock_universe:
            try:
                df = yf.download(symbol, start=start_date, end=end_date, progress=False)
                if not df.empty:
                    self.data[symbol] = df
                    print(f"✓ {symbol}")
            except Exception as e:
                print(f"✗ {symbol}: {e}")

        print(f"\n成功下载 {len(self.data)} 只股票\n")
        return self

    def calculate_factors(self):
        """计算因子"""
        print("计算因子...")

        all_factors = []

        for symbol, df in self.data.items():
            factors = pd.DataFrame(index=df.index)
            factors['symbol'] = symbol

            # 1. 动量因子
            factors['momentum_20'] = df['Close'].pct_change(20)  # 20日动量
            factors['momentum_60'] = df['Close'].pct_change(60)  # 60日动量

            # 2. 反转因子
            factors['reversal_5'] = -df['Close'].pct_change(5)  # 5日反转

            # 3. 波动率因子
            returns = df['Close'].pct_change()
            factors['volatility_20'] = returns.rolling(20).std()
            factors['volatility_60'] = returns.rolling(60).std()

            # 4. 成交量因子
            factors['volume_ratio'] = df['Volume'] / df['Volume'].rolling(20).mean()

            # 5. 价格因子
            factors['price_ma20_ratio'] = df['Close'] / df['Close'].rolling(20).mean() - 1
            factors['price_ma60_ratio'] = df['Close'] / df['Close'].rolling(60).mean() - 1

            # 6. 趋势因子
            factors['trend'] = (df['Close'].rolling(20).mean() /
                               df['Close'].rolling(60).mean() - 1)

            # 7. RSI
            factors['rsi'] = self._calculate_rsi(df['Close'])

            # 8. 布林带位置
            bb_middle = df['Close'].rolling(20).mean()
            bb_std = df['Close'].rolling(20).std()
            bb_upper = bb_middle + 2 * bb_std
            bb_lower = bb_middle - 2 * bb_std
            factors['bb_position'] = (df['Close'] - bb_lower) / (bb_upper - bb_lower)

            # 添加价格和未来收益(用于测试)
            factors['price'] = df['Close']
            factors['future_return_5'] = df['Close'].pct_change(5).shift(-5)
            factors['future_return_20'] = df['Close'].pct_change(20).shift(-20)

            all_factors.append(factors)

        # 合并所有股票的因子
        self.factors = pd.concat(all_factors)
        self.factors = self.factors.dropna()

        print(f"因子计算完成!共 {len(self.factors.columns)-3} 个因子\n")
        return self

    def _calculate_rsi(self, prices, period=14):
        """计算RSI"""
        delta = prices.diff()
        gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
        rs = gain / loss
        rsi = 100 - (100 / (1 + rs))
        return rsi

    def test_single_factor(self, factor_name):
        """单因子测试"""
        print(f"\n测试因子: {factor_name}")
        print("=" * 60)

        # 计算IC(信息系数)
        ic_5d = self.factors.groupby(self.factors.index).apply(
            lambda x: x[factor_name].corr(x['future_return_5'])
        )
        ic_20d = self.factors.groupby(self.factors.index).apply(
            lambda x: x[factor_name].corr(x['future_return_20'])
        )

        # IC统计
        ic_mean_5d = ic_5d.mean()
        ic_std_5d = ic_5d.std()
        ic_ir_5d = ic_mean_5d / ic_std_5d if ic_std_5d != 0 else 0

        ic_mean_20d = ic_20d.mean()
        ic_std_20d = ic_20d.std()
        ic_ir_20d = ic_mean_20d / ic_std_20d if ic_std_20d != 0 else 0

        print(f"\n【IC分析 - 5日未来收益】")
        print(f"  IC均值: {ic_mean_5d:.4f}")
        print(f"  IC标准差: {ic_std_5d:.4f}")
        print(f"  ICIR: {ic_ir_5d:.4f}")
        print(f"  IC胜率: {(ic_5d > 0).sum() / len(ic_5d) * 100:.2f}%")

        print(f"\n【IC分析 - 20日未来收益】")
        print(f"  IC均值: {ic_mean_20d:.4f}")
        print(f"  IC标准差: {ic_std_20d:.4f}")
        print(f"  ICIR: {ic_ir_20d:.4f}")
        print(f"  IC胜率: {(ic_20d > 0).sum() / len(ic_20d) * 100:.2f}%")

        # 分层回测
        self._layered_backtest(factor_name)

        return {
            'factor': factor_name,
            'ic_mean_5d': ic_mean_5d,
            'icir_5d': ic_ir_5d,
            'ic_mean_20d': ic_mean_20d,
            'icir_20d': ic_ir_20d
        }

    def _layered_backtest(self, factor_name, n_layers=5):
        """分层回测"""
        print(f"\n【分层回测 - {n_layers}层】")

        # 按因子值分层
        self.factors['layer'] = self.factors.groupby(self.factors.index)[factor_name].apply(
            lambda x: pd.qcut(x, n_layers, labels=False, duplicates='drop')
        )

        # 计算每层的平均收益
        layer_returns = self.factors.groupby('layer')['future_return_20'].mean()

        print("\n层级    平均收益")
        print("-" * 30)
        for layer, ret in layer_returns.items():
            print(f"第{layer+1}层    {ret*100:>7.2f}%")

        # 多空收益
        if len(layer_returns) >= 2:
            long_short = layer_returns.iloc[-1] - layer_returns.iloc[0]
            print(f"\n多空收益: {long_short*100:.2f}%")

    def test_all_factors(self):
        """测试所有因子"""
        print("\n" + "="*60)
        print("开始测试所有因子")
        print("="*60)

        factor_cols = [col for col in self.factors.columns
                      if col not in ['symbol', 'price', 'future_return_5',
                                    'future_return_20', 'layer']]

        results = []
        for factor in factor_cols:
            result = self.test_single_factor(factor)
            results.append(result)

        # 汇总结果
        results_df = pd.DataFrame(results)
        results_df = results_df.sort_values('icir_20d', ascending=False)

        print("\n" + "="*60)
        print("因子测试汇总(按ICIR_20d排序)")
        print("="*60)
        print(results_df.to_string(index=False))

        return results_df

    def normalize_factors(self):
        """因子标准化"""
        print("\n标准化因子...")

        factor_cols = [col for col in self.factors.columns
                      if col not in ['symbol', 'price', 'future_return_5',
                                    'future_return_20', 'layer']]

        for col in factor_cols:
            # Z-score标准化
            self.factors[f'{col}_norm'] = self.factors.groupby(self.factors.index)[col].transform(
                lambda x: (x - x.mean()) / x.std()
            )

        print("标准化完成!\n")
        return self

    def construct_composite_factor(self, weights=None):
        """构建复合因子"""
        print("\n构建复合因子...")

        normalized_cols = [col for col in self.factors.columns if col.endswith('_norm')]

        if weights is None:
            # 等权重
            weights = {col: 1.0 / len(normalized_cols) for col in normalized_cols}
        else:
            # 归一化权重
            total = sum(weights.values())
            weights = {k: v/total for k, v in weights.items()}

        print("因子权重:")
        for factor, weight in weights.items():
            print(f"  {factor}: {weight:.4f}")

        # 计算复合得分
        self.factors['composite_score'] = sum(
            self.factors[factor] * weight
            for factor, weight in weights.items()
        )

        print("\n复合因子构建完成!\n")
        return self

    def select_stocks(self, date, top_n=20):
        """选股"""
        # 获取该日期的数据
        date_data = self.factors[self.factors.index == date].copy()

        if date_data.empty:
            return []

        # 按复合得分排序
        date_data = date_data.sort_values('composite_score', ascending=False)

        # 选择TopN
        selected = date_data.head(top_n)['symbol'].tolist()

        return selected

    def backtest_portfolio(self, start_date, rebalance_freq='M', top_n=20):
        """组合回测"""
        print("\n开始组合回测...")
        print(f"调仓频率: {rebalance_freq}")
        print(f"持仓数量: {top_n}\n")

        # 生成调仓日期
        date_range = pd.date_range(start_date, self.factors.index.max(), freq=rebalance_freq)

        portfolio_returns = []
        holdings = []

        for i, date in enumerate(date_range[:-1]):
            # 选股
            selected_stocks = self.select_stocks(date, top_n)

            if not selected_stocks:
                continue

            # 计算下一期收益
            next_date = date_range[i + 1]

            period_returns = []
            for symbol in selected_stocks:
                stock_data = self.factors[
                    (self.factors['symbol'] == symbol) &
                    (self.factors.index >= date) &
                    (self.factors.index <= next_date)
                ]

                if len(stock_data) >= 2:
                    ret = (stock_data['price'].iloc[-1] /
                          stock_data['price'].iloc[0] - 1)
                    period_returns.append(ret)

            if period_returns:
                # 等权重组合收益
                portfolio_ret = np.mean(period_returns)
                portfolio_returns.append({
                    'date': date,
                    'return': portfolio_ret,
                    'holdings': selected_stocks
                })

        # 转为DataFrame
        results = pd.DataFrame(portfolio_returns)

        if results.empty:
            print("回测失败:没有有效数据")
            return None

        # 计算累计收益
        results['cumulative_return'] = (1 + results['return']).cumprod() - 1

        # 打印结果
        total_return = results['cumulative_return'].iloc[-1]
        annual_return = (1 + total_return) ** (252 / len(results)) - 1
        sharpe = results['return'].mean() / results['return'].std() * np.sqrt(12)

        print("="*60)
        print("回测结果")
        print("="*60)
        print(f"总收益率: {total_return*100:.2f}%")
        print(f"年化收益率: {annual_return*100:.2f}%")
        print(f"夏普比率: {sharpe:.2f}")
        print(f"调仓次数: {len(results)}")
        print("="*60 + "\n")

        # 绘制净值曲线
        plt.figure(figsize=(12, 6))
        plt.plot(results['date'], (1 + results['cumulative_return']) * 100,
                linewidth=2, label='策略净值')
        plt.axhline(y=100, color='gray', linestyle='--', alpha=0.5)
        plt.title('多因子选股策略净值曲线', fontsize=14)
        plt.xlabel('日期')
        plt.ylabel('净值')
        plt.legend()
        plt.grid(True, alpha=0.3)
        plt.tight_layout()
        plt.savefig('multi_factor_portfolio.png', dpi=300)
        print("净值曲线已保存: multi_factor_portfolio.png")
        plt.show()

        return results

def main():
    """主函数"""
    # 股票池(示例:科技股)
    stock_universe = [
        'AAPL', 'GOOGL', 'MSFT', 'AMZN', 'TSLA',
        'NVDA', 'META', 'NFLX', 'AMD', 'INTC',
        'ORCL', 'CRM', 'ADBE', 'CSCO', 'AVGO'
    ]

    # 创建系统
    system = MultiFactorSystem(stock_universe)

    # 运行完整流程
    system.download_data('2020-01-01', '2023-12-31')
    system.calculate_factors()

    # 测试所有因子
    factor_test_results = system.test_all_factors()

    # 标准化因子
    system.normalize_factors()

    # 构建复合因子(可以根据测试结果调整权重)
    # 选择ICIR最高的几个因子
    top_factors = factor_test_results.head(5)['factor'].tolist()
    weights = {f'{factor}_norm': 1.0 for factor in top_factors}

    system.construct_composite_factor(weights)

    # 回测
    results = system.backtest_portfolio(
        start_date='2021-01-01',
        rebalance_freq='M',  # 月度调仓
        top_n=10  # 持仓10只
    )

    print("\n系统运行完成!")

if __name__ == '__main__':
    main()
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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386

# 关键要点

# 1. 因子有效性判断

IC (Information Coefficient)

  • IC > 0.03:弱有效
  • IC > 0.05:中等有效
  • IC > 0.08:强有效

ICIR (IC Information Ratio)

  • ICIR > 0.5:较好
  • ICIR > 1.0:优秀
  • ICIR > 2.0:极好

# 2. 因子组合方法

等权重法

composite_score = (factor1 + factor2 + ... + factorN) / N
1

IC加权法

weight_i = IC_i / sum(IC_j)
composite_score = Σ(weight_i * factor_i)
1
2

最优化法

# 最大化组合IC
max IC(composite) = Σ(w_i * IC_i)
s.t. Σw_i = 1, w_i >= 0
1
2
3

# 3. 风险控制

行业中性

# 每个行业选相同数量的股票
for industry in industries:
    select_top_n_from_industry(industry, n=2)
1
2
3

市值中性

# 大中小盘均衡配置
select_stocks(large_cap, n=5)
select_stocks(mid_cap, n=5)
select_stocks(small_cap, n=5)
1
2
3
4

# 常见问题

# Q1: 因子为什么会失效?

原因:

  1. 市场环境变化
  2. 策略拥挤
  3. 过度拟合
  4. 数据问题

解决:

  • 定期重新测试
  • 动态调整权重
  • 多因子分散
  • 及时止损

# Q2: 如何发现新因子?

方法:

  1. 学术论文
  2. 行业研报
  3. 数据挖掘
  4. 创新思维

注意:

  • 避免数据挖掘陷阱
  • 样本外验证
  • 经济学解释

# Q3: 实盘如何应用?

流程:

  1. 每月/周调仓日
  2. 计算所有股票因子
  3. 按复合得分排序
  4. 选择Top N
  5. 等权重买入
  6. 下期调仓重复

# 进阶方向

  1. 机器学习增强

    • 用ML学习因子权重
    • 非线性因子组合
  2. 高频因子

    • 日内因子
    • 微观结构因子
  3. 另类数据

    • 舆情数据
    • 卫星图像
    • 社交媒体
  4. 组合优化

    • 风险平价
    • Black-Litterman模型
    • 马科维茨优化

# 进阶资源

# 推荐书籍

  1. 《主动投资组合管理》 - Richard Grinold
  2. 《量化投资策略》 - 石川
  3. 《因子投资》 - 安德鲁·贝金

# 研究平台

  • SSRN:学术论文
  • 聚宽:量化研究平台
  • 优矿:多因子研究

多因子选股是量化投资的核心方法,持续研究和优化是成功的关键!

上一篇:大语言模型概述 | 下一篇:高频交易策略实现

Last Updated: 9/25/2026, 2:08:32 PM