Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

回测示例求教:基于RSRS的ETF轮动策略(closed) #1814

Closed
wangyajieI opened this issue Oct 30, 2022 · 1 comment
Closed

回测示例求教:基于RSRS的ETF轮动策略(closed) #1814

wangyajieI opened this issue Oct 30, 2022 · 1 comment

Comments

@wangyajieI
Copy link

天神及在座的各位大神好:
quantaxis python版后期将QAStrategy merge到quantaxis工程了,但是好像没有回测示例。近期在聚宽上看到“基于RSRS的ETF轮动策略”此策略,由于其核心理念是多ETF轮动选最强,相对单一股票指标来说较为不同,天神及各位大佬是否有时间用quantaxis实现一下,相信是给广大入门同学一个非常不错的参考案例🎁
聚宽代码敬上:

# 克隆自聚宽文章:https://www.joinquant.com/post/38643
# 标题:ETF轮动策略升级-增加类别-低回撤
# 作者:长鸿

# 克隆自聚宽文章:https://www.joinquant.com/post/35959
# 标题:ETF轮动策略升级-多类别-低回撤
# 作者:宋兵乙

#修改几个ETF


# 调整为每日收盘后运行计算交易信号,第二个交易日进行交易
from jqdata import *
import pandas as pd
import talib as ta
import smtplib
from email.header import Header
from email.mime.text import MIMEText

import prettytable as pt

def initialize(context):
    
    g.purchases = []
    g.sells = []
    # 设置交易参数
    set_params()
    
    set_option("avoid_future_data", True)
    set_option('use_real_price', True)      # 用真实价格交易
    set_benchmark('000300.XSHG')
    log.set_level('order', 'error')
    #
    # 将滑点设置为0,表示成交价等于委托价
    set_slippage(FixedSlippage(0))
    # 手续费: 采用系统默认设置
    # open_tax、close_tax 买入和卖出印花税,基金与期货不收
    # open_commission、open_commission 买入和卖出时佣金
    set_order_cost(OrderCost(open_tax=0, close_tax=0, \
        open_commission=0.00015, close_commission=0.00015,\
        close_today_commission=0, min_commission=0), type='stock')
        
    # 开盘前运行
    # 计算出符合交易日期的基金symbol,此方案采用均线,因此就是在回溯13日(短期均线日期)之前上市的基金和股票集合
    run_daily(before_market_open, time='21:00', reference_security='000300.XSHG')
 
    # 21:00 计算交易信号
    run_daily(get_signal, time='21:00')
    # 9:35 进行交易
    run_daily(ETF_trade, time='9:32')


# 设置参数
def set_params():

    g.target_market = '000300.XSHG'
    
    g.moment_period = 13                # 计算行情趋势的短期均线
    g.ma_period = 10                    # 计算行情趋势的长期均线
    
    g.type_num = 5                      # 品种数量

    g.ETF_targets =  {
        # # A股指数ETF
        '000300.XSHG':'510300.XSHG',        # 沪深300
        '399006.XSHE':'159915.XSHE',        # 创业板
        '512690.XSHG':'512690.XSHG',        # 酒ETF
        '512480.XSHG':'512480.XSHG',        # 半导体ETF
        '515700.XSHG':'515700.XSHG',         # 新能车ETF
        '512760.XSHG':'515760.XSHG',        # 芯片ETF
        '516780.XSHG':'516780.XSHG',        # 稀土ETF
        '512220.XSHG':'512220.XSHG',        # 煤炭ETF
        '516880.XSHG':'516880.XSHG',        # 光伏50ETF
        '515210.XSHG':'515210.XSHG',        # 钢铁ETF
        '512880.XSHG':'512880.XSHG',        # 证券ETF
        '513050.XSHG':'513050.XSHG',        # 中概互联ETF
        '512660.XSHG':'512660.XSHG',        # 军工ETF
        '159992.XSHE':'159992.XSHE',        # 创新药ETF
        '588050.XSHG':'588050.XSHG',        # 科创ETF



        # # 国际期货
        '518880.XSHG':'518880.XSHG',        # 黄金ETF
        '161226.XSHE':'161226.XSHE',        # 白银基金
        
        # # 国内期货
        '159985.XSHE':'159985.XSHE',        # 豆粕ETF
        '159981.XSHE':'159981.XSHE',        # 能源化工ETF
        '159980.XSHE':'159980.XSHE',        # 有色期货
        
        # # 全球股指
        '513100.XSHG':'513100.XSHG',        # 纳斯达克ETF
        '513030.XSHG':'513030.XSHG',        # 德国ETF
        '513520.XSHG':'513520.XSHG',        # 日经ETF
        '164824.XSHE':'164824.XSHE',        # 印度基金

    }
    
    # A股指数
    g.local_stocks  = [
        '510300.XSHG',        # 沪深300
        '159915.XSHE',        # 创业板
        '512690.XSHG',        # 酒ETF
        '512480.XSHG',        # 半导体ETF
        '515700.XSHG',        # 新能车ETF
        '515760.XSHG',        # 芯片ETF
        '516780.XSHG',        # 稀土ETF
        '512220.XSHG',        # 煤炭ETF
        '516880.XSHG',        # 光伏50ETF
        '515210.XSHG',        # 钢铁ETF
        '512880.XSHG',        # 证券ETF
        #'513050.XSHG',        # 中概互联ETF
        '512660.XSHG',        # 军工ETF
        '159992.XSHE',        # 创新药ETF
        '588050.XSHG',        # 科创ETF
        
        ]
    # 全球股指
    g.global_stocks = [
        '513100.XSHG',        # 纳斯达克ETF
        '164824.XSHE',        # 印度基金
        '513030.XSHG',        # 德国ETF
        '513520.XSHG',        # 日经ETF
    ]
    # 国内期货
    g.local_futures = [
        '159980.XSHE',        # 有色期货
        '159981.XSHE',        # 能源化工ETF
        '159985.XSHE',        # 豆粕ETF
        
        ]
    # 全球期货
    g.global_futures = [
        '161226.XSHE',        # 白银基金
        '518880.XSHG',        # 黄金ETF
        # '501018.XSHG',        # 南方原油
        #  '180101',        # 蛇口产业园
        #  '180301',        # 盐田港REITs
        #  '180801',        # 绿能REITs
        #  '180201',        # 广州广河REITs
        #  '184801', 
        ]
    
    # 打印品种上市信息
    stocks_info = "\n股票池:\n"
    for security in g.ETF_targets.values():
        s_info = get_security_info(security)
        stocks_info += "【%s】%s 上市日期:%s\n" % (s_info.code, s_info.display_name, s_info.start_date)
    log.info(stocks_info)

def get_before_after_trade_days(date, count, is_before=True):
    """
    来自: https://www.joinquant.com/view/community/detail/c9827c6126003147912f1b47967052d9?type=1
    date :查询日期
    count : 前后追朔的数量
    is_before : True , 前count个交易日  ; False ,后count个交易日
    返回 : 基于date的日期, 向前或者向后count个交易日的日期 ,一个datetime.date 对象
    """
    all_date = pd.Series(get_all_trade_days())
    if isinstance(date, str):
        date = datetime.datetime.strptime(date, '%Y-%m-%d').date()
    if isinstance(date, datetime.datetime):
        date = date.date()

    if is_before:
        return all_date[all_date <= date].tail(count).values[0]
    else:
        return all_date[all_date >= date].head(count).values[-1]

def before_market_open(context):

    # 确保交易标的已经上市g.moment_period个交易日以上
    yesterday = context.previous_date
    list_date = get_before_after_trade_days(yesterday, g.moment_period+1)  # 今天的前g.moment_period个交易日的日期
    g.ETFList = {}
    
    #筛选品种,将上时间不足的品种排除
    #返回所有的股票/基金信息
    #  code   play-name name   start-date   end-date   type
    # 000001 平安银行   PAYH   1991-04-03  9999-01-01  stock
    all_funds = get_all_securities(types='fund', date=yesterday)   # 上个交易日之前上市的所有基金
   
    for idx in g.ETF_targets:

        symbol = g.ETF_targets[idx]  # symbol 是可交易的基金品种。example: 510300.XSHG
        if symbol in all_funds.index:
            if all_funds.loc[symbol].start_date <= list_date:  # 对应的基金也已经在要求的日期前上市
                g.ETFList[idx] = symbol                              # 则列入可交易对象中
    return

# 每日交易时
def ETF_trade(context):
    
    # 1. 卖出
    if len(g.sells)>0:
        for code in g.sells:
            log.info("卖出: %s" % code)
            order_target(code, 0)
    
    # 2. 买入
    if len(g.purchases)>0:
        for code in g.purchases:
            log.info('买入: %s' % code)
            order_target(code,g.df_etf[g.df_etf['基金代码'] == code]['股数'].values)

# 获取信号
def ma14_signal(security,etf_name,total_value,price_data):
    '''
        收盘价在13日均线以上,且
    '''
    
    # 今日收盘价
    price_data_14=price_data[-14:]
    now_close = price_data_14['close'][-1]  # exap: 0.914
    
    # g.moment_period日前收盘价
    previous_close = price_data_14['close'][-g.moment_period]  # exap:0.849
   
    # 计算均线
    ''' price_data.close.values 是一个14天的array
        array([0.849, 0.849, 0.864, 0.888, 0.886, 0.886, 0.886, 0.911, 0.915,
               0.912, 0.905, 0.892, 0.923, 0.914])
    '''
    
    
    ma13_filter = ta.MA(price_data_14.close.values, g.ma_period)[-1] # 只取最近(含当日)10天的均线值
    
    # ma_filter: 0.89469231
    # 计算动量
    # 均线状态 均线涨幅=收盘价-13日移动均线
    ma_status = now_close - ma13_filter

    # 均线涨幅 (当日收盘价-前一日收盘价)/前一日收盘价
    moment = (now_close - previous_close)/previous_close * 100

    # 计算持仓数量
    amount = int(total_value / now_close / g.type_num /100)*100
    
    g.df_etf = g.df_etf.append({'基金代码': security, 
                            '基金名称': etf_name,
                            '涨幅': moment,
                            '均线状态': ma_status,
                            '股数': amount,
                            },
                            ignore_index=True)
    
def get_signal(context):
   
    # 创建保持计算结果的DataFrame
    # df_etf 当前持仓状态
    g.df_etf = pd.DataFrame(columns=['基金代码', '基金名称','涨幅','均线状态','股数'])
    g.df_local_stocks = pd.DataFrame(columns=['基金代码', '基金名称','涨幅','均线状态','股数'])
    g.df_global_stocks = pd.DataFrame(columns=['基金代码', '基金名称','涨幅','均线状态','股数'])
    g.df_local_futures = pd.DataFrame(columns=['基金代码', '基金名称','涨幅','均线状态','股数'])
    g.df_global_futures = pd.DataFrame(columns=['基金代码', '基金名称','涨幅','均线状态','股数'])
    # 获取账户当前的资金
    total_value = context.portfolio.total_value
    current_data = get_current_data() # get_current_data:获取当前单位时间(当天/当前分钟)的涨跌停价, 是否停牌,当天的开盘价等
    print("\n总资产:{:.2f}万".format(context.portfolio.total_value/10000))
    
    # 获取当前时间
    current_time = context.current_dt
    # for 循环计算当日每只基金的涨幅,并打印出来
    for mkt_idx in g.ETFList:
        security = g.ETFList[mkt_idx]  # 指数对应的基金
        # get_security_info: 获取单个标的的信息
        etf_name = get_security_info(security).display_name  #  etf_name example: '纳指ETF'
        # 获取股票现价
        # frequency=1d 表示时间单位是一天
        # g.moment_period表示短期均线时间(13天)
        # count 与start_date二选一,指定后表示返回end_date之前count个frequency的数据,
        # 此 price_data 表示获取 security 股票 当天前13天的日线数据
        price_data = get_price(security, end_date=current_time, frequency='1d', fields=['close','high','low'], count=g.moment_period*10)
        '''
            price_date: 
                        close   high    low
            2022-07-26  0.849  0.851  0.846
            2022-07-27  0.849  0.852  0.845
            2022-07-28  0.864  0.868  0.864
            2022-07-29  0.888  0.888  0.884
        '''
        ma14_signal(security,etf_name,total_value,price_data)
    g.df_etf.sort_values(by='涨幅' ,axis=0, ascending=False, inplace=True)                        
    tb = pt.PrettyTable()
    
    #添加列数据
    tb.add_column('Index',g.df_etf.index)
    tb.add_column('ETF Code',list(g.df_etf['基金代码']))
    tb.add_column('Name',list(g.df_etf['基金名称']))
    tb.add_column('Moment',list(g.df_etf['涨幅'].values.round(2)))
    tb.add_column('Ma_Status',list(g.df_etf['均线状态'].values.round(2)))
    tb.add_column('Amount',list(g.df_etf['股数']))
    log.info('\n行情统计: \n%s' % tb)
    # 根据涨幅和均线状态筛选品种
    g.df_etf_buy = g.df_etf.copy() # 
    # 均线上涨的就是目标买入的etf
    g.df_etf_buy = g.df_etf_buy[g.df_etf_buy['均线状态']  > 0].head(3)
    # g.df_etf_buy =s # 均线上涨的就是目标买入的etf
    print(g.df_etf_buy)
    # 根据品种类别分为不同的DataFrame
    g.df_local_stocks = g.df_etf_buy.loc[g.df_etf_buy['基金代码'].isin(g.local_stocks)]
    g.df_global_stocks = g.df_etf_buy.loc[g.df_etf_buy['基金代码'].isin(g.global_stocks)]
    g.df_local_futures = g.df_etf_buy.loc[g.df_etf_buy['基金代码'].isin(g.local_futures)]
    g.df_global_futures = g.df_etf_buy.loc[g.df_etf_buy['基金代码'].isin(g.global_futures)]
    
     # 现在持仓的
    g.holdings = set(context.portfolio.positions.keys()) 
    # g.targets = []
    g.targets = g.df_etf_buy['基金代码'].tolist()
    print(g.targets)
    '''
    # 只买入当天比13日前涨,且涨幅最大的
    if len(g.df_local_stocks) > 0:
        g.targets.append(g.df_local_stocks.iloc[0]['基金代码'])
        # if len(g.df_local_stocks) > 1:
        #     g.targets.append(g.df_local_stocks.iloc[1]['基金代码'])
        # for i in range(0,len(g.df_local_stocks)):
        #     g.targets.append(g.df_local_stocks.iloc[i]['基金代码'])
        
    if len(g.df_global_stocks) > 0:
        g.targets.append(g.df_global_stocks.iloc[0]['基金代码'])
    if len(g.df_local_futures) > 0:
        g.targets.append(g.df_local_futures.iloc[0]['基金代码'])
    if len(g.df_global_futures) > 0:
        g.targets.append(g.df_global_futures.iloc[0]['基金代码'])
    '''
    content = '交易计划:\n'
    # 不在当天买入计划持仓基金,都卖出
    g.sells = [i for i in g.holdings if i not in (g.targets)]
    # 买入持仓外的目标基金
    g.purchases = [i for i in g.targets if i not in (list(g.holdings))] 
    
    # 1. 卖出不在targets中的
    if len(g.sells)>0:

        df_sells = g.df_etf.loc[g.df_etf['基金代码'].isin(g.sells)]
        tb = pt.PrettyTable()
        #添加列数据
        # tb.add_column('Index',df_sells.index)
        tb.add_column('ETF Code',list(df_sells['基金代码']))
        tb.add_column('Name',list(df_sells['基金名称']))
        
        str_more = '\n计划卖出: \n' + str(tb)
        content = content + str_more
        
        log.info(str_more)
        send_message(str_more) # 发送微信消息给客户
    
    if len(g.purchases)>0:
        
        df_purchase = g.df_etf.loc[g.df_etf['基金代码'].isin(g.purchases)]
        tb = pt.PrettyTable()
        #添加列数据
        tb.add_column('Index',df_purchase.index)
        tb.add_column('ETF Code',list(df_purchase['基金代码']))
        tb.add_column('Diaplay Name',list(df_purchase['基金名称']))
        tb.add_column('Amount',list(df_purchase['股数']))
        
        str_more = '\n计划买入:\n' + str(tb)
        content = content + str_more
        
        log.info(str_more)
        send_message(str_more)
        
    if (len(g.sells) == 0) and (len(g.purchases) == 0):
        
        str_more = '\n无交易计划: \n'
        content = content + str_more
        
        log.info('\n无交易计划: \n')
        send_message('\n无交易计划: \n')
    title = str(current_time)[:10] + '_ETF_轮动交易计划'
    # sendEmail(title, content)
            
    return
  `
@wangyajieI wangyajieI changed the title 回测示例求教:基于RSRS的ETF轮动策略 回测示例求教:基于RSRS的ETF轮动策略(closed) Dec 24, 2022
@wangyajieI
Copy link
Author

已解决。closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant