问题描述
解决方案
这是一个基于 皮尔托斯基 F-Score (Piotroski F-Score) 模型的选股策略实现。
策略原理
Piotroski F-Score 是一种通过财务指标评估公司财务状况的评分系统(0-9分),通常用于在低市净率(低 P/B)的股票池中筛选出基本面强劲的公司。
评分标准(共9分):
- 盈利能力 (4分)
- 资产回报率 (ROA) > 0
- 经营现金流 (CFO) > 0
- ROA 同比增长 (Current ROA > Previous ROA)
- 经营现金流 > 净利润 (Accruals: CFO > Net Income)
- 财务杠杆与流动性 (3分)
- 长期负债比率同比下降 (Leverage 下降)
- 流动比率同比上升 (Current Ratio 上升)
- 未增发新股 (股本数 $\le$ 上一年)
- 运营效率 (2分)
- 毛利率同比上升 (Gross Margin 上升)
- 资产周转率同比上升 (Asset Turnover 上升)
策略逻辑
- 初筛:选取全市场市净率 (P/B) 最低的 20% 股票(剔除停牌、ST)。
- 评分:对初筛股票计算 F-Score。
- 买入:选取 F-Score $\ge$ 7 分的股票。
- 调仓:按月调仓,等权重买入。
策略代码
# -*- coding: utf-8 -*-
from jqdata import *
import pandas as pd
import numpy as np
import datetime
def initialize(context):
# 设定基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 设定手续费
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# 设定全局变量
g.stock_num = 20 # 最终持仓数量
# 按月运行,每月第一个交易日
run_monthly(trade, 1)
def trade(context):
# 1. 获取股票池:全市场 P/B 最低的 20%
# 剔除停牌、ST、上市不满一年的股票
current_date = context.current_dt.date()
all_stocks = list(get_all_securities(['stock'], date=current_date).index)
# 过滤停牌和ST
current_data = get_current_data()
all_stocks = [stock for stock in all_stocks
if not current_data[stock].paused
and not current_data[stock].is_st
and '退' not in current_data[stock].name
and (current_date - get_security_info(stock).start_date).days > 365]
# 获取市净率数据
q = query(valuation.code, valuation.pb_ratio).filter(
valuation.code.in_(all_stocks)
).order_by(valuation.pb_ratio.asc())
df_pb = get_fundamentals(q)
# 取前20%低P/B的股票作为候选池
limit_idx = int(len(df_pb) * 0.2)
candidate_stocks = list(df_pb['code'][:limit_idx])
# 2. 计算 F-Score 并选股
# 获取当前和一年前的财务数据
# 注意:为了简化计算,这里对比的是"当前可见的最新财报"与"一年前可见的财报"
# 这种方式在回测中是无未来函数的
# 获取当前数据
df_curr = get_financial_data(candidate_stocks, current_date)
# 获取一年前数据
last_year_date = current_date - datetime.timedelta(days=365)
df_prev = get_financial_data(candidate_stocks, last_year_date)
if df_curr.empty or df_prev.empty:
return
# 合并数据以便比较
# 后缀 _curr 代表当前,_prev 代表去年
df_merge = pd.merge(df_curr, df_prev, on='code', suffixes=('_curr', '_prev'))
# 计算 F-Score
scores = calculate_f_score(df_merge)
# 筛选 F-Score >= 7 的股票
buy_list = scores[scores['f_score'] >= 7].index.tolist()
# 如果选出的股票过多,按市值从小到大排序取前 g.stock_num 只
if len(buy_list) > g.stock_num:
q_cap = query(valuation.code).filter(
valuation.code.in_(buy_list)
).order_by(valuation.market_cap.asc()).limit(g.stock_num)
buy_list = list(get_fundamentals(q_cap)['code'])
# 3. 执行交易
do_rebalance(context, buy_list)
def get_financial_data(stock_list, date):
"""
获取计算 F-Score 所需的财务数据
"""
q = query(
valuation.code,
# 1. 盈利能力指标
indicator.roa, # 资产回报率
income.net_profit, # 净利润
cash_flow.net_operate_cash_flow, # 经营现金流
balance.total_assets, # 总资产
# 2. 杠杆与流动性指标
balance.long_term_borrowing, # 长期借款 (近似长期负债)
balance.total_current_assets, # 流动资产
balance.total_current_liability, # 流动负债
valuation.capitalization, # 总股本
# 3. 运营效率指标
indicator.gross_profit_margin, # 毛利率
indicator.operation_revenue, # 营业收入
).filter(
valuation.code.in_(stock_list)
)
df = get_fundamentals(q, date=date)
# 数据清洗,填充空值为0
df = df.fillna(0)
# 计算衍生指标
# 资产周转率 = 营业收入 / 总资产
# 注意处理分母为0的情况
df['asset_turnover'] = df['operation_revenue'] / df['total_assets'].replace(0, np.nan)
df['asset_turnover'] = df['asset_turnover'].fillna(0)
# 流动比率 = 流动资产 / 流动负债
df['current_ratio'] = df['total_current_assets'] / df['total_current_liability'].replace(0, np.nan)
df['current_ratio'] = df['current_ratio'].fillna(0)
# 长期负债比率 = 长期借款 / 总资产 (简化版)
df['leverage'] = df['long_term_borrowing'] / df['total_assets'].replace(0, np.nan)
df['leverage'] = df['leverage'].fillna(0)
return df
def calculate_f_score(df):
"""
计算 Piotroski F-Score (0-9分)
"""
df['f_score'] = 0
# --- 1. 盈利能力 (Profitability) ---
# ROA > 0
df.loc[df['roa_curr'] > 0, 'f_score'] += 1
# CFO > 0
df.loc[df['net_operate_cash_flow_curr'] > 0, 'f_score'] += 1
# Delta ROA > 0 (ROA 同比增长)
df.loc[df['roa_curr'] > df['roa_prev'], 'f_score'] += 1
# Accruals: CFO > Net Profit (经营现金流大于净利润,说明盈余质量高)
df.loc[df['net_operate_cash_flow_curr'] > df['net_profit_curr'], 'f_score'] += 1
# --- 2. 杠杆、流动性与资金来源 (Leverage, Liquidity and Source of Funds) ---
# Delta Leverage < 0 (长期负债占比下降,越低越好)
df.loc[df['leverage_curr'] < df['leverage_prev'], 'f_score'] += 1
# Delta Current Ratio > 0 (流动比率上升)
df.loc[df['current_ratio_curr'] > df['current_ratio_prev'], 'f_score'] += 1
# No New Equity (未增发新股: 当前股本 <= 去年股本)
# 考虑到送转股等因素,严格相等比较难,这里放宽为股本没有显著增加
df.loc[df['capitalization_curr'] <= df['capitalization_prev'] * 1.01, 'f_score'] += 1
# --- 3. 运营效率 (Operating Efficiency) ---
# Delta Gross Margin > 0 (毛利率上升)
df.loc[df['gross_profit_margin_curr'] > df['gross_profit_margin_prev'], 'f_score'] += 1
# Delta Asset Turnover > 0 (资产周转率上升)
df.loc[df['asset_turnover_curr'] > df['asset_turnover_prev'], 'f_score'] += 1
return df[['code', 'f_score']].set_index('code')
def do_rebalance(context, buy_list):
"""
调仓函数
"""
# 获取当前持仓
current_positions = list(context.portfolio.positions.keys())
# 卖出不在买入列表中的股票
for stock in current_positions:
if stock not in buy_list:
order_target_value(stock, 0)
# 买入新股票
if len(buy_list) > 0:
# 等权重分配资金
position_count = len(buy_list)
value_per_stock = context.portfolio.total_value / position_count
for stock in buy_list:
order_target_value(stock, value_per_stock)
log.info("调仓完成,当前持仓数量: %d" % len(buy_list))
关键点说明
-
数据获取 (
get_fundamentals):- 策略使用了
get_fundamentals获取财务数据。 - 为了计算 F-Score 中的“同比变化”,我们分别获取了
current_date(当前)和current_date - 365 days(一年前)的数据进行对比。 - JoinQuant 的
get_fundamentals默认返回指定日期能看到的最新财报数据,因此这种写法避免了未来函数。
- 策略使用了
-
指标计算细节:
- ROA (资产回报率):直接使用
indicator.roa。 - CFO (经营现金流):使用
cash_flow.net_operate_cash_flow。 - Accruals (应计项目):比较经营现金流和净利润,现金流大于净利润通常意味着收益质量更高。
- Leverage (杠杆):使用
long_term_borrowing/total_assets近似计算。 - 新股增发:比较
capitalization(总股本)。
- ROA (资产回报率):直接使用
-
选股逻辑:
- 先筛选低估值(低 P/B),这是 Piotroski 策略的核心前提,即在“垃圾股”或“被低估股”中寻找基本面改善的标的。
- 然后计算 F-Score,只买入得分高的($\ge$ 7分)。
-
风险控制:
- 剔除了 ST 股和停牌股。
- 剔除了上市不满一年的次新股(数据可能不稳定)。
- 如果选出的股票太多,代码中增加了一个按市值从小到大排序取前 N 只的逻辑(小市值因子通常在 A 股有效)。
常见问题 Q&A
Q: 为什么使用市净率 (P/B) 进行初筛?
A: Piotroski F-Score 最初的设计目的就是为了区分“真正有价值的低估值股票”和“即将破产的垃圾股”。如果不先限制在低 P/B 股票池中,F-Score 的超额收益效果通常会减弱。
Q: 策略的回测频率为什么是按月?
A: 财务数据通常按季度更新,按月调仓可以及时捕捉到新发布的财报数据。如果希望降低换手率,可以改为按季度调仓(如 run_monthly(trade, 1) 改为每3个月运行一次)。
Q: 如何处理财报数据的滞后性?
A: 代码中使用了 get_fundamentals(..., date=current_date)。在回测中,聚宽平台会自动匹配该日期之前已发布的最新财报,确保不会用到未来数据(例如在4月1日只能看到去年年报或今年一季报,看不到未来的数据)。