【宽客学院】自定义买入卖出策略

回测机制
宽客江湖
标签: #<Tag:0x00007f5bf4c0a718> #<Tag:0x00007f5bf4c0a5d8>

(iQuant) #1

导语:策略思想丰富多样,尤其是在买入和卖出方面,一千个投资者可能有一千个交易想法。因此,本文告诉大家怎样进行灵活地买入和卖出,以便于大家能够更高效地开发量化策略。


作者:bigquant
阅读时间:20分钟
本文由BigQuant宽客学院推出,难度标签:☆☆☆

BigQuant平台提供了很多策略生成器的模板策略,其买入和卖出的思想是确定了的。由于每个人交易的想法可能千差万别,因此如果能灵活地自定义买入和卖出岂不是更好。BigQuant的策略编写语言是Python,策略的交易主要是通过策略回测机制完成,因此一方面需要了解Python语言,另一方面,也是最重要的是需要熟悉BigQuant回测机制

克隆策略

基础策略

In [1]:
## 基础配置
class conf:
    start_date = '2014-01-01'
    end_date='2017-07-17'
    split_date = '2015-01-01'
    instruments = D.instruments(start_date, end_date)
    hold_days = 30
    features = ['rank_pb_lf_0']
    # 数据标注标注
    label_expr = [
    # 计算未来一段时间(hold_days)的相对收益
    'shift(close, -5) / shift(open, -1) - shift(benchmark_close, -5) / shift(benchmark_open, -1)',
    # 极值处理:用1%和99%分位的值做clip
    'clip(label, all_quantile(label, 0.01), all_quantile(label, 0.99))',
    # 将分数映射到分类,这里使用20个分类,这里采取等宽离散化
    'all_wbins(label, 20)',
    # 过滤掉一字涨停的情况 (设置label为NaN,在后续处理和训练中会忽略NaN的label)
    'where(shift(high, -1) == shift(low, -1), NaN, label)'
    ]

## 量化回测 https://bigquant.com/docs/module_trade.html
# 回测引擎:准备数据,只执行一次
def prepare(context):
    # context.start_date / end_date,回测的时候,为trader传入参数;在实盘运行的时候,由系统替换为实盘日期
    instruments = D.instruments()
    ## 在样本外数据上进行预测
    n0 = M.general_feature_extractor.v5(
        instruments=D.instruments(),
        start_date=context.start_date, end_date=context.end_date,
        features=conf.features)
    n1 = M.derived_feature_extractor.v1(
        data=n0.data,
        features= conf.features)
    n2 = M.transform.v2(data=n1.data, transforms=None, drop_null=True)
    n3 = M.stock_ranker_predict.v5(model=context.options['model'], data=n2.data)
    context.instruments = n3.instruments
    context.options['predictions'] = n3.predictions

# 回测引擎:初始化函数,只执行一次
def initialize(context):
    # 加载预测数据
    context.ranker_prediction = context.options['predictions'].read_df()
    # 系统已经设置了默认的交易手续费和滑点,要修改手续费可使用如下函数
    context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
    # 预测数据,通过options传入进来,使用 read_df 函数,加载到内存 (DataFrame)
    # 设置买入的股票数量,这里买入预测股票列表排名靠前的5只
    stock_count = 3
    # 每只的股票的权重,如下的权重分配会使得靠前的股票分配多一点的资金,[0.339160, 0.213986, 0.169580, ..]
    context.stock_weights = T.norm([1 / math.log(i + 2) for i in range(0, stock_count)])
    # 设置每只股票占用的最大资金比例
    context.max_cash_per_instrument = 0.2

# 回测引擎:每日数据处理函数,每天执行一次
def handle_data(context, data):
    # 按日期过滤得到今日的预测数据
    ranker_prediction = context.ranker_prediction[
        context.ranker_prediction.date == data.current_dt.strftime('%Y-%m-%d')]
    # 1. 资金分配
    # 平均持仓时间是hold_days,每日都将买入股票,每日预期使用 1/hold_days 的资金
    # 实际操作中,会存在一定的买入误差,所以在前hold_days天,等量使用资金;之后,尽量使用剩余资金(这里设置最多用等量的1.5倍)
    is_staging = context.trading_day_index < context.options['hold_days'] # 是否在建仓期间(前 hold_days 天)
    cash_avg = context.portfolio.portfolio_value / context.options['hold_days']
    cash_for_buy = min(context.portfolio.cash, (1 if is_staging else 1.5) * cash_avg)
    cash_for_sell = cash_avg - (context.portfolio.cash - cash_for_buy)
    positions = {e.symbol: p.amount * p.last_sale_price
                 for e, p in context.portfolio.positions.items()}
    # 2. 生成卖出订单:hold_days天之后才开始卖出;对持仓的股票,按StockRanker预测的排序末位淘汰
    if not is_staging and cash_for_sell > 0:
        equities = {e.symbol: e for e, p in context.portfolio.positions.items()}
        instruments = list(reversed(list(ranker_prediction.instrument[ranker_prediction.instrument.apply(
                lambda x: x in equities and not context.has_unfinished_sell_order(equities[x]))])))
        # print('rank order for sell %s' % instruments)
        for instrument in instruments:
            context.order_target(context.symbol(instrument), 0)
            cash_for_sell -= positions[instrument]
            if cash_for_sell <= 0:
                break
    # 3. 生成买入订单:按StockRanker预测的排序,买入前面的stock_count只股票
    buy_cash_weights = context.stock_weights
    buy_instruments = list(ranker_prediction.instrument[:len(buy_cash_weights)])
    max_cash_per_instrument = context.portfolio.portfolio_value * context.max_cash_per_instrument
    for i, instrument in enumerate(buy_instruments):
        cash = cash_for_buy * buy_cash_weights[i]
        if cash > max_cash_per_instrument - positions.get(instrument, 0):
            # 确保股票持仓量不会超过每次股票最大的占用资金量
            cash = max_cash_per_instrument - positions.get(instrument, 0)
        if cash > 0:
            price = data.current(context.symbol(instrument), 'price')  # 最新价格
            stock_num = np.floor(cash/price/100)*100  # 向下取整
            context.order(context.symbol(instrument), stock_num) # 整百下单

            
## 通过训练集数据训练模型            
# 数据标注
m1 = M.advanced_auto_labeler.v1(
                               instruments=conf.instruments, start_date=conf.start_date, end_date=conf.split_date,
                               label_expr=conf.label_expr, benchmark='000300.SHA', cast_label_int=True)                     
# 抽取基础特征           
m2_1 = M.general_feature_extractor.v5(
        instruments=D.instruments(),
        start_date=conf.start_date, end_date=conf.split_date,
        features=conf.features)

# 抽取衍生特征 
m2_2 = M.derived_feature_extractor.v1(
        data=m2_1.data,
        features= conf.features)

# 特征转换
m3 = M.transform.v2(data=m2_2.data, transforms=None, drop_null=True)

# 合并标注和特征数据
m4 = M.join.v2(data1=m1.data, data2=m3.data, on=['date', 'instrument'], sort=False)

# 开始训练模型
m5 = M.stock_ranker_train.v4(training_ds=m4.data, features=conf.features)

## 测试集上进行回测
m6 = M.trade.v3(
    instruments=None,
    start_date=conf.split_date,
    end_date=conf.end_date,
    prepare=prepare,
    initialize=initialize,
    handle_data=handle_data,
    order_price_field_buy='open',       
    order_price_field_sell='close',      
    capital_base=50001,               
    benchmark='000300.SHA',             
    options={'hold_days': conf.hold_days, 'model': m5.model_id},
    m_deps=np.random.rand() # 避免使用缓存
)
[2018-01-18 20:23:44.531725] INFO: bigquant: advanced_auto_labeler.v1 开始运行..
[2018-01-18 20:23:44.538756] INFO: bigquant: 命中缓存
[2018-01-18 20:23:44.540249] INFO: bigquant: advanced_auto_labeler.v1 运行完成[0.008596s].
[2018-01-18 20:23:44.572042] INFO: bigquant: general_feature_extractor.v5 开始运行..
[2018-01-18 20:23:49.597835] INFO: general_feature_extractor: year 2014, featurerows=569948
[2018-01-18 20:23:52.455414] INFO: general_feature_extractor: year 2015, featurerows=0
[2018-01-18 20:23:52.471520] INFO: general_feature_extractor: total feature rows: 569948
[2018-01-18 20:23:52.474435] INFO: bigquant: general_feature_extractor.v5 运行完成[7.902456s].
[2018-01-18 20:23:52.493413] INFO: bigquant: derived_feature_extractor.v1 开始运行..
[2018-01-18 20:23:53.003542] INFO: derived_feature_extractor: /y_2014, 569948
[2018-01-18 20:23:53.380070] INFO: bigquant: derived_feature_extractor.v1 运行完成[0.88666s].
[2018-01-18 20:23:53.395060] INFO: bigquant: transform.v2 开始运行..
[2018-01-18 20:23:54.817768] INFO: transform: transformed /y_2014, 569948/569948
[2018-01-18 20:23:54.836931] INFO: transform: transformed rows: 569948/569948
[2018-01-18 20:23:54.875491] INFO: bigquant: transform.v2 运行完成[1.480396s].
[2018-01-18 20:23:54.890079] INFO: bigquant: join.v2 开始运行..
[2018-01-18 20:23:58.195094] INFO: join: /y_2014, rows=555191/569948, timetaken=1.998186s
[2018-01-18 20:23:58.375927] INFO: join: total result rows: 555191
[2018-01-18 20:23:58.378871] INFO: bigquant: join.v2 运行完成[3.488743s].
[2018-01-18 20:23:58.465993] INFO: bigquant: stock_ranker_train.v4 开始运行..
[2018-01-18 20:23:59.446846] INFO: df2bin: prepare bins ..
[2018-01-18 20:23:59.494251] INFO: df2bin: prepare data: training ..
[2018-01-18 20:23:59.549774] INFO: df2bin: sort ..
[2018-01-18 20:24:06.367311] INFO: stock_ranker_train: 7559b74e training: 555191 rows
[2018-01-18 20:25:15.874135] INFO: bigquant: stock_ranker_train.v4 运行完成[77.408155s].
[2018-01-18 20:25:15.947904] INFO: bigquant: backtest.v7 开始运行..
[2018-01-18 20:25:15.990330] INFO: bigquant: general_feature_extractor.v5 开始运行..
[2018-01-18 20:25:23.832283] INFO: general_feature_extractor: year 2015, featurerows=569698
[2018-01-18 20:25:35.130115] INFO: general_feature_extractor: year 2016, featurerows=641546
[2018-01-18 20:25:51.227248] INFO: general_feature_extractor: year 2017, featurerows=382398
[2018-01-18 20:25:51.266700] INFO: general_feature_extractor: total feature rows: 1593642
[2018-01-18 20:25:51.275477] INFO: bigquant: general_feature_extractor.v5 运行完成[35.285127s].
[2018-01-18 20:25:51.285414] INFO: bigquant: derived_feature_extractor.v1 开始运行..
[2018-01-18 20:25:52.462755] INFO: derived_feature_extractor: /y_2015, 569698
[2018-01-18 20:25:53.369834] INFO: derived_feature_extractor: /y_2016, 641546
[2018-01-18 20:25:53.875156] INFO: derived_feature_extractor: /y_2017, 382398
[2018-01-18 20:25:54.108168] INFO: bigquant: derived_feature_extractor.v1 运行完成[2.822755s].
[2018-01-18 20:25:54.118166] INFO: bigquant: transform.v2 开始运行..
[2018-01-18 20:25:55.133534] INFO: transform: transformed /y_2015, 569698/569698
[2018-01-18 20:25:56.025058] INFO: transform: transformed /y_2016, 641520/641546
[2018-01-18 20:25:56.520577] INFO: transform: transformed /y_2017, 382398/382398
[2018-01-18 20:25:56.548218] INFO: transform: transformed rows: 1593616/1593642
[2018-01-18 20:25:56.573223] INFO: bigquant: transform.v2 运行完成[2.455039s].
[2018-01-18 20:25:56.594106] INFO: bigquant: stock_ranker_predict.v5 开始运行..
[2018-01-18 20:25:58.047122] INFO: df2bin: prepare data: prediction ..
[2018-01-18 20:26:17.460477] INFO: stock_ranker_predict: 准备预测: 1593616 行
[2018-01-18 20:26:30.203778] INFO: bigquant: stock_ranker_predict.v5 运行完成[33.609646s].
[2018-01-18 20:26:30.368574] INFO: algo: set price type:backward_adjusted
[2018-01-18 20:34:29.319543] INFO: Performance: Simulated 618 trading days out of 618.
[2018-01-18 20:34:29.321823] INFO: Performance: first open: 2015-01-05 01:30:00+00:00
[2018-01-18 20:34:29.326346] INFO: Performance: last close: 2017-07-17 07:00:00+00:00
  • 收益率21.01%
  • 年化收益率8.09%
  • 基准收益率3.67%
  • 阿尔法0.05
  • 贝塔0.36
  • 夏普比率0.24
  • 胜率0.556
  • 盈亏比1.045
  • 收益波动率15.33%
  • 信息比率0.31
  • 最大回撤20.33%
[2018-01-18 20:34:37.271456] INFO: bigquant: backtest.v7 运行完成[561.323508s].

1.等权重买入股票

在基础策略中,排序靠前的股票买入资金权重会大一些,那么如果想等权重买入,应该怎样处理呢?

In [2]:
# 操作非常简单,如下处理:
# 假设买入股票为10只
weight = 1/10
context.order_target_percent(sid,1/10)   
# 使用order_target_percent下单接口即可,更多下单接口请参看以下链接:
#   https://bigquant.com/docs/module_trade.html#order-method-section

2.修改买入卖出时间

买入时间由order_price_field_buy 参数决定,如果为'open'表示以开盘价买入,买入时间就为开盘时间,'high','low','close'比较好理解

卖出时间由order_price_field_sell 参数决定,如果为'close'表示以收盘价卖出,卖出时间就为收盘时间,'high','low','open'比较好理解

In [ ]:
m6 = M.trade.v3(
    instruments=None,
    start_date=conf.split_date,
    end_date=conf.end_date,
    prepare=prepare,
    initialize=initialize,
    handle_data=handle_data,
    order_price_field_buy='close',       
    order_price_field_sell='close',      
    capital_base=50001,               
    benchmark='000300.SHA',             
    options={'hold_days': conf.hold_days, 'model': m5.model_id},
    m_deps=np.random.rand() # 避免使用缓存
)

3.隔几天运行一次

由于handle_data函数是每个交易日都会运行一次,那么如果想隔几天运行一次,相当于隔几天进行一下买入卖出交易,此时应该怎样修改代码呢?

In [ ]:
## 非常简单,这里我们的目的是不用每天运行handle_data函数,而是隔几天运行一次。一个极为简单的方法就是在handle_data函数中,加入一个判断条件,
## 如果达到判断条件就返回(return),这样就实现了当天不运行handle_data函数的目的。

def handle_data(context, data):
    
    #------------------------START:加入下面if的两行代码到之前的handle_data函数的最前部分即可-------------------
    # 相隔几天(以3天举例)运行一下handle_data函数
    if context.trading_day_index % 3 != 0:
        return 
    #------------------------END:加上这两句代码在handle_data函数就能实现隔几天运行下该函数的目的---------------------
     # 按日期过滤得到今日的预测数据
    ranker_prediction = context.ranker_prediction[
        context.ranker_prediction.date == data.current_dt.strftime('%Y-%m-%d')]
    # 1. 资金分配
    # 平均持仓时间是hold_days,每日都将买入股票,每日预期使用 1/hold_days 的资金
    # 实际操作中,会存在一定的买入误差,所以在前hold_days天,等量使用资金;之后,尽量使用剩余资金(这里设置最多用等量的1.5倍)
    is_staging = context.trading_day_index < context.options['hold_days'] # 是否在建仓期间(前 hold_days 天)
    cash_avg = context.portfolio.portfolio_value / context.options['hold_days']
    cash_for_buy = min(context.portfolio.cash, (1 if is_staging else 1.5) * cash_avg)
    cash_for_sell = cash_avg - (context.portfolio.cash - cash_for_buy)
    positions = {e.symbol: p.amount * p.last_sale_price
                 for e, p in context.portfolio.positions.items()}
    # 2. 生成卖出订单:hold_days天之后才开始卖出;对持仓的股票,按StockRanker预测的排序末位淘汰
    if not is_staging and cash_for_sell > 0:
        equities = {e.symbol: e for e, p in context.portfolio.positions.items()}
        instruments = list(reversed(list(ranker_prediction.instrument[ranker_prediction.instrument.apply(
                lambda x: x in equities and not context.has_unfinished_sell_order(equities[x]))])))
        # print('rank order for sell %s' % instruments)
        for instrument in instruments:
            context.order_target(context.symbol(instrument), 0)
            cash_for_sell -= positions[instrument]
            if cash_for_sell <= 0:
                break
    # 3. 生成买入订单:按StockRanker预测的排序,买入前面的stock_count只股票
    buy_cash_weights = context.stock_weights
    buy_instruments = list(ranker_prediction.instrument[:len(buy_cash_weights)])
    max_cash_per_instrument = context.portfolio.portfolio_value * context.max_cash_per_instrument
    for i, instrument in enumerate(buy_instruments):
        cash = cash_for_buy * buy_cash_weights[i]
        if cash > max_cash_per_instrument - positions.get(instrument, 0):
            # 确保股票持仓量不会超过每次股票最大的占用资金量
            cash = max_cash_per_instrument - positions.get(instrument, 0)
        if cash > 0:
            price = data.current(context.symbol(instrument), 'price')  # 最新价格
            stock_num = np.floor(cash/price/100)*100  # 向下取整
            context.order(context.symbol(instrument), stock_num) # 整百下单

4.持有固定天数卖出

在基础策略中。对持仓的股票,按StockRanker预测的排序末位淘汰,现在我们修改卖出规则,按照持有固定天数(hold_days)进行卖出,只需在handle_data函数中进行修改即可

In [ ]:
# 回测引擎:每日数据处理函数,每天执行一次
def handle_data(context, data):
    # 按日期过滤得到今日的预测数据
    ranker_prediction = context.ranker_prediction[
        context.ranker_prediction.date == data.current_dt.strftime('%Y-%m-%d')]

    # 1. 资金分配
    # 平均持仓时间是hold_days,每日都将买入股票,每日预期使用 1/hold_days 的资金
    # 实际操作中,会存在一定的买入误差,所以在前hold_days天,等量使用资金;之后,尽量使用剩余资金(这里设置最多用等量的1.5倍)
    is_staging = context.trading_day_index < context.options['hold_days'] # 是否在建仓期间(前 hold_days 天)
    cash_avg = context.portfolio.portfolio_value / context.options['hold_days']
    cash_for_buy = min(context.portfolio.cash, (1 if is_staging else 1.5) * cash_avg)
    positions = {e.symbol: p.amount * p.last_sale_price
                 for e, p in context.portfolio.positions.items()}
   
    #----------------------------START:持有固定天数卖出---------------------------
    today = data.current_dt
    # 不是建仓期(在前hold_days属于建仓期)
    if not is_staging:
        equities = {e.symbol: p for e, p in context.portfolio.positions.items() if p.amount>0}
        for instrument in equities:
#             print('last_sale_date: ', equities[instrument].last_sale_date)
            sid = equities[instrument].sid  # 交易标的
            # 今天和上次交易的时间相隔hold_days就全部卖出
            if today-equities[instrument].last_sale_date>=datetime.timedelta(context.options['hold_days']) and data.can_trade(context.symbol(instrument)):
                context.order_target_percent(sid, 0)
    #--------------------------------END:持有固定天数卖出---------------------------    
          

    # 3. 生成买入订单:按StockRanker预测的排序,买入前面的stock_count只股票
    buy_cash_weights = context.stock_weights
    buy_instruments = list(ranker_prediction.instrument[:len(buy_cash_weights)])
    max_cash_per_instrument = context.portfolio.portfolio_value * context.max_cash_per_instrument
    for i, instrument in enumerate(buy_instruments):
        cash = cash_for_buy * buy_cash_weights[i]
        if cash > max_cash_per_instrument - positions.get(instrument, 0):
            # 确保股票持仓量不会超过每次股票最大的占用资金量
            cash = max_cash_per_instrument - positions.get(instrument, 0)
        if cash > 0:
            price = data.current(context.symbol(instrument), 'price')  # 最新价格
            stock_num = np.floor(cash/price/100)*100  # 向下取整
            context.order(context.symbol(instrument), stock_num) # 整百下单

5. 一次性全仓买入股票,使现金为0,然后持有固定天数,换仓

换仓:卖出持仓股票,买入新的股票

在基础策略中,如果hold_days = 5,那么每天都需要买入20%仓位的股票,每天也需要卖出一定股票,每天滚动进行。但是,如果我只想在第一天买入100%资金股票, 持有五天,这五天内什么都不操作,等到5天满了以后再进行全部换仓,这样如何修改策略代码呢?方法很简单,也是修改handle_data中的代码。

In [ ]:
# 回测引擎:每日数据处理函数,每天执行一次
def handle_data(context, data):
    
    # 相隔几天(hold_days)进行一下换仓
    if context.trading_day_index % context.options['hold_days'] != 0:
        return 
    
    # 按日期过滤得到今日的预测数据
    ranker_prediction = context.ranker_prediction[
        context.ranker_prediction.date == data.current_dt.strftime('%Y-%m-%d')]
    # 目前持仓
    positions = {e.symbol: p.amount * p.last_sale_price
                 for e, p in context.portfolio.positions.items()}
    # 权重
    buy_cash_weights = context.stock_weights
    # 今日买入股票列表
    stock_to_buy = list(ranker_prediction.instrument[:len(buy_cash_weights)])
    # 持仓上限
    max_cash_per_instrument = context.portfolio.portfolio_value * context.max_cash_per_instrument

    # 通过positions对象,使用列表生成式的方法获取目前持仓的股票列表
    stock_hold_now = [equity.symbol for equity in context.portfolio.positions ]
    # 继续持有的股票:调仓时,如果买入的股票已经存在于目前的持仓里,那么应继续持有
    no_need_to_sell = [i for i in stock_hold_now if i in stock_to_buy]
    # 需要卖出的股票
    stock_to_sell = [i for i in stock_hold_now if i not in no_need_to_sell]
  
    # 卖出
    for stock in stock_to_sell:
        # 如果该股票停牌,则没法成交。因此需要用can_trade方法检查下该股票的状态
        # 如果返回真值,则可以正常下单,否则会出错
        # 因为stock是字符串格式,我们用symbol方法将其转化成平台可以接受的形式:Equity格式
        if data.can_trade(context.symbol(stock)):
            # order_target_percent是平台的一个下单接口,表明下单使得该股票的权重为0,
            #   即卖出全部股票,可参考回测文档
            context.order_target_percent(context.symbol(stock), 0)
    
    # 如果当天没有买入的股票,就返回
    if len(stock_to_buy) == 0:
        return
    
    # 买入
    for i, instrument in enumerate(stock_to_buy):
        cash = context.portfolio.portfolio_value * buy_cash_weights[i]
        if cash > max_cash_per_instrument - positions.get(instrument, 0):
            # 确保股票持仓量不会超过每次股票最大的占用资金量
            cash = max_cash_per_instrument - positions.get(instrument, 0)
        if cash > 0:
            price = data.current(context.symbol(instrument), 'price')  # 最新价格
            stock_num = np.floor(cash/price/100)*100  # 向下取整
            context.order(context.symbol(instrument), stock_num) # 整百下单

6.动态择时仓位调整

比如,根据市场状况,每天的买入股票的仓位并不是一成不变固定的,而是会依据市场情绪、新闻事件等进行动态调整

参考链接:

控制每日仓位

7.止盈止损处理

在交易中,如果需要加入更多的进场出场规则,比如止盈止损

参考链接:

止盈止损处理

8.修改策略回测价格模式

BigQuant平台现在支持三种价格模式,分别是前复权(forward_adjusted)、真实价格(original)、后复权(backward_adjusted)。目前默认是后复权模式

前复权回测模式

In [ ]:
m6 = M.trade.v3(
    instruments=None,
    start_date=conf.split_date,
    end_date=conf.end_date,
    prepare=prepare,
    initialize=initialize,
    handle_data=handle_data,
    order_price_field_buy='close',       
    order_price_field_sell='close',      
    capital_base=50001,               
    benchmark='000300.SHA',  
    price_type='foward_adjusted',  # 前复权回测模式
    options={'hold_days': conf.hold_days, 'model': m5.model_id},
    m_deps=np.random.rand() # 避免使用缓存
)

真实价格回测模式

In [ ]:
m6 = M.trade.v3(
    instruments=None,
    start_date=conf.split_date,
    end_date=conf.end_date,
    prepare=prepare,
    initialize=initialize,
    handle_data=handle_data,
    order_price_field_buy='close',       
    order_price_field_sell='close',      
    capital_base=50001,               
    benchmark='000300.SHA',  
    price_type='original', # 真实价格回测模式
    options={'hold_days': conf.hold_days, 'model': m5.model_id},
    m_deps=np.random.rand() # 避免使用缓存
)

小结: 本文总结了实际策略开发过程中的几种需求,希望对大家有所帮助,同时希望大家对回测机制有更深入的理解,能够快速灵活开发策略,验证策略思想。


   本文由BigQuant宽客学院推出,版权归BigQuant所有,转载请注明出处。


【宽客学院】开发传统趋势策略
为什么stockranker选股策略每日都调仓?
交易买入股票数量问题
请教如何完善handle模块卖出订单
默认的股票都是按比例购买的,我想等额资金交易该怎么设置?
如何获取 某个股票 持仓天数?
(jove) #2

4.持有固定天数卖出----------------有些问题请教:

today-equities是系统本身的定义的吗?
last_sale_date,today-equities在交易引擎api文档里没有找到相关介绍。在哪里可以找到详细的应用介绍。
today = data.current_dt 也没有用到。
这些具体如何调用?
谢谢!

我自己摸索写个简单的,但报错,估计是这个调用不正确,麻烦您看一下,问题出在哪里?怎么修改?

我的交易策略很简单:
例如,通过筛选(通过selected_data传入交易模块),在1月4日买一只股票,9日买一只股票,23日买入一只股票,这几只股票都需要持仓4天才卖出,其它时间不进行交易。

代码如下:

# 策略主体函数


# 初始化虚拟账户状态,只在第一个交易日运行
def initialize(context):
    # 设置手续费,买入时万3,卖出是千分之1.3,不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))

# 策略交易逻辑,每个交易日运行一次
def handle_data(context,data):
    today_buy = data.current_dt.strftime('%Y-%m-%d') # 交易日期
    # 调仓:卖出所有持有股票
    for equity in context.portfolio.positions:
       
        #--------------根据持仓天数决定卖出------------------
        today = data.current_dt
        if data.can_trade(equity) and context.portfolio.positions(equity).last_sale_date>=datetime.timedelta(context.options['rebalance_period']) :
            context.order_target_percent(equity, 0)
        
        #---------------end---------------------------------
    

    # 调仓:买入新的股票
    try:
        instruments_to_buy = context.options['selected_data'].ix[today_buy].instrument
    except:
        instruments_to_buy=[]
        
    if len(instruments_to_buy) == 0:
        return
    # 等量分配资金买入股票
    weight = 1.0 / len(instruments_to_buy)
    for instrument in instruments_to_buy:
        if data.can_trade(context.symbol(instrument)):
            context.order_target_percent(context.symbol(instrument), weight)
            

# 策略回测:
m = M.trade.v3(
    instruments=conf.instruments,
    start_date=conf.start_date,
    end_date=conf.end_date,
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=100000,
    benchmark='000300.INDX',
    # 传入数据给回测模块,所有回测函数里用到的数据都要从这里传入,并通过 context.options 使用,否则可能会遇到缓存问题
    options={'selected_data': selected_data, 'rebalance_period': 4},
)

selected_data是这样结构:也就是要买的股票信息,以date为索引

  	                 close_0	    date  	instrument
date				
2017/3/21	2424	794.790527	2017/3/21	600518.SHA
2017/5/23	4411	27.804249	2017/5/23	601088.SHA
2017/6/22	5343	22.647905	2017/6/22	601169.SHA
2017/7/10	5901	15.049096	2017/7/10	600958.SHA
2017/7/17	6135	9.917313	2017/7/17	600919.SHA
2017/8/28	7553	29.492424	2017/8/28	600999.SHA

报错信息:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-49-27540a760452> in <module>()
     66     benchmark='000300.INDX',
     67     # 传入数据给回测模块,所有回测函数里用到的数据都要从这里传入,并通过 context.options 使用,否则可能会遇到缓存问题
---> 68     options={'selected_data': selected_data, 'rebalance_period': 4},
     69 
     70 

<ipython-input-49-27540a760452> in handle_data(context, data)
     29         #--------------根据持仓天数决定卖出------------------
     30         today = data.current_dt
---> 31         if data.can_trade(equity) and context.portfolio.positions(equity).last_sale_date>=datetime.timedelta(context.options['rebalance_period']) :
     32             context.order_target_percent(equity, 0)
     33 

TypeError: 'Positions' object is not callable

(jove) #3

麻烦管理员看一下。谢谢!


(iQuant) #5

您好,你的策略代码有两处错误。
回测引擎相关方法的查询可以移步:交易引擎 - 数据对象

today 这些变量在后文中都是使用了的。

  • context.portfolio.positions(equity)
    在这里,context.portfolio.positions是一个持仓对象,你可以理解为一个字典,equity为股票代码,因此这里要用中括号,你用小括号的话相当于把Positions对象看成了可以调用的,实际上是不能调用的。所以有这样的报错:TypeError: ‘Positions’ object is not callable

  • today - context.portfolio.positions[equity].last_sale_date >=datetime.timedelta(context.options[‘rebalance_period’])
    因为策略为持有固定时间卖出,因此这里代码应如上面所述,其含义是 判断当天-买入日期 超过4天

具体策略修改后的代码可以参考下面的分享。

克隆策略
In [23]:
date = ['2018-01-04','2018-01-09','2018-01-23']
selected_data = pd.DataFrame({'instrument':[['600519.SHA'],['000002.SZA'],['000333.SZA']]},index= date)
In [24]:
def initialize(context):
    # 设置手续费,买入时万3,卖出是千分之1.3,不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))

# 策略交易逻辑,每个交易日运行一次
def handle_data(context,data):
    today_buy = data.current_dt.strftime('%Y-%m-%d') # 交易日期
    # 调仓:卖出所有持有股票
    for equity in context.portfolio.positions:
        #--------------根据持仓天数决定卖出------------------
        today = data.current_dt
        pos = context.portfolio.positions
        if data.can_trade(equity) and today - context.portfolio.positions[equity].last_sale_date  >=datetime.timedelta(context.options['rebalance_period']) :
            context.order_target_percent(equity, 0)
        #---------------end---------------------------------

    # 调仓:买入新的股票
    try:
        instruments_to_buy = context.options['selected_data'].ix[today_buy].instrument
    except:
        instruments_to_buy=[]
        
    if len(instruments_to_buy) == 0:
        return
    # 等量分配资金买入股票
    weight = 1.0 / len(instruments_to_buy)
    for instrument in instruments_to_buy:
        if data.can_trade(context.symbol(instrument)):
            context.order_target_percent(context.symbol(instrument), weight)
            
# 策略回测:
m = M.trade.v3(
    instruments=D.instruments(),
    start_date='2018-01-01',
    end_date='2018-01-27',
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=100000,
    benchmark='000300.INDX',
    # 传入数据给回测模块,所有回测函数里用到的数据都要从这里传入,并通过 context.options 使用,否则可能会遇到缓存问题
    options={'selected_data': selected_data, 'rebalance_period': 4},
) 
[2018-02-27 14:45:33.787972] INFO: bigquant: backtest.v7 开始运行..
[2018-02-27 14:45:33.902623] INFO: algo: set price type:backward_adjusted
[2018-02-27 14:45:57.203264] INFO: Performance: Simulated 19 trading days out of 19.
[2018-02-27 14:45:57.204846] INFO: Performance: first open: 2018-01-02 01:30:00+00:00
[2018-02-27 14:45:57.206079] INFO: Performance: last close: 2018-01-26 07:00:00+00:00
  • 收益率7.54%
  • 年化收益率162.23%
  • 基准收益率8.69%
  • 阿尔法2.12
  • 贝塔-0.27
  • 夏普比率5.66
  • 胜率1.0
  • 盈亏比--
  • 收益波动率27.88%
  • 信息比率-1.39
  • 最大回撤4.85%
[2018-02-27 14:45:57.791867] INFO: bigquant: backtest.v7 运行完成[24.003834s].


(jove) #6

非常感谢管理员快速回复!

对以上程序我添加了3个2017年的数据,发现一些奇怪的现象:(我对管理给出的程序未做改动)

1.虽然设定持仓天数为4,但实际上触发卖出的持仓时间可能会为:4天,5天,6天,甚至还有7天的情况
2.600606.SHA股票在条件满足的情况下,没有买入。

麻烦管理员看一下,是程序的bug,还是基础数据因为采取后复权,造成这个现象的?

克隆策略
In [22]:
date = ['2017-08-28','2017-09-17','2017-10-30','2018-01-04','2018-01-09','2018-01-23']
selected_data = pd.DataFrame({'instrument':[['600999.SHA'],['600606.SHA'],['600028.SHA'],['600519.SHA'],['000002.SZA'],['000333.SZA']]},index= date)
In [23]:
selected_data
Out[23]:
instrument
2017-08-28 [600999.SHA]
2017-09-17 [600606.SHA]
2017-10-30 [600028.SHA]
2018-01-04 [600519.SHA]
2018-01-09 [000002.SZA]
2018-01-23 [000333.SZA]
In [24]:
def initialize(context):
    # 设置手续费,买入时万3,卖出是千分之1.3,不足5元以5元计
    context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))

# 策略交易逻辑,每个交易日运行一次
def handle_data(context,data):
    today_buy = data.current_dt.strftime('%Y-%m-%d') # 交易日期
    # 调仓:卖出所有持有股票
    for equity in context.portfolio.positions:
        #--------------根据持仓天数决定卖出------------------
        today = data.current_dt
        pos = context.portfolio.positions
        if data.can_trade(equity) and today - context.portfolio.positions[equity].last_sale_date  >=datetime.timedelta(context.options['rebalance_period']) :
            context.order_target_percent(equity, 0)
            tem=today - context.portfolio.positions[equity].last_sale_date
            print(tem)
        #---------------end---------------------------------

    # 调仓:买入新的股票
    try:
        instruments_to_buy = context.options['selected_data'].ix[today_buy].instrument
    except:
        instruments_to_buy=[]
        
    if len(instruments_to_buy) == 0:
        return
    # 等量分配资金买入股票
    weight = 1.0 / len(instruments_to_buy)
    for instrument in instruments_to_buy:
        if data.can_trade(context.symbol(instrument)):
            context.order_target_percent(context.symbol(instrument), weight)
            
            
# 策略回测:
m = M.trade.v3(
    instruments=D.instruments(),
    start_date='2017-08-01',
    end_date='2018-01-27',
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=100000,
    benchmark='000300.INDX',
    # 传入数据给回测模块,所有回测函数里用到的数据都要从这里传入,并通过 context.options 使用,否则可能会遇到缓存问题
    options={'selected_data': selected_data, 'rebalance_period': 4},
) 
[2018-02-27 17:26:54.383648] INFO: bigquant: backtest.v7 开始运行..
[2018-02-27 17:26:54.644526] INFO: algo: set price type:backward_adjusted
6 days 00:00:00
6 days 00:00:00
4 days 00:00:00
5 days 00:00:00
[2018-02-27 17:27:47.840789] INFO: Performance: Simulated 123 trading days out of 123.
[2018-02-27 17:27:47.842684] INFO: Performance: first open: 2017-08-01 01:30:00+00:00
[2018-02-27 17:27:47.844100] INFO: Performance: last close: 2018-01-26 07:00:00+00:00
  • 收益率13.9%
  • 年化收益率30.55%
  • 基准收益率17.21%
  • 阿尔法0.25
  • 贝塔0.02
  • 夏普比率2.19
  • 胜率1.0
  • 盈亏比--
  • 收益波动率11.91%
  • 信息比率-0.49
  • 最大回撤4.85%
[2018-02-27 17:27:49.089790] INFO: bigquant: backtest.v7 运行完成[54.706245s].

(iQuant) #7

你好,本身是没有问题的。
比如你的策略,第一次交易是2017-08-29日买入600999,在8-30日的时候持有一天,在8-31日持有2天,9-1持有三天,然后遇到周末(9-2,9-3),最后在9月4日持有达到6天,这里6对应的是6个自然日,实际只有4个交易日,于是9-4下单,9月5日卖出。因此你再交易详情里看到2017-09-05卖出600999.SHA.

因此这里需要意识到的是,持仓达到4天实际上是指达到四个(或四个以上)自然日卖出。如果要实现达到四个交易日,修改下代码就能实现。


(jove) #8

谢谢解答!
测试结果完全正确。

按程序设定的的确是4个自然日(包括了周六、日),实际回测时,遇到周六或周日,因无法交易,就会往后顺延,就出现我之前提到的那个问题。
但这样设定对于评估结果随机性太大,还是要用4个交易日比较合适,麻烦一下管理员,如果要实现准确的4个交易日,程序怎么修改?

非常不好意思,提出这个请求,我对编写代码不熟悉,虽然也仔细看了技术文档,还是不得要领。


(jove) #9

@iQuant,可以实现4个交易日这个功能吗?是不是编写有些难度?


(小Q) #10

很好实现啊,做一个计数统计就可以,你可以先研究下,空了我们给个例子。


(lu0817) #11

如果我预测的是未来60天,我想提取未来50-60天的预测值是不是可以这样写,
ranker_prediction = context.ranker_prediction[0:10]
请问下


(达达) #12

这个要看您具体的问题,stockranker算法本身是把预测结果放在ranker_prediction中,

        context.ranker_prediction.date == data.current_dt.strftime('%Y-%m-%d')]

在每天handle调用中是根据handle当日的data.current来过滤每日的排名的,您可以考虑修改这个过滤 data.current_dt,比方说前移来实现你要的历史预测,在handle中应该是没法提取未来的值,因为未来的那天数据并未进入handle中,使用未来数据不知道您有什么特殊要求,毕竟一般认为未来数据不能引入到回测中


(峰) #13

试了第四项持有固定天数卖出,出现了股数为负的情况,感觉是由于成交率限制默认为0.025而使得卖出的信号重复多了几次,我的编程能力较弱,弄了好久也无法搞定,请问要加上哪些代码才能避免这种情况。
另外在模拟交易的交易机制里会出现股数为负的情况吗?


(达达) #14

多半是多次卖出导致的,可以考虑把止盈、止损以及固定天数卖出的股票分别记录在list中,在换仓逻辑中下单前确认是否已经卖出过来过滤,可以参考可视化模板,举个例子

    #---------------------------START:止赢止损模块(含建仓期)--------------------
    today_date = data.current_dt.strftime('%Y-%m-%d')
    positions_stop={e.symbol:p.cost_basis 
    for e,p in context.portfolio.positions.items()}
    # 新建当日止赢止损股票列表是为了handle_data 策略逻辑部分不再对该股票进行判断
    current_stopwin_stock=[]
    current_stoploss_stock = []   
    if len(positions_stop)>0:
        for i in positions_stop.keys():
            stock_cost=positions_stop[i]  
            stock_market_price=data.current(context.symbol(i),'price')  
            # 赚3元且可以交易and not context.has_unfinished_sell_order(equities[i])
            if stock_market_price-stock_cost>=0.5 and data.can_trade(context.symbol(i)) and not context.has_unfinished_sell_order(i):
                context.order_target_percent(context.symbol(i),0)      
                current_stopwin_stock.append(i)
                # 亏1元就止损and not context.has_unfinished_sell_order(equities[i])
            if stock_market_price - stock_cost  <= -1 and data.can_trade(context.symbol(i)) and not context.has_unfinished_sell_order(i):   
                context.order_target_percent(context.symbol(i),0)     
                current_stoploss_stock.append(i)
        if len(current_stopwin_stock)>0:
            print(today_date,'止盈股票列表',current_stopwin_stock)
        if len(current_stoploss_stock)>0:
            print(today_date,'止损股票列表',current_stoploss_stock)
    #--------------------------END: 止赢止损模块-----------------------------
    
    #--------------------------START:持有固定天数卖出(不含建仓期)---------------
    current_stopdays_stock = [] 
    today = data.current_dt
    today_date = data.current_dt.strftime('%Y-%m-%d')
    # 不是建仓期(在前hold_days属于建仓期)
    if not is_staging:
        equities = {e.symbol: p for e, p in context.portfolio.positions.items() if p.amount>0}
        if len(equities)>0:
            for i in equities:
                sid = equities[i].sid  # 交易标的
                # 今天和上次交易的时间相隔hold_days就全部卖出 datetime.timedelta(context.options['hold_days'])也可以换成自己需要的天数,比如datetime.timedelta(5)
                if today-equities[i].last_sale_date>=datetime.timedelta(context.options['hold_days']) and data.can_trade(context.symbol(i)) and not context.has_unfinished_sell_order(equities[i]):
                    context.order_target_percent(sid, 0)
                    current_stopdays_stock.append(i)
            if len(current_stopdays_stock)>0:        
                print(today_date,'固定天数卖出列表',current_stopdays_stock)
    #-------------------------------END:持有固定天数卖出--------------------------
       
    # 2. 生成卖出订单:hold_days天之后才开始卖出;对持仓的股票,按机器学习算法预测的排序末位淘汰
    if not is_staging and cash_for_sell > 0:
        equities = {e.symbol: e for e, p in context.perf_tracker.position_tracker.positions.items()}
        instruments = list(reversed(list(ranker_prediction.instrument[ranker_prediction.instrument.apply(
                lambda x: x in equities and not context.has_unfinished_sell_order(equities[x]))])))
        
        for instrument in instruments:
            if instrument  in current_stopwin_stock:
                continue
            if instrument  in current_stoploss_stock:
                continue
            if instrument  in current_stopdays_stock:
                continue
            context.order_target(context.symbol(instrument), 0)
            cash_for_sell -= positions[instrument]
            if cash_for_sell <= 0:
                break

    # 3. 生成买入订单:按机器学习算法预测的排序,买入前面的stock_count只股票
    buy_cash_weights = context.stock_weights
    buy_instruments = list(ranker_prediction.instrument[:len(buy_cash_weights)])
    max_cash_per_instrument = context.portfolio.portfolio_value * context.max_cash_per_instrument
    for i, instrument in enumerate(buy_instruments):
        if instrument  in current_stopwin_stock:
            continue
        if instrument  in current_stoploss_stock:
            continue
        if instrument  in current_stopdays_stock:
            continue        
        cash = cash_for_buy * buy_cash_weights[i]
        if cash > max_cash_per_instrument - positions.get(instrument, 0):
            # 确保股票持仓量不会超过每次股票最大的占用资金量
            cash = max_cash_per_instrument - positions.get(instrument, 0)
        if cash > 0:
            context.order_value(context.symbol(instrument), cash)

(峰) #15

原先为了省事,将官方的可视化ai选股直接使用,只修改持仓时间为2天与每天买入股票数目为3只,可是发现越往后每天持仓的股票数目多达15只,而且有7、8只股票的持仓时间竟然长达3、4个月,这个情况主要出现在回测后期,前期比较少(官方模板的hold_days为平均持仓时间我能理解,出现这种情况以我的辣鸡编程水平实在无法搞定,也感觉不太对劲)。所以在论坛找官方提供的固定天数平仓的代码希望能解决问题,可加入代码后出现股数为负的情况以及每隔几天会出现两三天会出现只买不卖的问题(应该是代码中建仓期的逻辑判断问题)
还是希望学院里提供的代码应该看仔细审核下是否符合预期目的,而不是只关注能否运行就完事了,不要出现太多逻辑问题;貌似today-equities[i].last_sale_date>=datetime.timedelta(context.options[‘hold_days’]) 会比设置context.options[‘hold_days’] 的多持仓两天,如果不仔细看感觉逻辑还蛮对的,哈哈
bigquant真的很好用,真的想把精力全都放在因子的构建与测试上,最近这几个问题对我这种初学者比较头疼


(达达) #16

1、这应该是模块之间的重复卖出问题,比如止损之后又换仓中卖出,学院只提供了功能代码块,但对策略实现逻辑之间的关联确实没有指引,这部分我们通过策略模板给一些样例,可以在QQ群中下载
2、有一些逻辑确实需要您校验一下,比如时间差1天的问题,可能每个人对持长天数的理解不一样