保持敬畏之心
交易是一场持久战

Pine Script(237):策略的盈利因子与悲观盈利因子PPF

#Pine Script入门教学

获取策略的盈利因子

任何交易策略都无法避免亏损,但一个成功的策略,其盈利必须足以覆盖所有亏损。盈利因子(profit factor)就是衡量策略盈利与亏损关系的一个常用指标。

盈利因子反映的是策略总盈利(gross profit)与总亏损(gross loss)之间的比率。简而言之,它告诉我们策略每亏损1个货币单位,能赚回多少个货币单位。盈利因子的计算公式如下:

其中,总盈利是所有盈利交易的利润总和,总亏损是所有亏损交易的亏损总和,两者都已计入佣金和滑点等交易成本。当总盈利大于总亏损时,策略便实现了净利润,其盈利因子也会大于1。

自定义函数

就目前而言,PineScript中并没有一个内置变量可以直接返回策略的盈利因子。但这并不妨碍我们使用它,因为我们可以轻松地编写一个自定义函数来实现这一功能。以下便是这个自定义函数:

// ProfitFactor() 函数返回策略当前的盈利因子,
// 计算结果包含了佣金和滑点(如果策略有相关设置)。
// 请注意:如果策略尚未产生任何亏损交易,
// 总亏损将为零,此时函数会返回 'na' (无效值)。
ProfitFactor() =>
    strategy.grossprofit / strategy.grossloss

这个 ProfitFactor() 函数能返回策略在当前K线的盈利因子。函数内部的逻辑很简单:用所有盈利交易的总和(strategy.grossprofit)除以所有亏损交易的总和(strategy.grossloss)。这两个值都是PineScript的内置变量,计算结果就是我们需要的盈利因子。

需要注意的是,如果策略运行至今还没有出现过任何亏损交易,那么 strategy.grossloss 的值就是零。在这种情况下,执行除法会出错,因此 ProfitFactor() 函数会返回一个 na 值。所以,要正确计算盈利因子,策略至少需要产生一笔亏损交易。

函数应用示例

将上面的函数代码复制粘贴到你的策略脚本中之后,你就可以在多处灵活地调用它了。最基础的用法,就是在需要时直接调用函数来获取当前的盈利因子:

// 获取当前的盈利因子
pfResult = ProfitFactor()

由于 ProfitFactor() 函数在每根K线上都会计算并返回一个值,我们也可以比较当前值与历史值。例如,要判断盈利因子是否在当前K线上有所提升,可以这样写:

// 比较当前与上一根K线的盈利因子
if ProfitFactor() > ProfitFactor()[1]
    // 盈利因子出现增长!

在策略回测历史数据时,它可以根据实时计算出的盈利因子来动态调整其行为,从而适应盈利能力的变化。例如,我们可以在盈利因子低于1(即策略处于亏损状态)时,将默认的下单量减少40%:

defaultOrderSize = 100

// 当盈利因子小于1时,减小订单规模
if ProfitFactor() < 1
    defaultOrderSize := 60

// 当价格高于20周期EMA时,按计算后的规模开多仓
if close > ta.ema(close, 20)
	strategy.entry("Enter Long", strategy.long, qty=defaultOrderSize)

ProfitFactor() 还可以和其他PineScript内置函数结合使用。比如,要捕捉盈利因子从上方跌破1的瞬间,我们可以将它与 ta.crossunder() 函数组合:

// 判断盈利因子是否跌破1
if ta.crossunder(ProfitFactor(), 1)
    label.new(x=bar_index, y=high, text="盈利因子 < 1")

为了更直观地监控盈利因子的变化,我们可以用 plot() 函数将其绘制在图表上:

// 将盈利因子以柱状图形式绘制在图表上
plot(ProfitFactor(), style=plot.style_columns, title="盈利因子")

策略范例

让我们在一个完整的策略中应用盈利因子。下面的脚本基于20周期的布林带进行交易。当价格上穿布林带上轨时,我们做多;当价格下穿下轨时,我们做空。

我们会在图表历史数据的最后一根K线上,创建一个文本标签,用来展示策略最终的盈利因子。如果策略是盈利的(盈利因子大于1),标签会显示为绿色,否则显示为红色。策略的完整代码如下:

//@version=5
strategy(title="盈利因子示例", overlay=true)

// ProfitFactor() 函数返回策略当前的盈利因子,
// 计算结果包含了佣金和滑点(如果策略有相关设置)。
// 注意:如果策略尚未产生任何亏损交易,
// 总亏损将为零,此时函数会返回 'na' (无效值)。
ProfitFactor() =>
    strategy.grossprofit / strategy.grossloss

// 计算布林带指标
[middleLine, upperBand, lowerBand] = ta.bb(close, 20, 2.0)

// 绘制布林带轨道线
plot(upperBand, color=color.teal, title="上轨")
plot(middleLine, color=color.gray, title="中轨")
plot(lowerBand, color=color.teal, title="下轨")

// 定义入场条件
if ta.crossover(close, upperBand)
    strategy.entry("Enter Long", strategy.long)

if ta.crossunder(close, lowerBand)
    strategy.entry("Enter Short", strategy.short)

// 定义出场条件
if ta.cross(close, middleLine)
    strategy.close_all(comment="平仓")

// 在历史回测的最后一根K线上,创建标签显示策略的盈利因子
if barstate.islastconfirmedhistory
    labelColour = ProfitFactor() > 1 ? #AAF0D1 : #F4C2C2
    labelText = "总盈利: " + 
             str.tostring(strategy.grossprofit, "##,###.00") + 
         "\n总亏损: " + str.tostring(strategy.grossloss, "##,###.00") + 
         "\n\n盈利因子: " + str.tostring(ProfitFactor(), "0.000")

    label.new(x=bar_index + 3, y=hl2, style=label.style_label_left,
         color=labelColour, text=labelText)

首先,我们用 strategy() 函数设置脚本的名称,并通过 overlay=true 将指标和交易信号直接叠加在主图上。接着,我们引入了前面定义的 ProfitFactor() 自定义函数。

然后,ta.bb() 函数被调用来计算参数为20周期和2.0标准差的布林带。我们将返回的中轨、上轨和下轨分别存入 middleLineupperBandlowerBand 变量,并用 plot() 函数将它们绘制成线。

随后的三个 if 语句构成了策略的交易逻辑。第一个使用 ta.crossover() 函数检测价格是否向上穿越了布林带上轨,如果是,则调用 strategy.entry() 执行多头开仓。第二个 if 语句则用 ta.crossunder() 函数检测价格是否向下跌破了下轨,如果是,则执行空头开仓。

第三个 if 语句负责平仓。它使用 ta.cross() 函数来判断价格是否与中轨发生了任何方向的穿越,一旦发生,strategy.close_all() 函数将平掉所有当前持有的仓位。

最后一部分代码,负责在图表上展示最终的盈利因子:

// 在历史回测的最后一根K线上,创建标签显示策略的盈利因子
if barstate.islastconfirmedhistory
    labelColour = ProfitFactor() > 1 ? #AAF0D1 : #F4C2C2
    labelText = "总盈利: " + 
             str.tostring(strategy.grossprofit, "##,###.00") + 
         "\n总亏损: " + str.tostring(strategy.grossloss, "##,###.00") + 
         "\n\n盈利因子: " + str.tostring(ProfitFactor(), "0.000")

    label.new(x=bar_index + 3, y=hl2, style=label.style_label_left,
         color=labelColour, text=labelText)

此处的 if 语句通过 barstate.islastconfirmedhistory 变量来判断当前是否为历史回测的最后一根K线,该变量在此刻会返回 true

if 模块内部,我们首先设定标签的颜色。通过调用 ProfitFactor() 函数判断盈利因子是否大于1。如果是,颜色设为 #AAF0D1(一种清新的薄荷绿);否则,设为 #F4C2C2(一种柔和的玫瑰红)。

接着,我们构建标签要显示的文本内容。我们将几个描述性字符串与三个关键变量的值拼接起来:总盈利(strategy.grossprofit)、总亏损(strategy.grossloss)和盈利因子(ProfitFactor())。由于这些变量是数值类型,而标签需要文本,我们使用 str.tostring() 函数将它们格式化为字符串。

最后,调用 label.new() 函数来创建标签。标签被放置在当前K线右侧3根K线的位置(bar_index + 3),垂直方向位于K线的中心点(hl2)。其样式为箭头指向左侧(label.style_label_left),颜色和文本内容则使用我们刚才定义好的变量。

当把这个脚本应用到图表上,它所生成的盈利因子标签效果如下:

简单总结一下:盈利因子用于衡量策略的总盈利相对于总亏损的规模。盈利因子大于1,意味着策略是盈利的,赚到的钱多于亏损的钱;小于1则意味着策略是亏损的。要计算盈利因子,我们只需将策略的总盈利除以其总亏损(取绝对值)。

悲观盈利因子PPF及其实现

悲观利润因子(Pessimistic Profit Factor)是衡量策略盈利能力的标准利润因子指标的一种更为保守的替代方案。它的核心目标是,通过对策略的历史回测结果进行悲观的调整,从而更真实地预估其在未来实盘交易中的可能表现。接下来,我们将深入探讨悲观利润因子的概念,并学习如何在PineScript中将其付诸实践。

什么是悲观利润因子

悲观利润因子(PPF)是基于Robert Pardo的悲观保证金回报率(Pessimistic Return On Margin, PROM)理念所衍生出的一个回测评估指标。PROM的核心思想是通过调低策略的总利润,同时调高其总亏损,来向下修正策略的回报率。

当我们将这一悲观原则应用于利润因子时,便可以定义出悲观利润因子的计算公式:

该公式中,gross profit(总利润)是所有盈利交易的累计收益,gross loss(总亏损)是所有亏损交易的累计损失,两者均已包含佣金和滑点成本;win count(盈利笔数)和loss count(亏损笔数)分别是盈利与亏损交易的总次数。

PPF的本质是在数学层面上,人为地减少总利润并增加总亏损。这种双向的负面调整,使得策略的表现评估更为严苛。因此,一个策略的PPF值,毫无疑问总会低于其常规的利润因子值。

悲观利润因子具备两大重要优势。一是预留安全边际:它假设策略未来的实盘表现会劣于其历史回测表现,从而降低了利润因子值,为评估提供了安全边际。二是惩罚小样本:相比于拥有大量交易记录的策略,PPF对交易样本量小的策略惩罚更重,交易笔数的统计学意义越弱,经过PPF调整后的利润因子就越差。

许多交易策略都面临着一个普遍问题:经过优化的历史回测表现,与随后的实盘结果之间存在巨大鸿沟。PPF正是试图通过对历史数据注入一份怀疑精神,来缩小这一鸿沟。如果一个策略在经过PPF的严苛评估后,其结果依然稳健,那么我们就有更强的信心认为,我们找到了一个在未来也可能持续有效的强大策略。

示例

让我们通过两个案例来感受一下PPF的作用。这两个案例的常规利润因子完全相同,但交易样本量的差异,导致了截然不同的PPF结果。

案例一,假设我们的回测结果如下:总利润$10,000,总亏损$8,000,盈利交易89笔,亏损交易70笔,利润因子1.25。该策略的PPF计算结果为0.9982,比其1.25的常规利润因子低了超过0.25。这个悲观调整,直接将一个账面上盈利的策略,打回了亏损的原形。

案例二,我们保持总盈亏不变,但减少交易笔数,再看原始利润因子同为1.25的策略表现如何:盈利交易26笔,亏损交易21笔,总利润$10,000不变,总亏损$8,000不变,利润因子1.25不变。此时,PPF进一步大幅下跌至0.8249(比常规利润因子低了0.425):

样本量的关键性

在上述第二个案例中,PPF值出现了显著下滑。其背后的数学原理如下:

PPF通过减去盈利笔数的平方根来调低总利润,并通过加上亏损笔数的平方根来调高总亏损。由于平方根函数的特性,输入值越小,其平方根占输入值的相对比重就越大。因此,当盈利交易的笔数较少时,其平方根的惩罚效应就相对更强。同理,当亏损笔数较少时,其平方根的放大效应也更为明显。

下表展示了在常规利润因子恒定为1.250的情况下,PPF随交易样本量的变化。随着表格中交易笔数的减少,PPF的数值也每况愈下:

盈利交易 亏损交易 利润因子 悲观利润因子 (PPF) 差异 差异 (%)
500 500 1.250 1.143 -0.107 -8.6%
350 350 1.250 1.123 -0.127 -10.1%
200 200 1.250 1.085 -0.165 -13.2%
125 125 1.250 1.045 -0.205 -16.4%
80 80 1.250 0.999 -0.251 -20.1%
50 50 1.250 0.940 -0.310 -24.8%
25 25 1.250 0.833 -0.417 -33.3%
10 10 1.250 0.649 -0.601 -48.1%

在数据的第一行,当交易样本充足时,PPF仍有1.143。但当表格拉到最后,极小的交易量将PPF打压至0.649,比常规利润因子恶化了48.1%。

值得注意的是,常规利润因子完全不受样本量影响。即便只有20笔交易,1.250的利润因子依然自信地宣告策略是盈利的。对于如此少的交易数据而言,这种结论未免过于乐观。

自定义PineScript函数

让我们看看如何在TradingView策略中计算悲观利润因子。以下自定义函数可以返回这个经过调整的利润因子:

// PessimisticProfitFactor() 函数返回策略的悲观利润因子,
// 计算结果已包含佣金和滑点(若有设置)的影响。
// 注意:当策略尚未产生任何亏损交易时,本函数将返回'na'(不可用)。
PessimisticProfitFactor() =>
    adjustedGrossProfit = (strategy.grossprofit / strategy.wintrades) * (strategy.wintrades - math.sqrt(strategy.wintrades))
    adjustedGrossLoss = (strategy.grossloss / strategy.losstrades) * (strategy.losstrades + math.sqrt(strategy.losstrades))
    adjustedGrossProfit / adjustedGrossLoss

这个 PessimisticProfitFactor() 函数的计算分为三步。第一步,调整总利润:我们先计算平均单笔盈利(strategy.grossprofit / strategy.wintrades),然后将其乘以盈利笔数(strategy.wintrades)减去其平方根(math.sqrt())的结果,从而得到悲观调整后的总利润。

第二步,我们用类似的方法处理总亏损:计算平均单笔亏损(strategy.grossloss / strategy.losstrades),然后将其乘以亏损笔数(strategy.losstrades)加上其平方根的结果,得到调整后的总亏损。

第三步,也是最后一步,将调整后的总利润(adjustedGrossProfit)除以调整后的总亏损(adjustedGrossLoss),最终结果即为悲观利润因子,并由函数返回。

需要注意的是,在策略尚未完成至少一笔盈利和一笔亏损交易之前,PessimisticProfitFactor() 函数会返回 na 值。我们需要累积一定的交易笔数,才能获得有实际意义的PPF值。

函数应用示例

将上述函数代码复制粘贴到我们的策略脚本后,我们便能以多种方式来运用它。若要获取策略当前的PPF值,只需直接调用函数即可:

// 获取当前周期的悲观利润因子
ppfResult = PessimisticProfitFactor()

随着策略在图表上逐根K线运行,PPF会根据策略表现动态变化。为了直观地观察这一变化过程,我们可以使用 plot() 函数将其绘制出来,例如,绘制成柱状图:

// 将当前的悲观利润因子以柱状图形式展示
plot(PessimisticProfitFactor(), style=plot.style_columns, title="PPF")

PessimisticProfitFactor() 函数在每根K线上都返回一个值,因此我们可以方便地比较当前值与历史值,从而判断PPF的变化趋势。例如,要检查PPF是否有所改善,可以这样写:

// 检查悲观利润因子在过去20根K线内是否有所提升
if PessimisticProfitFactor() > PessimisticProfitFactor()[1]
    // 当我们得知PPF提升后,可以在此执行相应操作
    label.new(bar_index, high, text="PPF有所改善!")

在回测过程中,策略甚至可以利用PPF值来进行动态决策,从而根据自身表现的好坏进行调整。假设我们希望在PPF低于0.8时自动减小下单量,可以这样实现:

defaultOrderSize = 5

// 当悲观利润因子低于0.8时,
// 采用更小的默认下单量
if PessimisticProfitFactor() < 0.8
    defaultOrderSize := 3

// 当收盘价突破过去20根K线的最高点时,
// 以指定的仓位大小建立多头头寸
if close > ta.highest(close, 20)[1]
    strategy.entry("Enter Long", strategy.long, qty=defaultOrderSize)

PessimisticProfitFactor() 函数返回的数值,同样可以与其他PineScript函数结合使用,这为我们分析和利用PPF创造了更多可能。例如,若想捕捉PPF上穿1.0的关键时刻,可以使用 ta.crossover() 函数:

// 检查悲观利润因子是否上穿了1.0
if ta.crossover(PessimisticProfitFactor(), 1)
    label.new(bar_index, high, text="PPF上穿1.0!")

策略范例

让我们通过一个完整的策略,来观察悲观利润因子在实际应用中的效果。以下脚本是一个突破策略:当价格向上突破近期高点时做多,当价格向下跌破近期低点时做空。

在图表末尾,当策略回测结束后,脚本会自动创建一个文本标签,用以展示策略的常规利润因子和悲观利润因子。策略的完整代码如下:

//@version=5
strategy(title="悲观利润因子 (PPF) 示例", overlay=true)

// PessimisticProfitFactor() 函数返回策略的悲观利润因子,
// 计算结果已包含佣金和滑点(若有设置)的影响。
// 注意:当策略尚未产生任何亏损交易时,本函数将返回'na'(不可用)。
PessimisticProfitFactor() =>
    adjustedGrossProfit = (strategy.grossprofit / strategy.wintrades) * (strategy.wintrades - math.sqrt(strategy.wintrades))
    adjustedGrossLoss = (strategy.grossloss / strategy.losstrades) * (strategy.losstrades + math.sqrt(strategy.losstrades))
    adjustedGrossProfit / adjustedGrossLoss

// ProfitFactor() 函数返回策略当前的常规利润因子,
// 结果已包含佣金和滑点。
// 注意:若无亏损交易,总亏损为零,函数将返回'na'。
ProfitFactor() =>
    strategy.grossprofit / strategy.grossloss

// 计算并绘制近期高点与低点
highestHigh = ta.highest(high, 20)[1]
lowestLow   = ta.lowest(low, 20)[1]
plot(highestHigh, color=color.green, title="近期高点")
plot(lowestLow, color=color.red, title="近期低点")

// 生成多头与空头交易信号
if high > highestHigh
    strategy.entry("Enter Long", strategy.long)

if low < lowestLow
    strategy.entry("Enter Short", strategy.short)

// 在最后一条确认的历史K线上,创建标签以显示
// 常规利润因子和悲观利润因子
if barstate.islastconfirmedhistory
    label.new(bar_index + 3, hl2, style=label.style_label_left,
         color=#CCCCFF, text="常规利润因子:\n" + 
             str.tostring(ProfitFactor(), "0.000") + 
             "\n悲观利润因子:\n" + 
             str.tostring(PessimisticProfitFactor(), "0.000"))

我们从 strategy() 函数开始,通过 title 参数为脚本命名,并通过 overlay=true 使脚本叠加显示在主图表上。然后,我们引入了前面讨论过的 PessimisticProfitFactor() 函数,并额外增加了一个自定义的 ProfitFactor() 函数,用以返回策略的常规利润因子。

接下来,ta.highest()ta.lowest() 函数分别计算过去20根K线的最高高点和最低低点。plot() 函数则将这两条线绘制在图表上。由于未指定绘图样式,它们会以默认的线形图显示,高点为绿色,低点为红色。

随后是交易信号的生成逻辑。第一个 if 语句判断当前K线的最高价是否突破了前20周期的最高点,若是,则通过 strategy.entry() 建立多头仓位(strategy.long)。第二个 if 语句则判断当前K线的最低价是否跌破了前20周期的最低点,若是,则建立空头仓位(strategy.short)。

策略的最后一部分代码,负责输出常规利润因子和悲观利润因子:

// 在最后一条确认的历史K线上,创建标签以显示
// 常规利润因子和悲观利润因子
if barstate.islastconfirmedhistory
    label.new(bar_index + 3, hl2, style=label.style_label_left,
         color=#CCCCFF, text="常规利润因子:\n" + 
             str.tostring(ProfitFactor(), "0.000") + 
             "\n悲观利润因子:\n" + 
             str.tostring(PessimisticProfitFactor(), "0.000"))

此处的 if 语句通过 barstate.islastconfirmedhistory 判断策略是否运行到了最后一条历史K线。如果是,label.new() 函数便会创建一个文本标签。

该标签被设置在当前K线向右偏移3根K线的位置(bar_index + 3),垂直位置在K线的中间价(hl2)。标签箭头朝左(label.style_label_left),背景色为 #CCCCFF(一种淡紫色)。

标签的文本内容,是将两个字符串与 ProfitFactor()PessimisticProfitFactor() 函数返回的数值拼接而成。由于 label.new()text 参数需要字符串格式,我们使用 str.tostring() 函数将这两个函数返回的数值转换为文本。

当我们将该策略加载到图表上,最终显示的带有悲观利润因子的标签,效果如下:

简单总结一下:悲观利润因子的核心假设是,策略在未来实盘交易中的总亏损会比(经过优化的)历史回测更高,而总利润则会更低。其计算方法是,在原总利润的基础上减去一个与盈利笔数的平方根相关的数值,同时在原总亏损的基础上增加一个与亏损笔数的平方根相关的数值。最终得到的是一个经过数学调整、数值更低的利润因子,该因子同时能有效惩罚那些交易样本量过小的策略。

赞(0)
未经允许不得转载:图道交易 » Pine Script(237):策略的盈利因子与悲观盈利因子PPF
分享到

评论 抢沙发

登录

找回密码

注册