ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

TradingView警报批量添加:3Commas信号对接与Playwright自动化实战

TradingView警报批量添加:3Commas信号对接与Playwright自动化实战 简介面向自动化交易场景的TypeScript开源工具专为3Commas用户解决TradingView缺少批量构建警报API的痛点。工具基于Puppeteer驱动Chromium浏览器按配置自动将自定义警报逐批写入账户免去手动维护数十乃至数百个交易对警报的重复劳动适合同时监控多标的的进阶量化开发者。源码结构清晰核心模块集中在src目录的add-tradingview-alerts、tv-page-actions与fetch-pairs等文件配合package.json、tsconfig.json可快速构建deploy_master.sh则提供一键部署能力。整个压缩包共18个文件容量约13.9MB涵盖TypeScript源码、JSON配置、演示GIF与README文档其中alert_tool_demo.gif完整演示了自动化添加警报过程blacklist.csv与config.example.yml可帮助用户配置排除列表及运行参数。资源已有1446人学习适合具备一定TypeScript基础、希望提升TradingView警报维护效率的开发者参考。1. 批量警报工具到底解决什么从手动重复到 3Commas 信号自动化先说一个具体场景你在 3Commas 里建好了信号类 bot策略跑起来完全没问题剩下最枯燥的一步是把十几个甚至几十个交易对逐个添加到 TradingView 的警报里。打开图表、右键、选条件、填触发价、粘贴 Webhook URL、再粘贴一长串 JSON 消息体点确定然后换下一个。这套动作重复二十次以后没人能保证每个交易对的 URL 和消息体都没贴错。标题里这类批量添加警报的工具就是把这段手工作业变成可重复执行的脚本任务它专门为 3Commas 的 TV 警报集成设计意味着不只是“帮你点几下创建按钮”还要按 3Commas 要求的消息体字段把 JSON 渲染好、塞进正确的 Webhook 输入框。这篇笔记从 3Commas 的警报对接格式讲起再到浏览器自动化的最小脚本、参数设置、避坑记录最后落到上线前的验证习惯适合正在被重复操作和隐性错配折磨的人。2. 3Commas 的 TV 警报集成要什么Webhook URL 与消息体结构2.1 先在 3Commas 端把三个值拿齐很多人以为“拿到一个 Webhook URL 就能收工”这是最容易翻车的第一步。3Commas 里每个 TradingView 信号 bot 在创建时后台会单独生成一个专属的 Webhook URL同时配套一串“发给 TradingView 时该用的 payload 模板”。我一般会先把这三样东西复制到本地文件里后面批量脚本的所有配置都以它们为准Webhook URL、bot_id、email_token。需要留意的是不同版本的 3Commas 界面展示字段的位置不完全一致有的在 bot 详情页直接给出 JSON 示例有的只在创建时显示一次。最稳妥的取法是重新打开那个 TradingView 信号 bot 的配置页找“Information about your TradingView alerts”之类的区域。email_token 是 3Commas 用来校验来源的令牌长度不短复制时容易多带空格或断行粘到配置文件后最好肉眼核对一遍。只要这三个值不齐后面的消息体结构再对也会被 3Commas 静默丢弃。这个从账号配置页取模板的动作恰恰说明批量工具的数据来源不是“自己编 JSON”而是“用账号里的真实字段去渲染”。工具的价值是把模板按交易对展开而不是替你发明字段。2.2 消息体是 TradingView 和 3Commas 之间的唯一契约TradingView 警报触发后会向 Webhook URL 发送一个 HTTP POST 请求请求体就是这个警报里填写的 Message 内容。3Commas 能识别的消息体是一段固定结构的 JSON下面这个结构是这类集成最常见的形态{ message_type: bot, bot_id: 12345, email_token: 0ffd6f9a..., delay_seconds: 0, deal: { pair: BTC_USDT, order_type: market, signal_type: open_long } }逐个字段说。message_type固定写bot表示这个请求是要驱动一个 bot 进行交易bot_id对应你要控制的信号 bot数字写错会跑到别的 bot 上email_token是 3Commas 校验来源的令牌缺失时请求被丢弃delay_seconds让 3Commas 在执行前延迟几秒可以用于等 K 线确认习惯上设 0 就是立刻执行deal块里的pair是交易对order_type写market表示市价单signal_type决定动作方向。signal_type的取值很关键常见四个open_long开多、close_long平多、open_short开空、close_short平空。批量工具在展开多个交易对时实际上就是保持外层字段不变替换deal.pair和signal_type。这也是为什么我在配置文件里把commas和alerts分开bot 层面的配置只有一份交易对层面的配置按行展开。顺带提醒pair的格式不是 TradingView 里的BTCUSDT而是 3Commas 习惯的下划线风格BTC_USDT。现货和合约的写法可能有差异第一次配好之后先手动创建一个警报验证消息体能被 3Commas 接收再上批量。2.3 为什么批量工具要做字符串替换而不是依赖占位符TradingView 的 Message 输入框里支持内置占位符比如{{ticker}}、{{exchange}}、{{close}}它们会在警报触发时被替换成当时的行情值。理论上可以让所有交易对共用一条 Message 模板靠占位符动态生成pair。但实际用下来这条路有坑{{ticker}}输出的是BTCUSDT这样不带下划线的格式而 3Commas 常见要求是BTC_USDT占位符拼不出下划线同时signal_type必须因交易对和方向而变一个模板解决不了开多和平多的切换。所以这类批量工具的常见做法不是在 TradingView 里做模板而是把“每个交易对该发什么消息体”提前在本地渲染好再通过浏览器自动化把渲染结果填进 Message 框。配置层面只有两个输入一份 bot 固定字段一份交易对清单加信号方向。工具负责把两者组合成最终的 JSON 字符串。这种设计的好处是渲染结果可以先打印出来人工核对真正发送到 3Commas 之前就已经知道每条消息长什么样。这也解释了标题里“专为 3Commas TV 警报集成设计”的含义它不是在 TradingView 警报创建工具上做简单封装而是要理解 3Commas 的消息体契约并围绕这个契约提供批量展开、替换、校验的能力。3. 用 Playwright 批量添加警报最小脚本与三个必调参数3.1 为什么选浏览器自动化而不是直接调内部接口在动手之前有必要解释一下技术选型。TradingView 并没有公开的“创建警报”官方 API网上流传过直接请求它内部警报接口的做法需要 chart 会话的 token、签名参数而且前端版本一升级接口路径就变请求频率稍微一高就触发风控。这类方案维护成本很高跑不了几天就静默失效属于典型的“黑匣子”方案。浏览器自动化的思路完全不同用 Playwright 驱动一个真实浏览器模拟人打开图表页、按快捷键、填表单、点按钮。链路长单个警报要花几秒钟但每一步都可以观察、可以重试即使 TradingView 改版也只是换几个选择器的问题。两个框架里我偏 Playwright 而不是 Puppeteer原因是 Playwright 的自动等待和选择器定位更适合这种多步骤表单操作而且持久化用户目录的方式比 Puppeteer 的 cookie 管理更省心。代价也很明确必须处理登录态、验证码和 UI 选择器的版本漂移这些会在后面的避坑章节展开。3.2 最小脚本打开图表、弹出警报对话框、填 Webhook 与消息体下面是一个能跑通主流程的最小脚本骨架。它假设 TradingView 界面语言是英文因为中文界面文案在不同版本之间变化更频繁定位器更容易失效。import re from playwright.sync_api import sync_playwright def add_alert(page, symbol: str, price: str, webhook_url: str, message: str): # 1. 直接用带 symbol 参数的图表 URL避免在页面里再搜交易对 page.goto(fhttps://www.tradingview.com/chart/?symbolBINANCE:{symbol}, wait_untildomcontentloaded) # 不要用 networkidleTradingView 的实时行情会让它永远等不完 page.wait_for_timeout(6000) # 2. AltA 是图表页创建警报的快捷键比右键菜单稳定 page.keyboard.press(Alta) page.wait_for_timeout(2500) # 3. 价格条件输入框。不同版本这里差异最大失效时用 codegen 重录 price_input page.locator(input[data-nameprice]).first price_input.fill(price) # 4. 定位 Webhook URL 与 Message 输入框并填入 page.locator(input[placeholderWebhook URL]).fill(webhook_url) page.locator(textarea[placeholderMessage]).fill(message) # 5. 点创建按钮用正则匹配避免大小写不一致 page.get_by_role(button, namere.compile(Create Alert, re.IGNORECASE)).click() page.wait_for_timeout(1500) with sync_playwright() as p: # 持久化用户目录第一次跑完手动登录之后登录态一直保留 context p.chromium.launch_persistent_context( user_data_dir./tv_profile, headlessFalse, localeen-US ) page context.new_page() # 第一次运行先手动登录一次之后可注释 input(登录完成后按回车继续...) add_alert( page, symbolBTCUSDT, price65000, webhook_urlhttps://webhook.3commas.io/webhook/你的地址, message{message_type:bot,bot_id:0,email_token:xxx,delay_seconds:0,deal:{pair:BTC_USDT,order_type:market,signal_type:open_long}} ) context.close()这段脚本有几个关键点。用domcontentloaded而不是networkidle因为 TradingView 图表会持续接收实时数据流networkidle可能一直等不到固定等 6 秒是给图表数据加载留缓冲时间短了弹不出警报对话框。headlessFalse是刻意为之无头模式下 TradingView 对自动化的识别更敏感而且第一次需要手动登录等登录态稳定后再考虑切到无头跑批量。input[data-nameprice]这个选择器是示例级定位不同界面版本里价格输入框的属性不一定叫这个。如果你打开录制工具发现定位不到把它换成录制生成的选择器即可。Webhook 和 Message 两个输入框用 placeholder 定位这是多年来相对稳定的属性。await page.wait_for_timeout这类固定等待确实有“玄学”成分但它比等待某个容易过期的元素更省心后续维护时只需要调大调小数值。3.3 三个必调参数加载等待、过期时间、重试间隔批量跑起来之后真正影响成功率的是下面三个参数它们不是默认值就能用的。参数建议值作用与风险图表加载等待610 秒太短则 AltA 时页面还没就绪对话框不出现警报过期时间按策略需求建议 7 天或更长TradingView 警报到期后静默失效不报错、不通知交易对间间隔12 秒连续快速创建会触发风控间隔太短容易弹出验证加载等待是整个脚本里最影响体感的参数。网络慢的时候 6 秒不够快的时候 4 秒就够。我的习惯是写成一个配置项page_load_timeout_ms而不是硬编码在代码里跑一轮如果失败率偏高先把它调大。过期时间不是只影响单次创建它决定你要不要写“定期重建警报”的任务。重试间隔则直接关联风控两个警报之间加page.wait_for_timeout(1500)通常够用不要用并发。3.4 批量循环里的幂等跳过别让脚本重跑时重复创建批量脚本最隐蔽的问题不是创建失败而是创建成功但本地不知道重跑时又创建一遍。解决方法是在本地维护一个状态文件记录每个交易对是否已创建、创建时的价格和过期时间。创建成功后再写盘而不是跑完统一写。import json from datetime import datetime, timezone def load_state(pathalerts_state.json): try: with open(path) as f: return json.load(f) except FileNotFoundError: return {} def should_skip(state, pair, signal_type, price, expire_at): key f{pair}:{signal_type} old state.get(key) if not old: return False # 价格变了说明配置更新过要重建 if old.get(price) ! price: return False # 过期时间早于当前时间也要重建 if datetime.fromisoformat(old[expire_at]) datetime.now(timezone.utc): return False return True这个函数的逻辑是先按pair:signal_type组成唯一键然后检查旧记录是否存在、触发价是否一致、是否已过期。三个条件都满足才跳过。这里有个实际细节expire_at必须用带时区的 ISO 格式写入否则本地时间和 UTC 时间混在一起到期判断会莫名提前或延后。我把这个状态文件和 TradingView 的警报列表当作两套独立数据源状态文件是本地预期TradingView 是实际状态。两者不一致时以手动核查为准而不是盲目相信状态文件。4. 一批警报的配置样例24 个交易对的方向怎么组织不混乱4.1 把 bot 配置与交易对清单分开写批量配置如果只有一个 JSON 大对象后期维护会很难受。我把配置拆成tv、commas、alerts三段tv段管 TradingView 侧的行为commas段管 3Commas 侧的固定字段alerts段是真正要扩展的交易对清单。下面这份配置覆盖了两个交易对、四个方向的完整结构。{ tv: { symbol_exchange: BINANCE, interval: 15, expiration_days: 7, page_load_timeout_ms: 8000 }, commas: { webhook_url: https://webhook.3commas.io/webhook/替换成你自己的, bot_id: 0, email_token: 从3Commas信号配置页复制, message_type: bot, delay_seconds: 0 }, alerts: [ {pair: BTC_USDT, signal_type: open_long, price: 65000}, {pair: BTC_USDT, signal_type: close_long, price: 68000}, {pair: ETH_USDT, signal_type: open_long, price: 3300}, {pair: ETH_USDT, signal_type: close_long, price: 3600} ] }tv段里symbol_exchange决定图表 URL 里的交易所前缀interval是警报所依赖的 K 线周期expiration_days控制警报到期时间。alerts段每一行就是一个将被创建的警报。注意pair用的是 3Commas 风格的下划线格式而tv.symbol_exchange是交易所前缀两者在渲染时要各司其职。4.2 从配置到消息体的渲染函数有了配置下一步是把每一行alerts渲染成 TradingView 里要填的 Message。渲染逻辑不复杂核心是保持外层字段来自commas段内层deal来自当前行的交易对和信号方向。import json from datetime import datetime, timedelta, timezone def render_messages(cfg): messages [] for a in cfg[alerts]: body { message_type: cfg[commas][message_type], bot_id: cfg[commas][bot_id], email_token: cfg[commas][email_token], delay_seconds: cfg[commas][delay_seconds], deal: { pair: a[pair], order_type: market, signal_type: a[signal_type] } } expire_at datetime.now(timezone.utc) timedelta(dayscfg[tv][expiration_days]) # TradingView 图表 URL 里的 symbol 不带下划线 symbol a[pair].replace(_, ) messages.append({ symbol: symbol, price: str(a[price]), message: json.dumps(body, ensure_asciiFalse), expire_at: expire_at.isoformat() }) return messagesrender_messages返回的列表每个元素都带symbol、price、message、expire_at四个字段正好对应浏览器自动化脚本需要的参数。symbol通过replace(_, )把BTC_USDT转成BTCUSDT而message里保留下划线格式给 3Commas 用。expiration_days在这里统一换算成expire_at写进状态文件做幂等判断。4.3 合并与拆分的边界什么时候不能省批量配置的主要陷阱是“想合并”。比如多个交易对用同一个开多信号有人会试图在一个警报里放多个交易对。3Commas 的机制是一条消息处理一个deal.pair所以每个交易对必须独立建警报。反过来同一个交易对的开多、平多、开空、平空也不适合塞进一条警报因为一根 K 线上如果同时满足开多和平多条件消息体只能表达一个方向另一个信号就丢了。我的拆分习惯是按pair加signal_type拆成独立条目一个条目对应一个警报。四个方向、24 个交易对配置里就是 96 行听着多但全部由渲染函数自动展开人工只需要维护交易对和价格阈值。这样拆完之后3Commas 端收到什么消息、哪个交易对、什么方向在配置里一目了然排查重复开单或漏单时也容易定位。5. 批量警报添加的避坑记录从无声无息到重复开单这一章全是血泪经验。批量添加警报的坑和策略本身的坑不太一样策略问题会在测试时暴露批量添加的坑则往往藏在“看起来都正常”的错觉里。下面五条按实际发生频率排序。5.1 警报创建成功但永远不触发现象是批量脚本跑完TradingView 警报列表里条目都在但 K 线走过好几根3Commas 一点动静没有。原因往往是图表周期和策略周期不一致脚本打开的图表默认落在上次浏览的周期比如 1 小时图你却在等 15 分钟 K 线收盘触发条件自然迟迟不触发。另一个常见原因是触发类型没选对比如想要交叉信号却留了默认的价格穿越条件。解决方式有两个落点。批量工具里固定interval参数每次打开图表 URL 后切换周期到指定值手动检查一个交易对的警报对话框确认Condition区域里的触发类型是否符合预期。TradingView 的周期切换可以在代码里用page.keyboard.type(15)加回车完成但更稳的是在配置里加interval字段后通过页面底部周期选择器点击选择器同样用 codegen 录制一次。5.2 3Commas 收不到请求但 TradingView 显示已发送现象是警报触发了TradingView 的 Webhook 状态是成功的3Commas 却没有开单。原因大概率是消息体不是合法 JSON或者deal块里的字段名不对。TradingView 对 Message 以{开头的请求会按application/json发送但如果消息体里混入了前导空格、换行或者{{close}}这类占位符被替换成一个字符串而不是数字整体 JSON 就可能解析失败3Commas 端直接丢弃。解决方法是把渲染函数的结果打印出来人工核对尤其看pair、signal_type、bot_id三个字段。不要在浏览器自动化里直接测试未知格式先在本地用json.loads(message)过一遍确保能解析。这个习惯能过滤掉至少一半的“收不到”问题。5.3 重复执行批量脚本导致同一信号开两次单现象是脚本跑到一半报错修复后重跑同一交易对出现了两条相同警报之后行情触发时 3Commas 连续收到两条同样的open_long仓位直接翻倍。原因就是没做幂等检查状态文件在创建完所有警报之后才写中途失败就丢了进度。解决方式是创建成功一条就写一条状态。更严格一点创建前先查状态文件should_skip返回 True 就跳过创建后再更新expire_at和price。同时 TradingView 侧的警报触发频率建议设置在警报对话框里选择“每根 K 线收盘时”而非“每次价格变化都推送”能减少重复信号的可能。5.4 登录态失效与验证码拦截现象是批量跑到第十几个交易对时页面弹出验证码或者提示重新登录。原因是自动化脚本的请求模式太规律短时间内反复打开相同结构的 URL、间隔固定容易被识别。另一个常见原因是第一次登录后没有保存持久化上下文第二次跑批量时又是未登录状态。解决方式有两个层次。短期用launch_persistent_context配user_data_dir保存登录态这个方案脚本里已经写了。长期要靠行为随机化交易对之间的等待时间不要固定 1500ms改成 1200 到 3000 之间随机不要在多个浏览器 tab 里并行创建警报并发是风控的高危行为。偶尔在批量过程中手动浏览几个页面也能降低整体风险。5.5 过期时间引发的“静默下线”现象是批量添加后一切正常但一段时间后策略不再触发3Commas 没有任何新开单看行情又觉得价格确实到了。原因是警报到期了。TradingView 的警报支持设置过期时间到期后不会推送任何通知警报列表里看起来还在但已经失效。解决方式是让状态文件承担“到期提醒”职能。render_messages里算出的expire_at写进状态文件定期检查哪些条目即将到期在到期前重建。我一般设置一个检查任务把expire_at在 24 小时内到期的条目提前重建避免半夜策略突然断掉。这也比依赖人脑记住“上周建的警报今天到期”要可靠得多。6. 收尾上线前先跑 dry-run 和到期检查别信“已经创建成功”6.1 先渲染不要先创建把渲染和创建分开是这套方案里最值得养成的习惯。渲染函数的输出应该先打印出来人工过目而不是直接喂给浏览器自动化。这个动作我每次都会做哪怕只是扫一眼交易对数量和信号方向。if __name__ __main__: cfg json.load(open(commas_tv_config.json)) msgs render_messages(cfg) for m in msgs: print(m[symbol], m[price]) print(m[message]) print(---)重点核对三件事deal.pair的下划线格式是否保留、signal_type是否和预期方向一致、bot_id和email_token是否来自正确的 bot。这些都对了再进行下一步。6.2 到期检查脚本提前一天重建最后一个技巧是写一个纯本地的到期检查函数不需要登录 TradingView只读状态文件。from datetime import datetime, timezone def check_expiry(state_pathalerts_state.json, rebuild_hours24): state load_state(state_path) now datetime.now(timezone.utc) for key, info in state.items(): exp datetime.fromisoformat(info[expire_at]) remain exp - now if remain.total_seconds() 0: print(f已过期需要重建: {key}) elif remain.total_seconds() rebuild_hours * 3600: print(f即将到期建议重建: {key}, 剩余 {remain})到期和即将到期分成两档打印方便脚本按输出决定是否触发重建。这个检查和 TradingView 的警报实际状态无关它只回答一个问题按照本地记录哪些警报该失效了。6.3 上线前的验证顺序我的固定顺序是先跑 dry-run 打印全部渲染消息核对五条以上然后检查状态文件里是否有即将到期或已过期的条目再随机挑一个交易对真实创建等一次触发确认 3Commas 能收到最后才放开全量批量。这套顺序看起来慢但避免的是“批量创建一百个警报后发现格式错了”这种没有后悔药的事故。批量加警报本身不难难的是每次运行之后都知道哪些有效、哪些该重建这种能核账的确定性才是这类工具真正值钱的地方。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进