问题描述
解决方案
这是一个基于乔尔·格林布拉特(Joel Greenblatt)在《股市稳赚》(The Little Book That Beats the Market)中提出的**神奇公式(Magic Formula)**选股策略的 Python 实现代码。
策略原理
神奇公式的核心思想是**“用便宜的价格买入好公司”**。它通过两个指标对股票进行排名:
- 资本回报率 (ROIC):衡量公司的盈利能力(好公司)。公式中通常使用
EBIT / (净流动资本 + 净固定资产),在 A 股回测中,我们可以直接使用聚宽提供的indicator.roic(投资资本回报率) 作为替代。 - 收益率 (Earnings Yield):衡量公司的估值(便宜)。公式中通常使用
EBIT / 企业价值 (EV),在 A 股回测中,我们可以使用市盈率的倒数(即1 / PE)或者直接使用valuation.pe_ratio(市盈率) 进行反向排序(PE 越低越好)。
选股逻辑:
- 选取 A 股股票池,剔除 ST、停牌、退市股票。
- 获取所有股票的 ROIC 和 PE 数据。
- 对 ROIC 从高到低排名。
- 对 PE 从低到高排名。
- 将两个排名相加得到总分,总分越低越好。
- 买入总分最低的前 N 只股票(例如 30 只)。
- 定期(例如每月或每季度)调仓。
策略代码
# -*- coding: utf-8 -*-
import pandas as pd
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')
# 设定股票交易手续费:买入万三,卖出万三加千一印花税,最低5元
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# 设定全局变量
g.stock_num = 30 # 持仓股票数量
# 设定定期运行:每月第一个交易日进行调仓
run_monthly(rebalance, 1, time='09:30')
def rebalance(context):
"""
调仓函数
"""
# 1. 获取股票池
# 获取所有A股
all_stocks = list(get_all_securities(['stock']).index)
# 过滤掉 ST, *ST, 退市, 停牌的股票
current_data = get_current_data()
all_stocks = [stock for stock in all_stocks
if not current_data[stock].is_st
and not current_data[stock].paused
and '退' not in current_data[stock].name]
# 2. 获取财务数据 (ROIC 和 PE)
# 查询 valuation.pe_ratio (市盈率) 和 indicator.roic (投资资本回报率)
# 过滤掉 PE < 0 (亏损) 的股票
q = query(
valuation.code,
valuation.pe_ratio,
indicator.roic
).filter(
valuation.code.in_(all_stocks),
valuation.pe_ratio > 0
)
df = get_fundamentals(q)
if df is None or len(df) == 0:
log.warn("未获取到财务数据,跳过本次调仓")
return
# 3. 计算排名
# 对 PE 进行升序排名 (PE越低越好,排名越靠前)
df['rank_pe'] = df['pe_ratio'].rank(ascending=True)
# 对 ROIC 进行降序排名 (ROIC越高越好,排名越靠前)
df['rank_roic'] = df['roic'].rank(ascending=False)
# 计算总分 (排名之和)
df['total_score'] = df['rank_pe'] + df['rank_roic']
# 4. 选股
# 按总分升序排列,取前 g.stock_num 只股票
df = df.sort_values('total_score', ascending=True)
target_list = df.head(g.stock_num)['code'].tolist()
# 5. 执行交易
trade(context, target_list)
def trade(context, target_list):
"""
交易执行函数
"""
# 获取当前持仓
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:
# 等权重分配资金
# 注意:这里简单处理,用总资产除以目标数量。实际交易中可能需要预留现金或考虑卖出后的可用资金。
position_value = context.portfolio.total_value / len(target_list)
for stock in target_list:
order_target_value(stock, position_value)
def before_trading_start(context):
"""
盘前运行函数
"""
pass
def after_trading_end(context):
"""
盘后运行函数
"""
pass
代码关键点解析
-
数据获取 (
get_fundamentals):- 我们使用了
valuation.pe_ratio代表估值因子。神奇公式原版使用的是 EBIT/EV,但在 A 股量化中,PE 是最接近且数据质量最稳定的替代指标。 - 我们使用了
indicator.roic代表质量因子。这直接对应了资本回报率。 - 我们在查询时过滤了
pe_ratio > 0,排除了亏损企业,这符合神奇公式寻找“好公司”的初衷。
- 我们使用了
-
排名逻辑 (
rank):df['pe_ratio'].rank(ascending=True): PE 越小,排名数值越小(例如 PE 最低的排名为 1)。df['roic'].rank(ascending=False): ROIC 越大,排名数值越小(例如 ROIC 最高的排名为 1)。total_score: 两者相加,分数越低,代表综合性价比越高。
-
调仓频率:
- 使用了
run_monthly设置为每月调仓。虽然格林布拉特建议一年调仓一次,但在 A 股市场波动较大的环境下,月度或季度调仓通常能获得更好的平滑收益。
- 使用了
-
过滤逻辑:
- 代码中显式过滤了 ST 股和停牌股,这是实盘和回测中必须做的风控处理,防止买入无法交易或风险极大的标的。
常见问题 Q&A
Q: 为什么不完全按照原书使用 EBIT/EV 和 EBIT/(净流动资本+净固定资产)?
A: 在聚宽等量化平台上,直接获取准确的 EV (企业价值) 和净流动资本的历史数据比较复杂,且容易受到财报公布时间滞后的影响。使用 PE 和 ROIC 是业界公认的在 A 股行之有效的简化替代方案,逻辑一致且数据更健壮。
Q: 这个策略适合什么资金规模?
A: 神奇公式通常选取的是基本面较好的股票,流动性尚可。代码中设置了持仓 30 只股票,适合中小资金规模。如果资金量非常大,建议增加持仓数量或增加流动性过滤条件。
Q: 回测时应该注意什么?
A: 建议开启 set_option('use_real_price', True) 以使用真实价格复权。此外,财务数据(get_fundamentals)在回测中会自动匹配当时能看到的最新财报,避免了未来函数,但在分析结果时要注意财报发布的季节性效应。