原作:[7年40倍,模拟超过两年,年化高回撤低](https://www.joinquant.com/view/community/detail/6d0d64532d0ad36c2c43292ee663042a?type=1)
作者:quakecat
优化:[2021年度文章精选第一篇策略的修订及详解-Python2版](https://www.joinquant.com/post/36375)
作者:蚂蚁量化
本文主要记录一下自己的改写过程,原策略本身就非常优秀,改写只是为了适应我自己的需要,没有孰优孰劣之分,感谢两位作者的分享!(7年40倍策略之后简称740策略)
**1.去掉择时部分,不择时收益更高**
是否需要择时,主要由投资者的风险承受能力决定,由于盈亏同源,择时一定会带来收益的下降。如果我判断策略失效会直接停掉而不是通过择时来降低回撤。即使择时,原策略的参数已经不适应当前市场情况,需要重新调参。
原策略无修改

没有择时以及仓位控制,原策略默认持仓数

**2.收益归因**
去掉择时和仓位控制后,更容易分析因子对策略收益的贡献。作者在选股前进行了一系列的筛选,比如用pb和市值,宽松地过滤了一下,以及过滤掉st停牌开盘涨跌停股票等操作,应该都只是排除掉异常股票,有必要但不是重点。
另外一些过滤,包括过滤昨日涨幅过大,过滤刚刚卖出时间不久的股票,过滤科创和创业板,过滤新股。
- 过滤昨日涨幅过大,不知道是否有必要,测试不足,先去掉。
- 卖出一段时间后不买回,我的有些策略中也采用了这项过滤,但是表现不稳定,有时会是负优化,去掉。
- 科创和创业板上小盘轮动确实不是很稳定,尤其是科创,收益差,还需要限价下单很麻烦,过滤掉没毛病。创业板我选择先保留。
- 过滤新股是因为新股上市拉高后一段时间内会持续走下坡路,这个时间在1年左右,选择过滤250-500天应该都没问题。
*原作中有一个关键的筛选,可以说奠定了整个回测的基调,代码如下*
```
rank_stock_list = get_fundamentals(query(
valuation.code, valuation.market_cap, valuation.circulating_market_cap
).filter(valuation.code.in_(stock_list)
).order_by(valuation.circulating_market_cap.asc()).limit(100))
```
**上面的过滤完成后应该还是会有3000只左右的股票,而这条代码就决定了关键的选股池是从这3000只中选择最小的100只,这种极度暴露单因子的做法结果是,小盘因子单独就已经决定了策略收益的走向,而作者之后的五因子只是在帮小盘因子锦上添花,更何况五因子中还有两个就是市值本身。最后通过五因子评分选出的4只股票又是进一步放大了小盘因子的风险暴露。**
小盘因子确实是长期稳定有效的,这里并不是批判原策略,只是想说明五因子的效果并没有之前预期那么大。不过,这种做因子的方法还是很有借鉴意义的。
**3.扩容**
说到这,其实这个策略已经拆解分析的差不多了,如果您只是小资金又不喜欢回撤,完全可以继续使用原策略,不需要做任何改变。
740策略本身只买不超过5只股票,如果改写持仓量为50只,收益就会明显下降,下图展示了没有择时和仓位控制,持仓50只的情况。

如果您想做一个50只持仓的小盘基金,还想达到40%以上的年化收益,仅靠调参740策略应该是不够的,需要与另一个策略结合。
[持仓95只大容量小市值,媲美金元顺安元启](https://www.joinquant.com/view/community/detail/6b331f66c139d4659734bf56973eadee)
作者:开心果
这个策略非常神奇,众所周知,小盘策略的收益受持仓数量影响递减明显,而这个策略在测试10-500只容量时,都能表现出稳定的超额收益,其选股逻辑也非常简单,只有以下几条。
```
df = get_fundamentals(query(
valuation.code
).filter(
valuation.code.in_(stocks),
valuation.pb_ratio > 0, #市净率
indicator.inc_return >0, #扣非净资产收益率
indicator.inc_total_revenue_year_on_year>0, #营业总收入同比增长率
indicator.inc_net_profit_year_on_year>0, #净利润同比增长率
).order_by(
valuation.market_cap.asc()
).limit(g.stock_num))
```
原作者还用了indicator.ocf_to_operating_profit > 5 (经营活动产生的现金流量净额/经营活动净收益 大于 5%),我不理解他为什么要使用5%,去掉了,影响不大。而其他的过滤条件是基于常见财务指标保留了基本面还可以的公司,从因子的角度来说即增加了价值类因子的暴露,这对于小盘因子来说是一种不错的补充。下图展示了原策略不同持仓数量的收益情况。

从图中可以看出,虽然收益受持仓量影响很小,但总体收益不高,是不是大持仓就一定要牺牲收益呢?不是!这里就可以结合之前740的五因子评分轮动了,做法如下。
```
def get_stock_list(context):
# 获取前N个单位时间当时的收盘价
def get_close(stock, n, unit):
return attribute_history(stock, n, unit, 'close')['close'][0]
# 获取现价相对N个单位前价格的涨幅
def get_return(stock, n, unit):
price_before = attribute_history(stock, n, unit, 'close')['close'][0]
price_now = get_close(stock, 1, '1m')
if not isnan(price_now) and not isnan(price_before) and price_before != 0:
return price_now / price_before
else:
return 100
# 获得初始列表
yesterday = context.previous_date
initial_list = get_all_securities('stock', yesterday).index.tolist()
initial_list = filter_kcbj_stock(initial_list)
initial_list = filter_new_stock(context, initial_list, 375)
initial_list = filter_st_stock(initial_list)
q = query(
valuation.code, valuation.market_cap, valuation.circulating_market_cap
).filter(
valuation.code.in_(initial_list),
valuation.pb_ratio > 0,
indicator.inc_return > 0,
indicator.inc_total_revenue_year_on_year > 0,
indicator.inc_net_profit_year_on_year > 0
).order_by(
valuation.market_cap.asc()).limit(100)
df = get_fundamentals(q, date=yesterday)
df.index = df.code
initial_list = list(df.index)
#为了方便查看五因子数值与最终评分,将其改写成dataframe
#获取原始值
MC, CMC, PN, TV, RE = [], [], [], [], []
for stock in initial_list:
#总市值
mc = df.loc[stock]['market_cap']
MC.append(mc)
#流通市值
cmc = df.loc[stock]['circulating_market_cap']
CMC.append(cmc)
#当前价格
pricenow = get_close(stock, 1, '1m')
PN.append(pricenow)
#5日累计成交量
total_volume_n = attribute_history(stock, 1200, '1m', 'volume')['volume'].sum()
TV.append(total_volume_n)
#60日涨幅
m_days_return = get_return(stock, 60, '1d')
RE.append(m_days_return)
#合并数据
df = pd.DataFrame(index=initial_list,
columns=['market_cap','circulating_market_cap','price_now','total_volume_n','m_days_return'])
df['market_cap'] = MC
df['circulating_market_cap'] = CMC
df['price_now'] = PN
df['total_volume_n'] = TV
df['m_days_return'] = RE
df = df.dropna()
min0, min1, min2, min3, min4 = min(MC), min(CMC), min(PN), min(TV), min(RE)
#计算合成因子
temp_list = []
for i in range(len(list(df.index))):
score = g.weights[0] * math.log(min0 / df.iloc[i,0]) + g.weights[1] * math.log(min1 / df.iloc[i,1]) + g.weights[2] * math.log(min2 / df.iloc[i,2]) + g.weights[3] * math.log(min3 / df.iloc[i,3]) + g.weights[4] * math.log(min4 / df.iloc[i,4])
temp_list.append(score)
df['score'] = temp_list
#排序并返回最终选股列表
df = df.sort_values(by='score', ascending=False)
final_list = list(df.index)
return final_list
```
通过基本面筛选后保留100只小盘股,在100只股票中用五因子筛选保留50只,每周调仓,结合我自己的涨停开板卖出逻辑,就可以达到回测图中的效果了。740侧重技术面与量价指标选股,缺乏基本面逻辑,大容量95原本是按月调仓的基金风格策略,缺乏对股票技术面的判断,不够灵活,二者正好具备互补的条件,这也是我认为可以结合一下的原因。
组合想法参考
[sales_growth因子选择股票池,小市值选股择时](https://www.joinquant.com/view/community/detail/13180466101a4bbdf6032790dc456082)
作者:Pengpengpeng
[因子择股基础上牛熊判断小市值策略](https://www.joinquant.com/view/community/detail/5265fd44a125b5fe9435aa9c12473b47)
作者:书鱼知小
改写后的740策略不包含任何择时,如果需要择时请自行设计。
**再次感谢以上各位朋友!**(不想打扰人家就不写@了)
**代码问题**
下面回测预览中,源码第22行
把g.limit_up_list=[]改成g.high_limit_list=[]
此问题不影响回测,但影响模拟跟一创实盘!!!请务必修改!!!
**风险提示**
1. 当前(2022年12月)小盘风格对沪深300超额收益处于历史高位
2. 持仓量大且周调仓可能产生更高的滑点。回测中收益虚高,因为我设置了滑点为零,单向成本万八。后来我又测试了下系统默认滑点跟交易成本,最终收益458%
3. 其它原因以后想到了再更新
当前小盘风格对沪深300超额收益处于历史高位
---
理论上,大部分策略不都呈现这种情况吗,没有很理解。
2022-11-25
@wywy1995 刚发现一个小瑕疵,测试用的50万资金起步,买入50只股票,价格高于10元的就不能买入,实际就暗含了价格< 10的条件。当然后面收益高了,这个暗含条件就逐渐释放了。非常优秀,我再继续学习,感谢感谢!
2022-11-25
清早前来点赞!大神这废寝忘食的啊,半夜都在工作,注意休息
2022-11-25
@蚂蚁量化 每个股票可以买1w块钱,没有说不能买超过10块钱的,是不能超过100啊
2022-11-25
@nora_joinquant 以前十几年的规律是每隔3-5年,就会出现一年超额不行的年份,分别是2017年和2011年
2022-11-25
@nora_joinquant 下面是我统计的一个大致数据
2022超额收益 62.23%
2021超额收益 50.01%
2020超额收益 23.03%
2019超额收益 43.70%
2018超额收益 62.28%
2017超额收益 -23.96%
2016超额收益 67.58%
2015超额收益 264.84%
2014超额收益 29.84%
2013超额收益 84.68%
2012超额收益 17.47%
2011超额收益 2.32%
2010超额收益 56.76%
2009超额收益 221.03%
2008超额收益 47.70%
大致是每累计到6-10倍的超额收益之后就出现一次超额消失的年份,时间3-5年
2022-11-25