🚀 AI 一键生成 ptrade 策略代码

如何在 PTrade 平台用 Python 实现 Fama-French 三因子选股模型?

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

问题描述

请帮我实现一个 Fama-French 三因子选股策略。

解决方案

这是一个基于 Fama-French 三因子模型 的选股策略实现。

策略原理

Fama-French 三因子模型认为股票的超额收益主要来源于三个因素:

  1. 市场风险因子 (MKT):市场整体的超额回报。
  2. 市值因子 (SMB - Small Minus Big):小市值股票通常优于大市值股票。
  3. 账面市值比因子 (HML - High Minus Low):高账面市值比(即低市净率 PB)的股票通常优于低账面市值比的股票。

策略逻辑

本策略将构建一个利用 SMBHML 因子的多头组合:

  1. 股票池:选取中证500指数成分股(000905.XBHS),因为相比沪深300,中证500更能体现中小市值特征。
  2. 过滤:剔除 ST、停牌、退市股票。
  3. 打分排序
    • 市值 (Market Cap):从小到大排序(因子:小市值)。
    • 市净率 (PB):从小到大排序(因子:高价值)。
    • 综合得分:将两者的排名相加,得分越低越好。
  4. 调仓频率:按月调仓(每月第一个交易日)。
  5. 资金分配:等权重买入排名前 N 的股票。

PTrade 策略代码

import pandas as pd
import numpy as np

def initialize(context):
    """
    策略初始化函数
    """
    # 设置基准为中证500
    set_benchmark('000905.XBHS')
    
    # 设置股票池为中证500(更能体现中小市值因子)
    g.index_code = '000905.XBHS'
    
    # 持仓数量
    g.stock_num = 20
    
    # 记录上一次调仓的月份,用于控制月度调仓
    g.last_month = 0
    
    # 设定手续费(股票:万三,最低5元)
    set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
    
    # 每天开盘后运行检查,判断是否需要调仓
    run_daily(context, check_and_rebalance, time='09:35')

def check_and_rebalance(context):
    """
    每日检查是否需要调仓(月度调仓逻辑)
    """
    # 获取当前日期的月份
    current_month = context.blotter.current_dt.month
    
    # 如果当前月份与上一次调仓月份不同,则执行调仓
    if current_month != g.last_month:
        log.info("新的一月,开始执行 Fama-French 三因子选股调仓...")
        rebalance(context)
        g.last_month = current_month

def rebalance(context):
    """
    核心调仓逻辑
    """
    # 1. 获取股票池(中证500成分股)
    # 注意:get_index_stocks 建议在盘前或盘中调用,不要在 initialize 中调用
    universe = get_index_stocks(g.index_code)
    
    if not universe:
        log.warning("获取指数成分股失败")
        return

    # 2. 过滤 ST、停牌、退市股票
    # filter_stock_by_status 返回的是剔除后的列表
    target_list = filter_stock_by_status(universe, filter_type=["ST", "HALT", "DELISTING"])
    
    if not target_list:
        log.warning("过滤后股票列表为空")
        return

    # 3. 获取基本面数据:总市值 (total_value) 和 市净率 (pb)
    # total_value 代表市值因子 (SMB),pb 代表价值因子 (HML)
    # 注意:PTrade 的 get_fundamentals 返回的是 DataFrame
    q_df = get_fundamentals(target_list, 'valuation', ['total_value', 'pb'])
    
    if q_df is None or q_df.empty:
        log.warning("获取基本面数据失败")
        return
    
    # 4. 数据清洗:去除空值和 PB <= 0 的数据(PB为负通常意味着资不抵债)
    q_df = q_df.dropna()
    q_df = q_df[q_df['pb'] > 0]

    # 5. 计算因子排名 (Rank)
    # 市值因子:越小越好 (SMB) -> 升序排名
    q_df['cap_rank'] = q_df['total_value'].rank(ascending=True)
    
    # 价值因子:PB 越小越好 (HML) -> 升序排名
    q_df['pb_rank'] = q_df['pb'].rank(ascending=True)
    
    # 6. 计算综合得分
    # 简单的等权打分法:排名之和越小,说明市值越小且估值越低
    q_df['total_score'] = q_df['cap_rank'] + q_df['pb_rank']
    
    # 7. 选取排名前 N 的股票
    q_df = q_df.sort_values(by='total_score', ascending=True)
    buy_list = q_df.index[:g.stock_num].tolist()
    
    log.info("本月选股列表: %s" % str(buy_list))
    
    # 8. 执行交易
    do_trade(context, buy_list)

def do_trade(context, buy_list):
    """
    执行具体的买卖操作
    """
    # 获取当前持仓
    positions = context.portfolio.positions.keys()
    
    # 卖出不在买入列表中的股票
    for stock in positions:
        if stock not in buy_list:
            # 检查是否停牌,停牌无法卖出
            stock_status = get_stock_status([stock], 'HALT')
            if stock_status[stock] is False: # False 表示未停牌
                order_target_value(stock, 0)
                log.info("卖出: %s" % stock)
    
    # 买入/调仓目标股票
    if len(buy_list) > 0:
        # 等权重分配资金
        # 注意:这里简单使用总资产/股票数量,实际交易中可能需要预留少量现金防止手续费不够
        position_value = context.portfolio.portfolio_value / len(buy_list)
        
        for stock in buy_list:
            # 检查是否停牌或涨停,避免废单(这里简单检查停牌,涨停由 order_target_value 内部机制或 check_limit 处理)
            stock_status = get_stock_status([stock], 'HALT')
            if stock_status[stock] is False:
                order_target_value(stock, position_value)
                # log.info("调整持仓: %s 到目标市值: %.2f" % (stock, position_value))

def handle_data(context, data):
    """
    必须实现的函数,这里留空,逻辑主要在 run_daily 中
    """
    pass

代码关键点解析

  1. 因子获取 (get_fundamentals)

    • 我们使用了 valuation 表。
    • total_value:A股总市值,用于衡量规模因子(SMB)。
    • pb:市净率,用于衡量价值因子(HML)。Fama-French 原文使用的是账面市值比(B/M),它是 PB 的倒数。因此,PB 越低 等同于 B/M 越高,符合价值股特征。
  2. 数据处理与打分

    • 使用了 pandasrank 函数对两个因子分别进行排名。
    • 直接将两个排名相加(cap_rank + pb_rank),这是一种简单有效的多因子合成方法。得分越低,代表该股票在“小市值”和“低估值”两个维度上综合表现越好。
  3. 调仓控制

    • 利用 g.last_month 变量记录上一次调仓的月份。
    • check_and_rebalance 中比较 current_monthg.last_month,实现自然月调仓(每月第一个交易日)。
  4. 交易执行

    • 使用 order_target_value 接口。这个接口非常适合定期调仓策略,它会自动计算买卖数量,将持仓调整到目标金额(即总资产的 1/N)。

常见问题 (Q&A)

Q: 为什么选择中证500而不是沪深300?
A: Fama-French 模型中的 SMB 因子强调小市值股票的超额收益。沪深300代表大盘蓝筹,市值普遍较大,区分度不高。中证500包含了大量中盘和小盘股,更适合挖掘小市值因子的超额收益。

Q: 代码中如何处理停牌股票?
A: 代码中使用了 filter_stock_by_status 在选股阶段剔除停牌股。在交易阶段(do_trade),再次调用了 get_stock_status 检查持仓股是否停牌,防止对停牌股下单导致报错或废单。

Q: 可以增加动量因子吗?
A: 可以。如果增加动量因子(Momentum),这就变成了 Carhart 四因子模型。你需要获取过去一段时间(如12个月)的涨跌幅,将其加入打分逻辑中(通常是买入过去涨幅高的股票)。

Q: 回测时需要注意什么?
A: 建议将 get_history 或数据获取的 count 设置得足够大以确保数据稳定,但在本策略中主要依赖财务数据,因此要注意财务数据的滞后性。PTrade 的 get_fundamentals 在回测模式下会自动处理“未来函数”,即只能获取到回测日期当时已发布的财报数据,这是安全的。