雪球上看到一個大V的ETF輪動策略,回測效果還不錯,比較適合個人投資者使用。
【策略思想】
針對多只指數(shù)基金,以等權重方式持有符合買入條件的基金,最高同時持有3只基金。沒有基金符合要求時空倉
【策略理論依據(jù)】
輪動策略的理論基礎是動量效應,也就是處于上漲狀態(tài)的基金會在一定時間內(nèi)保持上漲趨勢。
【買入條件】(兩個條件全部滿足才買入)
1、近13個交易日漲幅排名前三(設置漲幅閾值為0.1%),選擇最強勢的基金;
2、當前價大于13日均線,主要用于過濾假突破信號。
【賣出條件】(三個條件滿足一個就賣出)
1、近13個交易日漲幅排名未入前三(先剔除不符合買入條件的基金再排序);
2、近13個交易日漲幅不足0.1%;
3、當前價小于近13個交易日均線
4、上證指數(shù)連續(xù)6日不過7日量線,無條件賣出,提前出場等待
基于動量的輪動是一種偏進攻型的策略,不追求高勝率,核心邏輯在于“多賺少虧”,整體盈利。
輪動策略的“少虧”是通過輪動換倉實現(xiàn)的,但是我們發(fā)現(xiàn)基礎策略的回撤幅度仍然是非常大的(超過35%),通過同時持有多個標的分散風險,我們把回撤控制在了25%以內(nèi)。
這個策略如果改了運行時間的話,回測結果差異非常大,把交易時間設置在下午14:30以后會比較好。難道是因為我們的市場在收盤前半個小時經(jīng)常出現(xiàn)逆趨勢的波動?例如處于上漲周期的,經(jīng)常尾盤跳水,處于下降周期的,又經(jīng)常尾盤拉高,這樣一買一賣,差價就出來了。這應該有一個統(tǒng)計學上的解釋。但如果真是如此,這就是一個值得注意的策略鈍化的潛在風險,沒有辦法保證我們的市場風格會一直如此。
不同時間段的回測收益曲線如下:



ETF輪動
'''信號判斷:價格不低于13日均線,且價格相對于13日前上漲,綜合評分排名第一或者低于第一不超過閾值
止損信號:收益率峰值下跌20%點位,賣出全部,冷卻3天
確認信號:11:30
交易時間:14:40
均線周期:13
標題:滬深寬基ETF輪動策略低回撤
'''
from jqdata import *
=================================================
總體回測前設置參數(shù)和回測
=================================================
def initialize(context):
set_params() #1設置策參數(shù)
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 設置參數(shù)
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'], #創(chuàng)業(yè)板
['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): # 達到止損條件后發(fā)出空倉信號
if g.last == '0':# 持倉為空
log.info("交易信號:冷卻期保持空倉狀態(tài)")
return 'KEEP'# 持倉保持不變
else:# 持倉不為空
log.info("交易信號:收益率下跌超20%,空倉止損")
g.last = '0'
return 'sell_the_stocks'
i=0 # 計數(shù)器初始化
# dapan_stoploss() # 調(diào)用大盤止損函數(shù)設置均線周期
# 創(chuàng)建保持計算結果的DataFrame
df = pd.DataFrame()
for row in g.ETFList:
security = row[1]
# 獲取股票的收盤價
close_data = attribute_history(security, g.lag, '1d', ['close'],df=False)
# 獲取股票現(xiàn)價
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("行情統(tǒng)計結果表:\n%s" % (df))
if df.empty: # 表為空
if g.last == '0':# 持倉為空
log.info("交易信號:繼續(xù)保持空倉狀態(tài)")
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: # 排名第一股漲幅差距低于閾值,返回繼續(xù)持倉
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))
收益止損函數(shù)
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