3 分钟快速生成代码
输入想法,AI 即刻生成可运行代码
这是一个基于销售毛利率(Gross Profit Margin)同比增长率的选股策略。
get_fundamentals 获取 profit_ability 表中的 gross_income_ratio(销售毛利率)。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'](绝对值增长)。调仓逻辑:
before_trading_start 中通过对比月份变化来标记 g.rebalance_flag。handle_data 中执行具体的买卖操作。先卖出非目标股,释放资金,再买入目标股。兼容性:
order_target_value 进行等权重下单,这比计算手数更方便,能自动根据当前股价和资金计算买入数量。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 的数据。如果你的数据源或回测设置导致无法获取一年前的数据(例如新股上市不足一年),这些股票会被过滤掉。