2. アルファ銘柄の選択

参照元:docs/_joinquant_migration_source/Example_02_Alpha stock selection strategy.ipynb 最初のMarkdownセル。

この戦略は、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 銘柄が選択され、利用可能なすべての資金がこれらの選択された銘柄に均等に分配され、次の銘柄選択サイクルまで 1 か月間保有されます。

>>> 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 クラスを使用して、銘柄選択要素を計算した後、0 未満の要素をすべて削除し、ソート後に上位 30 銘柄を抽出します。取引シグナルは次のロジックに従って生成されます。 1. 現在の保有株を確認します。選択した 30 銘柄に保有株がない場合は、すべて売却します。 2. 現在の保有状況を確認します。新しく選択した株式が保有されていない場合は、新たに選択した株式を同じウェイトで購入します。

取引シグナルのタイプを PS に設定して、取引シグナルを生成します。取引シグナルの生成にはポジションデータが必要なため、バッチ生成モードは使用できません。リアルタイムモードのみ使用可能です。

>>> 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. カスタム戦略を設定する 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. カスタム戦略を設定するための 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=============

注: このセクションの図には、一部のビルド環境ではエンコーディングの互換性の問題が発生する可能性があります。表示されていなくても、手順例の理解には影響しません。