量化投资(ETF轮动)

雪球上看到一个大V的ETF轮动策略,回测效果还不错,比较适合个人投资者使用。

【策略思想】
针对多只指数基金,以等权重方式持有符合买入条件的基金,最高同时持有3只基金。没有基金符合要求时空仓
【策略理论依据】
轮动策略的理论基础是动量效应,也就是处于上涨状态的基金会在一定时间内保持上涨趋势。
【买入条件】(两个条件全部满足才买入)
1、近13个交易日涨幅排名前三(设置涨幅阈值为0.1%),选择最强势的基金;
2、当前价大于13日均线,主要用于过滤假突破信号。
【卖出条件】(三个条件满足一个就卖出)
1、近13个交易日涨幅排名未入前三(先剔除不符合买入条件的基金再排序);
2、近13个交易日涨幅不足0.1%;
3、当前价小于近13个交易日均线
4、上证指数连续6日不过7日量线,无条件卖出,提前出场等待

基于动量的轮动是一种偏进攻型的策略,不追求高胜率,核心逻辑在于“多赚少亏”,整体盈利。

轮动策略的“少亏”是通过轮动换仓实现的,但是我们发现基础策略的回撤幅度仍然是非常大的(超过35%),通过同时持有多个标的分散风险,我们把回撤控制在了25%以内。

这个策略如果改了运行时间的话,回测结果差异非常大,把交易时间设置在下午14:30以后会比较好。难道是因为我们的市场在收盘前半个小时经常出现逆趋势的波动?例如处于上涨周期的,经常尾盘跳水,处于下降周期的,又经常尾盘拉高,这样一买一卖,差价就出来了。这应该有一个统计学上的解释。但如果真是如此,这就是一个值得注意的策略钝化的潜在风险,没有办法保证我们的市场风格会一直如此。

不同时间段的回测收益曲线如下:

image.png
image.png
image.png

ETF轮动
'''信号判断:价格不低于13日均线,且价格相对于13日前上涨,综合评分排名第一或者低于第一不超过阈值
止损信号:收益率峰值下跌20%点位,卖出全部,冷却3天
确认信号:11:30
交易时间:14:40
均线周期:13
标题:沪深宽基ETF轮动策略低回撤
'''

from jqdata import *

=================================================
总体回测前设置参数和回测
=================================================

def initialize(context):
set_params()    #1设置策参数
set_variables() #2设置中间变量
set_backtest()  #3设置回测条件
set_slippage(FixedSlippage(0))
set_order_cost(OrderCost(open_tax=0, close_tax=0, \
    open_commission=0.0005, close_commission=0.0005,\
    close_today_commission=0, min_commission=5), type='fund')
run_daily(ETFtrade1, time='11:30')
run_daily(ETFtrade2, time='14:40')

1 设置参数

def set_params():
# 设置基准收益
set_benchmark('000300.XSHG')
g.returnsRate = 0  #峰值收益率初始化
g.CoolingOff = 0  #冷却期
g.signal = 'KEEP'  #交易信号初始化
g.lag = 13  #均线周期
g.shift = 0.2  #设置涨幅%偏差过滤阈值
g.last = '0' #持仓股票代码初始化
g.ETFList = np.array([
    ['399006.XSHE','159915.XSHE'], #创业板
    ['000300.XSHG','510300.XSHG'], #沪深300
    ['000905.XSHG','510500.XSHG'], #中证500
    #['399330.XSHE','159901.XSHE'], #深证100
    ['510880.XSHG','510880.XSHG'], #红利ETF
    #['511010.XSHG','511010.XSHG'], #国债ETF
    #['518880.XSHG','518880.XSHG'], #黄金ETF
    ['399932.XSHE','159928.XSHE'] #消费ETF
])

2 设置中间变量

def set_variables():
return

3 设置回测条件

def set_backtest():
set_option('use_real_price', True) #用真实价格交易
log.set_level('order', 'error')

=================================================
每日交易时
=================================================

def ETFtrade1(context):
g.signal = get_signal(context)

def ETFtrade2(context):
for stock in context.portfolio.positions.keys():
    if stock not in g.last:
        log.info("正在卖出遗留基金 %s" % stock)
        order_target_value(stock, 0)
if g.signal == 'sell_the_stocks':
    sell_the_stocks(context)
elif g.signal == 'KEEP':
    log.info("交易信号:持仓不变")
else:
    sell_the_stocks(context)    
    buy_the_stocks(context,g.signal)

5 获取信号

def get_signal(context):
if KeepReturns(context): # 达到止损条件后发出空仓信号
    if g.last == '0':# 持仓为空
        log.info("交易信号:冷却期保持空仓状态")
        return 'KEEP'# 持仓保持不变
    else:# 持仓不为空
        log.info("交易信号:收益率下跌超20%,空仓止损")
        g.last = '0'
        return 'sell_the_stocks'

i=0 # 计数器初始化
# dapan_stoploss() # 调用大盘止损函数设置均线周期
# 创建保持计算结果的DataFrame
df = pd.DataFrame()
for row in g.ETFList:
    security = row[1]
# 获取股票的收盘价
    close_data = attribute_history(security, g.lag, '1d', ['close'],df=False)
# 获取股票现价
    current_data = get_current_data()
    current_price = current_data[security].last_price
# 获取股票的阶段收盘价涨幅
    cp_increase = (current_price/close_data['close'][0]-1)*100
# 取得过去 g.lag 天的平均价格
    ma_n1 = close_data['close'].mean()
# 计算前一收盘价与均值差值    
    pre_price = (current_price/ma_n1-1)*100
    df.loc[i,'股票代码'] = row[1] # 把标的股票代码添加到DataFrame
    df.loc[i,'股票名称'] = get_security_info(row[1]).display_name # 把标的股票名称添加到DataFrame
    df.loc[i,'周期涨幅%'] = cp_increase # 把计算结果添加到DataFrame
    df.loc[i,'均线差值%'] = pre_price # 把计算结果添加到DataFrame
    i=i+1

# 删除不符合要求的标的
for t in df.index:
    if df.loc[t,'周期涨幅%'] < 0 or df.loc[t,'均线差值%'] < 0:
    #if df.loc[t,'均线差值'] < 0:    
        df=df.drop(t)


# 对计算结果表格进行从大到小排序
df.sort_values(by='周期涨幅%',ascending=False,inplace=True) # 按照涨幅排序
df.reset_index(drop=True, inplace=True) # 重新设置索引
df['周期涨幅%'].apply(lambda x:'%.2f' %x)
df['均线差值%'].apply(lambda x:'%.2f' %x)
log.info("行情统计结果表:\n%s" % (df))

if df.empty: # 表为空
    if g.last == '0':# 持仓为空
        log.info("交易信号:继续保持空仓状态")
        return 'KEEP'# 持仓保持不变
    else:# 持仓不为空
        log.info("交易信号:空仓")
        g.last = '0'
        return 'sell_the_stocks' # 当前价格低于均线卖出股票

elif g.last == '0': # 表不为空,持仓为空,购买排名第一股
    stockcode = str(df.iloc[0,0])
    g.last = stockcode
    log.info("交易信号:买入 %s" % (stockcode))
    return stockcode
    
elif g.last != '0': # 表不为空 持仓不为空
    if g.last not in df['股票代码'].values: # 如果持仓股不在表中,购买排名第一股
        stockcode = str(df.iloc[0,0])
        g.last = stockcode
        log.info("交易信号:买入 %s" % (stockcode))
        return stockcode
    if g.last in df['股票代码'].values:# 如果持仓股在表中
        for t in df.index: # 取得持仓股涨幅
            if df.loc[t,'股票代码'] == g.last:
                temp = df.loc[t,'周期涨幅%']
        if df.iloc[0,2] - temp < g.shift:  # 排名第一股涨幅差距低于阈值,返回继续持仓
            return 'KEEP' # 持仓保持不变
        else: # 排名第一股涨幅差距大于阈值,换股
            stockcode = str(df.iloc[0,0])
            g.last = stockcode
            log.info("交易信号:买入 %s" % (stockcode))
            return stockcode

卖出股票

def sell_the_stocks(context):
for stock in context.portfolio.positions.keys():
    return (log.info("正在卖出 %s" % stock), order_target_value(stock, 0))

买入股票

def buy_the_stocks(context,signal):
return (log.info("正在买入 %s"% signal 
),order_value(signal,context.portfolio.cash))

收益止损函数

def KeepReturns(context):
if g.CoolingOff > 0:
    g.CoolingOff = g.CoolingOff - 1
    return True
else:
    current_returns = context.portfolio.returns
    if current_returns > g.returnsRate:
        g.returnsRate = current_returns
        log.info("最高收益更新:{:.2%}".format(current_returns))
        return False
    elif current_returns - g.returnsRate > -0.2:
        return False
    else:
        current_returns - g.returnsRate-1 <= -0.2
        g.returnsRate = current_returns
        log.info("最高收益更新:{:.2%}".format(current_returns))
        g.CoolingOff = 3
        return True

=================================================
每日收盘后
=================================================

def after_trading_end(context):
log.info('今日持仓情况:%s',context.portfolio.positions.keys())
print("总权益:{:.2f}万".format(context.portfolio.total_value/10000))
return

你可能感兴趣的:(量化投资(ETF轮动))