Stocks notification: TradingView webhook to telegram

Stocks notification: TradingView webhook to telegram

Tradingview script alert + webhook -> API server -> Telegram

At the start of the year, I worked on a telegram bot that sends market data of certain stocks for the previous day's close to a telegram channel.


  • Basic info such as closing price, EMA20, difference between closing price and EMA20

  • Overextension from EMA20 based on the median delta when stock reverse in the next few days


Overview of the workflow

  1. Create an indicator script in TradingView

  2. Add indicator to chart

  3. Set an alert using the indicator as condition, and configure webhook.

  4. Once an alert is fired, the server saves the data in redis, and sends a notification to telegram before the market opens.

What's not covered

Infrastructure(set up server, HTTPS, etc), CICD, setting up an API server

Other data sources: VIX central

Create a script in TradingView

PineScript introduction

TradingView has its own programming language called PineScript, which allows us to create indicators and trading strategies.

Here are some recommended readings on PineScript:

Execution model explains how the script is executed on the chart, particularly the difference between historical and real-time bars.

Types and variable declaration.

Alerts to send data to a self-defined destination.

Create the script

The script that I have created retrieves the closing price and the 1D Exponential Moving Average(EMA) 20 for some tickers.

It uses built-in functions ta.ema to get the EMA and to get data for a particular ticker and timeframe, and alert.freq_once_per_bar_close to send the alert when the market closes.

Ticker name

The ticker argument for needs to be in the format <exchange><ticker>, which can be gotten from the address bar after searching for the ticker in

For SPY, it is


There is a self-defined secret <your secret here>, which is used to authenticate the request in the server.

indicator("Market data notification")

ema20 = ta.ema(close, 20)

array_start = '['
array_end = ']'
json_start = '{'
json_end = '}'

build_json_array(res, symbol, timeframe, closee, ema20) =>
    res + json_start + '"symbol": ' + symbol + ', "timeframe": ' + timeframe + ', "close": ' + closee + ', "ema20": ' + ema20 + json_end + ','

string[] json_key_values = array.new_string(0)

array.push(json_key_values, '"secret": "<your secret here>"')

data_payload = '"data": ' + array_start
data_payload := build_json_array(data_payload, '"SPY"', '"1d"', str.tostring("AMEX:SPY", "1D", close)), str.tostring("AMEX:SPY", "1D", ema20)))
data_payload := build_json_array(data_payload, '"QQQ"', '"1d"', str.tostring("NASDAQ:QQQ", "1D", close)), str.tostring("NASDAQ:QQQ", "1D", ema20)))
data_payload := build_json_array(data_payload, '"DJIA"', '"1d"', str.tostring("AMEX:DJIA", "1D", close)), str.tostring("AMEX:DJIA", "1D", ema20)))
data_payload := build_json_array(data_payload, '"IWM"', '"1d"', str.tostring("AMEX:IWM", "1D", close)), str.tostring("AMEX:IWM", "1D", ema20)))
data_payload := build_json_array(data_payload, '"AAPL"', '"1d"', str.tostring("NASDAQ:AAPL", "1D", close)), str.tostring("NASDAQ:AAPL", "1D", ema20)))
data_payload := build_json_array(data_payload, '"AMD"', '"1d"', str.tostring("NASDAQ:AMD", "1D", close)), str.tostring("NASDAQ:AMD", "1D", ema20)))
data_payload := build_json_array(data_payload, '"AMZN"', '"1d"', str.tostring("NASDAQ:AMZN", "1D", close)), str.tostring("NASDAQ:AMZN", "1D", ema20)))
data_payload := build_json_array(data_payload, '"BABA"', '"1d"', str.tostring("NYSE:BABA", "1D", close)), str.tostring("NYSE:BABA", "1D", ema20)))
data_payload := build_json_array(data_payload, '"COIN"', '"1d"', str.tostring("NASDAQ:COIN", "1D", close)), str.tostring("NASDAQ:COIN", "1D", ema20)))
data_payload := build_json_array(data_payload, '"GOOGL"', '"1d"', str.tostring("NASDAQ:GOOGL", "1D", close)), str.tostring("NASDAQ:GOOGL", "1D", ema20)))
data_payload := build_json_array(data_payload, '"META"', '"1d"', str.tostring("NASDAQ:META", "1D", close)), str.tostring("NASDAQ:META", "1D", ema20)))
data_payload := build_json_array(data_payload, '"MSFT"', '"1d"', str.tostring("NASDAQ:MSFT", "1D", close)), str.tostring("NASDAQ:MSFT", "1D", ema20)))
data_payload := build_json_array(data_payload, '"NFLX"', '"1d"', str.tostring("NASDAQ:NFLX", "1D", close)), str.tostring("NASDAQ:NFLX", "1D", ema20)))
data_payload := build_json_array(data_payload, '"NVDA"', '"1d"', str.tostring("NASDAQ:NVDA", "1D", close)), str.tostring("NASDAQ:NVDA", "1D", ema20)))
data_payload := build_json_array(data_payload, '"TSLA"', '"1d"', str.tostring("NASDAQ:TSLA", "1D", close)), str.tostring("NASDAQ:TSLA", "1D", ema20)))
data_payload := build_json_array(data_payload, '"VIX"', '"1d"', str.tostring("TVC:VIX", "1D", close)), str.tostring("TVC:VIX", "1D", ema20)))

// remove trailing comma from array
data_payload := str.substring(data_payload, 0, str.length(data_payload) - 1)
data_payload := data_payload + array_end

array.push(json_key_values, data_payload)

array.push(json_key_values, '"test_mode": "false"')

result_json = json_start + array.join(json_key_values, ',') + json_end

alert(result_json, alert.freq_once_per_bar_close)

Add the script in tradingview

Open the Pine Editor tab, add the script, and add it to the chart.

Set the alert and webhook

Set the alert using the indicator in the previous step as the condition, and configure webhook to send it to your server.

Note: Make sure the timeframe of the chart is set to daily(1D), so that the script will get triggered only once a day, when the market closes.

Data storage: Redis

Redis is chosen because it fits my need for a key-value storage, and has the option to persist data.

Data persistence

Redis offers 2 options: Redis Database(RDB) and Append Only File(AOF).

RDB is a snapshot of the data at specific intervals, while AOF is an append-only log of the write operations received by the server.

The tradeoffs of RDB and AOF complement each other, so it makes sense to use both.

For RDB, a snapshot will be created after 300 seconds and there is at least 1 change.

For AOF, write operations will be flushed to disk every second using the default fsync policy appendfsync everysec.

Data structure

Sorted set is chosen to store the data because the elements are sorted by score, which is the timestamp, and supports finding elements by score quickly.

Receive data from tradingview and post to telegram

We need a regular API server that can receive POST request from tradingview, and send it to telegram. I wrote it in Python, using the FastAPI framework, because it is just fast to develop with.

Webhook authenticity check

Since the webhook is a HTTP POST request that anyone can call, we need to ensure that it actually originates from tradingview, not someone else.

The first safeguard is to check the self-defined secret in the PineScript.

The second is to check if the request comes from one of tradingview's machines.

Create telegram bot

Set up bot and channel

Follow this guide to create a telegram bot, and note down the token, which is used to interact with the telegram bot API.

Add the bot as an administrator to the channel so that it can send messages.

Send a message to the channel

The channel id of the channel is needed for the bot to send the message.

There are two ways of getting the channel id:

First, go to the channel on telegram web and look at the URL, which is something like The channel id will be -100YYYYYYYYYY.

The second method is to use a bot to tell you the channel id.

Use a library to send the message

There are many client libraries for different languages. I chose python-telegram-bot.

Cron job

Finally, the last piece of the puzzle is to schedule the message 30 minutes before the market opens(9.30AM local time) using a cron job.

This is the script for sending the message.


One thing to note is that the timezone changes due to daylight savings time(DST). Without DST, the timezone is Pacific Standard Time(PST), which is UTC-5. With DST, it is Pacific Daylight Time(PDT) which is UTC-4.

On the application side, we just need to check against the local time:

def should_run() -> bool:
    if config.get_is_testing_telegram():
        return True

    now = get_current_datetime()
    local = get_current_datetime()
    local = local.replace(hour=config.get_stocks_job_start_local_hour(), minute=config.get_stocks_job_start_local_minute())
    delta = now - local

    should_run = abs(delta.total_seconds()) <= config.get_job_delay_tolerance_second()
        f'local time: {local}, current time: {now}, local hour to run: {config.get_stocks_job_start_local_hour()}, local minute to run: {config.get_stocks_job_start_local_minute()}, current hour {now.hour}, current minute: {now.minute}, delta second: {delta.total_seconds()}, should run: {should_run}')
    return should_run

On the cron side, the script needs to run on the hour with and without DST.

This is the cron used to run the script at 9AM every day from monday to saturday:

# In UTC
0 13,14 * * 1-6

Local development

Since a webhook requires a public HTTPS endpoint, we need a reverse proxy to tunnel traffic from the public to our local server.

Ngrok is simple and easy to use.


This is an overview of how to use push data from Tradingview to our own server and send it to telegram.

Tradingview supports more complex workflows such as executing trades based on certain strategies.



My channel:

Telegram bot API:

Python telegram bot:


Github link for backend:

Github link for infra setup and app automation:

API server:

Script for sending message:



My persistence config:

TradingView PineScript

PineScript doc:

PineScript reference:

My PineScript:

Did you find this article valuable?

Support Yap Han Chiang by becoming a sponsor. Any amount is appreciated!