请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  数理科学 帖子:3364680 新帖:1

【新手入门教程】白马股策略

蜡笔小新炒外汇发表于:5 月 9 日 20:30回复(1)

上一篇文章:【新手入门教程】简单市值轮动策略
下一篇文章:【新手入门教程】多股票追涨策略

股票市场中千余只股票,我们如何准确从中挑选具有良好的投资价值及潜力的股票呢?这就要用到我们今天所提到的白马股策略

白马股策略

学习内容:

  • 设置基准、滑点、佣金
  • 循环计数实现周期调仓
  • 创建持仓列表
  • 相关财务数据的读取和运用
  • 关键信息的打印

1 确定策略内容

白马股指相关的信息已经公开的股票,由于业绩较为明朗,内幕交易的可能性大大降低,同时又兼有业绩优良、高成长、低风险的特点,因而具备较高的投资价值

市场上较通行的衡量白马股的指标主要包括每股收益、每股净资产值、净资产收益率、净利润增长率、主营业务收入增长率和市盈率等。
因此这个策略的核心是股票池的筛选以及买入卖出条件的设置

基本思路:

筛选出沪深股市中符合:
每股收益(eps)>0.3,净资产收益率(roe)>15%,20<市盈率(pe ratio)<45,净利润增长率(inc_net_profit_annual)>0.3,
将符合以上条件的股票按照ROE降序排列,选取前五只买入
设置调仓周期为十天
一个调仓周期结束后,卖出不在下一周期中符合条件的股票集中的股票,买入符合条件的股票,始终保持最大持有数量为五只

理清楚了基本思路,下一步就是将这个思路翻译成计算机能懂的语言:
1.利用多层if条件嵌套对股票进行筛选
2.将本周期中符合条件的股票放在一起,利用dataframe创建持仓列表
3.到下一周期时进行对比,已有持仓列表的股票若在这一时期还在列表中则继续持有,若不在列表中则卖出

2 设置初始化条件

在之前的教程中,初始化条件里我们只指定了股票池。这次我们要进一步开发initialize函数,让它充分发挥光和热~
我们引入一个新的小伙伴:自定义函数

自定义函数就像它的名字一样,是用户根据自身需要而自己命名及撰写内容的函数。引入自定义函数是功能实现的需要,而将多个自定义函数作为另一个函数的主体内容则是因为当这函数需要完成的任务较多,将内容分在自定义函数建立函数嵌套可以使代码更加功能明确、符合逻辑。这里的自定义函数中我们只放入策略中所需要的部分参数的定义,之后我们会来详细探究自定义函数的相关使用。

每次初始化时,我们需要做的工作大致分为三个部分:set_params策略所需参数设置、set_backtest回测条件具体设置以及之前教程提到的运行频率run_daily的设置。

放在initialize下,这样每次运行初始化函数时也会将包含的自定义函数进行调用。

def initialize (context):
    set_params()   #将策略的参数定义做为该函数主要内容
    set_backtest() #将回测的条件设置做为该函数主要内容
    run_daily(trade, 'every_bar')   #设置运行频率

初始化参数
初始化的参数用于每次运行时更新。在这个策略中为了实现调仓周期的设定以及持有股票数量限制,设置计数日起始为0,调仓周期为10日,以及每次调仓持有的股票数量为5只。

def set_params():
    g.days=0               #设置周期计数起点
    g.refresh_rate=10      #设置调仓周期为10日
    g.stocknum=5           #持有的股票数量

回测条件
回测可以被认为是量化投资与其他投资相比的一个亮点。将策略运用已有的历史数据进行实现,观察该策略在过去的时期的表现状况如年化收益率、夏普率等做为对策略在未来表现估计的一个重要参考信息,从而为策略可以用于实盘中提供可靠的证据。因此回测的的意义主要在于证实策略的有效性,从而帮助我们筛选策略并最优化模型参数。

回测时通常需要选取相比较的基准收益进行对比以检验策略的收益及风险情况,因此基准应当选择能够代表股市整体运行情况的股票。JoinQuant平台回测基准的设定需要用到函数set_benchmark。默认的回测基准是(000300)沪深300指数,这里我们选择(000905)中证500作为基准。

回测试建议模拟盘使用真实价格以使回测结果更加具有代表性, 这就需要设置动态复权(真实价格)模式,即调用 set_option。 更多细节请看: 设置动态复权(真实价格)模式

为了检验错误,使用log.set_level过滤掉order系列API产生的比error级别低的信息

def set_backtest():
    set_benchmark('000905.XSHG')            #将基准设置为中证500
    set_option('use_real_price', True)      #设置动态复权(真实价格)模式
    log.set_level('order', 'error')         #设置不同种类的log的级别, 低于这个级别的log不会输出. 所有log的默认级别是debug

过滤停盘股票
当股票出现停牌状况时,我们要将其从待买入股票列表中剔除出去。停牌股票的筛选可通过get_current_data函数实现。除此之外该函数还可用作当前单位时间(当天/当前分钟)的涨跌停价、是否停牌、当天的开盘价等,具体见API:获取当前时间数据

paused指是否停止或者暂停了交易, 当停牌、未上市或者退市时会返回 True,反之返回false。在这里通过not取反,我们得到paused是false未停盘的股票list(用方括号表示[]),这涉及到列表生成式的内容,可以先记下来,我们之后会进行进一步说明。

def filter_paused_stock(stock_list):
    current_data = get_current_data()       #获取当前单位时间(当天/当前分钟)的涨跌停价, 是否停牌,当天的开盘价等
    return [stock for stock in stock_list if not current_data[stock].paused]  #生成一个未停牌股票代码的列表

手续费和滑点

手续费和滑点在每次交易时都会用到,因此我们将它放在before_trading_start函数中预先设置。该函数会在每天开始交易前被调用一次。before_trading_start的用法详情见:before_trading_start

现实交易中每笔交易会收取一定的交易税费,包含券商手续费和印花税。根据规定,2013年1月1日起,股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5元。

中国A股市场目前为双边收费,券商手续费默认值为万分之三,即0.03%,最少5元。印花税对卖方单边征收,对买方不再征收,系统默认为千分之一,即0.1%。

这里我们为求简单,将2013年之前的手续费统一设置为买入卖出佣金为0.3%,卖出印花税为 0.1%,无买入印花税。通过set_order_cost来实现。

在实战交易中,往往成交价和预期价格有一定偏差,因此我们加入了滑点来更好地模拟真实市场的表现。

通过set_slippage来设置回测具体的滑点参数。系统默认的滑点是PriceRelatedSlippage(0.00246),即交易时加减当时价格的0.123%,这里将滑点的值设置为固定值0.02

答疑与延伸:

  • 为什么实际交易中成交价和预期价格不同?主要有三方面原因。硬件方面当行情波动剧烈时,交易所依托的硬件如网络、系统、服务器等产生的问题而造成成交价格与挂单价格不一致。人为方面:一些不良交易商为获取不正当利益人为操控后台,使得交易平台行情报价与实际报价存在差别。流动性方面:当预期交易数量超过这个价格能接受的交易量,则超出部分按照下一个价格交易。
  • 滑点智能设置为固定值吗?还可以设置成当前价格的百分比。set_slippage的用法详情见:设置滑点
def before_trading_start(context):
    set_slip_fee(context)

# 根据不同的时间段设置滑点与手续费
def set_slip_fee(context):
    set_slippage(FixedSlippage(0.02))   #使用函数set_slippage()设置滑点为固定值0.02,即交易时加减0.01元

    dt=context.current_dt   #通过current_dt函数得到当前单位时间的开始时间

    if dt>datetime.datetime(2013,1, 1):
        #如果当前时间在2013.1.1之后,买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱,交易类型type设置为'stock'股票
        set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003, close_commission=0.0003, close_today_commission=0, min_commission=5), type='stock')

    else:
        #这里进行简化,如果当前时间在2013.1.1之前,买入时佣金千分之三,卖出时佣金千分之三加千分之一印花税, 每笔交易佣金最低扣5块钱,交易类型type设置为'stock'股票
        set_order_cost(OrderCost(open_tax=0, open_commission=0.003,close_commission=0.003, close_tax=0.001,min_commission=5), type='stock')

3 通过财务数据筛选股票

生活充满套路。

我们再次捡起上一篇教程简单市值轮动策略中的老套路--用get_fundamentals来使用财务数据中的市值数据。

分别按照自定义的白马股条件进行筛选,用desc()函数对净资产收益率进行降序排列(若用升序则用asc()),同时用limit()函数将股票数量限制在50只。

def trade(context):
    if g.days%g.refresh_rate == 0:
        stock_to_choose = get_fundamentals(query(   #通过query语句查询相关信息
        valuation.code, valuation.pe_ratio, indicator.eps, indicator.inc_return, indicator.inc_net_profit_annual
    ).filter(                                    #通过filter语句筛选出符合条件的股票
        valuation.pe_ratio < 45,
        valuation.pe_ratio > 20,                 #市盈率在35-40倍间  净利润增长率(inc_net_profit_annual)>0.3, 
        indicator.eps > 0.3,                     #每股收益(eps)大于0.3
        indicator.inc_net_profit_annual > 0.30,  #净利润增长率(inc_net_profit_annual)大于0.3
        indicator.roe > 15                     #净资产收益率(roe)大于15%
    ).order_by(
        # 按净资产收益率降序排列
        indicator.roe.desc()
    ).limit(
        50), date=None)

利用dataframe将这些股票放在一起,得到数据的类型如下图,dataframe的使用详见 :pandas库之数据查看、选择
gaitubao_com_14950946499719.jpeg

因为stock_to_choose作为dataframe格式包含code, pe_ratio, eps等多个列,无法直接对某一列进行循环,因此从dataframe中将'code'列的股票代码提取出来,创建一个新的单独的list取名为stockset,便于后面利用for语句对已持有的股票进行循环

通过context.portfolio.positions.keys()获取现在持有的股票的代码,设置为待选卖出名单,记为sell_list。

        stockset=list(stock_to_choose['code'])                 #抽出stock_to_choose中的股票代码列并存为list类型
        stockset = filter_paused_stock(stockset)              #过滤停牌股票

        sell_list = list(context.portfolio.positions.keys())   #通过portfolio的属性得到现持有股票的列表
        #log.info('sell info',sell_list) #选用`log.info()`显示卖出名单的信息

4 设定买入卖出条件及完善周期循环

通过for循环现有股票,判断现在持有的股票是否在新一轮的买入列表中。其中stocksort[:g.stocknum]表示取数量为g.stocknum的符合条件的股票,这里我们将持有股票数设定为五只。如果已持有的股票不在新的选出的股票中则卖出。

资金的分配方面,通过len()函数给出现在所持有的股票数量。如果小于五只,假设只有三只股票,那么还有两只股票的缺口,就把现有现金平均分为两份作为新的买入现金,否则不分配现金。

最后运行一天时在日期后 1实现计数功能。


        for stock in sell_list:
            if stock not in stockset[:g.stocknum]:       #如果现在持有的股票没有在新一个周期中挑选的待买入列表中
                stock_sell = stock
                order_target_value(stock_sell, 0)        #通过order_target_value()卖出

            ## 分配资金
        if len(context.portfolio.positions) < g.stocknum :       #如果现在持有的股票不足五只
            Num = g.stocknum - len(context.portfolio.positions)  #求出现在的空位
            Cash = context.portfolio.cash/Num                    #将现有的现金进行平分
        else: 
            Cash = 0                    #如果已经有五只股票,则不买入卖出
            Num = 0                     

        ## 买入股票
        for stock in stockset[:g.stocknum]:   #如果现在持有的股票已经在新一期挑选出的股票列表中则忽略
             if stock in sell_list:
                pass
             else:
                stock_buy = stock             #若不在则买入,同时“空位”减去一个
                order_target_value(stock_buy, Cash)
                Num = Num - 1
                if Num==0:
                    break

        # 天计数加一
        g.days = 1
    else:
        g.days  = 1

答疑与延伸:

  • 为什么要判断stock是否在sell_list? :sell_list显示的是现在所持有的股票,判断新选出的股票是否持有可以避免重复买入
  • 为什么要判断Num是否为0? :Num是预期持有的五只股票和现在实际持有的数量的差值,即剩下的“空位”。当我们执行了上一步order_target_value(stock_buy, Cash)的买入后,剩下的空位自然就要减少一个,以免超出规定的股票数量。当空位为0时,说明我们已经持有五只股票了,因此使用break跳出循环

策略完成,进行回测

把买入卖出的代码写好,策略就写完了,完整代码如下:

# 白马股选股策略入门
# 2015-01-01 到 2017-05-17, ¥2000000, 每天
from jqdata import *

'''
================================================================================
总体回测前
================================================================================
'''
#总体回测前要做的事情

#1
#设置策略参数
def initialize (context):
    set_params()
    set_backtest()
    run_daily(trade, 'every_bar')

def set_params():
    g.days=0    
    g.refresh_rate=10 
    g.stocknum=5   

#2
#设置回测条件   
def set_backtest():
    set_benchmark('000905.XSHG') 
    set_option('use_real_price', True)  

    log.set_level('order', 'error')

'''
================================================================================
每天开盘前
================================================================================
'''
#每天开盘前要做的事情
def before_trading_start(context):
    set_slip_fee(context)

# 根据不同的时间段设置滑点与手续费
def set_slip_fee(context):

    set_slippage(FixedSlippage(0.02)) 

    dt=context.current_dt

    if dt>datetime.datetime(2013,1, 1):
        set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0003, close_commission=0.0003, close_today_commission=0, min_commission=5), type='stock')

    else:
        set_order_cost(OrderCost(open_tax=0, open_commission=0.003,close_commission=0.003, close_tax=0.001,min_commission=5), type='stock')

# 过滤停牌股票
def filter_paused_stock(stock_list):
    current_data = get_current_data()
    return [stock for stock in stock_list if not current_data[stock].paused]

'''
================================================================================
每天交易时
================================================================================
'''    
def trade(context):
    if g.days%g.refresh_rate == 0:
        stock_to_choose = get_fundamentals(query(
        valuation.code, valuation.pe_ratio, valuation.market_cap, indicator.eps, indicator.inc_return, indicator.inc_net_profit_annual
    ).filter(
        valuation.pe_ratio < 45,
        valuation.pe_ratio > 20,
        indicator.eps > 0.3,
        indicator.inc_net_profit_annual > 0.30,
        indicator.roe > 15
    ).order_by(
        indicator.roe.desc()
    ).limit(
        50), date=None)

        stockset=list(stock_to_choose['code'])
        stockset = filter_paused_stock(stockset)

        ## 获取持仓列表
        sell_list = list(context.portfolio.positions.keys())
        #log.info('sell info',sell_list)

        for stock in sell_list:
            if stock not in stockset[:g.stocknum]:
                stock_sell = stock
                order_target_value(stock_sell, 0) 

            ## 分配资金
        if len(context.portfolio.positions) < g.stocknum :
            Num = g.stocknum - len(context.portfolio.positions)
            Cash = context.portfolio.cash/Num
        else: 
            Cash = 0
            Num = 0

        ## 买入股票
        for stock in stockset[:g.stocknum]:
            if stock in sell_list:
                pass
            else:
                stock_buy = stock
                order_target_value(stock_buy, Cash)
                Num = Num - 1
                if Num==0:
                    break

        # 天计数加一
        g.days = 1
    else:
        g.days  = 1

在2013.1.1-2017.5.17这段时间内进行回测,回测结果如图:

gaitubao_com_14996946577386.jpeg

自测与自学

1.是否学会设置基准、滑点、佣金?尝试将2013年前税费按照政策再次进行细分(例如假设2011年1月1日之后买入佣金为千分之一,卖出佣金为千分之二,其余不变;2009年1月1日之后买入税为千分之二,卖出税为千分之三,无佣金)
2.是否学会通过读取财务数据对股票进行筛选
3.是否学会建立dataframe并提取关键列信息
4.调整下策略,比如调仓周期,持仓股票数,回测看看效果如何?
5.尝试加入一些代码打印出每次调仓时持有股票情况

全部回复

0/140

达人推荐

量化课程

    移动端课程