故障排查 · Pine Script

Pine Script 的 Alert 没有记忆
为什么简单的触发逻辑会让自动化出问题

2026-07-22·10 分钟

策略触发了 alert,webhook 发出去,结果没几秒同一根 K 线上又冒出一笔几乎一样的订单。 或者反过来:你改了开仓条件、保存了脚本,线上那个 alert 却还是照上周的逻辑跑,好像 你什么都没改过一样。这两个都不是随机出问题。

它们的根源是同一件事:Pine Script 的 alert 不会记得自己已经发送过什么——而且更容易被 忽略的是,它跑的根本不是你现在图表上正在看的那份脚本。

如果这支策略是 AI 帮你生成的,它不会主动提醒你这些事。因为这不是语法问题——代码照样编译通过、 回测照样好看,出问题的是 TradingView 执行脚本的方式,以及 alert 的存储机制。 所以这篇不是要教你写 Pine,而是给你三件事:出问题背后的机制、让 AI 生成代码时该贴给它的指令原文, 以及拿到代码后该回头问它哪几个问题。

先说结论: alert()alertcondition() 的触发频率设置,只负责限流「同一根 K 线内这个条件最多可以触发几次」——alert() 是靠代码里的 freq 参数,alertcondition() 本身没有 freq 参数,是在建立 alert 的对话框里选 Frequency。它们不会确认 webhook 有没有送达, 不会帮你去重,也不知道上一根 K 线发生过什么——除非你自己用 var/varip 存起来。而且就算存了,这份记忆也只属于跑在 TradingView 服务器上的 那一个特定 alert 实例,不属于你在图表编辑器里看到的那份脚本。

脚本为什么没有记忆:它每根 K 线都从头跑一次

Pine 不是把你的脚本对着整张图表算一次就结束。TradingView 官方对执行模型的描述是: 脚本会「在历史 K 线与实时 tick 的序列上重复执行……针对每一根 K 线各自计算一次」。 在已经收盘的历史 K 线上,这件事很简单:从最早的 K 线到最新的,依次每根执行一次。

图表最右边那根还没收盘的 K 线——实时 K 线——就不一样了。因为它的 最高/最低/收盘价都还没定案,脚本会在每一个新进来的 tick 上重新跑一次,用最新价格重新算。 每次重跑之前,Pine 都会把这根 K 线上的变量「重置」(rollback)回这个 tick 发生前的状态, 确保上一次的临时性结果不会渗进下一次计算。只有这根 K 线最终、收盘那一个 tick 的结果, 才会真正写进永久的历史数据里。

Strategy 是部分例外:默认情况下,strategy 跟 indicator 不同,即使是实时 K 线也只在收盘后执行一次。要开启 calc_on_every_tick calc_on_order_fills,才会逐 tick 重新计算。

这件事对你下指令的影响是:当你跟 AI 说「同一个形态只准开仓一次」, 它必须自己造一个「记忆」出来,因为这个语言本身没有给它。 它会抓什么来当记忆——一个标志位、一个计数器、记住某根 K 线的编号——都是为了绕过这个执行模型。 下一节就是教你认出你拿到的是哪一种绕法。

AI 给的代码用了 varip,说明它想解决什么

有两个声明关键字决定变量的值会不会跨执行保留下来,区别正好对应上面讲的 rollback 机制。 你看这张表不是为了自己写得出来,而是为了:这两个词出现在生成的代码里时, 你知道 AI 当时在试图解决什么。

声明方式什么时候第一次设定会不会跨 K 线保留实时 K 线收盘前改动会不会留住
(不加关键字)每次执行不会——每次都重新初始化不会
var该 K 线收盘 tick 第一次执行时不会——会被还原回上一次确认收盘的状态
varip第一次执行,即使是收盘前会——不受 rollback 影响

这张表是给你对照用的,不用背。

在历史 K 线上这个区别完全看不出来,因为每根本来就只执行一次——var varip 在那里表现一模一样。区别只会在实时 K 线上体现出来:你在收盘前用 var 改的标志位,下一个 tick 一进来就悄悄还原,只有收盘那一刻的版本才会真正定下来。 而 strategy 因为默认每根 K 线(含实时)只执行一次,除非开启 calc_on_every_tickcalc_on_order_fills,否则 varip 的表现会跟 var 一样——这点很容易让人误以为 varip 永远比 var 更「实时」。

所以你该做什么?看到生成的代码里有 varip, 通常说明 AI 想跨 tick 记住某个状态——而在 alert 这个场景下,「跨 tick 记东西」 常常是用错的方法回答错的问题。直接问它:「这个状态在 alert 触发之后还可靠吗?我把 alert 删掉重建之后, 它在第一根 K 线上的值是多少?」如果回答完全没提到「会归零」,那就是你该追问下去的点。 任何拿 var 标志位来当「不要重复触发」防护的写法,同样问这一题。

如果对声明和执行的基础还不熟,可以先看我们的 Pine Script 入门教程——不过这篇剩下的内容,不看那篇也读得完。

直接贴这段指令,多数坑就不会被写进去

把下面这段当作「需求区块」,贴在你自己的策略描述上面。它刻意要求 AI 逐条回报, 而不是让它默默按自己的想法做:

text
帮我写一支 TradingView 用的 Pine Script v6 策略。这支策略会接 webhook 去下真实订单,
所以「可靠」比「漂亮」重要。

策略逻辑:[用白话描述你的开仓与平仓条件]

以下是硬性要求。全部照做,然后逐条编号回报你怎么处理的:

1. 用 strategy() 声明,不要用 indicator()。

2. 所有开仓与平仓条件,只能在「已确认的 K 线」上判断。
   用 barstate.isconfirmed 把关。然后列出脚本里还有哪些条件是在读
   「还没收盘那根 K 线」的 close / high / low。

3. 不要用 var 或 varip 来记住「这个信号我已经发过了」。
   如果你认为逻辑上非要这种记忆不可,先停下来跟我说明:
   当我把 alert 删掉、重新建立一个新的之后,那个标志位的值会是多少。

4. Alert message 必须是「单行、合法的 JSON」,不能有换行,
   字段就这几个:[贴上你接收端要求的字段清单]

5. 最后列出脚本里所有「实时 K 线行为跟历史 K 线行为不一样」的地方。

第 3 条是最有价值的一条。好的回答会告诉你「新建 alert 之后那个标志位会从零开始」; 如果它只回你一句「对,var 会保留」——这句话字面没错,但实务上会害你, 而且正好告诉你:去重这件事得放到脚本以外的地方做。

第 4 条那串 JSON,最后要贴在哪里?
三个位置,选错就是「alert 设好了、webhook 却收到空的 payload」:
一、代码里的 alert() 调用——消息由程序当场拼出来, 你不必在对话框里再打一次。
二、下单函数的 alert_message= 参数——这段文字只有在成交事件 触发、且消息里用了 {{strategy.order.alert_message}} 时才会被读出来。
三、TradingView「Create Alert」对话框的 Message 栏——这是唯一能用 {{strategy.*}} placeholder 的地方,前提是这个 alert 建立在 strategy 上。
界面上要点哪里(图表右上时钟 → Add Alert → Condition 选你的策略 → Notifications 勾 Webhook URL),在 用 Claude 写 Pine 那篇有逐步走过一遍。

线上跑的 alert,不是你现在看到的那份代码

这是最容易让人意外的地方——很多人以为「alert 就等于我的脚本」,但其实不是。 当你点击 Create Alert 的那一刻,TradingView 会把脚本、输入参数、图表标的/周期做成一份快照, 之后这份快照就在它自己的服务器上独立运行。官方文档说得很明确:「之后对脚本输入或图表的 修改,不会影响已经根据它们创建的、正在运行的 alert。」FAQ 被问到「改脚本会不会让正在跑 的 alert 跟着变」时回答更直接:「不会,除非你重新创建一个 alert……要让修改生效,必须删除 现有的 alert、重新创建一个新的。」

这对任何你用 var/varip 存起来追踪状态的东西(比如一个「这个 信号是不是已经发过」的标志位)有直接影响:这份记忆属于 alert 正在跑的那份冻结快照, 不属于你在图表面板里持续修改的脚本。你尽管改脚本——线上那个 alert 内部的计数器还是照旧逻辑跑, 直到你把它删掉重建。而你重建之后,那就是一份全新的快照:它的 var/varip 状态从零开始,重新用历史数据算一遍,完全不会继承旧 alert 实例累积过的 任何记忆。(这最后一句结论官方文档没有用单独一句话直接讲出来——它是把上面「快照机制」跟 下一节「脚本重启」两段官方陈述结合起来推导出的结果。)

你改了什么正在跑的 alert 会跟着变吗?官方依据
图表编辑器里的脚本代码不会「Subsequent changes to your script's inputs or the chart will thus not affect running alerts previously created from them.」
脚本的输入参数 / 图表品种或周期不会同一句官方原文——输入参数与图表本来就在创建当下被一并快照起来。
把 alert 删掉重建会——但那是一份全新的快照「To update an alert after making changes, delete the existing alert and create a new one.」它的 var/varip 状态会从零开始。

引用内容为 TradingView 官方 alerts 文档与 FAQ 的逐字原文。

这一段是最多人跟 AI 耗掉时间的地方。你把脚本贴回去、它找出问题、 你改好了、盯着线上看——结果什么都没变。不是因为改错了, 而是因为那个修改根本没有送到「正在跑的那个东西」上。再怎么下指令也到不了一个已经在跑的 alert。 唯一的路径是:改脚本 → 把 alert 删掉 → 重建一个新的。 这件事值得写在便签上,因为它看起来实在太像「AI 没修好」了。

「触发了、信号又不见了」:先检查这个

TradingView 对 repainting 的定义很直白:「脚本在历史计算跟实时计算/绘图之间表现不同的行为」。 这不代表它自动就是坏事——官方把某些 repainting(比如 close 在还没收盘的 K 线上会浮动)归类为「普遍但通常可接受」,但另一些类型则被标为「可能误导」甚至「不可接受」 (用实时的 K 线内数据去触发 alert 或下单,就属于不可接受的那一类)。

「触发了、又消失」这个现象背后的机制,官方称之为 fluid data(浮动数据):历史 K 线只会 存最终的 OHLC,但实时 K 线的最高/最低/收盘价在收盘前会随每个 tick 变动。如果你的条件式检查的是 当下的 closehigh,它可能在某个 tick 成立、alert 触发了, 但等 K 线真正收盘时条件已经不成立——alert 已经发出去了,下次刷新画面时,条件却看起来 「根本没发生过」。

官方给的正式解法是强制等 K 线收盘才判断:把 alert 频率设成Once Per Bar Close,或在代码里用 barstate.isconfirmed 挡住条件。TradingView 也坦白说这是有代价的取舍:「这些方法虽然能避免 repainting,但也会让 信号比会 repaint 的脚本更晚触发……鱼与熊掌不可兼得。」这种「收盘 vs 实时」的张力,也是同一个 alert 有时候测起来几乎瞬间送达、有时候又明显变慢的部分原因——我们的webhook 延迟拆解文讲清楚了哪些是你能调的、哪些是 TradingView 自身基础设施的部分。

落到实务:Frequency 那个下拉菜单是你自己在浏览器里点的,十秒钟的事。 其他部分则是「要交代给写代码的那一方」的指令——「所有条件都用 barstate.isconfirmed 把关,然后告诉我你改了哪几条」——这也正是上面那段指令第 2 条的内容。

把代码贴回去,问它这四个问题

不管脚本是 AI 生成的、论坛抄的,还是你自己几个月前写的,这一轮都要跑。 刻意先不让它改,是为了让你看出它到底有没有搞懂情况,再让它动手:

text
这是一支 Pine Script 策略。我要拿它建 TradingView alert 去接 webhook 实盘下单。
先不要改写,先回答我这四题:

1. 这支脚本里,有哪些条件可能「K 线中途成立、收盘时又不成立」?逐行列出来。

2. 这里面有没有用 var 或 varip 来避免重复触发?
   如果有:我把 alert 删掉、重新建一个之后,那个变量在第一根 K 线上的值是多少?

3. 如果我把 alert 的 Frequency 设成「Once Per Bar Close」,
   这支脚本的哪些逻辑行为会改变、哪些不会?

4. 这支脚本里有没有任何地方,是假设「我的服务器已经收到上一条 alert」?
   回答有或没有,并指出是哪一行。

以上都回答完,再给我修正版。

[把你的脚本贴在这里]

第 4 题是故意设的陷阱,正确答案是「没有」——Pine 脚本根本无从知道这件事。 如果它回答有,或是主动提议在脚本里加一套「送达确认」机制, 那这条回复里的其他内容你都要多留一分怀疑。 这篇最后一节会说明为什么 Pine 里没有东西能回答这个问题。

对照你的症状,找出对应的机制

把上面几件事串起来——逐根 K 线执行、var/varip 的作用范围、被冻结的 alert 快照、repainting—— 多数「我的自动化莫名其妙出问题」的反馈,其实都能对应到下面几种机制之一。

1
同一根 K 线上收到重复订单?
频率设成 All(alert.freq_all)这个设置本来就是「条件成立的每一个 tick 都发」,所以同一根 K 线发好几次是预期行为。Once Per Bar 的官方定义是「同一根实时 K 线只有第一次调用会触发 alert」,一根最多发一次;Once Per Bar Close 则是等收盘确认。
Alert 来自 strategy 的成交事件同一根 K 线本来就可能产生好几笔委托成交——入场单加上止损或止盈出场单都在同一根成交——每一笔成交各自发一条消息。这不是重复,是这根 K 线真的产生了多笔订单。
你的接收端回了 5xxTradingView 会在 5 秒后重发同一条通知,最多重发 3 次,等于同一次触发最多发 4 次(状态码 504 不在此列)。接收端如果已经收下 payload 却仍回 500,同一个信号就会进来四次。正确做法是收下后立刻回 2xx,慢的工作丢到后台异步处理。
2
你原本预期会出现的信号,却完全没出现?
Alert 最近被重建过任何用来记录「这个信号是不是已经发过」的 var/varip 计数器,在新快照创建时就归零了——这不是出问题,是一个从历史数据重新算起的全新实例。
脚本在 alert 创建之后被改过去 Alert Manager 对照 Condition 显示的内容跟你现在的脚本版本。两者对不上,说明线上的 alert 还在跑旧的快照——删掉重建。
3
线上的实际行为跟你现在编辑器看到的不一样?
确认对不上这是预期行为,不是 bug。改图表上的脚本,永远不会反映到已经根据它创建好的 alert 上——见上面「冻结副本」那段。
4
回测看起来很干净,但实盘信号乱七八糟?
条件式里有 repainting 成分回测本来就会出现一些 repainting,但实盘会在 K 线真正收盘前多出额外的假触发。改成 Once Per Bar Close,观察几个交易时段再比较。

上线前的自检清单,以及脚本永远做不到的那一件事

在你把这套东西指向真钱之前,把下面这张清单走一遍。 每一项都是你自己就能确认的——不是在浏览器上点一下,就是直接问 AI 一个问题, 完全不需要你会写 Pine:

  • alert 的 Frequency 设成 Once Per Bar Close,不是只设 Once Per Bar——光是这一点就能消掉多数由 repainting 造成的重复触发。
  • 任何会用到 close/high/low 的条件式, 都用 barstate.isconfirmed 挡住。
  • 你知道 alert 上次是什么时候重建的,能拿这个时间点跟你最后一次改脚本的时间对比。
  • var/varip 标志位做「不要重复触发」的防护时,清楚知道 alert 重建就会归零——不是只有图表刷新才会归零。
  • 你的接收端/执行端自己保留每一笔实际收到的 payload 记录,因为 TradingView 不会帮你留。

有一件事 Pine 真的给不了你:确认你发出的 webhook 有没有被下游收到并处理。 alert()alertcondition() 从设计上就是 fire-and-forget—— TradingView 的文档详细描述了频率跟触发机制,但完全没有任何一句话说 alert 引擎会跟踪 或确认接收端服务器对这条消息做了什么。同一个信号在下游是不是被处理了两次,不是 Pine 执行环境设计上会知道的事。

这其实是执行端的问题,不是 Pine 的问题。TVSBot 的做法是把收到的信号丢进后台异步处理, 并把每一条信号连同原始 payload 与完整处理记录存下来,逐条可查、可 Replay—— 让你能实际核对是 TradingView 真的发了两次,还是重复发生在链路的其他环节。

但要讲清楚:那是事后查得到,不是自动去重。真正要修复 alert 逻辑本身不可靠的问题, 还是得从上面那份 Pine 端清单开始。

常见问题

为什么 alert 在 K 线中途触发了,条件后来又消失了?
因为 alert 的 Frequency 设成 Once Per Bar(而不是 Once Per Bar Close),同时条件式检查的是 closehigh 这种在 K 线收盘前还会变动的值——这是一种 repainting。条件在 K 线中途看起来成立、 触发了 alert,等最后一个 tick 落地后又不成立了。这种情况下 Once Per Bar 并不会发两次, 官方文档写的是「同一根实时 K 线只有第一次调用会触发 alert」;问题在于那唯一一次发出, 依据的是一个撑不到收盘的数值。把 Frequency 改成 Once Per Bar Close,或在脚本里用 barstate.isconfirmed 把关,可以强制等收盘确认才判断。
我明明改了 Pine 脚本,为什么线上的 alert 还是照旧逻辑跑?
TradingView 在你创建 alert 的那一刻,会把脚本、输入参数与图表当时的状态存成一份快照。 根据 TradingView 自己的 FAQ,之后对脚本或图表的修改「不会影响已经创建好的 alert」—— 你必须删掉它、重新创建一个,修改才会生效。
var 变量存的计数器,是不是永久安全不会丢失?
只在那一次脚本执行(对正在跑的 alert 来说,就是那一份特定快照)存活期间有效。 它会跨历史与实时 K 线保留下来,但只要脚本重启(改输入值后重新编译、或把 alert 删掉重建),var 就会针对历史数据集从头重新初始化一遍。
AI 在我的策略里用了 varip,需要担心吗?
不一定,但值得问清楚它拿来做什么,因为这两个关键字只差在一件事上。var 的更改只有在 K 线收盘那个 tick 才会真正生效——在还没收盘的 实时 K 线中途做的改动,下一个 tick 一来就会被还原。varip 则是设定当下就立刻保留,逐 tick 都不受 rollback 影响。在历史 K 线上两者没有可见的区别, 因为那里每根本来就只执行一次。所以生成的代码里出现 varip, 通常是想跨 tick 保留某个状态——去问它保留的是什么状态、alert 重建之后那个值还剩多少。
我能确定 TradingView 只发了一次 webhook 吗?
不能——而且官方文档明载了一种一定会重发的情况:如果你的接收端返回 500 到 599 之间的状态码(504 除外),这条通知会在 5 秒后重发,最多重发 3 次,等于同一次触发最多发 4 次。反方向则没有任何「送达确认」机制,而 Frequency 设置只控制条件可以触发几次,不代表接收端真的只处理了一次那条消息。实务上比较可靠的 做法,是在你自己的接收/执行端把每一笔实际收到的 payload 都记录下来,拿这份记录当 依据去核对。

Get started

想把今天学到的东西自动化跑起来?

TVSBot 把 TradingView 的 alert 转成跨 7 家交易所的自动下单——非托管、用你自己的 API key,搭配后台异步信号处理与逐条可查的 Replay 记录,让你清楚知道每一笔信号实际收到了什么。

免费开始使用