2. Selección de acciones alfa

Fuente de referencia: docs/_joinquant_migration_source/Example_02_Alpha stock selection strategy.ipynb Primera celda de Markdown.

Esta estrategia se activa cada mes para calcular el EV/EBITDA anterior de las acciones componentes SHSE.000300 y selecciona acciones con EV/EBITDA mayor que 0. Luego, cierra las posiciones de las acciones clasificadas entre las 30 más pequeñas de EV/EBITDA y compra igualmente acciones clasificadas entre las 30 primeras de las 30 EV/EBITDA más pequeñas.

Datos retrospectivo: SHSE.000300 Acciones que componen el índice 300 de Shanghai y Shenzhen

Tiempo de prueba retrospectiva: 2016-04-05 al 2021-02-01

Primero, importe el módulo qteasy.

>>> import qteasy as qt

Antes de seleccionar acciones, debe verificar los datos históricos requeridos.

Los datos EV/EBITDA no existen directamente en los tipos de datos definidos por qteasy y deben calcularse mediante varias combinaciones de datos.

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

Los puntos de datos anteriores representan la capitalización de mercado total, los pasivos totales, el efectivo total y los equivalentes de efectivo, respectivamente. Estos datos deben extraerse de los tipos de datos integrados en QtEasy y calcularse utilizando la fórmula anterior para que sirvan como factores de selección de acciones. Después de excluir los factores con valores inferiores a 0, todos los factores de selección de acciones se ordenan de menor a mayor. Se seleccionan las 30 acciones principales en función de estos factores y todos los fondos disponibles se distribuyen uniformemente entre estas acciones seleccionadas y se mantienen durante un mes hasta el siguiente ciclo de selección de acciones.

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

El resultado es el siguiente:


['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. El primer método para configurar una estrategia personalizada: generar directamente una señal comercial proporcional (señal PS) utilizando datos de posición y datos de selección de acciones.

Usando la clase GeneralStrategy, después de calcular los factores de selección de acciones, se eliminan todos los factores menores que cero y las 30 acciones principales se extraen después de la clasificación. Las señales comerciales se generan según la siguiente lógica: 1. Verificar las tenencias actuales; Si alguna acción mantenida no está entre las 30 seleccionadas, véndala toda. 2. Verificar las existencias actuales; si las acciones recién seleccionadas no se mantienen, compre las acciones recién seleccionadas con la misma ponderación.

Establezca el tipo de señal comercial en PS para generar señales comerciales. Dado que la generación de señales comerciales requiere datos de posición, no se puede utilizar el modo de generación por lotes; Sólo se puede utilizar el modo en tiempo real.

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

Después de definir la estrategia comercial, puede comenzar a crear objetos Operator y realizar pruebas retrospectivas:

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

El resultado es el siguiente:

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

Nota: El diagrama de esta sección puede tener problemas de compatibilidad de codificación en algunos entornos de compilación. Si no se muestra, no afectará la comprensión de los pasos del ejemplo.

2.2. El segundo método para configurar una estrategia personalizada: establezca el tipo de señal comercial en PT, genere señales de posición objetivo y genere automáticamente señales comerciales durante el 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

Utilice el mismo método para crear un objeto Operator e iniciar la prueba retrospectiva.

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

El resultado es el siguiente:

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

Nota: El diagrama de esta sección puede tener problemas de compatibilidad de codificación en algunos entornos de compilación. Si no se muestra, no afectará la comprensión de los pasos del ejemplo.

2.3. El tercer método para configurar estrategias personalizadas: utilizar la clase de estrategia FactorSorter.

Utilizando la clase de estrategia FactorSorter, los factores de selección de acciones para las estrategias comerciales se generan directamente y luego se implementa la selección de acciones en función de los parámetros de selección de acciones de la estrategia FactorSorter.

Establezca el tipo de señal comercial en PT, genere objetivos de retención y genere automáticamente señales comerciales.

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

Utilice el mismo método para crear un objeto Operator e iniciar la prueba retrospectiva.

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

El resultado es el siguiente:





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

Nota: El diagrama de esta sección puede tener problemas de compatibilidad de codificación en algunos entornos de compilación. Si no se muestra, no afectará la comprensión de los pasos del ejemplo.