问题描述
解决方案
这是一个基于**本杰明·格雷厄姆(Benjamin Graham)**价值投资理念的PTrade量化选股策略。
策略核心理念
格雷厄姆被称为“价值投资之父”,其核心思想是寻找市场价格低于其内在价值的股票(即寻找“安全边际”)。
本策略选取了格雷厄姆最经典的几个量化指标进行筛选(防御型投资者标准):
- 低市盈率 (PE):PE < 15(或适度放宽至20),代表回本周期短。
- 低市净率 (PB):PB < 1.5,代表资产折价。
- 格雷厄姆指数 (PE * PB):PE * PB < 22.5。这是格雷厄姆的一个著名推论。
- 流动性 (流动比率):流动资产 / 流动负债 > 1.5,确保公司短期偿债能力强。
- 市值过滤:剔除极小市值的股票,保证流动性。
策略实现逻辑
- 股票池:选用中证800(000906.XBHS)作为基础池,覆盖大盘和中盘蓝筹。
- 调仓频率:按月调仓(每20个交易日)。
- 资金管理:等权重买入筛选出的前N只股票。
PTrade 策略代码
import pandas as pd
import numpy as np
def initialize(context):
"""
初始化函数,设置策略参数
"""
# 设置回测基准:沪深300
set_benchmark('000300.SS')
# 设置佣金:万三
set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
# 设置滑点:0.2%
set_slippage(slippage=0.002)
# 策略参数设置
g.stock_pool = '000906.XBHS' # 基础股票池:中证800
g.max_hold_stocks = 10 # 最大持仓数量
g.rebalance_days = 20 # 调仓周期(天)
g.days_counter = 0 # 计数器
# 格雷厄姆筛选阈值
g.pe_limit = 20.0 # 市盈率上限
g.pb_limit = 1.5 # 市净率上限
g.graham_number_limit = 22.5 # PE * PB 上限
g.current_ratio_limit = 1.5 # 流动比率下限 (流动资产/流动负债)
def before_trading_start(context, data):
"""
盘前处理
"""
pass
def handle_data(context, data):
"""
盘中处理,每日运行
"""
# 检查是否是调仓日
if g.days_counter % g.rebalance_days == 0:
rebalance(context)
g.days_counter += 1
def rebalance(context):
"""
调仓核心逻辑
"""
log.info("开始进行格雷厄姆价值选股调仓...")
# 1. 获取基础股票池
check_stocks = get_index_stocks(g.stock_pool)
if not check_stocks:
log.warning("获取股票池失败")
return
# 2. 获取财务数据
# 我们需要:市盈率(PE), 市净率(PB), 流动资产, 流动负债
# 注意:PTrade对单次查询量有限制,如果股票池很大,建议分批查询或只查核心指标
# 查询估值表 (valuation)
df_val = get_fundamentals(
check_stocks,
'valuation',
fields=['pe_ttm', 'pb', 'market_cap']
)
# 查询资产负债表 (balance_statement) 用于计算流动比率
# 注意:balance_statement 数据量较大,这里只取需要的字段
df_bal = get_fundamentals(
check_stocks,
'balance_statement',
fields=['total_current_assets', 'total_current_liability']
)
# 3. 数据清洗与合并
if df_val is None or df_bal is None:
log.warning("财务数据获取失败")
return
# 将索引转为列,方便合并 (PTrade返回的df索引通常是股票代码)
df_val['code'] = df_val.index
df_bal['code'] = df_bal.index
# 合并数据
df = pd.merge(df_val, df_bal, on='code', how='inner')
# 剔除停牌、ST等(简单剔除PE/PB为0或负值的异常数据)
df = df[df['pe_ttm'] > 0]
df = df[df['pb'] > 0]
# 4. 计算格雷厄姆指标
# 计算流动比率 = 流动资产 / 流动负债
df['current_ratio'] = df['total_current_assets'] / df['total_current_liability']
# 计算格雷厄姆乘积 = PE * PB
df['graham_multiplier'] = df['pe_ttm'] * df['pb']
# 5. 执行筛选条件
# 条件1: PE < 20
# 条件2: PB < 1.5
# 条件3: PE * PB < 22.5
# 条件4: 流动比率 > 1.5
condition = (
(df['pe_ttm'] < g.pe_limit) &
(df['pb'] < g.pb_limit) &
(df['graham_multiplier'] < g.graham_number_limit) &
(df['current_ratio'] > g.current_ratio_limit)
)
selected_df = df[condition]
# 6. 排序与截取
# 按照格雷厄姆乘积从小到大排序,越小越安全
selected_df = selected_df.sort_values(by='graham_multiplier', ascending=True)
# 取前N只
target_stocks = selected_df['code'].head(g.max_hold_stocks).tolist()
log.info("选出股票列表: %s" % target_stocks)
# 7. 执行交易
adjust_position(context, target_stocks)
def adjust_position(context, target_stocks):
"""
执行具体的买卖操作
"""
# 获取当前持仓
current_positions = list(context.portfolio.positions.keys())
# 卖出不在目标列表中的股票
for stock in current_positions:
if stock not in target_stocks:
# 检查是否停牌等状态,PTrade的order_target会自动处理,但最好加个日志
order_target_value(stock, 0)
log.info("卖出: %s" % stock)
# 如果没有选出股票,则空仓
if len(target_stocks) == 0:
return
# 买入目标股票
# 资金分配:等权重
position_count = len(target_stocks)
total_value = context.portfolio.portfolio_value
if position_count > 0:
value_per_stock = total_value / position_count
for stock in target_stocks:
order_target_value(stock, value_per_stock)
log.info("买入/调仓: %s, 目标金额: %s" % (stock, value_per_stock))
代码详解
-
initialize:- 设置了交易的基础环境(手续费、滑点)。
- 定义了格雷厄姆策略的关键阈值:
PE < 20,PB < 1.5,PE*PB < 22.5。 - 设置了调仓周期为20个交易日(约一个月)。
-
rebalance(核心逻辑):- 数据获取: 使用
get_fundamentals获取估值表(valuation)和资产负债表(balance_statement)。 - 指标计算:
- 流动比率 (Current Ratio): 这是一个非常重要的格雷厄姆指标,用于衡量企业的偿债能力和安全性。计算公式为
流动资产 / 流动负债。 - 格雷厄姆乘积:
PE * PB,这是格雷厄姆用来平衡成长性和资产价值的综合指标。
- 流动比率 (Current Ratio): 这是一个非常重要的格雷厄姆指标,用于衡量企业的偿债能力和安全性。计算公式为
- 筛选: 同时满足低估值(PE, PB)和高安全边际(流动比率)的股票。
- 排序: 按照
PE * PB从低到高排序,优先买入“最便宜”的股票。
- 数据获取: 使用
-
adjust_position:- 执行标准的等权重调仓。先卖出非目标持仓,再买入目标持仓。
注意事项
- 数据缺失处理: 代码中简单的使用了
inner join和>0过滤,实际运行时如果遇到财报空窗期,可能会导致选出的股票较少。 - Python版本: 代码完全兼容 Python 3.5 语法(不使用 f-string)。
- API调用: 严格遵循 PTrade 文档,使用
get_index_stocks和get_fundamentals。 - 生存偏差: 该策略使用的是
pe_ttm(滚动市盈率),在回测中表现较好,但在实际交易中需注意财报发布的滞后性(PTrade的get_fundamentals默认处理了未来函数问题,取的是当时已发布的数据)。
Q&A
Q: 为什么使用中证800而不是全A股?
A: 格雷厄姆策略偏好业绩稳定的大中型公司。全A股包含大量微盘股和壳资源,虽然数据上可能符合低PE/PB,但基本面风险极大(如财务造假)。中证800覆盖了主要的大中盘股,数据质量相对更好,且流动性更有保障。
Q: 如何让策略更激进?
A: 可以放宽筛选条件,例如将 g.pe_limit 提高到 25,或者移除 g.current_ratio_limit 的限制。也可以将排序规则改为按 pe_ttm 排序。
Q: 为什么不使用 get_snapshot 获取价格?
A: 在选股阶段,我们主要依赖财务数据。get_fundamentals 中的 valuation 表已经包含了基于最新收盘价计算的 PE 和 PB,这比手动获取快照再计算要高效且准确。