9. 使用qteasy創建自定義交易策略

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

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

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

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

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

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

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

9.1. 開始前的準備工作

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

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

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

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

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

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

9.2. 本節的目標

qteasy的內核被設計爲一個兼顧高速執行以及足夠的靈活性的框架,理論上您可以實現您所設想的任何類型的交易策略。

同時,qteasy的回測框架也做了相當多的特殊設計,可以完全避免您無意中在交易策略中導入」未來函數」,確保您的交易策略在回測時完全基於過去的數據,同時也使用了很多預處理技術以及JIT技術對內核關鍵函數進行了編譯,以實現不亞於C語言的運行速度。

不過,爲了實現理論上無限可能的交易策略,僅僅使用內置交易策略以及策略混合就不一定夠用了,一些特定的交易策略,或者一些特別複雜的交易策略是無法通過內置策略混合而成的,這就需要我們使用qteasy提供的Strategy基類,基於一定的規則創建一個自定義交易策略。

在本節中,我們將介紹qteasy的交易策略基類,通過幾個具體的例子詳細講解如何基於這幾個基類,創建一個只屬於您自己的交易策略。爲了循序漸進,我們先從一個較爲簡單的例子開始。

9.3. 自定義策略的實現方法

在量化交易的工作流程中,一個交易策略實際上就是一個函數,這個函數以已知的資訊作爲輸入,通過一系列邏輯推演,輸出交易決策。不管什麼技術流派,不管哪種交易風格,不管任何分析方法,交易策略的最根本的定義,就是如此。

在這裏插入圖片描述

比如:

  • 技術分析派利用過去的股票價格(輸入數據)計算技術指標(邏輯推演),進行買入/賣出操作(交易決策

  • 價值投資派利用上市公司的各項指標(輸入數據),分析公司的成長潛力(邏輯推演),決定買入/賣出哪一支股票(交易決策

  • 宏觀分析者即使不關心個股的價格,也需要參考熱點新聞、市場景氣(仍然是輸入數據),分析市場的整體趨勢(邏輯推演),決定是否入市(交易決策

  • 高頻或超高頻的套利交易,也需要根據短期內價格的實時變化(輸入數據),分析套利空間大小(邏輯推演),以便迅速介入操作(交易決策

上面的交易策略,如果以較高的頻率,跟蹤少數投資品種,就是所謂的“擇時交易策略”,如果以較低的頻率,跟蹤大量的投資品種,就是所謂的“選股策略”。

總之,一切量化交易,都是一套定期運行的邏輯推演,在每次運行時,提取當時的最新數據作爲輸入,輸出一套交易決策。如此反覆運行,形成穩定的交易操作流水,概莫能外,這就是交易策略的抽象概念。

9.4. 使用 qteasyStrategy 策略類

qteasy的交易策略就是基於上面的概念定義的。qteasy定義的策略類,就是數據和交易邏輯的載體。用戶通過繼承Strategy類,定義自己的策略類,在這個類中定義策略需要的數據以及策略的實現邏輯,就可以創建一個自定義交易策略了。

因此,在qteasy的設計中,一個交易策略包括下面四個核心要素:

策略以及策略組的運行時機

  • 策略的運行時機 —— 策略何時運行,以什麼頻率運行,這個要素是由策略組確定的。在將一個策略添加到Operator中的時候,可以指定參數run_freq run_timing,這兩個參數共同決定了策略的運行時機

策略的可調參數和歷史數據

qteasy中的交易策略還有一個特別的設計:可調參數。可調參數是指在策略運行過程中會被不斷調整的參數,例如均線週期、選股排名的股票數量等等,這些參數直接影響了策略的表現,因此我們希望能夠調整這些參數來找到最優的參數組合,從而使得策略表現最佳。qteasy提供了一個非常方便的接口來定義可調參數,用戶可以在策略屬性中定義任意數量的可調參數,並且爲每個可調參數指定名稱、取值範圍、數據類型等等,這些定義好的可調參數會被自動傳入策略實現函數中,用戶可以直接使用這些參數來實現交易邏輯。

  • 策略的可調參數 —— 策略中需要調整的參數,例如均線週期、選股排名的股票數量等等,這些參數直接影響了策略的表現,因此我們希望能夠調整這些參數來找到最優的參數組合,從而使得策略表現最佳。qteasy提供了一個非常方便的接口Parameters來定義可調參數,用戶可以在策略屬性中定義任意數量的可調參數,並且爲每個可調參數指定名稱、取值範圍、數據類型等等,這些定義好的可調參數會被自動傳入策略實現函數中,用戶可以直接使用這些參數來實現交易邏輯。

策略需要的數據以及運行邏輯

  • 策略需要的數據 —— 策略需要的數據輸入;例如,需要過去10天的日K線數據,還是過去一年的市盈率?策略所需的數據量可以由Strategy.data_types 屬性完全自由定義,用戶可以使用qt.StgData()對象定義任意類型的數據輸入,數據的頻率、數據的窗口長度都可以由策略屬性完全自由定義,qteasy會根據這些定義自動打包數據送入策略的實現函數中

  • 策略的運行邏輯 —— 策略的實現邏輯通過重寫Strategy.realize()方法,用戶可以自由定義如何使用輸入數據,產生交易信號。

策略的輸出信號

qteasy允許用戶定義不同類型的交易信號:PT PS VS等等,用戶可以根據自己的需要選擇不同類型的交易信號來實現不同類型的交易策略。關於交易信號的含義,簡述如下,要查看不同類型的交易信號的詳細介紹,參見交易信號

  • PT型信號 —— 代表倉位的百分比,輸出爲一個0到1之間的數值,保持持有多少比例的倉位,例如輸出0.5代表持有50%的倉位,輸出0代表不持有,輸出1代表全倉持有;

  • PS型信號 —— 代表交易百分比,輸出爲一個-1到1之間的數值,代表買入/賣出多少比例的倉位,例如輸出0.5代表買入50%的倉位,輸出-0.5代表賣出50%的倉位,輸出0代表不操作;

  • VS型信號 —— 代表交易的數量,輸出的數字代表需要買入/賣出的股票數量,例如500表示買入500股,-300表示賣出300股

除了上面跟策略有關的資訊以外,其餘所有的工作qteasy都已經做好了,所有的交易數據都會根據策略屬性被自動打包成一個ndarray數組,可以很方便地提取並使用;同一個交易策略,在實盤運行時會自動抽取交易數據,根據定義好的策略生成交易信號,在回測時也會自動提取歷史數據,自動生成歷史數據切片,不會形成未來函數。同時,所有交易數據都會

因此,在qteasy中的策略自定義非常簡單:

  • __init__() 在此方法中定義策略的所有參數,包括策略的名稱、描述、以及最重要的可調參數、數據類型等等,這些參數會被qteasy自動識別,在策略運行可以直接使用

  • realize() 在此方法中定義策略的運行邏輯:提取歷史數據,根據數據生成交易信號

在這裏插入圖片描述

除了上面所說的策略屬性以外,自定義策略同樣擁有與內置交易策略相同的基本屬性,例如可調參數等等,因爲它們與內置交易策略相同,在這裏就不贅述了。

9.5. 三種不同的自定義策略基類

qteasy提供了三種不同的策略類,便於用戶針對不同的情況創建自定義策略。

  • GeneralStg: 通用交易策略類,用戶需要在realize()方法中給出所有交易資產的交易決策信號

  • FactorSorter: 因子選股類,用戶只需要在realize()方法中定義出選股因子,便可以通過對象屬性實現多種選股動作

  • RuleIterator: 循環規則類,用戶只要針對一支股票定義選股或擇時規則,則同樣的規則會被循環作用於所有的股票,而且不同股票可以定義不同的參數

三種交易策略基類的屬性、方法都完全相同,區別僅在於realize()方法的定義。

下面,我們通過幾個循序漸進的例子來了解如何創建自定義策略。

9.6. 定義一個雙均線擇時交易策略

我們的第一個例子是最簡單的雙均線擇時交易策略,這是一個最經典的擇時交易策略。 這個均線擇時策略有兩個可調參數:

  • FMA 快均線週期

  • SMA 慢均線週期 策略根據過去一段時間的收盤價,計算上述兩個週期產生的簡單移動平均線,當兩根均線發生交叉時產生交易信號:

  • 當快均線自下而上穿過上邊界,發出全倉買入信號

  • 當快均線自上而下穿過上邊界,發出全部賣出信號

在這裏插入圖片描述

這個策略的邏輯非常簡單。那我們怎麼定義這個策略呢?首先,我們需要決定使用哪一種交易策略基類。很多情況下,三種交易策略基類都可以用來生成同樣的交易策略,只不過某些基類針對特定類型的策略提前做了一些定義,因而可以進一步簡化策略的代碼。這個策略是一個典型的擇時策略,是針對不同投資品種應用同一規則的策略類型,因此,我們可以先用RuleIterator策略類來建立策略。在後面的例子中我們會陸續講到另外兩種策略類。

接下來,我們把這個策略的三大要素明確一下:

  • 策略的運行時機 —— 爲了簡單,我們定義這個策略每天收盤時運行

  • 策略需要的數據 ——爲了計算兩條均線,我們需要每次策略運行時的歷史收盤價(“close”),而且需要過去連續至少SMA天的歷史數據,才足夠用來計算SMA慢均線

  • 策略的邏輯 ——提取收盤價後,首先計算兩條均線,然後判斷最近一天的均線是否有上穿/下穿。具體說來,就是比較昨天和今天兩個移動平均價的相對關係,如果昨天SMA大於FMA,而今天SMA就小於FMA了,說明FMA從下方上穿了SMA,應該產生全倉買入信號,這個信號爲1,如果情況正好相反,則輸出全倉賣出信號-1,其他情況下則輸出0,沒有交易信號。

有了上面的準備,那我們來看看策略代碼如何定義。一個最基本的策略代碼,第一步就是繼承策略基類(這裏是RuleIterator),創建一個自定義類:

from qteasy import RuleIterator
from qteasy import Parameter, StgData

class Cross_SMA(RuleIterator):

    # 策略的属性定义在__init__()方法中
    def __init__(self):
        super().__init__(
        )

    # 策略的具体实现代码写在策略的realize()函数中
    def realize(self):
        """策略的具体实现代码:
        """
        pass

好了,上面幾行代碼,就是我們第一個自定義交易策略的全部框架了,在這個框架中填充屬性,補充邏輯,就能成爲一個完整的交易策略。怎麼做呢,我們首先定義這個策略的最基本屬性——名稱、描述、以及可調參數:

名稱和描述都是策略的資訊,在後續調用時方便了解策略的用途,咱們按照喜好定義即可,比較關鍵的屬性是可調參數。

在我們這個策略中,我們希望快均線和慢均線的計算參數是可調的,因爲這兩個參數直接影響了快慢均線的具體位置,從而直接影響兩條均線的交叉點,從而形成不同的買賣點,參見下面兩張圖,分別顯示了同一只股票在同一段時間內不同速度均線的交叉情況,當均線的計算週期不同時,產生的買賣點也完全不同:

在這裏插入圖片描述 上圖中均線週期分別爲15天/40天,產生三次買入、兩次賣出信號 在這裏插入圖片描述 上圖中均線週期分別爲5天/50天,產生兩次買入、一次賣出信號

既然均線週期直接影響到策略的表現,因此我們自然希望找到最優的均線週期組合(參數組合),使得策略的表現最佳。爲了達到這個目的,qteasy允許用戶將這些參數定義爲“可調參數”,並提供優化算法來尋找最優參數。對所有的內置交易策略來說,可調參數的數量和含義是定義好的,用戶不能修改,但是在自定義策略這裏,用戶就有了很大的自由度,理論上講,策略運行過程中用到的任何變量,都可以被定義爲可調參數。

在這裏,我們將快慢均線的週期定義爲可調參數,在策略屬性中進行以下定義

策略的可調參數使用Parameter對象定義,利用這個對象我們可以爲每個可調參數指定名稱、取值範圍、數據類型等等,這些定義好的可調參數會被自動傳入策略實現函數中,用戶可以直接使用這些參數來實現交易邏輯。

pars=[Parameter((10, 100), data_type='int', name='fast', value=30),
      Parameter((10, 100), data_type='int', name='slow', value=60)],  # 策略默认参数是快均线周期30, 慢均线周期60

定義策略需要的數據

策略需要的數據StgData對象確定,qteasy中的每個數據類型對象都可以根據一個唯一ID,數據頻率以及資產類型確定。同時還可以指定數據窗口的長度。

定義好策略數據後,qteasy會自動將窗口內的數據打包送入策略realize()函數,如果在回測的過程中,所有歷史數據會根據同樣的規則分別打包成一系列的數據窗口,因此,不管是回測還是實盤運行,realize()函數接受到的歷史數據格式完全相同,處理方式也完全相同,確保實盤和回測運行的一致性,也避免了回測中可能出現的未來函數:

在這裏插入圖片描述

StgData數據類型的定義如下,我們需要過去201天的每日收盤價,之所以需要201天的收盤價,是因爲我們定義了可調參數的最大範圍爲200,爲了計算週期爲200的移動均線,需要201天的收盤價

data_types=[StgType('close', asset_type='ANY', freq='d', window_length=201)]  # 策略基于收盘价计算均线,因此数据类型为'close',历史数据的频率为日,历史数据窗口长度为201,每一次交易信号都是由它之前前201天的历史数据决定的

根據上面定義的數據類型,qteasy給該策略分配一個唯一的ID:close_ANY_d,使用這個ID就可以在策略實現函數中提取歷史數據了,後面我們會介紹如何提取歷史數據。

至此,自定義交易策略的所有重要屬性就全部定義好了。接下來我們來定義策略的實現。

自定義交易策略的實現:realize()

realize()方法中,我們需要做三件事情,我們一件件解決:

  • 獲取歷史數據

  • 獲取可調參數

  • 編寫邏輯,產生輸出

獲取歷史數據和可調參數值:

前面已經提過,這個策略的可調參數就是均線的計算週期,因此,爲了使用可調參數計算週期,我們需要取得可調參數的值以及具體的歷史數據。

在realize()方法中獲取可調參數和歷史數據非常簡單,分別使用self.get_data()self.get_pars()即可獲取。兩個方法都可以一次獲取多個數據或參數,獲取的數據會被解包到一個tuple中,用戶可以直接使用。

def realize(self):
    """策略的具体实现代码
    """
    close = self.get_data('close_ANY_d')  # 通过数据ID获取数据:最近201天的收盘价
    f, s = self.get_pars('fast', 'slow')  # 读取快均线(fast)和慢均线(slow)的计算周期

到這裏,實現策略邏輯所需要的元素都備齊了,接下來我們可以開始實現策略邏輯。

我們需要首先計算兩組移動平均價,如果用戶安裝了ta-lib庫,那麼可以直接調用ta-libSMA函數計算移動平均價,如果沒有安裝,現在也沒有太大關係,因爲qteasy爲用戶提供了免ta-lib版本的SMA函數,(並不是所有的技術指標都有免ta-lib版本,詳情參見參考文檔)可以直接引用計算。

def realize(self, h, **kwargs):
    """策略的具体实现代码
    """
    ...
    from qteasy.tafuncs import sma
    # 使用qt.sma计算简单移动平均价
    s_ma = sma(close, s)
    f_ma = sma(close, f)
    # 为了考察两条均线的交叉, 计算两根均线昨日和今日的值,以便判断
    s_today, s_last = s_ma[-1], s_ma[-2]
    f_today, f_last = f_ma[-1], f_ma[-2]

計算出移動均線後,我們可以直接在realize方法中定義策略的輸出,也就是交易決策。

對於RuleIterator類策略,不管我們的策略同時作用於多少支股票,我們都只需要定義一套規則即可,是爲“規則迭代”,因此,我們只需要輸出一個數字,代表交易決策即可。這個數字會被qteasy自動轉化爲不同的交易委託單。轉化的規則由Operator對象的工作模式確定,關於這一點在前面的教程中已經介紹過了,這裏不再贅述。

在本例子中,我們計劃讓交易策略輸出的信號代表將多大比例的總資產投入到投資產品中,那麼只需要在應當買入的當天,產生交易信號“1”,在應當賣出的當天,產生交易信號“-1”即可,如果不希望交易,則輸出“0”:

def realize(self, h, **kwargs):
    """策略的具体实现代码
    """
    ...
    if (f_last < s_last) and (f_today > s_today):  
        # 当快均线自下而上穿过上边界(即昨日快均线低于慢均线,而今天高于于慢均线),发出全仓买入信号
        return 1
    elif (f_last > s_last) and (f_today < s_today): 
        # 当快均线自上而下穿过上边界(即昨日快均线高于慢均线,而今天低于于慢均线),发出全部卖出信号
        return -1
    else:  # 其余情况不产生任何信号
        return 0

至此,這個交易策略就定義完成了!qteasy會完成所有背後的複雜工作,用戶僅需要集中精力解決策略的數據和邏輯定義即可。完整代碼如下(爲節約篇幅,刪除了所有註釋):

from qteasy import RuleIterator, Parameter, StgData
from qteasy.tafuncs import sma
# 创建双均线交易策略类
class Cross_SMA_PS(RuleIterator):
    def __init__(self):
        super().__init__(
            name='CROSSLINE',  # 策略的名称
            description='快慢双均线择时策略',  # 策略的描述
            pars=[Parameter((10, 100), par_type='int', name='fast', value=30),
                  Parameter((10, 100), par_type='int', name='slow', value=60)],
            data_types=[StgData('close', freq='d', asset_type='ANY', window_length=201)],
        )

    def realize(self):
        
        close = self.get_data('close_ANY_d')  # 通过数据ID获取数据:最近201天的收盘价
        f, s = self.get_pars('fast', 'slow')  # 读取快均线(fast)和慢均线(slow)的计算周期
        
        # 使用qt.sma计算简单移动平均价
        s_ma = sma(close, s)
        f_ma = sma(close, f)
        # 为了考察两条均线的交叉, 计算两根均线昨日和今日的值,以便判断
        s_today, s_last = s_ma[-1], s_ma[-2]
        f_today, f_last = f_ma[-1], f_ma[-2]
        
        if (f_last < s_last) and (f_today > s_today):  
            # 当快均线自下而上穿过上边界(即昨日快均线低于慢均线,而今天高于于慢均线),发出全仓买入信号
            return 1
        elif (f_last > s_last) and (f_today < s_today): 
            # 当快均线自上而下穿过上边界(即昨日快均线高于慢均线,而今天低于于慢均线),发出全部卖出信号
            return -1
        else:  # 其余情况不产生任何信号
            return 0

接下來,我們就可以像使用任何內置交易策略一樣使用這個自定義策略了。

我們需要創建一個新的Operator對象,並在創建對象的時候,將剛纔創建的Cross_SMA策略加入到Operator中,同時指定這個策略的信號類型爲PS型信號,這是告訴Operator使用PS規則來解析交易策略生成的信號:

op = qt.Operator(strategies=[Cross_SMA_PS()], signal_type='PS')

讓我們看看這個策略的回測結果。

第一個策略的回測結果

策略回測的參數設置與內置交易策略完全一樣

op = qt.Operator([Cross_SMA_PS()], signal_type='PS')

# 设置op的策略参数
op.set_parameter(0, 
                 par_values= (20, 60)  # 设置快慢均线周期分别为20天、60天
                )

# 设置基本回测参数,开始运行模拟交易回测
res = qt.run(op, 
             mode=1,  # 运行模式为回测模式
             asset_pool='000300.SH',  # 投资标的为000300.SH即沪深300指数
             invest_start='20110101',  # 回测开始日期
             invest_end='20191231', # 回测结束日期
             visual=True  # 生成交易回测结果分析图
            )

回測的結果如下:

在這裏插入圖片描述

下面,我們可以嘗試一下修改策略的可調參數,再重新跑一遍回測,回測區間與前一次相同:

op.set_parameter(0, 
                 par_values= (25, 166)  # 设置快慢均线周期分别为25天、166天
                )

# 设置基本回测参数,开始运行模拟交易回测,回测参数完全一样
res = qt.run(op, 
             mode=1,  # 运行模式为回测模式
             asset_pool='000300.SH',  # 投资标的为000300.SH即沪深300指数
             invest_start='20110101',  # 回测开始日期
             visual=True  # 生成交易回测结果分析图
            )

可以看到,改變參數後,策略的回測結果大爲改觀:要了解如何進行策略參數優化,請參考本教程的後續章節

====================================
|                                  |
|         BACKTEST REPORT          |
|                                  |
====================================
qteasy running mode: 1 - History back testing
time consumption for operate signal creation: 111.0 ms
time consumption for operation back testing:  2.1 ms
investment starts on      2011-01-04 15:00:00
ends on                   2019-12-30 15:00:00
Total looped periods:     9.0 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
000300.SH    6        7      13   46.8%     -0.0%     53.2%  

Total operation fee:     ¥      283.00
total investment amount: ¥  100,000.00
final value:              ¥  180,675.40
Total return:                    80.68% 
Avg Yearly return:                6.80%
Skewness:                         -1.01
Kurtosis:                         17.32
Benchmark return:                27.96% 
Benchmark Yearly return:          2.78%

------strategy loop_results indicators------ 
alpha:                           -1.011
Beta:                            -4.128
Sharp ratio:                      0.368
Info ratio:                       0.009
250 day volatility:               0.134
Max drawdown:                    31.58% 
    peak / valley:        2015-06-08 / 2015-07-08
    recovered on:         2018-01-22

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

在這裏插入圖片描述

至此,我們已經實現了一個簡單的自定義擇時交易策略,那麼另外兩種策略類如何實現呢?我們在下一章節中再用更多的例子來說明。

9.7. 本節回顧

在這一節中,我們瞭解了qteasy中對交易策略的抽象定義,瞭解了一個交易策略所包含的基本要素以及它們的定義方法,並且通過一個最簡單的例子,實際創建了一個自定義雙均線交易策略。

接下來,我們還將繼續介紹自定義交易策略,因爲相關的內容比較多,所以自定義交易策略相關的教程將佔用三個章節。在下一章節中,我們將學習如何使用另外兩種自定義策略基類(FactorSorter因子選股基類和GeneralStg通用策略基類)來創建交易策略。接着,我們將再用一個章節的篇幅,來介紹一個比較複雜的自定義交易策略,展示qteasy的靈活性,讓我們下一節見!