简介:这是一套基于TypeScript开发的自动化工具,专为高频交易场景设计,面向使用TradingView指标联动3Commas等机器人进行跨币对自动交易的开发者与量化实践者。它解决了TradingView官方无批量警报API导致需手动维护数十甚至数百个交易对警报的痛点,通过Puppeteer驱动Chromium浏览器实现警报的全自动添加与配置。资源包共18个文件,含5个核心TypeScript源码(如add-tradingview-alerts.ts、tv-page-actions.ts)、4个配置类JSON/YML文件(含市场列表、黑名单、示例配置)、1个Shell部署脚本、1个CSV黑名单及2个演示素材(GIF操作录屏、PNG条件说明图),辅以README、LICENSE等工程规范文件,整体13.9MB,结构清晰、开箱即用。目前已有1447人学习下载,使用者可直接复用完整脚本、参考可视化操作流程、快速适配自身交易策略,并基于源码二次扩展警报逻辑与多平台集成能力。
1. 这不是 TradingView 官方功能:一个用 TypeScript 写的「警报注入器」,专治 3Commas 用户手动点到手抽筋的痛
你有没有在 TradingView 上调好一套策略,画完布林带、MACD 和 RSI 三重过滤,信心满满地想把它塞进 3Commas 自动跟单?结果卡在最后一步:TradingView 的「添加警报」按钮,得一个个点开图表、右键、选条件、填 webhook URL、勾选「仅发送一次」……一套操作下来,10 个品种就得点 50 次,稍一走神就漏掉一个。这不是效率问题,是生理疲劳——我亲眼见过某开发者连续三天没睡够,就因为要给 47 个 BTC/USDT、ETH/USDT、SOL/USDT 等不同时间周期的图表批量配警报,最后导出的 CSV 里有 3 个 URL 少了个斜杠,导致 3Commas 根本收不到触发信号,策略静默了整整 12 小时。
add-tradingview-alerts-tool就是为这个场景生的:它不依赖 TradingView API(官方压根没开放警报创建接口),而是用 Puppeteer 驱动真实 Chromium 浏览器,模拟人工点击全流程;核心逻辑全用 TypeScript 编写,类型安全强、IDE 补全准、错误提示直指变量定义行;所有配置收口在config.json里,支持按 symbol、timeframe、alertName 做组合匹配,还能自动跳过已存在同名警报——不是“能跑就行”的脚本,是能放进 CI/CD 流水线、被团队成员复用、改一行参数就能切换实盘/回测环境的工程化工具。适合正在用 3Commas 做网格、马丁、止盈止损联动的中高频交易者,也适合需要把 TradingView 策略快速部署到多个子账户的量化运营岗。
2. 从零启动:TypeScript 环境搭建、Puppeteer 配置与 config.json 结构解析
2.1 初始化项目并安装核心依赖
我们不走 npm init -y 一键糊弄流。真实项目里,tsconfig.json的strict必须开,否则后期类型断言满天飞,debug 成本翻倍。先建空目录,初始化 TypeScript:
mkdir tv-alert-batch && cd tv-alert-batch npm init -y npm install --save-dev typescript @types/node @types/puppeteer npm install puppeteer npx tsc --init --strict --target es2020 --module commonjs --outDir dist --rootDir src提示:
@types/puppeteer是关键。没有它,TypeScript 不认识page.click()返回的是Promise<void>还是void,后续.then()链式调用会报错,且 IDE 无法智能提示page.waitForSelector()的 timeout 参数单位。
生成的tsconfig.json需手动补两行:
{ "compilerOptions": { "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "outDir": "dist", "rootDir": "src", "types": ["node", "puppeteer"] } }"types"字段显式声明,避免某些 TS 版本下@types/puppeteer被忽略。
2.2 Puppeteer 启动配置:绕过检测、稳定加载、精准等待
TradingView 页面大量使用动态渲染和防爬逻辑。直接puppeteer.launch()会触发 Cloudflare 验证或页面白屏。必须加三项硬核配置:
// src/browser.ts import puppeteer, { Browser, Page } from 'puppeteer'; export async function launchBrowser(): Promise<Browser> { return puppeteer.launch({ headless: 'new', // 必须用 'new',旧版 headless=true 已废弃且易被识别 args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-blink-features=AutomationControlled', '--disable-web-security', '--disable-features=IsolateOrigins,site-per-process' ], defaultViewport: { width: 1920, height: 1080 }, executablePath: '/usr/bin/chromium' // Linux 下指定系统 Chromium,避免 Puppeteer 自带二进制体积过大 }); } export async function waitForTradingViewReady(page: Page): Promise<void> { // TradingView 加载分三阶段:DOM 就绪 → 图表容器渲染 → 警报按钮可交互 await page.waitForSelector('.chart-container', { timeout: 30000 }); await page.waitForFunction(() => window.TradingView !== undefined, { timeout: 20000 }); await page.waitForSelector('button[data-name="alerts-button"]', { timeout: 15000 }); }--disable-blink-features=AutomationControlled是核心:它禁用 Blink 引擎的自动化特征标记,让navigator.webdriver返回undefined而非true;waitForFunction等待window.TradingView全局对象存在,比单纯等 DOM 更可靠——因为 TradingView 的 JS 是异步 chunk 加载的,.chart-container出现时TradingView对象可能还没挂载;- 所有
timeout显式设值,避免默认 30s 太长(调试时等得心焦)或太短(网络抖动就失败)。
2.3 config.json 全字段详解:为什么 alertName 必须带唯一前缀?
config.json是整个工具的控制中枢,结构必须严格。以下是一个生产环境可用的最小完备配置:
{ "tradingView": { "email": "your_tv_email@example.com", "password": "your_strong_password", "chartUrlTemplate": "https://www.tradingview.com/chart/?symbol={{symbol}}&interval={{interval}}" }, "alerts": [ { "symbol": "BTCUSDT", "timeframe": "5", "alertName": "PROD_BTCUSDT_5m_MACD_CROSS", "condition": "MACD() > signal() and MACD()[1] <= signal()[1]", "webhookUrl": "https://api.3commas.io/webhooks/xxx?token=yyy", "isOnce": true, "isSoundEnabled": false } ] }关键字段说明:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
tradingView.email | string | ✅ | TradingView 账号邮箱,必须已登录并保存密码(工具不处理二次验证) |
chartUrlTemplate | string | ✅ | 模板字符串,{{symbol}}和{{interval}}会被自动替换;interval值见 TradingView 文档:1=1分钟,5=5分钟,D=日线,W=周线 |
alerts[].symbol | string | ✅ | 交易所标准代码,如"BTCUSDT"(Binance)、"XBTUSD"(BitMEX),大小写敏感 |
alerts[].alertName | string | ✅ | 必须全局唯一,重复名称会导致 TradingView 跳过创建(它把 name 当主键);建议用ENV_SYMBOL_TF_STRATEGY格式,如STAGING_ETHUSDT_15m_RSI_OVERSOLD |
alerts[].condition | string | ✅ | TradingView 警报条件表达式,必须是合法 Pine Script v5 语法;[1]表示前一根K线,signal()是内置函数,不能写成macd_signal() |
alerts[].webhookUrl | string | ✅ | 3Commas Webhook 地址,末尾必须带?token=xxx,否则 3Commas 拒绝接收 |
注意:
condition字段是黑匣子。TradingView 不校验语法,只在创建时执行一次。如果写错(比如MACD()[1] < signal()少了等号),警报会创建成功但永不触发——你得去 TradingView 网页端手动编辑它才能看到报错。所以务必在config.json里先用 TradingView 编辑器验证好再粘贴进来。
3. 核心逻辑拆解:如何用 Puppeteer 精准定位并填写警报表单
3.1 图表加载与时间周期切换:用 CSS 选择器定位而非 XPath
TradingView 的时间周期按钮 HTML 结构极不稳定,XPath 容易因版本更新失效。我们改用>// src/alert-creator.ts export async function switchTimeframe(page: Page, timeframe: string): Promise<void> { // 先点开时间周期下拉框 await page.click('button[data-name="header-toolbar-interval"]'); // 等待下拉菜单出现,并查找匹配>export async function createAlert( page: Page, alertConfig: AlertConfig ): Promise<boolean> { // 1. 点击警报按钮 await page.click('button[data-name="alerts-button"]'); // 2. 等待弹窗出现(TradingView 用 role="dialog") await page.waitForSelector('div[role="dialog"]', { timeout: 10000 }); // 3. 填写警报名称(aria-label 包含 "Name") await page.fill('input[aria-label*="Name"]', alertConfig.alertName); // 4. 填写条件表达式(aria-label 包含 "Condition") await page.fill('textarea[aria-label*="Condition"]', alertConfig.condition); // 5. 勾选 "Only once" 复选框(label 文本匹配) if (alertConfig.isOnce) { const onceLabel = await page.$('label:has-text("Only once")'); if (onceLabel) { await onceLabel.click(); } } // 6. 填写 Webhook URL(aria-label 包含 "Webhook") await page.fill('input[aria-label*="Webhook"]', alertConfig.webhookUrl); // 7. 点击 "Create Alert" 按钮(用>// src/batch-runner.ts export async function runBatch(config: Config) { const browser = await launchBrowser(); const page = await browser.newPage(); try { // 1. 登录 TradingView await loginToTradingView(page, config.tradingView); // 2. 按 symbol 分组(同一 symbol 的图表可复用页面,减少 reload) const groupedAlerts = groupBy(config.alerts, 'symbol'); for (const [symbol, alerts] of Object.entries(groupedAlerts)) { console.log(`\n🔄 Processing symbol: ${symbol}`); // 3. 为每个 symbol 加载一次图表 const chartUrl = config.tradingView.chartUrlTemplate .replace('{{symbol}}', symbol) .replace('{{interval}}', alerts[0].timeframe); await page.goto(chartUrl, { waitUntil: 'networkidle0' }); await waitForTradingViewReady(page); // 4. 组内串行创建警报(避免同一图表频繁切换 timeframe) for (const alert of alerts) { console.log(` ➕ Creating alert: ${alert.alertName}`); try { await switchTimeframe(page, alert.timeframe); await createAlert(page, alert); console.log(` ✅ Success`); } catch (err) { console.error(` ❌ Failed: ${(err as Error).message}`); // 熔断:单个警报失败不影响后续,但记录日志供人工核查 } } } } finally { await browser.close(); } }
groupBy是 Lodash 的groupBy(alerts, 'symbol'),按 symbol 聚合,减少页面 reload 次数(TradingView reload 一次要 8~12 秒);waitUntil: 'networkidle0'比'domcontentloaded'更稳,确保所有 JS/CSS 加载完毕;- 不实现自动重试:TradingView 对重复提交敏感,重试可能触发风控。失败就记日志,人来判断是网络问题还是配置问题。
4. 避坑指南:5 个血泪经验总结的「警报创建翻车现场」
4.1 现象:浏览器打开后停留在登录页,控制台报net::ERR_CONNECTION_TIMED_OUT
原因:Puppeteer 默认使用自带 Chromium,其证书库老旧,访问 TradingView 时 SSL 握手失败(尤其在企业网络或某些 Linux 发行版)。
解决:强制使用系统 Chromium,并添加--ignore-certificate-errors参数(仅限内部可信环境):
# Ubuntu/Debian sudo apt install chromium-browser # 修改 launchBrowser() 中的 executablePath 为 '/usr/bin/chromium-browser' # 并在 args 中加入 '--ignore-certificate-errors'4.2 现象:await page.click('button[data-name="alerts-button"]')报错TimeoutError: waiting for selector "button[data-name=\"alerts-button\"]" failed
原因:TradingView 页面未完全加载,或当前视图是「市场概览」而非「图表页」。>await page.waitForSelector('.chart-container', { timeout: 30000 }); // 确保是图表页 await page.waitForSelector('button[data-name="alerts-button"]', { timeout: 15000 }); // 再找按钮
4.3 现象:警报创建成功,但在 TradingView 网页端查看时,condition 显示为Invalid condition
原因:config.json中的condition字符串含不可见 Unicode 字符(如 Word 复制来的全角括号、中文引号),或 Pine Script 语法错误(如MACD()[1] < signal()应为<=)。
解决:
- 用 VS Code 打开
config.json,开启「显示不可见字符」(Ctrl+Shift+P → Toggle Render Whitespace); - 在 TradingView 网页端新建一个临时警报,把
condition粘贴进去,用编辑器右上角的「检查」按钮验证; - 永远不要在微信/钉钉里复制 condition——它们会自动转中文标点。
4.4 现象:alertName相同的警报被跳过,但实际想更新它的 condition
原因:TradingView 的设计逻辑是「name 即唯一标识」,不存在「更新」操作,只有「创建」和「删除后重建」。工具检测到同名即跳过,防止误覆盖。
解决:
- 方案 A(推荐):修改
alertName,如从BTCUSDT_5m_MACD改为BTCUSDT_5m_MACD_v2; - 方案 B:手动在 TradingView 网页端删除旧警报,再运行工具;
- 方案 C(高级):扩展工具,增加
deleteAlertByName()函数,用 Puppeteer 模拟点击删除图标(需解析警报列表 DOM)。
4.5 现象:运行时 CPU 占用 100%,进程卡死无响应
原因:puppeteer.launch()未传headless: 'new',旧版 headless 模式在某些内核版本下有渲染线程死锁 Bug。
解决:
- 确认 Puppeteer 版本 ≥ 21.0.0;
launch()参数中headless必须为字符串'new',不能是布尔值true;- 若仍卡死,在
args中加入--single-process(仅开发调试用,降低稳定性)。
5. 进阶技巧:CI/CD 自动化部署、多账号轮询与警报健康度验证
5.1 接入 GitHub Actions:每日凌晨自动同步警报配置
把config.json放进 Git 仓库,用 GitHub Actions 实现无人值守更新。关键在于:TradingView 密码不能明文写进 workflow,必须用 Secrets:
# .github/workflows/deploy-alerts.yml name: Deploy TradingView Alerts on: schedule: - cron: '0 3 * * *' # 每天凌晨 3 点 workflow_dispatch: jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Install dependencies run: npm ci - name: Run alert batch env: TV_EMAIL: ${{ secrets.TV_EMAIL }} TV_PASSWORD: ${{ secrets.TV_PASSWORD }} run: | # 注入 secrets 到 config.json jq --arg email "$TV_EMAIL" --arg pass "$TV_PASSWORD" \ '.tradingView.email = $email | .tradingView.password = $pass' \ config.json > config.tmp && mv config.tmp config.json npm run build node dist/index.js - name: Notify on failure if: ${{ failure() }} uses: dawidd6/action-send-mail@v3 with: server_address: smtp.gmail.com server_port: 465 username: ${{ secrets.EMAIL_USER }} password: ${{ secrets.EMAIL_PASS }} subject: "🚨 TV Alerts Deployment Failed" body: "Check GitHub Actions logs for ${{ github.repository }}" to: your-email@example.comjq命令动态注入密码,避免 config.json 提交敏感信息;npm run build调用tsc编译,node dist/index.js运行;- 失败时邮件通知,比只看 Actions 日志更及时。
5.2 多账号轮询:用配置文件驱动,避免硬编码
当管理多个 TradingView 子账户(如实盘/模拟/研究)时,不应复制多份代码。改为accounts/目录下放多个 JSON:
accounts/ ├── live.json # 实盘账号 ├── paper.json # 模拟盘 └── research.json # 研究用每个文件结构同config.json,但只含tradingView字段:
// accounts/live.json { "tradingView": { "email": "live@xxx.com", "password": "xxx", "chartUrlTemplate": "https://www.tradingview.com/chart/?symbol={{symbol}}&interval={{interval}}" } }主程序读取时动态合并:
// src/index.ts import { readFileSync } from 'fs'; import { join } from 'path'; function loadAccountConfig(accountName: string): TradingViewConfig { const accountPath = join(__dirname, '..', 'accounts', `${accountName}.json`); const raw = readFileSync(accountPath, 'utf8'); return JSON.parse(raw).tradingView; } async function main() { const accountName = process.argv[2] || 'live'; // 默认 live const accountConfig = loadAccountConfig(accountName); // 合并到主 config const fullConfig = { ...JSON.parse(readFileSync('config.json', 'utf8')), tradingView: accountConfig }; await runBatch(fullConfig); }运行命令:npm start live/npm start paper,彻底解耦账号与策略。
5.3 警报健康度验证:用 Puppeteer 反向抓取已创建警报列表
工具只管创建,不管创建后是否有效。我们加一个verifyAlerts.ts,登录后抓取 TradingView 的警报列表,比对config.json中的alertName是否全部存在且 condition 一致:
// src/verify-alerts.ts export async function verifyAlerts(page: Page, expectedAlerts: AlertConfig[]) { // 1. 打开警报管理页 await page.goto('https://www.tradingview.com/alerts/', { waitUntil: 'networkidle0' }); await page.waitForSelector('div[data-widget-type="alerts-list"]', { timeout: 20000 }); // 2. 提取所有警报的 name 和 condition const actualAlerts = await page.evaluate(() => { return Array.from(document.querySelectorAll('div[data-widget-type="alerts-list"] div[data-row-id]')) .map(el => { const nameEl = el.querySelector('div[data-column="name"] div'); const conditionEl = el.querySelector('div[data-column="condition"] div'); return { name: nameEl?.textContent?.trim(), condition: conditionEl?.textContent?.trim() }; }) .filter(a => a.name && a.condition); }); // 3. 比对 const missing = expectedAlerts.filter(exp => !actualAlerts.some(act => act.name === exp.alertName) ); const mismatch = expectedAlerts.filter(exp => actualAlerts.some(act => act.name === exp.alertName && act.condition !== exp.condition ) ); if (missing.length > 0) { console.error('❌ Missing alerts:', missing.map(m => m.alertName)); } if (mismatch.length > 0) { console.warn('⚠️ Condition mismatch:', mismatch.map(m => m.alertName)); } if (missing.length === 0 && mismatch.length === 0) { console.log('✅ All alerts verified'); } }style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />