yutiansut / QUANTAXIS

QUANTAXIS 支持任务调度 分布式部署的 股票/期货/期权 数据/回测/模拟/交易/可视化/多账户 纯本地量化解决方案
https://yutiansut.github.io/QUANTAXIS/
MIT License
8.25k stars 2.97k forks source link

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

Closed wangyajieI closed 1 year ago

wangyajieI commented 2 years ago

天神及在座的各位大神好: 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 commented 1 year ago

已解决。closed