8. Use built-in strategies

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

  • Access, clean, store, process, visualize, and use 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

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

8.1. Before you start

Before starting this tutorial, make sure you have mastered the following:

  • Install and configure qteasy —— details please refer to QTEASY tutorial 1

  • A local data source has been set up, and sufficient historical data has already been downloaded locally (including the trading calendar; basic information on stocks/funds/indices; price data for stocks/funds/indices; and financial indicators or other financial data—see QTEASY Tutorial 2 for details)

  • Learn to create a trader object, use a built-in trading strategy and backtest its historical performance, check the backtest log, understand how to adjust the running parameters or adjustable parameters of the strategy, and improve the performance of the strategy —— QTEASY tutorial 3

QTEASY文档中,还能找到更多关于如何创建交易员对象运行策略,使用历史数据回测策略,检查回测交易记录,修改策略等等相关内容。对qteasy的基本使用方法还不熟悉的同学,可以移步那里查看更多详细说明。

8.2. Target of this chapter

In the previous tutorial section, we created an Operator trader object and had it run the first stock-picking trading strategy. However, in qteasy, a trader is capable of far more than just running a single trading strategy. In fact, the Operator object is like a real trader: it can simultaneously manage and run any number of trading strategies. These strategies can run separately at different times and frequencies, or many can run together in a batch; and strategies that run at the same time can also be “blended” in any specified way into the desired form.

You can think of a trader as a painter holding a palette with paints of different colors. They can use different colors to draw vivid lines, or mix several colors to create soft transitional tones. In this way, even if the painter has only a handful of paints, their brush can still blend millions of colors that make up the vast world.

This is exactly one of qteasy’s key design concepts: using the following two tools, the Operator trader object can combine and configure very simple trading strategies into very complex trading strategies, thereby enabling complex functionality. These two tools include:

  • Strategy Group: Traders can run multiple strategies in groups. Each group of trading strategies shares the same execution frequency (e.g., daily, hourly, or every minute) and the same execution timing (e.g., at the daily close or at 10:30 every day), allowing flexible control over when strategies run.

  • Blender Strategy Mixer: Trading strategies that run simultaneously within the same group will necessarily run at the same time. Each strategy will generate a set of trading signals according to its own logic, but this set of signals must be “merged” into a single set in some way. This merging is performed by the mixer blender, which is specifically responsible for combining the trading signals generated by multiple strategies into one set. The merging logic is entirely determined by the user-defined blender string composition string, giving the user full control.

In this chapter, we will learn how to use more built-in strategies in qteasy, how to make a trader run multiple trading strategies at the same time, and how to use the strategy mixer blender to generate different combination strategies using trading strategies.

At present, qteasy supports more than 70 built-in trading strategies, all of which are ready to use. For a complete list of built-in trading strategies, please refer to reference documents:

You can use the API below to get a list of all built-in trading strategies provided in qteasy:

import qteasy as qt
qt.built_ins()

The API above will return all built-in trading strategies in qteasy:

{'crossline': qteasy.built_in.CROSSLINE,
 'macd': qteasy.built_in.MACD,
 'dma': qteasy.built_in.DMA,
 'trix': qteasy.built_in.TRIX,
 'cdl': qteasy.built_in.CDL,
 'bband': qteasy.built_in.BBand,
 's-bband': qteasy.built_in.SoftBBand,
 'sarext': qteasy.built_in.SAREXT,
 'ssma': qteasy.built_in.SCRSSMA,
 'sdema': qteasy.built_in.SCRSDEMA,
 'sema': qteasy.built_in.SCRSEMA,
 'sht': qteasy.built_in.SCRSHT,
 'skama': qteasy.built_in.SCRSKAMA,
 'smama': qteasy.built_in.SCRSMAMA,
 ...}

The following list lists some of the built-in trading strategies that are ready to use in qteasy. For a complete list, please refer to reference documents

ID

Name

Description

crossline

TimingCrossline

crossline timing strategy class, using the cross of long and short moving averages to determine the long and short states
1. When the short moving average is above the long moving average and the distance is greater than l*m%, set the position target to 1
2. When the short moving average is below the long moving average and the distance is greater than l*m%, set the position target to -1
3. When the distance between the long and short moving averages is not greater than l*m%, set the position target to 0

macd

TimingMACD

MACD timing strategy class, using the MACD moving average strategy to generate the target position percentage:
1. When the MACD value is greater than 0, set the position target to 1
2. When the MACD value is less than 0, set the position target to 0

dma

TimingDMA

DMA timing strategy
1. When DMA is above AMA, it is a long interval, that is, after the DMA line crosses the AMA line from bottom to top, the output is 1
2. When DMA is below AMA, it is a short interval, that is, after the DMA line crosses the AMA line from top to bottom, the output is 0

trix

TimingTRIX

TRIX timing strategy, using the triple-smoothed exponential moving average price of the stock price for long and short judgment:
Calculate the triple-smoothed exponential moving average price TRIX of the price, and then calculate the moving average of the M-day TRIX:
1. When TRIX is above MATRIX, set the position target to 1
2. When TRIX is below MATRIX, set the position target to -1

cdl

TimingCDL

CDL timing strategy, find the cdldoji pattern that meets the requirements in the K-line chart
Search for the cdldoji pattern that appears in the historical data window (between 0 and 100), sum it up/100, calculate the equivalent cdldoji matching quantity, and use the matching quantity as the trading signal.

bband

TimingBBand

Bollinger Band trading strategy, determine long and short positions based on the relationship between stock prices and Bollinger Band upper and lower bands, and generate trading signals when prices cross above or below the Bollinger Band lines. The moving average type of the Bollinger Band line cannot be selected
1. When the price crosses above the upper band, a full position buy signal is generated
2. When the price crosses below the lower band, a full position sell signal is generated

s-bband

SoftBBand

Bollinger Band progressive trading strategy, determine long and short positions based on the relationship between stock prices and Bollinger Band upper and lower bands, and trading signals are not generated all at once, but gradually buy and sell. Calculate BBAND, check whether the price exceeds the upper or lower band of BBAND:
1. After the price is greater than the upper band, a 10% proportional buy signal is generated every day
2. After the price is lower than the lower band, a 33% proportional sell signal is generated every day

sarext

TimingSAREXT

Extended Parabolic SAR strategy, buy signal is issued when the index is greater than 0, and sell signal is issued when the index is less than 0

ssma

SCRSSMA

Single moving average cross strategy —— SMA moving average (simple moving average): set the position ratio according to the relative position of the stock price and the SMA moving average

sdema

SCRSDEMA

Single moving average cross strategy —— DEMA moving average (double exponential smoothing moving average): set the position ratio according to the relative position of the stock price and the DEMA moving average

sema

SCRSEMA

Single moving average cross strategy —— EMA moving average (exponential smoothing moving average): set the position ratio according to the relative position of the stock price and the EMA moving average

Complete list of built-in strategies, please refer to reference documents

Each built-in trading strategy has its own ID (e.g., crossline, macd, dma, etc. in the table above). Users can use this ID to obtain an instance of a built-in trading strategy and add it to a trader object for use. For example:

stg = qt.get_built_in_strategy('dma')
stg.info()

Output obtained:

================================ Strategy: DMA =================================
Strategy RULE-ITER(DMA)
Parameters: ['slow', 'long', 'diff'] = (12, 26, 9)                  
Date Types: close_ANY_d x 270                                       
----------------------------- Iteration Properties -----------------------------
Allow multi pars        True
Multi-parameter not set                      

If you need to view a detailed explanation of each built-in trading strategy, such as the meaning of strategy parameters, signal generation rules, you can view the doc-string of each trading strategy:

For example:

qt.built_in_doc('Crossline', print_out=True)

You can see

Init signature: qt.built_in.TimingCrossline(pars:tuple=(35, 120, 0.02))
Docstring:     
crossline择时策略类,利用长短均线的交叉确定多空状态

策略参数:
    s: int, 短均线计算日期;
    l: int, 长均线计算日期;
    m: float, 均线边界宽度(百分比);
信号类型:
    PT型:目标仓位百分比
信号规则:
    1,当短均线位于长均线上方,且距离大于l*m%时,设置仓位目标为1
    2,当短均线位于长均线下方,且距离大于l*mM时,设置仓位目标为-1
    3,当长短均线之间的距离不大于l*m%时,设置仓位目标为0

策略属性缺省值:
默认参数:(35, 120, 0.02)
数据类型:close 收盘价,单数据输入
采样频率:天
窗口长度:270
参数范围:[(10, 250), (10, 250), (0, 1)]
策略不支持参考数据,不支持交易数据
File:           ~/Library/CloudStorage/OneDrive-Personal/Projects/PycharmProjects/qteasy/qteasy/built_in.py
Type:           type
Subclasses:     

In interactive python environments such as ipython, you can also use ? to display detailed information about built-in trading strategies, for example:

>>> qt.built_in.SelectingNDayRateChange?

You can see:

Init signature: qt.built_in.SelectingNDayRateChange(pars=(14,))
Docstring:     
基础选股策略:根据股票以前n天的股价变动比例作为选股因子

策略参数:
    n: int, 股票历史数据的选择期
信号类型:
    PT型:百分比持仓比例信号
信号规则:
    在每个选股周期使用以前n天的股价变动比例作为选股因子进行选股
    通过以下策略属性控制选股方法:
    *max_sel_count:     float,  选股限额,表示最多选出的股票的数量,默认值:0.5,表示选中50%的股票
    *condition:         str ,   确定股票的筛选条件,默认值'any'
                                'any'        :默认值,选择所有可用股票
                                'greater'    :筛选出因子大于ubound的股票
                                'less'       :筛选出因子小于lbound的股票
                                'between'    :筛选出因子介于lbound与ubound之间的股票
                                'not_between':筛选出因子不在lbound与ubound之间的股票
    *lbound:            float,  执行条件筛选时的指标下界, 默认值np.-inf
    *ubound:            float,  执行条件筛选时的指标上界, 默认值np.inf
    *sort_ascending:    bool,   排序方法,默认值: False,
                                True: 优先选择因子最小的股票,
                                False, 优先选择因子最大的股票
    *weighting:         str ,   确定如何分配选中股票的权重
                                默认值: 'even'
                                'even'       :所有被选中的股票都获得同样的权重
                                'linear'     :权重根据因子排序线性分配
                                'distance'   :股票的权重与他们的指标与最低之间的差值(距离)成比例
                                'proportion' :权重与股票的因子分值成正比

策略属性缺省值:
默认参数:(14,)
数据类型:close 收盘价,单数据输入
采样频率:月
窗口长度:150
参数范围:[(2, 150)]
策略不支持参考数据,不支持交易数据
File:           ~/Library/CloudStorage/OneDrive-Personal/Projects/PycharmProjects/qteasy/qteasy/built_in.py
Type:           type
Subclasses:    

8.3. Combination of multiple strategies

In qteasy, an Operator trader object can run any number of trading strategies in groups at the same time. When running, each of these strategies separately retrieves the historical data it needs and independently generates different trading signals. These signals are then combined into a set of trading signals and executed in a unified manner.

Users can use this feature to run multiple trading strategies with different focuses in a trader object at the same time. For example, one trading strategy monitors the stock price of individual stocks and generates selection signals based on the stock price, the second trading strategy is specifically responsible for monitoring the trend of the broader market, and determines the overall position based on the trend of the broader market. The third trading strategy is specifically responsible for stop loss and stop profit, and stops loss at specific times. The final trading signal is mainly based on the first trading strategy, but is restrained by the second strategy, and will be completely controlled by the third strategy when necessary.

Or, users can easily formulate a “committee” strategy, in which multiple strategies independently make trading decisions in a comprehensive strategy, and the final trading signal is determined by the “committee” vote composed of all sub-strategies. The voting method can be simple majority, absolute majority, weighted voting results, etc.

In the combined trading strategies above, each individual trading strategy is very simple and easy to define, yet combining them can produce a greater effect. At the same time, each sub-strategy is independent and can be freely combined into complex, comprehensive trading strategies. This avoids repeatedly developing strategies from scratch: you only need to rearrange and recombine the sub-strategies and redefine how they are combined to quickly build a series of complex, comprehensive trading strategies. This can greatly improve the efficiency of building trading strategies and shorten the cycle. Time is money.

Mixing trading signals refers to performing various operations or functions on trading signals. This can range from simple logical operations and addition/subtraction to complex custom functions. Any function that can be applied to an ndarray can be used to mix trading signals, as long as the final output trading signal is meaningful.

Define strategy combination method blender

In an Operator trader object, all trading strategies must be assigned to a strategy group. If there is more than one strategy in a group, a blender must be defined. If no blender is explicitly defined and the number of strategies exceeds 1, qteasy will create a default blender when running the Operator. However, for multi-strategy setups to run correctly, users need to define the blender themselves.

blender_str is a user-defined combination expression. Users use this expression to determine how different trading strategies are combined. This combination expression uses arithmetic operators, logical operators, functions, and other symbols to specify how strategy signals are combined. The blender expression can include the following elements:

The symbols supported in the blender expression are as follows:

Element

Example

Description

Strategy number

s0, s1...

A string that starts with s and ends with a number. The number is the index of the strategy in Group, and represents the trading signal generated by that strategy.

Numbers

-1.35

Any legal number, the number involved in the expression operation

Operators

+

Numerical operators including + - * / ^

Logical operators

and

Logical operators such as &/~ and and/or/not are supported

Functions

sum()

Supported functions can be seen in the table below

Parentheses

()

Group operation

Example of blender

If there are three trading strategies in an Operator object (with serial numbers s0/s1/s2), the blender defined in the following ways is legal and available, and use Operator.set_blender() to set the blender

Blender expression defined using arithmetic operators

's0 + s1 + s2'

In this case the trading signals generated by the three trading strategies will be added up to become the final trading signal. If the result of strategy 0 is buy 10%, the result of strategy 1 is buy 10%, and the result of strategy 2 is buy 30%, then the final result is buy 50%

Blender expression defined using logical operators:

's0 and s1 and s2'

In this case the final trading signal will only be formed when trading strategies 1, 2, and 3 all generate trading signals. For example, if the result of strategy 1 is buy, the result of strategy 2 is buy, and strategy 3 has no trading signal, then the final result is no trading signal.

Blender expressions can also include parentheses and some functions:

'max(s0, s1) + s2'

Meaning the sum of the maximum value of the results of strategies 1 and 2 and the result of strategy 3 becomes the final trading signal. If the result of strategy 1 is buy 10%, the result of strategy 2 is buy 20%, and the result of strategy 3 is buy 30%, the final result is buy 50%

A strategy can appear more than once in the blender expression, and pure numbers are also supported:

'(0.5 * s0 + 1.0 * s1 + 1.5 * s2) / 3 * min(s0, s1, s2)'

Above blender expression means: first calculate the weighted average of the three strategy signals (weights are 0.5, 1.0, 1.5 respectively), and then multiply by the minimum value of the three signals

Function parameters in the blender expression are defined in the function name:

'clip_-0.5_0.5(s0 + s1 + s2) + pos_2_0.2(s0, s1, s2)'

Above blender expression defines two different function operations, and the final result is obtained by adding the results. The first function is range clipping, after adding the three sets of strategy signals, the signal values less than -0.5 and greater than 0.5 are clipped, and the calculation result is obtained; the second function is position judgment function, which counts the time periods in the three sets of signals with a position greater than 0.2, defines them as “long positions”, and then counts the number of long position recommendations in the three strategies in each time period. If more than two strategies hold long position recommendations, the full position long position is output, otherwise the empty position is output.

Following functions are supported in the blender expression:

Functions

Example of expression

Description

abs

abs(s0)

Absolute value function
Calculates the absolute value of all trading signals
Only one trading signal can be entered

avg

avg(s0, s1, ...)

Average value function
Calculate the average value of all trading signals
The number of input signals is unlimited

avgpos

avgpos_N_T(s0, s1, ...)

Average cumulative function
When the trading signal is a position target signal, count the number of non-empty position signals (output signal absolute value > T) generated at the same time. When the number of short/long signals is greater than N, output the average value of all short/long signals, otherwise output 0.
The number of input signals is unlimited

ceil

ceil(s0)

Ceiing function
Trading signal rounding up
Only one trading signal can be entered

clip

clip_U_L(s0)

Clip function
Clip the signal value that exceeds the range, and define the upper and lower ranges in the function name
Only one trading signal can be entered

combo

combo(s0, s1, ...)

Combo value function
Output the sum of all trading signals
The number of input signals is unlimited

committee

cmt_N_T(s0, s1, ...)

Committee function (equivalent to the cumulative position function pos))
When the trading signal is a target position signal, count how many non-empty-position signals (output signal absolute value > T) are generated at the same time. When the number of long/short signals is greater than N, output -1/1; otherwise output 0.
The number of input signals is unlimited

exp

exp(s0)

exp function
Calculate the signal power of e
Only one trading signal can be entered

floor

floor(s0)

Floor function
Trading signal rounding down
Only one trading signal can be entered

log

log(s0)

Logarithm function
Calculate the logarithm value with e as the base
Only one trading signal can be entered

log10

log10(s0)

10-based logarithm function
Calculate the logarithm value with 10 as the base
Only one trading signal can be entered

max

max(s0, s1, ...)

Maximum value function
Calculate the maximum value of all trading signals
The number of input signals is unlimited

min

min(s0, s1, ...)

Minimum value function
Calculate the minimum value of all trading signals
The number of input signals is unlimited

pos

pos_N_T(s0, s1, ...)

Accumulated position function
When the trading signal is a position target signal, count the number of non-empty position signals (output signal absolute value > T) generated at the same time. When the number of long/short signals is greater than N, output -1/1, otherwise output 0.
The number of input signals is unlimited

position

position_N_T(s0, s1, ...)

Accumulated position function
When the trading signal is a position target signal, count the number of non-empty position signals (output signal absolute value > T) generated at the same time. When the number of long/short signals is greater than N, output -1/1, otherwise output 0.
The number of input signals is unlimited

pow

pow(s0, s1)

Power function
Calculate the second signal power of the first trading signal, that is, sig0^sig1
The number of input signals can only be two

power

power(s0, s1)

Power function
Calculate the second signal power of the first trading signal, that is, sig0^sig1
The number of input signals can only be two

sqrt

sqrt(s0)

Square root function
Trading signal square root
Only one trading signal can be entered

str

str_T(s0, s1, ...)

Strength cumulative function
Sum all trading signals, output 1 when the signal strength exceeds T, otherwise output 0
The number of input signals is unlimited

strength

strength_T(s0, s1, ...)

Strength cumulative function
Sum all trading signals, output 1 when the signal strength exceeds T, otherwise output 0
The number of input signals is unlimited

sum

sum(s0, s1, ...)

Combo value function
Output the sum of all trading signals
The number of input signals is unlimited

unify

unify(s0)

Uniform function
Normalize trading signals, scale trading signals in the same row proportionally to make the sum of each row 1
Only one trading signal can be entered

vote

vote_N_T(s0, s1, ...)

Vote function (equivalent to cumulative position function)
When the trading signal is a position target signal, count the number of non-empty position signals (output signal absolute value > T) generated at the same time. When the number of long/short signals is greater than N, output -1/1, otherwise output 0.
The number of input signals is unlimited

Following methods can be used to set or get the blender of the strategy

operator.set_blender(blender_str=None, group_id=None)

To set the blender, pass in an expression blender_str directly. This expression will be automatically parsed and used to combine trading strategies. When using set_blender(), you can use the group_id parameter to specify which strategy group this blender is for. If group_id is not specified, this blender is used for all strategy groups by default.

operator.view_blender()

View the blender. Note that, for readability, the strategy codes s0, s1, and s2 in the blender expression will be automatically replaced with the specific strategy IDs, as shown in the example below:

>>> op = qt.Operator('dma, macd, trix')
>>> op.set_blender('(0.5 * s0 + 1.0 * s1 + 1.5 * s2) / 3 * min(s0, s1, s2)', group_id='Group_1')
>>> op.view_blender()

operator.view_blender() outputs a dictionary where the keys are strategy group IDs and the values are blender expressions. In the blender expression, s0, s1, and s2 have already been replaced with the specific strategy IDs:

The output is as follows:

{'Group_1': '(0.5 * dma + 1.0 * macd + 1.5 * trix) / 3 * min(dma, macd, trix)'}

If you specify the group_id parameter when calling view_blender(), it will only output the blender expression for that strategy group:

>>> op.view_blender(group_id='Group_1')

The output is as follows:

'(0.5 * dma + 1.0 * macd+ 1.5 * trix) / 3 * min(dma, macd, trix)'

Above example s0,s1,s2 are replaced by dma, macd, trix, but if the Operator contains multiple identical strategies, they will be automatically assigned different strategy IDs to distinguish:

>>> op = qt.Operator('dma, dma, dma')
>>> op.set_blender('(0.5 * s0 + 1.0 * s1 + 1.5 * s2) / 3 * min(s0, s1, s2)')
>>> op.view_blender('Group_1')

The output is as follows:

'(0.5 * dma_0 + 1.0 * dma_1 + 1.5 * dma_2) / 3 * min(dma_0, dma_1, dma_2)'

Example of using blender

An example is used below to demonstrate how blender works:

We generate a trader object and run five DMA trading strategies at the same time, but the five trading strategies have different adjustable parameters. At this time, we can understand that the trader runs five of the same trading logic at the same time, but these five trading logics are configured with different parameters, so under the same input conditions, different trading signals are generated, which means that the five trading strategies have their own emphasis, some are good at capturing long-term variables, and some are good at tracking short-term trends.

Now, the same five trading strategies, but we will use three different examples to show three different blending methods, to show you that even if the same trading strategy and trading parameters, in the same historical interval, different blending methods can also affect the final trading results.

First blending method: Weighted average blending

The first blending method takes a weighted average of the results of the five trading strategies. The weights for each trading strategy are as follows:

  • s0: weight 0.8

  • s1: weight 1.2

  • s2: weight 2.0

  • s3: weight 0.5

  • s4: weight 1.5

To achieve the weighted-average blending above, we can define the following blender expression. It means: first multiply the results of the five trading strategies by their respective weights, then divide by the sum of the weights (i.e., 5) to obtain the weighted-average result:

(0.8*s0+1.2*s1+2*s2+0.5*s3+1.5*s4)/5

Next, we set this blender expression on the trader object, run the strategies, and obtain the backtest results:

# 创建一个交易员对象,同时运行五个相同的dma交易策略,这些交易策略运行方式相同,但是设置不同的参数后,会产生不同的交易信号。我们通过不同的策略组合方式,得到不同的回测结果
op = qt.Operator('dma, dma, dma, dma, dma')
# 分别给五个不同的交易策略设置不同的策略参数,使他们产生不同的交易信号
op.set_parameter(stg_id=0, par_values=(132, 200, 24))
op.set_parameter(stg_id=1, par_values=(124, 187, 51))
op.set_parameter(stg_id=2, par_values=(103, 81, 16))
op.set_parameter(stg_id=3, par_values=(48, 111, 148))
op.set_parameter(stg_id=4, par_values=(104, 127, 58))

op.set_blender('(0.8*s0+1.2*s1+2*s2+0.5*s3+1.5*s4)/5')

# 运行策略
res = qt.run(op, mode=1, invest_start='20160405', invest_end='20210201')

The resulting backtest report is as follows: annualized return 5.51%, Sharpe ratio 0.44. For how to interpret the backtest report, please refer to the relevant content in Tutorial Section 3.

Here we are not focusing on how to improve the performance of the trading strategies. Instead, we focus on how the same trading strategies produce different backtest results under different blending methods, and these differences in backtest results come entirely from the different blending methods.

====================================
|                                  |
|         BACKTEST REPORT          |
|                                  |
====================================
qteasy running mode: 1 - History back testing
time consumption for operate signal creation: 171.1 ms
time consumption for operation back testing:  3.2 ms
investment starts on      2016-04-05 15:00:00
ends on                   2021-01-29 15: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
000300.SH   370      354    724   88.1%     -0.0%     11.9%  

Total operation fee:     ¥    3,847.81
total investment amount: ¥  100,000.00
final value:              ¥  129,535.45
Total return:                    29.54% 
Avg Yearly return:                5.51%
Skewness:                         -0.90
Kurtosis:                         12.22
Benchmark return:                63.94% 
Benchmark Yearly return:         10.80%

------strategy loop_results indicators------ 
alpha:                           -0.060
Beta:                             1.311
Sharp ratio:                      0.440
Info ratio:                      -0.041
250 day volatility:               0.125
Max drawdown:                    18.49% 
    peak / valley:        2018-01-26 / 2019-01-03
    recovered on:         2019-03-05


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

png

Examine profit curve chart above carefully, and you will notice that the background of the chart is drawn with different shades of green stripes. These stripes represent the position ratio in that period of time. White represents an empty position, that is, not holding any stocks at all, and the darkest green represents 100% holding stocks. The green in the middle represents the position ratio between 0 and 100%, and the higher the position ratio, the darker the color of the stripes.

Because the outputs of the five trading strategies are combined as a weighted average, any change in the output of any one strategy will cause the final trading signal to change. This also makes the number of trades over the entire backtest period very frequent (a total of 370 sell operations and 354 buy operations), and the position ratio also fluctuates frequently between 0% and 100%.

You can see from the chart that there are different shades of green in the entire trading history interval. If you examine each holding interval very carefully, you will find that the holding ratio of these intervals corresponds exactly to the blending results of the five trading strategies at that time: when all trading strategies “decide unanimously” to buy full position, the weighted average result is 100% buy, but as long as one or more strategies decide to hold an empty position, the final weighted average result is to hold a certain percentage of stocks, which is equal to the weighted average result of the five strategy signals. The final result is that the position ratio fluctuates between 0% and 100%, and the time of completely empty position and completely full position is not long.

That also means that we cannot completely avoid one-way decline market by empty position, but always maintaining a certain position can better grasp the upward channel.

Meanwhile, you should also be able to observe that because the position is flexibly adjusted, the position (the depth of green) in the one-way decline market is obviously lower, and the position in the rising market is higher, which is exactly what we expect.

Next, let’s take a look at a completely different blending method:

Second blending method: Committee voting

This blender lets the same five trading strategies form a “committee” to determine the position through equal voting, and the position must be one of the two results of “non-black or white”: either full position or empty position. The expression is as follows:

At this point we need to use the special function supported by blender_str, the “committee function”, cmt_N_T(s0, s1, ...). This function means: when the number of strategies that simultaneously generate a non-flat signal (with absolute signal value > T) is greater than N, output fully invested long (1) or fully invested short (-1); otherwise output flat (0). Therefore, when N=3 and T=0, the expression is as follows:

cmt_3_0(s0, s1, s2, s3, s4)

This is equivalent to creating a committee consisting of five trading strategies: only when at least three strategies simultaneously recommend a fully invested long position will the committee output a fully invested long recommendation; otherwise it outputs a flat (no position) recommendation.

So the final result is that the position ratio is either 100% (when three or more strategies output a fully invested recommendation) or 0% (when fewer than three strategies output a fully invested recommendation). In a one-way down market, the position ratio is either 100% or 0%, with no intermediate state. The trader Operator is essentially executing a trading strategy that determines the position based on the vote of a five-member investment committee. Each committee member has their own view (because their parameters differ), but the final decision is determined by the majority.

op.set_blender('cmt_3_0(s0, s1, s2, s3, s4)')  # 将委员会混合函数设置为blender表达式
# 运行策略
res = qt.run(op, mode=1)

The results are as follows: annualized return 6.43, Sharpe ratio 0.487

====================================
|                                  |
|         BACKTEST REPORT          |
|                                  |
====================================
qteasy running mode: 1 - History back testing
time consumption for operate signal creation: 179.3 ms
time consumption for operation back testing:  3.2 ms
investment starts on      2016-04-05 15:00:00
ends on                   2021-01-29 15: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
000300.SH    10       9      19   57.2%     -0.0%     42.8%  

Total operation fee:     ¥      452.82
total investment amount: ¥  100,000.00
final value:              ¥  135,022.14
Total return:                    35.02% 
Avg Yearly return:                6.43%
Skewness:                         -0.74
Kurtosis:                         11.60
Benchmark return:                63.94% 
Benchmark Yearly return:         10.80%

------strategy loop_results indicators------ 
alpha:                           -0.034
Beta:                             1.001
Sharp ratio:                      0.487
Info ratio:                      -0.026
250 day volatility:               0.138
Max drawdown:                    22.60% 
    peak / valley:        2019-04-19 / 2020-07-24
    recovered on:         Not recovered!


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

png

You must have noticed that this time the output is very different from the previous one: in the last backtest the position ratio changed gradually (the shade of the green bands in the background of the return chart represents the position ratio), whereas this time it’s black-or-white—either fully invested or flat—and the number of trades is also greatly reduced, with only nine buys and ten sells in total. If you analyze the trade log carefully, you’ll find that it goes fully invested only when three trading strategies vote in favor of being fully invested; the rest of the time it stays flat. Therefore, in a one-way down market, the equity curve is a straight line. But when fully invested, if the stock falls, there is no way to reduce losses by appropriately trimming the position.

Next, we still use this committee, but now as long as two votes are cast for full position, the final position will be full position:

Third blending method: Committee voting

The third portfolio construction method: still a committee strategy, but the voting threshold for outputting a fully invested long position is changed to 2 votes, i.e., as long as two strategies think it should output long, it will do so.

op.set_blender('pos_2_0(s0, s1, s2, s3, s4)')
# 运行策略
res = qt.run(op, mode=1)

The results are as follows: annualized return 8.46%, Sharpe ratio 0.612

====================================
|                                  |
|         BACKTEST REPORT          |
|                                  |
====================================
qteasy running mode: 1 - History back testing
time consumption for operate signal creation: 180.0 ms
time consumption for operation back testing:  3.5 ms
investment starts on      2016-04-05 15:00:00
ends on                   2021-01-29 15: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
000300.SH    12       10     22   71.5%     -0.0%     28.5%  

Total operation fee:     ¥      548.27
total investment amount: ¥  100,000.00
final value:              ¥  147,914.95
Total return:                    47.91% 
Avg Yearly return:                8.46%
Skewness:                         -0.64
Kurtosis:                          8.61
Benchmark return:                63.94% 
Benchmark Yearly return:         10.80%

------strategy loop_results indicators------ 
alpha:                           -0.018
Beta:                             1.001
Sharp ratio:                      0.612
Info ratio:                      -0.018
250 day volatility:               0.154
Max drawdown:                    22.34% 
    peak / valley:        2019-04-19 / 2020-07-24
    recovered on:         Not recovered!


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

png

Compare the second chart and the third chart, you will find that the full position interval has obviously become longer, this is because the strategy that used to require three votes to be fully positioned, now only needs two votes, so it is easier to achieve full position

8.4. Recap

Alright—by now you should have a preliminary understanding of how trading strategies can be blended. Our tutorial will continue: qteasy offers even more ways to implement the trading strategies you want. In fact, although the core of qteasy is designed as a vectorized strategy engine that is conducive to high-speed backtesting and high-speed execution, it still takes sufficient flexibility into account. In theory, you can implement any type of trading strategy you can imagine.

Meanwhile, qteasy’s backtesting framework has also made quite a lot of special designs, which can completely avoid you inadvertently importing “future functions” into the trading strategy, ensuring that your trading strategy is completely based on past data during backtesting. At the same time, many preprocessing techniques and JIT techniques are used to compile the kernel key functions to achieve a running speed no less than that of C language.

However, in order to achieve theoretically infinite possible trading strategies, it may not be enough to use only built-in trading strategies and strategy blending. Some specific trading strategies, or some particularly complex trading strategies cannot be created by built-in strategy blending. This requires us to use the Strategy base class provided by qteasy to create a custom trading strategy based on certain rules.

In the next section of the qteasy tutorial, we will use an example to introduce how to create a custom trading strategy, how to define the basic parameters of the strategy, how to define the data types required by the strategy, how to set the logic for generating trading signals…