2. Alpha Stock Selection

Reference source: docs/_joinquant_migration_source/Example_02_Alpha stock selection strategy.ipynb First Markdown cell.

This strategy triggers every month to calculate the past EV/EBITDA of SHSE.000300 component stocks and selects stocks with EV/EBITDA greater than 0. Then, it closes the positions of stocks ranked in the smallest 30 of EV/EBITDA and equally purchases stocks ranked in the top 30 of the smallest EV/EBITDA.

Backtest data: SHSE.000300 Shanghai and Shenzhen 300 Index component stocks

Backtest time: 2016-04-05 to 2021-02-01

First, import the qteasy module.

>>> import qteasy as qt

Before stock selection, you need to check the required historical data

EV/EBITDA data does not exist directly in the data types defined by qteasy, and needs to be calculated through several data combinations

EV/EBITDA = (Market Capitalization + Total Debt - Total Cash) / EBITDA

The data points above represent total market capitalization, total liabilities, total cash, and cash equivalents, respectively. These data need to be extracted from the built-in data types in QtEasy and calculated using the formula above to serve as stock selection factors. After excluding factors with values less than 0, all stock selection factors are sorted from smallest to largest. The top 30 stocks based on these factors are selected, and all available funds are evenly distributed among these selected stocks and held for one month until the next stock selection cycle.

>>> 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

The output is as follows:


['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. The first method for setting up a custom strategy: Directly generate a proportional trading signal (PS signal) using position data and stock selection data.

Using the GeneralStrategy class, after calculating the stock selection factors, all factors less than zero are removed, and the top 30 stocks are extracted after sorting. Trading signals are generated according to the following logic: 1. Check current holdings; if any held stock is not among the selected 30, sell all of it. 2. Check current holdings; if the newly selected stock is not held, buy the newly selected stock with equal weighting.

Set the trading signal type to PS to generate trading signals. Since generating trading signals requires position data, batch generation mode cannot be used; only real-time mode can be used.

>>> 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

After defining the trading strategy, you can start creating Operator objects and starting backtesting:

>>> 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)

The output is as follows:

     ====================================
     |                                  |
     |       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=============

Note: The diagram in this section may have encoding compatibility issues in some build environments. If it is not shown, it will not affect the understanding of the example steps.

2.2. The second method for setting up a custom strategy: Set the trading signal type to PT, generate target position signals, and automatically generate trading signals during backtesting.

>>> 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

Use the same method to create an Operator object and start the backtest.

>>> 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
...             )

The output is as follows:

     ====================================
     |                                  |
     |       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=============

Note: The diagram in this section may have encoding compatibility issues in some build environments. If it is not shown, it will not affect the understanding of the example steps.

2.3. The third method for setting custom strategies: using the FactorSorter strategy class.

Using the FactorSorter strategy class, stock selection factors for trading strategies are directly generated, and then stock selection is implemented based on the stock selection parameters of the FactorSorter strategy.

Set the trading signal type to PT, generate holding targets, and automatically generate trading signals.

>>> 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

Use the same method to create an Operator object and start the backtest.

>>> 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
...             )

The output is as follows:





     ====================================
     |                                  |
     |       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=============

Note: The diagram in this section may have encoding compatibility issues in some build environments. If it is not shown, it will not affect the understanding of the example steps.