回测结果:回测区间 2023-01-01 ~ 2025-02-02 、策略收益 40.06% 、 年化19%
天天想着年化N倍的圣杯策略,到最后发现都是白折腾,徒耗光阴,蓦然回首还是价值投资才是最终的归宿?毕竟这条路上有无数成功的先例。
思路:
1、选上市200天以上主板成熟个股;
2、价格低于价值(pb< 1);
3、公司真的在不断赚钱,赚真钱,主要用到:
(1)经营现金流:这个比利润真实,不像利润容易作假;
(2)扣非净利润:别靠做账做出假利润;
(3)总资产收益率:别靠负债做高ROE;
(4)净利润同比增长:说明经营良好; 再加上排序指标,取前5支,每月调仓。
随着股市波动,在大牛市顶端会自动空仓留住利润,因为那个位置选不出有价值的票了。
ps,回测过从2005年至今(2023.4.11),超额收益一直稳定上升。
策略说明:
本策略基于价值投资理念,通过筛选低市净率(PB < 1)、高资产回报率(ROA > 0.15)、净利润同比增长的股票,构建投资组合。策略的核心思想是寻找被市场低估的优质公司,长期持有以获取超额收益。
策略核心逻辑:
-
股票池筛选:
-
筛选上市200天以上的主板股票,排除创业板、ST股、停牌股、涨停或跌停的股票。
-
筛选市净率(PB)小于1的股票,确保价格低于账面价值。
-
筛选经营活动现金流净额大于1亿、扣非净利润大于1亿、资产回报率(ROA)大于0.15、净利润同比增长的股票。
-
-
交易决策:
-
每月第一个交易日开盘前筛选符合条件的股票,按资产回报率(ROA)排序,选取前5只股票。
-
每月第一个交易日开盘时调整持仓,买入新筛选的股票,卖出不再符合条件的股票。
-
-
风险控制:
-
设置止损条件,当持仓股票亏损超过5%时,卖出止损。
-
通过跌停数量判断市场风险,若跌停数量较多,则暂停买入。
-
策略特点:
-
价值投资:通过筛选低市净率、高资产回报率的股票,寻找被市场低估的优质公司。
-
长期持有:每月调仓一次,适合中长期投资者。
-
动态仓位管理:限制持仓数量,避免过度集中风险。
适用场景:
-
适用于A股市场,能够有效捕捉被市场低估的优质公司。
-
适合中长期投资者,通过长期持有获取稳定收益。
风险提示:
-
价值投资策略在牛市中可能表现不佳,需注意市场风险。
-
策略依赖于财务数据的准确性和及时性,需定期更新数据。
策略代码结构:
-
初始化函数(
initialize):-
设定基准指数(沪深300)。
-
配置交易手续费和滑点。
-
定义每日运行的函数(开盘前、开盘时、收盘后)。
-
-
开盘前运行函数(
before_market_open):-
筛选符合条件的股票,按资产回报率(ROA)排序,选取前5只股票。
-
-
开盘时运行函数(
my_trade):-
根据筛选出的股票调整持仓,买入新筛选的股票,卖出不再符合条件的股票。
-
-
收盘后运行函数(
after_market_close):-
记录当天的成交记录,并输出日志。
-
-
辅助函数:
-
order_target_value_:自定义下单函数,记录买入或卖出操作。 -
open_position:买入指定价值的股票。 -
close_position:卖出指定持仓的股票。 -
adjust_position:调整持仓,买入新筛选的股票,卖出不再符合条件的股票。 -
get_name:获取股票的名称。
-
策略优化建议:
-
参数优化:
-
可对市净率(PB)、资产回报率(ROA)等筛选条件进行优化,以适应不同的市场周期。
-
调整持仓数量(如5只),以平衡风险与收益。
-
-
风险控制:
-
增加最大回撤限制,当回撤超过一定比例时暂停交易。
-
设置单笔交易的最大亏损比例,避免单笔交易亏损过大。
-
-
多因子结合:
-
可结合其他财务指标(如市盈率、市净率等)进行综合筛选,提高策略的稳定性。
-
总结:
本策略通过价值投资理念,筛选被市场低估的优质公司,适合中长期投资者在A股市场中获取稳定收益。策略逻辑清晰,适合有一定编程基础的投资者进行进一步优化和实盘测试。
策略源码
# 风险及免责提示:该策略由聚宽用户在聚宽社区分享,仅供学习交流使用。
# 标题:大市值价值投资,从2005年至今超额稳定
from jqdata import * # 从jqdata模块导入所有内容
# 初始化函数,设定基准等等
def initialize(context):
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
#防止未来函数
set_option("avoid_future_data", True)
# 输出内容到日志 log.info()
log.info('初始函数开始运行且全局只运行一次')
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
g.buy_stock_count = 5 # 定义全局变量,用于指定策略希望持有的股票数量是5只
g.check_out_lists = [] # 定义全局变量,用于存储策略筛选出来的股票代码列表;初始化为空列表,表示开始时没有选定任何股票
# 设置交易成本, 卖出时印花税 买入时印花税 卖出时佣金 最低佣金,不包含印花税
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.00012, close_commission=0.00012, min_commission=5),
type='stock') # type: 股票
run_monthly(before_market_open, 1, time='5:00', reference_security='000300.XSHG') # 每月第一个交易日开盘前 5:00 运行 before_market_open
run_monthly(my_trade, 1, time='9:30', reference_security='000300.XSHG') # 每月第一个交易日开盘时 9:30 运行 my_trade
run_daily(after_market_close, time='after_close', reference_security='000300.XSHG') # 收盘后 after_close 运行 after_market_close
# 开盘时运行函数
def my_trade(context):
# 调用adjust_position函数来调整当前的投资组合,的是在开盘时根据预设的股票列表调整持仓,执行买入或卖出操作
adjust_position(context, g.check_out_lists)
# 收盘后运行函数
def after_market_close(context):
positions_dict = context.portfolio.positions # 获取当前投资组合中所有持仓的字典对象
for position in list(positions_dict.values()): # 遍历投资组合中的每个持仓项
# 使用log.info打印每个持仓的详细信息,格式化字符串,输出持仓的股票名称、数量、市值、盈利百分比和建仓时间
log.info("当前持仓:{0}, 数量:{1}, 市值:{2}, 盈利:{3}%, 建仓时间:{4}".format(get_name(position.security), position.total_amount, round(position.value,0), round((position.value-(position.avg_cost*position.total_amount))/(position.avg_cost*position.total_amount)*100,1), position.init_time))
log.info('#########################################################################################\n\n')
开平仓关键函数解锁后查看:
# 交易函数,用于调整持仓
def adjust_position(context, buy_stocks):
for stock in context.portfolio.positions: # 遍历当前投资组合中的所有持仓股票
current_data = get_current_data() # 获取当前的股票数据
nosell_1 = context.portfolio.positions[stock].price >= current_data[stock].high_limit # 检查是否因为涨停而不能卖出
sell_2 = stock not in buy_stocks # 检查该股票是否不在买入列表中
if sell_2 and not nosell_1: # 如果股票不在买入列表中且不是因为涨停而不能卖出,则卖出该股票
log.info("调出平仓:[%s]" % (stock))
position = context.portfolio.positions[stock] # 获取该股票的持仓信息
close_position(position) # 调用close_position函数尝试卖出该股票
else:
log.info("已持仓,本次不买入:[%s]" % (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 stock not in context.portfolio.positions: # 如果该股票不在当前持仓中
if open_position(stock, value): # 调用open_position函数尝试买入该股票
if len(context.portfolio.positions) == g.buy_stock_count: # 如果买入成功且持仓数量达到预设的股票数量,则停止买入
break
# 通过代码返回股票名称
def get_name(stk):
return get_security_info(stk).display_name+':'+stk[:6] # get_security_info 可以获取股票等的信息,包括代码,中文名称等
# 开盘前运行函数
def before_market_open(context):
g.check_out_lists = [] # 清空前一个交易日的股票池列表
current_data = get_current_data() # 获取当前的交易数据
check_date = context.previous_date - datetime.timedelta(days=200) # 设置查询日期为前一个交易日减去200天,用于获取足够历史数据的证券列表
all_stocks = list(get_all_securities(date=check_date).index) # 获取指定日期的所有证券列表
# 过滤掉不符合条件的股票:创业板、ST股、停牌、当日涨停或跌停的股票
all_stocks = [stock for stock in all_stocks if not (
(current_data[stock].day_open == current_data[stock].high_limit) or # 涨停开盘
(current_data[stock].day_open == current_data[stock].low_limit) or # 跌停开盘
current_data[stock].paused or # 停牌
current_data[stock].is_st or # ST
('ST' in current_data[stock].name) or
('*' in current_data[stock].name) or
('退' in current_data[stock].name) or
(stock.startswith('30')) or # 创业
(stock.startswith('68')) or # 科创
(stock.startswith('8')) or # 北交
(stock.startswith('4')) # 北交
)]
# 构建查询对象,用于筛选符合条件的股票
q = query(
valuation.code, valuation.market_cap, valuation.pe_ratio, income.total_operating_revenue
).filter(
valuation.pb_ratio < 1, # 市净率小于1
cash_flow.subtotal_operate_cash_inflow > 1e6, # 经营活动产生的现金流量净额大于1亿
indicator.adjusted_profit > 1e6, # 调整后的净利润大于1亿
indicator.roa > 0.15, # 资产回报率大于0.15
indicator.inc_net_profit_year_on_year > 0, # 净利润同比增长大于0
valuation.code.in_(all_stocks) # 股票代码在过滤后的列表中
).order_by(
indicator.roa.desc() # 按资产回报率降序排列
).limit(
g.buy_stock_count * 3 # 限制查询结果数量为预设买入股票数量的3倍
)
check_out_lists = list(get_fundamentals(q).code) # 执行查询并获取符合条件的股票列表
check_out_lists = check_out_lists[:g.buy_stock_count] # 取需要的股票数量,即预设买入股票数量
g.check_out_lists = check_out_lists # 更新全局变量check_out_lists为新的股票池
log.info("今日股票池:%s" % g.check_out_lists) # 记录今日股票池到日志
2025-02-21
