🚀 AI 一键生成 ptrade 策略代码

PTrade 选股进阶:如何用 Python 构建多因子选股模型?

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

问题描述

我想编写一个多因子选股策略,结合市盈率、市净率和净资产收益率等多个指标进行选股,该如何实现?

解决方案

这是一个非常经典的多因子选股策略需求。在量化交易中,结合市盈率(PE)、**市净率(PB)净资产收益率(ROE)**通常被称为“低估值高质量”策略。

策略设计思路

  1. 选股范围(股票池):通常选择沪深300或中证500作为基础股票池,这里我们以沪深300为例。
  2. 因子选择
    • PE (TTM):滚动市盈率,越低越好(代表估值低)。
    • PB:市净率,越低越好(代表估值低)。
    • ROE (TTM):滚动净资产收益率,越高越好(代表盈利能力强)。
  3. 数据处理
    • 剔除停牌、ST、退市股票。
    • 剔除因子数据缺失的股票。
    • 打分法:对每个因子进行排名。PE和PB按升序排名(值越小排名越靠前),ROE按降序排名(值越大排名越靠前)。将三个排名相加得到总分,总分越低越好。
  4. 调仓周期:按月调仓(例如每个月第一个交易日)。
  5. 资金分配:等权重买入排名前N只股票。

PTrade 策略代码实现

以下是完整的策略代码。代码基于Python 3.5语法编写,可以直接在PTrade的回测或交易环境中运行。

import pandas as pd
import numpy as np

def initialize(context):
    """
    初始化函数,设置策略参数
    """
    # 1. 设置基准指数:沪深300
    g.index_security = '000300.SS'
    # 2. 设置持仓数量
    g.stock_num = 10
    # 3. 设置调仓标志,默认为False
    g.rebalance_flag = False
    
    # 设置回测参数(仅回测有效,实盘会忽略)
    set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
    set_slippage(slippage=0.002)

def before_trading_start(context, data):
    """
    盘前处理函数,每天开盘前运行
    """
    # 获取当前日期
    current_date = context.blotter.current_dt.date()
    
    # 判断是否为月初(每月第一个交易日调仓)
    # 获取未来几天的交易日,如果当前日期是本月第一个交易日,则标记为调仓日
    # 这里使用一种简化的判断逻辑:获取过去20个交易日,判断月份是否发生变化
    previous_date = get_trading_day(-1)
    
    if previous_date.month != current_date.month:
        g.rebalance_flag = True
    else:
        g.rebalance_flag = False

def handle_data(context, data):
    """
    盘中运行函数
    """
    # 如果是调仓日,则执行选股和交易逻辑
    if g.rebalance_flag:
        rebalance(context)
        # 执行完后重置标志
        g.rebalance_flag = False

def rebalance(context):
    """
    核心调仓逻辑
    """
    log.info("开始进行多因子选股调仓...")
    
    # 1. 获取股票池(沪深300成分股)
    # 注意:get_index_stocks在回测中默认取回测当日成分股
    stocks = get_index_stocks(g.index_security)
    
    # 2. 过滤掉 ST、停牌、退市的股票
    # filter_stock_by_status 返回的是剔除后的列表
    stocks = filter_stock_by_status(stocks, filter_type=["ST", "HALT", "DELISTING"])
    
    if not stocks:
        log.warning("股票池为空,跳过本次调仓")
        return

    # 3. 获取财务数据
    # PTrade的get_fundamentals一次只能查一张表,所以需要分两次查询再合并
    
    # 3.1 查询估值数据:PE(TTM) 和 PB
    # pe_ttm: 市盈率TTM, pb: 市净率
    df_val = get_fundamentals(
        stocks, 
        table='valuation', 
        fields=['pe_ttm', 'pb']
    )
    
    # 3.2 查询盈利能力数据:ROE(TTM)
    # roe_ttm: 净资产收益率TTM
    df_prof = get_fundamentals(
        stocks, 
        table='profit_ability', 
        fields=['roe_ttm']
    )
    
    # 4. 数据合并与清洗
    if df_val is None or df_prof is None:
        log.warning("获取财务数据失败")
        return
        
    # 将两个DataFrame合并,索引通常是股票代码
    # 注意:PTrade返回的DataFrame索引通常是股票代码
    df = pd.concat([df_val, df_prof], axis=1, join='inner')
    
    # 去除包含空值的行
    df = df.dropna()
    
    # 过滤掉PE为负(亏损)的股票,通常亏损股的估值指标会失真
    df = df[df['pe_ttm'] > 0]
    
    if len(df) == 0:
        log.warning("数据清洗后无股票可选")
        return

    # 5. 多因子打分 (Rank Scoring)
    # 策略:低PE、低PB、高ROE
    
    # 对PE进行排名,从小到大,名次越小越好
    df['rank_pe'] = df['pe_ttm'].rank(ascending=True)
    
    # 对PB进行排名,从小到大,名次越小越好
    df['rank_pb'] = df['pb'].rank(ascending=True)
    
    # 对ROE进行排名,从大到小,名次越小越好 (ascending=False)
    df['rank_roe'] = df['roe_ttm'].rank(ascending=False)
    
    # 计算总分(等权重相加)
    df['total_score'] = df['rank_pe'] + df['rank_pb'] + df['rank_roe']
    
    # 6. 选出总分最低(排名最靠前)的N只股票
    df_sorted = df.sort_values(by='total_score', ascending=True)
    target_stocks = df_sorted.head(g.stock_num).index.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())
    
    # 1. 卖出不在目标列表中的股票
    for stock in current_positions:
        if stock not in target_stocks:
            # 检查是否停牌,停牌无法卖出
            if check_limit(stock).get(stock) == 0: # 0表示既不涨停也不跌停,这里简单判断,严谨可用get_stock_status
                 # 强行平仓
                order_target_value(stock, 0)
    
    # 2. 买入目标股票
    # 计算每只股票的目标资金分配(等权重)
    if len(target_stocks) > 0:
        # 获取当前账户总资产
        total_value = context.portfolio.portfolio_value
        # 预留一点现金防止手续费不够,乘以0.98
        target_value_per_stock = (total_value * 0.98) / len(target_stocks)
        
        for stock in target_stocks:
            # 下单调整到目标金额
            order_target_value(stock, target_value_per_stock)

代码关键点解析

  1. 数据获取 (get_fundamentals)

    • PTrade的财务数据分布在不同的表中。市盈率 (pe_ttm) 和市净率 (pb) 在 valuation 表中,而净资产收益率 (roe_ttm) 在 profit_ability 表中。
    • 代码中分别调用了两次接口,然后使用 pd.concat 将数据合并,这是处理多表因子的标准做法。
  2. 因子排序 (rank)

    • 我们没有直接使用因子的绝对值进行加权,而是使用了排名法
    • rank(ascending=True):值越小,排名越靠前(得分越低)。适用于PE和PB。
    • rank(ascending=False):值越大,排名越靠前(得分越低)。适用于ROE。
    • 最后将三个排名相加,总分越低代表综合表现越好。
  3. 调仓逻辑

    • before_trading_start 中通过比较昨日和今日的月份来判断是否是月初。
    • 使用 order_target_value 进行下单,这会自动计算需要买入或卖出的股数,非常适合等权重策略。
  4. 风险控制

    • 使用了 filter_stock_by_status 剔除了ST、停牌和退市股票,防止买入无法交易或风险过大的标的。
    • 剔除了PE为负值的亏损股票。

常见问题 (Q&A)

Q: 为什么使用 TTM (Trailing Twelve Months) 数据?
A: TTM 数据包含了最近12个月的财务数据,相比静态数据(仅看上一年报)更能反映公司当前的经营状况,且消除了季节性波动的影响。

Q: 如何修改选股数量?
A: 修改 initialize 函数中的 g.stock_num = 10,将 10 改为你想要的数量即可。

Q: 我想加入市值因子(小市值策略),怎么改?
A: 在 valuation 表中获取 total_value (总市值) 字段,对其进行 rank(ascending=True) 排名,并加入到 total_score 的计算中即可。

Q: 为什么代码中没有 import ptrade
A: PTrade 平台的策略代码运行在特定的沙箱环境中,所有 API(如 get_fundamentals, order 等)都是内置的全局函数,无需导入,直接调用即可。

策略与金融概念

什么是多因子选股?

多因子选股是一种量化投资策略,它假设股票的收益可以由一组共同的“因子”来解释。通过寻找那些在历史上能够产生超额收益的因子(如低估值、高盈利、高动量等),并结合这些因子对股票进行打分或排序,从而构建投资组合。

为什么同时看PE、PB和ROE?

这三个指标构成了一个经典的“高性价比”筛选体系:

  1. PE (市盈率):衡量买入成本与公司盈利的关系。低PE意味着你用较少的钱买到了较多的盈利。
  2. PB (市净率):衡量买入成本与公司净资产的关系。低PB意味着股价接近甚至低于公司的账面价值,具有安全边际。
  3. ROE (净资产收益率):衡量公司利用股东资本赚钱的能力。高ROE意味着公司也是“印钞机”。
    结合逻辑:如果只看低PE/PB,可能会买到基本面恶化的“垃圾股”;加上高ROE,就确保了买入的是既便宜又赚钱的好公司。