雪球上看到一个大V的ETF轮动策略,回测效果还不错,比较适合个人投资者使用。
【策略思想】
针对多只指数基金,以等权重方式持有符合买入条件的基金,最高同时持有3只基金。没有基金符合要求时空仓
【策略理论依据】
轮动策略的理论基础是动量效应,也就是处于上涨状态的基金会在一定时间内保持上涨趋势。
【买入条件】(两个条件全部满足才买入)
1、近13个交易日涨幅排名前三(设置涨幅阈值为0.1%),选择最强势的基金;
2、当前价大于13日均线,主要用于过滤假突破信号。
【卖出条件】(三个条件满足一个就卖出)
1、近13个交易日涨幅排名未入前三(先剔除不符合买入条件的基金再排序);
2、近13个交易日涨幅不足0.1%;
3、当前价小于近13个交易日均线
4、上证指数连续6日不过7日量线,无条件卖出,提前出场等待
基于动量的轮动是一种偏进攻型的策略,不追求高胜率,核心逻辑在于“多赚少亏”,整体盈利。
轮动策略的“少亏”是通过轮动换仓实现的,但是我们发现基础策略的回撤幅度仍然是非常大的(超过35%),通过同时持有多个标的分散风险,我们把回撤控制在了25%以内。
这个策略如果改了运行时间的话,回测结果差异非常大,把交易时间设置在下午14:30以后会比较好。难道是因为我们的市场在收盘前半个小时经常出现逆趋势的波动?例如处于上涨周期的,经常尾盘跳水,处于下降周期的,又经常尾盘拉高,这样一买一卖,差价就出来了。这应该有一个统计学上的解释。但如果真是如此,这就是一个值得注意的策略钝化的潜在风险,没有办法保证我们的市场风格会一直如此。
不同时间段的回测收益曲线如下:
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