10. Another Custom Strategy

qteasy is a fully localized deployment and operation of quantitative trading analysis toolkit, with the following functions:

  • Acquisition, cleaning, storage, processing, visualization, and use of financial data

  • Create quantitative trading strategies and provide a large number of built-in basic trading strategies

  • Vectorized high-speed trading strategy backtesting and trading result evaluation

  • Optimization and evaluation of trading strategy parameters

  • Deployment and live operation of trading strategies

In this tutorial, you will fully understand the main functions and usage of qteasy through a series of practical examples.

10.1. Before you start

Please make sure you have mastered the following content before starting this tutorial:

  • Install and configure qteasy —— QTEASY Tutorial 1

  • Set up a local data source, and have downloaded enough historical data to the local —

  • Learn to create a trader object, use built-in trading strategies —— QTEASY Tutorial 3

  • Learn to use the mixer to mix multiple simple strategies into more complex trading strategies —— QTEASY Tutorial 4

  • Learn how to customize trading strategies —— QTEASY Tutorial 5

QTEASY文档中,还能找到更多关于使用内置交易策略、创建自定义策略等等相关内容。对qteasy的基本使用方法还不熟悉的同学,可以移步那里查看更多详细说明。

10.2. Target of this tutorial

In this section, we will continue the content of the previous section and introduce the trading strategy base class of qteasy. After introducing the simplest timing trading strategy class, we will introduce how to use the other two strategy base classes provided by qteasy to create a multi-factor stock selection strategy.

In order to provide sufficient ease of use, the various strategy base classes provided by qteasy are essentially the same, just a pre-processing form provided to reduce the user’s coding workload, and even different trading strategy base classes can be understood as “syntax sugar” designed for specific trading strategies. Therefore, the same trading strategy can often be implemented using multiple different trading strategy base classes. Therefore, in this section, we will use two different strategy base classes to implement an Alpha stock selection trading strategy.

10.3. How Alpha stock selection strategy works

The Alpha stock selection strategy we are discussing here is a low-frequency stock selection strategy that can run weekly or monthly. Each time the stock is selected, it will traverse all the constituent stocks of the HS300 index, and prioritize these 300 stocks according to certain criteria. Select the top 30 stocks from them, hold them equally, that is, rebalance the stocks once a month, sell the stocks with lower rankings during the rebalancing, buy the stocks with higher rankings, and ensure that the stocks are held in equal shares.

The ranking basis of the Alpha stock selection strategy is calculated from two financial indicators for each stock: EV (enterprise value) and EBITDA (earnings before interest, taxes, depreciation, and amortization). For each stock, compute the ratio of EV to EBITDA. When this ratio is greater than 0, it indicates that the listed company is profitable (because EBITDA is positive). In this case, the ratio represents the total enterprise value that needs to be invested for the company to earn one yuan of profit. Naturally, the lower this ratio, the better. For example, the data for the following two listed companies are as follows:

  • Company A has an EBITDA of ten million, and an enterprise market value of ten billion. EV/EBITDA = 1000. This indicates that for every 1,000 yuan of the company’s market value, it can earn 1 yuan in profit.

  • Company B also has an EBITDA of ten million, and an enterprise market value of one hundred billion. EV/EBITDA = 10000, indicating that for every 10,000 yuan of the company’s market value, it can earn 1 yuan in profit.

Normally, we naturally feel that Company A is better because it earns the same profit with less company market value. At this time, we think that Company A’s ranking is relatively high.

According to the above rules, at the end of each month, we will rank all listed companies of the HS300 constituent stocks from small to large, and remove companies with EV/EBITDA less than 0 (of course, companies with negative profits should be removed). After that, select the top 30 companies to hold, which is the Alpha stock selection trading strategy.

In fact, for a stock-picking strategy like this that ranks by indicators, qteasy provides a built-in trading strategy that can implement it directly.

Use built_in_doc to view the documentation for this built-in trading strategy:

>>> import qteasy as qt
>>> qt.built_in_doc('finance', print_out=True)

The output is as follows:

以股票过去一段时间内的财务指标的平均值作为选股因子选股
        基础选股策略。以股票的历史指标的平均值作为选股因子,因子排序参数可以作为策略参数传入
        改变策略数据类型,根据不同的历史数据选股,选股参数可以通过pars传入
    策略参数:
        - sort_ascending: enum, 是否升序排列因子
            - True: 优先选择因子最小的股票,
            - False, 优先选择因子最大的股票
        - weighting: enum, 股票仓位分配比例权重
            - even       :默认值, 所有被选中的股票都获得同样的权重
            - linear     :权重根据因子排序线性分配
            - distance   :股票的权重与他们的指标与最低之间的差值(距离)成比例
            - proportion :权重与股票的因子分值成正比
        - condition: enum, 股票筛选条件
            - any        :默认值,选择所有可用股票
            - greater    :筛选出因子大于ubound的股票
            - less       :筛选出因子小于lbound的股票
            - between    :筛选出因子介于lbound与ubound之间的股票
            - not_between:筛选出因子不在lbound与ubound之间的股票
        - lbound: float, 股票筛选下限值, 默认值np.-inf
        - ubound: float, 股票筛选上限值, 默认值np.inf
        - max_sel_count: float, 抽取的股票的数量(p>=1)或比例(p<1), 默认值: 0.5,表示选中50%的股票
    信号类型:
        PT型: 百分比持仓比例信号
    信号规则:
        使用data_types指定一种数据类型,将股票过去的datatypes数据取平均值,将该平均值作为选股因子进行选股
    策略属性缺省值:
        默认参数: (True, 'even', 'greater', 0, 0, 0.25)
        数据类型: eps 每股收益,单数据输入
        窗口长度: 270
        参数范围: [(True, False),
        ('even', 'linear', 'proportion'),
        ('any', 'greater', 'less', 'between', 'not_between'),
        (-np.inf, np.inf),
        (-np.inf, np.inf),
        (0, 1.)]
    策略不支持参考数据,不支持交易数据

However, this built-in trading strategy only supports using qteasy built-in historical data types as stock selection factors. For example, data such as PE (price-to-earnings ratio) and profit are built-in historical data in qteasy and can be referenced directly. But if the stock selection factor cannot be found in qteasy’s built-in historical data, you cannot use the built-in trading strategy directly. The EV/EBITDA indicator is a calculated indicator, so we must use a custom trading strategy and compute this indicator within the custom strategy.

10.4. Calculate stock selection indicators

To calculate EV/EBITDA, we must at least confirm whether qteasy has provided EV and EBITDA these two types of historical data:

We can use the API: find_history_data() to check whether a historical data type is supported by qteasy. First, look up ebitda.

>>> qt.find_history_data('ebitda')

The output is as follows:

matched following history data, 
use "qt.get_history_data()" to load these historical data by its data_id:
------------------------------------------------------------------------
              freq asset      table                  desc
data_id                                                  
ebitda           q     E  financial  上市公司财务指标 - 息税折旧摊销前利润
========================================================================
['income_ebitda', 'ebitda']

Data types in qteasy need to be created via qteasy.DataType. DataType represents a kind of historical data type—i.e., a category of information that qteasy can extract directly from historical data. Through data type objects, qteasy provides a unified data interface, allowing users to easily obtain various kinds of historical data without worrying about how the data type is stored or where it is stored. At the same time, qteasy fully encapsulates all the complex underlying data logic such as type handling, frequency conversion, and ticker matching, so users don’t need to care about the storage method of each kind of data and can use it directly.

关于qteasy数据类型的更详细介绍,请参见QTEASY文档

qteasy’s DataType includes three attributes:

  • name: the name of the data type, for example ebitda in the return value above

  • freq: the frequency of the data, for example q in the return value above, which represents quarterly data

  • asset_type: the asset type of the data; for example, E in the return value above represents stock data

These three attributes together define a unique data type. qteasy has a large number of built-in historical data types. Users can directly use these data types to obtain historical data, without having to calculate them themselves or process raw data to derive these data types.

From the return value above, we can see that in qteasy’s built-in historical data types, EBITDA is a standard historical data type. This data comes from the listed companies’ financial indicators table financial, and the data frequency is q (quarterly):

Next, look at EV:

>>> qt.find_history_data('ev')

The output is as follows:

matched following history data, 
use "qt.get_history_data()" to load these historical data by its data_id:
------------------------------------------------------------------------
Empty DataFrame
Columns: [freq, asset_type, table_name, description]
Index: []
========================================================================

This indicates that among qteasy’s built-in historical data types, no historical data type named EV was found; in this case, you can use the parameter fuzzy=True to confirm.

>>> qt.find_history_data('ev', fuzzy=True)

The output is as follows:

matched following history data, 
use "qt.get_history_data()" to load these historical data by its data_id:
------------------------------------------------------------------------
                                    name  freq asset              table               column                desc
data_id                                                                                                         
sw_level                        sw_level  None   IDX  sw_industry_basic                level         申万行业分类 - 级别
sw_level|%                    sw_level|%  None   IDX  sw_industry_basic                level        申万行业分类筛选 - %
managers_lev                managers_lev     d     E       stk_managers                  lev       公司高管信息 - 岗位类别
total_revenue              total_revenue     q     E             income        total_revenue     上市公司利润表 - 营业总收入
revenue                          revenue     q     E             income              revenue      上市公司利润表 - 营业收入
withdra_biz_devfund  withdra_biz_devfund     q     E             income  withdra_biz_devfund  上市公司利润表 - 提取企业发展基金
express_revenue          express_revenue     q     E            express              revenue  上市公司业绩快报 - 营业收入(元)
total_revenue_ps        total_revenue_ps     q     E          financial     total_revenue_ps  上市公司财务指标 - 每股营业总收入
revenue_ps                    revenue_ps     q     E          financial           revenue_ps   上市公司财务指标 - 每股营业收入
========================================================================

The table above lists the data types that have already been defined in qteasy and can be used directly. Please pay attention to the name / freq / asset columns, which represent the data type name, data frequency, and asset type, respectively. Together, these three columns define a unique data type. Users can create a data type object via qteasy.DataType(name, freq, asset) to obtain the historical data of that data type.

Although EV is not among qteasy’s built-in historical data types, we can see that there are some historical data types related to EV, such as total revenue, earnings per share, and so on. These data types are related to EV, but they are not the EV we need.

However, we know that EV can be calculated using the following formula:

\[::\]

And the above financial indicators are directly supported by qteasy

  • Total Market Value - Data Type: total_mv

  • Total Liabilities - Data Type: total_liab

  • Total Cash - Data Type: c_cash_equ_end_period

So we can test it and look at the detailed explanations of these data types:

>>> qt.find_history_data('total_mv', fuzzy=True)

You will get the following output:

matched following history data, 
use "qt.get_history_data()" to load these historical data by its data_id:
------------------------------------------------------------------------
                      name freq asset             table    column                 desc
data_id                                                                               
ths_total_mv  ths_total_mv    d   IDX   ths_index_daily  total_mv  同花顺指数日K线 - 总市值 (万元)
sw_total_mv    sw_total_mv    d   IDX    sw_index_daily  total_mv   申万指数日K线 - 总市值 (万元)
total_mv          total_mv    d   IDX   index_indicator  total_mv    指数技术指标 - 当日总市值(元)
total_mv          total_mv    d     E   stock_indicator  total_mv    股票技术指标 - 总市值 (万元)
total_mv_2      total_mv_2    d     E  stock_indicator2  total_mv     股票技术指标 - 总市值(亿元)
========================================================================

Note that the total_mv data type has two versions: one in units of 10,000 yuan and one in units of 100 million yuan. When calculating EV/EBITDA, strictly speaking the unit is not important, but in other cases you need to pay attention. Here we multiply this data by 10,000 to unify the units.

Here we choose DataType('total_mv', 'd', 'E'). This data type represents the total market capitalization of a listed company on each day, in units of 10,000 yuan.

>>> qt.find_history_data('total_liab', fuzzy=True)
matched following history data, 
use "qt.get_history_data()" to load these historical data by its data_id:
------------------------------------------------------------------------
                                    name freq asset    table               column                   desc
data_id                                                                                                 
total_liab                    total_liab    q     E  balance           total_liab       上市公司资产负债表 - 负债合计
total_liab_hldr_eqy  total_liab_hldr_eqy    q     E  balance  total_liab_hldr_eqy  上市公司资产负债表 - 负债及股东权益总计
========================================================================

Here we can choose the data type DataType('total_liab', 'q', 'E'). This data type represents the total liabilities of a listed company at the end of each quarter, in units of yuan.

>>> qt.find_history_data('cash', fuzzy=True)
matched following history data, 
use "qt.get_history_data()" to load these historical data by its data_id:
------------------------------------------------------------------------
                                                      name freq asset      table                        column                                desc
data_id                                                                                                                                           
cash_reser_cb                                cash_reser_cb    q     E    balance                 cash_reser_cb             上市公司资产负债表 - 现金及存放中央银行款项
ifc_cash_incr                                ifc_cash_incr    q     E   cashflow                 ifc_cash_incr            上市公司现金流量表 - 收取利息和手续费净增加额
oth_cash_pay_oper_act                oth_cash_pay_oper_act    q     E   cashflow         oth_cash_pay_oper_act          上市公司现金流量表 - 支付其他与经营活动有关的现金
st_cash_out_act                            st_cash_out_act    q     E   cashflow               st_cash_out_act              上市公司现金流量表 - 经营活动现金流出小计
n_cashflow_act                              n_cashflow_act    q     E   cashflow                n_cashflow_act           上市公司现金流量表 - 经营活动产生的现金流量净额
n_cashflow_inv_act                      n_cashflow_inv_act    q     E   cashflow            n_cashflow_inv_act           上市公司现金流量表 - 投资活动产生的现金流量净额
oth_cash_recp_ral_fnc_act        oth_cash_recp_ral_fnc_act    q     E   cashflow     oth_cash_recp_ral_fnc_act          上市公司现金流量表 - 收到其他与筹资活动有关的现金
stot_cash_in_fnc_act                  stot_cash_in_fnc_act    q     E   cashflow          stot_cash_in_fnc_act              上市公司现金流量表 - 筹资活动现金流入小计
free_cashflow                                free_cashflow    q     E   cashflow                 free_cashflow                上市公司现金流量表 - 企业自由现金流量
oth_cashpay_ral_fnc_act            oth_cashpay_ral_fnc_act    q     E   cashflow       oth_cashpay_ral_fnc_act          上市公司现金流量表 - 支付其他与筹资活动有关的现金
stot_cashout_fnc_act                  stot_cashout_fnc_act    q     E   cashflow          stot_cashout_fnc_act              上市公司现金流量表 - 筹资活动现金流出小计
n_cash_flows_fnc_act                  n_cash_flows_fnc_act    q     E   cashflow          n_cash_flows_fnc_act           上市公司现金流量表 - 筹资活动产生的现金流量净额
eff_fx_flu_cash                            eff_fx_flu_cash    q     E   cashflow               eff_fx_flu_cash              上市公司现金流量表 - 汇率变动对现金的影响
n_incr_cash_cash_equ                  n_incr_cash_cash_equ    q     E   cashflow          n_incr_cash_cash_equ            上市公司现金流量表 - 现金及现金等价物净增加额
c_cash_equ_beg_period                c_cash_equ_beg_period    q     E   cashflow         c_cash_equ_beg_period            上市公司现金流量表 - 期初现金及现金等价物余额
c_cash_equ_end_period                c_cash_equ_end_period    q     E   cashflow         c_cash_equ_end_period            上市公司现金流量表 - 期末现金及现金等价物余额
incl_cash_rec_saims                    incl_cash_rec_saims    q     E   cashflow           incl_cash_rec_saims     上市公司现金流量表 - 其中:子公司吸收少数股东投资收到的现金
im_net_cashflow_oper_act          im_net_cashflow_oper_act    q     E   cashflow      im_net_cashflow_oper_act      上市公司现金流量表 - 经营活动产生的现金流量净额(间接法)
im_n_incr_cash_equ                      im_n_incr_cash_equ    q     E   cashflow            im_n_incr_cash_equ       上市公司现金流量表 - 现金及现金等价物净增加额(间接法)
net_cash_rece_sec                        net_cash_rece_sec    q     E   cashflow             net_cash_rece_sec        上市公司现金流量表 - 代理买卖证券收到的现金净额(元)
cashflow_credit_impa_loss        cashflow_credit_impa_loss    q     E   cashflow              credit_impa_loss                  上市公司现金流量表 - 信用减值损失
end_bal_cash                                  end_bal_cash    q     E   cashflow                  end_bal_cash                 上市公司现金流量表 - 现金的期末余额
beg_bal_cash                                  beg_bal_cash    q     E   cashflow                  beg_bal_cash               上市公司现金流量表 - 减:现金的期初余额
end_bal_cash_equ                          end_bal_cash_equ    q     E   cashflow              end_bal_cash_equ            上市公司现金流量表 - 加:现金等价物的期末余额
beg_bal_cash_equ                          beg_bal_cash_equ    q     E   cashflow              beg_bal_cash_equ            上市公司现金流量表 - 减:现金等价物的期初余额
cash_ratio                                      cash_ratio    q     E  financial                    cash_ratio                   上市公司财务指标 - 保守速动比率
salescash_to_or                            salescash_to_or    q     E  financial               salescash_to_or       上市公司财务指标 - 销售商品提供劳务收到的现金/营业收入
cash_to_liqdebt                            cash_to_liqdebt    q     E  financial               cash_to_liqdebt                上市公司财务指标 - 货币资金/流动负债
cash_to_liqdebt_withinterest  cash_to_liqdebt_withinterest    q     E  financial  cash_to_liqdebt_withinterest              上市公司财务指标 - 货币资金/带息流动负债
q_salescash_to_or                        q_salescash_to_or    q     E  financial             q_salescash_to_or  上市公司财务指标 - 销售商品提供劳务收到的现金/营业收入(单季度)
cash_div_planned                          cash_div_planned    d     E   dividend                      cash_div                         预案-每股分红(税后)
cash_div_tax_planned                  cash_div_tax_planned    d     E   dividend                  cash_div_tax                         预案-每股分红(税前)
cash_div_approved                        cash_div_approved    d     E   dividend                      cash_div                     股东大会批准-每股分红(税后)
cash_div_tax_approved                cash_div_tax_approved    d     E   dividend                  cash_div_tax                     股东大会批准-每股分红(税前)
cash_div                                          cash_div    d     E   dividend                      cash_div                         实施-每股分红(税后)
cash_div_tax                                  cash_div_tax    d     E   dividend                  cash_div_tax                         实施-每股分红(税前)
========================================================================

There are many data types related to cash, but the total cash and cash equivalents we need is DataType(c_cash_equ_end_period, 'q', 'E'). This data type represents the total cash and cash equivalents of a listed company at the end of each quarter.

Based on the information above, we can choose the following four data types to calculate EV:

  • DataType('total_mv', 'd', 'E'), this data type represents the total market capitalization of a listed company on each day, in units of 10,000 yuan.

  • DataType('total_liab', 'q', 'E'): this data type represents a listed company’s total liabilities at the end of each quarter, in yuan.

  • DataType('c_cash_equ_end_period', 'q', 'E'): this data type represents a listed company’s total cash and cash equivalents at the end of each quarter, in yuan.

We can run a quick test. qteasy provides a very convenient API: get_history_data(), which can directly retrieve historical data for these data types:

# 创建数据类型对象
dtypes = [DataType('total_mv', freq='d', asset_type='E'),
          DataType('total_liab', freq='q', asset_type='E'),
          DataType('c_cash_equ_end_period', freq='q', asset_type='E'),
          DataType('ebitda', freq='q', asset_type='E')]
# 获取沪深300指数成分股(这里只获取前20支股票)
shares = qt.filter_stock_codes(index='000300.SH', date='20220131')[:20] 
# 获取所有股票的总市值、总负债、总现金、EBITDA数据
dt = qt.get_history_data(data_types=dtypes, shares=shares, asset_type='any', freq='m')
# 随便选择一支股票,转化为DataFrame检查数据是否正确获取
one_share = shares[1]
df = dt[one_share]
# 计算EV/EBITDA选股因子
df['ev_to_ebitda'] = (df.total_mv + df.total_liab - df.c_cash_equ_end_period) / df.ebitda
print(df)
                total_mv    total_liab  c_cash_equ_end_period        ebitda  \
2022-01-04  2.382041e+07           NaN                    NaN           NaN   
2022-01-05  2.461094e+07           NaN                    NaN           NaN   
2022-01-06  2.447143e+07           NaN                    NaN           NaN   
2022-01-07  2.544796e+07           NaN                    NaN           NaN   
2022-01-10  2.576185e+07           NaN                    NaN           NaN   
...                  ...           ...                    ...           ...   
2022-12-26  2.136561e+07  1.426656e+12           1.158051e+11  2.969171e+10   
2022-12-27  2.152844e+07  1.426656e+12           1.158051e+11  2.969171e+10   
2022-12-28  2.160986e+07  1.426656e+12           1.158051e+11  2.969171e+10   
2022-12-29  2.112137e+07  1.426656e+12           1.158051e+11  2.969171e+10   
2022-12-30  2.116789e+07  1.426656e+12           1.158051e+11  2.969171e+10   

            ev_to_ebitda  
2022-01-04           NaN  
2022-01-05           NaN  
2022-01-06           NaN  
2022-01-07           NaN  
2022-01-10           NaN  
...                  ...  
2022-12-26     51.344518  
2022-12-27     51.399358  
2022-12-28     51.426778  
2022-12-29     51.262258  
2022-12-30     51.277926  

[242 rows x 5 columns]

We can see that the stock selection factor has been calculated, so we can start defining the trading strategy.

10.5. Define Alpha stock selection strategy with FactorSorter

qteasy provides the FactorSorter trading strategy class for this type of timed stock selection trading strategy. As the name suggests, this trading strategy base class allows users to calculate a set of stock selection factors in the implementation method of the strategy, so that the strategy can automatically sort all stocks according to the value of the stock selection factor, and select the stocks with higher rankings. As for the sorting method, screening rules, stock holding weights, etc., can be set through strategy parameters.

If it meets the definition of the trading strategy above, using the FactorSorter strategy base class will be very convenient.

Next, let’s define it step by step. First, inherit FactorSorter and define a class. In the previous section, we defined the name, description, default parameters, and other information in the __init__() method of the custom strategy. However, we can also ignore the __init__() method and only pass in parameters when creating the strategy object. This is also possible. We do it here:

>>> class AlphaFac(qt.FactorSorter):  # 注意这里使用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 * 10000 + total_liab - cash_equ) / ebitda
...         return factor  # 直接返回选股因子,策略就定义好了

Same as in the previous section, the first step in realize() is to fetch historical data. We know the historical data includes four types: total_mv, total_liab, c_cash_equ_end_period, ebitda. These four historical data series will be defined as four DataTypes and passed into the strategy. To use these historical data in the strategy, you can directly call self.get_data():

# 从历史数据编码中读取四种历史数据的最新数值
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,息税折旧摊销前利润
        ...

The self.get_data() method retrieves the corresponding historical data via each data item’s data ID. By default, the ID of each data item is a combination of its name, asset type, and frequency, for example:

  • DataType('total_mv', 'd', 'E'): the ID of this data type is total_mv_E_d.

  • DataType('total_liab', 'q', 'E'): the ID of this data type is: total_liab_E_d.

Before the trading strategy starts running, qteasy will prepare the corresponding trading data. All trading data are stored in a numpy array: the number of columns corresponds to the number of stocks in the universe, and each column corresponds to one stock in the universe; the number of rows corresponds to the length of the time window, each row corresponds to a time point in the time window and is sorted in ascending order, and the last column represents the latest historical data visible at the time of trading.

Following this rule, to get the most recent historical data that can be seen on the trading day for the i-th stock, just access array(-1, i). With the loop below, you can access the data for all stocks at the same point in time:

total_mv = self.get_data('total_mv_E_d')
# 循环访问每一支股票的total_mv
for i in len(total_mv[-1]):
    print(f'total mv of share {i}: {total_mv[-1, i]}')

However, using a for-loop to access data is relatively inefficient. You should use vectorized operations as much as possible in your strategy to save time.

If you are ready for the above, it is very convenient to calculate the stock selection factor. Moreover, since we use the FactorSorter strategy base class, after calculating the stock selection factor, you can directly return the stock selection factor, and qteasy will handle the rest of the stock selection operation:

# 选股因子为EV/EBIDTA,使用下面公式计算
factor = (total_mv * 10000 + total_liab - cash_equ) / ebitda
return factor  # 直接返回选股因子,策略就定义好了

Now, with just six lines of code, a custom Alpha stock selection trading strategy is defined. Isn’t it very simple?

OK, let’s see how the backtest results are?

10.6. Results of backtesting trading strategies

Since we ignored the __init__() method of the strategy class, the complete strategy parameters must be entered when instantiating the strategy object:

>>> from qteasy import Parameter, StgData

>>> alpha = AlphaFac(
...     pars=[],
...     name='AlphaSel',
...     description='本策略每隔1个月定时触发计算SHSE.000300成份股的过去的EV/EBITDA并选取EV/EBITDA大于0的股票',
...     data_types=[DataType('total_mv', freq='d', asset_type='E'),
...                 DataType('total_liab', freq='q', asset_type='E'),
...                 DataType('c_cash_equ_end_period', freq='q', asset_type='E'), 
...                 DataType('ebitda', freq='q', asset_type='E')],
...     window_length=[20, 20, 10, 10],  # 现在可以为每一种数据类型设置不同的窗口长度
...     max_sel_count=30,  # 设置选股数量,最多选出30个股票
...     condition='greater',  # 设置筛选条件,仅筛选因子大于ubound的股票
...     ubound=0.0,  # 设置筛选条件,仅筛选因子大于0的股票
...     weighting='even',  # 设置股票权重,所有选中的股票平均分配权重
...     sort_ascending=True,  # 设置排序方式,因子从小到大排序选择头30名
... )  

Then create an Operator object, because we want to control the position ratio, it is best to use the ‘PT’ signal type:

>>> op = qt.Operator(alpha, signal_type='PT')
>>> res = op.run(mode=1,
...        asset_type='E',
...        asset_pool=shares,
...        PT_buy_threshold=0.0,
...        PT_sell_threshold=0.0,
...        trade_batch_size=100,
...        sell_batch_size=1)

Results of backtesting:

     ====================================
     |                                  |
     |       BACK TESTING RESULT        |
     |                                  |
     ====================================

qteasy running mode: 1 - History back testing
time consumption for operate signal creation: 9.4ms
time consumption for operation back looping:  5s 831.0ms

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        2       3   10.3%      0.0%     89.7%  
000786.SZ    2        3       5   27.5%      0.0%     72.5%  
000895.SZ    1        0       1   62.6%      0.0%     37.4%  
002001.SZ    2        2       4   55.8%      0.0%     44.2%  
002007.SZ    3        1       4   68.3%      0.0%     31.7%  
002027.SZ    2        9      11   41.3%      0.0%     58.7%  
002032.SZ    2        0       2    5.9%      0.0%     94.1%  
002044.SZ    1        1       2    1.8%      0.0%     98.2%  
002049.SZ    1        1       2    5.1%      0.0%     94.9%  
002050.SZ    4        5       9   13.8%      0.0%     86.2%  
...            ...     ...   ...      ...       ...       ...
603517.SH    1        1       2    1.8%      0.0%     98.2%  
603806.SH    6        3       9   39.8%      0.0%     60.2%  
603899.SH    1        1       2   31.0%      0.0%     69.0%  
000408.SZ    3        6       9   35.5%      0.0%     64.5%  
002648.SZ    1        1       2    5.2%      0.0%     94.8%  
002920.SZ    1        1       2    1.7%      0.0%     98.3%  
300223.SZ    1        1       2    5.2%      0.0%     94.8%  
600219.SH    1        1       2    6.1%      0.0%     93.9%  
603185.SH    1        1       2    5.2%      0.0%     94.8%  
688005.SH    1        1       2    5.2%      0.0%     94.8%   

Total operation fee:     ¥      928.22
total investment amount: ¥  100,000.00
final value:              ¥  159,072.14
Total return:                    59.07% 
Avg Yearly return:               10.09%
Skewness:                         -0.28
Kurtosis:                          3.29
Benchmark return:                65.96% 
Benchmark Yearly return:         11.06%

------strategy loop_results indicators------ 
alpha:                           -0.012
Beta:                             1.310
Sharp ratio:                      1.191
Info ratio:                      -0.010
250 day volatility:               0.105
Max drawdown:                    20.49% 
    peak / valley:        2018-05-22 / 2019-01-03
    recovered on:         2019-12-26

===========END OF REPORT=============

Backtesting results show that this strategy cannot effectively outperform the Shanghai and Shenzhen 300 Index, but overall, the drawdown is relatively small and the risk is low, making it a good bottom strategy.

But the performance of the strategy is not the focus of our discussion. Next, let’s take a look at how to define the same Alpha stock selection strategy without using the FactorSorter base class.

10.7. Define an Alpha stock selection strategy with GeneralStg

We have introduced two strategy base classes before:

  • RuleIterator: Users only need to define stock selection rules for a stock, and qteasy will apply the same rules to all stocks in the stock pool, and can also set different adjustable parameters for different stocks

  • FactorSorter:Users only need to define a stock selection factor, and qteasy will automatically select the best stocks to hold based on the stock selection factor after sorting, and sell the stocks that are not qualified.

While GeneralStg is a basic strategy base class provided by qteasy, it does not provide any “syntax sugar” functions to help users reduce coding workload, but precisely because it does not have syntax sugar, it is a real “universal” strategy class that can be used to create trading strategies more freely.

Above Alpha stock selection trading strategy can be easily implemented with FactorSorter, but to understand GeneralStg, let’s see how to use it to create the same strategy:

Here is the complete code:


class AlphaPT(qt.GeneralStg):
    
    def realize(self, h, r=None, t=None, pars=None):

        # 从历史数据编码中读取四种历史数据的最新数值
        total_mv = h[:, -1, 0]  # 总市值
        total_liab = h[:, -1, 1]  # 总负债
        cash_equ = h[:, -1, 2]  # 现金及现金等价物总额
        ebitda = h[:, -1, 3]  # 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个股票的序号
        not_selected = arg_partitioned[30:]  # 未被选中的其他股票的序号(包括因子为NaN的股票)
        
        # 开始生成PT交易信号
        signal = np.zeros_like(factors)
        # 所有被选中的股票的持仓目标被设置为0.03,表示持有3.3%
        signal[selected] = 0.0333
        # 其余未选中的所有股票持仓目标在PT信号模式下被设置为0,代表目标仓位为0
        signal[not_selected] = 0  
        
        return signal    

Compare the above code with the code of FactorSorter, you can find that the code of GeneralStg has more factor processing work after calculating the stock selection factor:

  • Remove factors less than zero

  • Sort and select the smallest 30 of the remaining factors

  • Set the position ratio of the selected stocks to 3.3%

In fact, all of the above work is the “syntax sugar” provided by FactorSorter, which we must manually implement here. It is worth noting that the sorting and other codes I used in the above examples are highly optimized numpy codes directly extracted from FactorSorter, and their running speed is very fast, much faster than the code that general users can write. Therefore, as long as the conditions permit, users should try to use these syntax sugars as much as possible, and only write sorting code when necessary.

You may study the above code, but please note that if you use the GeneralStg strategy class, the output of the strategy should be the target position of the stock, not the stock selection factor.

Here is the backtesting results:

10.8. Backtesting results:

Backtesting with the same data:

alpha = AlphaPT(pars=(),
                 par_count=0,
                 par_types=[],
                 par_range=[],
                 name='AlphaSel',
                 description='本策略每隔1个月定时触发计算SHSE.000300成份股的过去的EV/EBITDA并选取EV/EBITDA大于0的股票',
                 data_types='total_mv, total_liab, c_cash_equ_end_period, ebitda',
                 run_freq='m',
                 data_freq='d',
                 window_length=100)
op = qt.Operator(alpha, signal_type='PT')
res = op.run(mode=1,
             asset_type='E',
             asset_pool=shares,
             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
            )

Results of backtesting:

     ====================================
     |                                  |
     |       BACK TESTING RESULT        |
     |                                  |
     ====================================

qteasy running mode: 1 - History back testing
time consumption for operate signal creation: 7.2ms
time consumption for operation back looping:  6s 308.5ms

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        3       5   27.5%      0.0%     72.5%  
000895.SZ    1        1       2   68.7%      0.0%     31.3%  
002001.SZ    2        2       4   57.5%      0.0%     42.5%  
002007.SZ    0        1       1   68.3%      0.0%     31.7%  
002027.SZ    6        7      13   41.3%      0.0%     58.7%  
002032.SZ    3        1       4    7.5%      0.0%     92.5%  
002044.SZ    1        1       2    1.8%      0.0%     98.2%  
002049.SZ    1        1       2    5.1%      0.0%     94.9%  
002050.SZ    4        4       8   13.8%      0.0%     86.2%  
...            ...     ...   ...      ...       ...       ...
603806.SH    5        3       8   62.1%      0.0%     37.9%  
603899.SH    2        3       5   36.3%      0.0%     63.7%  
000408.SZ    3        5       8   35.5%      0.0%     64.5%  
002648.SZ    1        1       2    5.2%      0.0%     94.8%  
002920.SZ    1        1       2    5.1%      0.0%     94.9%  
300223.SZ    1        2       3    5.2%      0.0%     94.8%  
300496.SZ    1        1       2   10.5%      0.0%     89.5%  
600219.SH    1        1       2    6.1%      0.0%     93.9%  
603185.SH    1        1       2    5.2%      0.0%     94.8%  
688005.SH    1        2       3    5.2%      0.0%     94.8%   

Total operation fee:     ¥      985.25
total investment amount: ¥  100,000.00
final value:              ¥  189,723.44
Total return:                    89.72% 
Avg Yearly return:               14.18%
Skewness:                         -0.41
Kurtosis:                          2.87
Benchmark return:                65.96% 
Benchmark Yearly return:         11.06%

------strategy loop_results indicators------ 
alpha:                            0.044
Beta:                             1.134
Sharp ratio:                      1.284
Info ratio:                       0.011
250 day volatility:               0.120
Max drawdown:                    20.95% 
    peak / valley:        2018-05-22 / 2019-01-03
    recovered on:         2019-09-09

===========END OF REPORT=============

Results of the two trading strategies are basically the same

10.9. Recap

We have learned the usage of the other two trading strategy base classes FactorSorter and GeneralStg provided by qteasy in this section, and actually created two trading strategies. Although different base classes are used, the same Alpha stock selection trading strategy is created.

In the next section, we will continue to introduce custom trading strategies, but will use a more complex example to demonstrate the usage of custom trading strategies. Stay tuned!