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


blog photo
Joanne SnelSeptember 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!

Title Card for the Article "Setting up your own lemon.markets Telegram trading bot"

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.

iPhone GIF of using Telegram 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 API key, 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.SPACE:[MessageHandler(Filters.text & ~Filters.regex('^/'),
8                                          TradingBot().get_space_id],
9            TradingBot.TYPE:[MessageHandler(Filters.text & ~Filters.regex('^/'),
10                                              TradingBot().get_type)],
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_space 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 select the space they wish to use, we expect the user-input to be a string:

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 ‘SPACE’ state and a text input that does not begin with ‘/’ is received, run the get_space 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 Space() 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 (note that the range is updated once we introduce more conversation states). 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 select their Space of choice and moves into the first conversation state: SPACE. As we saw in ConversationHandler.py, once the conversation is in the SPACE state, the get_space function is called. In this function, the user input is used to retrieve the appropriate Space ID and the user is informed of their choice.

Can you figure out what the cancel function does?

Incorporating lemon.markets 🍋

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

1import os
2import requests
3class Space():
4  
5    def __init__(self):
6        self.api_key: str = os.environ.get("API_KEY")
7        self.url_trading: str = os.environ.get("BASE_URL_TRADING")
8  
9    def get_spaces(self):
10        endpoint = f'spaces'
11        results = requests.post(self.url_trading + endpoint, 
12                                headers={"Authorization": f"Bearer {self.api_key}"}
13                               ).json()['results']
14        spaces: dict = {}
15          
16        for result in results:
17            spaces[result['name']] = result['id']
18            
19    return spaces

The Space() class is initialised with your API key and the lemon.markets (paper) trading URL, which we have defined in an .env file:

1API_KEY=<api key here>
2BASE_URL_TRADING=<trading URL here>
3BOT_TOKEN=<bot token here>

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

Our function get_space() works by outputting a dictionary of Space names (keys) and IDs (values) when we call the /spaces 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.SPACE:[MessageHandler(Filters.text & ~Filters.regex('^/'),
29                                          TradingBot().get_space)],
30        },
31        # if user currently in conversation but state has no handler or 
32        # handle inappropriate for update
33        fallbacks=[CommandHandler('cancel', TradingBot().cancel)],
34    )
35    dispatcher.add_handler(conv_handler)
36    # Start the Bot
37    updater.start_polling()
38    # Run the Bot until you press Ctrl-C
39    updater.idle()
40if __name__ == '__main__':
41    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 allows you to select a Space. 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 Space selection. 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

blog photo

Blog 38 - The market maker explained

Market Makers are crucial to provide liquidity to stock exchanges. In this blog post, we talk about what Market Makers do and why they are useful.

blog photo

5 (+1) YouTube channels for FinTech enthusiasts 

YouTube is a great way to learn about new things, including financial education or coding. Therefore, in this article we’d like to introduce you to 5 YouTube channels to level up your trading literacy.

blog photo

A short introduction to derivatives

In this article, we'd like to introduce you derivatives - they come up in finance and leave a lot of people scratching their heads, though it's totally worth it! Curious about hearing what's the difference between investing and trading, the coherence between finance and weather & why the Greeks even appear here? We'll discuss who’s using them, what they are and how they can be valued in the following.

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.

Need any help?
Ask a question in our CommunityAsk a question in our CommunityGet started with our DocumentationGet started with our DocumentationGet inspired on our BlogGet inspired on our Blog
© lemon.markets 2021Privacy PolicyImprint
All systems normal

As a tied agent under § 3 Sec. 2 WplG on the account and under the liability of DonauCapital Wertpapier GmbH, Passauer Str. 5, 94161 Ruderting (short: DonauCapital), lemon.markets GmbH offers you the receipt and transmission of orders for clients (§ 2 Sec. 2 Nr. 3 WpIG) of financial instruments according to § 3 Sec. 5 WpIG as well as brokerage of accounts.