问题描述
解决方案
这是一个基于 EV/EBITDA(企业价值倍数) 的价值选股策略。
策略原理
EV/EBITDA 是一个广泛使用的价值投资指标,相比于市盈率(PE),它更能排除资本结构(债务杠杆)、税收政策和折旧摊销政策的影响,从而更纯粹地反映企业的估值水平。
- EV (企业价值) = 市值 + 净债务 = 市值 + 总负债 - 现金及现金等价物
- EBITDA (息税折旧摊销前利润) = 净利润 + 所得税 + 利息费用 + 折旧 + 摊销
- 选股逻辑:EV/EBITDA 数值越低,代表企业被低估的可能性越大。本策略将在沪深300成分股中,选取 EV/EBITDA 最小的股票进行持仓。
策略实现细节
- 股票池:沪深300成分股。
- 过滤条件:剔除停牌、ST股、EBITDA为负(亏损或经营状况极差)的股票。
- 调仓频率:按月调仓(每月第一个交易日)。
- 资金管理:等权重买入排名前 20 的股票。
策略代码
# -*- coding: utf-8 -*-
from jqdata import *
def initialize(context):
"""
初始化函数,设定基准、手续费、滑点等
"""
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
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')
# 设定全局变量:持仓数量
g.stock_num = 20
# 设定按月运行,每月第一个交易日09:30运行
run_monthly(rebalance, monthday=1, time='09:30')
def rebalance(context):
"""
调仓函数
"""
# 1. 获取股票池:沪深300
stocks = get_index_stocks('000300.XSHG')
# 2. 过滤 ST 和 停牌 的股票
current_data = get_current_data()
stocks = [s for s in stocks if not current_data[s].is_st and not current_data[s].paused]
# 3. 获取财务数据
# 我们需要计算 EV 和 EBITDA
# EV = 市值 + 总负债 - 货币资金(现金及现金等价物)
# EBITDA = 净利润 + 所得税 + 财务费用(近似利息) + 折旧 + 摊销
q = query(
valuation.code,
valuation.market_cap, # 总市值 (单位:亿元)
balance.total_liability, # 总负债 (单位:元)
balance.cash_equivalents, # 货币资金/现金及现金等价物 (单位:元)
income.net_profit, # 净利润 (单位:元)
income.income_tax_expense, # 所得税费用 (单位:元)
income.financial_expense, # 财务费用 (单位:元)
cash_flow.depreciation_of_fixed_assets, # 固定资产折旧 (单位:元)
cash_flow.amortization_of_intangible_assets, # 无形资产摊销 (单位:元)
cash_flow.amortization_of_long_term_deferred_expenses # 长期待摊费用摊销 (单位:元)
).filter(
valuation.code.in_(stocks)
)
df = get_fundamentals(q)
# 数据清洗:填充空值为0
df.fillna(0, inplace=True)
# 4. 计算指标
# 注意:valuation.market_cap 单位是亿元,需要转换为元 (* 10^8)
df['EV'] = df['market_cap'] * 1e8 + df['total_liability'] - df['cash_equivalents']
df['EBITDA'] = df['net_profit'] + df['income_tax_expense'] + df['financial_expense'] + \
df['depreciation_of_fixed_assets'] + df['amortization_of_intangible_assets'] + \
df['amortization_of_long_term_deferred_expenses']
# 过滤掉 EBITDA <= 0 的公司(避免估值无意义或亏损严重)
df = df[df['EBITDA'] > 0]
# 计算 EV/EBITDA
df['EV_EBITDA'] = df['EV'] / df['EBITDA']
# 5. 选股
# 按照 EV/EBITDA 从小到大排序
df = df.sort_values('EV_EBITDA', ascending=True)
# 取前 g.stock_num 只股票
target_list = df['code'][:g.stock_num].tolist()
# 6. 执行交易
# 获取当前持仓
current_holdings = list(context.portfolio.positions.keys())
# 卖出不在目标列表中的股票
for stock in current_holdings:
if stock not in target_list:
order_target_value(stock, 0)
# 买入目标列表中的股票
if len(target_list) > 0:
# 将资金等分
# 注意:这里使用 total_value (总资产) 来计算每只股票的目标持仓,保持仓位平衡
target_value = context.portfolio.total_value / len(target_list)
for stock in target_list:
order_target_value(stock, target_value)
log.info("调仓完成,当前持有股票数量: %d" % len(target_list))
常见问题 Q&A
Q1: 为什么使用 get_fundamentals 而不是直接调用因子库?
A: 虽然聚宽因子库(jqfactor)提供了现成的因子,但使用 get_fundamentals 从原始财务报表计算可以让策略逻辑更加透明,且方便您根据需要微调公式(例如调整 EV 的计算方式,是否包含少数股东权益等)。此外,get_fundamentals 在回测中会自动处理“未来函数”问题,确保只获取当时已发布的财报数据。
Q2: 代码中 valuation.market_cap * 1e8 是什么意思?
A: 聚宽数据库中,valuation 表里的 market_cap(市值)单位是亿元,而 balance 和 income 表里的财务数据单位是元。为了保证公式 EV = 市值 + 负债 - 现金 的量级一致,必须将市值乘以 $10^8$ 转换为元。
Q3: 为什么过滤掉 EBITDA <= 0 的股票?
A: 当 EBITDA 为负时,说明企业经营现金流压力较大或处于亏损状态,此时计算出的 EV/EBITDA 比率为负数,在数学排序上虽然“小”,但并不代表“低估”,而是代表“差”。因此在价值选股中通常会剔除这类股票。
Q4: 策略可以如何优化?
A:
- 行业中性化:EV/EBITDA 在重资产行业(如公用事业、制造)和轻资产行业(如科技、服务)之间差异很大。可以考虑在每个行业内分别选股,而不是全市场混排。
- 增加质量因子:低估值可能意味着“价值陷阱”。可以结合 ROE(净资产收益率)或 盈利增长率 来筛选既便宜又好的公司。
- 止损机制:加入个股止损或大盘风控(如大盘跌破20日均线空仓)。