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的计算逻辑和实战流程串起来了:

PGR/PLR计算实战知识体系 原始交易数据 数据预处理(成本计算、状态标记) PGR计算(盈利卖出/盈利总次数) PLR计算(亏损卖出/亏损总次数) 滚动窗口计算(60天窗口) 滚动窗口计算(60天窗口) 可视化趋势图(PGR vs 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