Minimum for automation: Essential plan.
https://your-server.com/webhook/tradingviewtext/plain (not application/json, even if your message is JSON)| Placeholder | Description | Example |
|---|---|---|
{{ticker}} |
Symbol ticker | BTCUSDT |
{{exchange}} |
Exchange name | BYBIT |
{{close}} |
Current close price | 67432.50 |
{{open}} |
Current open price | 67100.00 |
{{high}} |
Current high | 67500.00 |
{{low}} |
Current low | 67000.00 |
{{volume}} |
Current volume | 1234.56 |
{{time}} |
UTC timestamp | 2025-01-15T14:30:00Z |
{{timenow}} |
Current time | 2025-01-15T14:30:05Z |
{{interval}} |
Chart timeframe | 15 (minutes) |
{{plot_0}}, {{plot_1}} ... |
Indicator plot values | 72.5 (RSI value) |
{{strategy.order.action}} |
Strategy order direction | buy or sell |
{{strategy.order.id}} |
Strategy order ID | Long Entry |
{{strategy.order.price}} |
Strategy order price | 67432.50 |
{{strategy.order.contracts}} |
Contracts/quantity | 0.1 |
{{strategy.position_size}} |
Current position size | 0.1 or -0.1 |
{{strategy.market_position}} |
Position type | long, short, or flat |
{{strategy.prev_market_position}} |
Previous position | flat |
{{strategy.order.alert_message}} |
Custom message from Pine Script | (see below) |
{
"secret": "YOUR_SECRET_KEY_HERE",
"ticker": "{{ticker}}",
"exchange": "{{exchange}}",
"action": "{{strategy.order.action}}",
"order_id": "{{strategy.order.id}}",
"price": {{close}},
"position_size": {{strategy.position_size}},
"market_position": "{{strategy.market_position}}",
"prev_market_position": "{{strategy.prev_market_position}}",
"contracts": {{strategy.order.contracts}},
"interval": "{{interval}}",
"time": "{{time}}"
}
alert_message Parameter (Strategy Only)strategy.entry("Long", strategy.long, alert_message='{"action": "buy", "ticker": "' + syminfo.ticker + '", "price": ' + str.tostring(close) + '}')
When creating the alert, set message to: {{strategy.order.alert_message}}
//@version=5
strategy("RSI Webhook Strategy", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100)
rsiLength = input.int(14, "RSI Length")
overbought = input.int(70, "Overbought Level")
oversold = input.int(30, "Oversold Level")
rsiValue = ta.rsi(close, rsiLength)
longCondition = ta.crossover(rsiValue, oversold)
shortCondition = ta.crossunder(rsiValue, overbought)
if longCondition
strategy.entry("RSI Long", strategy.long,
alert_message='{"action":"buy","ticker":"' + syminfo.ticker + '","price":' + str.tostring(close) + ',"rsi":' + str.tostring(rsiValue) + ',"strategy":"rsi"}')
if shortCondition
strategy.entry("RSI Short", strategy.short,
alert_message='{"action":"sell","ticker":"' + syminfo.ticker + '","price":' + str.tostring(close) + ',"rsi":' + str.tostring(rsiValue) + ',"strategy":"rsi"}')
//@version=5
strategy("EMA Cross Webhook", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100)
fastLen = input.int(9, "Fast EMA")
slowLen = input.int(21, "Slow EMA")
fastEMA = ta.ema(close, fastLen)
slowEMA = ta.ema(close, slowLen)
if ta.crossover(fastEMA, slowEMA)
strategy.entry("EMA Long", strategy.long,
alert_message='{"action":"buy","ticker":"' + syminfo.ticker + '","price":' + str.tostring(close) + ',"strategy":"ema_cross"}')
if ta.crossunder(fastEMA, slowEMA)
strategy.entry("EMA Short", strategy.short,
alert_message='{"action":"sell","ticker":"' + syminfo.ticker + '","price":' + str.tostring(close) + ',"strategy":"ema_cross"}')
//@version=5
strategy("Volume Spike Webhook", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100)
volMultiplier = input.float(2.0, "Volume Spike Multiplier")
volSmaLen = input.int(20, "Volume SMA Length")
avgVol = ta.sma(volume, volSmaLen)
isVolumeSpike = volume > avgVol * volMultiplier
isBullish = close > open
isBearish = close < open
if isVolumeSpike and isBullish
strategy.entry("Vol Long", strategy.long,
alert_message='{"action":"buy","ticker":"' + syminfo.ticker + '","price":' + str.tostring(close) + ',"vol_ratio":' + str.tostring(volume/avgVol) + ',"strategy":"volume_spike"}')
if isVolumeSpike and isBearish
strategy.entry("Vol Short", strategy.short,
alert_message='{"action":"sell","ticker":"' + syminfo.ticker + '","price":' + str.tostring(close) + ',"vol_ratio":' + str.tostring(volume/avgVol) + ',"strategy":"volume_spike"}')
//@version=5
strategy("Multi Signal Webhook", overlay=true, default_qty_type=strategy.percent_of_equity, default_qty_value=100)
rsi = ta.rsi(close, 14)
fast = ta.ema(close, 9)
slow = ta.ema(close, 21)
avgVol = ta.sma(volume, 20)
longSignal = rsi < 40 and ta.crossover(fast, slow) and volume > avgVol * 1.5
shortSignal = rsi > 60 and ta.crossunder(fast, slow) and volume > avgVol * 1.5
if longSignal
strategy.entry("Multi Long", strategy.long,
alert_message='{"action":"buy","ticker":"' + syminfo.ticker + '","price":' + str.tostring(close) + ',"rsi":' + str.tostring(rsi) + ',"strategy":"multi"}')
if shortSignal
strategy.entry("Multi Short", strategy.short,
alert_message='{"action":"sell","ticker":"' + syminfo.ticker + '","price":' + str.tostring(close) + ',"rsi":' + str.tostring(rsi) + ',"strategy":"multi"}')
const express = require('express');
const app = express();
const SECRET = process.env.TV_WEBHOOK_SECRET || 'your-secret-key';
const ALLOWED_IPS = [
'52.89.214.238', '34.212.75.30',
'54.218.53.128', '52.32.178.7'
];
app.use('/webhook', express.text({ type: '*/*' }));
function checkIP(req, res, next) {
const clientIP = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip;
if (process.env.NODE_ENV === 'production' && !ALLOWED_IPS.includes(clientIP)) {
console.log(`[${new Date().toISOString()}] Blocked IP: ${clientIP}`);
return res.status(403).json({ error: 'Forbidden' });
}
next();
}
app.post('/webhook/tradingview', checkIP, async (req, res) => {
const timestamp = new Date().toISOString();
try {
let payload;
try {
payload = JSON.parse(req.body);
} catch (e) {
console.log(`[${timestamp}] Non-JSON alert: ${req.body}`);
return res.status(400).json({ error: 'Invalid JSON' });
}
if (payload.secret !== SECRET) {
console.log(`[${timestamp}] Invalid secret from alert`);
return res.status(401).json({ error: 'Unauthorized' });
}
console.log(`[${timestamp}] Alert received:`, JSON.stringify(payload));
const result = await processSignal(payload);
res.status(200).json({ success: true, result });
} catch (err) {
console.error(`[${timestamp}] Error:`, err.message);
res.status(500).json({ error: 'Internal error' });
}
});
const PORT = process.env.WEBHOOK_PORT || 3400;
app.listen(PORT, () => {
console.log(`[${new Date().toISOString()}] Webhook receiver on port ${PORT}`);
});
Layer 1: Secret in Payload
{"secret": "a7f3b2c9d1e4...", "action": "buy", ...}
Layer 2: IP Whitelisting (Nginx)
location /webhook/tradingview {
allow 52.89.214.238;
allow 34.212.75.30;
allow 54.218.53.128;
allow 52.32.178.7;
deny all;
proxy_pass http://localhost:3400;
proxy_set_header X-Forwarded-For $remote_addr;
}
Layer 3: HTTPS Only (Let's Encrypt โ )
Layer 4: Rate Limiting
const rateLimit = require('express-rate-limit');
app.use('/webhook', rateLimit({ windowMs: 60000, max: 30 }));
Layer 5: Non-Obvious URL Path
https://your-server.com/webhook/tv-8f3a7b2c1d
| Step | Latency | Notes |
|---|---|---|
| TV alert detection | 0-15 sec | Checked at bar close |
| TV sends webhook | 50-500 ms | TV server โ your server |
| Server processes | 5-50 ms | Parse JSON + logic |
| Bybit API call | 50-200 ms | Order placement |
| Order fill | 10-100 ms | Market order, liquid markets |
| Total | ~1-16 sec | Dominant: TV bar close timing |
Key: This is NOT HFT. Suits 5m-1D strategies. For sub-second, use direct exchange WebSocket.
TradingView (Pine Script Strategy)
|
| HTTPS POST (JSON payload)
v
Nginx (SSL + IP whitelist)
|
v
Webhook Receiver (Express :3400)
- Parse JSON, verify secret, validate
|
v
Signal Router
- Risk check (max daily loss, position sizing)
- Cooldown (min time between trades)
- Duplicate detection
|
+---> Bybit V5 API (place order)
+---> Logger (trades.log)
+---> Telegram notify (@Bender137_bot)
| Item | Cost | Notes |
|---|---|---|
| TradingView Essential | $12.95/mo | Minimum for webhooks |
| TradingView Plus | $24.95/mo | 100 alerts, multi-pair |
| Bybit API | Free | |
| Server (existing) | $0 | Already on szhub.space |
| Total | $13-25/mo |