2. Alpha-Aktienauswahl-Handelsstrategie
Referenzquelle: docs/_joinquant_migration_source/Example_02_Alpha stock selection strategy.ipynb Erste Markdown-Zelle.
Diese Strategie berechnet monatlich das EV/EBITDA der im SHSE.000300 enthaltenen Aktien, wählt Aktien mit einem EV/EBITDA größer als 0 aus, schließt dann die Positionen von Aktien, deren EV/EBITDA-Rang nicht zu den 30 niedrigsten gehört, und kauft gleichgewichtete Aktien, deren EV/EBITDA-Rang zu den 30 höchsten gehört.
Die Backtesting-Daten umfassen: SHSE.000300 CSI 300 Indexbestandteile.
Der Backtesting-Zeitraum erstreckte sich vom 5. April 2016 bis zum 1. Februar 2021.
Zuerst importieren Sie das Modul qteasy.
>>> import qteasy as qt
Vor der Aktienauswahl ist es notwendig, die entsprechenden historischen Daten zu prüfen.
EV/EBITDA-Daten existieren nicht direkt in den in QtEasy definierten Datentypen; sie müssen durch die Kombination mehrerer Datenpunkte berechnet werden.
EV/EBITDA = (Marktkapitalisierung + Gesamtverschuldung - Gesamtliquidität) / EBITDA
Die oben genannten Datenpunkte repräsentieren die gesamte Marktkapitalisierung, die Gesamtverbindlichkeiten, die liquiden Mittel und die Zahlungsmitteläquivalente. Diese Daten müssen aus den integrierten Datentypen von QtEasy extrahiert und mithilfe der oben genannten Formel berechnet werden, um als Aktienauswahlkriterien zu dienen. Nach dem Ausschluss von Kriterien mit Werten unter 0 werden alle Aktienauswahlkriterien aufsteigend sortiert. Die 30 Aktien mit den besten Werten werden anhand dieser Kriterien ausgewählt, und das gesamte verfügbare Kapital wird gleichmäßig auf diese Aktien verteilt und für einen Monat bis zum nächsten Aktienauswahlzyklus gehalten.
>>> 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
Die Ausgabe sieht folgendermaßen aus
['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. Die erste Methode zur Einrichtung einer benutzerdefinierten Strategie generiert direkt ein proportionales Handelssignal PS unter Verwendung von Positionsdaten und Aktienauswahldaten.
Mithilfe der Klasse „GeneralStrategy“ werden nach der Berechnung der Aktienauswahlfaktoren alle Faktoren kleiner als null entfernt und die 30 besten Aktien nach der Sortierung extrahiert. Handelssignale werden nach folgender Logik generiert: 1. Überprüfung des aktuellen Bestands; falls eine Aktie nicht unter den 30 ausgewählten Aktien ist, wird sie vollständig verkauft. 2. Überprüfung des aktuellen Bestands; falls die neu ausgewählte Aktie nicht im Bestand ist, wird sie mit gleicher Gewichtung gekauft.
Stellen Sie den Handelssignaltyp auf PS ein, um Handelssignale zu generieren. Da für die Generierung von Handelssignalen Positionsdaten benötigt werden, kann der Batch-Generierungsmodus nicht verwendet werden; es ist nur der Echtzeitmodus verfügbar.
>>> 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
Nachdem die Handelsstrategie definiert wurde, können Sie mit der Erstellung von Operator-Objekten und dem Backtesting beginnen:
>>> 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)
Die Ausgabe sieht folgendermaßen aus
====================================
| |
| 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=============
Hinweis: Das Diagramm in diesem Abschnitt kann in manchen Build-Umgebungen Kompatibilitätsprobleme mit der Kodierung aufweisen. Falls es nicht angezeigt wird, hat dies keinen Einfluss auf das Verständnis der Beispielschritte.
2.2. Die zweite Methode zum Einrichten einer benutzerdefinierten Strategie: Stellen Sie den Handelssignaltyp auf PT ein, generieren Sie Zielpositionssignale und lassen Sie Handelssignale während des Backtestings automatisch generieren.
>>> 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
Verwenden Sie dieselbe Methode, um ein Operator-Objekt zu erstellen und den Backtest zu starten.
>>> 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
... )
Die Ausgabe sieht folgendermaßen aus
====================================
| |
| 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=============
Hinweis: Das Diagramm in diesem Abschnitt kann in manchen Build-Umgebungen Kompatibilitätsprobleme mit der Kodierung aufweisen. Falls es nicht angezeigt wird, hat dies keinen Einfluss auf das Verständnis der Beispielschritte.
2.3. Die dritte Methode zum Festlegen benutzerdefinierter Strategien: Verwendung der FactorSorter-Strategieklasse.
Mithilfe der FactorSorter-Strategieklasse werden direkt Aktienauswahlfaktoren für Handelsstrategien generiert, und anschließend wird die Aktienauswahl auf Basis der Aktienauswahlparameter der FactorSorter-Strategie implementiert.
Stellen Sie den Handelssignaltyp auf PT ein, generieren Sie Halteziele und automatische Handelssignale.
>>> 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
Verwenden Sie dieselbe Methode, um ein Operator-Objekt zu erstellen und den Backtest zu starten.
>>> 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
... )
Die Ausgabe sieht folgendermaßen aus
====================================
| |
| 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=============
Hinweis: Das Diagramm in diesem Abschnitt kann in manchen Build-Umgebungen Kompatibilitätsprobleme mit der Kodierung aufweisen. Falls es nicht angezeigt wird, hat dies keinen Einfluss auf das Verständnis der Beispielschritte.