3 分钟快速生成代码
输入想法,AI 即刻生成可运行代码
这是一个基于格林布拉特(Greenblatt)“神奇公式”(Magic Formula)的PTrade量化选股策略。
神奇公式的核心思想是用便宜的价格买入好公司。
PE_TTM 从低到高排序(即盈利率从高到低)。import pandas as pd
import numpy as np
def initialize(context):
"""
初始化函数,设置策略参数
"""
# 设置持仓数量
g.stock_num = 30
# 设定按月运行,每月第一个交易日进行调仓
run_daily(context, monthly_rebalance, time='09:35')
# 记录上一次调仓的月份,用于控制调仓频率
g.last_month = 0
def monthly_rebalance(context):
"""
月度调仓主函数
"""
# 获取当前回测日期的月份
current_month = context.blotter.current_dt.month
# 如果当前月份与上一次调仓月份相同,则跳过(确保每月只调一次)
if current_month == g.last_month:
return
# 更新调仓月份
g.last_month = current_month
log.info("开始执行神奇公式选股调仓...")
# 1. 获取股票池:全A股
# 注意:get_Ashares返回的是list
all_stocks = get_Ashares()
# 2. 过滤掉ST、停牌、退市的股票
# filter_stock_by_status 默认过滤 ST, HALT, DELISTING
target_list = filter_stock_by_status(all_stocks)
if not target_list:
log.warning("当前市场无符合条件的股票")
return
# 3. 获取财务数据
# 我们需要获取 PE_TTM (用于计算盈利率) 和 ROIC (用于计算资本回报率)
# get_trading_day(-1) 获取前一个交易日,避免未来函数
yesterday = get_trading_day(-1)
date_str = yesterday.strftime('%Y%m%d')
# 分批获取数据以防超时或数据量过大,这里演示直接获取,如果股票过多建议分批
# 获取估值数据:PE_TTM
df_val = get_fundamentals(target_list, 'valuation', ['pe_ttm', 'total_value'], date=date_str)
# 获取盈利能力数据:ROIC
df_prof = get_fundamentals(target_list, 'profit_ability', ['roic'], date=date_str)
# 如果数据获取失败,则退出
if df_val is None or df_prof is None:
log.warning("财务数据获取失败")
return
# 4. 数据处理与合并
# 将两个DataFrame合并,索引通常是股票代码
df = pd.concat([df_val, df_prof], axis=1, join='inner')
# 剔除数据缺失的行
df = df.dropna()
# 剔除亏损股(PE_TTM <= 0)
df = df[df['pe_ttm'] > 0]
# 5. 计算排名
# 因子1:ROIC 从高到低排名 (ascending=False)
df['rank_roic'] = df['roic'].rank(ascending=False)
# 因子2:PE_TTM 从低到高排名 (ascending=True),即盈利率从高到低
df['rank_pe'] = df['pe_ttm'].rank(ascending=True)
# 综合排名:两个排名相加
df['total_score'] = df['rank_roic'] + df['rank_pe']
# 按综合排名从小到大排序
df = df.sort_values(by='total_score', ascending=True)
# 取前N只股票
buy_list = df.index[:g.stock_num].tolist()
log.info("本期选中股票数量: %d" % len(buy_list))
log.info("选股列表: %s" % buy_list)
# 6. 执行交易
adjust_position(context, buy_list)
def adjust_position(context, buy_list):
"""
交易执行函数
"""
# 获取当前持仓
current_positions = list(context.portfolio.positions.keys())
# 卖出不在买入列表中的股票
for stock in current_positions:
if stock not in buy_list:
order_target_value(stock, 0)
log.info("卖出: %s" % stock)
# 买入/调整在买入列表中的股票
# 采用等权重买入
if len(buy_list) > 0:
# 计算每只股票的目标市值 = 当前总资产 / 计划持仓数
target_value = context.portfolio.portfolio_value / len(buy_list)
for stock in buy_list:
order_target_value(stock, target_value)
# log.info("买入/调整: %s, 目标市值: %.2f" % (stock, target_value))
def handle_data(context, data):
"""
盘中运行函数,本策略为日线级选股,此处留空即可
"""
pass
initialize:
g.stock_num = 30。run_daily 配合 monthly_rebalance 函数中的月份判断逻辑,实现了月度调仓。这里设置在 09:35 执行,是为了避开开盘集合竞价的剧烈波动,且确保能取到最新的行情快照(虽然选股用的是昨日数据)。monthly_rebalance:
get_Ashares() 获取全市场代码,然后通过 filter_stock_by_status 剔除 ST、停牌和退市股票,这是实盘中非常重要的一步。get_fundamentals 获取财务数据。valuation 表中的 pe_ttm(滚动市盈率)用来代表估值。profit_ability 表中的 roic(投入资本回报率)用来代表公司质量。pe_ttm <= 0 的亏损公司,因为神奇公式通常只针对有盈利的企业。rank 函数。ascending=False。ascending=True。total_score。adjust_position:
buy_list 中,直接清仓(order_target_value(stock, 0))。order_target_value 自动调整仓位。如果已持有且上涨了,会卖出部分止盈;如果未持有,会买入。get_trading_day(-1) 获取前一交易日的数据进行选股,这是为了避免“未来函数”(即在回测时用到了当时还未发布的数据)。get_industry_stocks 获取特定行业代码并在 target_list 中剔除。Q: 为什么使用 PE_TTM 而不是 EBIT/EV?
A: 原版神奇公式使用 EBIT/EV(息税前利润/企业价值)。在A股数据中,直接获取准确的 EV(企业价值)需要计算市值+净债务,处理起来比较复杂且容易出错。PE_TTM(滚动市盈率)是盈利率(Earnings Yield)的倒数,在数学逻辑上是等价的(寻找低PE即寻找高盈利率),且 PTrade 直接提供了该字段,数据更稳定。
Q: 如何剔除银行和非银金融行业?
A: 可以在 monthly_rebalance 中添加如下逻辑:
# 获取金融行业股票列表 (证监会行业代码 J)
financial_stocks = get_industry_stocks('J00000.XBHS')
# 在 target_list 中剔除
target_list = [s for s in target_list if s not in financial_stocks]
Q: 策略可以改为周度调仓吗?
A: 可以。将 monthly_rebalance 中的月份判断逻辑去掉,直接在 initialize 中使用 run_weekly(context, weekly_rebalance, weekday=1, time='09:35') 即可实现每周一调仓。但基本面策略通常不需要如此高频的调仓。