问题描述
解决方案
这是一个基于**本杰明·格雷厄姆(Benjamin Graham)**经典价值投资理论设计的选股策略。
策略核心逻辑:格雷厄姆数字 (Graham Number)
格雷厄姆数字是衡量股票每股内在价值的指标。根据格雷厄姆的理论,防御型投资者应寻找市盈率(PE)低于15且市净率(PB)低于1.5的股票。
公式推导如下:
$$15 \times 1.5 = 22.5$$
$$PE \times PB < 22.5$$
由此得出格雷厄姆数字公式:
$$ \text{Graham Number} = \sqrt{22.5 \times \text{每股收益(EPS)} \times \text{每股净资产(BVPS)}} $$
策略逻辑:
- 选股范围:全A股(剔除ST、停牌、次新股)。
- 核心过滤:
- 市盈率 (PE) > 0 (保证公司盈利)。
- PE $\times$ PB < 22.5 (满足格雷厄姆的安全边际)。
- 排序因子:按照 $PE \times PB$ 从小到大排序(估值越低越好),或者按照 (格雷厄姆数字 / 当前股价) 从大到小排序。
- 调仓频率:按月调仓。
- 资金分配:等权重买入排名前 N 的股票。
策略代码实现
# -*- coding: utf-8 -*-
from jqdata import *
import pandas as pd
import numpy as np
def initialize(context):
"""
初始化函数
"""
# 设定基准为沪深300
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# --- 策略参数设置 ---
g.stock_num = 10 # 持仓股票数量
g.graham_limit = 22.5 # 格雷厄姆乘数上限 (PE * PB < 22.5)
# 设置交易成本(股票佣金万三,印花税千一)
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# 按月运行,每月第一个交易日进行调仓
run_monthly(trade, monthday=1, time='09:30')
def trade(context):
"""
交易主函数
"""
# 1. 获取初始股票池
# 选取全市场股票
all_stocks = list(get_all_securities(['stock']).index)
# 2. 过滤股票
# 剔除停牌、ST、退市、上市不满60天的次新股
check_out_list = filter_basic_stock(all_stocks, context)
# 3. 获取财务数据并计算格雷厄姆指标
# 查询 PE(市盈率) 和 PB(市净率)
# 这里使用 valuation.pe_ratio (PE-TTM) 和 valuation.pb_ratio
q = query(
valuation.code,
valuation.pe_ratio,
valuation.pb_ratio
).filter(
valuation.code.in_(check_out_list),
valuation.pe_ratio > 0, # 剔除亏损股
valuation.pb_ratio > 0
)
df = get_fundamentals(q, date=context.previous_date)
if df is None or len(df) == 0:
log.warn("未获取到财务数据,跳过本次调仓")
return
# 4. 计算格雷厄姆因子 (Graham Factor)
# 逻辑:PE * PB < 22.5 等价于 股价 < 格雷厄姆数字
# 我们计算 PE * PB 值,值越小代表安全边际越高
df['graham_score'] = df['pe_ratio'] * df['pb_ratio']
# 筛选满足格雷厄姆条件的股票 (PE * PB < 22.5)
df = df[df['graham_score'] < g.graham_limit]
# 按照得分从小到大排序 (越小越便宜)
df = df.sort_values(by='graham_score', ascending=True)
# 取前 N 只股票作为目标持仓
target_list = list(df['code'][:g.stock_num])
log.info(f"本月选股数量: {len(target_list)}")
log.info(f"选股列表: {target_list}")
# 5. 执行交易
do_rebalance(context, target_list)
def do_rebalance(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:
# 等权重分配资金
# 注意:这里使用 total_value 可能会包含停牌股票的市值,更严谨的做法是计算可用资金
# 简单起见,这里使用总资产/目标数量
position_value = context.portfolio.total_value / len(target_list)
for stock in target_list:
order_target_value(stock, position_value)
def filter_basic_stock(stock_list, context):
"""
基础过滤器:剔除停牌、ST、次新股
"""
current_data = get_current_data()
return_list = []
for stock in stock_list:
# 剔除停牌
if current_data[stock].paused:
continue
# 剔除ST
if current_data[stock].is_st:
continue
# 剔除上市不满60天的次新股
# 获取股票上市时间
start_date = get_security_info(stock).start_date
days_listed = (context.current_dt.date() - start_date).days
if days_listed < 60:
continue
# 剔除涨跌停(无法买入或卖出,虽然回测引擎可能撮合,但实盘需剔除)
if current_data[stock].last_price >= current_data[stock].high_limit:
continue
if current_data[stock].last_price <= current_data[stock].low_limit:
continue
return_list.append(stock)
return return_list
策略说明与 Q&A
1. 为什么使用 PE * PB 而不是直接计算格雷厄姆数字?
数学上是等价的。
- 格雷厄姆数字公式:$V = \sqrt{22.5 \times EPS \times BVPS}$
- 买入条件:$Price < V$
- 两边平方:$Price^2 < 22.5 \times EPS \times BVPS$
- 移项:$\frac{Price}{EPS} \times \frac{Price}{BVPS} < 22.5$
- 即:$PE \times PB < 22.5$
直接使用聚宽提供的 PE 和 PB 数据计算更加方便且数据源统一。
2. 策略的风险点在哪里?
- 价值陷阱:低估值的股票可能基本面正在恶化(如夕阳行业),导致股价持续下跌,虽然 PE/PB 很低,但未来盈利可能消失。
- 市场风格:在成长股牛市中,深度价值策略往往跑输大盘。
- 持仓分散度:代码中设置了持仓10只,如果资金量大建议增加到20-30只以分散个股暴雷风险。
3. 如何优化这个策略?
- 增加盈利能力过滤:例如要求 ROE > 8% 或 10%,避免买入资产质量太差的公司。
- 增加现金流过滤:要求经营性现金流为正,避免纸面富贵。
- 行业中性化:银行和地产行业通常 PE/PB 极低,容易导致策略全仓银行地产。可以限制单一行业的持仓比例,或者在各行业内分别选股。
4. 回测建议
建议回测时间拉长(例如5年以上),因为价值回归通常需要较长的周期。
常见问题 (Q&A)
Q: 为什么 get_fundamentals 只能获取昨天的数据?
A: 为了避免未来函数。在回测中,当天的财务数据(如市值、PE)通常在收盘后才能确定,因此在盘中交易时只能使用前一个交易日的数据。
Q: 代码中的 set_option('use_real_price', True) 是什么意思?
A: 这开启了动态复权模式。这意味着回测引擎会使用真实的股票历史价格进行撮合,并在分红送股时自动调整持仓数量。这是最接近实盘的模拟方式。
Q: 如果选出的股票少于10只怎么办?
A: 代码逻辑是 df[df['graham_score'] < g.graham_limit]。如果市场整体高估,满足条件的股票可能很少。在这种情况下,策略会自动只买入满足条件的股票,剩余资金会保留为现金。这是符合格雷厄姆“宁缺毋滥”的投资原则的。