10. 創建自定義因子選股交易策略

qteasy是一個完全本地化部署和運行的量化交易分析工具包,具備以下功能:

  • 金融數據的獲取、清洗、存儲以及處理、可視化、使用

  • 量化交易策略的創建,並提供大量內置基本交易策略

  • 向量化的高速交易策略回測及交易結果評價

  • 交易策略參數的優化以及評價

  • 交易策略的部署、實盤運行

通過本系列教程,您將會通過一系列的實際示例,充分了解qteasy的主要功能以及使用方法。

10.1. 開始前的準備工作

在開始本節教程前,請先確保您已經掌握了下面的內容:

  • 安裝、配置qteasy —— QTEASY教程1

  • 設置了一個本地數據源,並已經將足夠的歷史數據下載到本地——QTEASY教程2

  • 學會創建交易員對象,使用內置交易策略,——QTEASY教程3

  • 學會使用混合器,將多個簡單策略混合成較爲複雜的交易策略——QTEASY教程4

  • 瞭解如何自定義交易策略——QTEASY教程5

QTEASY文檔中,還能找到更多關於使用內置交易策略、創建自定義策略等等相關內容。對qteasy的基本使用方法還不熟悉的同學,可以移步那裏查看更多詳細說明。

10.2. 本節的目標

在本節中,我們將承接上一節開始的內容,介紹qteasy的交易策略基類,在介紹過一個最簡單的擇時交易策略類以後,我們將介紹如何使用qteasy提供的另外兩種策略基類,創建一個多因子選股策略。

爲了提供足夠的使用便利性,qteasy的提供的各種策略基類本質上並無區別,只是爲了減少用戶編碼工作量而提供的預處理形式,甚至可以將不同的交易策略基類理解成,爲了特定交易策略設計的“語法糖”,因此,同一交易策略往往可以用多種不同的交易策略基類實現,因此,在本節中,我們將用兩種不同的策略基類來實現一個Alpha選股交易策略。

10.3. Alpha選股策略的選股思想

我們在這裏討論的Alpha選股策略是一個低頻運行的選股策略,這個策略可以每週或者每月運行一次,每次選股時會遍歷HS300指數的全部成分股,依照一定的標準將這300支股票進行優先級排序,從中選擇出排位靠前的30支股票,等權持有,也就是說,每個月進行一次調倉換股,調倉時將排名靠後的股票賣掉,買入排名靠前的股票,並確保股票的持有份額相同。

Alpha選股策略的排名依據每一支股票的兩個財務指標:EV(企業市場價值)以及EBITDA(息稅折舊攤銷前利潤)來計算,對每一支股票計算EV與EBITDA的比值,當這個比值大於0的時候,說明該上市公司是盈利的(因爲EBITDA爲正)。這時,這個比值代表該公司每賺到一塊錢利潤,需要投入的企業總價值。自然,這個比值越低越好。例如,下面兩家上市公司數據如下:

  • A公司的EBITDA爲一千萬,而企業市場價值爲一百億,EV/EBITDA=1000.。說明該公司每一千元的市場價值可以掙到一元錢利潤

  • B公司的EBITDA同樣爲一千萬,企業市場價值爲一千億,EV/EBITDA=10000,說明該公司每一萬元的市場價值可以掙到一元錢利潤

從常理分析,我們自然會覺得A公司比較好,因爲靠着較少的公司市場價值,就掙到了同樣的利潤,這時我們認爲A公司的排名比較靠前。

按照上面的規則,我們在每個月的最後一天,將HS300成分股的所有上市公司全部進行一次從小到大排名,剔除掉EV/EBITDA小於0的公司(盈利爲負的公司當然應該剔除)以後,選擇排名最靠前的30個公司持有,就是Alpha選股交易策略。

其實,類似於這樣的指標排序選股策略,qteasy提供了一個內置交易策略可以直接實現。

使用built_in_doc來查看這個內置交易策略的文檔:

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

輸出如下:

以股票过去一段时间内的财务指标的平均值作为选股因子选股
        基础选股策略。以股票的历史指标的平均值作为选股因子,因子排序参数可以作为策略参数传入
        改变策略数据类型,根据不同的历史数据选股,选股参数可以通过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.)]
    策略不支持参考数据,不支持交易数据

不過這個內置交易策略僅支持以qteasy內置歷史數據類型爲選股因子,例如pe市盈率、profit利潤等數據是qteasy的內置歷史數據,可以直接引用。但如果是qteasy內置歷史數據中找不到的選股因子,就不能直接使用內置交易策略了。EV/EBITDA這個指標是一個計算指標,因此,我們必須使用自定義交易策略。並在自定義策略中計算該指標。

10.4. 計算選股指標

爲了計算EV/EBITDA,我們必須至少先確認qteasy中是否已經提供了EV和EBITDA這兩種歷史數據:

我們可以使用API:find_history_data()來查看歷史數據類型是否被qteasy支持,首先查找 ebitda

>>> qt.find_history_data('ebitda')

輸出如下:

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

qteasy中的數據類型需要通過qteasy.DataType來創建,DataType代表了一種歷史數據類型,它代表了qteasy可以直接從歷史數據中提取出的一類資訊,通過數據類型對象,qteasy提供了統一的數據接口,使得用戶可以非常容易地獲取到各種歷史數據資訊,而不需要關心數據類型的存儲方式、存儲位置;同時,qteasy完全封裝了所有數據類型的類型處理、頻率轉換、股票代碼匹配等等非常繁雜的底層數據邏輯,使得用戶可以完全不用關心每一種數據的存儲方式、直接使用即可。

關於qteasy數據類型的更詳細介紹,請參見QTEASY文檔

qteasyDataType包含三個屬性:

  • name: 數據類型的名稱,例如上面返回值中的ebitda

  • freq: 數據的頻率,例如上面返回值中的q,代表季度數據

  • asset_type: 數據的資產類型,例如上面返回值中的E,代表股票數據

以上三個屬性共同定義了一種唯一的數據類型。qteasy內置了大量的歷史數據類型,用戶可以直接使用這些數據類型來獲取歷史數據,而不需要自己去計算或者處理原始數據來得到這些數據類型。

從上面的返回值可以看出,在qteasy的內置歷史數據類型中,EBITDA是一個標準的歷史數據類型,這個數據來自於上市公司財務指標表 financial, 數據頻率爲 q(季度):

接下來查看EV:

>>> qt.find_history_data('ev')

輸出如下:

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: []
========================================================================

表明在 qteasy 的內置歷史數據類型中,沒有找到名字爲EV的歷史數據類型,此時可以使用參數fuzzy=True來確認。

>>> qt.find_history_data('ev', 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                                                                                                         
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   上市公司财务指标 - 每股营业收入
========================================================================

上面這張表格列出了qteasy中已經定義好且可以直接使用的數據類型,請注意 name / freq / asset 這三列,分別代表了數據類型的名稱、數據頻率以及數據的資產類型,這三列共同定義了一個唯一的數據類型,用戶可以通過qteasy.DataType(name, freq, asset)來創建一個數據類型對象來獲取這種數據類型的歷史數據。

儘管EV並不在 qteasy 的內置歷史數據類型中,但我們可以看到有一些與EV相關的歷史數據類型,例如總收入、每股收入等等,這些數據類型雖然與EV相關,但並不是我們需要的EV。

不過,我們知道EV可以通過下面的公式計算:

\[::\]

而上面幾個財務指標都是qteasy直接支持的:

  • 總市值 - 數據類型: total_mv

  • 總負債 - 數據類型: total_liab

  • 總現金 - 數據類型: c_cash_equ_end_period

爲此我們可以測試一下,查看這些數據類型的詳細解釋:

>>> qt.find_history_data('total_mv', 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                                                                               
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     股票技术指标 - 总市值(亿元)
========================================================================

這裏需要注意的是total_mv這個數據類型有兩個版本,一個是以萬元爲單位的,一個是以億元爲單位的,我們在計算EV/EBITDA的時候,嚴格說來單位並不重要,但是在其他情況下需要注意,我們這裏將該數據乘以10000,以統一單位。

我們在這選擇DataType('total_mv', 'd', 'E'),這個數據類型代表了上市公司每天的總市值,單位爲萬元。

>>> 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  上市公司资产负债表 - 负债及股东权益总计
========================================================================

在這裏我們可以選擇數據類型DataType('total_liab', 'q', 'E'),這個數據類型代表了上市公司每個季度末的總負債,單位爲元。

>>> 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                         实施-每股分红(税前)
========================================================================

與cash相關的數據類型有很多,但我們需要的現金及現金等價物總額是DataType(c_cash_equ_end_period, 'q', 'E'),這個數據類型代表了上市公司每個季度末的現金及現金等價物總額。

根據上面的資訊,我們可以選擇下面四種數據類型來計算EV:

  • DataType('total_mv', 'd', 'E'),這個數據類型代表了上市公司每天的總市值,單位爲萬元。

  • DataType('total_liab', 'q', 'E'),這個數據類型代表了上市公司每個季度末的總負債,單位爲元。

  • DataType('c_cash_equ_end_period', 'q', 'E'),這個數據類型代表了上市公司每個季度末的現金及現金等價物總額,單位爲元。

我們可以測試一下,qteasy提供了一個非常方便的API:get_history_data(),可以直接獲取到這些數據類型的歷史數據:

# 创建数据类型对象
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]

可以看到選股因子已經計算出來了,那麼我們可以開始定義交易策略了。

10.5. FactorSorter定義Alpha選股策略

針對這種定時選股類型的交易策略,qteasy提供了FactorSorter交易策略類,顧名思義,這個交易策略基類允許用戶在策略的實現方法中計算一組選股因子,這樣策略就可以自動將所有的股票按照選股因子的值排序,並選出排名靠前的股票。至於排序方法、篩選規則、股票持倉權重等都可以通過策略參數設置。

如果符合上面定義的交易策略,使用FactorSorter策略基類將會非常方便。

下面我們就來一步步定義看看,首先繼承FactorSorter並定義一個類,在上一個章節中,我們在自定義策略的__init__()方法中定義名稱、描述以及默認參數等資訊,然而我們也可以忽略__init__()方法,僅僅在創建策略對象時傳入參數等資訊,這也是可以的,我們在這裏就這樣做:

>>> 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  # 直接返回选股因子,策略就定义好了

與上一節相同,在realize()中需要做的第一步是獲取歷史數據。我們知道歷史數據包括total_mv, total_liab, c_cash_equ_end_period, ebitda等四種,這四種歷史數據將會被定義爲四種DataType傳入策略中。在策略中要使用這些歷史數據,可以直接使用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,息税折旧摊销前利润
        ...

self.get_data()方法通過每一種數據的數據ID來獲取相應的歷史數據。而每一種數據的ID默認ID就是它的名稱、資產類型和頻率的組合,例如:

  • DataType('total_mv', 'd', 'E'),這個數據類型的ID爲 total_mv_E_d

  • DataType('total_liab', 'q', 'E'),這個數據類型的ID爲:total_liab_E_d

qteasy會在交易策略開始運行之前準備好相應的交易數據,所有的交易數據都是一個numpy數組,這個數組的行數與投資池中的股票數量相同,的每一列對應着股票池中的一支股票;而行數與時間窗口的長度相同,每一行對應着時間窗口中的一個時間點,且以升序排列,最後一列代表交易當時能看到的最新的歷史數據。

按照這個規則,如果要獲取第I支股票在交易日當天能看到的最近的歷史數據,只要訪問array(-1, i)即可,通過下面的循環即可訪問同一時間內的所有股票的數據:

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]}')

不過,使用for-loop來訪問數據的效率比較低,您最好在策略中儘量使用向量化的操作以節省時間。

做好上述準備後,計算選股因子就非常方便了,而且,由於我們使用了FactorSorter策略基類,計算好選股因子後,直接返回選股因子就可以了,qteasy會處理剩下的選股操作:

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

至此,僅僅用六行代碼,一個自定義Alpha選股交易策略就定義好了。是不是非常簡單?

好了,我們來看看回測的結果如何?

10.6. 交易策略的回測結果

由於我們忽略了策略類的__init__()方法,因此在實例化策略對象時,必須輸入完整的策略參數:

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

然後創建一個Operator對象,因爲我們希望控制持倉比例,因此最好使用“PT”信號類型:

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

回測結果如下:

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

在這裏插入圖片描述

回測結果顯示這個策略並不能非常有效地跑贏滬深300指數,不過總體來說回撤較小一些,風險較低,是一個不錯的保底策略。

但策略的表現並不是我們討論的重點,下面我們再來看一看,如果不用FactorSorter基類,如何定義同樣的Alpha選股策略。

10.7. GeneralStg定義一個Alpha選股策略

前面已經提過了兩種策略基類:

  • RuleIterator: 用戶只需要針對一支股票定義選股規則,qteasy便能將同樣的規則應用到股票池中所有的惡股票上,而且還能針對不同股票設置不同的可調參數

  • FactorSorter:用戶只需要定義一個選股因子,qteasy便能根據選股因子自動排序後選擇最優的股票持有,並賣掉不夠格的股票。

GeneralStgqteasy提供的一個最基本的策略基類,它沒有提供任何“語法糖”功能,幫助用戶降低編碼工作量,但是正是因爲沒有語法糖,它纔是一個真正的“萬能”策略類,可以用來更加自由地創建交易策略。

上面的Alpha選股交易策略可以很容易用FactorSorter實現,但爲了瞭解GeneralStg,我們來看看如何使用它來創建相同的策略:

直接把完整的代碼貼出來:


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    

將上面的代碼與FactorSorter的代碼對比,可以發現,GeneralStg的代碼在計算出選股因子以後,還多出了因子處理的工作:

  • 剔除小於零的因子

  • 排序並選出剩餘因子中最小的30個

  • 選出股票後將他們的持倉比例設置爲3.3%

事實上,上面的這些工作都是FactorSorter提供的“語法糖”,在這裏我們必須手動實現而已。值得注意的是,我在上面例子中使用的排序等代碼都是從FactorSorter中直接提取出來的高度優化的numpy代碼,它們的運行速度是很快的,比一般用戶能寫出的代碼快很多,因此,只要條件允許,用戶都應該儘量利用這些語法糖,只有在不得已的情況下才自己編寫排序代碼。

大家可以研究一下上面的代碼,但是請注意,如果使用GeneralStg策略類,策略的輸出應該是股票的目標倉位,而不是選股因子。

下面看看回測結果:

10.8. 回測結果:

使用同樣的數據進行回測:

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
            )

回測結果如下:

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

在這裏插入圖片描述

兩種交易策略的輸出結果基本相同

10.9. 本節回顧

通過本節的學習,我們瞭解了qteasy提供的另外兩種交易策略基類FactorSorterGeneralStg的使用方法,實際創建了兩個交易策略,雖然使用不同的基類,但是創建出了基本相同的Alpha選股交易策略。

在下一個章節中,我們仍然將繼續介紹自定義交易策略,但是會用一個更加複雜的例子來演示自定義交易策略的使用方法。敬請期待!