策略系列篇(一)—小市值轮动

前言

写于2022年一月,临近春节,祝大家新年快乐!写这篇文章的时候,刚经历了1.25号4000+只股票下跌,虽然入市不久,仓位也仅控制在3层,但是仍然缺乏安全感。于是提笔把入市半年的诸多策略以及成长记录下来。

首先,投资就是一场战争。所有人入市的初心都是为了赚钱,但是市场是一个不可预知的混沌模型,小到几百块的资金,大到国家资本,无数个资金相互交织组成一个金融战场。既然是战场就会有厮杀,就会有胜负,有的人凯旋而归,有的人战死沙场。尽管金融市场不是真正的打仗,不会直接性的造成死亡,但是本质亦不远矣。试想有的人辛辛苦苦积攒下来的积蓄,跑不过通货膨胀,购买力甚至还在下降,只能维持温饱而已;试想有的城市通过政策补贴吸引人才,完成转型升级,然后实现飞跃发展;试想有的国家肆意的开放金融市场,经济的飞速发展到盛极而衰。小到个体,大到城市,甚至是国家,投资皆是一场战争。

其次,投资需要好策略和好心态。兵者,国之大事,死生之地,存亡之道,不可不察也战争,是国家的大事,它关系到军民的生死,国家的存亡,不能不慎重考察研究。

投资需要好策略。举个例子,就像是战场时期,1000士兵对上10倍于己的平民肯定是*。为什么呢?显而易见,士兵之于平民,胜在训练组织能力,进退有度,指挥统一,反之,平民虽然人数众多,但是杂乱无序,只能四散而逃,之所以士兵能有较高水平的训练组织能力,依赖于平时的针对训练,我认为对士兵所做的训练,在投资中类似于根据历史数据来进行训练学习,找出特征因子,最终形成一个好的交易策略,应用于市场中。

投资需要好心态。举个例子,刚训练好的新兵第一次上战场的伤亡概率最高,而老兵损伤往往要小得多。显而易见,不能适应战场的新兵都已经阵亡了,活下来的老兵或多或少都有领悟。尽管新兵老兵在平时训练时是一样的,但是战场上为什么会有如此巨大的差别呢?我认为取决于是否有好的心态,市场瞬息万变,没有人能肯定未来会发生什么,每逢大事当有静气。在做足准备的情况下,需要坚定不移的相信自己的判断,同时要具有底线思维。

最后,好策略和好心态是相辅相成的。如果没有好的交易策略,长期无逻辑的交易,是不可能有好心态的;如果有好的交易策略,没有良好的心态,也难以执行。

好的策略需要自身不断地迭代更新,需要不断的学习知识,提升认知;而好的心态需要市场不断地锤炼打磨,需要进行体会总结。

一个完整的交易策略一般包括如下几点构成:

        1.买什么? 选股

        2.买卖多少?仓位控制

        3.何时买卖?入场时机

        4.止损条件。何时退出亏损的仓位

        5.止赢条件。何时退出盈利的仓位

废话完毕,正文开始!

---------------分割线---------------

1. 原理说明

小市值效应,本质上说就是市值小的股票,在将来上涨的概率越大。在全球各大市场上都有验证,在A股市场也存在此效应。

至于为什么有小市值效应?笔者推测有如下3点:

1.可能是投资者普遍不愿意持有小公司股票,使得这些小公司价格普遍偏低,甚至低于成本价,因此会有较高的预期收益率;

2.A股市场存在上市公司的壳价值,可能被溢价收购,导致小市值公司股票大幅度增值;

3.小市值公司成长空间更大,方便炒作;

2. 策略逻辑

第一步:确定调仓频率,每天更新调仓
第二步:确定股票池股票数量,这里假设有5支
第三步:调仓日当天获取前一天的历史数据,并按照市值由小到大排序
第四步:买入前5支股票

回测期:2011-12-30 到 2021-12-31
股票池:中小板股票
回测初始资金:10万

3. 回测结果和稳健性分析

回测时间为近10年,收益高达442倍,最大回测仅32%。

设定初始资金10万,手续费率为0.03%,滑点比率为0.1%。回测结果如下图所示:

策略系列篇(一)—小市值轮动

4. 策略代码

# 导入函数库
from jqdata import *


# 初始化函数,设定基准等等
def initialize(context):
	# 设定沪深300作为基准
	set_benchmark('000300.XSHG')
	# 开启动态复权模式(真实价格)
	set_option('use_real_price', True)
	#防止未来函数
	set_option("avoid_future_data", True)
	#滑点设置为0.001
	set_slippage(FixedSlippage(0.001))
	# 输出内容到日志 log.info()
	log.info('初始函数开始运行且全局只运行一次')
	# 过滤掉order系列API产生的比error级别低的log
	log.set_level('order', 'error')
	
	# 股票池
	g.security_universe_index = "399101.XSHE"  # 中小板
	g.buy_stock_count = 5
	
	### 股票相关设定 ###
	# 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱
	set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5),
				   type='stock')
	
	## 运行函数(reference_security为运行时间的参考标的;传入的标的只做种类区分,因此传入'000300.XSHG'或'510300.XSHG'是一样的)
	# 定时运行
	run_daily(my_trade, time='9:30', reference_security='000300.XSHG')
	# 收盘后运行
	run_daily(after_market_close, time='after_close', reference_security='000300.XSHG')


## 开盘时运行函数
def my_trade(context):
	# 选取中小板中市值最小的若干只
	check_out_lists = get_index_stocks(g.security_universe_index)
	q = query(valuation.code).filter(
		valuation.code.in_(check_out_lists)
	).order_by(
		valuation.circulating_market_cap.asc()
	).limit(
		g.buy_stock_count * 3
	)
	check_out_lists = list(get_fundamentals(q).code)
	# 过滤: 三停(停牌、涨停、跌停)及st,*st,退市
	check_out_lists = filter_st_stock(check_out_lists)
	check_out_lists = filter_limitup_stock(context, check_out_lists)
	check_out_lists = filter_limitdown_stock(context, check_out_lists)
	check_out_lists = filter_paused_stock(check_out_lists)
	# 取需要的只数
	check_out_lists = check_out_lists[:g.buy_stock_count]
	# 买卖
	adjust_position(context, check_out_lists)


## 收盘后运行函数
def after_market_close(context):
	log.info(str('函数运行时间(after_market_close):' + str(context.current_dt.time())))
	# 得到当天所有成交记录
	trades = get_trades()
	for _trade in trades.values():
		log.info('成交记录:' + str(_trade))
	log.info('一天结束')
	log.info('##############################################################')


# 自定义下单
# 根据Joinquant文档,当前报单函数都是阻塞执行,报单函数(如order_target_value)返回即表示报单完成
# 报单成功返回报单(不代表一定会成交),否则返回None
def order_target_value_(security, value):
	if value == 0:
		log.debug("Selling out %s" % (security))
	else:
		log.debug("Order %s to value %f" % (security, value))
	
	# 如果股票停牌,创建报单会失败,order_target_value 返回None
	# 如果股票涨跌停,创建报单会成功,order_target_value 返回Order,但是报单会取消
	# 部成部撤的报单,聚宽状态是已撤,此时成交量>0,可通过成交量判断是否有成交
	return order_target_value(security, value)


# 开仓,买入指定价值的证券
# 报单成功并成交(包括全部成交或部分成交,此时成交量大于0),返回True
# 报单失败或者报单成功但被取消(此时成交量等于0),返回False
def open_position(security, value):
	order = order_target_value_(security, value)
	if order != None and order.filled > 0:
		return True
	return False


# 平仓,卖出指定持仓
# 平仓成功并全部成交,返回True
# 报单失败或者报单成功但被取消(此时成交量等于0),或者报单非全部成交,返回False
def close_position(position):
	security = position.security
	order = order_target_value_(security, 0)  # 可能会因停牌失败
	if order != None:
		if order.status == OrderStatus.held and order.filled == order.amount:
			return True
	
	return False


# 交易
def adjust_position(context, buy_stocks):
	for stock in context.portfolio.positions:
		if stock not in buy_stocks:
			log.info("stock [%s] in position is not buyable" % (stock))
			position = context.portfolio.positions[stock]
			close_position(position)
		else:
			log.info("stock [%s] is already in position" % (stock))
	
	# 根据股票数量分仓
	# 此处只根据可用金额平均分配购买,不能保证每个仓位平均分配
	position_count = len(context.portfolio.positions)
	if g.buy_stock_count > position_count:
		value = context.portfolio.cash / (g.buy_stock_count - position_count)
		
		for stock in buy_stocks:
			if context.portfolio.positions[stock].total_amount == 0:
				if open_position(stock, value):
					if len(context.portfolio.positions) == g.buy_stock_count:
						break


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


# 过滤ST及其他具有退市标签的股票
def filter_st_stock(stock_list):
	current_data = get_current_data()
	return [stock for stock in stock_list
			if not current_data[stock].is_st
			and 'ST' not in current_data[stock].name
			and '*' not in current_data[stock].name
			and '退' not in current_data[stock].name]


# 过滤涨停的股票
def filter_limitup_stock(context, stock_list):
	last_prices = history(1, unit='1m', field='close', security_list=stock_list)
	current_data = get_current_data()
	
	# 已存在于持仓的股票即使涨停也不过滤,避免此股票再次可买,但因被过滤而导致选择别的股票
	return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
			or last_prices[stock][-1] < current_data[stock].high_limit]


# return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
#    or last_prices[stock][-1] < current_data[stock].high_limit * 0.995]

# 过滤跌停的股票
def filter_limitdown_stock(context, stock_list):
	last_prices = history(1, unit='1m', field='close', security_list=stock_list)
	current_data = get_current_data()
	
	return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
			or last_prices[stock][-1] > current_data[stock].low_limit]

5. 策略思考以及改进

策略优势:收益高,持续有效;

策略不足:高波动性,收益曲线不平滑;

众所周知,高波动极其考验投资者的执行力,往往大家都会高估自己的承受能力。

6. 后续其他

策略源码如上,喜欢的朋友可以自己运行一下。

后续有时间我也会陆续更新其他策略,持续学习,持续分享。

注:此策略只用于学习、交流、演示,不构成任何投资建议。

上一篇:浅谈 Glide - BitmapPool 的存储时机 & 解答 ViewTarget 在同一View显示不同的图片时,总用同一个 Bitmap 引用的原因


下一篇:人工智能、大数据、数据挖掘、机器学习-数据集来源