Setting up your own Telegram bot to trade with the lemon.markets API (Part 1 of 2)


blog photo
Published by Joanne Snel on September 22, 2021
Insights


Hi! My name is Joanne and I’m part of the team at lemon.markets. I’ve been working on a fun use-case for our product for the past few weeks and I’m very excited to share it with you! If you’re not yet familiar with lemon.markets, we’re a start-up from Berlin that’s making it possible for developers to create their own brokerage experience at the stock market. There’s hundreds of use-cases for our product, from automated trading strategies to portfolio visualisation dashboards. Today, I’ll show you how you can connect the lemon.markets API to the Telegram API. Why? So you can have a personalised butler — ahem, bot — that can place trades with a few very simple messages.

In this article, I will run you through setting up your bot, establishing contact with it and, finally, connecting your bot to lemon.markets. To keep things short and sweet, we’ll just be authenticating your account in this article, but there will be a second blog post that shows you all the fun features you can implement into your bot. This tutorial can also be found on YouTube for those of you who learn better visually. And if you directly want to get started, you can also access the publicly available GitHub repository. You can also chat to our  @LemonTraderBot on Telegram to get a feel for what’s possible. Let’s get started!

lemon.markets 🤝 Telegram

Why would you go through the hassle of setting up your own Telegram bot? There’s several reasons, for one, it makes trading more accessible. If you can place trades in the same place where you text your friends, trading becomes much more convenient (or you might just be incentivised to regularly check your portfolio). In addition, this is a great way to create your own frontend brokerage experience when,

  1. you don’t have any frontend experience or, 
  2. you don’t want to invest the time into creating your own frontend. 

And lastly, placing trades conversationally might simply be more intuitive — with frequent checks along the way (along with warnings if you can’t afford your purchase), you’re made aware of what you’re buying. The concept is similar to TaxFix, another Berlin-based start-up making filing taxes a little less painful by presenting it in a chat-bot form. 

The @LemonTraderBot is just a starting point — there’s plenty of edge cases that can be defined (and which you can implement if you foresee them happening in your use-case). There’s also a bunch of additional functionalities that we haven’t thought of yet (and for good reason). With this tutorial, we want to encourage our users to come up with their own products that suit their own needs. 

And if Telegram isn’t your app of choice, you can also set up a WhatsApp or Discord bot.

A sample conversation you could have with the @LemonTraderBot

Setting up your Telegram Bot 🤖


Let’s start the project! After you’ve installed Telegram on your smartphone and signed up for an account, you’ll need to configure your own lemon.markets Telegram Bot. Luckily, Telegram has made this really simple: all you have to do is text @BotFather on Telegram. If you need a step-by-step tutorial, you can follow this official one. In short, you’ll send @BotFather the/newbot command, choose your bot’s name and username and, in turn, you will receive an authorisation token for your bot. This token is similar to your lemon.markets client secret and client ID, so keep it safe (and store it in an .env file) because it allows anyone with access to it to control your bot. You won’t find this file in the GitHub repository because we’ve added it to our .gitignore file. If you’re unfamiliar with this convention, you can read about it here. Unfortunately, we’ve claimed the username @LemonTraderBot, but we’re sure you’ll come up with something equally fitting. 

You can communicate with your bot using the Telegram API, which is quite extensive. I’ve opted to use the python-telegram-bot wrapper, which makes interacting with the API that much easier. Here’s a helpful tutorial to get your first bot running.

Earth to Bot 🌍

Now, once we’re all set up on the Telegram side, we want to establish first contact with our bot. We’re using Python for this tutorial, but feel free to adapt it to whatever language you prefer to write in. 

The python-telegram-bot wrapper works via its Updater and Dispatcher classes. On a higher level, the logic is as follows: you create an Updater object that points to your bot (via its API token). The Updater class is continuously fetching updates from Telegram, and once something is received, it passes it onto the Dispatcher class. Every Updater instance is associated with a Dispatcher object which holds different Handlers. Depending on what kind of input is received, the Dispatcher passes it onto a particular Handler, which is defined by you and performs a certain action. For example, we could (and will) set up a CommandHandler that interprets a user-message ‘/start’ as the sign to initiate a particular conversation. The wrapper contains several different types of Handler subclasses, but we’ll be using the ConversationHandler, CommandHandler and MessageHandler for this tutorial. If this sounds confusing, don’t worry, as soon as you see it in action it’ll become clear. 

Telegram Handlers ✋

We’re going to structure our code using these Handlers. At the core of our project lies the ConversationHandler because at the end of the day, we’re having a conversation with our bot. A ConversationHandler takes three collections as input: entry_pointsstates and fallbacks. The entry_points collection is a list of commands that initiate the conversation. The second collection, a dictionary named states, contains the different conversation steps and one or more related Handlers that is called if the user sends a message when the conversation with them is currently in that state. And the third collection, a list called fallbacks, is used if the user is currently in a conversation but an unexpected response is received. Our ConversationHandler is going to look like this:

1conv_handler = ConversationHandler(
2        # initiate the conversation
3        entry_points=[CommandHandler('start', TradingBot().start)],
4        # different conversation steps and handlers that should be used if user sends a
5        # message when conversation with them is currently in that state
6        states={
7            TradingBot.ID:[MessageHandler(Filters.text & ~Filters.regex('^/'),
8                                          TradingBot().get_client_id)],
9            TradingBot.SECRET:[MessageHandler(Filters.text & ~Filters.regex('^/'),
10                                              TradingBot().get_client_secret)],
11        },
12        # if user currently in conversation but state has no handler or 
13        # handle inappropriate for update
14        fallbacks=[CommandHandler('cancel', TradingBot().cancel)],
15    )

There’s a few things in this code snippet that we haven’t seen yet: a TradingBot() class with several functions, including startget_client_idget_client_secret and cancel. We’ll need to define those shortly, but let’s first talk about the difference between a CommandHandler and a MessageHandler: Telegram recognises a ‘command’ as a user message that starts with a slash (‘/’), for example ‘/start’, and a ‘message’ as any other text input. It’s common practice to initiate a bot with the ‘/start’ command, so we’ll follow that convention and use a CommandHandler. When we expect the user to fill in their client ID, this will look something like:

1b2297358-e23a-4c0a-a156-c7804ef75c4a

Therefore we use the MessageHandler with two filters in that case: one which makes sure that only text input is registered as an appropriate answer from the user (rather than, e.g. an image) and one which makes sure that commands are not incorrectly registered as textual responses. In plain English, lines 7–8 translate to: if the conversation is at the ‘ID’ state and a text input that does not begin with ‘/’ is received, run the get_client_id function on this TradingBot object. The following lines can be interpreted in a similar manner.

Adding Functionality 👷‍♀

Now that the skeleton for our conversation is set up, let’s define those functions of the TradingBot class we mentioned earlier. 

1from telegram import Update
2from telegram.ext import CallbackContext, ConversationHandler
3from models.Token import Token
4class TradingBot:
5    ID, SECRET = range(2)
6    def start(self, update: Update, context: CallbackContext) -> int:
7        """Initiates conversation and prompts user to fill in lemon.markets client ID."""
8        context.user_data.clear()
9        update.message.reply_text(
10            'Hi! I\'m the Lemon Trader Bot! I can place trades for you using the lemon.markets API. '
11            'Send /cancel to stop talking to me.\n\n'
12            'Please fill in your lemon.markets client ID.',
13        )
14        print("Conversation started.")
15        print(context.user_data)
16        return TradingBot.ID
17    def get_client_id(self, update: Update, context: CallbackContext) -> int:
18        """Prompts user to fill in lemon.markets client secret."""
19        context.user_data['client_id'] = update.message.text
20        update.message.reply_text('Please enter your lemon.markets client secret.')
21        print(context.user_data)
22        return TradingBot.SECRET
23    def get_client_secret(self, update: Update, context: CallbackContext) -> int:
24        """Authenticates user."""
25        context.user_data['client_secret'] = update.message.text
26        print(context.user_data)
27        try:
28            authentication = Token().authenticate(context.user_data['client_id'], context.user_data['client_secret'])
29            
30            context.user_data['access_token'] = authentication.get('access_token')
31            # if credentials not correct, prompt user to fill in client ID again
32            if 'access_token' not in authentication:
33                update.message.reply_text(
34                    'Authentication failed. Please fill in your client ID again.'
35                )
36                return TradingBot.ID
37        except Exception as e:
38            print(e)
39            update.message.reply_text(
40                "There was an error, ending conversation. If you'd like to try again, send /start")
41            return ConversationHandler.END
42        update.message.reply_text(
43            'Authentication successful.'
44        )
45        print(context.user_data)
46        return ConversationHandler.END
47    
48    def cancel(self, update: Update, context: CallbackContext) -> int:
49        """Cancels and ends the conversation."""
50        update.message.reply_text(
51            "Bye! Come back if you would like to make any other trades.", reply_markup=ReplyKeyboardRemove()
52        )
53        print(context.user_data)
54        return ConversationHandler.END

Again, just like before, we encounter some functions and classes we haven’t defined yet, like the Token() class that’s imported in line 4. But, let’s talk about the things we do know first. 

In line 7 we define our different conversation states as integers 0 or 1. Each function returns the conversation state that logically follows from the conversation flow. For example, the start function sends a welcome message to the chat, prompts the user to fill in their client ID and moves into the first conversation state: ID. As we saw in ConversationHandler.py, once the conversation is in the ID state, the get_client_id function is called. In this function, the user input is stored in a dictionary unique to the user, the user is prompted to fill in their client secret and the conversation is moved into the SECRET state. 

Once the conversation is in the SECRET state, the get_client_secret function is called. Again, the secret is saved in the user-specific dictionary. Then, within a try-except block, the account is authenticated and the access token is retrieved. This will result in one of three possible scenarios:

  • If there is no access token within the request response (this means that the credentials are invalid), the user is prompted to fill in their details again and the conversation is brought back to the ID state. 
  • If the API does not produce a response, an exception is raised and the conversation ends. 
  • If the authentication is successful and an access token is retrieved, the user is notified and the conversation ends. 

Can you figure out what the cancel function does? Hint: the logic is similar to the exception raised in the previous function. 

Incorporating lemon.markets 🍋

It’s time to address the Token() class that seems to magically authenticate our lemon.markets account. Truth is, you’re the magician and your magic looks as follows:

1import os
2import requests
3class Token():
4    def __init__(self):
5        self.auth_url: str = os.environ.get("AUTH_URL")
6    
7    def get_token(self, endpoint: str, data):
8        response = requests.post(self.auth_url + endpoint, data)
9        return response.json()    
10    def authenticate(self, client_id: str, client_secret: str):
11        token_details = {
12            "client_id": client_id,
13            "client_secret": client_secret,
14            "grant_type": "client_credentials",
15        }
16        endpoint = f'oauth2/token/'
17        response = self.get_token(endpoint, token_details)
18        return response

The Token() class is initialised with an authentication URL, which we have defined in an .env file:

1AUTH_URL=<authorisation URL here>
2BOT_TOKEN=<bot token here>

Note that we’ve also defined the bot token in this .env file, we’ll use it later.

Our two functions get_token() and authenticate() work together to output the lemon.markets API response in JSON format when we call the authentication endpoint. See our documentation to learn more.

Putting it all together 🛠

The ConversationHandler we defined earlier still needs to be placed within a working script. Let’s define our main.py file as follows:

1import logging
2import os
3from dotenv import load_dotenv
4from models.TradingBot import TradingBot
5from telegram.ext import (
6    Updater,
7    CommandHandler,
8    MessageHandler,
9    Filters,
10    ConversationHandler,
11)
12logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
13                    level=logging.DEBUG)
14logger = logging.getLogger(__name__)
15def main() -> None:
16    load_dotenv()
17    """Start the bot."""
18    # Create the Updater and pass it to your bot's token.
19    updater = Updater(os.getenv('BOT_TOKEN'), use_context=True)
20    # Get the dispatcher to register handlers
21    dispatcher = updater.dispatcher
22   conv_handler = ConversationHandler(
23        # initiate the conversation
24        entry_points=[CommandHandler('start', TradingBot().start)],
25        # different conversation steps and handlers that should be used if user sends a
26        # message when conversation with them is currently in that state
27        states={
28            TradingBot.ID:[MessageHandler(Filters.text & ~Filters.regex('^/'),
29                                          TradingBot().get_client_id)],
30            TradingBot.SECRET:[MessageHandler(Filters.text & ~Filters.regex('^/'),
31                                              TradingBot().get_client_secret)],
32        },
33        # if user currently in conversation but state has no handler or 
34        # handle inappropriate for update
35        fallbacks=[CommandHandler('cancel', TradingBot().cancel)],
36    )
37    dispatcher.add_handler(conv_handler)
38    # Start the Bot
39    updater.start_polling()
40    # Run the Bot until you press Ctrl-C
41    updater.idle()
42if __name__ == '__main__':
43    main()

In lines 15–18, we set up a logger, which prints the bot status to the terminal and allows us to ‘see’ what’s happening (also, quite useful for debugging). In the main method, we initialise an Updater object, assign a Dispatcher to it and add our Handler to the Dispatcher. In order to start the bot, we call the start_polling() function on the Updater. We also need to call the idle() function.

And, that should be it. Running this script will result in a bot that authenticates your lemon.markets account. Naturally, this isn’t very exciting in terms of functionality — so, what else is there?

What’s Next? 🤔

If you’ve played around with @LemonTraderBot, you’ll notice that the bot goes far beyond authentication (in fact, in our own trading bot we’ve actually removed the authentication step to allow everyone, even those without a lemon.markets account, to try it out). And your bot can be enriched in a similar fashion — you can allow it to search the lemon.market database, place buy or sell orders, see into your portfolio or report simple metrics like your Space balance. We’re going to implement all that and more in the second part of this blog-post series.

If you can’t wait, you can check out our GitHub repository in the meantime, which should give you a sneak peak of what’s to come. 

If this sounds like a fun project to you and you’re not yet part of our community, sign up to lemon.markets here and join our vibrant Slack. We’ve got active members exchanging their builds and visions. Maybe you could share some of yours? 

More details on how to add packages to your app can be found here. Now, all you need to is run:

If you have any additional questions in regard to this Telegram bot, feel free to send us an email. And if you’ve got any interesting feature requests for this bot, let us know (or create a branch on our repo!). 

Stay tuned for part 2 and see you on 🍋.markets, 

Joanne

You might also be interested in

Mapping a Ticker Symbol to ISIN using OpenFIGI & lemon.markets

blog photo

When you start trading on different exchanges, you’ll notice that sometimes they have unique ways of identifying financial instruments. For example, US exchanges often use tickers, whereas German exchanges reference an ISIN. And sometimes, moving between these symbologies isn’t as smooth as you’d expect.  Instead, automate the process by writing (less than 10 lines of) code to make this ‘translation’ for you. Keep reading to learn how you can use the OpenFIGI and lemon.markets APIs to map tickers to ISINs.

10 mistakes when it comes to (automated) trading & how to avoid them

blog photo

Hi! My name is Joanne and I’m part of the lemon.markets team in Berlin. We’re building a brokerage API to enable developers to curate their own stock market experience. Along with that, we hope to foster an environment where users can find and share their knowledge on the intersection between algorithm development and the stock market, among other things. We want to contribute to this conversation as well, and that’s why we’ve gathered a list of ten mistakes that those beginning in automated trading (and, to be fair, sometimes even seasoned pros) might make. But, most importantly, we’ve also listed how you can avoid them. 

Integrating lemon.markets into your Telegram bot (Part 2 of 2)

blog photo

Hi! I’m Joanne and I’m part of the team at lemon.markets. We’re a Berlin-based start-up powering programmatic trading via APIs. Our goal is to provide the tools to allow developers to design their own brokerage experience at the stock market. There’s about a hundred and one use-cases for our product, one of them being to create your own frontend interface to place trades. You could start from scratch or you could use a preexisting service, like Telegram. In this article, I’ll be extending the project presented in our previous article. We’ll be adding various conversation states that communicate with the lemon.markets endpoints.

Dive Deeper

Find more resources to get started easily

Check out our documentation to find out more about our API structure, different endpoints and specific use cases.

Engage

Join lemon.markets community

Join our Slack channel to actively participate in our community, ask questions to other users and stay up to date at all times.

Contribute

Interested in building lemon.markets with us?

We are always looking for great additions to our team that help us build a brokerage infrastructure for the 21st century.

Products
Pricing
For Developers
SlackGithubBlog
© lemon.markets 2021Privacy PolicyImprint
All systems normal