├── FAMA 三因子构建.py ├── README.md ├── 多因子Alpha策略_科大财经.ipynb └── 网格交易策略代码.ipynb /FAMA 三因子构建.py: -------------------------------------------------------------------------------- 1 | # FAMA 三因子 2 | # 作者 科大财经 3 | 4 | import pandas as pd 5 | import numpy as np 6 | import tqdm 7 | 8 | # 计算 HML SMB 9 | SMB_total = pd.DataFrame() 10 | HML_total = pd.DataFrame() 11 | for i in tqdm(range(len(month_date)-1)): 12 | start = pd.to_datetime(str(month_date[i])) 13 | end = pd.to_datetime(str(month_date[i+1])) 14 | all_price_change_rate_single_priod = all_price_change_rate.loc[start:end] 15 | #price_change_rate_singal_priod = price_change_rate.loc[start:end] 16 | single_priod = hml_base[hml_base.date == month_date[i]].set_index(['order_book_id','date']).dropna() 17 | single_priod = single_priod.sort_values('mc') #市值升序排布 18 | S = single_priod.index[:int(len(single_priod.index)/2)].get_level_values(0) 19 | B = single_priod.index[int(len(single_priod.index)/2):].get_level_values(0) 20 | single_priod = single_priod.sort_values('pb') #净市率升序排列 21 | L = single_priod.index[:int(len(single_priod.index)*0.3)].get_level_values(0) 22 | M = single_priod.index[int(len(single_priod.index)*0.3):int(len(single_priod.index)*0.7)].get_level_values(0) 23 | H = single_priod.index[int(len(single_priod.index)*0.7):].get_level_values(0) 24 | 25 | portfolio = {} 26 | portfolio['SL'] = list(set(S).intersection(set(L))) 27 | portfolio['SM'] = list(set(S).intersection(set(M))) 28 | portfolio['SH'] = list(set(S).intersection(set(H))) 29 | portfolio['BL'] = list(set(B).intersection(set(L))) 30 | portfolio['BM'] = list(set(B).intersection(set(M))) 31 | portfolio['BH'] = list(set(B).intersection(set(H))) 32 | 33 | # 市值加权求出每个类别的日收益率时间序列 34 | portfolio_return = {} 35 | for por in portfolio: 36 | portfolio_return[por] = 0 37 | for stock in portfolio[por]: 38 | portfolio_return[por] += single_priod['mc'][stock].values*all_price_change_rate_single_priod[stock].fillna(0) 39 | portfolio_return[por] = portfolio_return[por]/np.sum(single_priod['mc'][portfolio[por]]) 40 | 41 | SMB = ((portfolio_return['SL']+portfolio_return['SM']+portfolio_return['SH'])/3-(portfolio_return['BL']+portfolio_return['BM']+portfolio_return['BH'])/3).iloc[:-1] 42 | HML = ((portfolio_return['SH']+portfolio_return['BH'])/2-(portfolio_return['SL']+portfolio_return['BL'])/2).iloc[:-1] 43 | #Rb = price_change_rate_singal_priod['000001.XSHG']-price_change_rate_singal_priod['risk_free_rate'] 44 | SMB_total = pd.concat([SMB_total,SMB],axis = 0) 45 | HML_total = pd.concat([HML_total,HML],axis = 0) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quant_Strategy 2 | 多因子alpha模型采用往期B站视频中所讲解的**中位数去极值法**和**因子无量纲处理**(标准化法)处理数据异常值,已经过回测绩效检验。 3 | -------------------------------------------------------------------------------- /多因子Alpha策略_科大财经.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "28c6d5bf", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "'''\n", 11 | "# 策略名: 多因子Alpha策略\n", 12 | "# 作者: 科大财经\n", 13 | "# 微信:18258005335\n", 14 | "# 或微信:85368454154\n", 15 | "# 若有疑问可咨询\n", 16 | "'''\n", 17 | "import numpy as np\n", 18 | "import pandas as pd\n", 19 | "# 初始化函数 #######################################################################\n", 20 | "def init(context):\n", 21 | "\n", 22 | " context.last_date = ''\n", 23 | " context.hold_max = 30\n", 24 | " run_monthly(func=func_run_monthly, date_rule=-1)\n", 25 | "\n", 26 | "\n", 27 | "# 月末调仓函数 #######################################################################\n", 28 | "def func_run_monthly(context, bar_dict):\n", 29 | " # 获取昨日日期\n", 30 | " date = get_last_datetime().strftime('%Y%m%d')\n", 31 | " # 获取上个月末调仓日期\n", 32 | " context.last_date = func_get_end_date_of_last_month(date)\n", 33 | " \n", 34 | " log.info('############################## ' + str(date) + ' ###############################')\n", 35 | " \n", 36 | " # 获取所有A股股票代码\n", 37 | " securities = list(get_all_securities('stock', date).index)\n", 38 | " \n", 39 | " # 获取pb, pe, ps财务因子为正的股票\n", 40 | " q = query(\n", 41 | " valuation.symbol,\n", 42 | " valuation.pb,\n", 43 | " valuation.ps_ttm,\n", 44 | " valuation.pe_ttm\n", 45 | " ).filter(\n", 46 | " valuation.pb > 0,\n", 47 | " valuation.ps_ttm > 0,\n", 48 | " valuation.pe_ttm > 0,\n", 49 | " valuation.symbol.in_(securities)\n", 50 | " )\n", 51 | " df = get_fundamentals(q, date)\n", 52 | " securities = list(df['valuation_symbol'].values)\n", 53 | " \n", 54 | " \n", 55 | " # 计算过去一个月的股价动量、成交金额、ST信息\n", 56 | " values = get_price(securities, context.last_date, date, '1d', ['close','turnover','is_st'], skip_paused = False, fq = 'pre', is_panel = 0)\n", 57 | " \n", 58 | " momentum = []\n", 59 | " turnover = []\n", 60 | " st = []\n", 61 | " for stock in securities:\n", 62 | " try:\n", 63 | " momentum.append((values[stock]['close'][-1] - values[stock]['close'][0]) / values[stock]['close'][0])\n", 64 | " turnover.append(values[stock]['turnover'].sum())\n", 65 | " st.append(values[stock]['is_st'][-1])\n", 66 | " except:\n", 67 | " log.info('数据缺失: %s' % stock)\n", 68 | " momentum.append(None)\n", 69 | " turnover.append(None)\n", 70 | " st.append(None)\n", 71 | " \n", 72 | " df['momentum'] = np.array(momentum)\n", 73 | " df['turnover'] = np.array(turnover)\n", 74 | " df['is_st'] = np.array(st)\n", 75 | " \n", 76 | " # 去掉ST和成交金额为0的股票\n", 77 | " df[df['is_st'] == 1] = None\n", 78 | " df[df['turnover'] == 0] = None\n", 79 | " df = df.dropna()\n", 80 | " \n", 81 | " \n", 82 | " # 去极值\n", 83 | " df = winsorize(df, 'valuation_pb', 20).copy()\n", 84 | " df = winsorize(df, 'valuation_ps_ttm', 20).copy()\n", 85 | " df = winsorize(df, 'valuation_pe_ttm', 20).copy()\n", 86 | " df = winsorize(df, 'momentum', 20).copy()\n", 87 | " df = winsorize(df, 'turnover', 20).copy()\n", 88 | " df = df.dropna()\n", 89 | " \n", 90 | " \n", 91 | " # 为全部A股打分,综合得分越小越好\n", 92 | " df['scores'] = 0\n", 93 | " \n", 94 | " list_pb = list(df.sort_values(['valuation_pb'], ascending = True)['valuation_symbol'].values)\n", 95 | " func_scores(df, list_pb)\n", 96 | " list_ps = list(df.sort_values(['valuation_ps_ttm'], ascending = True)['valuation_symbol'].values)\n", 97 | " func_scores(df, list_ps)\n", 98 | " list_pe = list(df.sort_values(['valuation_pe_ttm'], ascending = True)['valuation_symbol'].values)\n", 99 | " func_scores(df, list_pe)\n", 100 | " list_mo = list(df.sort_values(['momentum'], ascending = True)['valuation_symbol'].values)\n", 101 | " func_scores(df, list_mo)\n", 102 | " list_to = list(df.sort_values(['turnover'], ascending = True)['valuation_symbol'].values)\n", 103 | " func_scores(df, list_to)\n", 104 | " \n", 105 | " \n", 106 | " # 根据股票综合得分为股票排序\n", 107 | " context.selected = list(df.sort_values(['scores'], ascending = True)['valuation_symbol'].values)\n", 108 | " \n", 109 | " # 买入挑选的股票\n", 110 | " func_do_trade(context, bar_dict)\n", 111 | " \n", 112 | " context.last_date = date\n", 113 | "\n", 114 | "\n", 115 | "#### 每日检查止损条件\n", 116 | "def handle_bar(context, bar_dict):\n", 117 | " \n", 118 | " last_date = get_last_datetime().strftime('%Y%m%d')\n", 119 | " if last_date != context.last_date and len(list(context.portfolio.stock_account.positions.keys())) > 0:\n", 120 | " # 如果不是调仓日且有持仓,判断止损条件\n", 121 | " func_stop_loss(context, bar_dict)\n", 122 | "\n", 123 | "\n", 124 | "################## 以下为功能函数, 在主要函数中调用 ##########################\n", 125 | "\n", 126 | "#### 1. 获取上月月末日期 #####################################################\n", 127 | "def func_get_end_date_of_last_month(current_date):\n", 128 | " trade_days = list(get_trade_days(None, current_date, count=30))\n", 129 | " \n", 130 | " for i in range(len(trade_days)):\n", 131 | " trade_days[i] = trade_days[i].strftime('%Y%m%d')\n", 132 | " \n", 133 | " for date in reversed(trade_days):\n", 134 | " if date[5] != current_date[5]:\n", 135 | " return date\n", 136 | " \n", 137 | " log.info('Cannot find the end date of last month.')\n", 138 | " return\n", 139 | "\n", 140 | "\n", 141 | "#### 2. 中位数去极值函数 ####################################################\n", 142 | "def winsorize(df, factor, n=20):\n", 143 | " '''\n", 144 | " df为bar_dictFrame数据\n", 145 | " factor为需要去极值的列名称\n", 146 | " n 为判断极值上下边界的常数\n", 147 | " '''\n", 148 | " ls_raw = np.array(df[factor].values)\n", 149 | " ls_raw.sort(axis = 0)\n", 150 | " # 获取中位数\n", 151 | " D_M = np.median(ls_raw)\n", 152 | " \n", 153 | " # 计算离差值\n", 154 | " ls_deviation = abs(ls_raw - D_M)\n", 155 | " ls_deviation.sort(axis = 0)\n", 156 | " # 获取离差中位数\n", 157 | " D_MAD = np.median(ls_deviation)\n", 158 | " \n", 159 | " # 将大于中位数n倍离差中位数的值赋为NaN\n", 160 | " df.loc[df[factor] >= D_M + n * D_MAD, factor] = None\n", 161 | " # 将小于中位数n倍离差中位数的值赋为NaN\n", 162 | " df.loc[df[factor] <= D_M - n * D_MAD, factor] = None\n", 163 | " \n", 164 | " return df\n", 165 | "\n", 166 | "\n", 167 | "#### 3. 按因子排序打分函数 #############################################################\n", 168 | "def func_scores(df, ls):\n", 169 | " '''\n", 170 | " 按照因子暴露值将股票分为20档\n", 171 | " 第一档股票综合得分+1分\n", 172 | " 第二档股票综合得分+2分\n", 173 | " 以此类推\n", 174 | " '''\n", 175 | " quotient = len(ls) // 20\n", 176 | " remainder = len(ls) % 20\n", 177 | " layer = np.array([quotient]*20)\n", 178 | " \n", 179 | " for i in range(0, remainder):\n", 180 | " layer[-(1+i)] += 1\n", 181 | " \n", 182 | " layer = np.insert(layer, 0, 0)\n", 183 | " layer = layer.cumsum()\n", 184 | " \n", 185 | " for i in range(0,20):\n", 186 | " for j in range(layer[i], layer[i+1]):\n", 187 | " df.loc[df['valuation_symbol'] == ls[j], 'scores'] += (i + 1)\n", 188 | "\n", 189 | "\n", 190 | "#### 4.下单函数 ###################################################################\n", 191 | "def func_do_trade(context, bar_dict):\n", 192 | " # 先清空所有持仓\n", 193 | " if len(list(context.portfolio.stock_account.positions.keys())) > 0:\n", 194 | " for stock in list(context.portfolio.stock_account.positions.keys()):\n", 195 | " order_target(stock, 0)\n", 196 | " \n", 197 | " # 买入前30支股票\n", 198 | " for stock in context.selected:\n", 199 | " order_target_percent(stock, 1./context.hold_max)\n", 200 | " if len(list(context.portfolio.stock_account.positions.keys())) >= context.hold_max:\n", 201 | " break\n", 202 | " return\n", 203 | "\n", 204 | "\n", 205 | "#### 5.止损函数 ####################################################################\n", 206 | "def func_stop_loss(context, bar_dict):\n", 207 | " #获取账户持仓信息\n", 208 | " holdstock = list(context.portfolio.stock_account.positions.keys()) \n", 209 | " if len(holdstock) > 0:\n", 210 | " num = -0.1\n", 211 | " for stock in holdstock:\n", 212 | " close = history(stock,['close'],1,'1d').values\n", 213 | " if close/context.portfolio.positions[stock].last_price -1 <= num:\n", 214 | " order_target(stock,0)\n", 215 | " log.info('股票{}已止损'.format(stock))\n", 216 | " \n", 217 | " \n", 218 | " #获取账户持仓信息\n", 219 | " holdstock = list(context.portfolio.stock_account.positions.keys()) \n", 220 | " if len(holdstock) > 0:\n", 221 | " num = - 0.13\n", 222 | " T = history('000001.SH',['quote_rate'],7,'1d').values.sum()\n", 223 | " if T < num*100:\n", 224 | " log.info('上证指数连续三天下跌{}已清仓'.format(T))\n", 225 | " for stock in holdstock:\n", 226 | " order_target(stock,0)" 227 | ] 228 | } 229 | ], 230 | "metadata": { 231 | "kernelspec": { 232 | "display_name": "Python 3 (ipykernel)", 233 | "language": "python", 234 | "name": "python3" 235 | }, 236 | "language_info": { 237 | "codemirror_mode": { 238 | "name": "ipython", 239 | "version": 3 240 | }, 241 | "file_extension": ".py", 242 | "mimetype": "text/x-python", 243 | "name": "python", 244 | "nbconvert_exporter": "python", 245 | "pygments_lexer": "ipython3", 246 | "version": "3.9.7" 247 | }, 248 | "toc": { 249 | "base_numbering": 1, 250 | "nav_menu": {}, 251 | "number_sections": true, 252 | "sideBar": true, 253 | "skip_h1_title": false, 254 | "title_cell": "Table of Contents", 255 | "title_sidebar": "Contents", 256 | "toc_cell": false, 257 | "toc_position": {}, 258 | "toc_section_display": true, 259 | "toc_window_display": false 260 | } 261 | }, 262 | "nbformat": 4, 263 | "nbformat_minor": 5 264 | } 265 | -------------------------------------------------------------------------------- /网格交易策略代码.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "98e46bf1", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "# coding=utf-8\n", 11 | "from __future__ import print_function, absolute_import, unicode_literals\n", 12 | "import numpy as np\n", 13 | "import pandas as pd\n", 14 | "from gm.api import *\n", 15 | "'''\n", 16 | "本策略标的为:SHFE.rb1901\n", 17 | "价格中枢设定为:前一交易日的收盘价\n", 18 | "从阻力位到压力位分别为:1.03 * open、1.02 * open、1.01 * open、open、0.99 * open、0.98 * open、0.97 * open\n", 19 | "每变动一个网格,交易量变化100个单位\n", 20 | "回测数据为:SHFE.rb1901的1min数据\n", 21 | "回测时间为:2017-07-01 08:00:00到2017-10-01 16:00:00\n", 22 | "'''\n", 23 | "def init(context):\n", 24 | " # 策略标的为SHFE.rb1901\n", 25 | " context.symbol = 'SHFE.rb1901'\n", 26 | " # 订阅SHFE.rb1901, bar频率为1min\n", 27 | " subscribe(symbols = context.symbol, frequency='60s')\n", 28 | " # 设置每变动一格,增减的数量\n", 29 | " context.volume = 1\n", 30 | " # 储存前一个网格所处区间,用来和最新网格所处区间作比较\n", 31 | " context.last_grid = 0\n", 32 | " # 以前一日的收盘价为中枢价格\n", 33 | " context.center = history_n(symbol= context.symbol,frequency='1d',end_time=context.now,count = 1,fields = 'close')[0]['close']\n", 34 | " # 记录上一次交易时网格范围的变化情况(例如从4区到5区,记为4,5)\n", 35 | " context.grid_change_last = [0,0]\n", 36 | "def on_bar(context, bars):\n", 37 | " bar = bars[0]\n", 38 | " # 获取多仓仓位\n", 39 | " position_long = context.account().position(symbol=context.symbol, side=PositionSide_Long)\n", 40 | " # 获取空仓仓位\n", 41 | " position_short = context.account().position(symbol=context.symbol, side=PositionSide_Short)\n", 42 | " # 设置网格和当前价格所处的网格区域\n", 43 | " context.band = np.array([0.97, 0.98, 0.99, 1, 1.01, 1.02, 1.03]) * context.center\n", 44 | " grid = pd.cut([bar.close], context.band, labels=[1, 2, 3, 4, 5, 6])[0]\n", 45 | " # 如果价格超出网格设置范围,则提示调节网格宽度和数量\n", 46 | " if np.isnan(grid):\n", 47 | " print('价格波动超过网格范围,可适当调节网格宽度和数量')\n", 48 | " # 如果新的价格所处网格区间和前一个价格所处的网格区间不同,说明触碰到了网格线,需要进行交易\n", 49 | " # 如果新网格大于前一天的网格,做空或平多\n", 50 | " if context.last_grid < grid:\n", 51 | " # 记录新旧格子范围(按照大小排序)\n", 52 | " grid_change_new = [context.last_grid,grid]\n", 53 | " # 几种例外:\n", 54 | " # 当last_grid = 0 时是初始阶段,不构成信号\n", 55 | " # 如果此时grid = 3,说明当前价格仅在开盘价之下的3区域中,没有突破网格线\n", 56 | " # 如果此时grid = 4,说明当前价格仅在开盘价之上的4区域中,没有突破网格线\n", 57 | " if context.last_grid == 0:\n", 58 | " context.last_grid = grid\n", 59 | " return\n", 60 | " if context.last_grid != 0:\n", 61 | " # 如果前一次开仓是4-5,这一次是5-4,算是没有突破,不成交\n", 62 | " if grid_change_new != context.grid_change_last:\n", 63 | " # 更新前一次的数据\n", 64 | " context.last_grid = grid\n", 65 | " context.grid_change_last = grid_change_new\n", 66 | " # 如果有多仓,平多\n", 67 | " if position_long:\n", 68 | " order_volume(symbol=context.symbol, volume=context.volume, side=OrderSide_Sell, order_type=OrderType_Market,\n", 69 | " position_effect=PositionEffect_Close)\n", 70 | " print('以市价单平多仓{}手'.format(context.volume))\n", 71 | " # 否则,做空\n", 72 | " if not position_long:\n", 73 | " order_volume(symbol=context.symbol, volume=context.volume, side=OrderSide_Sell, order_type=OrderType_Market,\n", 74 | " position_effect=PositionEffect_Open)\n", 75 | " print('以市价单开空{}手'.format(context.volume))\n", 76 | " # 如果新网格小于前一天的网格,做多或平空\n", 77 | " if context.last_grid > grid:\n", 78 | " # 记录新旧格子范围(按照大小排序)\n", 79 | " grid_change_new = [grid,context.last_grid]\n", 80 | " # 几种例外:\n", 81 | " # 当last_grid = 0 时是初始阶段,不构成信号\n", 82 | " # 如果此时grid = 3,说明当前价格仅在开盘价之下的3区域中,没有突破网格线\n", 83 | " # 如果此时grid = 4,说明当前价格仅在开盘价之上的4区域中,没有突破网格线\n", 84 | " if context.last_grid == 0:\n", 85 | " context.last_grid = grid\n", 86 | " return\n", 87 | " if context.last_grid != 0:\n", 88 | " # 如果前一次开仓是4-5,这一次是5-4,算是没有突破,不成交\n", 89 | " if grid_change_new != context.grid_change_last:\n", 90 | " # 更新前一次的数据\n", 91 | " context.last_grid = grid\n", 92 | " context.grid_change_last = grid_change_new\n", 93 | " # 如果有空仓,平空\n", 94 | " if position_short:\n", 95 | " order_volume(symbol=context.symbol, volume=context.volume, side=OrderSide_Buy,\n", 96 | " order_type=OrderType_Market,\n", 97 | " position_effect=PositionEffect_Close)\n", 98 | " print('以市价单平空仓{}手'.format(context.volume))\n", 99 | " # 否则,做多\n", 100 | " if not position_short:\n", 101 | " order_volume(symbol=context.symbol, volume=context.volume, side=OrderSide_Buy,\n", 102 | " order_type=OrderType_Market,\n", 103 | " position_effect=PositionEffect_Open)\n", 104 | " print('以市价单开多{}手'.format(context.volume))\n", 105 | " # 设计一个止损条件:当持仓量达到10手,全部平仓\n", 106 | " if position_short == 10 or position_long == 10:\n", 107 | " order_close_all()\n", 108 | " print('触发止损,全部平仓')\n", 109 | "if __name__ == '__main__':\n", 110 | " '''\n", 111 | " strategy_id策略ID,由系统生成\n", 112 | " filename文件名,请与本文件名保持一致\n", 113 | " mode实时模式:MODE_LIVE回测模式:MODE_BACKTEST\n", 114 | " token绑定计算机的ID,可在系统设置-密钥管理中生成\n", 115 | " backtest_start_time回测开始时间\n", 116 | " backtest_end_time回测结束时间\n", 117 | " backtest_adjust股票复权方式不复权:ADJUST_NONE前复权:ADJUST_PREV后复权:ADJUST_POST\n", 118 | " backtest_initial_cash回测初始资金\n", 119 | " backtest_commission_ratio回测佣金比例\n", 120 | " backtest_slippage_ratio回测滑点比例\n", 121 | " '''\n", 122 | " run(strategy_id='strategy_id',\n", 123 | " filename='main.py',\n", 124 | " mode=MODE_BACKTEST,\n", 125 | " token='token_id',\n", 126 | " backtest_start_time='2018-07-01 08:00:00',\n", 127 | " backtest_end_time='2018-10-01 16:00:00',\n", 128 | " backtest_adjust=ADJUST_PREV,\n", 129 | " backtest_initial_cash=100000,\n", 130 | " backtest_commission_ratio=0.0001,\n", 131 | " backtest_slippage_ratio=0.0001)" 132 | ] 133 | } 134 | ], 135 | "metadata": { 136 | "kernelspec": { 137 | "display_name": "Python 3 (ipykernel)", 138 | "language": "python", 139 | "name": "python3" 140 | }, 141 | "language_info": { 142 | "codemirror_mode": { 143 | "name": "ipython", 144 | "version": 3 145 | }, 146 | "file_extension": ".py", 147 | "mimetype": "text/x-python", 148 | "name": "python", 149 | "nbconvert_exporter": "python", 150 | "pygments_lexer": "ipython3", 151 | "version": "3.9.7" 152 | }, 153 | "toc": { 154 | "base_numbering": 1, 155 | "nav_menu": {}, 156 | "number_sections": true, 157 | "sideBar": true, 158 | "skip_h1_title": false, 159 | "title_cell": "Table of Contents", 160 | "title_sidebar": "Contents", 161 | "toc_cell": false, 162 | "toc_position": {}, 163 | "toc_section_display": true, 164 | "toc_window_display": false 165 | } 166 | }, 167 | "nbformat": 4, 168 | "nbformat_minor": 5 169 | } 170 | --------------------------------------------------------------------------------