4. PGR与PLR计算实战:从理论到代码
好了,咱们直接进入正题。PGR和PLR这两个指标,说白了就是用来量化「处置效应」的——也就是投资者倾向于过早卖出盈利股、死扛亏损股的那个毛病。我个人习惯把PGR叫做「盈利卖出比例」,PLR叫做「亏损卖出比例」。当PGR显著高于PLR时,就说明处置效应在作祟。
4.1 Python实现PGR计算
先看PGR。它的公式其实很简单:
PGR = 盈利卖出次数 / (盈利卖出次数 + 盈利持有次数)
嗯,这里要注意:分母是「所有盈利状态下的交易机会」,包括卖出的和没卖的。我刚开始做这个计算时,差点把分母搞成总交易次数,那算出来的东西就完全变味了。
直接上代码:
import pandas as pd
import numpy as np
def calculate_pgr(trades, price_col='close', buy_col='buy_signal', sell_col='sell_signal'):
"""
计算PGR (Proportion of Gains Realized)
Parameters:
-----------
trades : DataFrame
包含价格和交易信号的DataFrame
price_col : str
价格列名
buy_col : str
买入信号列名(1表示买入)
sell_col : str
卖出信号列名(1表示卖出)
Returns:
--------
float : PGR值
"""
# 计算持仓成本(用买入价格近似)
trades['cost'] = trades[price_col].where(trades[buy_col] == 1).ffill()
# 判断是否盈利
trades['is_gain'] = trades[price_col] > trades['cost']
# 盈利时卖出
gain_sells = ((trades[sell_col] == 1) & trades['is_gain']).sum()
# 盈利时持有(包括未卖出和未买入的状态)
gain_holds = trades['is_gain'].sum() - gain_sells
# 计算PGR
pgr = gain_sells / (gain_sells + gain_holds) if (gain_sells + gain_holds) > 0 else 0
return pgr
避坑指南:我曾经在计算盈利持有次数时,直接用了所有盈利状态的天数。结果发现PGR被严重低估了。后来才意识到,应该只统计「有持仓且盈利」的状态,而不是所有盈利的日子。你想想看,空仓时价格再高也跟你没关系啊。
4.2 Python实现PLR计算
PLR的逻辑和PGR对称,只是把「盈利」换成「亏损」:
PLR = 亏损卖出次数 / (亏损卖出次数 + 亏损持有次数)
代码实现:
def calculate_plr(trades, price_col='close', buy_col='buy_signal', sell_col='sell_signal'):
"""
计算PLR (Proportion of Losses Realized)
Parameters:
-----------
参数同calculate_pgr
Returns:
--------
float : PLR值
"""
trades['cost'] = trades[price_col].where(trades[buy_col] == 1).ffill()
# 判断是否亏损
trades['is_loss'] = trades[price_col] < trades['cost']
# 亏损时卖出
loss_sells = ((trades[sell_col] == 1) & trades['is_loss']).sum()
# 亏损时持有
loss_holds = trades['is_loss'].sum() - loss_sells
# 计算PLR
plr = loss_sells / (loss_sells + loss_holds) if (loss_sells + loss_holds) > 0 else 0
return plr
注意:当价格恰好等于成本价时,既不算盈利也不算亏损。我建议用一个小阈值(比如0.1%)来处理平盘状态,否则这些样本会被白白丢弃。
4.3 滚动窗口计算
单次计算的PGR和PLR意义不大。真正有价值的是看它们随时间的变化趋势。我个人习惯用滚动窗口来计算——比如过去60个交易日的滚动PGR和PLR。
def rolling_pgr_plr(trades, window=60, price_col='close',
buy_col='buy_signal', sell_col='sell_signal'):
"""
滚动计算PGR和PLR
Parameters:
-----------
trades : DataFrame
交易数据,需按时间排序
window : int
滚动窗口大小(交易日数)
Returns:
--------
DataFrame : 包含滚动PGR和PLR
"""
results = []
for i in range(window, len(trades) + 1):
window_data = trades.iloc[i-window:i].copy()
pgr = calculate_pgr(window_data, price_col, buy_col, sell_col)
plr = calculate_plr(window_data, price_col, buy_col, sell_col)
results.append({
'date': trades.index[i-1],
'PGR': pgr,
'PLR': plr,
'PGR_PLR_diff': pgr - plr
})
return pd.DataFrame(results).set_index('date')
核心要点:滚动窗口大小怎么选?我一般用60天(约3个月),太短了噪声大,太长了反应迟钝。你可以试试20天和120天,看看哪个更适合你的策略。
4.4 可视化PGR/PLR趋势
光看数字不够直观。咱们画个图,一眼就能看出处置效应的变化趋势。
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
def plot_pgr_plr_trend(rolling_results, figsize=(12, 6)):
"""
可视化PGR和PLR的滚动趋势
Parameters:
-----------
rolling_results : DataFrame
由rolling_pgr_plr函数返回的结果
figsize : tuple
图表尺寸
"""
fig, ax = plt.subplots(figsize=figsize)
# 绘制PGR和PLR
ax.plot(rolling_results.index, rolling_results['PGR'],
label='PGR (盈利卖出比例)', color='green', linewidth=2)
ax.plot(rolling_results.index, rolling_results['PLR'],
label='PLR (亏损卖出比例)', color='red', linewidth=2)
# 填充差值区域
ax.fill_between(rolling_results.index,
rolling_results['PGR'],
rolling_results['PLR'],
where=rolling_results['PGR'] > rolling_results['PLR'],
color='green', alpha=0.1, label='处置效应区域')
ax.fill_between(rolling_results.index,
rolling_results['PGR'],
rolling_results['PLR'],
where=rolling_results['PGR'] < rolling_results['PLR'],
color='red', alpha=0.1, label='反向效应区域')
ax.set_title('PGR vs PLR 滚动趋势 (窗口=60天)', fontsize=14)
ax.set_ylabel('比例', fontsize=12)
ax.set_xlabel('日期', fontsize=12)
ax.legend(loc='best')
ax.grid(True, alpha=0.3)
# 格式化x轴日期
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m'))
ax.xaxis.set_major_locator(mdates.MonthLocator(interval=3))
plt.xticks(rotation=45)
plt.tight_layout()
return fig
我的经验:当PGR持续高于PLR时,说明市场存在明显的处置效应。这时候如果你做量化策略,可以考虑反向操作——别人急着卖盈利股,你反而可以等等;别人死扛亏损股,你该止损就止损。
4.5 知识体系总览
下面这张图把PGR/PLR的计算逻辑和实战流程串起来了:
4.6 实战案例:完整流程
最后,咱们把上面所有代码串起来,跑一个完整的例子:
# 模拟数据(实际使用时替换为真实数据)
np.random.seed(42)
dates = pd.date_range('2023-01-01', periods=500, freq='D')
price = 100 + np.cumsum(np.random.randn(500) * 0.5)
buy_signals = np.random.choice([0, 1], size=500, p=[0.95, 0.05])
sell_signals = np.random.choice([0, 1], size=500, p=[0.95, 0.05])
trades = pd.DataFrame({
'close': price,
'buy_signal': buy_signals,
'sell_signal': sell_signals
}, index=dates)
# 计算滚动PGR和PLR
rolling_results = rolling_pgr_plr(trades, window=60)
# 可视化
fig = plot_pgr_plr_trend(rolling_results)
plt.show()
# 输出统计摘要
print(f"平均PGR: {rolling_results['PGR'].mean():.3f}")
print(f"平均PLR: {rolling_results['PLR'].mean():.3f}")
print(f"PGR - PLR 均值: {rolling_results['PGR_PLR_diff'].mean():.3f}")
关键结论:如果PGR - PLR的均值显著大于0(比如超过0.1),说明你的交易策略或样本群体存在明显的处置效应。这时候优化止损策略就很有必要了——具体怎么优化,咱们后面章节会详细展开。
嗯,PGR和PLR的计算实战就到这里。代码虽然不长,但每个细节都值得推敲。我建议你拿自己的交易数据跑一遍,看看有没有处置效应在拖你的后腿。
公众号:蓝海资料掘金营,微信deep3321