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


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


Note: If you haven’t read the first article in this two-part series, click here to find out how to set up Telegram and get it connected to the lemon.markets API. 

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. In addition, I’ll show how to convert user-input into API requests and provide the user with useful data, such as presenting the current price of an instrument before confirming a trade. After reading this article, you will know how to program your Telegram bot such that it can place and activate a trade. 

I’m assuming that you’ve already set up your Telegram bot and connected it to the lemon.markets authorisation process. If you haven’t, I urge you to read this article first. If you want to try out our bot before implementing your own, feel free to text @LemonTraderBot directly in Telegram. We’ve set up the bot as an example for you to see how it could look like. That being said, let’s determine where we are and where we’re going:

Current State of Affairs 🔎

At the moment, the bot performs the following actions:

  1. prompt user to fill in client ID,
  2. prompt user to fill in client secret, 
  3. retrieve lemon.markets access token through authentication endpoint. 

With this access token, we can authenticate all of the API requests we’re going to make in the following additions to this project. Recall that, in the previous article, we set up and used a ConversationHandler to hold the two different conversation states. Now, we’re simply going to add additional states that perform certain API calls based on user input. 

What’s the Plan? 🏁

We want to structure the bot such that the following steps can be fulfilled:

  1. prompt user to select instrument type, 
  2. prompt user to fill in instrument name, 
  3. search lemon.markets database, 
  4. prompt user to select desired instrument from list, 
  5. prompt user to indicate side, i.e. ‘buy’ or ‘sell’, 
  6. prompt user to indicate quantity, 
  7. place order and prompt user to confirm
  8. activate order and send confirmation message. 

We’re going to set up six additional states in this article: TYPE, REPLY, NAME, ISIN, SIDE and QUANTITY. We’ve got a ways to go, so let’s get started right away!

A sample conversation you could have with the @LemonTraderBot

Bot Implementation 🤖

If you want to follow along, the GitHub repository can be found here

Selecting Instrument Type: TYPE 🎸

Another nifty feature of the Telegram API (and the python-telegram-bot wrapper) is the ability to create custom keyboards. To increase ease of use, you can opt to allow your user to click buttons with predefined responses, rather than typing out a reply. To select an instrument type, there are only a few options available, therefore this is the perfect scenario in which to use a custom keyboard. Let’s imagine we allow our bot to trade only stocks and ETFs, then:

1reply_keyboard = [['Stock', 'ETF']]

The keyboard is implemented as follows:

1update.message.reply_text(
2    'Authentication successful.\n\n'
3    'What type of instrument do you want to trade?',
4    reply_markup=ReplyKeyboardMarkup(
5        reply_keyboard, one_time_keyboard=True,
6    ),
7)

These changes are made to the get_client_secret() method in the TradingBot.py class. And, to continue the conversation, we’ll need to return the next state (i.e. TradingBot.TYPE) rather than end the conversation (as we did in the first article). 

PS. make sure to initialise TYPE at the beginning of your script! (along with all the other states)

Searching lemon.markets: REPLY, NAME & ISIN 🍋

We’re going to introduce three new methods in TradingBot.py: get_search_query(), get_instrument_name() and get_isin().

1def get_search_query(self, update: Update, context: CallbackContext) -> int:
2    """Prompts user to enter instrument name."""
3    # store user response in dictionary with key 'type'
4    context.user_data['type'] = update.message.text.lower()
5    update.message.reply_text(
6        f'What is the name of the {context.user_data["type"]} you would like to trade?')
7    print(f'user data: {context.user_data}')
8    return TradingBot.REPLY
9def get_instrument_name(self, update: Update, context: CallbackContext) -> int:
10    """Searches for instrument and prompts user to select an instrument."""
11    context.user_data['search_query'] = update.message.text.lower()
12    print(f'user data: {context.user_data}')
13        
14    try:
15        instruments = Instrument(context.user_data['access_token']).
16            get_titles(context.user_data['search_query'], context.user_data['type'])
17    except Exception as e:
18        print(e)
19        update.message.reply_text(
20            "There was an error, ending the conversation."
21            "If you'd like to try again, send /start.")
22        return ConversationHandler.END
23    titles = list(instruments.keys())
24    reply_keyboard = [titles]
25    update.message.reply_text(
26        f'Please choose the instrument you wish to trade.',
27        reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
28    )
29    return TradingBot.NAME
30def get_isin(self, update: Update, context: CallbackContext) -> int:
31    """Retrieves ISIN and prompts user to select side (buy/sell)."""
32    text = update.message.text
33    print(f'user data: {context.user_data}')
34    try:
35        instruments = Instrument(context.user_data['access_token']).
36            get_titles(context.user_data['search_query'], context.user_data['type'])
37    except Exception as e:
38        print(e)
39        update.message.reply_text(
40            "There was an error, ending the conversation."
41            "If you'd like to try again, send /start.")
42        return ConversationHandler.END
43    context.user_data['title'] = text
44    context.user_data['isin'] = instruments.get(text)
45    reply_keyboard = [['Buy', 'Sell']]
46    update.message.reply_text(
47        f'Would you like to buy or sell {context.user_data["title"]}?',
48        reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)
49    )
50    return TradingBot.ISIN

The first function, get_search_query() is pretty self-explanatory: we store the user input (regarding type) in user_data and prompt the user to fill in the stock or ETF name.

The function get_instrument_name() is a little bit more complicated, mostly because we encounter a new class that we haven’t defined yet: Instrument (which corresponds to the lemon.markets Instrument endpoint). The next function, get_isin() uses the same Instrument class, so let’s look into the class first.

1import os
2from helpers import RequestHandler
3class Instrument(RequestHandler):
4    def get_titles(self, search_query: str, instrument_type: str):
5        endpoint = f'instruments/?search={search_query}&type={instrument_type}'
6        response = self.get_data_market(endpoint)
7        results = response['results']
8        instruments: dict = {}
9        if len(results) <= 3:
10            for result in results:
11                instruments[result['title']] = result['isin']
12        else:
13            for result in results[:4]:
14                instruments[result['title']] = result['isin']
15        return instruments
16			
17    def get_price(self, isin: str):
18        mic = os.getenv("MIC")
19        endpoint = f'quotes/?from=latest&mic={mic}&isin={isin}'
20        response = self.get_data_market(endpoint)
21        print(response)
22        bid = response['results'][0]['b']
23        ask = response['results'][0]['a']
24        return bid, ask

As you can see, the Instrument class mimics the Token class — it refers to a particular lemon.markets endpoint and retrieves a response based on user-input. In this case, we have two methods: get_titles() and get_price(). The former retrieves a dictionary of titles (key) & ISINs (value) of the top three search results following a particular query and the latter retrieves a tuple of bid and ask price based on an ISIN.

To revisit the methods in the previous code snippet, get_instrument_name() constructs a reply keyboard of dictionary keys. Then, depending on which button the user clicks, get_isin() retrieves the ISIN (value) that is associated with that title. Note that we place any calls to the API within a try-except block to catch any invalid responses from the API (which might occur if we feed it unexpected information).

Preparing the Order: SIDE, QUANTITY 💁

Now that we’ve gathered which instrument will be traded, we have to determine in which direction (buy/sell) and how many.

1def get_side(self, update: Update, context: CallbackContext) -> int:
2    """Retrieves total balance (buy) or amount of shares owned (sell), most recent price and prompts user to
3    indicate quantity. """
4    context.user_data['side'] = update.message.text.lower()
5    print(f'user data: {context.user_data}')
6    try:
7        [context.user_data['bid'], context.user_data['ask']] = Instrument(context.user_data['access_token']).\
8            get_price(context.user_data['isin'])         
9        context.user_data['balance'] = Space(context.user_data['access_token']).\
10            get_balance(context.user_data['space_uuid'])
11    except Exception as e:
12        print(e)
13        update.message.reply_text(
14            "There was an error, ending the conversation. If you'd like to try again, send /start.")
15        return ConversationHandler.END
16    # if user chooses buy, present ask price, total balance and ask how many to buy
17    if context.user_data['side'] == 'buy':
18        update.message.reply_text(
19            f'This instrument is currently trading for €{context.user_data["ask"]}, your total balance is '
20            f'€{round(context.user_data["balance"], 2)}. '
21            f'How many shares do you wish to {context.user_data["side"]}?'
22        )
23    # if user chooses sell, retrieve how many shares owned
24    else:
25       positions = Portfolio(context.user_data['access_token']).get_portfolio(context.user_data['space_uuid'], )
26       # initialise shares owned to 0
27        context.user_data['shares_owned'] = 0
28       # if instrument in portfolio, update shares owned
29       for position in positions:
30            if position['instrument']['isin'] == context.user_data['isin']:
31                context.user_data['shares_owned'] = position['quantity']
32       update.message.reply_text(
33           f'This instrument can be sold for €{round(context.user_data["bid"], 2)}, you currently own '
34            f'{context.user_data["shares_owned"]} share(s). '
35            f'How many shares do you wish to {context.user_data["side"]}?'
36        )
37    return TradingBot.SIDE
38def get_quantity(self, update: Update, context: CallbackContext) -> int:
39    """Processes quantity, places order (if possible) and prompts
40    user to confirm order. """
41    context.user_data['quantity'] = float(update.message.text.lower())
42    print(f'user data: {context.user_data}')
43    reply_keyboard = [['Confirm', 'Cancel']]
44    # determine total cost of buy or sell
45    if context.user_data['side'] == 'buy':
46        context.user_data['total'] = context.user_data['quantity'] * float(context.user_data['ask'])
47    else:
48        context.user_data['total'] = context.user_data['quantity'] * float(context.user_data['bid'])
49    valid_time = (datetime.datetime.now() + datetime.timedelta(hours=1)).timestamp()
50    else:
51        try:
52            # ensure you have a valid token
53            context.user_data['access_token'] = Token().authenticate(
54                context.user_data['client_id'],
55                context.user_data['client_secret']
56            ).get('access_token')
57            # place order
58            context.user_data['order_uuid'] = \
59                Order(token=context.user_data['access_token']).place_order(
60                    isin=context.user_data['isin'],
61                    valid_until=valid_time,
62                    side=context.user_data['side'],
63                    quantity=context.user_data['quantity'],
64                    space_uuid=context.user_data['space_uuid']
65                )['uuid']
66        except Exception as e:
67            print(e)
68            update.message.reply_text(
69                "There was an error, ending the conversation. If you'd like to try again, send /start.")
70            return ConversationHandler.END
71        update.message.reply_text(
72            f'You\'ve indicated that you wish to {context.user_data["side"]} {int(context.user_data["quantity"])} '
73            f'share(s) of {context.user_data["title"]} at a total of €{round(context.user_data["total"], 2)}. '
74            f'Please confirm or cancel your order to continue.',
75            reply_markup=ReplyKeyboardMarkup(
76                reply_keyboard, one_time_keyboard=True,
77            )
78        )
79        return TradingBot.QUANTITY

In the above code snippet, we introduce three new classes: Space, Portfolio and Order that all use the Spaces endpoint. The above code follows the same format that we’ve been implementing thus far, so to avoid being repetitive, let’s dive into what these classes entail instead.

1from helpers import RequestHandler
2class Space(RequestHandler):
3    def get_space_uuid(self,):
4        endpoint = f'spaces'
5        response = self.get_data_trading(endpoint)['results']
6        return response[0]['uuid']
7    def get_balance(self, space_uuid):
8        endpoint = f'spaces/{space_uuid}/'
9        response = self.get_data_trading(endpoint)
10        return float(response['state']['cash_to_invest'])

The Space class has two functions: get_space_uuid() and get_balance(). From the API response, we return the space_uuid and the cash_to_invest. Note: the get_space_uuid() function is used within the get_client_secret() function in the TradingBot class to store the space_uuid in user_data. Have a look at the GitHub repository for clarification.

1from helpers import RequestHandler
2class Portfolio(RequestHandler):
3    def get_portfolio(self, space_uuid) -> list:
4        endpoint = f'spaces/{space_uuid}/portfolio/'
5        response = self.get_data_trading(endpoint)
6        return response['results']

The Portfolio class retrieves all the current positions (including information such as the average price, see our documentation to see the API output).

1from helpers import RequestHandler
2class Order(RequestHandler):
3    def place_order(self, isin: str, valid_until: float, quantity: int, side: str, space_uuid: str):
4        order_details = {
5            "isin": isin,
6            "valid_until": valid_until,
7            "side": side,
8            "quantity": quantity,
9        }
10        endpoint = f'spaces/{space_uuid}/orders/'
11        response = self.post_data(endpoint, order_details)
12        return response
13    def activate_order(self, order_uuid: str, space_uuid: str):
14        endpoint = f'spaces/{space_uuid}/orders/{order_uuid}/activate/'
15        response = self.put_data(endpoint)
16        return response
17    def get_order(self, order_uuid: str, space_uuid: str):
18        endpoint = f'spaces/{space_uuid}/orders/{order_uuid}/'
19        response = self.get_data_trading(endpoint)
20        return response

The Order class can place, activate and get an order. If you want to see these classes in action, check the repo!

We use all three aforementioned classes in the TradingBot class. As you can see in the code snippet at the top of this sub-section, we have the get_side() and get_quantity() functions. Depending on whether the user decides to buy or sell the instrument, the get_side() function has two different paths it can take. Space.get_balance() and Instrument.get_price() are called to obtain the available funds and current price of the chosen instrument. These two pieces of information are displayed in a message to the user when they are prompted to indicate quantity. Eventually, this information can also be used to restrict orders if they fall outside the bounds of their funds (we don’t implement that in this blog-post, but if you check out our GitHub repository, you can find it there).

If the user decides to place a buy order, the ask price is presented and no additional actions are taken. If the user chooses to sell, the Portfolio class is called to retrieve all current positions. Then, the bid price is presented along with the number of currently held positions.

TradingBot.get_quantity() stores the quantity and places the order with an up-to-date access token. The last step is to activate the order.

Activating the Order: CONFIRMATION ✅

At lemon.markets, we want to make sure that you’re aware of the trades that are being placed. Therefore, after you place an order, it also needs to be activated — that’s what’s happening in the confirm_order() function.

1def confirm_order(self, update: Update, context: CallbackContext) -> int:
2        """Activates order (if applicable), displays purchase/sale price and prompts user to indicate whether any
3        additional trades should be made. """
4        context.user_data['order_decision'] = update.message.text
5        print(f'user data: {context.user_data}')
6        if context.user_data['order_decision'] == 'Cancel':
7            update.message.reply_text(
8                'You\'ve cancelled your order.'
9            )
10        else:
11            try:
12                Order(context.user_data['access_token']).activate_order(
13                    context.user_data['order_uuid'],
14                    context.user_data['space_uuid']
15                )
16            except Exception as e:
17                print(e)
18                update.message.reply_text(
19                    "There was an error, ending the conversation. If you'd like to try again, send /start.")
20                return ConversationHandler.END
21            # keep checking order status until executed so that execution price can be retrieved
22            update.message.reply_text(
23                'Please wait while we process your order.'
24            )
25            while True:
26                order_summary = Order(context.user_data['access_token']).get_order(
27                    context.user_data['order_uuid'],
28                    context.user_data['space_uuid']
29                )
30                if order_summary.get('status') == 'executed':
31                    print('executed')
32                    break
33                time.sleep(1)
34            context.user_data['average_price'] = order_summary.get('average_price')
35            update.message.reply_text(
36                f'Your order was executed at €{round(float(context.user_data["average_price"]), 2)} per share. '
37            )
38        return ConversationHandler.END

In addition, the function also keeps retrieving the order information until the status goes from ‘Activated’ to ‘Executed’. Only then can the final purchase or sell price be retrieved. The bot sends a confirmation message and the conversation ends.

The last step is to insert these states and functions into our ConversationHandler. Let’s do that now.

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 message
5        # when conversation with them is currently in that state
6        states={
7            TradingBot.ID: [MessageHandler(Filters.text & ~Filters.regex('^/'), TradingBot().get_client_id)],
8            TradingBot.SECRET: [MessageHandler(Filters.text & ~Filters.regex('^/'), TradingBot().get_client_secret)],
9            TradingBot.TYPE: [
10                MessageHandler(
11                    Filters.regex('^(Stock|stock|Bond|bond|Fund|fund|ETF|etf|Warrant|warrant)$') & ~Filters.regex('^/'),
12                    TradingBot().get_search_query)],
13            TradingBot.REPLY: [MessageHandler(Filters.text & ~Filters.regex('^/'), TradingBot().get_instrument_name)],
14            TradingBot.NAME: [MessageHandler(Filters.text & ~Filters.regex('^/'), TradingBot().get_isin)],
15            TradingBot.ISIN: [MessageHandler(Filters.text & ~Filters.regex('^/'), TradingBot().get_side)],
16            TradingBot.SIDE: [MessageHandler(Filters.text & ~Filters.regex('^/'), TradingBot().get_quantity)],
17            TradingBot.QUANTITY: [MessageHandler(Filters.text & ~Filters.regex('^/'), TradingBot().confirm_order)],
18        },
19        # if user currently in conversation but state has no handler or handle inappropriate for update
20        fallbacks=[CommandHandler(('cancel', 'end'), TradingBot().cancel)],
21    )

We understand that that was a lot of information and perhaps a bit difficult to follow due to the size of the project. If you’re more of a visual learner, we’ve also created a YouTube tutorial that walks you through the whole process. And, if you’d rather just see the code, check out our GitHub repository, which includes everything we’ve mentioned in this post (along with some other useful features). You can clone the repository and recreate the project on your local device.

Further Improvements

The fun thing about this project is that it can forever be expanded with new features. We’ve implemented some additional features into our bot, such as (but not limited to):

  • when searching for an instrument, allow user to select ‘Other’ if the instrument is not in the list; then, prompt user to fill in their search query again,
  • when indicating a quantity, if user enters a non-integer, send error message (so far, lemon.markets does not support fractional shares),
  • when placing a buy order, send error message if buying more than available balance allows (lemon.markets does not support leveraging),
  • when placing a sell order, send error message if selling more than user owns (lemon.markets does not support going short) and/or
  • differentiate between user_data and chat_data; store all user-specific information in the former, such as client ID, client secret, access token and space UUID and all order-specific information in the latter, then when order is complete, just clear the chat_data and user does not need to log in again to place other trade.

But, besides this, there’s a lot more features you might think to implement, such as:

  • implement command that suggests a stock purchase based on technical signals,
  • allow user to quickly place trade by writing ‘buy 10 Tesla shares’ rather than having to go through the whole conversation and/or
  • implement command that outputs relative and/or absolute performance.

If you end up implementing any of these features, let us know! Or send a pull-request to the GitHub repository, we’d love to see what you built.

We hope this article served as some inspiration to integrate lemon.markets into your own Telegram bot. Remember, the contents of this article can also be found in video-format on our YouTube channel (along with several other interesting projects). And, don’t forget to check out the corresponding GitHub repository.

Our waitlist is steadily growing, have you signed up yet? We’d love to see you on board. And tell us what you’re building! Whether that’s through Slack or email.

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