当前位置: 首页 > ds >正文

量化交易 - 上证50利用动态PE增强模型 - python

思路

我是这么想的, 价格围绕价值波动, 而股票的价值参考指标是PE市盈率, 那么我在PE低的时候买入,在高的时候卖出,看看是否可以进行赚钱.

(参考了另一博主的, 但他的pe是固定值, 我这里换成动态获取)

计算动态PE

上一个帖子我们已经知道如何计算指数的PE了, 现在我们动态来获取上证50的PE

2009-04-30才有数据; 所以从2011开始测试, 最好有2年历史数据

from jqdata import *
import pandas as pd
import datetime as dtdef get_index_pe(index_code, date):"""根据指数成分股权重计算整体市盈率 PE"""# 成分股权重(%)weights = get_index_weights(index_id=index_code, date=date)if weights.empty:return Nonestocks = list(weights.index)# 获取市值和净利润q = query(valuation.code,valuation.market_cap,   # 总市值(亿)income.net_profit       # 净利润(元)).filter(valuation.code.in_(stocks))df = get_fundamentals(q, date=date)if df.empty:return None# 合并权重df = df.merge(weights[['weight']], left_on='code', right_index=True, how='inner')# 市值、利润加权df['market_cap'] = df['market_cap'] * 1e8  # 转元df['weight'] = df['weight'] / 100          # 百分比转小数total_mv = (df['market_cap'] * df['weight']).sum()total_profit = (df['net_profit'] * df['weight']).sum()if total_profit <= 0:return Nonereturn total_mv / total_profitdef get_monthly_pe(index_code, start_date, end_date):"""每月1号计算指数PE"""dates = pd.date_range(start_date, end_date, freq="MS")  # 每月首日pe_list = []for d in dates:# 获取前一天的,防止出现未来函数d_str = (d - dt.timedelta(days=1)).strftime("%Y-%m-%d")try:pe = get_index_pe(index_code, d_str)if pe is not None:pe_list.append((d_str, pe))except:continuereturn pd.DataFrame(pe_list, columns=['date','pe']).set_index('date')# ================== 示例 ==================
index_code = "000016.XSHG"  # 上证50
start_date = "2011-01-01"   # 2009-04-30才有数据; 所以从2011开始测试, 最好有3年历史数据
end_date = dt.date.today().strftime("%Y-%m-%d")pe_df = get_monthly_pe(index_code, start_date, end_date)
pe_df

动态PE+买卖逻辑

我们采取最近2年的动态PE阈值, 15%为低点买入, 85%为高点卖出

from jqdata import *
import pandas as pd
import numpy as np
import datetime as dt
import matplotlib.pyplot as plt############################################
# 1) 按成分股权重计算当期指数PE
############################################
def get_index_pe(index_code, date):"""根据指数成分股权重计算整体市盈率 PE = (权重加权总市值) / (权重加权净利润):param index_code: 指数代码,例如 '000016.XSHG' (上证50):param date:      日期字符串 'YYYY-MM-DD'(将取该日可用的最新基本面):return:          PE (float) 或 None"""# 成分股权重(%)weights = get_index_weights(index_id=index_code, date=date)if weights is None or len(weights)==0:return Noneweights = weights.copy()weights.index.name = 'code'weights = weights[['weight']]stocks = list(weights.index)# 获取市值与净利润(注意单位)q = query(valuation.code,valuation.market_cap,   # 总市值(亿)income.net_profit       # 净利润(元)).filter(valuation.code.in_(stocks))df = get_fundamentals(q, date=date)if df is None or len(df)==0:return None# 合并权重df = df.merge(weights, on='code', how='inner')# 单位换算 + 权重转小数df['market_cap'] = df['market_cap'] * 1e8   # 亿元 -> 元df['weight']     = df['weight'] / 100.0# 按权重加总total_mv     = (df['market_cap'] * df['weight']).sum()total_profit = (df['net_profit'] * df['weight']).sum()if pd.isna(total_profit) or total_profit <= 0:return Nonereturn float(total_mv / total_profit)def get_monthly_pe(index_code, start_date, end_date):"""每月1号(往前推1天,避免“未来函数”)计算指数PE返回:按日期索引的 DataFrame: ['pe']"""dates = pd.date_range(start_date, end_date, freq="MS")  # 每月首日rows = []for d in dates:d_str = (d - dt.timedelta(days=1)).strftime("%Y-%m-%d")  # 前一天try:pe = get_index_pe(index_code, d_str)if pe is not None and np.isfinite(pe):rows.append((d_str, pe))except Exception as e:# 可按需打印日志# print("PE计算失败:", d_str, e)continueif not rows:return pd.DataFrame(columns=['pe'])return pd.DataFrame(rows, columns=['date','pe']).set_index('date').sort_index()############################################
# 2) 工具函数:获取指数日线收盘价
############################################
def get_index_close_series(index_code, start_date, end_date):"""返回指数的日频收盘价 Series(索引为交易日)"""px = get_price(index_code, start_date=start_date, end_date=end_date,frequency='daily', fields=['close'], panel=False, skip_paused=True, fq='pre')px = px[['close']].rename(columns={'close':'close'}).dropna()return px['close']############################################
# 3) 生成策略信号(24个月滚动分位:15/85)
############################################
def build_signals_from_pe(pe_df, lookback_months=24, low_pct=15, high_pct=85):"""输入:月频 PE(index: 月份对应的观测日期(取每月1号前1日))逻辑:- 计算滚动窗口(近24个月)的15/85分位- 若当月PE <= p15: 设定目标持仓=1(满仓)- 若当月PE >= p85: 设定目标持仓=0(空仓)- 否则:保持上期目标持仓不变输出:月频 DataFrame ['pe','p15','p85','pos']"""df = pe_df.copy()df.index = pd.to_datetime(df.index)df = df.sort_index()df['p15'] = df['pe'].rolling(lookback_months, min_periods=max(12, lookback_months//3))\.apply(lambda x: np.nanpercentile(x, low_pct), raw=True)df['p85'] = df['pe'].rolling(lookback_months, min_periods=max(12, lookback_months//3))\.apply(lambda x: np.nanpercentile(x, high_pct), raw=True)# 生成目标持仓(0或1)pos = []last_pos = 0for dt_i, row in df.iterrows():pe   = row['pe']p15  = row['p15']p85  = row['p85']new_pos = last_posif np.isfinite(pe) and np.isfinite(p15) and np.isfinite(p85):if pe <= p15:new_pos = 1elif pe >= p85:new_pos = 0pos.append(new_pos)last_pos = new_posdf['pos'] = posreturn df[['pe','p15','p85','pos']]############################################
# 4) 将月频信号映射到日频,进行回测
############################################
def backtest_long_flat(index_code, start_date, end_date,pe_month_df, cost_per_trade=0.0005):""":param index_code: 指数代码:param start_date, end_date: 回测区间:param pe_month_df: 月频包含 ['pos'] 的 DataFrame(信号):param cost_per_trade: 单边交易成本(默认0.0005=5bp),换仓日收取一次:return: 回测结果 DataFrame(含策略和基准累计净值)、绩效字典"""# 指数日线close = get_index_close_series(index_code, start_date, end_date)ret = close.pct_change().fillna(0.0)ret.name = 'idx_ret'# 将月频信号对齐到日频,并“次一交易日生效”daily_pos = pe_month_df['pos'].copy()daily_pos.index = pd.to_datetime(daily_pos.index)# 用日频交易日日历重采样并前向填充pos_daily = daily_pos.reindex(ret.index, method='ffill').fillna(0)# 为避免未来函数:当日信号次一交易日生效pos_eff = pos_daily.shift(1).fillna(0)# 策略日收益strat_ret = pos_eff * ret# 交易成本:当“目标生效持仓”发生变化的当天扣一次单边成本# 注意:这里的成本是对净值直接扣减,不是对收益率做乘法trade_flag = (pos_eff != pos_eff.shift(1)).astype(int).fillna(0)# 忽略首日的“建仓”成本也可按需选择;这里默认计入成本strat_ret = strat_ret - trade_flag * cost_per_trade# 累计净值bench_nav = (1 + ret).cumprod()strat_nav = (1 + strat_ret).cumprod()out = pd.DataFrame({'bench_nav': bench_nav,'strat_nav': strat_nav,'position': pos_eff})# 绩效指标def max_drawdown(nav):cummax = nav.cummax()dd = nav / cummax - 1return dd.min()ann_factor = 252strat_ann_ret   = (strat_nav.iloc[-1])**(ann_factor/len(out)) - 1bench_ann_ret   = (bench_nav.iloc[-1])**(ann_factor/len(out)) - 1strat_ann_vol   = strat_ret.std() * np.sqrt(ann_factor)bench_ann_vol   = ret.std() * np.sqrt(ann_factor)strat_sharpe    = strat_ann_ret / strat_ann_vol if strat_ann_vol>0 else np.nanbench_sharpe    = bench_ann_ret / bench_ann_vol if bench_ann_vol>0 else np.nanstrat_mdd       = max_drawdown(strat_nav)bench_mdd       = max_drawdown(bench_nav)stats = {'策略年化收益': strat_ann_ret,'基准年化收益': bench_ann_ret,'策略年化波动': strat_ann_vol,'基准年化波动': bench_ann_vol,'策略夏普比': strat_sharpe,'基准夏普比': bench_sharpe,'策略最大回撤': strat_mdd,'基准最大回撤': bench_mdd,'交易次数(含首日建仓)': int(trade_flag.sum())}return out, stats############################################
# 5) 统一跑一遍 + 画图
############################################
def run_pipeline(index_code="000016.XSHG",start_date="2011-01-01",end_date=None,lookback_months=36, low_pct=15, high_pct=85,cost_per_trade=0.0005,show_pe_panel=False):if end_date is None:end_date = dt.date.today().strftime("%Y-%m-%d")# 计算月频PEpe_df = get_monthly_pe(index_code, start_date, end_date)if pe_df is None or pe_df.empty:raise ValueError("未能计算到任何PE数据,请检查时间区间或基础数据可用性。")# 生成月频信号sig_df = build_signals_from_pe(pe_df, lookback_months, low_pct, high_pct)# 回测result_df, stats = backtest_long_flat(index_code, start_date, end_date, sig_df, cost_per_trade)# 打印绩效print("========== 绩效指标 ==========")for k, v in stats.items():if '回撤' in k:print(f"{k}: {v:.2%}")elif '次数' in k:print(f"{k}: {v}")else:print(f"{k}: {v:.2%}")# 画图:策略 vs 上证50(基准)plt.figure(figsize=(10, 5))plt.plot(result_df.index, result_df['bench_nav'], label='上证50-买入持有')plt.plot(result_df.index, result_df['strat_nav'], label='策略净值(PE 15/85%)')plt.title(f"{index_code} 策略 vs 基准 累计净值")plt.legend()plt.grid(True)plt.tight_layout()plt.show()# 可选:展示PE与分位及持仓(辅助面板)if show_pe_panel:# 将月频对齐到日频以便更平滑显示(仅用于可视化)pe_daily = sig_df[['pe','p15','p85','pos']].reindex(result_df.index, method='ffill')fig, ax1 = plt.subplots(figsize=(10, 4))ax1.plot(pe_daily.index, pe_daily['pe'], label='PE')ax1.plot(pe_daily.index, pe_daily['p15'], label='P15', linestyle='--')ax1.plot(pe_daily.index, pe_daily['p85'], label='P85', linestyle='--')ax1.set_ylabel('PE')ax1.grid(True)ax2 = ax1.twinx()ax2.plot(pe_daily.index, pe_daily['pos'], label='仓位(右)', alpha=0.3)ax2.set_ylim(-0.05, 1.05)ax2.set_ylabel('仓位')lines, labels = ax1.get_legend_handles_labels()lines2, labels2 = ax2.get_legend_handles_labels()ax1.legend(lines+lines2, labels+labels2, loc='upper left')plt.title("月频PE与分位及仓位")plt.tight_layout()plt.show()return pe_df, sig_df, result_df, stats# ================== 主函数运行 ==================index_code = "000016.XSHG"  # 上证50
start_date = "2011-01-01"   # 建议>=2011,保证滚动窗口有足够历史
end_date   = dt.date.today().strftime("%Y-%m-%d")# 最近2年(24个月)动态PE阈值,15%低买、85%高卖
pe_df, sig_df, result_df, stats = run_pipeline(index_code=index_code,start_date=start_date,end_date=end_date,lookback_months=24,low_pct=15,high_pct=85,cost_per_trade=0.0005,   # 可按券商费率调整show_pe_panel=True       # 如需看PE与仓位面板改为True
)

显示结果如下:

========== 绩效指标 ==========
策略年化收益: 5.02%
基准年化收益: 2.64%
策略年化波动: 14.02%
基准年化波动: 21.68%
策略夏普比: 35.77%
基准夏普比: 12.18%
策略最大回撤: -32.31%
基准最大回撤: -45.41%
交易次数(含首日建仓): 13

更多开源代码  https://github.com/JizhiXiang/Quant-Strategy

参考:

https://www.joinquant.com/view/community/detail/929041711bbd4ae14758cb02683b3938

手动计算指数PE:

https://www.joinquant.com/view/community/detail/672f025e00eb79ad69458f8ae0dc9f07

http://www.xdnf.cn/news/18582.html

相关文章:

  • React 学习笔记1 组件、State
  • 线性回归学习笔记
  • JAVA-15 (2025.08.20学习记录)
  • 集成电路学习:什么是Template Matching模版匹配
  • week3-[循环嵌套]好数
  • 基于Python与Tkinter开发的微博多功能自动化助手
  • Android焦点窗口变化导致遥控键值监听失效问题分析
  • # 重磅发布 | onecode 3.0.1 Base 源码正式开源:AI赋能的企业级开发框架
  • XXL-Job REST API 工具类完全解析:简化分布式任务调度集成
  • (第二十期上)HTML 超链接标签 a
  • 【python与生活】如何从视频中提取关键帧?
  • FPGA DP1.4 With DSC解决方案
  • 【华为OD-C卷-019 对称字符串 100分(python、java、c++、js、c)】
  • Vitest 测试框架完全指南 – 极速单元测试解决方案
  • C++ 常见的排序算法详解
  • AI 产业落地:从 “实验室神话” 到 “车间烟火气” 的跨越
  • Spring Cloud Netflix学习笔记06-Zuul
  • 机器学习中的集成算法与 k 均值聚类算法概述
  • uniapp跨域怎么解决
  • Go 并发编程-channel
  • 详解开源关键信息提取方案PP-ChatOCRv4的设计与实现
  • AI客服系统架构与实现:大模型、知识库与多轮对话的最佳实践
  • Android为ijkplayer设置音频发音类型usage
  • 【C2000常见问题】JTAG仿真器类型和JTAG Debug定位方法
  • 机器学习核心算法笔记:集成学习与聚类算法
  • springboot人事管理系统源码和论文
  • c#语言的学习【02,函数重载】
  • GPT5 / 深度研究功能 无法触发
  • 网络流量分析——基础知识(二)(Tcpdump 基础知识)
  • HTTP/2 性能提升的核心原因