12 KiB
Vendored
CONTRIBUTING
First off, thanks for taking the time to contribute (or at least read the Contributing Guidelines for the Discord Bot)! 🚀
Introduction
The following is a set of guidelines for contributing to OpenBB Terminal. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
The Github Discord Bot board will display the tickets we're currently working on, or have planned to work on. As a first step towards contribution, you should look into what is there to implement or reach out to the community in discord and ask what they want/need. For more information reach out to @lardigalltomhistoria or @SexyYear on discord.
The reader reading this document should read CONTRIBUTING.md document. This is because the contributing steps are identical with the difference of having a different coding structure, which is where this document will mainly focus.
Code Structure
It is critical for our discord bot to go hand in hand with OpenBB Terminal commands structure. As we want the
community to learn the structure of the commands for GST and the GST bot, simultaneously. This is so that if I know
that I can reach "Price vs Short Interest" command in Terminal through (stocks)>(dps)>psi, I know that the discord
bot will invoke the same command with !stocks.dps.psi.
The code structure is as follows:
discordbot/discordbot.py
/economy/economy_menu.py
/currencies.py
/...
/feargreed.py
/stocks/dark_pool_shorts/dps_menu.py
/dpotc.py
/...
/spos.py
The main difference between discord bot code structure and OpenBB Terminal is in the fact that in OpenBB Terminal the files are organized by data source whereas in the discord bot each files corresponds at a different bot command.
The file that has the name of the context or category (e.g. economy_menu or dps_menu) will contain a collection of
possible commands from the ones within that same directory. The user will be able to select these through an emoji reaction.
Developer Journey
When adding a new command to our discord bot this is the journey that the developer must follow.
Does the appropriate context/category already exists for the command you are looking to implement? If it does,
jump to adding a new command, otherwise carry on.
Adding a new context/category
If you want to add a new context/category, you just need to create the corresponding _menu file in the correct
location. E.g. let's suppose you want to create stocks/dark_pool_shorts, then you create a dps_menu.py file under
that directory. Such dps_menu.py file will have a code along these lines:
import asyncio
import discord
import discordbot.config_discordbot as cfg
import yfinance as yf
# pylint: disable=wrong-import-order
from discordbot.run_discordbot import gst_bot
class DarkPoolShortsCommands(discord.ext.commands.Cog):
"""Dark Pool Shorts menu"""
def __init__(self, bot: discord.ext.commands.Bot):
self.bot = bot
# The commands will be added here as a function
def setup(bot: discord.ext.commands.Bot):
gst_bot.add_cog(DarkPoolShortsCommands(bot))
In order for the bot to know about this new context/category you need to go into
discordbot.py and add it to the section before running the bot, using:
gst_bot.load_extension("stocks.dark_pool_shorts.dps_menu")
Adding a new command
For adding a new command, you need to add the name of the command under the context/category it belongs to.
Let's assume you want to add the ftd command (failure-to-deliver) on stocks/dark_pool_shorts, then you need to
create an ftd.py.
Such file will have the following format:
# DO ALL THE IMPORTS NECESSARY HERE
import os
from datetime import datetime, timedelta
import discord
import yfinance as yf
from openbb_terminal.rich_config import console
import discordbot.config_discordbot as cfg
import discordbot.helpers
from matplotlib import pyplot as plt
import matplotlib.dates as mdates
from discordbot.run_discordbot import gst_imgur
# IMPORT THE GST MODEL/VIEW THAT RETRIEVES DATA WANTED
from openbb_terminal.stocks.dark_pool_shorts import sec_model
# DEFINE THE COMMAND AND ITS ARGUMENTS
async def ftd_command(ctx, ticker="", start="", end=""):
"""Fails-to-deliver data [SEC]"""
# ADD A BIG TRY-CATCH IN CASE IT FAILS TO RETURN A MESSAGE TO USER
try:
# DEBUG USER INPUT
if cfg.DEBUG:
console.print(f"\n!stocks.dps.ftd {ticker} {start} {end}")
# CHECK FOR ARGUMENT VALIDITY
if ticker == "":
raise Exception("Stock ticker is required")
ticker = ticker.upper()
stock = yf.download(ticker, progress=False)
if stock.empty:
raise Exception("Stock ticker is invalid")
if start == "":
start = datetime.now() - timedelta(days=365)
else:
start = datetime.strptime(start, cfg.DATE_FORMAT)
if end == "":
end = datetime.now()
else:
end = datetime.strptime(end, cfg.DATE_FORMAT)
# RETRIEVE DATA
ftds_data = sec_model.get_fails_to_deliver(ticker, start, end, 0)
# DEBUG OUTPUT
if cfg.DEBUG:
console.print(ftds_data.to_string())
# PROCESS DATA TO OUTPUT
plt.bar(
ftds_data["SETTLEMENT DATE"],
ftds_data["QUANTITY (FAILS)"] / 1000,
)
plt.ylabel("Shares [K]")
plt.title(f"Fails-to-deliver Data for {ticker}")
plt.grid(b=True, which="major", color="#666666", linestyle="-", alpha=0.2)
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter("%Y/%m/%d"))
plt.gca().xaxis.set_major_locator(mdates.DayLocator(interval=7))
plt.gcf().autofmt_xdate()
plt.xlabel("Days")
_ = plt.gca().twinx()
stock= discordbot.helpers.load(ticker, start)
stock_ftd = stock[stock.index > start]
stock_ftd = stock_ftd[stock_ftd.index < end]
plt.plot(stock_ftd.index, stock_ftd["Adj Close"], color="tab:orange")
plt.ylabel("Share Price [$]")
plt.savefig("dps_ftd.png")
plt.close("all")
uploaded_image = gst_imgur.upload_image("dps_ftd.png", title="something")
image_link = uploaded_image.link
# PREPARE SUCCESSFUL DISCORD EMBED TO SHOW USER
title = "Stocks: [SEC] Failure-to-deliver " + ticker
embed = discord.Embed(title=title, colour=cfg.COLOR)
embed.set_author(
name=cfg.AUTHOR_NAME,
icon_url=cfg.AUTHOR_ICON_URL,
)
embed.set_image(url=image_link)
os.remove("dps_ftd.png")
# SEND MESSAGE IN DISCORD
await ctx.send(embed=embed)
except Exception as e:
# PREPARE DISCORD EMBED TO SHOW USER THE ERROR
embed = discord.Embed(
title=f"ERROR Stocks: [SEC] Failure-to-deliver {ticker}",
colour=cfg.COLOR,
description=e,
)
embed.set_author(
name=cfg.AUTHOR_NAME,
icon_url=cfg.AUTHOR_ICON_URL,
)
await ctx.send(embed=embed)
The main variation in each command occurs at the output level, where the bot reply could be either:
- A simple string.
- A table, which relies on a pandas dataframe output and pagination is used for the user to iterate through columns. See shorted.py
- An image, which relies on matplotlib output and IMGUR is used to upload this image to send to the user. See ftd.py.
Once the command file has been added, one needs to make sure our bot is aware of it. Therefore we need to add it in
the dps_menu file, same one that was created in Adding a new context/category.
This is achieved by adding the following method to DarkPoolShortsCommands(discord.ext.commands.Cog) class:
@discord.ext.commands.command(name="stocks.dps.ftd")
async def ftd(self, ctx: discord.ext.commands.Context, ticker="", start="", end=""):
"""Fails-to-deliver data [SEC]
Parameters
----------
ticker: str
Stock ticker
start: datetime
Start of date
end: datetime
End of date
"""
await ftd_command(ctx, ticker, start, end)
Note that the docstring is CRITICAL to be added as this will allow the user to query about this command with
!help stocks.dps.ftd and understand what the function outputs, what source it uses and ultimately, the expected arguments.
Selecting a command through a reaction in a menu bot message
Finally, it was important for us that the users had the same experience given by the terminal. Hence we set the code
structure in a way so that the user could call a menu like !stocks.dps and be able to select multiple commands from
the menu that popped-up through message reactions.
Taking the dps_menu as example, this is done by adding the following method to
DarkPoolShortsCommands(discord.ext.commands.Cog) class:
@discord.ext.commands.command(name="stocks.dps")
async def dark_pool_shorts_menu(self, ctx: discord.ext.commands.Context):
"""Stocks Context - Shows Dark Pool Shorts Menu"""
if cfg.DEBUG:
console.print(f"\n!stocks.dps {ticker}")
text = (
"0️⃣ !stocks.dps.shorted <NUM>\n"
"1️⃣ !stocks.dps.hsi <NUM>\n"
"2️⃣ !stocks.dps.pos <NUM> <SORT>\n"
)
title = "Dark Pool Shorts (DPS) Menu"
embed = discord.Embed(title=title, description=text, colour=cfg.COLOR)
embed.set_author(
name=cfg.AUTHOR_NAME,
icon_url=cfg.AUTHOR_ICON_URL,
)
msg = await ctx.send(embed=embed)
emoji_list = ["0️⃣", "1️⃣", "2️⃣"]
for emoji in emoji_list:
await msg.add_reaction(emoji)
def check(reaction, user):
return user == ctx.message.author and str(reaction.emoji) in emoji_list
try:
reaction, _ = await gst_bot.wait_for(
"reaction_add", timeout=cfg.MENU_TIMEOUT, check=check
)
if reaction.emoji == "0️⃣":
if cfg.DEBUG:
console.print("Reaction selected: 0")
await shorted_command(ctx)
elif reaction.emoji == "1️⃣":
if cfg.DEBUG:
console.print("Reaction selected: 1")
await hsi_command(ctx)
elif reaction.emoji == "2️⃣":
if cfg.DEBUG:
console.print("Reaction selected: 2")
await pos_command(ctx)
for emoji in emoji_list:
await msg.remove_reaction(emoji, ctx.bot.user)
except asyncio.TimeoutError:
text = text + "\n\nCommand timeout."
embed = discord.Embed(title=title, description=text)
await msg.edit(embed=embed)
for emoji in emoji_list:
await msg.remove_reaction(emoji, ctx.bot.user)