First off, thanks for taking the time to contribute (or at least read the Contributing Guidelines)! 🚀
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.
- CONTRIBUTING
Increase OpenBB Terminal reach:
- Star the repo.
- Pass the word to your friends/family.
- Create content (e.g. youtube videos) using OpenBB Terminal.
- Share your terminal graphs and interpretations with other Reddit users (example).
- Join our discord and interact with other users.
If you are the typical retail trader that uses the terminal on a daily basis, there are a lot of ways you can con contribute:
- Test Terminal's features.
- Report bugs or even sketch new feature ideas (we have a large dev community since we're open source, hence there's a lot we can do).
- Improve our documentation or even features by suggesting enhancements.
- Search for more APIs that we can add to our terminal.
- Search websites that we can scrape useful data for free.
- Contact interesting people in our behalf towards partnerships which will provide our user base with more data.
- Reach out to developers/mathematicians/data scientists/finance people to help us build the #1 Retail Trader terminal.
For a 1h coding session where the (old) architecture of the repo is explained while a new feature is added, check here.
The fact that this is an Open Source project makes the possibilities of contributing pretty much unlimited. In here you should consider what you want to gain out of this experience of contributing, some examples we've seen since the repository is live:
- Machine Learning engineers working on our prediction models
- Data Scientists improving our algorithms to make sense out of the data
- Mathematicians exploring our residual analysis menu
- Finance students evaluating a DCF spreadsheet from terminal's data
- DevOps engineers making the repository more robust and ensuring good practices
- Pick a feature you want to implement or a bug.
- If out are out of ideas, look into our issues or
search for
# TODO
in our repository. - Feel free to discuss what you'll be working on via discord https://discord.gg/Up2QGbMKHY, to avoid duplicate work.
Item | Description | Example |
---|---|---|
CONTEXT | Specific instrument world to analyse. | stocks , crypto , economy |
CATEGORY | Group of similar COMMANDS to do on the instrument There are the specialized categories, specific to each CONTEXT and there are common categories which are not specific to one CONTEXT. |
due_diligence , technical_analysis , insider |
COMMAND | Operation on one or no instrument that retrieves data in form of string, table or plot. | rating , supplier , sentiment |
The following layout is expected: /<context>/<category>/<command_files>
If there are sub-categories, the layout will be: /<context>/<category>/<sub-category>/<command_files>
Example:
openbb_terminal/stocks/stocks_controller.py
/stocks_helper.py
/due_diligence/dd_controller.py
/marketwatch_view.py
/marketwatch_model.py
/finviz_view.py
/finviz_model.py
/technical_analysis/ta_controller.py
/tradingview_view.py
/tradingview_model.py
/common/technical_analysis/overlap_view.py
/overlap_model.py
/crypto/crypto_controller.py
/crypto_helper.py
/due_diligence/dd_controller.py
/binance_view.py
/binance_model.py
/technical_analysis/ta_controller.py
With:
Context | Category | File | Description |
---|---|---|---|
stocks/ |
stocks_controller.py |
Manages stocks context from a user perspective, i.e. routing commands and arguments to output data, or, more importantly, redirecting to the selected category. | |
stocks/ |
stocks_helper.py |
Helper to stocks_controller.py . This file is meant to implement commands needed by stocks_controller.py |
|
stocks/ |
due_diligence/ |
dd_controller.py |
Manages due_diligence category from stocks context from a user perspective, i.e. routing commands and arguments to output data. |
stocks/ |
due_diligence/ |
marketwatch_view.py |
This file contains functions that rely on Market Watch data. These functions represent commands that belong to due_diligence category from stocks context. These functions are called by dd_controller.py using the arguments given by the user and will output either a string, table or plot. |
stocks/ |
due_diligence/ |
marketwatch_model.py |
This file contains functions that rely on Market Watch data. These functions represent commands that belong to due_diligence category from stocks context. These functions are called by marketwatch_view.py and will return data to be processed in either a string, dictionary or dataframe format. |
stocks/ |
due_diligence/ |
finviz_view.py |
This file contains functions that rely on Finviz data. These functions represent commands that belong to due_diligence category from stocks context. These functions are called by dd_controller.py using the arguments given by the user and will output either a string, table or plot. |
stocks/ |
due_diligence/ |
finviz_model.py |
This file contains functions that rely on Finviz data. These functions represent commands that belong to due_diligence category from stocks context. These functions are called by finviz_view.py and will return data to be processed in either a string, dictionary or dataframe format. |
stocks/ |
technical_analysis/ |
ta_controller.py |
Manages technical_analysis category from stocks context from a user perspective, i.e. routing commands and arguments to output data. |
stocks/ |
technical_analysis/ |
tradingview_view.py |
This file contains functions that rely on TradingView data. These functions represent commands that belong to technical_analysis category from stocks context. These functions are called by ta_controller.py using the arguments given by the user and will output either a string, table or plot. |
stocks/ |
technical_analysis/ |
tradingview_model.py |
This file contains functions that rely on TradingView data. These functions represent commands that belong to technical_analysis category from stocks context. These functions are called by tradingview_view.py and will return data to be processed in either a string, dictionary or dataframe format. |
common/ |
technical_analysis/ |
overlap_view.py |
This file contains functions that rely on overlap data. In this case overlap is not a data source, but the type of technical analysis performed. These functions represent commands that belong to technical_analysis category from MULTIPLE contexts. These functions are called by ta_controller.py , from MULTIPLE contexts, using the arguments given by the user and will output either a string, table or plot. Due to the fact that this file is common to multiple contexts the functions need to be generic enough to accommodate for this. E.g. if we are proving a dataframe to these functions, we should make sure that stocks/ta_controller.py and crypto/ta_controller use the same formatting. |
common/ |
technical_analysis/ |
overlap_model.py |
This file contains functions that rely on overlap data. In this case overlap is not a data source, but the type of technical analysis performed. These functions represent commands that belong to technical_analysis category from MULTIPLE contexts. These functions are called by overlap_view.py , and will return data to be processed in either a string, dictionary or dataframe format. Due to the fact that this file is common to multiple contexts the functions need to be generic enough to accommodate for this. E.g. if we are getting the sentiment of an instrument, we should ensure that these functions accept both a "GME" or a "BTC", for stocks and crypto , respectively. |
Process to add a new command. shorted
command from category dark_pool_shorts
and context stocks
will be used as
example. Since this command uses data from Yahoo Finance, a yahoofinance_view.py
and a yahoofinance_model.py
files
will be implemented.
- Create a file with the source of data as the name followed by
_model
if it doesn't exist, e.g.yahoofinance_model
- Add the documentation header
- Do the necessary imports to get the data
- Define a function starting with
get_
- In that function:
- Use typing hints
- Write a descriptive description where at the end the source is specified
- Obtain the data and return it. Sometimes the model can contain a more complex logic to it, if the scraping is not straightforward. If the data is returned directly by an API, we still want to wrap it around a model function to keep consistency across codebase and be more future proof.
""" Yahoo Finance Model """
__docformat__ = "numpy"
import pandas as pd
import requests
def get_most_shorted() -> pd.DataFrame:
"""Get most shorted stock screener [Source: Yahoo Finance]
Returns
-------
pd.DataFrame
Most Shorted Stocks
"""
url = "https://finance.yahoo.com/screener/predefined/most_shorted_stocks"
data = pd.read_html(requests.get(url).text)[0]
data = data.iloc[:, :-1]
return data
Note:
- As explained before, it is possible that this file needs to be created under
common/
directory rather thanstocks/
, which means that when that happens this function should be done in a generic way, i.e. not mentioning stocks or a specific context. - If the model require an API key or some sort of secrets, make sure to handle the error and output relevant message.
In the example below, you can see that we explicitly handle 4 important error types:
- Invalid API Keys
- API Keys not authorized for Premium feature
- Empty return payload
- Invalid arguments (Optional)
It's not always possible to distinguish error types using status_code. So depending on the API provider, you can use either error messages or exception.
def get_economy_calendar_events() -> pd.DataFrame:
"""Get economic calendar events
Returns
-------
pd.DataFrame
Get dataframe with economic calendar events
"""
response = requests.get(
f"https://finnhub.io/api/v1/calendar/economic?token={cfg.API_FINNHUB_KEY}"
)
df = pd.DataFrame()
if response.status_code == 200:
d_data = response.json()
if "economicCalendar" in d_data:
df = pd.DataFrame(d_data["economicCalendar"])
else:
console.print("No latest economy calendar events found\n")
elif response.status_code == 401:
console.print("[red]Invalid API Key[/red]\n")
elif response.status_code == 403:
console.print("[red]API Key not authorized for Premium Feature[/red]\n")
else:
console.print(f"Error in request: {response.json()['error']}", "\n")
return df
- Create a file with the source of data as the name followed by
_view
if it doesn't exist, e.g.yahoofinance_view
- Add the documentation header
- Do the necessary imports to display the data. One of these is the
_model
associated with this_view
. I.e. from same data source. - Define a function starting with
display_
- In this function:
- Use typing hints
- Write a descriptive description where at the end the source is specified
- Get the data from the
_model
and parse it to be output in a more meaningful way. - Ensure that the data that comes through is reasonable, i.e. at least that we aren't displaying an empty dataframe.
- Do not degrade the main data dataframe coming from model if there's an export flag. This is so that the export can
have all the data rather than the short amount of information we may show to the user. Thus, in order to do so
df_data = df.copy()
can be useful as if you changedf_data
,df
remains intact. - Always add a new line at the end, this allows for an additional line between 2 commands and makes it easier for the user to visualize what is happening.
- If the source requires an API Key or some sort of tokens, add
check_api_key
decorator on that specific view. This will throw a warning if users forget to set their Keys or Tokens - Finally, call
export_data
where the variables are export variable, current filename, command name, and dataframe.
""" Yahoo Finance View """
__docformat__ = "numpy"
import os
from openbb_terminal.rich_config import console
from openbb_terminal.helper_funcs import export_data, print_rich_table
from openbb_terminal.stocks.dark_pool_shorts import yahoofinance_model
def display_most_shorted(num_stocks: int, export: str):
"""Display most shorted stocks screener. [Source: Yahoo Finance]
Parameters
----------
num_stocks: int
Number of stocks to display
export : str
Export dataframe data to csv,json,xlsx file
"""
df = yahoofinance_model.get_most_shorted()
df.dropna(how="all", axis=1, inplace=True)
df = df.replace(float("NaN"), "")
if df.empty:
console.print("No data found.")
else:
print_rich_table(
df.head(num_stocks),
headers=list(df.columns),
show_index=False,
title="Most shorted stocks"
)
console.print("")
export_data(
export,
os.path.dirname(os.path.abspath(__file__)),
"shorted",
df,
)
Note: As explained before, it is possible that this file needs to be created under common/
directory rather than
stocks/
, which means that when that happens this function should be done in a generic way, i.e. not mentioning stocks
or a specific context. The arguments will need to be parsed by stocks_controller,py
and the other controller this
function shares the data output with.
-
Import
_view
associated with command we want to allow user to select. -
Add command name to variable
CHOICES
fromDarkPoolShortsController
class. -
Add command name and description to
print_help()
. In addition, an additional line above must contain the Source of information. E.g.Yahoo Finance: shorted show most shorted stocks
-
Add a method to
DarkPoolShortsController
class with name:call_
followed by command name.- This method must start defining a parser with arguments
add_help=False
andformatter_class=argparse.ArgumentDefaultsHelpFormatter
. In additionprog
must have the same name as the command, anddescription
should be self-explanatory ending with a mention of the data source. - Add parser arguments after defining parser. One important argument to add is the export capability. All commands should be able to export data.
- Initialize a try-catch block. This is so that if there is an issue the terminal doesn't crash and the user can keep using it.
- If there is a single or even a main argument, a block of code must be used to insert a fake argument on the list of args provided by the user. This makes the terminal usage being faster.
- Parse known args from list of arguments and values provided by the user.
- Call the function contained in a
_view.py
file with the arguments parsed by argparse.
- This method must start defining a parser with arguments
def call_shorted(self, other_args: List[str]):
"""Process shorted command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="shorted",
description="Print up to 25 top ticker most shorted. [Source: Yahoo Finance]",
)
parser.add_argument(
"-n",
"--num",
action="store",
dest="num",
type=check_int_range(1, 25),
default=5,
help="Number of the most shorted stocks to retrieve.",
)
parser.add_argument(
"--export",
choices=["csv", "json", "xlsx"],
default="",
type=str,
dest="export",
help="Export dataframe data to csv,json,xlsx file",
)
if other_args:
if "-" not in other_args[0]:
other_args.insert(0, "-n")
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
yahoofinance_view.display_most_shorted(
num_stocks=ns_parser.num,
export=ns_parser.export,
)
When in doubt, follow https://www.python.org/dev/peps/pep-0008/.
- The name of the variables must be descriptive of what they stand for. I.e.
ticker
is descriptive,aux
is not. - Single character variables must be avoided. Except if they correspond to the iterator of a loop.
The docstring format used in numpy, an example is shown below:
def command_foo(var1: str, var2: List[int], var3: bool = False) -> Tuple[int, pd.DataFrame]:
"""Small description
[Optional: Longer description]
Parameters
----------
var1 : str
var1 description
var2 : List[int]
var2 description
var3 : bool, optional
var3 description
Returns
-------
foo : int
returned foo description
pd.DataFrame
dataframe returned
"""
The following linters are used by our codebase:
Linter | Description |
---|---|
bandit | security analyzer |
black | code formatter |
codespell | spelling checker |
flake8 | style guide enforcer |
mypy | static typing checker |
pyupgrade | upgrade syntax for newer versions |
safety | checks security vulnerabilities |
pylint | bug and quality checker |
markdownlint | markdown linter |
At this stage it is assumed that you have already forked the project and are ready to start working.
Git hook scripts are useful for identifying simple issues before submission to code review. We run our hooks on every commit to automatically point out issues in code such as missing semicolons, trailing whitespace, and debug statements. By pointing these issues out before code review, this allows a code reviewer to focus on the architecture of a change while not wasting time with trivial style nitpicks.
Install the pre-commit hooks by running: pre-commit install
.
Although the Coding Guidelines section has been already explained. It is worth mentioning that if you want to be faster
at developing a new feature, you may implement it first on a jupyter notebook
and then carry it across to the
terminal. This is mostly useful when the feature relies on scraping data from a website, or implementing a Neural
Network model.
- Create your Feature Branch, e.g.
git checkout -b feature/AmazingFeature
- Check the files you have touched using
git status
- Stage the files you want to commit, e.g.
git add openbb_terminal/stocks/stocks_controller.py openbb_terminal/stocks/stocks_helper.py
. Note: DON'T addconfig_terminal.py
or.env
files with personal information, or evenfeature_flags.py
which is user-dependent. - Write a concise commit message under 50 characters, e.g.
git commit -m "meaningful commit message"
. If your PR solves an issue raised by a user, you may specify such issue by adding #ISSUE_NUMBER to the commit message, so that these get linked. Note: If you installed pre-commit hooks and one of the formatters re-formats your code, you'll need to go back to step 3 to add these.
Unit tests minimize errors in code and quickly find errors when they do arise.
Pytest allows users to quickly create unit tests in Python. To use pytest run pytest tests/
.
Coverage allows users to see how complete unit tests are for Python. To use coverage do the following:
coverage run -m pytest
coverage html
To view the tests find the htmlcov folder in the main directory and open the index.html file. This will show a detailed report of testing coverage.
VCRPY allows us to save data from request methods to a .YAML file. This increases test integrity and significantly speeds up the time it takes to run tests. To use VCRPY add @pytest.mark.vcr above any function you write.
- High-level documentation:
- Add a row to the table on the README of the corresponding context/folder.
- E.g. if you are adding
pipo
feature todiscovery
menu understocks
context, you need to go into the corresponding README and add a new row to the table, like:
Command | Description | Source |
---|---|---|
pipo |
past IPOs | Finnhub |
- Low-level documentation:
Once you're happy with what you have, push your branch to remote. E.g. git push origin feature/AmazingFeature
A user may create a Draft Pull Request when he/she wants to discuss implementation with the team.
As reviewers, you should select: @DidierRLopes and @jmaslek.
A label must be selected from the following types:
Label name | Description | Example |
---|---|---|
feat XS |
Extra small feature | Add a preset |
feat S |
Small T-Shirt size Feature | New single command added |
feat M |
Medium T-Shirt size feature | Multiple commands added from same data source |
feat L |
Large T-Shirt size Feature | New category added under context |
feat XL |
Extra Large feature | New context added |
enhancement |
Enhancement | Add new parameter to existing command |
bug |
Fix a bug | Fixes terminal crashing or warning message |
build |
Build-related work | Fix a github action that is breaking the build |
tests |
Test-related work | Add/improve tests |
docs |
Improvements on documentation | Add/improve documentation |
refactor |
Refactor code | Changing argparse location |
docker |
Docker-related work | Add/improve docker |
help wanted |
Extra attention is needed | When a contributor needs help |
do not merge |
Label to prevent pull request merge | When PR is not ready to be merged just yet |
As soon as the Pull Request is opened, our repository has a specific set of github actions that will not only run linters on the branch just pushed, but also run pytest on it. This allows for another layer of safety on the code developed.
In addition, our team is known for performing diligent
code reviews. This not only allows us to reduce the amount of
iterations on that code and have it to be more future proof, but also allows the developer to learn/improve his coding skills.
Often in the past the reviewers have suggested better coding practices, e.g. using 1_000_000
instead of 1000000
for
better visibility, or suggesting a speed optimization improvement.