问题描述
解决方案
这是一个基于销售毛利率(Gross Profit Margin)同比增长率的选股策略。
策略逻辑与设计
- 核心指标:销售毛利率 = (营业收入 - 营业成本) / 营业收入。
- 该指标反映了企业产品的定价能力和成本控制能力。
- 增长逻辑:如果一家公司的销售毛利率同比大幅增长,通常意味着其产品竞争力增强(提价)或原材料成本下降,是业绩改善的先行指标。
- 选股范围:沪深300成分股(确保流动性和基本面质量)。
- 数据获取:
- 使用
get_fundamentals获取profit_ability表中的gross_income_ratio(销售毛利率)。 - 获取当前日期的数据和一年前同期的数据进行对比,计算同比增长率。
- 使用
- 交易逻辑:
- 调仓频率:按月调仓(每月第一个交易日)。
- 排序:按毛利率增长值从大到小排序。
- 持仓:持有排名前 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
代码关键点解析
-
数据获取 (
get_fundamentals):- 我们使用了
profit_ability表。 gross_income_ratio代表销售毛利率。- 通过传入
date参数,分别获取“当前回测日期”和“一年前日期”的数据。PTrade 的机制是返回指定日期前最新发布的一份财报数据,这样可以有效模拟同比(YoY)比较。
- 我们使用了
-
增长计算:
- 代码中使用了
df_merged['gross_margin_curr'] - df_merged['gross_margin_last'](绝对值增长)。 - 为什么不用百分比增长率? 因为毛利率本身是一个百分比数据(例如 20%)。如果从 1% 涨到 2%,增长率是 100%;如果从 40% 涨到 41%,增长率只有 2.5%。但在基本面分析中,高毛利公司提升 1 个百分点的难度往往比低毛利公司更大,且含金量更高。直接比较数值差额(Delta)通常比比较增长比率(Rate)更直观且不易产生极端值。
- 代码中使用了
-
调仓逻辑:
- 在
before_trading_start中通过对比月份变化来标记g.rebalance_flag。 - 在
handle_data中执行具体的买卖操作。先卖出非目标股,释放资金,再买入目标股。
- 在
-
兼容性:
- 代码遵循 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 的数据。如果你的数据源或回测设置导致无法获取一年前的数据(例如新股上市不足一年),这些股票会被过滤掉。