问题描述
解决方案
这是一个非常经典的“价值+动量”双因子选股策略。
策略逻辑分析
- 初选(价值因子):使用市盈率(PE-TTM)作为估值指标。通常认为低市盈率代表股票被低估,具有安全边际。我们将剔除亏损股(PE < 0),并按PE从小到大排序,选取排名靠前的一部分股票。
- 复选(动量因子):在低估值股票池中,计算过去一段时间(如20个交易日)的涨跌幅。选取涨幅最高的股票,这代表市场资金近期对该股比较认可,具有上涨惯性。
- 交易执行:卖出不在新目标列表中的持仓,买入新选出的股票,通常采用等权重买入。
- 调仓周期:为了避免过度交易产生高额手续费,通常设置为按周或按月调仓。
PTrade 策略代码实现
以下是完整的策略代码。该代码兼容 Python 3.5 环境,可以直接在 PTrade 回测或交易环境中运行。
def initialize(context):
"""
策略初始化函数,只在策略启动时执行一次
"""
# 设置基准指数:沪深300
set_benchmark('000300.SS')
# 设置回测期间的佣金和滑点(模拟真实交易成本)
set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
set_slippage(slippage=0.002)
# --- 策略参数设置 ---
# 股票池范围:沪深300成分股
g.index_scope = '000300.SS'
# 初选:按低PE选出的股票数量
g.low_pe_count = 50
# 最终持仓:从初选中再按动量选出的股票数量
g.hold_count = 10
# 动量计算周期(过去N天的涨跌幅)
g.momentum_days = 20
# 调仓频率(天),这里设置为每5个交易日调仓一次
g.rebalance_days = 5
# 计数器
g.days_counter = 0
def before_trading_start(context, data):
"""
盘前处理函数,每日开盘前运行
"""
# 增加计数器
g.days_counter += 1
def handle_data(context, data):
"""
盘中处理函数,按分钟或日线频率运行
"""
# 检查是否达到调仓周期
if g.days_counter % g.rebalance_days != 0:
return
# --- 第一步:获取股票池 ---
# 获取沪深300成分股
check_stocks = get_index_stocks(g.index_scope)
if not check_stocks:
log.info("未获取到成分股信息")
return
# --- 第二步:根据市盈率(PE)筛选低估值股票 ---
# 获取市盈率数据 (pe_ttm: 滚动市盈率)
# 注意:get_fundamentals 查询的是财务数据
q = get_fundamentals(check_stocks, 'valuation', ['pe_ttm'], date=None)
if q is None or len(q) == 0:
return
# 过滤掉PE为负(亏损)的股票,且过滤掉PE异常大的数据
# pandas筛选:pe_ttm > 0
q = q[q['pe_ttm'] > 0]
# 按照PE从小到大排序
q = q.sort_values(by='pe_ttm', ascending=True)
# 取出PE最低的前N只股票作为初选池
if len(q) > g.low_pe_count:
candidates = list(q.index[:g.low_pe_count])
else:
candidates = list(q.index)
if not candidates:
return
# --- 第三步:根据近期涨跌幅(动量)筛选强势股 ---
# 获取初选池股票过去N天的历史价格数据
# count=g.momentum_days + 1 是为了计算 N 天前的收盘价和昨天的收盘价的涨幅
price_data = get_history(g.momentum_days + 1, '1d', 'close', security_list=candidates, fq='pre')
momentum_scores = {}
for stock in candidates:
# 获取该股票的收盘价序列
# 注意:get_history返回的数据结构在不同版本可能略有不同,这里使用query确保兼容性
# 或者直接通过列索引获取,如果candidates是列表,price_data通常是DataFrame(列为股票代码)或Panel
# 在PTrade中,get_history多股查询返回DataFrame时,列名为股票代码
try:
# 尝试获取该股票的收盘价序列
closes = price_data[stock]
# 确保数据长度足够
if len(closes) == g.momentum_days + 1:
# 计算涨跌幅:(最新收盘价 - N天前收盘价) / N天前收盘价
# iloc[-1]是最近一天,iloc[0]是N天前
ret = (closes.iloc[-1] - closes.iloc[0]) / closes.iloc[0]
momentum_scores[stock] = ret
except:
continue
# 对动量得分进行排序(从大到小,选涨得最好的)
# Python 3.5 写法
sorted_stocks = sorted(momentum_scores.items(), key=lambda x: x[1], reverse=True)
# 取出前K只股票作为最终买入目标
buy_list = [x[0] for x in sorted_stocks[:g.hold_count]]
log.info("今日选股结果: %s" % buy_list)
# --- 第四步:执行交易 ---
# 1. 卖出不在目标列表中的持仓
current_positions = context.portfolio.positions
for stock in list(current_positions.keys()):
if stock not in buy_list:
# 如果有持仓且不在买入名单中,清仓
if current_positions[stock].amount > 0:
order_target_value(stock, 0)
log.info("卖出: %s" % stock)
# 2. 买入目标列表中的股票
if len(buy_list) > 0:
# 等权重分配资金
# 获取当前总资产(现金+持仓市值)
total_value = context.portfolio.portfolio_value
# 每只股票的目标市值
target_value = total_value / len(buy_list)
for stock in buy_list:
order_target_value(stock, target_value)
log.info("买入/调仓: %s, 目标市值: %f" % (stock, target_value))
代码关键点解析
-
get_fundamentals:- 这是获取财务数据的核心函数。
valuation表包含了估值数据。pe_ttm(滚动市盈率) 比静态市盈率更能反映近期盈利能力。- 代码中增加了
q['pe_ttm'] > 0的过滤条件,剔除了亏损企业,防止选入基本面极差的股票。
-
get_history:- 用于获取历史行情数据来计算动量。
- 参数
fq='pre'表示使用前复权数据,计算收益率时必须使用复权价格,否则分红派息会导致价格跳空,影响动量计算准确性。 - 计算公式为
(当前价格 - N天前价格) / N天前价格。
-
order_target_value:- 使用目标市值下单函数。
- 相比于直接用
order(按股数下单),order_target_value更适合定期调仓策略。它会自动计算需要买入或卖出的数量,使该股票的持仓市值达到指定金额(这里是总资产的 1/10)。
-
调仓控制:
- 使用
g.days_counter配合取模运算%来控制调仓频率。 - 过于频繁的调仓(如每天)会导致手续费吞噬利润,对于基本面策略,通常周频或月频较为合适。
- 使用
常见问题 Q&A
Q: 为什么选择沪深300作为股票池?
A: 沪深300成分股流动性好,财务数据相对规范,不容易出现无法成交或退市暴雷的情况,适合作为策略验证的基准。如果想追求更高收益,可以将 g.index_scope 改为 '000905.SS' (中证500) 或 '000852.SS' (中证1000)。
Q: 为什么要先选低估值再选动量,而不是反过来?
A: 这是一个典型的“安全边际下寻找进攻机会”的思路。先选低估值是为了防守,确保买入的股票不贵;再选动量是为了进攻,避免买入“低估值陷阱”(即长期不涨甚至阴跌的股票)。
Q: 如何查看策略运行结果?
A: 将此代码复制到 PTrade 的策略编辑框中,点击“回测”,设置好回测时间段(例如过去两年),即可看到收益曲线和详细交易记录。