3 分钟快速生成代码
输入想法,AI 即刻生成可运行代码
这是一个基于 皮尔托斯基 F-Score (Piotroski F-Score) 模型的选股策略实现。
Piotroski F-Score 是一种通过财务指标评估公司财务状况的评分系统(0-9分),通常用于在低市净率(低 P/B)的股票池中筛选出基本面强劲的公司。
评分标准(共9分):
# -*- 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 获取财务数据。current_date(当前)和 current_date - 365 days(一年前)的数据进行对比。get_fundamentals 默认返回指定日期能看到的最新财报数据,因此这种写法避免了未来函数。指标计算细节:
indicator.roa。cash_flow.net_operate_cash_flow。long_term_borrowing / total_assets 近似计算。capitalization (总股本)。选股逻辑:
风险控制:
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日只能看到去年年报或今年一季报,看不到未来的数据)。