🚀 AI 一键生成 joinquant 策略代码

如何用 Python 实现格林布拉特的神奇公式(Magic Formula)选股策略?

joinquant | 发布时间: 2025/12/19
以下内容由 EasyQuant 生成。

问题描述

请帮我用 Python 实现一个神奇公式选股策略

解决方案

这是一个基于乔尔·格林布拉特(Joel Greenblatt)在《股市稳赚》(The Little Book That Beats the Market)中提出的**神奇公式(Magic Formula)**选股策略的 Python 实现代码。

策略原理

神奇公式的核心思想是**“用便宜的价格买入好公司”**。它通过两个指标对股票进行排名:

  1. 资本回报率 (ROIC):衡量公司的盈利能力(好公司)。公式中通常使用 EBIT / (净流动资本 + 净固定资产),在 A 股回测中,我们可以直接使用聚宽提供的 indicator.roic (投资资本回报率) 作为替代。
  2. 收益率 (Earnings Yield):衡量公司的估值(便宜)。公式中通常使用 EBIT / 企业价值 (EV),在 A 股回测中,我们可以使用市盈率的倒数(即 1 / PE)或者直接使用 valuation.pe_ratio (市盈率) 进行反向排序(PE 越低越好)。

选股逻辑:

  1. 选取 A 股股票池,剔除 ST、停牌、退市股票。
  2. 获取所有股票的 ROIC 和 PE 数据。
  3. 对 ROIC 从高到低排名。
  4. 对 PE 从低到高排名。
  5. 将两个排名相加得到总分,总分越低越好。
  6. 买入总分最低的前 N 只股票(例如 30 只)。
  7. 定期(例如每月或每季度)调仓。

策略代码

# -*- 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

代码关键点解析

  1. 数据获取 (get_fundamentals):

    • 我们使用了 valuation.pe_ratio 代表估值因子。神奇公式原版使用的是 EBIT/EV,但在 A 股量化中,PE 是最接近且数据质量最稳定的替代指标。
    • 我们使用了 indicator.roic 代表质量因子。这直接对应了资本回报率。
    • 我们在查询时过滤了 pe_ratio > 0,排除了亏损企业,这符合神奇公式寻找“好公司”的初衷。
  2. 排名逻辑 (rank):

    • df['pe_ratio'].rank(ascending=True): PE 越小,排名数值越小(例如 PE 最低的排名为 1)。
    • df['roic'].rank(ascending=False): ROIC 越大,排名数值越小(例如 ROIC 最高的排名为 1)。
    • total_score: 两者相加,分数越低,代表综合性价比越高。
  3. 调仓频率:

    • 使用了 run_monthly 设置为每月调仓。虽然格林布拉特建议一年调仓一次,但在 A 股市场波动较大的环境下,月度或季度调仓通常能获得更好的平滑收益。
  4. 过滤逻辑:

    • 代码中显式过滤了 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)在回测中会自动匹配当时能看到的最新财报,避免了未来函数,但在分析结果时要注意财报发布的季节性效应。