🚀 AI 一键生成 ptrade 策略代码

PTrade 策略实战:基于销售毛利率增长的成长股筛选

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

问题描述

如何编写基于销售毛利率增长的选股策略?

解决方案

这是一个基于销售毛利率(Gross Profit Margin)同比增长率的选股策略。

策略逻辑与设计

  1. 核心指标:销售毛利率 = (营业收入 - 营业成本) / 营业收入。
    • 该指标反映了企业产品的定价能力和成本控制能力。
    • 增长逻辑:如果一家公司的销售毛利率同比大幅增长,通常意味着其产品竞争力增强(提价)或原材料成本下降,是业绩改善的先行指标。
  2. 选股范围:沪深300成分股(确保流动性和基本面质量)。
  3. 数据获取
    • 使用 get_fundamentals 获取 profit_ability 表中的 gross_income_ratio(销售毛利率)。
    • 获取当前日期的数据和一年前同期的数据进行对比,计算同比增长率。
  4. 交易逻辑
    • 调仓频率:按月调仓(每月第一个交易日)。
    • 排序:按毛利率增长值从大到小排序。
    • 持仓:持有排名前 N 只股票(例如前 10 只),等权重买入。
    • 卖出:如果持仓股票不在新的优选列表中,则卖出。

PTrade 策略代码实现

import pandas as pd
import numpy as np
import datetime

def initialize(context):
    """
    初始化函数,设置策略参数
    """
    # 设定持仓股票数量
    g.stock_num = 10
    
    # 设定调仓周期(这里通过变量控制,在handle_data中判断是否为月初)
    g.rebalance_flag = False
    
    # 设置基准(沪深300)
    set_benchmark('000300.SS')
    
    # 开启滑点和手续费设置(可选,根据实际需求调整)
    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()
    
    # 获取沪深300成分股作为股票池
    # 注意:get_index_stocks 建议在 before_trading_start 中调用
    g.security_list = get_index_stocks('000300.SS')
    set_universe(g.security_list)
    
    # 判断是否为月初(每月第一个交易日调仓)
    # 获取未来几天的交易日,如果今天是本月第一个交易日,则标记为True
    # 这里简化逻辑:如果今天是该月的第一天或者上一个交易日是上个月,则调仓
    # PTrade中简单判断月初的方法:
    previous_date = context.previous_date.date()
    if current_date.month != previous_date.month:
        g.rebalance_flag = True
    else:
        g.rebalance_flag = False

def handle_data(context, data):
    """
    盘中处理:执行选股和交易逻辑
    """
    # 如果不需要调仓,直接返回
    if not g.rebalance_flag:
        return

    # 1. 获取财务数据
    # 获取当前回测日期的销售毛利率
    # profit_ability 表中的 gross_income_ratio 字段
    current_date_str = context.blotter.current_dt.strftime("%Y%m%d")
    
    # 获取当前最新发布的财务数据
    df_current = get_fundamentals(
        g.security_list, 
        'profit_ability', 
        'gross_income_ratio', 
        date=current_date_str
    )
    
    # 获取一年前的财务数据(用于计算同比增长)
    # 注意:这里简单取365天前的数据作为对比基准,PTrade会自动返回该日期前最新发布的财报
    last_year_date = context.blotter.current_dt - datetime.timedelta(days=365)
    last_year_date_str = last_year_date.strftime("%Y%m%d")
    
    df_last_year = get_fundamentals(
        g.security_list, 
        'profit_ability', 
        'gross_income_ratio', 
        date=last_year_date_str
    )
    
    # 2. 数据清洗与计算
    # 检查数据是否获取成功
    if df_current is None or df_last_year is None:
        log.warning("财务数据获取失败,跳过本次调仓")
        g.rebalance_flag = False
        return
        
    # 将数据转换为 DataFrame 方便处理 (PTrade返回的通常是DataFrame,索引为股票代码)
    # 重命名列以区分
    df_current.columns = ['gross_margin_curr']
    df_last_year.columns = ['gross_margin_last']
    
    # 合并数据
    df_merged = pd.concat([df_current, df_last_year], axis=1)
    
    # 去除空值
    df_merged = df_merged.dropna()
    
    # 计算毛利率增长值 (当前 - 去年同期)
    # 也可以计算增长率 (当前 - 去年) / abs(去年),但在毛利率为负或极小时增长率会失真,
    # 这里采用绝对值增长量更稳健
    df_merged['growth'] = df_merged['gross_margin_curr'] - df_merged['gross_margin_last']
    
    # 3. 选股排序
    # 按照增长值降序排列
    df_sorted = df_merged.sort_values(by='growth', ascending=False)
    
    # 取前 N 只股票
    target_list = df_sorted.head(g.stock_num).index.tolist()
    
    log.info("本月选股名单: %s" % target_list)
    
    # 4. 执行交易
    # 获取当前持仓
    current_positions = list(context.portfolio.positions.keys())
    
    # 卖出不在目标列表中的股票
    for stock in current_positions:
        if stock not in target_list:
            order_target_value(stock, 0)
            log.info("卖出: %s" % stock)
            
    # 买入目标列表中的股票
    # 资金分配:等权重分配
    if len(target_list) > 0:
        # 获取当前账户总资产
        total_value = context.portfolio.portfolio_value
        # 每只股票的目标市值
        target_value_per_stock = total_value / len(target_list)
        
        for stock in target_list:
            # 过滤停牌、跌停等无法买入的情况(order函数内部有一定保护,但最好在逻辑层处理)
            # 这里直接下单,PTrade回测引擎会自动处理停牌无法成交的情况
            order_target_value(stock, target_value_per_stock)
            log.info("买入/调仓: %s, 目标市值: %f" % (stock, target_value_per_stock))
            
    # 调仓完成,重置标志位
    g.rebalance_flag = False

def after_trading_end(context, data):
    """
    盘后处理
    """
    pass

代码关键点解析

  1. 数据获取 (get_fundamentals)

    • 我们使用了 profit_ability 表。
    • gross_income_ratio 代表销售毛利率。
    • 通过传入 date 参数,分别获取“当前回测日期”和“一年前日期”的数据。PTrade 的机制是返回指定日期前最新发布的一份财报数据,这样可以有效模拟同比(YoY)比较。
  2. 增长计算

    • 代码中使用了 df_merged['gross_margin_curr'] - df_merged['gross_margin_last'](绝对值增长)。
    • 为什么不用百分比增长率? 因为毛利率本身是一个百分比数据(例如 20%)。如果从 1% 涨到 2%,增长率是 100%;如果从 40% 涨到 41%,增长率只有 2.5%。但在基本面分析中,高毛利公司提升 1 个百分点的难度往往比低毛利公司更大,且含金量更高。直接比较数值差额(Delta)通常比比较增长比率(Rate)更直观且不易产生极端值。
  3. 调仓逻辑

    • before_trading_start 中通过对比月份变化来标记 g.rebalance_flag
    • handle_data 中执行具体的买卖操作。先卖出非目标股,释放资金,再买入目标股。
  4. 兼容性

    • 代码遵循 Python 3.5 语法规范(避免了 f-string 等新特性),确保在 PTrade 旧版环境中也能运行。
    • 使用了 order_target_value 进行等权重下单,这比计算手数更方便,能自动根据当前股价和资金计算买入数量。

常见问题 (Q&A)

Q: 为什么使用 get_index_stocks 而不是直接全市场选股?
A: 全市场选股容易选中流动性差、市值极小或财务数据异常的股票。使用沪深300(000300.SS)或中证500(000905.XBHS)作为股票池,可以保证选出的股票基本面相对稳健,适合基本面因子策略。

Q: 策略可以增加止损吗?
A: 可以。你可以在 handle_data 中增加逻辑,遍历持仓股票,如果 (现价 - 成本价) / 成本价 < -0.1 (亏损10%),则强制执行 order_target(stock, 0)

Q: 如何查看毛利率的具体数值?
A: get_fundamentals 返回的数据中,gross_income_ratio 的单位通常是百分比的数值(例如返回 20.5 代表 20.5%)。建议在策略运行初期使用 log.info(df_merged.head()) 打印数据查看具体格式。

Q: 为什么回测时第一年可能没有交易?
A: 因为策略需要获取“一年前”的数据来计算增长。如果回测开始时间是 2016-01-01,代码会尝试获取 2015-01-01 的数据。如果你的数据源或回测设置导致无法获取一年前的数据(例如新股上市不足一年),这些股票会被过滤掉。