top of page

Perplexity + Python to Build an AI Stock Alert Telegram Bot

  • Writer: Nikhil Adithyan
    Nikhil Adithyan
  • 2 days ago
  • 8 min read

Integrating Perplexity AI for intelligent news filtering


ree

Our previous article on developing a custom alerting mechanism using FMP Endpoints to retrieve data and Telegram for notifications is an excellent way to receive events of interest almost in real-time as they occur.


One of the biggest mistakes someone can make with alerts is getting excited about all the information they can receive, leading them to configure as many alerts as they can think of. This results in an enormous amount of information; their mobile just won’t stop vibrating with notifications. They become overwhelmed by this flood of information, which ultimately causes them to stop reading the messages and give up.


The solution to this issue — aside from the obvious options of not configuring so many alerts, limiting the number of stocks you monitor, or increasing your notification thresholds — is to introduce AI to filter the received information. This is especially useful for news, where there is no other way to automatically filter which news you receive based on its importance. In this article, we will demonstrate how to use the Perplexity API to filter the news obtained through the FMP Stock News API.


What to expect in this article

In this article, we will develop a Python script that will do the following:


  • Will get the news of a list of stocks

  • For each news item, we will send it to Perplexity’s API and ask for a sentiment score from -1 (negative) to 1 (positive)

  • Based on the sentiment score we receive, we will notify ourselves via Telegram only for news exceeding a predefined threshold.

  • Additionally, we will monitor the cost of API usage to ensure it does not exceed a specified limit.


How to set up Perplexity API

To set up the Perplexity API, click on your user icon at the bottom left of your screen, then select the API option. If you’ve never used this before, you’ll be prompted to create an API Group.


ree

The logic of the group is useful for enterprise use and having different members for various APIs. If this is not necessary, you will probably only need one group. Also, note that Perplexity API does not offer a Free Tier, so you’ll need to add a payment method and purchase some credits. If you are a PRO user, you already receive $5 of free credits per month for API usage.


After that, you should select the “API Keys” option, which will lead you to a list of your previously created API keys. If you have never used it, this list will be empty. Next, select “Accept the Terms and Generate API Key”. You can choose a name for this API Key (or not, as in my case). The API key will then be created, and you can copy it to your clipboard.


ree

Let’s Code

Now that we have our Perplexity API key, let’s start coding. As usual, we will define the imports and parameters



import requests
import os
import json
from datetime import date, timedelta
import schedule
import time

TELEGRAM_TOKEN = 'YOUR TELEGRAM TOKEN'
TELEGRAM_CHAT_ID = 'YOUR CHAT ID'

FMP_API_KEY = 'YOUR FMP TOKEN'

STOCKS_TO_CHECK = ['AAPL','MSFT','GOOGL']

PERPLEXITY_API_KEY = 'YOUR PERPLEXITY API KEY'

STOP_AT_COST = 1

NEWS_UPDATE_THRESHOLD = 0.7

total_cost = 0

Besides the obvious tokens needed for FMP, Telegram, and Perplexity, the remaining parameters are used for:


  • Your universe to check stocks should include the tickers you want to retrieve news for.


  • Stop At Cost is the variable that tracks the total cost for the specific run with perplexity and will stop the script if this amount is exceeded. Note that the cost is for the current run and does not remember the cost of previous usage.


  • The new threshold for updates is the sentiment score above which you will be notified about the news. This means that an article with a sentiment score greater than 0.7 or less than -0.7 (negative sentiment) will be sent to you via Telegram.


  • total_cost is the variable that will keep the total cost during the run


Now, let’s recall two important functions from the previous article. One is the logging system, which ensures we do not process the same news item repeatedly, and the other is the one that sends the message to Telegram.



def get_log(file_log_name):
    if not os.path.exists(file_log_name):
        with open(file_log_name, "w", encoding="utf-8") as f:
            json.dump([], f, ensure_ascii=False, indent=2)
        print(f"Created {file_log_name}")
    return json.load(open(file_log_name, "r", encoding="utf-8"))

def send_telegram_message(message: str):
    url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
    payload = {
        "chat_id": TELEGRAM_CHAT_ID,
        "text": message,
        "parse_mode": "Markdown"  
    }

    response = requests.post(url, data=payload)

    response.raise_for_status()
    print("Message sent:", response.json())

The next step is to begin the Perplexity integration. We will create a function that takes the article text as input and returns the payload that should be sent to Perplexity. We developed this as a separate function because it is quite important, and we need to pause here to explain its purpose.


  • We will configure the Sonar model — this is the most basic model for perplexity and a good starting point for our article. When, and if, you encounter more complex needs, you should consider the advantages and disadvantages of each supported model and choose carefully.


  • The role system is the persona you want your model to adopt. In our case, it should behave like a financial analyst and have the know-how to assess the sentiment of news items.


  • Role user: This is the actual question. We are asking (1) to analyse the sentiment on a scale from -1 (negative) to 1 (positive), (2) also inquire about the time period during which that news is expected to have an impact, and (3) specify that the reply should be a JSON object instead of plain text to be read.


  • Response format: We ensure that the return will be a JSON object with a specific schema (note how you should set up enums to have a uniform reply through the API).


  • Temperature controls creativity versus precision; 0.7 offers slightly varied but still focused outputs. Lower values make it more deterministic. From 0, it is completely deterministic, up to 1.5 or 2.


  • Max Tokens limits the response length for efficiency, since only a small JSON object is expected



def get_perplexity_payload(text):
    payload = {
        "model": "sonar",
        "messages": [
            {
                "role": "system",
                "content": (
                    "You are a highly experienced financial analyst with deep expertise in interpreting financial news, "
                    "analyzing market trends, and providing precise insights. Your responses should be data-driven, concise, and professional, "
                    "focusing on clarity and actionable information relevant to financial contexts. "
                    "Always assess both the sentiment and the likely timeframe of market impact from the given news."
                ),
            },
            {
                "role": "user",
                "content": (
                        "Analyze the sentiment of the following stock news text with a score from -1 (very negative) to 1 (very positive), "
                        "and identify whether the potential effects of this news are likely to be immediate, short-term, mid-term, or long-term. "
                        "Do not perform any web searches. Return a JSON object in the following format: "
                        "{\"sentiment_score\": number, \"effect_duration\": string}." + text
                ),
            },
        ],
        "response_format": {
            "type": "json_schema",
            "json_schema": {
                "schema": {
                    "type": "object",
                    "properties": {
                        "sentiment_score": {"type": "number"},
                        "effect_duration": {
                            "type": "string",
                            "enum": ["immediate", "short-term", "mid-term", "long-term"]
                        }
                    },
                    "required": ["sentiment_score", "effect_duration"]
                }
            }
        },
        "temperature": 0.7,
        "max_tokens": 100,
    }

return payload

Now we will prepare the function that will be responsible for getting the sentiment:


  • Later, you will see that the FMP Stock News API returns, among other things, the title and the text of the article, which we will pass to the perplexity payload along with the symbol.

  • The function will return the sentiment score, the effect duration, and the total usage (the cost) of the specific call.



def get_sentiment(symbol, title, text):

    url_perplexity = "https://api.perplexity.ai/chat/completions"
    headers_perplexity = {
        "Authorization": f"Bearer {PERPLEXITY_API_KEY}",
        "Content-Type": "application/json",
    }

    text_for_payload = f'For stock with symbol {symbol} I have an article with title: "{title}" and text: "{text}"'
    payload = get_perplexity_payload(text_for_payload)
    response = requests.post(url_perplexity, headers=headers_perplexity, json=payload)
    response = response.json()
    data = json.loads(response['choices'][0]['message']['content'])
    print(f'I got sentiment score: {data["sentiment_score"]} and effect duration: {data["effect_duration"]} with cost: {response["usage"]["cost"]["total_cost"]} for {symbol} and article with title: {title} and text: {text}')
    return data["sentiment_score"], data["effect_duration"], response['usage']['cost']['total_cost']

In the previous article, we had a function called get_stock_news responsible for getting the news for each stock. We will use it as is.



def get_stocks_news(days_to_check=5, limit=3):

    today = date.today()
    from_date = (today - timedelta(days=days_to_check)).strftime('%Y-%m-%d')
    to_date = today.strftime('%Y-%m-%d')

    print(f"Will get the stock news from {from_date} to {to_date}")

    news_data = []

    for stock in STOCKS_TO_CHECK:
        url = f"https://financialmodelingprep.com/stable/news/stock"
        querystring = {"apikey": FMP_API_KEY, "symbols": stock, "from": from_date, "to": to_date, "limit": limit}
        response = requests.get(url, querystring)
        data = response.json()
        print(f"Got {len(data)} news for {stock}")
        news_data = news_data + data
    return news_data

Also, we will use the previous articles’ function run_and_notify_stock_news , however, we will make some small changes:


  • We will call the new function to get the sentiment

  • We will be filtering based on the sentiment threshold, so we don’t send any new articles about the stocks we monitor

  • We will be updating the global variable total_cost each time we call the perplexity API

  • And finally, we will perform a check to ensure that if the cost of a specific run exceeds the defined limit, the script will be stopped to prevent further costs.



def run_and_notify_stock_news():
    global total_cost
    file_log_name = 'stock_news_log.txt'
    news = get_stocks_news()
    news_log = get_log(file_log_name)
    for news_item in news:
        if news_item not in news_log:
            try:
                news_log.append(news_item)
                sentiment_score, effect_duration, cost = get_sentiment(news_item['symbol'], news_item['title'], news_item['text'])
                total_cost = total_cost + cost

                if (sentiment_score > NEWS_UPDATE_THRESHOLD) | (sentiment_score < -NEWS_UPDATE_THRESHOLD):
                    if sentiment_score > NEWS_UPDATE_THRESHOLD:
                        message = f"🚀 Positive news with sentiment *{sentiment_score}* and *{effect_duration}* effect, for *{news_item['symbol']}:* {news_item['title']}. [link]({news_item['url']})"
                    elif sentiment_score < -NEWS_UPDATE_THRESHOLD:
                        message = f"⚠️ Negative news with sentiment *{sentiment_score}* and *{effect_duration}* effect, for *{news_item['symbol']}:* {news_item['title']}. [link]({news_item['url']})"
                    else:
                        message = 'No message to send'
                    send_telegram_message(message)

                # Log the news so do not send it again
                with open(file_log_name, "w", encoding="utf-8") as f:
                    json.dump(news_log, f, ensure_ascii=False, indent=2)
            except Exception as e:
                send_telegram_message(f"Error getting sentiment for {news_item['symbol']}: {e}")

            #Stop if the cost exceeds the threshold
            if total_cost >= STOP_AT_COST:
                print(f"Stopping at cost: {total_cost}")
                break
    send_cost_notification()

At the end of our script, we will be adding the following:


  • Schedule the news to be analysed hourly at 00 minutes.

  • Schedule to receive a notification of the total cost so far every hour at 30 minutes past the hour. We can also use this as a heartbeat to check if your script is running. If we don’t receive any message for some time, it might mean that there are no important messages so far, or our script has stopped running for some reason. The latter can be checked if we did not receive the heartbeat either.


def send_cost_notification():
    send_telegram_message(f"Total cost so far: {total_cost}")

def main():
    send_telegram_message("Bot Started!")
    schedule.every().hour.at(":00").do(run_and_notify_stock_news)
    schedule.every().hour.at(":30").do(send_cost_notification)
    try:
        while True:
            schedule.run_pending()
            time.sleep(1)
    except KeyboardInterrupt:
        send_telegram_message(f"Bot Stopped with total cost so far {total_cost}")
    except Exception as e:
        send_telegram_message(f"Bot crashed with error: {e}. Total cost so far: {total_cost}")

if __name__ == "__main__":
    run_and_notify_stock_news()
    main()

Now we have our script ready to run. Below, you can see two cases for which we received notifications of important news.


You can see that Apple had some important positive news with new highs; however, Google received some bad news with OpenAI’s Browser forcing Google’s stock to decline…


ree

ree


Next Steps and Closure

There are many ways you can experiment with AI and its applications.


  • With the news, you can experiment with different prompts and models. What we demonstrated here is a simple prompt designed to capture the overall sentiment of the stock. You can become more explicit and, for example, modify it to try to predict if the stock’s price will peak and trough within the next three months. If the low price exceeds 10%, you can then receive a notification.


  • You can make your own predictions. For example, each weekend, send the stock prices along with some technical indicators, sector index, and fundamental data like P/E or ROE, and ask for perplexity to predict whether the stock will outperform its sector index.


  • You can send SEC filings or earnings transcripts as soon as they are published and receive summaries based on a custom prompt, so you don’t have to read (or listen) to the entire call.


The information is available, and the tools are there as well. It’s simply a matter of using them wisely. Developing AI prompts is a whole new realm in the industry. Find what suits your style and strategy. Ask the right questions to AI using the proper data, and you will be able to trade profitably with confidence.


I hope you enjoyed this article.

Comments


Bring information-rich articles and research works straight to your inbox (it's not that hard). 

Thanks for subscribing!

© 2023 by InsightBig. Powered and secured by Wix

bottom of page