Make a Discord Bot for you and your friends to trade stocks
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!
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.txtCOPY /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.
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:
From here we’ll click on New Application
and name our wonderful new application. This will bring us to the General Information page.
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.
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.
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.
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
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.
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.
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.
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.
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!
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.
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!
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
.
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/googThe 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}")
Since this is an image being uploaded to Discord, we can share a public link to anyone.
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
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.
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? 🚀
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.
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.
@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 =].
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.