Make a Discord Bot for you and your friends to trade stocks

Irving Derin
14 min readMar 26, 2021

--

Sitting at home for a year, one starts to explore a variety of new and different hobbies to try and keep their sanity. Me personally, I decided to help my friends find financial ruin with a stock trading Discord bot!

Along this journey I’ve decided to build my new discord friend, Dispaca the Alpaca!

What a cutie!
Alpaca Photo by Carlos Ruiz Huaman on Unsplash

I’ll walk through my steps of building this potentially horrible idea so that others can also share their wealth amongst their friends, with no accountability (yet… Stay tuned for part 2).

⚠️ WARNINGS AND DISCLOSURES ⚠️

This tutorial will be using a paper account on https://app.alpaca.markets/, so there is no real money involved.

IF YOU DECIDE TO USE REAL MONEY, be very mindful that there could be legal and tax ramifications for allowing other people trading securities in your name. I do not know what those ramifications are; the knowledge that they exist is enough to keep me away from giving other people that sort of control. My recommendation is that you too don’t give other people control over your money. Any actions you take outside of that recommendation is your own damn decision.

Here are some useful resources in general:

Step 0 — Setting up an environment

This tutorial won’t go into the knitty-gritty details of my development setup, but I will discuss how I’m hosting this bot and my process in getting changes out into ‘production’.

Caprover on DigitalOcean
Caprover is a deployment platform that I’ve found myself using over the last couple of months to simplifying self hosting some of the open source software I use (bitwarden, ect) as well small personal projects.

I found Caprover since I use DigitalOcean(referral link 💛) as my hosting provider. Their marketplace has an easy to deploy image that gets you started pretty quickly. Just make sure that you’ve got a domain name ready to go.

We’ll be deploying Dockerfiles to Caprover in this tutorial, so any other hosting options or local docker runs will work just as well.

Initial File Structure
We’ll be starting this project off with the following file structure.

.
├── Dockerfile
├── README.md
├── requirements.txt
└── src
└── bot.py

This corresponds to our Docker looking like this.

FROM gorialis/discord.pyWORKDIR /botCOPY requirements.txt ./
RUN pip install -r requirements.txt
COPY /src .ENV DISCORD_TOKEN=[WE WILL GET THIS SOON]
ENV ALPACA_KEY_ID=[WE WILL GET THIS SOON]
ENV ALPACA_KEY_SECRET=[WE WILL GET THIS SOON]
CMD ["python", "./bot.py"]

The base image for this Dockerfile was built with the discord.py library in mind.

With this file structure and docker installed, all we need to do to get up and running is just run the following command.

$ docker build -t dispacapy .
$ docker run dispacapy

Any time we add a change, we just rerun docker build and docker run and we’ll be able to see our changes deployed live!

Step 1 — Setting up your Discord Bot

At the end of this step we’ll have a Discord bot user that we can invite to our server, as well as an application token to add to our Dockerfile.

Photo by Alexander Shatov on Unsplash

First thing we need to do is get our discord bot set up and ready to go.

Note that I’m writing this tutorial after slash commands were introduced, but before I really explored how to use them.

Since we’re only making a chat bot without any special kinds of permission, we don’t need to worry about messy OAuth stuff.

We start off by going to https://discord.com/developers. I’m assuming that you, the reader, have a discord account. If not go ahead and make one!

Once you’re logged in you should find a page that looks similar to this screenshot:

Taken from my own developers homepage

From here we’ll click on New Application and name our wonderful new application. This will bring us to the General Information page.

General Information page for our bot

To make our application an actual bot, we need to click on the Bot sidebar option and then Build-A-Bot. YOU HAVE TO DO THIS TO CREATE A BOT USER.

Before we made our bot.
After we added a bot. That token is long deleted ;)

This is an irreversible action and Discord will make sure to tell you that. Once our bot is created, we can grab our DISCORD_TOKEN for our Dockerfile.

DO. NOT. PUSH. YOUR. DISCORD. TOKEN. TO. GITHUB.

They will find it. They will tell you about it. They will revoke it.

I’m speaking from experience

Finally the last step is to invite your bot to your server. This is done by going to the OAuth2 side menu option. We can get our bot going without requiring us to setup user authentication and redirects. This greatly simplifies our lives.

Permissions and scopes we’ll need for this project

We’ll select the bot OAuth scope, which will generate an invite link that we can use to add our bot to a server. Before we add our bot, we want to make sure we’ve got our permissions selected as well.

For this project we’ll only need Send Messages, Attach Files, and Add Reactions. Once you’re all set, go to the link you’ve generated and you’ll find yourself able to add your bot to any server that gives you the permission to do so.

https://discord.com/api/oauth2/authorize?client_id=myclientid12345&permissions=34880&scope=bot -- THIS IS NOT A REAL URL
Selection screen for where to add your bot.

Step 2 — Setting up your Alpaca account

At the end of this step we’ll have an Alpaca paper account that will allow us to trade stocks, as well as get historical data. Their API documentation is pretty fantastic.

https://app.alpaca.markets is the first Commission Free API Stock Broker

I like Alpaca. It’s easy to get up and running. It gives you a paper account to practice your algorithms and then makes it easy to switch over to real money.

⚠️ BE VERY CAREFUL WHEN TRADING YOUR OWN MONEY. ⚠️

When you make your account and verify your email address, you’ll find yourself in a potentially intimidating screen. We can completely ignore it by switching over to our paper account. We do that by clicking on Live Trading and selecting the paper trading account in the dropdown.

Intimidating signup page
Our safe paper trading account

Once we’re on our paper trading account, we can generate our API Key and add them to our Dockerfile above. This is done by clicking on the View button in the Your API Keys box.

Another set of long deleted credentials.

Step 3 —Relaying data from Alpaca to you

We have a Discord bot account. We have an Alpaca trading account. Lets start building a bot getting data from it. At the end of this step we will have the early workings of a bot that will respond to commands and give us information about an individual stock. It’s always important to be informed when trading.

At the end of this step we’ll have charts on demand

This tutorial won’t touch upon how to do any kind of financial analysis. That’s a complicated topic that I’m not qualified to talk about. There are plenty of other sources on Medium, YouTube and what have you that will teach you about markets.

If we open up bot.py we can get started on returning some information when we ask for it.

## Discord.py is a well supported wrapper for the Discord API
import discord
from discord.ext import commands
## alpaca_trade_api wraps the Alpaca API
import alpaca_trade_api as tradeapi
## We'll be using matplotlib to generate simple line graphs
import matplotlib.pyplot as plt
## Useful imports to have
import io, os
## Environmental Consts
## These are set in the Dockerfile
DISCORD_TOKEN = os.environ.get("DISCORD_TOKEN")
ALPACA_KEY_ID = os.environ.get("ALPACA_KEY_ID")
ALPACA_KEY_SECRET = os.environ.get("ALPACA_KEY_SECRET")
ALPACA_BASE_URL = 'https://paper-api.alpaca.markets'
## These are parameters to make our future charts prettier
plt.rcParams.update({'xtick.labelsize' : 'small',
'ytick.labelsize' : 'small',
'figure.figsize' : [16,9]})
## Connect to your Alpaca accountalpaca_api = tradeapi.REST(ALPACA_KEY_ID,
ALPACA_KEY_SECRET,
base_url=ALPACA_BASE_URL,
api_version='v2')
## Initialize our Discord bot
## Using a command prefix helps find issued commands
bot = commands.Bot(command_prefix='>')
## Create our first command
@bot.command()
async def hello_world(context):
await context.send("Hello Dispaca!")
## Any other commands we want to add should be added here## Start our bot
bot.run(DISCORD_TOKEN)

If we deploy our bot.py from above, we’ll be able to get our first bot communications!

Type in >hello_world into a server that you share with your bot, and you’ll get a response back!

Our first command to Dispaca!

Every subsequent command that we add is going to follow the same structure that the async def hello_world() follows. We use the @bot.command() decorator to designate functions as commands. Be sure to check out the documentation to see what more you can do with the decorators.

Now that we can have a conversation with Dispaca, lets start making it a useful conversation.

@bot.command()
async def account(context):
print("Checking account")
. ## Retrieve your Alpaca paper account info
account_info = alpaca_api.get_account()
await context.send(f"{account_info}")

Adding async def account(context) to your bot will allow you to use the >account command.

Retrieving your account info yields a JSON object

When called, your bot will reach out to the Alpaca API and retrieve your account information. It’ll be ugly but cleaning it up will be for another section. Before we move on, it’s important to note that this will be where we can retrieve our buying power, cash and other important values.

@bot.command()
async def last_price(context, ticker):
print(f"Checking the last price of {ticker}")

last_price = alpaca_api.get_last_trade(ticker)
await context.send(f"Last price for {ticker} was {last_price}")

Here we’ve added >last_price that will do exactly what it says- grab the last price of some stock. As an example we can send >last_price GOOG and get the last price!

Getting useful stock data!

Let us take this moment to talk about exceptions. Discord.py will ignore exceptions in commands. The code as written above can easily fail when getting a command, and the user might never know.

Here’s what happens when you send >last_price goog.

We didn’t get our price data again. goog is different from GOOG

Looking at the output logs from Docker will reveal exactly what went wrong.

Ignoring exception in command last_price:
...
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://data.alpaca.markets/v1/last/stocks/goog
The above exception was the direct cause of the following exception:
...
discord.ext.commands.errors.CommandInvokeError: Command raised an exception: HTTPError: 404 Client Error: Not Found for url:
https://data.alpaca.markets/v1/last/stocks/goog

Alpaca’s market data is case sensitive, so searching for goog will not yield the same results as GOOG.

@bot.command()
async def last_price(context, ticker):
if isinstance(ticker, str):
ticker = ticker.upper()
print(f"Checking the last price of {ticker}")
try:
last_price = alpaca_api.get_last_trade(ticker)
await context.send(f"{ticker} -- ${last_price.price}")
except Exception as e:
await context.send(f"Error getting the last price: {e}")

For a quick mitigation, lets set all inputs to upper case. Also we should add an exception so that we can send back notice of a failure.

Now we’re handling some errors.

Finally, lets make a chart and send it to the user. What good is the last price without some context. While there are several different ways to share a chart, I decided to generate a .png with Matplotlib and send it to Discord. This way we don’t have to worry about any other services or hosting.

@bot.command()
async def check(context, ticker):
print(f"Checking the history of a stock")

## Make sure the symbol is upper case
if isinstance(ticker, str):
ticker = ticker.upper()
try: ## Retrieve the last 100 days of trading data
bars = alpaca_api.get_barset(ticker, 'day', limit=100)
bars = bars.df[ticker]

## This bytes buffer will hole the image we send back
fig = io.BytesIO()
## Grab the last closing price
last_price = bars.tail(1)['close'].values[0]
## Make a chart from the data we retrieved
plt.title(f"{ticker} -- Last Price ${last_price:.02f}")
plt.xlabel("Last 100 days")
plt.plot(bars["close"])
## Save the image to the buffer we created earlier
plt.savefig(fig, format="png")
fig.seek(0)
## Sending back the image to the user.
await context.send(file=discord.File(fig, f"{ticker}.png"))
plt.close() except Exception as e:
await context.send(f"Error getting the stock's data: {e}")
We’ve got a graph!!

Since this is an image being uploaded to Discord, we can share a public link to anyone.

https://media.discordapp.net/attachments/824442560540835890/824491703192649748/GOOG.png?width=1258&height=707

Step 4 — Generating Embeds for general user friendliness

Now we can access a lot of data and make simple graphs of that data. Lets start making some of that data easier to look at in discord. In this step we’ll be designing an Embed, populating it with only the information we care about, and return it back to the user.

I used Discord Embed Sandbox to design my embeds. Other tools also exist

Mocking an Embed with https://cog-creators.github.io/discord-embed-sandbox/

Lets look back at what the account JSON looked like. (Documentation on accounts).

Account({   
'account_blocked': False,
'account_number': 'PA0T4EI00TV0',
'buying_power': '400000',
'cash': '100000',
'created_at': '2021-03-25T00:17:25.566381Z',
'currency': 'USD',
'daytrade_count': 0,
'daytrading_buying_power': '400000',
'equity': '100000',
'id': '246a152e-a15d-433a-b5dc-0abcaca6d656',
'initial_margin': '0',
'last_equity': '100000',
'last_maintenance_margin': '0',
'long_market_value': '0',
'maintenance_margin': '0',
'multiplier': '4',
'pattern_day_trader': False,
'portfolio_value': '100000',
'regt_buying_power': '200000',
'short_market_value': '0',
'shorting_enabled': True,
'sma': '0',
'status': 'ACTIVE',
'trade_suspended_by_user': False,
'trading_blocked': False,
'transfers_blocked': False})

Feels like we can confidently grab:

  • cash
  • buying_power
  • equity
  • portfolio_value

If we find that we need other values, we’ll add them later.

def generate_account_embed(account):
embed=discord.Embed(title="Account Status",
description="Alpaca Markets Account Status",
color=0x47c02c)

embed.add_field(name="Cash", value=f"${account.cash}")
embed.add_field(name="Buying Power",
value=f"${account.buying_power}")
embed.add_field(name="Portfolio Value",
value=f"${account.portfolio_value}")
embed.add_field(name="Equity", value=f"${account.equity}")

return embed

Here we’re using the generated python code as a template for our embed. We populate it with all of the variables that we’re passing in.

## Replace the previous instance of this function in bot.py
@bot.command()
async def account(context):
print(f"Checking account")
account_info = alpaca_api.get_account()
account_embed = generate_account_embed(account_info)
await context.send(embed=account_embed)

Don’t forget to replace your account function in your bot.py before reruning.

Account values embed

Look at how much cleaner and easier it is to see your current account status.

Step 5 — Reactions for trade confirmations

It’s been a long journey, but we’re finally ready to ask Dispaca to go buy some securities for us. At the end of this step we’ll be able to ask Dispaca to make a trade for us, as well as confirm with us before it issues that trade. We wouldn’t want to accidentally buy 100 shares of GameStop… right? 🚀

That could have been a mistake

In the case of our bot, lets see what the simplest buy command could look like.

@bot.command()
async def buy(context, ticker, quantity):
print(f"Ordering {quantity} shares of {ticker}")
buy_order = alpaca_api.submit_order(ticker, quantity,'buy',
'market', 'day')
await context.send(f"{buy_order}")

Issuing a >buy GME 100 will just tell Alpaca that we want 100 shares of GameStop and to go buy them.

We don’t want this at all..

No confirmation, no checking of account value, nothing. All we would get back is an order confirmation. An ugly JSON blob at that.

Order({   
'asset_class': 'us_equity',
'asset_id': '9eae922d-8d67-4c40-b6ea-faca9b092b89',
'canceled_at': None,
'client_order_id': '6ff64956-0123-4567-887a-632086981208',
'created_at': '2021-03-24T21:07:28.551801Z',
'expired_at': None,
'extended_hours': False,
'failed_at': None,
'filled_at': None,
'filled_avg_price': None,
'filled_qty': '0',
'hwm': None,
'id': 'd1d08f71-a4a5-45f7-8508-64a86918b74e',
'legs': None,
'limit_price': None,
'notional': None,
'order_class': '',
'order_type': 'market',
'qty': '100',
'replaced_at': None,
'replaced_by': None,
'replaces': None,
'side': 'buy',
'status': 'accepted',
'stop_price': None,
'submitted_at': '2021-03-24T21:07:28.54462Z',
'symbol': 'GME',
'time_in_force': 'day',
'trail_percent': None,
'trail_price': None,
'type': 'market',
'updated_at': '2021-03-24T21:07:28.551801Z'})

Buying securities shouldn’t be easy, it’s a lot of responsibility! How about instead we build an embed that we can use to show what our order is going to look like. Then we wait for an order confirmation via a reaction on that embed. If we get the right reaction we make the order, otherwise we just time out and pretend that nothing ever never happened.

def generate_buy_embed(ticker, quantity, market_price):
embed = discord.Embed()
total_cost = int(quantity) * market_price
embed=discord.Embed(title=f"Buying {ticker}",
description="Review your buy order below.\
React with 👍 to confirm in the next 30 seconds")
embed.add_field(name="Quantity",
value=f"{quantity}", inline=False)
embed.add_field(name="Per Share Cost",
value=f"${market_price}", inline=False)
embed.add_field(name="Estimated Cost",
value=f"${total_cost}", inline=False)
embed.add_field(name="In Force",
value="Good Until Cancelled", inline=False )
return embed

Above we’re starting to build up what we want our embed to look like. We want to make the information quick to digest, as well as simple to react to.

What our embed will look like when rendered in Discord
@bot.command()
async def buy(context, ticker, quantity):
if isinstance(ticker, str):
ticker = ticker.upper()
## Lets get some supporting information about this stock
try:
last_trade = alpaca_api.get_last_trade(ticker)
last_price = last_trade.price
except Exception as e:
await context.send(f"Error getting the last price: {e}")
return
## This is the embed we set up earlier
buy_embed = generate_buy_embed(ticker, quantity, last_price)
await context.send(embed=buy_embed) ## We only care about the user who started the trade
def check(reaction, user):
return user == context.message.author
try:
## Wait for a reaction event. 30 second timeout.
reaction, user = await bot.wait_for("reaction_add",
timeout=30.0, check=check)
except TimeoutError:
await context.send("Cancelling the trade. No activity")
else:
if str(reaction.emoji) == '👍':
await context.send("Executing on the trade")
## Placing the actual order
placed_order = alpaca_api.submit_order(symbol=ticker,
qty=quantity,
side='buy',
type='market',
time_in_force='gtc')
await context.send(f"Order ID: {placed_order.id}")
else:
await context.send("Cancelling Order")

This is a bit more complex of a command, but it’s super cool!

When Dispaca sees a >buy GME 100, it’ll go out and grab the last trade price. We will use that price to estimate the cost of our order.

await bot.wait_for("reaction_add", timeout=30.0, check=check)

Here we’re telling our bot to listen for reactions for 30 seconds. If the user who started the order responds with 👍, then the order will proceed! We send a buy order at the current market price. On the other hand if the user reacts with any other emoji, or 30 seconds pass, then the order will be cancelled.

This gives us at least a sense of how much we’ll be spending and the added benefit of avoiding a catastrophic mistake =].

A successful trade being issued

I strongly recommend checking out Alpaca’s Order API to see how you can make different types of market orders. For this tutorial we went with the simplest option.

Conclusion

Thank you for following this tutorial till the end. At this point you should have been able to deploy a discord bot that can buy, with confirmation, stocks on your behalf.

There’s still A LOT that can be done such as selling the securities you’ve just bought, and more advanced order types and analysis. Stay tuned for Part 2 where I explore more topics in building out Dispaca, the Discord Alpaca.

--

--

Irving Derin

Boston Based Software Engineer. Touching upon Python, hardware and other stuffs