├── attach.png ├── example.py └── README.md /attach.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TruthHun/multi-factor-stock-selection/HEAD/attach.png -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | from __future__ import print_function, absolute_import, unicode_literals 3 | import numpy as np 4 | from gm.api import * 5 | from pandas import DataFrame 6 | ''' 7 | 本策略每隔1个月定时触发,根据Fama-French三因子模型对每只股票进行回归,得到其alpha值。 8 | 假设Fama-French三因子模型可以完全解释市场,则alpha为负表明市场低估该股,因此应该买入。 9 | 策略思路: 10 | 计算市场收益率、个股的账面市值比和市值,并对后两个进行了分类, 11 | 根据分类得到的组合分别计算其市值加权收益率、SMB和HML. 12 | 对各个股票进行回归(假设无风险收益率等于0)得到alpha值. 13 | 选取alpha值小于0并为最小的10只股票进入标的池 14 | 平掉不在标的池的股票并等权买入在标的池的股票 15 | 回测数据:SHSE.000300的成份股 16 | 回测时间:2017-07-01 08:00:00到2017-10-01 16:00:00 17 | ''' 18 | def init(context): 19 | # 每月第一个交易日的09:40 定时执行algo任务 20 | schedule(schedule_func=algo, date_rule='1m', time_rule='09:40:00') 21 | print(order_target_percent(symbol='SHSE.600000', percent=0.5, order_type=OrderType_Market, 22 | position_side=PositionSide_Long)) 23 | # 数据滑窗 24 | context.date = 20 25 | # 设置开仓的最大资金量 26 | context.ratio = 0.8 27 | # 账面市值比的大/中/小分类 28 | context.BM_BIG = 3.0 29 | context.BM_MID = 2.0 30 | context.BM_SMA = 1.0 31 | # 市值大/小分类 32 | context.MV_BIG = 2.0 33 | context.MV_SMA = 1.0 34 | # 计算市值加权的收益率,MV为市值的分类,BM为账目市值比的分类 35 | def market_value_weighted(stocks, MV, BM): 36 | select = stocks[(stocks.NEGOTIABLEMV == MV) & (stocks.BM == BM)] 37 | market_value = select['mv'].values 38 | mv_total = np.sum(market_value) 39 | mv_weighted = [mv / mv_total for mv in market_value] 40 | stock_return = select['return'].values 41 | # 返回市值加权的收益率的和 42 | return_total = [] 43 | for i in range(len(mv_weighted)): 44 | return_total.append(mv_weighted[i] * stock_return[i]) 45 | return_total = np.sum(return_total) 46 | return return_total 47 | def algo(context): 48 | # 获取上一个交易日的日期 49 | last_day = get_previous_trading_date(exchange='SHSE', date=context.now) 50 | # 获取沪深300成份股 51 | context.stock300 = get_history_constituents(index='SHSE.000300', start_date=last_day, 52 | end_date=last_day)[0]['constituents'].keys() 53 | # 获取当天有交易的股票 54 | not_suspended = get_history_instruments(symbols=context.stock300, start_date=last_day, end_date=last_day) 55 | not_suspended = [item['symbol'] for item in not_suspended if not item['is_suspended']] 56 | fin = get_fundamentals(table='tq_sk_finindic', symbols=not_suspended, start_date=last_day, end_date=last_day, 57 | fields='PB,NEGOTIABLEMV', df=True) 58 | # 计算账面市值比,为P/B的倒数 59 | fin['PB'] = (fin['PB'] ** -1) 60 | # 计算市值的50%的分位点,用于后面的分类 61 | size_gate = fin['NEGOTIABLEMV'].quantile(0.50) 62 | # 计算账面市值比的30%和70%分位点,用于后面的分类 63 | bm_gate = [fin['PB'].quantile(0.30), fin['PB'].quantile(0.70)] 64 | fin.index = fin.symbol 65 | x_return = [] 66 | # 对未停牌的股票进行处理 67 | for symbol in not_suspended: 68 | # 计算收益率 69 | close = history_n(symbol=symbol, frequency='1d', count=context.date + 1, end_time=last_day, fields='close', 70 | skip_suspended=True, fill_missing='Last', adjust=ADJUST_PREV, df=True)['close'].values 71 | stock_return = close[-1] / close[0] - 1 72 | pb = fin['PB'][symbol] 73 | market_value = fin['NEGOTIABLEMV'][symbol] 74 | # 获取[股票代码. 股票收益率, 账面市值比的分类, 市值的分类, 流通市值] 75 | if pb < bm_gate[0]: 76 | if market_value < size_gate: 77 | label = [symbol, stock_return, context.BM_SMA, context.MV_SMA, market_value] 78 | else: 79 | label = [symbol, stock_return, context.BM_SMA, context.MV_BIG, market_value] 80 | elif pb < bm_gate[1]: 81 | if market_value < size_gate: 82 | label = [symbol, stock_return, context.BM_MID, context.MV_SMA, market_value] 83 | else: 84 | label = [symbol, stock_return, context.BM_MID, context.MV_BIG, market_value] 85 | elif market_value < size_gate: 86 | label = [symbol, stock_return, context.BM_BIG, context.MV_SMA, market_value] 87 | else: 88 | label = [symbol, stock_return, context.BM_BIG, context.MV_BIG, market_value] 89 | if len(x_return) == 0: 90 | x_return = label 91 | else: 92 | x_return = np.vstack([x_return, label]) 93 | stocks = DataFrame(data=x_return, columns=['symbol', 'return', 'BM', 'NEGOTIABLEMV', 'mv']) 94 | stocks.index = stocks.symbol 95 | columns = ['return', 'BM', 'NEGOTIABLEMV', 'mv'] 96 | for column in columns: 97 | stocks[column] = stocks[column].astype(np.float64) 98 | # 计算SMB.HML和市场收益率 99 | # 获取小市值组合的市值加权组合收益率 100 | smb_s = (market_value_weighted(stocks, context.MV_SMA, context.BM_SMA) + 101 | market_value_weighted(stocks, context.MV_SMA, context.BM_MID) + 102 | market_value_weighted(stocks, context.MV_SMA, context.BM_BIG)) / 3 103 | # 获取大市值组合的市值加权组合收益率 104 | smb_b = (market_value_weighted(stocks, context.MV_BIG, context.BM_SMA) + 105 | market_value_weighted(stocks, context.MV_BIG, context.BM_MID) + 106 | market_value_weighted(stocks, context.MV_BIG, context.BM_BIG)) / 3 107 | smb = smb_s - smb_b 108 | # 获取大账面市值比组合的市值加权组合收益率 109 | hml_b = (market_value_weighted(stocks, context.MV_SMA, 3) + 110 | market_value_weighted(stocks, context.MV_BIG, context.BM_BIG)) / 2 111 | # 获取小账面市值比组合的市值加权组合收益率 112 | hml_s = (market_value_weighted(stocks, context.MV_SMA, context.BM_SMA) + 113 | market_value_weighted(stocks, context.MV_BIG, context.BM_SMA)) / 2 114 | hml = hml_b - hml_s 115 | close = history_n(symbol='SHSE.000300', frequency='1d', count=context.date + 1, 116 | end_time=last_day, fields='close', skip_suspended=True, 117 | fill_missing='Last', adjust=ADJUST_PREV, df=True)['close'].values 118 | market_return = close[-1] / close[0] - 1 119 | coff_pool = [] 120 | # 对每只股票进行回归获取其alpha值 121 | for stock in stocks.index: 122 | x_value = np.array([[market_return], [smb], [hml], [1.0]]) 123 | y_value = np.array([stocks['return'][stock]]) 124 | # OLS估计系数 125 | coff = np.linalg.lstsq(x_value.T, y_value)[0][3] 126 | coff_pool.append(coff) 127 | # 获取alpha最小并且小于0的10只的股票进行操作(若少于10只则全部买入) 128 | stocks['alpha'] = coff_pool 129 | stocks = stocks[stocks.alpha < 0].sort_values(by='alpha').head(10) 130 | symbols_pool = stocks.index.tolist() 131 | positions = context.account().positions() 132 | # 平不在标的池的股票 133 | for position in positions: 134 | symbol = position['symbol'] 135 | if symbol not in symbols_pool: 136 | order_target_percent(symbol=symbol, percent=0, order_type=OrderType_Market, 137 | position_side=PositionSide_Long) 138 | print('市价单平不在标的池的', symbol) 139 | # 获取股票的权重 140 | percent = context.ratio / len(symbols_pool) 141 | # 买在标的池中的股票 142 | for symbol in symbols_pool: 143 | order_target_percent(symbol=symbol, percent=percent, order_type=OrderType_Market, 144 | position_side=PositionSide_Long) 145 | print(symbol, '以市价单调多仓到仓位', percent) 146 | if __name__ == '__main__': 147 | ''' 148 | strategy_id策略ID,由系统生成 149 | filename文件名,请与本文件名保持一致 150 | mode实时模式:MODE_LIVE回测模式:MODE_BACKTEST 151 | token绑定计算机的ID,可在系统设置-密钥管理中生成 152 | backtest_start_time回测开始时间 153 | backtest_end_time回测结束时间 154 | backtest_adjust股票复权方式不复权:ADJUST_NONE前复权:ADJUST_PREV后复权:ADJUST_POST 155 | backtest_initial_cash回测初始资金 156 | backtest_commission_ratio回测佣金比例 157 | backtest_slippage_ratio回测滑点比例 158 | ''' 159 | run(strategy_id='strategy_id', 160 | filename='main.py', 161 | mode=MODE_BACKTEST, 162 | token='token_id', 163 | backtest_start_time='2017-07-01 08:00:00', 164 | backtest_end_time='2017-10-01 16:00:00', 165 | backtest_adjust=ADJUST_PREV, 166 | backtest_initial_cash=10000000, 167 | backtest_commission_ratio=0.0001, 168 | backtest_slippage_ratio=0.0001) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 多因子选股(股票) 2 | 基于Fama三因子构成的多因子策略 3 | 4 | ## 源码 5 | ```python 6 | # coding=utf-8 7 | from __future__ import print_function, absolute_import, unicode_literals 8 | import numpy as np 9 | from gm.api import * 10 | from pandas import DataFrame 11 | ''' 12 | 本策略每隔1个月定时触发,根据Fama-French三因子模型对每只股票进行回归,得到其alpha值。 13 | 假设Fama-French三因子模型可以完全解释市场,则alpha为负表明市场低估该股,因此应该买入。 14 | 策略思路: 15 | 计算市场收益率、个股的账面市值比和市值,并对后两个进行了分类, 16 | 根据分类得到的组合分别计算其市值加权收益率、SMB和HML. 17 | 对各个股票进行回归(假设无风险收益率等于0)得到alpha值. 18 | 选取alpha值小于0并为最小的10只股票进入标的池 19 | 平掉不在标的池的股票并等权买入在标的池的股票 20 | 回测数据:SHSE.000300的成份股 21 | 回测时间:2017-07-01 08:00:00到2017-10-01 16:00:00 22 | ''' 23 | def init(context): 24 | # 每月第一个交易日的09:40 定时执行algo任务 25 | schedule(schedule_func=algo, date_rule='1m', time_rule='09:40:00') 26 | print(order_target_percent(symbol='SHSE.600000', percent=0.5, order_type=OrderType_Market, 27 | position_side=PositionSide_Long)) 28 | # 数据滑窗 29 | context.date = 20 30 | # 设置开仓的最大资金量 31 | context.ratio = 0.8 32 | # 账面市值比的大/中/小分类 33 | context.BM_BIG = 3.0 34 | context.BM_MID = 2.0 35 | context.BM_SMA = 1.0 36 | # 市值大/小分类 37 | context.MV_BIG = 2.0 38 | context.MV_SMA = 1.0 39 | # 计算市值加权的收益率,MV为市值的分类,BM为账目市值比的分类 40 | def market_value_weighted(stocks, MV, BM): 41 | select = stocks[(stocks.NEGOTIABLEMV == MV) & (stocks.BM == BM)] 42 | market_value = select['mv'].values 43 | mv_total = np.sum(market_value) 44 | mv_weighted = [mv / mv_total for mv in market_value] 45 | stock_return = select['return'].values 46 | # 返回市值加权的收益率的和 47 | return_total = [] 48 | for i in range(len(mv_weighted)): 49 | return_total.append(mv_weighted[i] * stock_return[i]) 50 | return_total = np.sum(return_total) 51 | return return_total 52 | def algo(context): 53 | # 获取上一个交易日的日期 54 | last_day = get_previous_trading_date(exchange='SHSE', date=context.now) 55 | # 获取沪深300成份股 56 | context.stock300 = get_history_constituents(index='SHSE.000300', start_date=last_day, 57 | end_date=last_day)[0]['constituents'].keys() 58 | # 获取当天有交易的股票 59 | not_suspended = get_history_instruments(symbols=context.stock300, start_date=last_day, end_date=last_day) 60 | not_suspended = [item['symbol'] for item in not_suspended if not item['is_suspended']] 61 | fin = get_fundamentals(table='tq_sk_finindic', symbols=not_suspended, start_date=last_day, end_date=last_day, 62 | fields='PB,NEGOTIABLEMV', df=True) 63 | # 计算账面市值比,为P/B的倒数 64 | fin['PB'] = (fin['PB'] ** -1) 65 | # 计算市值的50%的分位点,用于后面的分类 66 | size_gate = fin['NEGOTIABLEMV'].quantile(0.50) 67 | # 计算账面市值比的30%和70%分位点,用于后面的分类 68 | bm_gate = [fin['PB'].quantile(0.30), fin['PB'].quantile(0.70)] 69 | fin.index = fin.symbol 70 | x_return = [] 71 | # 对未停牌的股票进行处理 72 | for symbol in not_suspended: 73 | # 计算收益率 74 | close = history_n(symbol=symbol, frequency='1d', count=context.date + 1, end_time=last_day, fields='close', 75 | skip_suspended=True, fill_missing='Last', adjust=ADJUST_PREV, df=True)['close'].values 76 | stock_return = close[-1] / close[0] - 1 77 | pb = fin['PB'][symbol] 78 | market_value = fin['NEGOTIABLEMV'][symbol] 79 | # 获取[股票代码. 股票收益率, 账面市值比的分类, 市值的分类, 流通市值] 80 | if pb < bm_gate[0]: 81 | if market_value < size_gate: 82 | label = [symbol, stock_return, context.BM_SMA, context.MV_SMA, market_value] 83 | else: 84 | label = [symbol, stock_return, context.BM_SMA, context.MV_BIG, market_value] 85 | elif pb < bm_gate[1]: 86 | if market_value < size_gate: 87 | label = [symbol, stock_return, context.BM_MID, context.MV_SMA, market_value] 88 | else: 89 | label = [symbol, stock_return, context.BM_MID, context.MV_BIG, market_value] 90 | elif market_value < size_gate: 91 | label = [symbol, stock_return, context.BM_BIG, context.MV_SMA, market_value] 92 | else: 93 | label = [symbol, stock_return, context.BM_BIG, context.MV_BIG, market_value] 94 | if len(x_return) == 0: 95 | x_return = label 96 | else: 97 | x_return = np.vstack([x_return, label]) 98 | stocks = DataFrame(data=x_return, columns=['symbol', 'return', 'BM', 'NEGOTIABLEMV', 'mv']) 99 | stocks.index = stocks.symbol 100 | columns = ['return', 'BM', 'NEGOTIABLEMV', 'mv'] 101 | for column in columns: 102 | stocks[column] = stocks[column].astype(np.float64) 103 | # 计算SMB.HML和市场收益率 104 | # 获取小市值组合的市值加权组合收益率 105 | smb_s = (market_value_weighted(stocks, context.MV_SMA, context.BM_SMA) + 106 | market_value_weighted(stocks, context.MV_SMA, context.BM_MID) + 107 | market_value_weighted(stocks, context.MV_SMA, context.BM_BIG)) / 3 108 | # 获取大市值组合的市值加权组合收益率 109 | smb_b = (market_value_weighted(stocks, context.MV_BIG, context.BM_SMA) + 110 | market_value_weighted(stocks, context.MV_BIG, context.BM_MID) + 111 | market_value_weighted(stocks, context.MV_BIG, context.BM_BIG)) / 3 112 | smb = smb_s - smb_b 113 | # 获取大账面市值比组合的市值加权组合收益率 114 | hml_b = (market_value_weighted(stocks, context.MV_SMA, 3) + 115 | market_value_weighted(stocks, context.MV_BIG, context.BM_BIG)) / 2 116 | # 获取小账面市值比组合的市值加权组合收益率 117 | hml_s = (market_value_weighted(stocks, context.MV_SMA, context.BM_SMA) + 118 | market_value_weighted(stocks, context.MV_BIG, context.BM_SMA)) / 2 119 | hml = hml_b - hml_s 120 | close = history_n(symbol='SHSE.000300', frequency='1d', count=context.date + 1, 121 | end_time=last_day, fields='close', skip_suspended=True, 122 | fill_missing='Last', adjust=ADJUST_PREV, df=True)['close'].values 123 | market_return = close[-1] / close[0] - 1 124 | coff_pool = [] 125 | # 对每只股票进行回归获取其alpha值 126 | for stock in stocks.index: 127 | x_value = np.array([[market_return], [smb], [hml], [1.0]]) 128 | y_value = np.array([stocks['return'][stock]]) 129 | # OLS估计系数 130 | coff = np.linalg.lstsq(x_value.T, y_value)[0][3] 131 | coff_pool.append(coff) 132 | # 获取alpha最小并且小于0的10只的股票进行操作(若少于10只则全部买入) 133 | stocks['alpha'] = coff_pool 134 | stocks = stocks[stocks.alpha < 0].sort_values(by='alpha').head(10) 135 | symbols_pool = stocks.index.tolist() 136 | positions = context.account().positions() 137 | # 平不在标的池的股票 138 | for position in positions: 139 | symbol = position['symbol'] 140 | if symbol not in symbols_pool: 141 | order_target_percent(symbol=symbol, percent=0, order_type=OrderType_Market, 142 | position_side=PositionSide_Long) 143 | print('市价单平不在标的池的', symbol) 144 | # 获取股票的权重 145 | percent = context.ratio / len(symbols_pool) 146 | # 买在标的池中的股票 147 | for symbol in symbols_pool: 148 | order_target_percent(symbol=symbol, percent=percent, order_type=OrderType_Market, 149 | position_side=PositionSide_Long) 150 | print(symbol, '以市价单调多仓到仓位', percent) 151 | if __name__ == '__main__': 152 | ''' 153 | strategy_id策略ID,由系统生成 154 | filename文件名,请与本文件名保持一致 155 | mode实时模式:MODE_LIVE回测模式:MODE_BACKTEST 156 | token绑定计算机的ID,可在系统设置-密钥管理中生成 157 | backtest_start_time回测开始时间 158 | backtest_end_time回测结束时间 159 | backtest_adjust股票复权方式不复权:ADJUST_NONE前复权:ADJUST_PREV后复权:ADJUST_POST 160 | backtest_initial_cash回测初始资金 161 | backtest_commission_ratio回测佣金比例 162 | backtest_slippage_ratio回测滑点比例 163 | ''' 164 | run(strategy_id='strategy_id', 165 | filename='main.py', 166 | mode=MODE_BACKTEST, 167 | token='token_id', 168 | backtest_start_time='2017-07-01 08:00:00', 169 | backtest_end_time='2017-10-01 16:00:00', 170 | backtest_adjust=ADJUST_PREV, 171 | backtest_initial_cash=10000000, 172 | backtest_commission_ratio=0.0001, 173 | backtest_slippage_ratio=0.0001) 174 | ``` 175 | 176 | ## 绩效图 177 | ![绩效图](attach.png) --------------------------------------------------------------------------------