2. Alpha選股交易策略
參考來源:docs/_joinquant_migration_source/Example_02_Alpha選股策略.ipynb 第一個 Markdown cell。
本策略每隔1個月定時觸發計算SHSE.000300成份股的過去的EV/EBITDA並選取EV/EBITDA大於0的股票,隨後平掉排名EV/EBITDA不在最小的30的股票持倉並等權購買EV/EBITDA最小排名在前30的股票
回測數據爲:SHSE.000300滬深300指數成份股
回測時間爲:2016-04-05 到 2021-02-01
首先導入qteasy模塊
>>> import qteasy as qt
在選股之前,需要檢查需要的歷史數據
EV/EBITDA數據並不直接存在於qteasy定義的數據類型中,需要通過幾個數據組合計算出來
EV/EBITDA = (Market Capitalization + Total Debt - Total Cash) / EBITDA
上面幾個數據分別代表總市值、總負債、總現金及現金等價物,這些數據需要從qteasy內置的數據類型中分別提取,並使用上面的公式計算後,作爲選股因子。排除掉小於0的因子後,將所有選股因子從小到大排列,選出因子排在最前的30支股票,將手中的全部資金平均分配投入到所有選中的股票中持有一個月,直到下一次選股爲止。
>>> htypes = 'total_mv, total_liab, c_cash_equ_end_period, ebitda'
>>> shares = qt.filter_stock_codes(index='000300.SH', date='20220131')
>>> print(shares[0:50])
>>> dt = qt.get_history_data(htypes, shares=shares, asset_type='any', freq='m')
>>> one_share = shares[24]
>>> df = dt[one_share]
>>> df['ev_to_ebitda'] = (df.total_mv + df.total_liab - df.c_cash_equ_end_period) / df.ebitda
輸出如下:
['000001.SZ', '000002.SZ', '000063.SZ', '000066.SZ', '000069.SZ', '000100.SZ', '000157.SZ', '000166.SZ', '000301.SZ', '000333.SZ', '000338.SZ', '000425.SZ', '000538.SZ', '000568.SZ', '000596.SZ', '000625.SZ', '000651.SZ', '000661.SZ', '000703.SZ', '000708.SZ', '000725.SZ', '000768.SZ', '000776.SZ', '000783.SZ', '000786.SZ', '000800.SZ', '000858.SZ', '000876.SZ', '000895.SZ', '000938.SZ', '000963.SZ', '000977.SZ', '001979.SZ', '002001.SZ', '002007.SZ', '002008.SZ', '002024.SZ', '002027.SZ', '002032.SZ', '002044.SZ', '002049.SZ', '002050.SZ', '002064.SZ', '002120.SZ', '002129.SZ', '002142.SZ', '002157.SZ', '002179.SZ', '002202.SZ', '002230.SZ']
2.1. 第一種自定義策略設置方法: 使用持倉數據和選股數據直接生成比例交易信號PS信號:
使用GeneralStrategy策略類,計算選股因子後,去掉所有小於零的因子,排序後提取排名前三十的股票 按以下邏輯生成交易信號: 1,檢查當前持倉,如果持倉的股票不在選中的30箇中間,則全數賣出 2,檢查當前持倉,如果新選中的股票沒有持倉,則等權買入新增選中的股票
設置交易信號類型爲PS,生成交易信號 由於生成交易信號需要用到持倉數據,因此不能使用批量生成模式,只能使用realtime模式
>>> class AlphaPS(qt.GeneralStg):
...
... def realize(self):
...
... # 从历史数据编码中读取四种历史数据的最新数值
... total_mv = self.get_data('total_mv_E_d')[-1] # 总市值
... total_liab = self.get_data('total_liab_E_q')[-1] # 总负债
... cash_equ = self.get_data('c_cash_equ_end_period_E_q')[-1] # 现金及现金等价物总额
... ebitda = self.get_data('ebitda_E_q')[-1] # ebitda,息税折旧摊销前利润
...
... # 从持仓数据中读取当前的持仓数量,并找到持仓股序号
... own_amounts = self.get_data('proc.own_amounts')
... owned = np.where(own_amounts > 0)[0] # 所有持仓股的序号
... not_owned = np.where(own_amounts == 0)[0] # 所有未持仓的股票序号
...
... # 选股因子为EV/EBIDTA,使用下面公式计算
... factors = (total_mv + total_liab - cash_equ) / ebitda
... # 处理交易信号,将所有小于0的因子变为NaN
... factors = np.where(factors < 0, np.nan, factors)
... # 选出数值最小的30个股票的序号
... arg_partitioned = factors.argpartition(30)
... selected = arg_partitioned[:30] # 被选中的30个股票的序号
... not_selected = arg_partitioned[30:] # 未被选中的其他股票的序号(包括因子为NaN的股票)
...
... # 开始生成交易信号
... signal = np.zeros_like(factors)
... # 如果持仓为正,且未被选中,生成全仓卖出交易信号
... own_but_not_selected = np.intersect1d(owned, not_selected)
... signal[own_but_not_selected] = -1 # 在PS信号模式下 -1 代表全仓卖出
...
... # 如果持仓为零,且被选中,生成全仓买入交易信号
... selected_but_not_own = np.intersect1d(not_owned, selected)
... signal[selected_but_not_own] = 0.0333 # 在PS信号模式下,+1 代表全仓买进 (如果多只股票均同时全仓买进,则会根据资金总量平均分配资金)
...
... return signal
定義好交易策略之後,就可以開始創建Operator對象並啓動回測了:
>>> import numpy as np
>>> alpha = AlphaPS(pars=[],
... name='AlphaPS',
... description='本策略每隔1个月定时触发计算SHSE.000300成份股的过去的EV/EBITDA并选取EV/EBITDA大于0的股票',
... data_types=[DataType('total_mv', asset_type='E'),
... DataType('total_liab'),
... DataType('c_cash_equ_end_period'),
... DataType('ebitda')],
... window_length=10)
>>> op = qt.Operator(alpha, signal_type='PS')
>>> qt.run(op=op,
... mode=1,
... asset_type='E',
... asset_pool=shares,
... invest_start='20160405',
... invest_end='20210201',
... trade_batch_size=100,
... sell_batch_size=1,
... trade_log=True)
輸出如下:
====================================
| |
| BACK TESTING RESULT |
| |
====================================
qteasy running mode: 1 - History back testing
time consumption for operate signal creation: 0.0 ms
time consumption for operation back looping: 14 sec 449.9 ms
investment starts on 2016-04-05 00:00:00
ends on 2021-02-01 00:00:00
Total looped periods: 4.8 years.
-------------operation summary:------------
Only non-empty shares are displayed, call
"loop_result["oper_count"]" for complete operation summary
Sell Cnt Buy Cnt Total Long pct Short pct Empty pct
000301.SZ 1 1 2 10.3% 0.0% 89.7%
000786.SZ 2 2 4 24.3% 0.0% 75.7%
000895.SZ 2 3 5 66.6% 0.0% 33.4%
002001.SZ 3 3 6 55.5% 0.0% 44.5%
002007.SZ 1 2 3 62.4% 0.0% 37.6%
002027.SZ 2 2 4 41.1% 0.0% 58.9%
002032.SZ 1 1 2 3.6% 0.0% 96.4%
002044.SZ 1 1 2 3.6% 0.0% 96.4%
002049.SZ 1 1 2 3.0% 0.0% 97.0%
002050.SZ 3 3 6 12.7% 0.0% 87.3%
... ... ... ... ... ... ...
300223.SZ 1 1 2 5.3% 0.0% 94.7%
300496.SZ 1 1 2 5.1% 0.0% 94.9%
600219.SH 0 1 1 5.9% 0.0% 94.1%
603185.SH 1 1 2 5.1% 0.0% 94.9%
688005.SH 1 1 2 5.1% 0.0% 94.9%
002756.SZ 2 2 4 58.3% 0.0% 41.7%
600233.SH 2 2 4 36.0% 0.0% 64.0%
600674.SH 2 2 4 7.0% 0.0% 93.0%
601689.SH 2 2 4 20.9% 0.0% 79.1%
600732.SH 1 1 2 5.5% 0.0% 94.5%
Total operation fee: ¥ 1,565.00
total investment amount: ¥ 100,000.00
final value: ¥ 206,286.74
Total return: 106.29%
Avg Yearly return: 16.17%
Skewness: -0.54
Kurtosis: 2.78
Benchmark return: 65.96%
Benchmark Yearly return: 11.06%
------strategy loop_results indicators------
alpha: 0.071
Beta: 1.047
Sharp ratio: 1.204
Info ratio: 0.031
250 day volatility: 0.131
Max drawdown: 19.42%
peak / valley: 2017-11-16 / 2019-01-03
recovered on: 2019-09-19
===========END OF REPORT=============
注:本段示意圖在部分構建環境中可能存在編碼兼容問題,若未顯示不影響示例步驟理解。
2.2. 第二種自定義策略設置方法:設置交易信號類型爲PT,生成持倉目標信號,在回測過程中自動生成交易信號
>>> class AlphaPT(qt.GeneralStg):
...
... def realize(self):
...
... # 从历史数据编码中读取四种历史数据的最新数值
... total_mv = self.get_data('total_mv_E_d')[-1] # 总市值
... total_liab = self.get_data('total_liab_E_q')[-1] # 总负债
... cash_equ = self.get_data('c_cash_equ_end_period_E_q')[-1] # 现金及现金等价物总额
... ebitda = self.get_data('ebitda_E_q')[-1] # ebitda,息税折旧摊销前利润
...
... # 选股因子为EV/EBIDTA,使用下面公式计算
... factors = (total_mv + total_liab - cash_equ) / ebitda
... # 处理交易信号,将所有小于0的因子变为NaN
... factors = np.where(factors < 0, np.nan, factors)
... # 选出数值最小的30个股票的序号
... arg_partitioned = factors.argpartition(30)
... selected = arg_partitioned[:30] # 被选中的30个股票的序号,此时股票可能有NaN被选中的情况,需要去掉
... not_selected = arg_partitioned[30:] # 未被选中的其他股票的序号(包括因子为NaN的股票)
...
... #如果选出的股票中有因子为NaN的,则剔除掉
... selected = selected[~np.isnan(selected)]
... sel_count = len(selected)
...
... # 开始生成PT交易信号
... signal = np.zeros_like(factors)
... # 所有被选中的股票的持仓目标被设置为0.03,表示持有3.3%
... signal[selected] = 1 / sel_count
... # 其余未选中的所有股票持仓目标在PT信号模式下被设置为0,代表目标仓位为0
... signal[not_selected] = 0
...
... return signal
使用同樣的方法創建一個Operator對象並啓動回測。
>>> import numpy as np
>>> alpha = AlphaPT(pars=(),
... name='AlphaSel',
... description='本策略每隔1个月定时触发计算SHSE.000300成份股的过去的EV/EBITDA并选取EV/EBITDA大于0的股票',
... data_types=[DataType('total_mv', asset_type='E'),
... DataType('total_liab'),
... DataType('c_cash_equ_end_period'),
... DataType('ebitda')],
... window_length=10)
>>> op = qt.Operator(alpha, signal_type='PT', run_freq='M')
>>> res = qt.run(op=op,
... mode=1,
... asset_type='E',
... asset_pool=shares,
... invest_start='20160405',
... invest_end='20210201',
... PT_buy_threshold=0.00, # 如果设置PBT=0.00,PST=0.03,最终收益会达到30万元
... PT_sell_threshold=0.00,
... trade_batch_size=100,
... sell_batch_size=1,
... trade_log=True
... )
輸出如下:
====================================
| |
| BACK TESTING RESULT |
| |
====================================
qteasy running mode: 1 - History back testing
time consumption for operate signal creation: 499.7 ms
time consumption for operation back looping: 8 sec 898.2 ms
investment starts on 2016-04-05 00:00:00
ends on 2021-02-01 00:00:00
Total looped periods: 4.8 years.
-------------operation summary:------------
Only non-empty shares are displayed, call
"loop_result["oper_count"]" for complete operation summary
Sell Cnt Buy Cnt Total Long pct Short pct Empty pct
000301.SZ 2 1 3 10.3% 0.0% 89.7%
000786.SZ 2 3 5 24.3% 0.0% 75.7%
000895.SZ 2 2 4 67.8% 0.0% 32.2%
002001.SZ 3 3 6 56.7% 0.0% 43.3%
002007.SZ 2 2 4 62.4% 0.0% 37.6%
002027.SZ 4 8 12 41.1% 0.0% 58.9%
002032.SZ 2 0 2 6.4% 0.0% 93.6%
002044.SZ 1 2 3 3.6% 0.0% 96.4%
002049.SZ 1 1 2 1.3% 0.0% 98.7%
002050.SZ 3 3 6 12.7% 0.0% 87.3%
... ... ... ... ... ... ...
300223.SZ 1 1 2 5.3% 0.0% 94.7%
300496.SZ 1 1 2 5.1% 0.0% 94.9%
600219.SH 1 1 2 5.9% 0.0% 94.1%
603185.SH 1 1 2 5.1% 0.0% 94.9%
688005.SH 1 1 2 5.1% 0.0% 94.9%
002756.SZ 3 4 7 58.3% 0.0% 41.7%
600233.SH 3 3 6 36.0% 0.0% 64.0%
600674.SH 2 1 3 8.2% 0.0% 91.8%
601689.SH 2 2 4 20.9% 0.0% 79.1%
600732.SH 1 1 2 5.5% 0.0% 94.5%
Total operation fee: ¥ 2,190.00
total investment amount: ¥ 100,000.00
final value: ¥ 194,897.64
Total return: 94.90%
Avg Yearly return: 14.82%
Skewness: -0.48
Kurtosis: 2.82
Benchmark return: 65.96%
Benchmark Yearly return: 11.06%
------strategy loop_results indicators------
alpha: 0.049
Beta: 1.103
Sharp ratio: 1.225
Info ratio: 0.026
250 day volatility: 0.127
Max drawdown: 22.90%
peak / valley: 2018-03-12 / 2019-01-03
recovered on: 2019-12-17
===========END OF REPORT=============
注:本段示意圖在部分構建環境中可能存在編碼兼容問題,若未顯示不影響示例步驟理解。
2.3. 第三種自定義策略設置方法:使用FactorSorter策略類
使用FactorSorter策略類,直接生成交易策略的選股因子,再根據 FactorSorter策略的選股參數實現選股
設置交易信號類型爲PT,生成持倉目標,自動生成交易信號
>>> class AlphaFac(qt.FactorSorter):
...
... def realize(self):
...
... # 从历史数据编码中读取四种历史数据的最新数值
... total_mv = self.get_data('total_mv_E_d')[-1] # 总市值
... total_liab = self.get_data('total_liab_E_q')[-1] # 总负债
... cash_equ = self.get_data('c_cash_equ_end_period_E_q')[-1] # 现金及现金等价物总额
... ebitda = self.get_data('ebitda_E_q')[-1] # ebitda,息税折旧摊销前利润
...
... # 选股因子为EV/EBIDTA,使用下面公式计算
... factor = (total_mv + total_liab - cash_equ) / ebitda
...
... # 由于使用因子排序选股策略,因此直接返回选股因子即可,策略会自动根据设置条件选股
... return factor
使用同樣的方法創建一個Operator對象並啓動回測。
>>> alpha = AlphaFac(pars=(),
... name='AlphaSel',
... description='本策略每隔1个月定时触发计算SHSE.000300成份股的过去的EV/EBITDA并选取EV/EBITDA大于0的股票',
... data_types=[DataType('total_mv', asset_type='E'),
... DataType('total_liab'),
... DataType('c_cash_equ_end_period'),
... DataType('ebitda')],
... window_length=10,
... max_sel_count=30, # 设置选股数量,最多选出30个股票
... condition='greater', # 设置筛选条件,仅筛选因子大于ubound的股票
... ubound=0.0, # 设置筛选条件,仅筛选因子大于0的股票
... weighting='even', # 设置股票权重,所有选中的股票平均分配权重
... sort_ascending=True) # 设置排序方式,因子从小到大排序选择头30名
>>> op = qt.Operator(alpha, signal_type='PT', run_freq='ME')
>>> res = qt.run(op=op,
... mode=1,
... asset_type='E',
... asset_pool=shares,
... invest_start='20160405',
... invest_end='20210201',
... PT_buy_threshold=0.00, # 如果设置PBT=0.00,PST=0.03,最终收益会达到30万元
... PT_sell_threshold=0.00,
... trade_batch_size=1,
... sell_batch_size=1,
... trade_log=True
... )
輸出如下:
====================================
| |
| BACK TESTING RESULT |
| |
====================================
qteasy running mode: 1 - History back testing
time consumption for operate signal creation: 10.9 ms
time consumption for operation back looping: 6 sec 200.0 ms
investment starts on 2016-04-05 00:00:00
ends on 2021-02-01 00:00:00
Total looped periods: 4.8 years.
-------------operation summary:------------
Only non-empty shares are displayed, call
"loop_result["oper_count"]" for complete operation summary
Sell Cnt Buy Cnt Total Long pct Short pct Empty pct
000301.SZ 2 1 3 10.3% 0.0% 89.7%
000786.SZ 2 3 5 24.3% 0.0% 75.7%
000895.SZ 2 2 4 67.8% 0.0% 32.2%
002001.SZ 3 3 6 56.7% 0.0% 43.3%
002007.SZ 2 2 4 62.4% 0.0% 37.6%
002027.SZ 4 8 12 41.1% 0.0% 58.9%
002032.SZ 2 0 2 6.4% 0.0% 93.6%
002044.SZ 1 2 3 3.6% 0.0% 96.4%
002049.SZ 1 1 2 1.3% 0.0% 98.7%
002050.SZ 3 3 6 12.7% 0.0% 87.3%
... ... ... ... ... ... ...
300223.SZ 1 1 2 5.3% 0.0% 94.7%
300496.SZ 1 1 2 5.1% 0.0% 94.9%
600219.SH 1 1 2 5.9% 0.0% 94.1%
603185.SH 1 1 2 5.1% 0.0% 94.9%
688005.SH 1 1 2 5.1% 0.0% 94.9%
002756.SZ 3 4 7 58.3% 0.0% 41.7%
600233.SH 3 3 6 36.0% 0.0% 64.0%
600674.SH 2 1 3 8.2% 0.0% 91.8%
601689.SH 2 2 4 20.9% 0.0% 79.1%
600732.SH 1 1 2 5.5% 0.0% 94.5%
Total operation fee: ¥ 2,195.00
total investment amount: ¥ 100,000.00
final value: ¥ 194,976.99
Total return: 94.98%
Avg Yearly return: 14.82%
Skewness: -0.48
Kurtosis: 2.82
Benchmark return: 65.96%
Benchmark Yearly return: 11.06%
------strategy loop_results indicators------
alpha: 0.049
Beta: 1.103
Sharp ratio: 1.225
Info ratio: 0.026
250 day volatility: 0.127
Max drawdown: 22.90%
peak / valley: 2018-03-12 / 2019-01-03
recovered on: 2019-12-17
===========END OF REPORT=============
注:本段示意圖在部分構建環境中可能存在編碼兼容問題,若未顯示不影響示例步驟理解。