基于QP的优化选股策略


1. 项目基本情况

1.1 股票池

本项目股票池中总共有120只股票,具体股票代码见附录。

1.2 回测时间

本项目采用两年的回测时间:2019年1月3日-2021年1月22日。

1.3 交易成本

本项目股票交易费用统一设置成0.3%,且如果交易费用小于5元,按照5元算。

1.4 策略评估

本项目基于以下几个指标对策略本身以及策略与策略之间的评估:
-基准:沪深300
-策略收益率
-夏普比例
-最大回撤

1.5 具体买卖策略

调仓周期为20个工作日,最大允许持仓股票数为10只,且交易日时间窗口为50个工作日。起始账户现金为10000元,在交易日时间窗口50个工作日才开始运行策略,在策略的第一天,按照选股策略从股票池中选中m只股票,将当前现金平均(或优化权重)分配至前10只股票,并在第二天以开盘价购买。如果m < 10,则把现金平均(或优化权重)分配到满足选股条件的股票。在之后的20个工作日之内,不做任何变动;在20个工作日之后,先清仓(把所有股票以开盘价卖出),再用已有的现金买入满足选股策略的股票并选取前10只股票,再把现金平均(或优化权重)分配至选中的股票,以此类推。最终得到策略的回测结果。

2. 策略1

2.1 技术指标MA多头

MA的定义:移动平均线,将一定时期内的价格(指数)加以平均,并把不同时间的平均值连接起来,形成一根MA,用来观察证券价格变动趋势的一种技术指标。移动平均线
移动平均线MA的具体计算公式:
M A = 1 n ∑ i = 1 p i MA= \frac{1}{n}\sum_{i=1}p_i MA=n1i=1pi
其中, p i p_i pi表示前i天的股票价格,n为所求一定时期内的天数。
MA多头的选股判定标准为:当MA短线位于MA长线上方时,选择该股票;本策略MA短线设置为5天,记为MA5;MA长线设置为20天,记为MA20;即当MA5>MA10,则产生购买信号,此时购买该只股票。
全局变量

import pandas as pd
import numpy as np

# 开始日期
start_date = '2019-01-03'

# 结束日期
end_date = '2021-01-22'

# 策略比较参考标准: 沪深 300
benchmark = '000300.SHA'

# 初始资金
capital_base = 10000

# 读取数据
stock_info = pd.read_csv('stock_list.csv', sep=',', header=None)

# 证券代码列表
instruments = stock_info.iloc[:, 0].tolist()

print(instruments)

初始化模块(只运行一次)

def initialize(context):
    """初始化虚拟账户状态,只在第一个交易日运行"""

    # 设置手续费,买入和卖出都是0.3%, 不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.003, sell_cost=0.003, min_cost=5))

    # macd 参数设置
    # dif短线: 12 dif长线: 26 dea: 9
    context.macd_short = 12
    context.macd_long = 26
    context.macd_dea = 9

    # ma参数设置
    # ma短线: 5 ma长线: 20
    context.ma_short = 5 
    context.ma_long = 20

    # 调仓周期(多少个交易日调仓)
    context.trading_period = 20
    # 最大允许持仓股票数为 10 只
    context.max_portfolio_size = 10
    # 最近多少个交易日时间窗口
    context.observation = 50
    context.ins = instruments

每日执行模块

import talib

def handle_data(context, data):
    """策略交易逻辑,每个交易日运行一次"""

    # context.trading_day_index:交易日序号,第一个交易日为0
    # 在 context.observation 个交易日以后才开始真正运行
    if context.trading_day_index < context.observation:
        return

    # 是否需要调仓
    if context.trading_day_index % context.trading_period != 0:
        return

    # 选择的股票
    stock_selected = []

    # 依次遍历
    for stock in instruments:    
        # 当选择的股票数量达到最大股票数,停止运行
        if len(stock_selected) == context.max_portfolio_size:
            break

        # 通过股票代码获取股票对象
        sid = context.symbol(stock)

        # 当前价格
        price = data.current(sid, 'price')
        # 如果当前股票没有价格,则运行下一个股票
        if np.isnan(price):
            continue

        # 获取价格数据,获取过去交易日观察窗口的价格数据
        prices = data.history(sid, 'price', context.observation, '1d')

        # https://mrjbq7.github.io/ta-lib/index.html
        # fastperiod 指更短时段的EMA的长度,slowperiod指更长时段的EMA的长度,signalperiod指DEA长度
        macd, signal, hist = talib.MACD(np.array(prices),
                                        context.macd_short,
                                        context.macd_long,
                                        context.macd_dea)
        
        # 调仓:卖出所有持有股票
        # 计算现在portfolio中股票的仓位
        cur_position = context.portfolio.positions[sid].amount

        # 停牌的股票,将不能卖出,将在下一个调仓期处理
        if cur_position > 0 and data.can_trade(sid):
            # 清仓卖出
            context.order_target_value(sid, 0)

        ma_short = data.history(sid, 'price', context.ma_short, '1d').mean()
        ma_long = data.history(sid, 'price', context.ma_long, '1d').mean()

        # 调仓:买入新的股票
        if ma_short > ma_long and data.can_trade(sid):
            stock_selected.append(stock)
                    
    stock_selected = stock_selected[:context.max_portfolio_size]
    if len(stock_selected) > 0:
        # 等量分配资金买入股票
        per_stock_ratio = 1 / len(stock_selected)
        for stock in stock_selected:
            sid = context.symbol(stock)
            context.order_target_percent(sid, per_stock_ratio)

回测模块

# https://bigquant.com/doc/module_trade.html#M.trade.v3
m_macd = M.trade.v4(
    instruments=instruments,
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=capital_base,
    benchmark=benchmark,
)

按照前文所述买卖策略,在BigQuant进行回测,得到回测结果:
在这里插入图片描述
回测指标

基准收益率策略收益率夏普比率最大回撤
87.56%-6.2%-0.2716.23%

通过策略指标可以看出,单一使用MA多头技术策略,其回测表现比较差劲。

2.2 技术指标MACD金叉

MACD的定义为:异同移动平均线,是从双指数移动平均线发展而来,由快的指数移动平均线(EMA12)减去慢的指数移动平均线(EMA26)得到快线DIF,再用DIF的9日加权移动均线得到,最后2 * (DIF - DEA)得到MACD柱。MACD指标运用快慢均线的离散、聚合表征当前多空状态和股价可能的发展变化趋势。MACD指标
指数移动平均线EMA的计算公式为:
E M A ( N ) = 2 N + 1 X + N − 1 N + 1 E M A ′ ( N ) EMA(N)= \frac{2}{N+1}X+\frac{N-1}{N+1}EMA'(N) EMA(N)=N+12X+N+1N1EMA(N)
其中,X为当天的价格,N为天数,EMA(N)为当天的EMA值,EMA’(N)为前一天的EMA值。
MACD金叉的选股判定标准为:当MACD快线上穿MACD慢线时,选择该只股票;本策略MACD快线设置为12天,记为MACD12;MACD慢线设置为26天,记为MACD26;即当MACD12上穿MACD26时,产生购买信号,此时购买该只股票。
全局变量

import pandas as pd
import numpy as np

# 开始日期
start_date = '2019-01-03'

# 结束日期
end_date = '2021-01-22'

# 策略比较参考标准: 沪深 300
benchmark = '000300.SHA'

# 初始资金
capital_base = 10000

# 读取数据
stock_info = pd.read_csv('stock_list.csv', sep=',', header=None)

# 证券代码列表
instruments = stock_info.iloc[:, 0].tolist()

print(instruments)

初始化模块(只运行一次)

def initialize(context):
    """初始化虚拟账户状态,只在第一个交易日运行"""

    # 设置手续费,买入和卖出都是0.3%, 不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.003, sell_cost=0.003, min_cost=5))

    # macd 参数设置
    # dif短线: 12 dif长线: 26 dea: 9
    context.macd_short = 12
    context.macd_long = 26
    context.macd_dea = 9

    # ma参数设置
    # ma短线: 5 ma长线: 20
    context.ma_short = 5 
    context.ma_long = 20

    # 调仓周期(多少个交易日调仓)
    context.trading_period = 20
    # 最大允许持仓股票数为 10 只
    context.max_portfolio_size = 10
    # 最近多少个交易日时间窗口
    context.observation = 50
    context.ins = instruments

每日执行模块

import talib

def handle_data(context, data):
    """策略交易逻辑,每个交易日运行一次"""

    # context.trading_day_index:交易日序号,第一个交易日为0
    # 在 context.observation 个交易日以后才开始真正运行
    if context.trading_day_index < context.observation:
        return

    # 是否需要调仓
    if context.trading_day_index % context.trading_period != 0:
        return

    # 选择的股票
    stock_selected = []

    # 依次遍历
    for stock in instruments:    
        # 当选择的股票数量达到最大股票数,停止运行
        if len(stock_selected) == context.max_portfolio_size:
            break

        # 通过股票代码获取股票对象
        sid = context.symbol(stock)

        # 当前价格
        price = data.current(sid, 'price')
        # 如果当前股票没有价格,则运行下一个股票
        if np.isnan(price):
            continue

        # 获取价格数据,获取过去交易日观察窗口的价格数据
        prices = data.history(sid, 'price', context.observation, '1d')

        # https://mrjbq7.github.io/ta-lib/index.html
        # fastperiod 指更短时段的EMA的长度,slowperiod指更长时段的EMA的长度,signalperiod指DEA长度
        macd, signal, hist = talib.MACD(np.array(prices),
                                        context.macd_short,
                                        context.macd_long,
                                        context.macd_dea)
        
        # 调仓:卖出所有持有股票
        # 计算现在portfolio中股票的仓位
        cur_position = context.portfolio.positions[sid].amount

        # 停牌的股票,将不能卖出,将在下一个调仓期处理
        if cur_position > 0 and data.can_trade(sid):
            # 清仓卖出
            context.order_target_value(sid, 0)

        ma_short = data.history(sid, 'price', context.ma_short, '1d').mean()
        ma_long = data.history(sid, 'price', context.ma_long, '1d').mean()

        # 调仓:买入新的股票
        # 买入逻辑 macd 上穿 signal
        if (macd[-1] - signal[-1] > 0) and (macd[-2] - signal[-2] < 0):
            stock_selected.append(stock)
                    
    stock_selected = stock_selected[:context.max_portfolio_size]
    if len(stock_selected) > 0:
        # 等量分配资金买入股票
        per_stock_ratio = 1 / len(stock_selected)
        for stock in stock_selected:
            sid = context.symbol(stock)
            context.order_target_percent(sid, per_stock_ratio)

回测模块

# https://bigquant.com/doc/module_trade.html#M.trade.v3
m_macd = M.trade.v4(
    instruments=instruments,
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=capital_base,
    benchmark=benchmark,
)

按照前文所述买卖策略,在BigQuant进行回测,得到回测结果:
在这里插入图片描述
回测指标

基准收益率策略收益率夏普比率最大回撤
87.56%-45.91%-1.1153.42%

可以看出,单一使用MACD金叉技术策略,其回测表现也是比较差劲。

2.3 技术指标MACD金叉 + MA多头

结合MACD金叉和MA多头技术策略,即在MA5 > MA10 且 MACD12上穿MACD26时,为购买信号,此时购买该只股票。
全局变量

import pandas as pd
import numpy as np

# 开始日期
start_date = '2019-01-03'

# 结束日期
end_date = '2021-01-22'

# 策略比较参考标准: 沪深 300
benchmark = '000300.SHA'

# 初始资金
capital_base = 10000

# 读取数据
stock_info = pd.read_csv('stock_list.csv', sep=',', header=None)

# 证券代码列表
instruments = stock_info.iloc[:, 0].tolist()

print(instruments)

初始化模块(只运行一次)

def initialize(context):
    """初始化虚拟账户状态,只在第一个交易日运行"""

    # 设置手续费,买入和卖出都是0.3%, 不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.003, sell_cost=0.003, min_cost=5))

    # macd 参数设置
    # dif短线: 12 dif长线: 26 dea: 9
    context.macd_short = 12
    context.macd_long = 26
    context.macd_dea = 9

    # ma参数设置
    # ma短线: 5 ma长线: 20
    context.ma_short = 5 
    context.ma_long = 20

    # 调仓周期(多少个交易日调仓)
    context.trading_period = 20
    # 最大允许持仓股票数为 10 只
    context.max_portfolio_size = 10
    # 最近多少个交易日时间窗口
    context.observation = 50
    context.ins = instruments

执行模块

import talib

def handle_data(context, data):
    """策略交易逻辑,每个交易日运行一次"""

    # context.trading_day_index:交易日序号,第一个交易日为0
    # 在 context.observation 个交易日以后才开始真正运行
    if context.trading_day_index < context.observation:
        return

    # 是否需要调仓
    if context.trading_day_index % context.trading_period != 0:
        return

    # 选择的股票
    stock_selected = []

    # 依次遍历
    for stock in instruments:    
        # 当选择的股票数量达到最大股票数,停止运行
        if len(stock_selected) == context.max_portfolio_size:
            break

        # 通过股票代码获取股票对象
        sid = context.symbol(stock)

        # 当前价格
        price = data.current(sid, 'price')
        # 如果当前股票没有价格,则运行下一个股票
        if np.isnan(price):
            continue

        # 获取价格数据,获取过去交易日观察窗口的价格数据
        prices = data.history(sid, 'price', context.observation, '1d')

        # https://mrjbq7.github.io/ta-lib/index.html
        # fastperiod 指更短时段的EMA的长度,slowperiod指更长时段的EMA的长度,signalperiod指DEA长度
        macd, signal, hist = talib.MACD(np.array(prices),
                                        context.macd_short,
                                        context.macd_long,
                                        context.macd_dea)
        
        # 调仓:卖出所有持有股票
        # 计算现在portfolio中股票的仓位
        cur_position = context.portfolio.positions[sid].amount

        # 停牌的股票,将不能卖出,将在下一个调仓期处理
        if cur_position > 0 and data.can_trade(sid):
            # 清仓卖出
            context.order_target_value(sid, 0)

        ma_short = data.history(sid, 'price', context.ma_short, '1d').mean()
        ma_long = data.history(sid, 'price', context.ma_long, '1d').mean()

        # 调仓:买入新的股票
        if ma_short > ma_long and data.can_trade(sid):
            # 买入逻辑 macd 上穿 signal
            if (macd[-1] - signal[-1] > 0) and (macd[-2] - signal[-2] < 0):
                stock_selected.append(stock)
                    
    stock_selected = stock_selected[:context.max_portfolio_size]
    if len(stock_selected) > 0:
        # 等量分配资金买入股票
        per_stock_ratio = 1 / len(stock_selected)
        for stock in stock_selected:
            sid = context.symbol(stock)
            context.order_target_percent(sid, per_stock_ratio)

回测模块

# https://bigquant.com/doc/module_trade.html#M.trade.v3
m_macd = M.trade.v4(
    instruments=instruments,
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=capital_base,
    benchmark=benchmark,
)

按照前文所述买卖策略,在BigQuant进行回测,得到回测结果:
在这里插入图片描述
回测指标

基准收益率策略收益率夏普比率最大回撤
87.56%47.32%0.6433.3%

由上表可知,投资组合的夏普比率相比单一技术策略投资由负转正,说明投资组合比单一技术策略能够更有效规避风险,实现更高的超额收益。

3. 策略2

3.1 技术指标KDJ

KDJ指标定义为:随机指标,根据统计学原理,通过一个特定的周期(常为9日、9周等)内出现过的最高价、最低价及最后一个计算周期的收盘价及这三者之间的比例关系,来计算最后一个周期的未成熟随机值RSV,然后根据平滑移动平均线的方法来计算K值、D值与J值,并绘成曲线图来研判股票走势。KDJ指标
未成熟随机值RSV的具体计算公式:

R S V = C − L n H n − L n × 100 RSV= \frac{C-L_n}{H_n-L_n}×100 RSV=HnLnCLn×100

其中,C为当天的价格, L n L_n Ln为前n天内的最低价, H n H_n Hn为前n天内的最高价。
为了解决RSV指标波动幅度过大的问题,引入K指标,对RSV值进行平滑得到的结果。K值的具体计算公式:

K i = 2 3 K i − 1 + 1 3 R S V i K_i= \frac{2}{3}K_{i-1}+\frac{1}{3}RSV_i Ki=32Ki1+31RSVi

其中, K i K_i Ki为当天的K值,RSVi为当天的RSV值, K i − 1 K_{i-1} Ki1为前一天的K值。
D值的具体计算公式:

D i = 2 3 D i − 1 + 1 3 K i D_i= \frac{2}{3}D_{i-1}+\frac{1}{3}K_i Di=32Di1+31Ki

其中, D i − 1 D_{i-1} Di1为前一天的D值。
在计算第1期的K值和D值,如果没有特别指定,则K值和D值一般初始化为50。
J指标是KD指标的辅助指标,进一步反映了K指标和D指标的偏离程度。J指标的具体计算公式:

J i = 3 × K i − 2 × D i J_i= 3×K_{i}-2×D_i Ji=3×Ki2×Di

KDJ指标原理:随机指标KDJ是以最高价、最低价及收盘价为基本数据进行计算,得出的K值、D值和J值分别在指标的坐标上形成的一个点,连接无数个这样的点位,就形成一个完整的、能反映价格波动趋势的KDJ指标。它主要利用价格波动的真实波幅来反映价格走势的强弱和超买超卖现象,在价格尚未上升或下降之前发出买卖信号的一种技术工具。它在设计过程中主要是研究最高价、最低价和收盘价之间的关系,同时也融合了动量观念、强弱指标和移动平均线的一些优点,因此,能够比较迅速、快捷、直观地研判行情。由于KDJ线本质上是一个随机波动的观念,故其对于掌握中短期行情走势比较准确。KDJ原理

3.2 技术指标KDJ超买超卖

KDJ超买超卖的选股判定标准为:当K值在80以上,D值在75以上,J值在100以上时,是典型的超买标准;当K值在20以下,D值在25以下,J值在0以下时,是典型的超卖标准。超买超卖的概念为:对某种金融交易标的物的过度买入为超买,反之,过度卖出为超卖。所以,超买的情况,需要将股票卖出;超卖的情况,需要将股票买入。当天K值小于20,且当天D值小于25时,产生购买信号,买入该只股票。

全局变量

import pandas as pd
import numpy as np

# 开始日期
start_date = '2019-01-03'

# 结束日期
end_date = '2021-01-22'

# 策略比较参考标准: 沪深 300
benchmark = '000300.SHA'

# 初始资金
capital_base = 10000

# 读取数据
stock_info = pd.read_csv('stock_list.csv', sep=',', header=None)

# 证券代码列表
instruments = stock_info.iloc[:, 0].tolist()

初始化模块(只运行一次)

def initialize(context):
    """初始化虚拟账户状态,只在第一个交易日运行"""

    # 设置手续费,买入和卖出都是0.3%, 不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.003, sell_cost=0.003, min_cost=5))

    # macd 参数设置
    # dif短线: 12 dif长线: 26 dea: 9
    context.macd_short = 12
    context.macd_long = 26
    context.macd_dea = 9

    # ma参数设置
    # ma短线: 5 ma长线: 20
    context.ma_short = 5 
    context.ma_long = 20

    # 调仓周期(多少个交易日调仓)
    context.trading_period = 20
    # 最大允许持仓股票数为 10 只
    context.max_portfolio_size = 10
    # 最近多少个交易日时间窗口
    context.observation = 50
    context.ins = instruments

执行模块

import talib as ta

def handle_data(context, data):
    """策略交易逻辑,每个交易日运行一次"""

    # context.trading_day_index:交易日序号,第一个交易日为0
    # 在 context.observation 个交易日以后才开始真正运行
    if context.trading_day_index < context.observation:
        return

    # 是否需要调仓
    if context.trading_day_index % context.trading_period != 0:
        return

    # 选择的股票
    stock_selected = []

    # 依次遍历, https://bigquant.com/wiki/doc/celve-qUHrgr5Ay5
    for stock in instruments:    
        k = stock # 标的为字符串格式
        sid = context.symbol(k) # 将标的转化为equity格式
        price = data.current(sid, 'price') # 最新价格
        cash = context.portfolio.cash  # 现金
        cur_position = context.portfolio.positions[sid].amount # 持仓
        curr= data.current(sid,'price')
        indicators={}      #指标
        hp=data.history(sid, 'high', 50, '1d')  # 最高价指标
        lp=data.history(sid, 'low', 50, '1d')   # 最低价指标
        cp=data.history(sid, 'price', 50, '1d')  # 收盘价指标
        if not hp.any() or not lp.any() or not cp.any(): # 过滤掉全部为空值的股票
            continue
        # fastk即未成熟随机指标RSV值,slowk是经过平滑的RSV,即k值;slowd是再次经过平滑的k,即d值;最后返回k值和d值
        indicators['k'],indicators['d']=ta.STOCH(np.array(hp,dtype='f8'),
                                                 np.array(lp,dtype='f8'),
                                                 np.array(cp,dtype='f8'),
                                                 fastk_period=9,
                                                 slowk_period=3,
                                                 slowk_matype=0,
                                                 slowd_period=3,
                                                 slowd_matype=0,)
        # j指标
        indicators['j']=np.array(indicators['k'])*3-np.array(indicators['d'])*2
        indicators['closePrice']=cp
        indicators=pd.DataFrame(indicators)#将字典形式转化为dataframe格式
        
        # k[t-1]>80,d[t-2]>80,j[t]>100时,k[t-2]>d[t-2], k[t-1]<d[t-1],卖出
        # k[t-1]<20,d[t-2]<20,j[t]<0时,k[t-2]<d[t-2], k[t-1]>d[t-1],买入
#         if indicators.iloc[-1]['k'] > 80 and indicators.iloc[-2]['d'] > 80 :
#             if indicators.iloc[-1]['k'] < indicators.iloc[-1]['d'] and indicators.iloc[-2]['k'] > indicators.iloc[-2]['d']:
#                 if float((indicators.iloc[-2]['k']-indicators.iloc[-2]['d'])/indicators.iloc[-2]['k'])>0.01 and float((indicators.iloc[-1]['d']-indicators.iloc[-1]['k'])/indicators.iloc[-1]['d'])>0.01:
#                     if cur_position >= 0:
#                         context.order_target_percent(sid, 0)
        # 调仓:卖出所有持有股票
        # 停牌的股票,将不能卖出,将在下一个调仓期处理
        if cur_position > 0 and data.can_trade(sid):
            # 清仓卖出
            context.order_target_value(sid, 0)
            
        # 交易逻辑,KDJ超买超卖
        if indicators.iloc[-1]['k'] < 20 and indicators.iloc[-1]['d'] < 25:
            if indicators.iloc[-1]['j'] < 0 and cash >= 0:
                number = (int((cash / curr) / 100)) * 100
                if number > 0:
                    stock_selected.append(stock) 
                            
    stock_selected = stock_selected[:context.max_portfolio_size]
    if len(stock_selected) > 0:
        # 等量分配资金买入股票
        per_stock_ratio = 1 / len(stock_selected)
        for stock in stock_selected:
            sid = context.symbol(stock)
            context.order_target_percent(sid, per_stock_ratio)

回测模块

# https://bigquant.com/doc/module_trade.html#M.trade.v3
m_macd = M.trade.v4(
    instruments=instruments,
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=capital_base,
    benchmark=benchmark,
)

同样,按照前文所述买卖策略,在BigQuant进行回测,得到回测结果:
在这里插入图片描述
回测指标

基准收益率策略收益率夏普比率最大回撤
87.56%-7.86%-0.4715.14%

可以看出,技术指标KDJ超买超卖策略在本项目中的回测收益表现并不好,但相比技术指标MACD,其最大回撤大幅度下降,该策略风险相对较小。

3.3 技术指标KDJ金叉死叉

KDJ金叉死叉的选股判定标准为:当K值上穿D值,即当K值前一天小于D值,后一天就大于D值,形成K值由下向上突破D值时,形成金叉,产生买入信号,且当K值和D值都小于25时,其信号较为准确可靠。
全局变量

import pandas as pd
import numpy as np

# 开始日期
start_date = '2019-01-03'

# 结束日期
end_date = '2021-01-22'

# 策略比较参考标准: 沪深 300
benchmark = '000300.SHA'

# 初始资金
capital_base = 10000

# 读取数据
stock_info = pd.read_csv('stock_list.csv', sep=',', header=None)

# 证券代码列表
instruments = stock_info.iloc[:, 0].tolist()

初始化模块(只运行一次)

def initialize(context):
    """初始化虚拟账户状态,只在第一个交易日运行"""

    # 设置手续费,买入和卖出都是0.3%, 不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.003, sell_cost=0.003, min_cost=5))

    # macd 参数设置
    # dif短线: 12 dif长线: 26 dea: 9
    context.macd_short = 12
    context.macd_long = 26
    context.macd_dea = 9

    # ma参数设置
    # ma短线: 5 ma长线: 20
    context.ma_short = 5 
    context.ma_long = 20

    # 调仓周期(多少个交易日调仓)
    context.trading_period = 20
    # 最大允许持仓股票数为 10 只
    context.max_portfolio_size = 10
    # 最近多少个交易日时间窗口
    context.observation = 50
    context.ins = instruments

执行模块

import talib as ta

def handle_data(context, data):
    """策略交易逻辑,每个交易日运行一次"""

    # context.trading_day_index:交易日序号,第一个交易日为0
    # 在 context.observation 个交易日以后才开始真正运行
    if context.trading_day_index < context.observation:
        return

    # 是否需要调仓
    if context.trading_day_index % context.trading_period != 0:
        return

    # 选择的股票
    stock_selected = []

    # 依次遍历, https://bigquant.com/wiki/doc/celve-qUHrgr5Ay5
    for stock in instruments:    
        k = stock # 标的为字符串格式
        sid = context.symbol(k) # 将标的转化为equity格式
        price = data.current(sid, 'price') # 最新价格
        cash = context.portfolio.cash  # 现金
        cur_position = context.portfolio.positions[sid].amount # 持仓
        curr= data.current(sid,'price')
        indicators={}      #指标
        hp=data.history(sid, 'high', 50, '1d')  # 最高价指标
        lp=data.history(sid, 'low', 50, '1d')   # 最低价指标
        cp=data.history(sid, 'price', 50, '1d')  # 收盘价指标
        if not hp.any() or not lp.any() or not cp.any(): # 过滤掉全部为空值的股票
            continue
        # fastk即未成熟随机指标RSV值,slowk是经过平滑的RSV,即k值;slowd是再次经过平滑的k,即d值;最后返回k值和d值
        indicators['k'],indicators['d']=ta.STOCH(np.array(hp,dtype='f8'),
                                                 np.array(lp,dtype='f8'),
                                                 np.array(cp,dtype='f8'),
                                                 fastk_period=9,
                                                 slowk_period=3,
                                                 slowk_matype=0,
                                                 slowd_period=3,
                                                 slowd_matype=0,)
        # j指标
        indicators['j']=np.array(indicators['k'])*3-np.array(indicators['d'])*2
        indicators['closePrice']=cp
        indicators=pd.DataFrame(indicators)#将字典形式转化为dataframe格式
        
        # k[t-1]>80,d[t-2]>80,j[t]>100时,k[t-2]>d[t-2], k[t-1]<d[t-1],卖出
        # k[t-1]<20,d[t-2]<20,j[t]<0时,k[t-2]<d[t-2], k[t-1]>d[t-1],买入
        if indicators.iloc[-1]['k'] > 80 and indicators.iloc[-2]['d'] > 80 :
            if indicators.iloc[-1]['k'] < indicators.iloc[-1]['d'] and indicators.iloc[-2]['k'] > indicators.iloc[-2]['d']:
                if float((indicators.iloc[-2]['k']-indicators.iloc[-2]['d'])/indicators.iloc[-2]['k'])>0.01 and float((indicators.iloc[-1]['d']-indicators.iloc[-1]['k'])/indicators.iloc[-1]['d'])>0.01:
                    if cur_position >= 0:
                        context.order_target_percent(sid, 0)
#         # 调仓:卖出所有持有股票
#         # 停牌的股票,将不能卖出,将在下一个调仓期处理
#         if cur_position > 0 and data.can_trade(sid):
#             # 清仓卖出
#             context.order_target_value(sid, 0)
            
        # 交易逻辑,KDJ金叉死叉
        if indicators.iloc[-1]['k'] < 25 and indicators.iloc[-2]['d'] < 25:
            if indicators.iloc[-1]['k'] > indicators.iloc[-1]['d'] and indicators.iloc[-2]['k'] < indicators.iloc[-2]['d']:
                if float((indicators.iloc[-2]['d']-indicators.iloc[-2]['k'])/indicators.iloc[-2]['d'])>0.01 and float((indicators.iloc[-1]['k']-indicators.iloc[-1]['d'])/indicators.iloc[-1]['k'])>0.01:
                    if cash >= 0:
                        number = (int((cash / curr) / 100)) * 100
                        if number > 0:
                            context.order_target_percent(sid, number)
                            # stock_selected.append(stock) 
                            
#     stock_selected = stock_selected[:context.max_portfolio_size]
#     if len(stock_selected) > 0:
#         # 等量分配资金买入股票
#         per_stock_ratio = 1 / len(stock_selected)
#         for stock in stock_selected:
#             sid = context.symbol(stock)
#             context.order_target_percent(sid, per_stock_ratio)

回测模块

# https://bigquant.com/doc/module_trade.html#M.trade.v3
m_macd = M.trade.v4(
    instruments=instruments,
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=capital_base,
    benchmark=benchmark,
)

同样,按照前文所述买卖策略,在BigQuant进行回测,得到回测结果:
在这里插入图片描述
回测指标

基准收益率策略收益率夏普比率最大回撤
87.56%33.02%0.6427.82%

可以看出,KDJ金叉选股策略相比KDJ超买策略其回测收益表现较好,但最大回撤表现较差,大于20%达到27.82%;观察KDJ金叉回测收益曲线图和MACD金叉+MA多头的回测收益曲线图可以看出,KDJ金叉更能反映真实指数中短期行情的走势,与基准指数更加吻合,且二者的夏普比率相等的情况下,KDJ金叉的最大回撤较低,风险更加可控。
现在我们对买卖策略稍加修改,买入策略不再限制股票的数量,卖出策略则改为符合KDJ死叉才进行卖出,得到回测结果:
在这里插入图片描述
回测指标

基准收益率策略收益率夏普比率最大回撤
87.56%97.99%1.2419.93%

可以看出,经过修改买卖策略的KDJ金叉死叉策略其收益率大大提高,并且最大回撤可以控制在20%以内,做到最大化收益的同时,尽可能降低风险。

3.4 技术指标KDJ顶背离底背离

顶背离指的是股价持续上涨并不断创新高,但是该股的技术指标却没有与该股价同步新高,而是出现滞涨甚至出现下跌走弱的现象。顶背离往往出现在股票上涨末期,上涨末期由于股价顺势上涨,而买盘力量无法继续跟上,空头逐渐占优,因而会出现股价与技术指标的背离,意味着股价很可能由上涨转为下跌,需警惕风险,背离次数越多,下跌动能越强。底背离指的是股价持续下跌不断创新低,但是该股的技术指标却没有与该股价同步新低,而是出现止跌甚至出现转强上涨的现象。底背离往往出现在股票下跌末期,下跌末期由于股价顺势下跌,而卖盘力量枯竭,技术指标先行钝化,多头逐渐占优,意味着股价很可能由下跌转为上涨,需乐观捕捉机会,背离次数越多,上涨动能越大。顶背离与底背离

对于KDJ指标,顶背离分析为:当指数创新高,而K、D值不配合创新高,指数走势一峰比一峰高,而K、D线却一峰比一峰低,即为顶背离,卖出信号;当指数创新低,K、D值不配合创新低,指数走势一峰比一峰低,而K、D线却一峰比一峰高,即为底背离,买入信号。
KDJ顶背离底背离的选股判定标准为:当天的K值大于80,前一天的D值大于80,当天的J值大于100时,股价创50日新高,而KDJ指标未创新高,为顶背离,产生卖出信号,卖出该只股票;当天的K值小于20,前一天的D值小于20,当天的J值小于0时,股价创50日新低,而KDJ指标未创新低,为底背离,产生买入信号,买入该只股票。

全局变量

import pandas as pd
import numpy as np

# 开始日期
start_date = '2019-01-03'

# 结束日期
end_date = '2021-01-22'

# 策略比较参考标准: 沪深 300
benchmark = '000300.SHA'

# 初始资金
capital_base = 10000

# 读取数据
stock_info = pd.read_csv('stock_list.csv', sep=',', header=None)

# 证券代码列表
instruments = stock_info.iloc[:, 0].tolist()

初始化模块(只运行一次)

def initialize(context):
    """初始化虚拟账户状态,只在第一个交易日运行"""

    # 设置手续费,买入和卖出都是0.3%, 不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.003, sell_cost=0.003, min_cost=5))

    # macd 参数设置
    # dif短线: 12 dif长线: 26 dea: 9
    context.macd_short = 12
    context.macd_long = 26
    context.macd_dea = 9

    # ma参数设置
    # ma短线: 5 ma长线: 20
    context.ma_short = 5 
    context.ma_long = 20

    # 调仓周期(多少个交易日调仓)
    context.trading_period = 20
    # 最大允许持仓股票数为 10 只
    context.max_portfolio_size = 10
    # 最近多少个交易日时间窗口
    context.observation = 50
    context.ins = instruments

执行模块

import talib as ta

def handle_data(context, data):
    """策略交易逻辑,每个交易日运行一次"""

    # context.trading_day_index:交易日序号,第一个交易日为0
    # 在 context.observation 个交易日以后才开始真正运行
    if context.trading_day_index < context.observation:
        return

    # 是否需要调仓
    if context.trading_day_index % context.trading_period != 0:
        return

    # 选择的股票
    stock_selected = []

    # 依次遍历, https://bigquant.com/wiki/doc/celve-qUHrgr5Ay5
    for stock in instruments:    
        k = stock # 标的为字符串格式
        sid = context.symbol(k) # 将标的转化为equity格式
        price = data.current(sid, 'price') # 最新价格
        cash = context.portfolio.cash  # 现金
        cur_position = context.portfolio.positions[sid].amount # 持仓
        curr= data.current(sid,'price')
        indicators={}      #指标
        hp=data.history(sid, 'high', 50, '1d')  # 最高价指标
        lp=data.history(sid, 'low', 50, '1d')   # 最低价指标
        cp=data.history(sid, 'price', 50, '1d')  # 收盘价指标
        if not hp.any() or not lp.any() or not cp.any(): # 过滤掉全部为空值的股票
            continue
        # fastk即未成熟随机指标RSV值,slowk是经过平滑的RSV,即k值;slowd是再次经过平滑的k,即d值;最后返回k值和d值
        indicators['k'],indicators['d']=ta.STOCH(np.array(hp,dtype='f8'),
                                                 np.array(lp,dtype='f8'),
                                                 np.array(cp,dtype='f8'),
                                                 fastk_period=9,
                                                 slowk_period=3,
                                                 slowk_matype=0,
                                                 slowd_period=3,
                                                 slowd_matype=0,)
        # j指标
        indicators['j']=np.array(indicators['k'])*3-np.array(indicators['d'])*2
        indicators['closePrice']=cp
        indicators=pd.DataFrame(indicators)#将字典形式转化为dataframe格式
        
        k_min=float(indicators.loc[:,['k']].min())
        d_min=float(indicators.loc[:,['d']].min())
        j_min=float(indicators.loc[:,['j']].min())
        k_max=float(indicators.loc[:,['k']].max())
        d_max=float(indicators.loc[:,['d']].max())
        j_max=float(indicators.loc[:,['j']].max())
        # 交易逻辑
        if indicators.iloc[-1]['k'] > 80 and indicators.iloc[-2]['d'] > 80 :
            if indicators.iloc[-1]['k'] < indicators.iloc[-1]['d'] and indicators.iloc[-2]['k'] > indicators.iloc[-2]['d']:
                if curr > data.history(sid, 'price', 50, '1d').max():
                    if indicators.iloc[-1]['k']<k_max and indicators.iloc[-1]['d']<d_max:
                         if indicators.iloc[-1]['j']<j_max:#kdj 未创新高
                                if cur_position >= 0:
                                    context.order_target_percent(sid, 0)
#         # 调仓:卖出所有持有股票
#         # 停牌的股票,将不能卖出,将在下一个调仓期处理
#         if cur_position > 0 and data.can_trade(sid):
#             # 清仓卖出
#             context.order_target_value(sid, 0)
            
        if indicators.iloc[-1]['k'] < 20 and indicators.iloc[-2]['d'] < 20 :
            if indicators.iloc[-1]['k'] > indicators.iloc[-1]['d'] and indicators.iloc[-2]['k'] < indicators.iloc[-2]['d']:
                if curr < data.history(sid, 'price', 50, '1d').min():
                     if indicators.iloc[-1]['k'] >k_min:
                        if indicators.iloc[-1]['d'] > d_min :
                            if indicators.iloc[-1]['j']>j_min:#kdj 未创新低
                                if cash >= 0:
                                    number = (int((cash / curr) / 100)) * 100
                                    if number > 0:
                                        # context.order_target_percent(sid, number)
                                        stock_selected.append(stock)  

    stock_selected = stock_selected[:context.max_portfolio_size]
    if len(stock_selected) > 0:
        # 等量分配资金买入股票
        per_stock_ratio = 1 / len(stock_selected)
        for stock in stock_selected:
            sid = context.symbol(stock)
            context.order_target_percent(sid, per_stock_ratio)

回测模块

# https://bigquant.com/doc/module_trade.html#M.trade.v3
m_macd = M.trade.v4(
    instruments=instruments,
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=capital_base,
    benchmark=benchmark,
)

同样,按照前文所述买卖策略,在BigQuant进行回测,得到回测结果:
在这里插入图片描述
回测指标

基准收益率策略收益率夏普比率最大回撤
87.56%3.38%-0.197.31%

可以发现,按照前文规定的买卖策略应用KDJ技术指标的底背离选股策略,其在回测时期内的交易次数可能太少,导致整体收益变动不大。
接下来稍微将买卖策略进行修改,即对于符合底背离条件的股票仍然选取前10只进行买入,而对于符合顶背离条件的股票才进行卖出,得到回测结果:
在这里插入图片描述
回测指标

基准收益率策略收益率夏普比率最大回撤
87.56%160.39%1.1932.76%

回测结果的结果的收益表现非常好,达到160.39%,且夏普比率1.19,终于跑赢了大盘指数,但是仔细看会发现,其该策略的交易记录只有一次的买入,该只股票为“郑州煤电”,股票代码为600121.SHA,有一点像是做了一次“价值投资”,押中了郑州煤电这只股票进行投资。但其实不然,通过对策略的分析,在整个回测时期内,买入这只股票后,其实并没有找到机会将其卖出,直到回测周期结束才不得不卖出。

4. 策略3

4.1 经典的QP优化策略

对于优化问题,经典方法有QP(Quadratic Programming,二次规划)优化方法,二次规划指的是在线性等式、不等式约束条件下的最小化二次函数的问题,该问题的标准形式为:
m i n x 1 2 x T Q x + c T x \qquad min_x \qquad \qquad \frac{1}{2}x^TQx+c^Tx minx21xTQx+cTx
s . t A x = b \qquad s.t \qquad \qquad \qquad Ax=b s.tAx=b
x ≥ 0 \qquad \qquad \qquad \qquad \qquad x≥0 x0

其中, A ∈ R ( m × n ) A∈R^{(m×n)} AR(m×n) b ∈ R m b∈R^m bRm c ∈ R n c∈R^n cRn Q ∈ R ( m × n ) Q∈R^{(m×n)} QR(m×n)给定, x ∈ R n x∈R^n xRn,QP优化是非线性优化问题中的特殊分类,并且线性规划为二次规划的一种特殊。前文所述,在选股完成后,本项目的买卖策略为将现金平均分散给每一只股票,但是这样的资金分配方式很可能不是最优的,所以我们需要对每只股票进行权重优化,这里需要用到QP优化方法求解。具体过程如下:
首先定义变量x=( x 1 x_1 x1, x 2 x_2 x2, ……, x n x_n xn),代表每一只股票的权重,n表示股票池的大小。需要注意一点 ∑ i = 1 n x i = 1 \sum_{i=1}^nx_i=1 i=1nxi=1
然后根据历史数据计算出每只股票的期望收益 μ μ μ = ( μ 1 μ_1 μ1, μ 2 μ_2 μ2,⋯⋯, μ n μ_n μn ),具体计算按照每5个工作日来进行,也就是每5个工作日计算收益,并求这5个数据的平均作为该只股票的期望收益(平均收益)。
假设股票i的收益服从均值为 μ − i \overset{-}μ^i μi ,方差为 σ i σ^i σi的高斯分布,方差 σ i σ^i σi的计算公式为:

σ i = 1 d − 1 ∑ k = 1 d ( μ k i − μ − i ) 2 σ^i=\frac{1}{d-1}\sum_{k=1}^d(μ_k^i-\overset{-}μ^i)^2 σi=d11k=1d(μkiμi)2

其中,d表示样本量,这里d=5; μ − i \overset{-}μ^i μi为股票i在5个工作日内的平均收益; μ k i μ_k^i μki表示股票i在第k个工作日的收益; σ i σ^i σi为股票i在5个工作日内的收益方差。

方差用来度量单个随机变量的离散程度即股票i的收益 μ i μ_i μi的离散程度,而协方差用来刻画两个随机变量的相似程度,假设有另一只股票j的收益也服从均值为 μ − j \overset{-}μ^j μj,方差为 σ i σ^i σi的高斯分布,两个随机变量的协方差计算公式为:

σ ( μ i , μ j ) = 1 d − 1 ∑ k = 1 d ( μ k i − μ − i ) ( μ k j − μ − j ) σ(μ_i, μ_j) = \frac{1}{d-1}\sum_{k=1}^d(μ_k^i-\overset{-}μ^i)(μ_k^j-\overset{-}μ^j) σ(μi,μj)=d11k=1d(μkiμi)(μkjμj)

其中, μ k i μ_k^i μki μ k j μ_k^j μkj分别为股票 i i i 和股票 j j j 在第k个工作日的收益; σ ( μ i , μ j ) σ(μ_i,μ_j ) σ(μi,μj)为股票i和股票j的协方差。由于 i = 1 , … … , n i = 1,……,n i=1,,n j = 1 , … … , n j = 1,……,n j=1,,n,所以对于所有的股票,两两股票收益之间的协方差矩阵计算如下:

∑ = ( σ ( μ 1 , μ 1 ) ⋯ σ ( μ 1 , μ n ) ⋮ ⋱ ⋮ σ ( μ n , μ 1 ) ⋯ σ ( μ n , μ n ) ) \sum =\left( \begin{matrix} σ(μ_1, μ_1) & \cdots & σ(μ_1, μ_n) \\ \vdots & \ddots & \vdots \\ σ(μ_n, μ_1) & \cdots & σ(μ_n, μ_n) \end{matrix} \right) =σ(μ1,μ1)σ(μn,μ1)σ(μ1,μn)σ(μn,μn)
其中, Σ Σ Σ为收益的协方差矩阵,代表股票之间的关系。
为了得到最优的选股策略,我们需要将收益最大化,同时还能最小化风险,一般来说,收益的协方差矩阵可以表征股票收益的风险,具体过程如下:
投资组合的期望收益为:
E [ x ] = μ T x = ( μ 1 , μ 2 , ⋯   , μ n ) ( x 1 x 2 ⋮ x n ) E[x] = μ^Tx = (μ_1,μ_2,\cdots,μ_n) \left( \begin{matrix} x_1 \\ x_2 \\ \vdots \\ x_n \end{matrix} \right) E[x]=μTx=(μ1,μ2,,μn)x1x2xn

投资组合的风险为:
V a r [ x ] = ∑ i , j σ ( μ i , μ j ) x i x j = x T ∑ x Var[x] =\sum_{i,j}σ(μ_i, μ_j)x_ix_j=x^T\sum{}x Var[x]=i,jσ(μi,μj)xixj=xTx

经典QP优化问题为:
m i n x − μ T x + λ 1 2 x T ∑ x \qquad min_x \qquad \qquad -μ^Tx +\lambda\frac{1}{2}x^T\sum{}x minxμTx+λ21xTx
s . t ∑ i = 1 n x i = 1 \qquad s.t \qquad \qquad \qquad \sum_{i=1}^nx_i=1 s.ti=1nxi=1
x i ≥ 0 , i = 1 , 2 , ⋯   , n \qquad \qquad \qquad \qquad \qquad x_i≥0, i = 1,2, \cdots, n xi0,i=1,2,,n

全局变量

import cvxopt as opt
from cvxopt import blas, solvers
import pandas as pd
import numpy as np

# 开始日期
start_date = '2019-01-03'

# 结束日期
end_date = '2021-01-22'

# 策略比较参考标准: 沪深 300
benchmark = '000300.SHA'

# 初始资金
capital_base = 10000

# 读取数据
stock_info = pd.read_csv('stock_list.csv', sep=',', header=None)

# 证券代码列表
instruments = stock_info.iloc[:, 0].tolist()[:40]

print(instruments)

初始化模块(只运行一次)

def initialize(context):
    """初始化虚拟账户状态,只在第一个交易日运行"""

    # 设置手续费,买入和卖出都是0.3%, 不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.003, sell_cost=0.003, min_cost=5))

   

    # 调仓周期(多少个交易日调仓)
    context.trading_period = 20
    
    # 最近多少个交易日时间窗口
    context.observation = 50
    context.ins = instruments

def optimal_portfolio(returns):
    n = len(returns)
    returns = np.asmatrix(returns)

    # 收益的协方差矩阵,代表着股票之间的关系
    S = opt.matrix(np.cov(returns))
    # 每只股票的期望收益
    pbar = opt.matrix(np.mean(returns, axis=1))

    # 权重 -xi <= 0,则 xi >= 0
    G = -opt.matrix(np.eye(n))
    h = opt.matrix(0.0, (n, 1))
    # sum(xi) = 1
    A = opt.matrix(1.0, (1, n))
    b = opt.matrix(1.0)

    solvers.options['show_progress'] = False
    wt = solvers.qp(S, -pbar, G, h, A, b)['x']
    return np.asarray(wt)

执行模块

import talib


def handle_data(context, data):
    """策略交易逻辑,每个交易日运行一次"""

    # context.trading_day_index:交易日序号,第一个交易日为0
    # 在 context.observation 个交易日以后才开始真正运行
    if context.trading_day_index < context.observation:
        return

    # 是否需要调仓
    if context.trading_day_index % context.trading_period != 0:
        return
    
    

    # get the historical data and compute the price change values
    prices = pd.DataFrame()
    for stock in context.ins:
        sid = context.symbol(stock)
        # 调仓:卖出所有持有股票
        # 计算现在portfolio中股票的仓位
        cur_position = context.portfolio.positions[sid].amount

        # 停牌的股票,将不能卖出,将在下一个调仓期处理
        if cur_position > 0 and data.can_trade(sid):
            # 清仓卖出
            context.order_target_value(sid, 0)
        # 每 5 个工作日来计算收益
        price = data.history(sid, 'price', 5, '1d').dropna()
        prices = pd.concat([prices, price], axis=1)
        # print(prices)

    # https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pct_change.html
    # 计算价格变化比例
    returns = prices.pct_change()
    returns.fillna(0, inplace=True)
    returns = returns.iloc[1:, ].values
    # print(returns.shape)
    # print(np.cov(returns))

    # run qp 
    weights = optimal_portfolio(returns.T)
    # print(weights[31], weights[36])

    # update weight for each stock
    cash = context.portfolio.cash
    for stock, weight in zip(prices.columns, weights):
        if data.can_trade(stock):
            price = data.current(stock, 'price')
            context.order(stock, int((weight * cash) / price / 100) * 100)

回测模块

# https://bigquant.com/doc/module_trade.html#M.trade.v3
m_macd = M.trade.v4(
    instruments=instruments,
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=capital_base,
    benchmark=benchmark,
)

基于前文所述买卖策略,在调仓日仍先清仓卖出全部持仓的股票,而选取股票池前40只股票买入,并按照QP优化策略求出每只股票的最优权重进行资金分配,在BigQuant进行回测,得到回测结果:
在这里插入图片描述
回测指标

基准收益率策略收益率夏普比率最大回撤
87.56%110.41%1.037.79%

由上可知,回测收益表现较好,策略收益率为110.41%,夏普比率1.0,但最大回撤超过20%,达到37.79%,风险较高。通过查看交易详情,发现每次调仓日只买入了一两只股票,也就是说每一次进行权重优化后,只有一两只股票的权重占比是比较大的,其余的权重占比太小以至于不足以买入股票,所以导致每次调仓日只买入一两只股票,不利于风险分散,导致风险过高。

5. 策略4

5.1 具有多样性的QP优化策略

对经典的QP优化问题进行改造,考虑更多要素,比如股票选择上的多样性(来抵抗风险),以及股票数目的限制,调仓上的限制等等)。
我们希望得到一个风险较低的股票投资组合,则需要让持仓中的股票具备多样化。由前文可知,按照经典QP优化问题求解,会发现某些股票的权重特别高,其余股票权重比较低,没有起到股票投资组合抵抗风险的效果。另外,由于来自一个板块的股票的涨跌是比较相似的,如果被选中的股票全部来自一个板块,也是没有起到抵抗风险的作用。
所以,对经典QP优化问题的改造思路为:限制一只股票的权重,并且限制来自于同一个板块的股票的权重之和。
改造后具有多样性的QP优化问题为:
m i n x − μ T x + λ 1 2 x T ∑ x \qquad min_x \qquad \qquad -μ^Tx +\lambda\frac{1}{2}x^T\sum{}x minxμTx+λ21xTx
s . t ∑ i = 1 n x i = 1 \qquad s.t \qquad \qquad \qquad \sum_{i=1}^nx_i=1 s.ti=1nxi=1
x i ≥ 0 , i = 1 , 2 , ⋯   , n \qquad \qquad \qquad \qquad \qquad x_i≥0, i = 1,2, \cdots, n xi0,i=1,2,,n
x i ≤ m , i = 1 , 2 , ⋯   , n \qquad \qquad \qquad \qquad \qquad x_i≤m, i = 1,2, \cdots, n xim,i=1,2,,n
∑ i ∈ s e c t o r k x i ≤ m k , i = 1 , 2 , ⋯   , K \qquad \qquad \qquad \qquad \qquad \sum_{i∈sector_k}x_i≤m_k, i = 1,2, \cdots, K isectorkximk,i=1,2,,K

其中, m m m为每只股票的最大权重限制, m = 0.2 m=0.2 m=0.2 m k m_k mk为来自第 k k k个板块股票的最大权重的限制, m k = 0.5 m_k=0.5 mk=0.5

全局变量

import cvxopt as opt
from cvxopt import blas, solvers
import pandas as pd
import numpy as np

# 开始日期
start_date = '2019-01-03'

# 结束日期
end_date = '2021-01-22'

# 策略比较参考标准: 沪深 300
benchmark = '000300.SHA'

# 初始资金
capital_base = 10000

# 读取数据
stock_info = pd.read_csv('stock_list.csv', sep=',', header=None)

# 证券代码列表
instruments = stock_info.iloc[:, 0].tolist()[:40]

print(instruments)

初始化模块(只运行一次)

def initialize(context):
    """初始化虚拟账户状态,只在第一个交易日运行"""

    # 设置手续费,买入和卖出都是0.3%, 不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.003, sell_cost=0.003, min_cost=5))

   

    # 调仓周期(多少个交易日调仓)
    context.trading_period = 20
    
    # 最近多少个交易日时间窗口
    context.observation = 50
    context.ins = instruments

def optimal_portfolio(returns, m, mk):
    
    n = len(returns)
    returns = np.asmatrix(returns)
    # 不同板块
    num_secs = 4
    
    # 收益的协方差矩阵,代表着股票之间的关系
    S = opt.matrix(np.cov(returns))
    # 每只股票的期望收益
    pbar = opt.matrix(np.mean(returns, axis=1))

    # 权重 xi >= 0
    tmp_cons1 = pd.DataFrame(-np.eye(n))
    # 每一只股票权重的最大限制 xi <= m
    tmp_cons2 = pd.DataFrame(np.eye(n))
    G = pd.concat([tmp_cons1, tmp_cons2], axis=0)
    
    # 第 k 个板块的股票权重之和不能超过 mkAdjustType
    a = [1]*10
    b = [0]*10
    for i in range(num_secs):
        tmp = pd.DataFrame(b*i + a + b*(num_secs-1-i)).T  # 每10只股票来自同一个板块
        G = pd.concat([G, tmp], axis=0)
    G = opt.matrix(G.to_numpy())
    
    # 构建 h
    h = []

    # 权重 xi >= 0
    for i in range(n):
        h.append([0.0])

    # 每一只股票权重的最大限制 xi <= 0.2
    for i in range(n): 
        h.append([m])

    # 第 k 个板块的股票权重之和不能超过 0.5
    for i in range(num_secs):
        h.append([mk])

    h = opt.matrix(np.array(h))
    
    # sum(xi) = 1
    A = opt.matrix(1.0, (1, n))
    b = opt.matrix(1.0)
    
    solvers.options['show_progress'] = False
    wt = solvers.qp(S, -pbar, G, h, A, b)['x']
    return np.asarray(wt)

执行模块

import talib


def handle_data(context, data):
    """策略交易逻辑,每个交易日运行一次"""

    # context.trading_day_index:交易日序号,第一个交易日为0
    # 在 context.observation 个交易日以后才开始真正运行
    if context.trading_day_index < context.observation:
        return

    # 是否需要调仓
    if context.trading_day_index % context.trading_period != 0:
        return
    
    

    # get the historical data and compute the price change values
    prices = pd.DataFrame()
    for stock in context.ins:
        sid = context.symbol(stock)
        # 调仓:卖出所有持有股票
        # 计算现在portfolio中股票的仓位
        cur_position = context.portfolio.positions[sid].amount

        # 停牌的股票,将不能卖出,将在下一个调仓期处理
        if cur_position > 0 and data.can_trade(sid):
            # 清仓卖出
            context.order_target_value(sid, 0)
        # 每 5 个工作日来计算收益
        price = data.history(sid, 'price', 5, '1d').dropna()
        prices = pd.concat([prices, price], axis=1)
        # print(prices)

    # https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pct_change.html
    # 计算价格变化比例
    returns = prices.pct_change()
    returns.fillna(0, inplace=True)
    returns = returns.iloc[1:, ].values
    # print(returns.shape)
    # print(np.cov(returns))

    # run qp 
    weights = optimal_portfolio(returns.T, 0.6, 0.7)
    # print(weights[31], weights[36])

    # update weight for each stock
    cash = context.portfolio.cash
    for stock, weight in zip(prices.columns, weights):
        if data.can_trade(stock):
            price = data.current(stock, 'price')
            context.order(stock, int((weight * cash) / price / 100) * 100)

回测模块

# https://bigquant.com/doc/module_trade.html#M.trade.v3
m_macd = M.trade.v4(
    instruments=instruments,
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=capital_base,
    benchmark=benchmark,
)

基于上一节的买卖策略,采用具有多样性的QP优化权重,并同样对前40只股票按权重分配,在BigQuant进行回测,得到回测结果:
在这里插入图片描述
回测指标

基准收益率策略收益率夏普比率最大回撤
87.56%0.08%-0.1422.83%

由上可知,风险分散后,策略收益几乎等于0,相当于保住了本金,但是由于大盘指数一直在变化,不进则退,该策略没有跑过大盘指数,该策略收益表现实际是亏钱的,而且最大回撤超过22.83%,风险并不在可接受范围内。
对每只股票的最大权重和来自同一板块的最大权重之和进行参数分析,可得:

m m m m k m_k mk基准收益率策略收益率夏普比率最大回撤
0.20.387.56%11.47%0.2514.89%
0.20.587.56%0.08%-0.1422.83%
0.20.787.56%-19.89%-1.0236.02%
0.40.387.56%20.11%0.4516.92%
0.40.587.56%-15.12%-0.3943.12%
0.40.787.56%-22.52%-0.5247.84%
0.60.387.56%20.11%0.4516.92%
0.60.587.56%-4.08%-0.0640.0%
0.60.787.56%4.18%0.1244.18%

由上表可知,一般来说,在给定相同的 m m m时,即每只股票最大权重相同时,来自同一板块的最大权重之和越大,其策略收益下降,最大回撤上升,风险变大,表明随着来自同一板块的股票数目上限越大,购买的股票来自同一板块的数量越多,而同一板块的股票具有相似的涨跌,其风险得不到有效分散,一荣俱荣,一损俱损,具有较高的风险。给定相同的mk,即来自同一板块的权重之和一定时,每只股票最大权重越大,总体来说,其最大回撤会越来越大,但收益率没有明显的趋势;这是因为每只股票即使可以分配更大的权重,但是在来自同一板块的权重之和这个限制条件下,每只股票并不一定能分配得到最大的权重,但是权重限制放松,仍会导致更多的资金集中在某几只股票上,不能抵抗风险。

6. 策略5

6.1 技术指标MA多头+经典QP优化

合技术指标MA多头和经典QP优化策略,具体实现如下:首先进行选股,选股标准为:当MA5>MA10,则产生购买信号,将该只股票放入准备要购买的股票池当中。然后利用经典QP优化策略对准备要购买的股票进行权重分配,得到最优权重后即按照求解出来的权重比例进行股票的购买。
全局变量

import cvxopt as opt
from cvxopt import blas, solvers
import pandas as pd
import numpy as np

# 开始日期
start_date = '2019-01-03'

# 结束日期
end_date = '2021-01-22'

# 策略比较参考标准: 沪深 300
benchmark = '000300.SHA'

# 初始资金
capital_base = 10000

# 读取数据
stock_info = pd.read_csv('stock_list.csv', sep=',', header=None)

# 证券代码列表
instruments = stock_info.iloc[:, 0].tolist()[:40]

print(instruments)

初始化模块(只运行一次)

def initialize(context):
    """初始化虚拟账户状态,只在第一个交易日运行"""

    # 设置手续费,买入和卖出都是0.3%, 不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.003, sell_cost=0.003, min_cost=5))
    
    # macd 参数设置
    # dif短线: 12 dif长线: 26 dea: 9
    context.macd_short = 12
    context.macd_long = 26
    context.macd_dea = 9

    # ma参数设置
    # ma短线: 5 ma长线: 20
    context.ma_short = 5 
    context.ma_long = 20
   

    # 调仓周期(多少个交易日调仓)
    context.trading_period = 20
    
    # 最近多少个交易日时间窗口
    context.observation = 50
    # 最大允许持仓股票数为 10 只
    context.max_portfolio_size = 10
    context.ins = instruments


def optimal_portfolio(returns):
    n = len(returns)
    returns = np.asmatrix(returns)

    # 收益的协方差矩阵,代表着股票之间的关系
    S = opt.matrix(np.cov(returns))
    # 每只股票的期望收益
    pbar = opt.matrix(np.mean(returns, axis=1))

    # 权重 -xi <= 0,则 xi >= 0
    G = -opt.matrix(np.eye(n))
    h = opt.matrix(0.0, (n, 1))
    # sum(xi) = 1
    A = opt.matrix(1.0, (1, n))
    b = opt.matrix(1.0)

    solvers.options['show_progress'] = False
    wt = solvers.qp(S, -pbar, G, h, A, b)['x']
    return np.asarray(wt)

执行模块

import talib


def handle_data(context, data):
    """策略交易逻辑,每个交易日运行一次"""

    # context.trading_day_index:交易日序号,第一个交易日为0
    # 在 context.observation 个交易日以后才开始真正运行
    if context.trading_day_index < context.observation:
        return

    # 是否需要调仓
    if context.trading_day_index % context.trading_period != 0:
        return
    
    
    # 选择的股票
    stock_selected = []

    # 依次遍历
    for stock in instruments:    
#         # 当选择的股票数量达到最大股票数,停止运行
#         if len(stock_selected) == context.max_portfolio_size:
#             break

        # 通过股票代码获取股票对象
        sid = context.symbol(stock)

        # 当前价格
        price = data.current(sid, 'price')
        # 如果当前股票没有价格,则运行下一个股票
        if np.isnan(price):
            continue

        # 获取价格数据,获取过去交易日观察窗口的价格数据
        prices = data.history(sid, 'price', context.observation, '1d')

        # https://mrjbq7.github.io/ta-lib/index.html
        # fastperiod 指更短时段的EMA的长度,slowperiod指更长时段的EMA的长度,signalperiod指DEA长度
        macd, signal, hist = talib.MACD(np.array(prices),
                                        context.macd_short,
                                        context.macd_long,
                                        context.macd_dea)

        # 调仓:卖出所有持有股票
        # 计算现在portfolio中股票的仓位
        cur_position = context.portfolio.positions[sid].amount

        # 停牌的股票,将不能卖出,将在下一个调仓期处理
        if cur_position > 0 and data.can_trade(sid):
            # 清仓卖出
            context.order_target_value(sid, 0)

        ma_short = data.history(sid, 'price', context.ma_short, '1d').mean()
        ma_long = data.history(sid, 'price', context.ma_long, '1d').mean()

        # 调仓:买入新的股票
        if ma_short > ma_long and data.can_trade(sid):
            stock_selected.append(stock)
            
    print(len(stock_selected))
    if len(stock_selected) == 0:
        return
    # get the historical data and compute the price change values
    prices = pd.DataFrame()
    for stock in stock_selected: # 遍历经过技术指标筛选的股票
        sid = context.symbol(stock)
        price = data.history(sid, 'price', 5, '1d').dropna()
        prices = pd.concat([prices, price], axis=1)
        # int(prices)

    # https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pct_change.html
    # 计算价格变化比例
    returns = prices.pct_change()
    returns.fillna(0, inplace=True)
    returns = returns.iloc[1:, ].values
    # print(returns)
    # print(np.cov(returns))
    
    cash = context.portfolio.cash
    
    # run qp 
    if len(returns[0]) > 1:
        weights = optimal_portfolio(returns.T)
        for stock, weight in zip(prices.columns, weights):
            if data.can_trade(stock):
                price = data.current(stock, 'price')
                context.order(stock, int((weight * cash) / price / 100) * 100)
    else:
        for stock in prices.columns:
            if data.can_trade(stock):
                price = data.current(stock, 'price')
                context.order(stock, int(cash / price / 100) * 100)
        
    # print(weights[31], weights[36])

    # update weight for each stock
   
    

回测模块

# https://bigquant.com/doc/module_trade.html#M.trade.v3
m_macd = M.trade.v4(
    instruments=instruments,
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=capital_base,
    benchmark=benchmark,
)

如前文所示,基于1.5所述的买卖策略,在BigQuant进行回测,得到回测结果:
在这里插入图片描述
回测指标对比

策略基准收益率策略收益率夏普比率最大回撤
MA多头+经典QP87.56%112.6%1.0931.95%
MA多头87.56%-6.2%-0.2716.23%
经典QP87.56%110.41%1.037.79%

由上可知,经典QP权重优化可以使得收益最大化,但是同时风险也比较大,而MA多头可以控制低风险,但收益不佳;在使用技术指标MA多头的前提下,对选取的股票进行权重优化可以大大提高股票投资组合的期望收益,但同时伴随较大的风险。

6.2 技术指标MACD金叉+经典QP优化

结合技术指标MACD金叉和经典QP优化策略,具体实现如下:首先进行选股,选股标准为:当MACD12上穿MACD26时,产生购买信号,将该只股票放入准备要购买的股票池当中。然后利用经典QP优化策略对准备要购买的股票进行权重分配,得到最优权重后即按照求解出来的权重比例进行股票的购买。
全局变量

import cvxopt as opt
from cvxopt import blas, solvers
import pandas as pd
import numpy as np

# 开始日期
start_date = '2019-01-03'

# 结束日期
end_date = '2021-01-22'

# 策略比较参考标准: 沪深 300
benchmark = '000300.SHA'

# 初始资金
capital_base = 10000

# 读取数据
stock_info = pd.read_csv('stock_list.csv', sep=',', header=None)

# 证券代码列表
instruments = stock_info.iloc[:, 0].tolist()[:40]

print(instruments)

初始化模块(只运行一次)

def initialize(context):
    """初始化虚拟账户状态,只在第一个交易日运行"""

    # 设置手续费,买入和卖出都是0.3%, 不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.003, sell_cost=0.003, min_cost=5))
    
    # macd 参数设置
    # dif短线: 12 dif长线: 26 dea: 9
    context.macd_short = 12
    context.macd_long = 26
    context.macd_dea = 9

    # ma参数设置
    # ma短线: 5 ma长线: 20
    context.ma_short = 5 
    context.ma_long = 20
   

    # 调仓周期(多少个交易日调仓)
    context.trading_period = 20
    
    # 最近多少个交易日时间窗口
    context.observation = 50
    # 最大允许持仓股票数为 10 只
    context.max_portfolio_size = 10
    context.ins = instruments


def optimal_portfolio(returns):
    n = len(returns)
    returns = np.asmatrix(returns)

    # 收益的协方差矩阵,代表着股票之间的关系
    S = opt.matrix(np.cov(returns))
    # 每只股票的期望收益
    pbar = opt.matrix(np.mean(returns, axis=1))

    # 权重 -xi <= 0,则 xi >= 0
    G = -opt.matrix(np.eye(n))
    h = opt.matrix(0.0, (n, 1))
    # sum(xi) = 1
    A = opt.matrix(1.0, (1, n))
    b = opt.matrix(1.0)

    solvers.options['show_progress'] = False
    wt = solvers.qp(S, -pbar, G, h, A, b)['x']
    return np.asarray(wt)

执行模块

import talib


def handle_data(context, data):
    """策略交易逻辑,每个交易日运行一次"""

    # context.trading_day_index:交易日序号,第一个交易日为0
    # 在 context.observation 个交易日以后才开始真正运行
    if context.trading_day_index < context.observation:
        return

    # 是否需要调仓
    if context.trading_day_index % context.trading_period != 0:
        return
    
    
    # 选择的股票
    stock_selected = []

    # 依次遍历
    for stock in instruments:    
#         # 当选择的股票数量达到最大股票数,停止运行
#         if len(stock_selected) == context.max_portfolio_size:
#             break

        # 通过股票代码获取股票对象
        sid = context.symbol(stock)

        # 当前价格
        price = data.current(sid, 'price')
        # 如果当前股票没有价格,则运行下一个股票
        if np.isnan(price):
            continue

        # 获取价格数据,获取过去交易日观察窗口的价格数据
        prices = data.history(sid, 'price', context.observation, '1d')

        # https://mrjbq7.github.io/ta-lib/index.html
        # fastperiod 指更短时段的EMA的长度,slowperiod指更长时段的EMA的长度,signalperiod指DEA长度
        macd, signal, hist = talib.MACD(np.array(prices),
                                        context.macd_short,
                                        context.macd_long,
                                        context.macd_dea)

        # 调仓:卖出所有持有股票
        # 计算现在portfolio中股票的仓位
        cur_position = context.portfolio.positions[sid].amount

        # 停牌的股票,将不能卖出,将在下一个调仓期处理
        if cur_position > 0 and data.can_trade(sid):
            # 清仓卖出
            context.order_target_value(sid, 0)

        ma_short = data.history(sid, 'price', context.ma_short, '1d').mean()
        ma_long = data.history(sid, 'price', context.ma_long, '1d').mean()

        # 调仓:买入新的股票
        # 买入逻辑 macd 上穿 signal
        if (macd[-1] - signal[-1] > 0) and (macd[-2] - signal[-2] < 0):
            stock_selected.append(stock)
    
    print(len(stock_selected))
    if len(stock_selected) == 0:
        return
    # get the historical data and compute the price change values
    prices = pd.DataFrame()
    for stock in stock_selected: # 遍历经过技术指标筛选的股票
        sid = context.symbol(stock)
        price = data.history(sid, 'price', 5, '1d').dropna()
        prices = pd.concat([prices, price], axis=1)
        # int(prices)

    # https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pct_change.html
    # 计算价格变化比例
    returns = prices.pct_change()
    returns.fillna(0, inplace=True)
    returns = returns.iloc[1:, ].values
    # print(returns)
    # print(np.cov(returns))
    
    cash = context.portfolio.cash
    
    # run qp 
    if len(returns[0]) > 1:
        weights = optimal_portfolio(returns.T)
        for stock, weight in zip(prices.columns, weights):
            if data.can_trade(stock):
                price = data.current(stock, 'price')
                context.order(stock, int((weight * cash) / price / 100) * 100)
    else:
        for stock in prices.columns:
            if data.can_trade(stock):
                price = data.current(stock, 'price')
                context.order(stock, int(cash / price / 100) * 100)
        
    # print(weights[31], weights[36])

    # update weight for each stock
   
    

回测模块

# https://bigquant.com/doc/module_trade.html#M.trade.v3
m_macd = M.trade.v4(
    instruments=instruments,
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=capital_base,
    benchmark=benchmark,
)

基于1.5所述的买卖策略,在BigQuant进行回测,得到回测结果:
在这里插入图片描述
回测指标对比

策略基准收益率策略收益率夏普比率最大回撤
MACD金叉+经典QP87.56%-11.25%-0.1829.33%
MACD金叉87.56%-45.91%-1.1153.42%
经典QP87.56%110.41%1.037.79%

由上可知,经典QP权重优化可以让单一的技术指标策略获得更高的收益,而对于原本较高风险的MACD策略,融合经典QP权重优化可以有效降低风险。

6.3 技术指标MACD金叉+MA多头+经典QP优化

结合技术指标MACD金叉、MA多头和经典QP优化策略,具体实现如下:首先进行选股,选股标准为:在MA5 > MA10 且 MACD12上穿MACD26时,为购买信号,将该只股票放入准备要购买的股票池当中。然后利用经典QP优化策略对准备要购买的股票进行权重分配,得到最优权重后即按照求解出来的权重比例进行股票的购买。
全局变量

import cvxopt as opt
from cvxopt import blas, solvers
import pandas as pd
import numpy as np

# 开始日期
start_date = '2019-01-03'

# 结束日期
end_date = '2021-01-22'

# 策略比较参考标准: 沪深 300
benchmark = '000300.SHA'

# 初始资金
capital_base = 10000

# 读取数据
stock_info = pd.read_csv('stock_list.csv', sep=',', header=None)

# 证券代码列表
instruments = stock_info.iloc[:, 0].tolist()

print(instruments)

初始化模块(只运行一次)

def initialize(context):
    """初始化虚拟账户状态,只在第一个交易日运行"""

    # 设置手续费,买入和卖出都是0.3%, 不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.003, sell_cost=0.003, min_cost=5))
    
    # macd 参数设置
    # dif短线: 12 dif长线: 26 dea: 9
    context.macd_short = 12
    context.macd_long = 26
    context.macd_dea = 9

    # ma参数设置
    # ma短线: 5 ma长线: 20
    context.ma_short = 5 
    context.ma_long = 20
   

    # 调仓周期(多少个交易日调仓)
    context.trading_period = 20
    
    # 最近多少个交易日时间窗口
    context.observation = 50
    # 最大允许持仓股票数为 10 只
    context.max_portfolio_size = 10
    context.ins = instruments


def optimal_portfolio(returns):
    n = len(returns)
    returns = np.asmatrix(returns)

    # 收益的协方差矩阵,代表着股票之间的关系
    S = opt.matrix(np.cov(returns))
    # 每只股票的期望收益
    pbar = opt.matrix(np.mean(returns, axis=1))

    # 权重 -xi <= 0,则 xi >= 0
    G = -opt.matrix(np.eye(n))
    h = opt.matrix(0.0, (n, 1))
    # sum(xi) = 1
    A = opt.matrix(1.0, (1, n))
    b = opt.matrix(1.0)

    solvers.options['show_progress'] = False
    wt = solvers.qp(S, -pbar, G, h, A, b)['x']
    return np.asarray(wt)

执行模块

import talib


def handle_data(context, data):
    """策略交易逻辑,每个交易日运行一次"""

    # context.trading_day_index:交易日序号,第一个交易日为0
    # 在 context.observation 个交易日以后才开始真正运行
    if context.trading_day_index < context.observation:
        return

    # 是否需要调仓
    if context.trading_day_index % context.trading_period != 0:
        return
    
    
    # 选择的股票
    stock_selected = []

    # 依次遍历
    for stock in instruments:    
#         # 当选择的股票数量达到最大股票数,停止运行
#         if len(stock_selected) == context.max_portfolio_size:
#             break

        # 通过股票代码获取股票对象
        sid = context.symbol(stock)

        # 当前价格
        price = data.current(sid, 'price')
        # 如果当前股票没有价格,则运行下一个股票
        if np.isnan(price):
            continue

        # 获取价格数据,获取过去交易日观察窗口的价格数据
        prices = data.history(sid, 'price', context.observation, '1d')

        # https://mrjbq7.github.io/ta-lib/index.html
        # fastperiod 指更短时段的EMA的长度,slowperiod指更长时段的EMA的长度,signalperiod指DEA长度
        macd, signal, hist = talib.MACD(np.array(prices),
                                        context.macd_short,
                                        context.macd_long,
                                        context.macd_dea)

        # 调仓:卖出所有持有股票
        # 计算现在portfolio中股票的仓位
        cur_position = context.portfolio.positions[sid].amount

        # 停牌的股票,将不能卖出,将在下一个调仓期处理
        if cur_position > 0 and data.can_trade(sid):
            # 清仓卖出
            context.order_target_value(sid, 0)

        ma_short = data.history(sid, 'price', context.ma_short, '1d').mean()
        ma_long = data.history(sid, 'price', context.ma_long, '1d').mean()

        # 调仓:买入新的股票
        # 买入逻辑 macd 上穿 signal
        if ma_short > ma_long and data.can_trade(sid):
            if (macd[-1] - signal[-1] > 0) and (macd[-2] - signal[-2] < 0):
                stock_selected.append(stock)
    
    print(len(stock_selected))
    if len(stock_selected) == 0:
        return
    # get the historical data and compute the price change values
    prices = pd.DataFrame()
    for stock in stock_selected: # 遍历经过技术指标筛选的股票
        sid = context.symbol(stock)
        price = data.history(sid, 'price', 5, '1d').dropna()
        prices = pd.concat([prices, price], axis=1)
        # int(prices)

    # https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.pct_change.html
    # 计算价格变化比例
    returns = prices.pct_change()
    returns.fillna(0, inplace=True)
    returns = returns.iloc[1:, ].values
    # print(returns)
    # print(np.cov(returns))
    
    cash = context.portfolio.cash
    
    # run qp 
    if len(returns[0]) > 1:
        weights = optimal_portfolio(returns.T)
        for stock, weight in zip(prices.columns, weights):
            if data.can_trade(stock):
                price = data.current(stock, 'price')
                context.order(stock, int((weight * cash) / price / 100) * 100)
    else:
        for stock in prices.columns:
            if data.can_trade(stock):
                price = data.current(stock, 'price')
                context.order(stock, int(cash / price / 100) * 100)
        
    # print(weights[31], weights[36])

    # update weight for each stock
   
    

回测模块

# https://bigquant.com/doc/module_trade.html#M.trade.v3
m_macd = M.trade.v4(
    instruments=instruments,
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=capital_base,
    benchmark=benchmark,
)

基于1.5所述的买卖策略,在BigQuant进行回测,得到回测结果:
在这里插入图片描述
回测指标对比

策略基准收益率策略收益率夏普比率最大回撤
MACD金叉+MA多头+经典QP87.56%45.49%0.631.74%
MACD金叉+MA多头87.56%47.32%0.6433.3%
经典QP87.56%110.41%1.037.79%

由上可知,经过经典QP权重优化的MACD金叉和MA多头策略的收益基本维持一个水平但略有下降,最大回撤也是在同一水平但也是略有下降。

附录

股票池代码

000021.SZA, 710000
000034.SZA, 710000
000066.SZA, 710000
000158.SZA, 710000
000555.SZA, 710000
000606.SZA, 710000
000662.SZA, 710000
000938.SZA, 710000
000948.SZA, 710000
000606.SZA, 710000
300157.SZA, 210000
300164.SZA, 210000
300191.SZA, 210000
600121.SHA, 210000
600123.SHA, 210000
600157.SHA, 210000
600188.SHA, 210000
600193.SHA, 210000
600295.SHA, 210000
600339.SHA, 210000
300610.SZA, 220000
300637.SZA, 220000
300641.SZA, 220000
300655.SZA, 220000
300665.SZA, 220000
300690.SZA, 220000
300699.SZA, 220000
300716.SZA, 220000
300717.SZA, 220000
300721.SZA, 220000
000060.SZA, 240000
000426.SZA, 240000
000511.SZA, 240000
000603.SZA, 240000
000612.SZA, 240000
000630.SZA, 240000
000633.SZA, 240000
000657.SZA, 240000
000688.SZA, 240000
000693.SZA, 240000
300592.SZA, 620000
300621.SZA, 620000
300635.SZA, 620000
300649.SZA, 620000
300668.SZA, 620000
600039.SHA, 620000
600068.SHA, 620000
600133.SHA, 620000
600170.SHA, 620000
600209.SHA, 620000
603218.SHA, 630000
603320.SHA, 630000
603333.SHA, 630000
603396.SHA, 630000
603416.SHA, 630000
603488.SHA, 630000
603507.SHA, 630000
603577.SHA, 630000
603606.SHA, 630000
603488.SHA, 630000
300403.SZA, 330000
300475.SZA, 330000
600060.SHA, 330000
600336.SHA, 330000
600619.SHA, 330000
600690.SHA, 330000
600839.SHA, 330000
600854.SHA, 330000
600870.SHA, 330000
600983.SHA, 330000
600867.SHA, 370000
600896.SHA, 370000
600976.SHA, 370000
600993.SHA, 370000
600998.SHA, 370000
601607.SHA, 370000
603079.SHA, 370000
603108.SHA, 370000
603127.SHA, 370000
603139.SHA, 370000
002351.SZA, 270000
002369.SZA, 270000
002371.SZA, 270000
002388.SZA, 270000
002384.SZA, 270000
002402.SZA, 270000
002414.SZA, 270000
002463.SZA, 270000
002475.SZA, 270000
002484.SZA, 270000
002711.SZA, 420000
002769.SZA, 420000
002800.SZA, 420000
002889.SZA, 420000
300013.SZA, 420000
300240.SZA, 420000
300350.SZA, 420000
600004.SHA, 420000
600009.SHA, 420000
600026.SHA, 420000
601128.SHA, 480000
601166.SHA, 480000
601169.SHA, 480000
601229.SHA, 480000
601288.SHA, 480000
601398.SHA, 480000
601328.SHA, 480000
601818.SHA, 480000
601939.SHA, 480000
601988.SHA, 480000
600804.SHA, 730000
603042.SHA, 730000
603083.SHA, 730000
603118.SHA, 730000
603322.SHA, 730000
603421.SHA, 730000
603559.SHA, 730000
603602.SHA, 730000
603703.SHA, 730000
603803.SHA, 730000

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值