根据今年Crypto的表现,其实从CoinGlass的持仓&多空比&资金费率、聚合现货订单薄或者是全网期货持仓、链上数据、稳定币发行量、市占率等数据,这些东西对于行情来说并没有指示,只能起事后分析的作用,看多了反而使信息接受冗余。


虽然很多策略从三四年前甚至更远开始算回测,可以有N倍收益,但如果只是测试从3月14日到现在这半年内的数据,你很难看到有表现好的。


是的,意思就是如果在2015年投入$1,今天将收获$10,116USD。

但实际上从2024年初至今总收益仅为140%,更不用提利润在2021~2022横盘了一整年。

根据我看过一些策略的实际情况,经常发现利润回撤和修复的时间相当久。

于是窝老另写Strategy做BTC回测,结果超乎预料:

历史回测开始时间结束时间
回测&交易范围2024-01-01 08:002024-09-18 00:00

可以看到,这个策略不管遇上暴涨暴跌的单边行情,还是来回洗的横盘震荡,都可以稳定盈利(也就是低点不断抬高)。

而最大回撤是9.45%,这个利润/亏损比相当优秀,因为策略的数据表现优异(特别是最大回撤控制)意味着可以加大风险来换取超额复利利润。

DetailsVer. IVer. IIVer. IIIVer. IVVer. Ⅴ
净利润211.79%183.62%218.15%260.64%279.78%
最大亏损9.45%7.95%6.63%6.63%6.63%
胜率66.67%57.34%53.52%55.48%56.08%
夏普比率1.881.4831.8431.7911.678
索提诺比率N/AN/AN/AN/AN/A
盈利因子2.262.2192.3212.5392.652

设定两倍杠杆:

当杠杆x2,虽然此时的理论最高回撤相比原来提升了一倍,但因为杠杆*胜率*多次交易积累所带来的复利效应的原因,收益由211.79%超额提升到了779.78%。

由于极高的净利润/最大亏损比,所以数据层面的损失很小:

DetailsVer. IVer. IIVer. IIIVer. IVVer. Ⅴ
净利润779.78%643.80%827.34%1,089.60%1,216.98%
最大亏损18.56%15.50%12.93%12.93%12.93%
胜率66.67%57.34%53.52%55.48%56.08%
夏普比率1.7661.4331.7651.6841.583
索提诺比率N/AN/AN/AN/AN/A
盈利因子2.2262.0862.252.4592.63

Post Script:收益曲线顺滑版(单利)↓

DetailsVer. IVer. IIVer. IIIVer. IVVer. Ⅴ
净利润500.41%704.50%722.08%797.83%820.88%
最大亏损18.00%19.64%18.13%18.13%18.13%
胜率66.67%57.34%53.52%55.48%56.08%
夏普比率1.3930.780.8830.9230.861
索提诺比率N/AN/AN/AN/AN/A
盈利因子2.2952.4022.4272.6422.682

平衡最大可接受回撤的杠杆:

可以看到,继续放大回撤风险的情况下,收益直接来到了13,547.06%,只算纯利润是1倍杠杆情况下的63.96倍(不考虑投资基数),而最大回撤仅仅为原来(9.45%)的5.27倍。

这就是高胜率,优秀数据和风险控制好的情况下,放大杠杆带来的复利效应;坏处是随时有可能一笔交易让你亏损超过20%。

不过就超过2/3的胜率,以及各项优秀数据来看,你确实可以说,这种杠杆其实是可以上实盘的。

因为量化策略要解决的问题就是:

如何在控制亏损的情况下,最多的保留策略运行过程中获得的利润。

如果你的胜率足够高,能够有效限制最大回撤,开高杠杆(其实图中杠杆也不算高)是完全可行的。

DetailsVer. IVer. IIVer. IIIVer. IVVer. Ⅴ
净利润13,547.06%44,077.06%173,836.83%423,370.89%625,045. 00%
最大亏损49.86%49.86%49.85%49.85%49.85%
胜率66.67%57.34%53.52%55.48%56.08%
夏普比率1.4191.1921.3991.0971.048
索提诺比率N/A36.147N/AN/AN/A
盈利因子2.0511.7312.0272.1752.734

DetailsVer. IVer. IIVer. IIIVer. IVVer. Ⅴ
净利润26,290.76%123,504.04%485,789.49%1,497,634.32%2,393,310. 15%
最大亏损59.87%59.84%59.86%59.86%59.86%
胜率66.67%57.34%53.52%55.48%56.08%
夏普比率1.3151.1281.3320.9510.912
索提诺比率N/A27.655N/AN/AN/A
盈利因子1.9821.6531.9672.1162.783

数据堆彻

前面可以看到,在1倍杠杆的时候,夏普比率还能接近1.9,随着杠杆拉升,当回撤放大至60%,数据同时也降低到了1.3。

那么,有没有夏普比率奇高的策略呢?

窝老刚好也写出了一种可能:

1倍杠杆的情况下,和前面的对比其实没有区别,但是:

不夸张的说,就我有限的认知来看,这的确是我见过数据最好的Pine Script策略。

并且,这个版本的策略无限拉高杠杆时数据损失也很小:

直接回撤80%,看一眼数据:

这个夏普比率是怎么能维持在1.8的?

毕竟,前面的版本,回撤放大到60%,Sharpe就已经降到1.3了…


隐性未来预见:

量化策略回测中有很多陷阱,其中「未来函数」就是最容易导致误解的。

未来函数会引发重绘(Repainting),指的是图表数据后指针或是策略进出场点不一致,如果代码里有参数引发了重绘,策略的收益就会相差几倍。

也许不是刻舟求剑式指定历史高低点的过拟合来优化策略,但是不经意间会导致疏忽。

举例:参数「calc_on_order_fills」如果你设置为勾选,那么每一次的进场点会从OPEN而不是CLOSE计算,结果就是每一次交易至少有0.*%甚至*%的领先优势,以此为计算的亏损也会更少。

运行时如果你的代码有重绘的可能就会引发提示,同一个策略上引用未来函数会让收益率大幅增长。

比如:

如图所示,引用未来函数之后,净利润从1,100.09%直飙到1,930.45%,不仅改变了部分曲线,也放大了收益,显然脱离了真实情况,实盘运行起来的收益更接近图1。


量化策略的好处也许就在于:

你不用特别去关注Fed的动向、加息降息周期、扩表缩表进程、TGA抽取ON RRP的流动性给市场放水、耶伦偏短期的调整债券发行方式、美元指数、国债收益率、宏观经济以及美国通胀、失业率、泡沫化水平和市场流动性等数据。

也不会被4月13日凌晨的以色列导弹袭击以及7月31日BOJ加息导致的日元Carry Trade套利交易平仓带来的暴跌影响。

所有这些,5月1日洗盘,和5月20日放出的ETH ETF炒作消息的拉盘,都不会在情绪上另外作用到策略。

所谓量化策略,就是「计划你的交易,交易你的计划」。

忽略情绪,所有逻辑都基于冰冷的代码来执行。

完美符合经济学意义上的纯粹理性人。

3 responses to “Pine Script 策略”

  1. wp178245861 Avatar
    wp178245861

    量化和AI的普及终将改变整个金融市场的交易方式,遗憾的是大部分普通参与者都会成为时代的垫脚石。最近本人在大A为国化债,被庄家和量化反复收割,属实让人泪目。本着打不过就加入的心态开始学习Pine Script,但仓促间写成的代码回测总是惨不忍睹,又不死心,于是斗胆向博主请教Strategy,能否分享源码,以资共勉。
    感激不敬。

    此致 !

    敬礼!

    PowerArmor
    [email protected]

    1. Keihou Shuu Avatar
      Keihou Shuu

      我觉得你可以在Pine Script策略最下方加入如下代码来显示阶段收益,这样能更好优化:
      // 新增:每月收益率显示
      showMonthlyReturn = input.bool(true, “Show monthly earnings”, group = groupParameterSettings)
      fontSizeOptions = input.string(“large”, title = “Font Size”, options = [“tiny”, “small”, “normal”, “large”, “huge”], group = groupParameterSettings)

      var month_return = map.new<int, float>()
      var month_strat_money = map.new<int, float>()
      var years = array.new()

      int now = year * 10000 + month
      flag = true
      for mon in map.keys(month_strat_money)
      if now == mon
      flag := false
      if flag == true
      map.put(month_strat_money, now, strategy.equity)
      start_money = map.get(month_strat_money, now)
      map.put(month_return, now, (strategy.equity / start_money – 1.0) * 100)
      if years.includes(year) == false
      years.push(year)
      labels = array.from(“”, “Jan”, “Feb”, “Mar”, “Apr”, “May”, “Jun”, “Jul”, “Aug”, “Sep”, “Oct”, “Nov”, “Dec”, “Q1”, “Q2”, “Q3”, “Q4”, “Annual Return”)
      make_table(years, labels) =>
      rows = array.size(years) + 1
      columns = array.size(labels)
      backgroundColor = color.new(color.rgb(212, 212, 212), 0)
      winColor = color.rgb(81, 127, 70)
      lossColor = color.rgb(167, 76, 70)
      e = table.new(position.bottom_right, rows = rows, columns = columns, bgcolor = backgroundColor, border_width = 2, border_color = color.black, frame_width = 2, frame_color = color.black)
      for i = 0 to rows – 1
      for j = 0 to columns – 1
      table.cell_set_text_color(e, j, i, color.black)
      table.cell_set_text_size(e, j, i, fontSizeOptions)
      if i > 0 and j > 0
      table.cell_set_bgcolor(e, j, i, color.rgb(161, 162, 165))
      table.cell_set_text_color(e, j, i, color.white)
      for j = 0 to columns – 1
      table.cell_set_text(e, j, 0, array.get(labels, j))
      for i = 1 to rows – 1
      table.cell_set_text(e, 0, i, str.tostring(array.get(years, i – 1)))
      start_year = array.get(years, 0)
      sum_year = array.new(array.size(years), 1.0)
      quarterly_sum = array.new
      (4 * array.size(years), 1.0)
      for before in map.keys(month_return)
      nowyear = before / 10000
      nowmonth = before % 10000
      beforevalue = array.get(sum_year, nowyear – start_year)
      beforevalue *= (1.0 + map.get(month_return, before) / 100)
      array.set(sum_year, nowyear – start_year, beforevalue)
      return_value = map.get(month_return, before)
      return_value_str = return_value >= 0 ? “+” + str.tostring(return_value, “#.##”) : str.tostring(return_value, “#.##”)
      table.cell_set_bgcolor(e, nowmonth, (nowyear – start_year + 1), return_value >= 0 ? winColor : lossColor)
      table.cell_set_text(e, nowmonth, (nowyear – start_year + 1), return_value_str + “%”) // 添加百分比符号
      // 计算季度收益
      quarter = (nowmonth – 1) / 3
      quarterly_index = (nowyear – start_year) * 4 + quarter
      quarterly_value = array.get(quarterly_sum, quarterly_index)
      quarterly_value *= (1.0 + map.get(month_return, before) / 100)
      array.set(quarterly_sum, quarterly_index, quarterly_value)
      for i = 1 to rows – 1
      return_value = (array.get(sum_year, i – 1) – 1.0) * 100
      return_value_str = return_value >= 0 ? “+” + str.tostring(return_value, “#.##”) : str.tostring(return_value, “#.##”)
      table.cell_set_bgcolor(e, columns – 1, i, return_value >= 0 ? winColor : lossColor)
      table.cell_set_text(e, columns – 1, i, return_value_str + “%”) // 添加百分比符号
      // 显示季度收益
      for q = 0 to 3
      quarterly_return = (array.get(quarterly_sum, (i – 1) * 4 + q) – 1.0) * 100
      quarterly_return_str = quarterly_return >= 0 ? “+” + str.tostring(quarterly_return, “#.##”) : str.tostring(quarterly_return, “#.##”)
      table.cell_set_bgcolor(e, 12 + q + 1, i, quarterly_return >= 0 ? winColor : lossColor)
      table.cell_set_text(e, 12 + q + 1, i, quarterly_return_str + “%”) // 添加百分比符号
      if showMonthlyReturn
      make_table(years, labels)

    2. Keihou Shuu Avatar
      Keihou Shuu

      对了,如果是单利情况下,也就是除了百分比以外还显示详细P&L金额的,可以用这个版本:
      // 新增:每月收益金额显示
      showMonthlyReturn = input.bool(true, “Show monthly earnings”, group = groupParameterSettings)
      fontSizeOptions = input.string(“normal”, title = “Font Size”, options = [“tiny”, “small”, “normal”, “large”, “huge”], group = groupParameterSettings)

      // 初始化变量
      var month_return = map.new<int, float>()
      var month_strat_money = map.new<int, float>()
      var month_return_percent = map.new<int, float>()
      var years = array.new()

      // 计算当前月份的收益
      int now = year * 10000 + month
      if not map.contains(month_strat_money, now)
      map.put(month_strat_money, now, strategy.equity)
      start_money = map.get(month_strat_money, now)
      month_change = strategy.equity – start_money
      month_return_percent_value = (strategy.equity / start_money – 1.0) * 100
      map.put(month_return, now, month_change)
      map.put(month_return_percent, now, month_return_percent_value)
      if not years.includes(year)
      years.push(year)

      // 标签数组
      labels = array.from(“”, “Jan”, “Feb”, “Mar”, “Apr”, “May”, “Jun”, “Jul”, “Aug”, “Sep”, “Oct”, “Nov”, “Dec”, “Q1”, “Q2”, “Q3”, “Q4”, “Annual Return”)

      // 创建表格函数
      make_table(years, labels) =>
      rows = array.size(years) + 1
      columns = array.size(labels)
      backgroundColor = color.new(color.rgb(212, 212, 212), 0)
      winColor = color.rgb(81, 127, 70)
      lossColor = color.rgb(167, 76, 70)
      e = table.new(position.bottom_right, rows = rows, columns = columns, bgcolor = backgroundColor, border_width = 2, border_color = color.black, frame_width = 2, frame_color = color.black)

      // 设置表格样式
      for i = 0 to rows - 1
      for j = 0 to columns - 1
      table.cell_set_text_color(e, j, i, color.black)
      table.cell_set_text_size(e, j, i, fontSizeOptions)
      if i > 0 and j > 0
      table.cell_set_bgcolor(e, j, i, color.rgb(161, 162, 165))
      table.cell_set_text_color(e, j, i, color.white)
      for j = 0 to columns - 1
      table.cell_set_text(e, j, 0, array.get(labels, j))
      for i = 1 to rows - 1
      table.cell_set_text(e, 0, i, str.tostring(array.get(years, i - 1)))

      // 初始化年度和季度收益数组
      start_year = array.get(years, 0)
      sum_year = array.new<float>(array.size(years), 0.0)
      quarterly_sum = array.new<float>(4 * array.size(years), 0.0)
      quarterly_sum_percent = array.new<float>(4 * array.size(years), 1.0)
      annual_sum_percent = array.new<float>(array.size(years), 1.0)

      // 计算收益
      for before in map.keys(month_return)
      nowyear = before / 10000
      nowmonth = before % 10000
      year_index = nowyear - start_year
      quarter = (nowmonth - 1) / 3
      quarterly_index = year_index * 4 + quarter

      // 更新年度收益
      array.set(sum_year, year_index, array.get(sum_year, year_index) + map.get(month_return, before))
      array.set(annual_sum_percent, year_index, array.get(annual_sum_percent, year_index) * (1.0 + map.get(month_return_percent, before) / 100))

      // 更新季度收益
      array.set(quarterly_sum, quarterly_index, array.get(quarterly_sum, quarterly_index) + map.get(month_return, before))
      array.set(quarterly_sum_percent, quarterly_index, array.get(quarterly_sum_percent, quarterly_index) * (1.0 + map.get(month_return_percent, before) / 100))

      // 设置单元格内容
      return_value = map.get(month_return, before)
      return_percent = map.get(month_return_percent, before)
      return_value_str = return_value >= 0 ? "+" + str.tostring(return_value, "#.##") : str.tostring(return_value, "#.##")
      return_percent_str = return_percent >= 0 ? "+" + str.tostring(return_percent, "#.##") : str.tostring(return_percent, "#.##")
      table.cell_set_bgcolor(e, nowmonth, year_index + 1, return_value >= 0 ? winColor : lossColor)
      table.cell_set_text(e, nowmonth, year_index + 1, return_value_str + "\n(" + return_percent_str + "%)")

      // 显示季度和年度收益
      for i = 1 to rows - 1
      year_index = i - 1
      return_value = array.get(sum_year, year_index)
      return_percent = (array.get(annual_sum_percent, year_index) - 1.0) * 100
      return_value_str = return_value >= 0 ? "+" + str.tostring(return_value, "#.##") : str.tostring(return_value, "#.##")
      return_percent_str = return_percent >= 0 ? "+" + str.tostring(return_percent, "#.##") : str.tostring(return_percent, "#.##")
      table.cell_set_bgcolor(e, columns - 1, i, return_value >= 0 ? winColor : lossColor)
      table.cell_set_text(e, columns - 1, i, return_value_str + "\n(" + return_percent_str + "%)")
      for q = 0 to 3
      quarterly_return = array.get(quarterly_sum, year_index * 4 + q)
      quarterly_return_percent = (array.get(quarterly_sum_percent, year_index * 4 + q) - 1.0) * 100
      quarterly_return_str = quarterly_return >= 0 ? "+" + str.tostring(quarterly_return, "#.##") : str.tostring(quarterly_return, "#.##")
      quarterly_return_percent_str = quarterly_return_percent >= 0 ? "+" + str.tostring(quarterly_return_percent, "#.##") : str.tostring(quarterly_return_percent, "#.##")
      table.cell_set_bgcolor(e, 12 + q + 1, i, quarterly_return >= 0 ? winColor : lossColor)
      table.cell_set_text(e, 12 + q + 1, i, quarterly_return_str + "\n(" + quarterly_return_percent_str + "%)")

      if showMonthlyReturn
      make_table(years, labels)

Leave a Reply


Producer's Personal Blog

Copyright © 2024 Keihou Shuu