上一篇文章:[【新手入门教程】白马股策略][1]
下一篇文章:[【新手入门教程】MACD均线择时策略][19]
[19]: https://www.joinquant.com/post/7095
##多股票追涨策略##
> 学习内容:
> - 学会多股票追涨策略
> - 动态设置股票池,并去除ST、停牌
> - 通过attribute_history读取历史数据
> - 通过逻辑语言设置多重买入卖出条件并生成list股票列表
> - list切片方法初体验以及最值、均值
> - 止损设置
[1]: https://www.joinquant.com/post/6879?tag=python
###1 确定策略内容###
在之前的多股票策略中,我们学习了利用计算机强大的数据处理能力,**同时监视市场上多只股票**,如果满足条件就进行相应交易。简言之,对多个股票分别实行原本的单股票策略。但当时策略中我们使用的股票池手动添加的。
```
def initialize(context):
g.security = ['002043.XSHE','002582.XSHE'] # 将兔宝宝、好想你 两只股票代码存入list中
```
这种方法存在的一个明显缺陷是,操作对象始终是这两只股票,难以将范围扩大到全部股票市场中,同时也缺乏必要的止损设置。为了达到动态调整股票池的效果,这就要用到我们今天涉及的多股票的追涨策略。
基本思路:
> - 筛选出符合:
0<市盈率(pe_ratio)<30, 市净率(pb_ratio)<3, 净利润环比增长率(inc_net_profit_annual)>0.3的股票,按照销售毛利率进行降序排列,选取前150只
> - 买入:股票成交量突破20日内最高并且股票价格向上突破10日均线;
> - 卖出:成交量创10日内新低或者股价向下突破5日新低
> - 止损:当个股价格比买入价下跌超过10%时卖出止损
理清楚了基本思路,下一步就是将这个思路翻译成计算机能懂的语言:
1.运用`get_fundalmentals`函数查询相关财务数据并筛选出符合条件的股票
2.运用`attribute_history`查询单只股票确定时段内成交量和收盘价信息
3.利用if条件语句以及and、or逻辑语句进行买入卖出条件设置
4.设置个股止损条件
###2 运行前:过滤停牌、退市及ST股票###
**过滤停牌股票**
在上个教程中我们初步涉及了过滤停牌股票的相关操作。这里我们再来复习一下:
利用`get_current_data`函数,通过**列表生成式**返回去除停牌股票的股票列表
```
def paused_filter(security_list):
current_data = get_current_data() #通过get_current_data()获取当前单位时间(当天/当前分钟)的涨跌停价, 是否停牌,当天的开盘价等
security_list = [stock for stock in security_list if not current_data[stock].paused]#将没有停牌的股票代码储存在一个列表中
return security_list #返回储存代码的列表
```
**过滤退市股票**
在JoinQuant平台中,如果股票退市则在股票名加入“退”,因此这里可以通过对股票名称的筛选过滤掉已退市股票
```
def delisted_filter(security_list):
current_data = get_current_data()
security_list = [stock for stock in security_list if not '退' in current_data[stock].name]#如果股票名称中没有“退”则储存在列表中
return security_list
```
**过滤ST股票**
ST是英文Special Treatment的缩写。名称前有“ST”字样的股票,通常称为ST股票。ST表示该股票的**财务状况或其他状况异常**,如上市公司经审计连续两个会计年度的净利润都为负值,或者上市公司最近一个会计年度经审计每股净资产低于股票面值等。
类似的,\*ST一般指已连续三年出现亏损的公司股票,有退市风险。证券交易所对其实行”警示存在终止上市风险的特别处理“,在其股票简称前加”\*ST“字样以示区别。
简单来说,**ST股是指财务状况出了问题的股票,而\*ST则是指问题更加严重的股票**。
```
def st_filter(security_list):
current_data = get_current_data()
security_list = [stock for stock in security_list if not current_data[stock].is_st]#通过`is_st`判断是否为ST股或*ST
return security_list
```
**>- Tips:**
如果觉得分开写停牌、退市和过滤ST股比较麻烦,可以将其综合成一个函数:
```
def filter(security_list):
current_data = get_current_data()
return[stock for stock in stock_list
if not current_data[stock].paused
and not '退' in current_data[stock].name
and not current_data[stock].is_st]
```
之后在确定股票池时调用`filter()`函数
###3 依据财务数据对股票池进行筛选###
在确定了策略内容的基础上,我们要把策略运用于一个基础较为良好的股票池,“优中选优”以取得好的效果。这里我们再次运用`get_fundamentals()`函数。这次使用的财务指标还是我们的老朋友市值(market_cap)、市盈率(pe_ratio)、市净率(pb_ratio)、净利润环比增长率(inc_net_profit_annual),按照销售毛利率进行降序排列,并将其中的股票代码取出放在list列中。复习:[`get_fundamentals()`函数的用法][5]
[5]: https://www.joinquant.com/api#getfundamentals-查询财务数据
```
df = get_fundamentals(query(
valuation.code, valuation.pe_ratio,
valuation.market_cap,valuation.pb_ratio,
indicator.inc_net_profit_annual
).filter(
valuation.pe_ratio>0, #挑选出市盈率在30-40倍间
valuation.pe_ratio<30,
valuation.pb_ratio<3, #挑选出市净率小于3
indicator.inc_net_profit_annual > 0.30 #净利润增长大于0.3
).order_by(
indicator.gross_profit_margin.desc() #按照净利润环比增长率降序排列
).limit(
150), date=None)
stockset = list(df['code']) #将dataframe的'code'列放入list中
stockset = paused_filter(stockset)
stockset = delisted_filter(stockset)
stockset = st_filter(stockset) #分别过滤停牌、退市和ST股(注意啦!之前写好的的函数在这里调用)
```
> 答疑与延伸:
> -***为什么选取这几个指标进行筛选?*** 追涨杀跌策略的特点简单来概括就是“跟风”,在股票涨势和成交量喜人时入市,在二者表现不佳时平仓。因此这里只是简单的对产生这样趋势的股票“资质”进行初步筛选,选用的指标也是较为常见的财务指标,旨在提供初步思路。
> -**一个警示** 单独写出来的过滤停牌、退市和ST股的函数记得在确定选中的股票池后进行调用!
###3 历史数据的读取###
运用`attribute_history()`函数获取股票相关历史数据。
类似上篇教程中的卖出列表,这里我们设置buylist为一个空的列表,不断填入需要买入的股票代码。
```
buylist = []
```
在这个策略中,我们要实现的买入卖出条件是:
> - 买入:股票成交量超过20日内最高值并且股票价格向上突破10日均线
> - 卖出:成交量创10日内新低或者股价向下突破5日新低
因此在数据方面,我们需要选择股票20日成交量和股票价格10日均值。均可通过`attribute_history()`实现,将函数传递的最后一个参数类型改变即可。该函数返回类型默认为pandas.DataFrame, 我们还可以用这个函数获取开盘价、收盘价、最高价、最低价等,具体用法详见API文档:[`attribute_history`用法][2]
[2]: https://www.joinquant.com/api#attributehistory-获取历史数据
```
for stock in stockset:
close_data = attribute_history(stock, 11, '1d', ['close']) #获得今天以及之前10日股票收盘价状况
volume = attribute_history(stock, 21, '1d', ['volume']) #获得今天以及之前20日股票成交量状况
```
> 答疑与延伸:
> -**我们需要20日成交量最高值以及10日价格,为什么函数中的参数分别为21和11?** 因为最后通过`attribute_history()`函数得到的数据最后一个为今天的数据,所以加上今天的需要将历史时段长度加一
接下来我们就要从中读取我们需要用到的指标。
首先是成交量突破20日内最高,将最大成交量设为max20_vol,然后运用volume['volume']提取成交量的dataframe里面volume列的内容。想获得20日内最大成交量,首先要知道20日内成交量分别是多少。这里通过list切片的方法,[:20]得到前二十日的成交量,然后通过`max()`函数得到成交量的20日最大值。
```
max20_vol = max(volume['volume'][:20]) #求二十日交易量的最大值,用max()函数实现
```
我们还需要10日内的成交量,通过改变切片参数,调整为[11:20]即可。这里我们需要最近十日成交量的最小值,因此调用函数`min()`实现。
```
min_vol = min(volume['volume'][11:20]) #取最近的十日交易量,用min()求得最小值
```
类似的,我们分别得到收盘价的十日均值以及五日最小值。这里要注意的是求最大值和最小值的`max()`和`min()`括号中要放入整个函数,而求平均值的函数`mean()`则要带在函数后面。
```
max10 = close_data['close'][:10].mean() #运用mean()函数得到收盘价的十日均值
min5 = min(close_data['close'][5:10]) #运用min()函数得到收盘价五日最小值
```
取当前价格和交易量时,可以用**倒序切片法**,在序号前加上负号即为按照倒序进行计数,[-1]即为取list中最后一个数。
```
cur_vol = volume['volume'][-1] #当前成交量
cur_price = close_data['close'][-1] #当前价格
```
通过`context.portfolio.positions.keys()`得到现在所持有的股票代码,将其放入sell_list中以待符合条件时进行买卖设置。
> 答疑与延伸:
> -**切片的负数值则表明倒序,那么如何取倒着的一段数据呢?** 我们举个栗子来说明。之前我们根据需要取收盘价今日前十日数据,运用正序为close_data['close'][:10],而运用倒序则为close_data['close'][-11:-1]
> -**切片区间的设置注意事项?** 需要注意的是,list正序时,计数从0开始,即第一个数据的编号为0,之后依次加一。而采用倒序的时候最后一个值的编号则为-1,前面的则依次减一.区间为[a:b]含义为**从编号为a的数据起**到编号为**b-1**的数据止。
###4 卖出条件的设置###
接下来就是买入卖出条件的设置。
首先我们来看卖出。利用`context.portfolio.positions.keys()`获取现有的股票代码。
我们采取的策略是“成交量创10日内新低或者股价向下突破5日新低就卖出”。翻译为计算机语言就是“当前成交量小于十日内最小成交量或者当前股票价格小于五日内股票平均价格则卖出”。这里涉及到两个大小的比较以及**逻辑运算**问题,同时我们要注意这两个条件满足其一即执行卖出指令。关于”或者“的逻辑词我们用**or**连接这两个比较选项,并用`order_target_value()`执行卖出指令。
```
sell_list = list(context.portfolio.positions.keys())
if (cur_vol < min_vol) or (cur_price < min5): #如果当前交易量小于近五日成交量而且当前价格小于五日成交价格
stock_sell = stock
order_target_value(stock_sell, 0) #卖出这只股票
```
###5 买入条件的设置###
买入方面采用相同的思路,首先将策略中的买入条件“股票成交量突破20日内最高并且股票价格向上突破10日均线”翻译成计算星人能懂的语言,即“当股票当日成交量大于20日内最大成交量并且股票价格大于10日内股票价格平均值则买入“。同时这里还有一个隐含的条件,即若该股票已经在现有持仓股票中则不重复买入。
在选出合适的可以买入的股票后,利用`append()`将股票代码放在buylist[]的列表中,以便接下来统计买入的数量进而根据数量平均分配现金的操作。
```
elif (cur_vol >= max_vol) and (cur_price >= max10) and (stock not in sell_list): #如果股票符合三个买入条件
buylist.append(stock) #将股票代码增加到购买列表buylist中
```
执行买入指令时,我们将现有的现金按照买入列表buylist里面的股票数量分成相应等份,运用`order_target_value`实现买入指令。这里有一个问题就是,可能买入列表并没有符合我们条件的股票,也就是买入列表的长度为0,这样计算分配的现金额时就会出现除数为0的情况。为了避免这种情况,我们可以首先加一个判断语句,如果没有符合条件的股票则把Cash的值设置为当前可使用资金。
```
if len(buylist)==0: #如果购买列表里面没有股票,那么将cash设定为当前所有的资金
Cash = context.portfolio.available_cash
else:
Cash = context.portfolio.available_cash/len(buylist) #如果购买列表里有可买入股票,则将当前可用现金按照股票数量进行均分
for stock in buylist:
order_target_value(stock, Cash)
```
> 答疑与延伸:
> - **当股票列表里面没有股票时为什么不把Cash设置为0?** 设置成0也可以,但是如果没有股票的话无法执行后面的for循环以及`order_target_value(stock, Cash)`,即当买入列表为空则不买入,效果相同。
###6 设置止损条件###
止损指当某一投资出现的亏损达到预定数额时,及时斩仓出局,以避免形成更大的亏损。其目的就在于投资失误时把损失限定在较小的范围内。
股票止损的方法大致分为两大类:
1.根据大盘止损
>-根据大盘指数N日均线进行止损
>-根据大盘指数跌幅进行止损
2.根据个股止损
>-设置止损比例
>-设置固定止损点
这里我们用个股止损设置止损比例的方法进行风险控制。定义一个名为`security_stoploss()`的函数,读出股票当前价格以及平均价格。当当前价格低于平均价格超过一定比例时则卖出。这里我们将参数loss的值设置为0.1,含义即为当跌幅超过10%时平仓止损。
```
def security_stoploss(context,loss=0.1):
if len(context.portfolio.positions)>0: #当现在手中持有的股票数不为0时
for stock in context.portfolio.positions.keys():
avg_cost = context.portfolio.positions[stock].avg_cost #求得该股票在这段时间内的平均价格
current_price = context.portfolio.positions[stock].price #得到股票当前价格
if 1 - current_price/avg_cost >= loss: #当跌幅超过预设loss值时平仓止损
order_target_value(stock, 0)
```
> 答疑与延伸:
> - **为什么要进行止损?** 量化的其中一个优点即在于运用计算机而非人的主观思维进行操作。股票投资与赌博的一个重要区别就在于前者可通过止损把损失限制在一定的范围之内,同时又能够最大限度地获取成功的报酬,换言之,止损使得以较小代价博取较大利益成为可能。股市中无数血的事实表明,一次意外的投资错误足以致命,但止损能帮助投资者化险为夷。
> - **四种止损方法应该如何进行选用?** 止损方法不是唯一的,止损方法的选用同样不是唯一的。策略的止损一般要根据策略的特点进行选择。在策略中我们可以尝试不同的止损方法,也可以采用个股止损和根据大盘走势止损相结合的方法,不同方法尝试后寻找效果最好的。
###策略完成,进行回测###
把买入卖出的代码写好,策略就写完了,完整代码如下:
```
#多股票追涨杀跌策略
import jqdata
def initialize(context):
set_benchmark('000300.XSHG')
set_option('use_real_price', True)
log.set_level('order', 'error')
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
run_daily(market_open, time='every_bar', reference_security='000300.XSHG')
def paused_filter(security_list):
current_data = get_current_data()
security_list = [stock for stock in security_list if not current_data[stock].paused]
return security_list
def delisted_filter(security_list):
current_data = get_current_data()
security_list = [stock for stock in security_list if not '退' in current_data[stock].name]
return security_list
def st_filter(security_list):
current_data = get_current_data()
security_list = [stock for stock in security_list if not current_data[stock].is_st]
return security_list
def market_open(context):
df = get_fundamentals(query(
valuation.code, valuation.pe_ratio, valuation.market_cap,valuation.pb_ratio,indicator.eps, indicator.inc_return, indicator.inc_net_profit_annual
).filter(
valuation.pe_ratio>0,
valuation.pe_ratio<30,
valuation.pb_ratio<3,
indicator.inc_net_profit_annual > 0.30
).order_by(
indicator.gross_profit_margin.desc()
).limit(
150), date=None)
stockset = list(df['code'])
stockset = paused_filter(stockset)
stockset = delisted_filter(stockset)
stockset = st_filter(stockset)
buylist = []
for stock in stockset:
close_data = attribute_history(stock, 11, '1d', ['close'])
volume = attribute_history(stock, 21, '1d', ['volume'])
max20_vol = max(volume['volume'][:20])
min_vol = min(volume['volume'][11:20])
max10 = close_data['close'][:10].mean()
min5 = min(close_data['close'][5:10])
cur_vol = volume['volume'][-1]
cur_price = close_data['close'][-1]
sell_list = list(context.portfolio.positions.keys())
if (cur_vol < min_vol) or (cur_price < min5):
stock_sell = stock
order_target_value(stock_sell, 0)
elif (cur_vol >= max20_vol) and (cur_price >= max10) and (stock not in sell_list):
buylist.append(stock)
if len(buylist)==0:
Cash = context.portfolio.available_cash
else:
Cash = context.portfolio.available_cash/len(buylist)
for stock in buylist:
order_target_value(stock, Cash)
def security_stoploss(context,loss=0.1):
if len(context.portfolio.positions)>0:
for stock in context.portfolio.positions.keys():
avg_cost = context.portfolio.positions[stock].avg_cost
current_price = context.portfolio.positions[stock].price
if 1 - current_price/avg_cost >= loss:
log.info(str(stock) + ' 跌幅达个股止损线,平仓止损!')
order_target_value(stock, 0)
```
在2015.1.1-2016.12.31这段时间内进行回测,回测结果如图:
![gaitubao_com_14968032933576.jpeg][15]
[15]: https://image.joinquant.com/eca26ed01e94db4858676197623b471c
##自测与自学##
1.是否学会多股票追涨杀跌策略
2.是否学会动态设置股票池,并去除ST、停牌
3.是否学会list切片方法
4.尝试通过attribute_history读取30日开盘价(open)收盘价(close)以及最高价(high)
5.对策略进行调整,比如将买入条件中股票成交量设置为突破15日新高,回测看看效果如何?
6.尝试将个股根据比例止损改为根据具体价格止损
超级感谢能得到您的喜欢!大家一起学习呀!@archwolf
2017-05-25
我拷贝这个策略,测试了一下,3个过滤函数没有作用。感觉是主程序没有调用(老小白,请指教)。
2017-06-06
感谢慧眼指点!检查了一遍后发现确实是忘记调用啦 已经在market_open函数中的stockset设置里面更新啦@成都老柴 肥肠感谢!
2017-06-07
API文档中说attribute_history取得的数据,不包括当天啊,应该就不用加一天了吧。用cur_vol = volume['volume'][-1]、cur_price = close_data['close'][-1] 这2个公式,取到的应该是上一交易日的数据,不是当天的吧。
2017-11-11