问题描述
解决方案
在PTrade量化交易平台中,营业周期(Operating Cycle) 是衡量企业资金周转效率的重要财务指标。它反映了企业从取得存货开始到销售存货并收回现金为止所需要的时间。
公式通常为:
$$ \text{营业周期} = \text{存货周转天数} + \text{应收账款周转天数} $$
一般而言,营业周期越短,说明资金周转速度越快,企业资金利用效率越高。
在PTrade中,您不需要手动计算该指标,系统在财务数据接口 get_fundamentals 的 operating_ability(营运能力)表中直接提供了 oper_cycle 字段。
以下是具体的实现方案和策略代码。
1. 策略思路
- 股票池:选取沪深300成分股作为基础股票池。
- 选股逻辑:
- 获取股票池中每只股票的营业周期数据。
- 剔除数据缺失或异常(如小于等于0)的股票。
- 按照营业周期从小到大排序(周期越短越好)。
- 选取排名前10的股票作为持仓目标。
- 交易执行:
- 每日(或调仓周期)开盘前进行选股。
- 盘中对不在目标列表中的持仓股票进行卖出。
- 对在目标列表中但未持有的股票进行买入(等权重买入)。
2. 核心API说明
get_fundamentals: 获取财务数据。- 表名:
'operating_ability'(营运能力表)。 - 字段:
'oper_cycle'(营业周期)。
3. 策略代码实现
def initialize(context):
"""
初始化函数,设置策略参数
"""
# 设定基准为沪深300
set_benchmark('000300.SS')
# 设定手续费等(此处使用默认,可按需添加 set_commission)
# 定义全局变量
g.stock_pool = [] # 股票池
g.target_list = [] # 目标持仓列表
g.hold_num = 10 # 持仓数量
# 设置运行频率,这里演示为每天盘前选股
# 实际基本面策略通常不需要每天调仓,可改为每周或每月
run_daily(context, select_stocks, time='09:30')
def before_trading_start(context, data):
"""
盘前处理:获取股票池
"""
# 获取沪深300成分股
g.stock_pool = get_index_stocks('000300.SS')
# 设置股票池,保证数据获取范围
set_universe(g.stock_pool)
def select_stocks(context):
"""
选股逻辑函数:计算并排序营业周期
"""
# 获取当前回测日期
current_date = context.blotter.current_dt.strftime("%Y%m%d")
# 查询财务数据:营运能力表中的营业周期字段
# q_df 返回的是 DataFrame,索引为股票代码
df = get_fundamentals(
g.stock_pool,
'operating_ability',
'oper_cycle',
date=current_date
)
if df is None or len(df) == 0:
log.info("未获取到财务数据")
g.target_list = []
return
# 数据清洗
# 1. 去除空值
df = df.dropna()
# 2. 营业周期一般为正数,过滤掉异常值
df = df[df['oper_cycle'] > 0]
# 排序:按营业周期从小到大排序 (ascending=True)
# 周期越短,资金周转效率越高
df_sorted = df.sort_values(by='oper_cycle', ascending=True)
# 取前 N 只股票
g.target_list = df_sorted.head(g.hold_num).index.tolist()
log.info("今日选股(营业周期最短前%s名): %s" % (g.hold_num, g.target_list))
def handle_data(context, data):
"""
盘中交易执行
"""
# 1. 卖出不在目标列表中的持仓
# 获取当前所有持仓
positions = context.portfolio.positions
for security in list(positions.keys()):
# 如果持仓股票不在今日目标列表中,且有持仓量,则卖出
if security not in g.target_list and positions[security].amount > 0:
order_target(security, 0)
log.info("卖出非目标股票: %s" % security)
# 2. 买入目标列表中的股票
if len(g.target_list) > 0:
# 等权重分配资金
# 注意:这里简单处理,未考虑卖出后的资金释放延迟,实盘需注意可用资金
position_count = len(g.target_list)
value_per_stock = context.portfolio.portfolio_value / position_count
for security in g.target_list:
# 过滤停牌、退市等无法交易的股票
if data[security].is_open == 0:
continue
# 下单调整至目标市值
order_target_value(security, value_per_stock)
4. 代码解析与注意事项
-
数据获取 (
get_fundamentals):- 我们使用了
operating_ability表。 oper_cycle是系统预计算好的字段,无需手动提取存货和应收账款数据进行加减。date参数传入当前日期,PTrade会自动匹配该日期前最新发布的财务报表数据(避免未来函数)。
- 我们使用了
-
排序逻辑:
df.sort_values(by='oper_cycle', ascending=True):营业周期越短,代表公司从投入资金生产到收回现金的时间越短,通常被视为运营效率高的表现,因此选择升序排列。
-
异常值处理:
- 代码中过滤了
oper_cycle <= 0的情况。虽然理论上营业周期极少为负(除非应付账款周转天数极长且计算公式不同,但在标准定义中通常关注正向周期),但在量化数据清洗中,剔除负值或NaN是必要的步骤以防止干扰排序。
- 代码中过滤了
-
调仓频率:
- 示例代码使用了
run_daily每天检查。由于财务数据通常按季度更新,您可以将select_stocks的运行逻辑修改为按月或按季度运行,以减少不必要的计算和日志输出。
- 示例代码使用了
-
Python版本兼容性:
- 代码遵循 Python 3.5+ 语法,未使用 f-string,而是使用了
%s格式化字符串,确保在 PTrade 旧版本环境中也能稳定运行。
- 代码遵循 Python 3.5+ 语法,未使用 f-string,而是使用了
Q&A
Q: 如果我想手动计算营业周期,应该获取哪些字段?
A: 如果您想深入研究或自定义计算,可以获取以下字段:
inventory_turnover_days(存货周转天数)accounts_receivables_turnover_days(应收账款周转天数)
两者相加即为营业周期。如果还需要计算现金循环周期 (Cash Conversion Cycle),则还需要减去accounts_payable_turnover_days(应付账款周转天数)。
Q: 这个策略适合所有行业吗?
A: 不一定。营业周期指标在制造业、零售业中非常有效。但对于银行、软件服务或轻资产行业,该指标可能不具备横向可比性。建议在 get_index_stocks 后,配合 get_industry_stocks 进行行业筛选,只在特定行业内进行排名。
Q: 回测时为什么有时候选出的股票没有买入?
A: 可能是因为该股票当日停牌(data[security].is_open == 0),或者处于涨停/跌停状态无法成交。代码中已经加入了 is_open 的判断来避免对停牌股票下单。