From ed35c94b359ef4fb8f8af576ac2ee460c785cb70 Mon Sep 17 00:00:00 2001 From: James Maslek Date: Fri, 3 Mar 2023 12:01:41 -0500 Subject: [PATCH 01/17] Update contributing guide --- CONTRIBUTING.md | 296 ++++++++++++------ .../miscellaneous/data_sources_default.json | 2 +- .../fundamental_analysis/fa_controller.py | 50 ++- .../stocks/fundamental_analysis/fmp_model.py | 34 +- .../stocks/fundamental_analysis/fmp_view.py | 50 +++ 5 files changed, 311 insertions(+), 121 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43c004641e32..f5f2fc62c08b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,7 @@ Use your best judgment, and feel free to propose changes to this document in a p - [Adding a new command](#adding-a-new-command) - [Select Feature](#select-feature) - [Model](#model) + - [Data source](#data-source) - [View](#view) - [Controller](#controller) - [Add SDK endpoint](#add-sdk-endpoint) @@ -59,9 +60,8 @@ Use your best judgment, and feel free to propose changes to this document in a p Before implementing a new command we highly recommend that you go through [Understand Code Structure](#understand-code-structure) and [Follow Coding Guidelines](#follow-coding-guidelines). This will allow you to get your PR merged faster and keep consistency of our code base. -In the next sections we describe the 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. +In the next sections we describe the process to add a new command. +We will be adding a function to get price targets from the Financial Modeling Prep api. Note that there already exists a function to get price targets from the Business Insider website, `stocks/fa/pt`, so we will be adding a new function to get price targets from the Financial Modeling Prep api, and go through adding sources. ### Select Feature @@ -69,9 +69,29 @@ will be implemented. - Feel free to discuss what you'll be working on either directly on [the issue](https://github.com/OpenBB-finance/OpenBBTerminal/issues) or on [our Discord](www.openbb.co/discord). - This ensures someone from the team can help you and there isn't duplicated work. +Before writing any code, it is good to understand what the data will look like. In this case, we will be getting the price targets from the Financial Modeling Prep api, and the data will look like this: + +```json +[ + { + "symbol": "AAPL", + "publishedDate": "2023-02-03T16:19:00.000Z", + "newsURL": "https://pulse2.com/apple-stock-receives-a-195-price-target-aapl/", + "newsTitle": "Apple Stock Receives A $195 Price Target (AAPL)", + "analystName": "Cowen Cowen", + "priceTarget": 195, + "adjPriceTarget": 195, + "priceWhenPosted": 154.5, + "newsPublisher": "Pulse 2.0", + "newsBaseURL": "pulse2.com", + "analystCompany": "Cowen & Co." + } +``` + + ### Model -1. Create a file with the source of data as the name followed by `_model` if it doesn't exist, e.g. `yahoofinance_model` +1. Create a file with the source of data as the name followed by `_model` if it doesn't exist. In this case, the file `openbb_terminal/stocs/fundamental_analysis/fmp_model.py` already exists, so we will add the function to that file. 2. Add the documentation header 3. Do the necessary imports to get the data 4. Define a function starting with `get_` @@ -81,41 +101,62 @@ will be implemented. 3. Utilizing a third party API, get and return the data. ```python -""" Yahoo Finance Model """ +""" Financial Modeling Prep Model """ __docformat__ = "numpy" import logging import pandas as pd -import requests -from openbb_terminal.decorators import log_start_end -from openbb_terminal.helper_funcs import get_user_agent +from openbb_terminal import config_terminal as cfg +from openbb_terminal.decorators import check_api_key, log_start_end +from openbb_terminal.helpers import request logger = logging.getLogger(__name__) @log_start_end(log=logger) -def get_most_shorted() -> pd.DataFrame: - """Get most shorted stock screener [Source: Yahoo Finance] +@check_api_key(["API_KEY_FINANCIALMODELINGPREP"]) +def get_price_targets(cls, symbol: str) -> pd.DataFrame: + """Get price targets for a company [Source: Financial Modeling Prep] + + Parameters + ---------- + symbol : str + Symbol to get data for Returns ------- pd.DataFrame - Most Shorted Stocks + DataFrame of price targets """ - url = "https://finance.yahoo.com/screener/predefined/most_shorted_stocks" + url = f"https://financialmodelingprep.com/api/v4/price-target?symbol={symbol}&apikey={cfg.API_KEY_FINANCIALMODELINGPREP}" + response = request(url) + # Check if response is valid + if response.status_code != 200: + console.print(f"[red]Error, Status Code: {response.status_code}[/red]\n") + return pd.DataFrame() + if "Error Message" in response.json(): + console.print( + f"[red]Error, Message: {response.json()['Error Message']}[/red]\n" + ) + return pd.DataFrame() + return pd.DataFrame(response.json()) - data = pd.read_html( - request(url, headers={"User-Agent": get_user_agent()}).text - )[0] - data = data.iloc[:, :-1] - return data ``` +In this function: +- We use the `@log_start_end` decorator to add the function to our logs for debugging purposes. +- We add the `check_api_key` decorator to confirm the api key is valid. +- We have type hinting and a doctring describing the function. +- We use the openbb_terminal helper function `request`, which is an abstracted version of the requests, which allows us to add user agents, timeouts, caches, etc to any request in the terminal. +- We check for different error messages. This will depend on the API provider and usually requires some trial and error. With the FMP api, if there is an invalid symbol, we get a response code of 200, but the json response has an error message field. Same with an invalid api key. +- When an error is caught, we still return an empty dataframe. +- We return the json response as a pandas dataframe. Most functions in the terminal should return a datatframe, but if not, make sure that the return type is specified. +- The API key is imported from the config_terminal file. This is important so that runtime import issues are not encountered. + Note: -1. 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 +1. 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. 2. If the model require an API key, make sure to handle the error and output relevant message. @@ -159,9 +200,18 @@ def get_economy_calendar_events() -> pd.DataFrame: return df ``` +### Data source + +Now that we have added the model function getting, we need to specify that this is an available data source. To do so, we edit the `openbb_terminal/miscellaneous/data_sources_default.json` file. This file, described below, uses a dictionary structure to identify available sources. Since we are adding FMP to `stocks/fa/pt`, we find that entry and append it: +```json + "fa": { + "pt": ["BusinessInsider", "FinancialModelingPrep"], +``` +If you are adding a new function with a new data source, make a new value in the file. + ### View -1. Create a file with the source of data as the name followed by `_view` if it doesn't exist, e.g. `yahoofinance_view` +1. Create a file with the source of data as the name followed by `_view` if it doesn't exist, e.g. `fmp_view` 2. Add the documentation header 3. Do the necessary imports to display the data. One of these is the `_model` associated with this `_view`. I.e. from same data source. 4. Define a function starting with `display_` @@ -169,67 +219,72 @@ def get_economy_calendar_events() -> pd.DataFrame: - 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 change `df_data`, `df` remains intact. + - 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 change `df_data`, `df` remains intact. 6. 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 API Keys 7. Finally, call `export_data` where the variables are export variable, current filename, command name, and dataframe. ```python -""" Yahoo Finance View """ -__docformat__ = "numpy" - -import logging -import os - -from openbb_terminal.decorators import log_start_end -from openbb_terminal.helper_funcs import export_data, print_rich_table -from openbb_terminal.rich_config import console -from openbb_terminal.stocks.dark_pool_shorts import yahoofinance_model - -logger = logging.getLogger(__name__) - @log_start_end(log=logger) -def display_most_shorted(limit: int = 10, export: str = ""): - """Display most shorted stocks screener. [Source: Yahoo Finance] +@check_api_key(["API_KEY_FINANCIALMODELINGPREP"]) +def display_price_targets( + symbol: str, limit: int = 10, export: str = "", sheet_name: Optional[str] = None +): + """Display price targets for a given ticker. [Source: Financial Modeling Prep] Parameters ---------- + symbol : str + Symbol limit: int - Number of stocks to display - export : str + Number of last days ratings to display + export: str Export dataframe data to csv,json,xlsx file + sheet_name: str + Optionally specify the name of the sheet the data is exported to. """ - df = yahoofinance_model.get_most_shorted().head(limit) - 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, headers=list(df.columns), show_index=False, title="Most Shorted Stocks" - ) - + columns_to_show = [ + "publishedDate", + "analystCompany", + "adjPriceTarget", + "priceWhenPosted", + ] + price_targets = fmp_model.get_price_targets(symbol) + if price_targets.empty: + console.print(f"[red]No price targets found for {symbol}[/red]\n") + return + price_targets["publishedDate"] = price_targets["publishedDate"].apply( + lambda x: datetime.strptime(x, "%Y-%m-%dT%H:%M:%S.%fZ").strftime("%Y-%m-%d %H:%M") + ) export_data( export, os.path.dirname(os.path.abspath(__file__)), - "shorted", - df, + "pt", + price_targets, + sheet_name, ) + print_rich_table( + price_targets[columns_to_show].head(limit), + headers=["Date", "Company", "Target", "Posted Price"], + show_index=False, + title=f"{symbol.upper()} Price Targets", + ) ``` - -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. +In this function: +- We use the same log and api decorators as in the model. +- We define the columns we want to show to the user. +- We get the data from the fmp_model function +- We check if there is data. If something went wrong, we don't want to show it, so we print a message and return. Note that because we have error messages in both the model and view, there will be two print outs. If you wish to just show one, it is better to handle in the model. +- We do some parsing of the data to make it more readable. In this case, the output from FMP is not very clear at quick glance, we we put it into something more readable. +- We export the data. In this function, I decided to export after doing the manipulation. If we do any removing of columns, we should copy the dataframe before exporting. +- We print the data in table form using our `print_rich_table`. This provides a nice console print using the rich library. Note that here I show the top `limit` rows of the dataframe. Care should be taken to make sure that things are sorted. If a sort is required, there is a `reverse` argument that can be added to sort in reverse order. ### Controller -1. Import `_view` associated with command we want to allow user to select. -2. Add command name to variable `CHOICES` from `DarkPoolShortsController` class. +Now that we have the model and views, it is time to add to the controller. + +1. Import the associated `_view` function to the controller. +2. Add command name to variable `CHOICES_COMMANDS` from `FundamentalAnalysisController` class. 3. Add command and source to `print_help()`. ```python @@ -243,15 +298,13 @@ function shares the data output with. 4. If there is a condition to display or not the command, this is something that can be leveraged through this `add_cmd` method, e.g. `mt.add_cmd("shorted", self.ticker_is_loaded)`. -5. Add command description to file `i18n/en.yml`. Use the path and command name as key, e.g. `stocks/dps/shorted` and the value as description. Please fill in other languages if this is something that you know. +5. Add command description to file `i18n/en.yml`. Use the path and command name as key, e.g. `stocks/fa/pt` and the value as description. Please fill in other languages if this is something that you know. -6. Add a method to `DarkPoolShortsController` class with name: `call_` followed by command name. +6. Add a method to `FundamentalAnalysisController` class with name: `call_` followed by command name. - This method must start defining a parser with arguments `add_help=False` and - `formatter_class=argparse.ArgumentDefaultsHelpFormatter`. In addition `prog` must have the same name as the command, - and `description` should be self-explanatory ending with a mention of the data source. + `formatter_class=argparse.ArgumentDefaultsHelpFormatter`. In addition `prog` must have the same name as the command, and `description` 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. - - 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. + - 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. ```python if other_args and "-" not in other_args[0][0]: @@ -261,48 +314,109 @@ function shares the data output with. - 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. +Note that the function self.parse_known_args_and_warn() has some additional options we can add. If the function is showing a chart, but we want the option to show raw data, we can add the `raw=True` keyword and the resulting namespace will have the `raw` attribute. Same with limit, we can pass limit=10 to add the `-l` flag with default=10. Here we also specify the export, and whether it is data only, plots only or anything. This function also adds the `source` attribute to the namespace. In our example, this is important because we added an additional source. + +Our new function will be: + ```python -def call_shorted(self, other_args: List[str]): - """Process shorted command""" + @log_start_end(log=logger) + def call_pt(self, other_args: List[str]): + """Process pt 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]", + prog="pt", + description="""Prints price target from analysts. [Source: Business Insider]""", ) - if other_args and "-" not in other_args[0]: - other_args.insert(0, "-l") - - ns_parser = parse_known_args_and_warn( - parser, - other_args, - limit=10, - export=EXPORT_ONLY_RAW_DATA_ALLOWED + parser.add_argument( + "-t", + "--ticker", + dest="ticker", + help="Ticker to analyze", + type=str, + default=None, + ) + if other_args and "-" not in other_args[0][0]: + other_args.insert(0, "-t") + ns_parser = self.parse_known_args_and_warn( + parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES, raw=True, limit=10 ) - if ns_parser: - yahoofinance_view.display_most_shorted( - num_stocks=ns_parser.num, - export=ns_parser.export, - ) + if ns_parser.ticker: + self.ticker = ns_parser.ticker + self.custom_load_wrapper([self.ticker]) + + if ns_parser.source == "BusinessInsider": + business_insider_view.price_target_from_analysts( + symbol=self.ticker, + data=self.stock, + start_date=self.start, + limit=ns_parser.limit, + raw=ns_parser.raw, + export=ns_parser.export, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, + ) + elif ns_parser.source == "FinancialModelingPrep": + fmp_view.display_price_targets( + symbol=self.ticker, + limit=ns_parser.limit, + export=ns_parser.export, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, + ) ``` +Here, we make the parser, add the arguments, and then parse the arguments. In order to use the fact that we had a new source, we add the logic to access the correct view function. In this specific menu, we also allow the user to specify the symbol with -t, which is what the first block is doing. + +Now from the terminal, this function can be run as desired: +```bash +2023 Mar 03, 11:37 (๐Ÿฆ‹) /stocks/fa/ $ pt -t aapl --source FinancialModelingPrep + + AAPL Price Targets +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ +โ”ƒ Date โ”ƒ Company โ”ƒ Target โ”ƒ Posted Price โ”ƒ +โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ +โ”‚ 2023-02-03 16:19 โ”‚ Cowen & Co. โ”‚ 195.00 โ”‚ 154.50 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 2023-02-03 09:31 โ”‚ D.A. Davidson โ”‚ 173.00 โ”‚ 157.09 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 2023-02-03 08:30 โ”‚ Rosenblatt Securities โ”‚ 173.00 โ”‚ 150.82 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 2023-02-03 08:29 โ”‚ Wedbush โ”‚ 180.00 โ”‚ 150.82 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 2023-02-03 07:21 โ”‚ Raymond James โ”‚ 170.00 โ”‚ 150.82 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 2023-02-03 07:05 โ”‚ Barclays โ”‚ 145.00 โ”‚ 150.82 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 2023-02-03 03:08 โ”‚ KeyBanc โ”‚ 177.00 โ”‚ 150.82 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 2023-02-02 02:08 โ”‚ Rosenblatt Securities โ”‚ 165.00 โ”‚ 145.43 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 2023-02-02 02:08 โ”‚ Deutsche Bank โ”‚ 160.00 โ”‚ 145.43 โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ 2023-02-02 02:08 โ”‚ J.P. Morgan โ”‚ 180.00 โ”‚ 145.43 โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + + If a new menu is being added the code looks like this: ```python @log_start_end(log=logger) -def call_dps(self, _): - """Process dps command""" - from openbb_terminal.stocks.dark_pool_shorts.dps_controller import ( - DarkPoolShortsController, +def call_fa(self, _): + """Process fa command""" + from openbb_terminal.stocks.fundamental_analysis.fa_controller import ( + FundamentalAnalysisController, ) self.queue = self.load_class( - DarkPoolShortsController, self.ticker, self.start, self.stock, self.queue + FundamentalAnalysisController, self.ticker, self.start, self.stock, self.queue ) ``` -The **import only occurs inside this menu call**, this is so that the loading time only happens here and not at the terminal startup. This is to avoid slow loading times for users that are not interested in `stocks/dps` menu. +The **import only occurs inside this menu call**, this is so that the loading time only happens here and not at the terminal startup. This is to avoid slow loading times for users that are not interested in `stocks/fa` menu. In addition, note the `self.load_class` which allows to not create a new DarkPoolShortsController instance but re-load the previous created one. Unless the arguments `self.ticker, self.start, self.stock` have changed since. The `self.queue` list of commands is passed around as it contains the commands that the terminal must perform. diff --git a/openbb_terminal/miscellaneous/data_sources_default.json b/openbb_terminal/miscellaneous/data_sources_default.json index 3e34d30783a1..66174b24ea56 100644 --- a/openbb_terminal/miscellaneous/data_sources_default.json +++ b/openbb_terminal/miscellaneous/data_sources_default.json @@ -255,7 +255,7 @@ "dupont": ["AlphaVantage"], "rating": ["Finviz", "FinancialModelingPrep"], "rot": ["Finnhub"], - "pt": ["BusinessInsider"], + "pt": ["BusinessInsider", "FinancialModelingPrep"], "est": ["BusinessInsider"], "sec": ["MarketWatch", "FinancialModelingPrep"], "supplier": ["CSIMarket"], diff --git a/openbb_terminal/stocks/fundamental_analysis/fa_controller.py b/openbb_terminal/stocks/fundamental_analysis/fa_controller.py index 42cbb9717168..20f981dc3718 100644 --- a/openbb_terminal/stocks/fundamental_analysis/fa_controller.py +++ b/openbb_terminal/stocks/fundamental_analysis/fa_controller.py @@ -1707,43 +1707,37 @@ def call_pt(self, other_args: List[str]): type=str, default=None, ) - parser.add_argument( - "--raw", - action="store_true", - dest="raw", - help="Only output raw data", - ) - parser.add_argument( - "-l", - "--limit", - action="store", - dest="limit", - type=check_positive, - default=10, - help="Limit of latest price targets from analysts to print.", - ) - if other_args and "-" not in other_args[0][0]: other_args.insert(0, "-l") ns_parser = self.parse_known_args_and_warn( - parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES + parser, other_args, EXPORT_BOTH_RAW_DATA_AND_FIGURES, raw=True, limit=10 ) if ns_parser: if ns_parser.ticker: self.ticker = ns_parser.ticker self.custom_load_wrapper([self.ticker]) - business_insider_view.price_target_from_analysts( - symbol=self.ticker, - data=self.stock, - start_date=self.start, - limit=ns_parser.limit, - raw=ns_parser.raw, - export=ns_parser.export, - sheet_name=" ".join(ns_parser.sheet_name) - if ns_parser.sheet_name - else None, - ) + if ns_parser.source == "BusinessInsider": + business_insider_view.price_target_from_analysts( + symbol=self.ticker, + data=self.stock, + start_date=self.start, + limit=ns_parser.limit, + raw=ns_parser.raw, + export=ns_parser.export, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, + ) + elif ns_parser.source == "FinancialModelingPrep": + fmp_view.display_price_targets( + symbol=self.ticker, + limit=ns_parser.limit, + export=ns_parser.export, + sheet_name=" ".join(ns_parser.sheet_name) + if ns_parser.sheet_name + else None, + ) @log_start_end(log=logger) def call_est(self, other_args: List[str]): diff --git a/openbb_terminal/stocks/fundamental_analysis/fmp_model.py b/openbb_terminal/stocks/fundamental_analysis/fmp_model.py index f58b7e650dae..c8eb85005d4d 100644 --- a/openbb_terminal/stocks/fundamental_analysis/fmp_model.py +++ b/openbb_terminal/stocks/fundamental_analysis/fmp_model.py @@ -11,7 +11,7 @@ from openbb_terminal import config_terminal as cfg from openbb_terminal.decorators import check_api_key, log_start_end -from openbb_terminal.helper_funcs import lambda_long_number_format +from openbb_terminal.helper_funcs import lambda_long_number_format, request from openbb_terminal.rich_config import console from openbb_terminal.stocks.fundamental_analysis.fa_helper import clean_df_index @@ -673,3 +673,35 @@ def get_rating(symbol: str) -> pd.DataFrame: else: df = pd.DataFrame() return df + + +@log_start_end(log=logger) +@check_api_key(["API_KEY_FINANCIALMODELINGPREP"]) +def get_price_targets(symbol: str) -> pd.DataFrame: + """Get price targets for a company [Source: Financial Modeling Prep] + + Parameters + ---------- + symbol : str + Symbol to get data for + + Returns + ------- + pd.DataFrame + DataFrame of price targets + """ + url = ( + "https://financialmodelingprep.com/api/v4/price-target?" + f"symbol={symbol}&apikey={cfg.API_KEY_FINANCIALMODELINGPREP}" + ) + response = request(url) + # Check if response is valid + if response.status_code != 200: + console.print(f"[red]Error, Status Code: {response.status_code}[/red]\n") + return pd.DataFrame() + if "Error Message" in response.json(): + console.print( + f"[red]Error, Message: {response.json()['Error Message']}[/red]\n" + ) + return pd.DataFrame() + return pd.DataFrame(response.json()) diff --git a/openbb_terminal/stocks/fundamental_analysis/fmp_view.py b/openbb_terminal/stocks/fundamental_analysis/fmp_view.py index e9ff750e8179..18063ac821bd 100644 --- a/openbb_terminal/stocks/fundamental_analysis/fmp_view.py +++ b/openbb_terminal/stocks/fundamental_analysis/fmp_view.py @@ -3,6 +3,7 @@ import logging import os +from datetime import datetime from typing import Optional import pandas as pd @@ -776,3 +777,52 @@ def rating( df, sheet_name, ) + + +@log_start_end(log=logger) +@check_api_key(["API_KEY_FINANCIALMODELINGPREP"]) +def display_price_targets( + symbol: str, limit: int = 10, export: str = "", sheet_name: Optional[str] = None +): + """Display price targets for a given ticker. [Source: Financial Modeling Prep] + + Parameters + ---------- + symbol : str + Symbol + limit: int + Number of last days ratings to display + export: str + Export dataframe data to csv,json,xlsx file + sheet_name: str + Optionally specify the name of the sheet the data is exported to. + """ + columns_to_show = [ + "publishedDate", + "analystCompany", + "adjPriceTarget", + "priceWhenPosted", + ] + price_targets = fmp_model.get_price_targets(symbol) + if price_targets.empty: + console.print(f"[red]No price targets found for {symbol}[/red]\n") + return + price_targets["publishedDate"] = price_targets["publishedDate"].apply( + lambda x: datetime.strptime(x, "%Y-%m-%dT%H:%M:%S.%fZ").strftime( + "%Y-%m-%d %H:%M" + ) + ) + export_data( + export, + os.path.dirname(os.path.abspath(__file__)), + "pt", + price_targets, + sheet_name, + ) + + print_rich_table( + price_targets[columns_to_show].head(limit), + headers=["Date", "Company", "Target", "Posted Price"], + show_index=False, + title=f"{symbol.upper()} Price Targets", + ) From 82c3e02f663fe369ea1cdc9ddd81a875850c7a03 Mon Sep 17 00:00:00 2001 From: James Maslek Date: Fri, 3 Mar 2023 12:08:53 -0500 Subject: [PATCH 02/17] lint md --- CONTRIBUTING.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f5f2fc62c08b..989fbdae4f4a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,7 +88,6 @@ Before writing any code, it is good to understand what the data will look like. } ``` - ### Model 1. Create a file with the source of data as the name followed by `_model` if it doesn't exist. In this case, the file `openbb_terminal/stocs/fundamental_analysis/fmp_model.py` already exists, so we will add the function to that file. @@ -141,9 +140,10 @@ def get_price_targets(cls, symbol: str) -> pd.DataFrame: ) return pd.DataFrame() return pd.DataFrame(response.json()) - ``` + In this function: + - We use the `@log_start_end` decorator to add the function to our logs for debugging purposes. - We add the `check_api_key` decorator to confirm the api key is valid. - We have type hinting and a doctring describing the function. @@ -153,11 +153,9 @@ In this function: - We return the json response as a pandas dataframe. Most functions in the terminal should return a datatframe, but if not, make sure that the return type is specified. - The API key is imported from the config_terminal file. This is important so that runtime import issues are not encountered. - Note: -1. 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. +1. 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. 2. If the model require an API key, 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: @@ -270,6 +268,7 @@ def display_price_targets( title=f"{symbol.upper()} Price Targets", ) ``` + In this function: - We use the same log and api decorators as in the model. - We define the columns we want to show to the user. @@ -400,7 +399,6 @@ Now from the terminal, this function can be run as desired: โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` - If a new menu is being added the code looks like this: ```python From 4a136f4d51d4c01b3d907a4b3186aec755e1561a Mon Sep 17 00:00:00 2001 From: James Maslek Date: Fri, 3 Mar 2023 12:10:19 -0500 Subject: [PATCH 03/17] lint md --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 989fbdae4f4a..c5b6d26f69b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -201,10 +201,12 @@ def get_economy_calendar_events() -> pd.DataFrame: ### Data source Now that we have added the model function getting, we need to specify that this is an available data source. To do so, we edit the `openbb_terminal/miscellaneous/data_sources_default.json` file. This file, described below, uses a dictionary structure to identify available sources. Since we are adding FMP to `stocks/fa/pt`, we find that entry and append it: + ```json "fa": { "pt": ["BusinessInsider", "FinancialModelingPrep"], ``` + If you are adding a new function with a new data source, make a new value in the file. ### View @@ -270,6 +272,7 @@ def display_price_targets( ``` In this function: + - We use the same log and api decorators as in the model. - We define the columns we want to show to the user. - We get the data from the fmp_model function @@ -370,6 +373,7 @@ Our new function will be: Here, we make the parser, add the arguments, and then parse the arguments. In order to use the fact that we had a new source, we add the logic to access the correct view function. In this specific menu, we also allow the user to specify the symbol with -t, which is what the first block is doing. Now from the terminal, this function can be run as desired: + ```bash 2023 Mar 03, 11:37 (๐Ÿฆ‹) /stocks/fa/ $ pt -t aapl --source FinancialModelingPrep From 7c98c09fa1f00f18404c65d8788e13d1f715fd1a Mon Sep 17 00:00:00 2001 From: James Maslek Date: Fri, 3 Mar 2023 12:13:25 -0500 Subject: [PATCH 04/17] lint md --- CONTRIBUTING.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c5b6d26f69b9..b82a9b361599 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -316,7 +316,8 @@ Now that we have the model and views, it is time to add to the controller. - 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. -Note that the function self.parse_known_args_and_warn() has some additional options we can add. If the function is showing a chart, but we want the option to show raw data, we can add the `raw=True` keyword and the resulting namespace will have the `raw` attribute. Same with limit, we can pass limit=10 to add the `-l` flag with default=10. Here we also specify the export, and whether it is data only, plots only or anything. This function also adds the `source` attribute to the namespace. In our example, this is important because we added an additional source. +Note that the function self.parse_known_args_and_warn() has some additional options we can add. If the function is showing a chart, but we want the option to show raw data, we can add the `raw=True` keyword and the resulting namespace will have the `raw` attribute. +Same with limit, we can pass limit=10 to add the `-l` flag with default=10. Here we also specify the export, and whether it is data only, plots only or anything. This function also adds the `source` attribute to the namespace. In our example, this is important because we added an additional source. Our new function will be: @@ -988,7 +989,7 @@ Dictionary of input datasets : `datasets` *(Dict[str, pd.DataFrame])* Note: Most occurrences are on the econometrics menu and might be refactored in near future -Input dataset : `data` *(pd.DataFrame)* +Input dataset : `data` _(pd.DataFrame)_ ```python def process_data(..., data: pd.DataFrame, ...): From 0a69c1ece49979be7102b7436f24e8a730f91a1d Mon Sep 17 00:00:00 2001 From: James Maslek Date: Fri, 3 Mar 2023 12:43:46 -0500 Subject: [PATCH 05/17] Tests + fix some markdownlint things --- CONTRIBUTING.md | 66 +++++++++---------- .../test_fa_controller/test_print_help.txt | 2 +- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b82a9b361599..ee45dfb8375b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -838,7 +838,7 @@ def func(..., argument_name: argument_type = default, ...): #### Flags -Show raw data : `raw` *(bool)* +Show raw data : `raw` _(bool)_ ```python def display_data(..., raw: bool = False, ...): @@ -847,7 +847,7 @@ def display_data(..., raw: bool = False, ...): print_rich_table(...) ``` -Sort in ascending order : `ascend` *(bool)* +Sort in ascending order : `ascend` _(bool)_ ```python def display_data(..., sortby: str = "", ascend: bool = False, ...): @@ -856,7 +856,7 @@ def display_data(..., sortby: str = "", ascend: bool = False, ...): data = data.sort_values(by=sortby, ascend=ascend) ``` -Show plot : `plot` *(bool)* +Show plot : `plot` _(bool)_ ```python def display_data(..., plot: bool = False, ...): @@ -870,7 +870,7 @@ def display_data(..., plot: bool = False, ...): #### Output format -Format to export data : `export` *(str), e.g. csv, json, xlsx* +Format to export data : `export` _(str), e.g. csv, json, xlsx_ ```python def display_data(..., export: str = "", ...): @@ -878,7 +878,7 @@ def display_data(..., export: str = "", ...): export_data(export, os.path.dirname(os.path.abspath(__file__)), "func", data) ``` -Whether to display plot or return figure *(False: display, True: return)* : `external_axes` *(bool)* +Whether to display plot or return figure _(False: display, True: return)_ : `external_axes` _(bool)_ ```python def display_data(..., external_axes: bool = False, ...): @@ -888,7 +888,7 @@ def display_data(..., external_axes: bool = False, ...): return fig.show(external=external_axes) ``` -Field by which to sort : `sortby` *(str), e.g. "Volume"* +Field by which to sort : `sortby` _(str), e.g. "Volume"_ ```python def display_data(..., sortby: str = "col", ...): @@ -897,7 +897,7 @@ def display_data(..., sortby: str = "col", ...): data = data.sort_values(by=sortby) ``` -Maximum limit number of output items : `limit` *(int)* +Maximum limit number of output items : `limit` _(int)_ ```python def display_data(..., limit = 10, ...): @@ -912,9 +912,9 @@ def display_data(..., limit = 10, ...): #### Time-related -Date from which data is fetched (YYYY-MM-DD) : `start_date` *(str), e.g. 2022-01-01* +Date from which data is fetched (YYYY-MM-DD) : `start_date` _(str), e.g. 2022-01-01_ -Date up to which data is fetched (YYYY-MM-DD) : `end_date` *(str), e.g. 2022-12-31* +Date up to which data is fetched (YYYY-MM-DD) : `end_date` _(str), e.g. 2022-12-31_ Note: We want to accept dates in string because it is easier to deal from user standpoint. Inside the function you can convert it to datetime and check its validity. Please specify date format in docstring. @@ -933,9 +933,9 @@ def get_historical_data(..., start_date: str = "2022-01-01", end_date: str = "20 data = source_model.get_data(data_name, start_date, end_date, ...) ``` -Year from which data is fetched (YYYY) : `start_year` *(str), e.g. 2022* +Year from which data is fetched (YYYY) : `start_year` _(str), e.g. 2022_ -Year up to which data is fetched (YYYY) : `end_year` *(str), e.g. 2023* +Year up to which data is fetched (YYYY) : `end_year` _(str), e.g. 2023_ ```python def get_historical_data(..., start_year: str = "2022", end_year str = "2023", ...): @@ -943,7 +943,7 @@ def get_historical_data(..., start_year: str = "2022", end_year str = "2023", .. data = source_model.get_data(data_name, start_year, end_year, ...) ``` -Interval for data observations : `interval` *(str), e.g. 60m, 90m, 1h* +Interval for data observations : `interval` _(str), e.g. 60m, 90m, 1h_ ```python def get_prices(interval: str = "60m", ...): @@ -955,7 +955,7 @@ def get_prices(interval: str = "60m", ...): ) ``` -Rolling window length : `window` *(int/str), e.g. 252, 252d* +Rolling window length : `window` _(int/str), e.g. 252, 252d_ ```python def get_rolling_sum(returns: pd.Series, window: str = "252d"): @@ -968,7 +968,7 @@ def get_rolling_sum(returns: pd.Series, window: str = "252d"): Search term used to query : `query` (str) -Maximum limit of search items/periods in data source: `limit` *(int)* +Maximum limit of search items/periods in data source: `limit` _(int)_ Note: please specify limit application in docstring @@ -985,7 +985,7 @@ def get_data_from_source(..., limit: int = 10, ...): data = source.get_data(data_name, n_results=limit, ...) ``` -Dictionary of input datasets : `datasets` *(Dict[str, pd.DataFrame])* +Dictionary of input datasets : `datasets` _(Dict[str, pd.DataFrame])_ Note: Most occurrences are on the econometrics menu and might be refactored in near future @@ -1005,13 +1005,13 @@ def process_data(..., data: pd.DataFrame, ...): col_data = pd.DataFrame(data["Col"]) ``` -Dataset name : `dataset_name` *(str)* +Dataset name : `dataset_name` _(str)_ -Input series : `data` *(pd.Series)* +Input series : `data` _(pd.Series)_ -Dependent variable series : `dependent_series` *(pd.Series)* +Dependent variable series : `dependent_series` _(pd.Series)_ -Independent variable series : `independent_series` *(pd.Series)* +Independent variable series : `independent_series` _(pd.Series)_ ```python def get_econometric_test(dependent_series, independent_series, ...): @@ -1020,17 +1020,17 @@ def get_econometric_test(dependent_series, independent_series, ...): result = econometric_test(dataset, ...) ``` -Country name : `country` *(str), e.g. United States, Portugal* +Country name : `country` _(str), e.g. United States, Portugal_ -Country initials or abbreviation : `country_code` *(str) e.g. US, PT, USA, POR* +Country initials or abbreviation : `country_code` _(str) e.g. US, PT, USA, POR_ -Currency to convert data : `currency` *(str) e.g. EUR, USD* +Currency to convert data : `currency` _(str) e.g. EUR, USD_
#### Financial instrument characteristics -Instrument ticker, name or currency pair : `symbol` *(str), e.g. AAPL, ethereum, ETH, ETH-USD* +Instrument ticker, name or currency pair : `symbol` _(str), e.g. AAPL, ethereum, ETH, ETH-USD_ ```python def get_prices(symbol: str = "AAPL", ...): @@ -1041,15 +1041,15 @@ def get_prices(symbol: str = "AAPL", ...): ) ``` -Instrument name: `name` *(str)* +Instrument name: `name` _(str)_ Note: If a function has both name and symbol as parameter, we should distinguish them and call it name -List of instrument tickers, names or currency pairs : `symbols` *(List/List[str]), e.g. ["AAPL", "MSFT"]* +List of instrument tickers, names or currency pairs : `symbols` _(List/List[str]), e.g. ["AAPL", "MSFT"]_ -Base currency under ***BASE***-QUOTE โ†’ ***XXX***-YYY convention : `from_symbol` *(str), e.g. ETH in ETH-USD* +Base currency under ***BASE***-QUOTE โ†’ ***XXX***-YYY convention : `from_symbol` _(str), e.g. ETH in ETH-USD_ -Quote currency under BASE-***QUOTE*** โ†’ XXX-***YYY*** convention : `to_symbol` *(str), e.g. USD in ETH-USD* +Quote currency under BASE-***QUOTE*** โ†’ XXX-***YYY*** convention : `to_symbol` _(str), e.g. USD in ETH-USD_ ```python def get_exchange_rate(from_symbol: str = "", to_symbol: str = "", ...): @@ -1057,17 +1057,17 @@ def get_exchange_rate(from_symbol: str = "", to_symbol: str = "", ...): df = source.get_quotes(from_symbol, to_symbol, ...) ``` -Instrument price : `price` *(float)* +Instrument price : `price` _(float)_ -Instrument implied volatility : `implied_volatility` *(float)* +Instrument implied volatility : `implied_volatility` _(float)_ -Option strike price : `strike_price` *(float)* +Option strike price : `strike_price` _(float)_ -Option days until expiration : `time_to_expiration` *(float/str)* +Option days until expiration : `time_to_expiration` _(float/str)_ -Risk free rate : `risk_free_rate` *(float)* +Risk free rate : `risk_free_rate` _(float)_ -Options expiry date : `expiry` *(str)* +Options expiry date : `expiry` _(str)_
diff --git a/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_fa_controller/test_print_help.txt b/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_fa_controller/test_print_help.txt index eee07a2aabe9..0f72bdcb4122 100644 --- a/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_fa_controller/test_print_help.txt +++ b/tests/openbb_terminal/stocks/fundamental_analysis/txt/test_fa_controller/test_print_help.txt @@ -35,7 +35,7 @@ Future Expectations: epsfc Earning Estimate by Analysts - EPS [SeekingAlpha] revfc Earning Estimate by Analysts - Revenue [SeekingAlpha] est quarter and year analysts earnings estimates [BusinessInsider] - pt price targets over time [BusinessInsider] + pt price targets over time [BusinessInsider, FinancialModelingPrep] dcf advanced Excel customizable discounted cash flow [StockAnalysis] dcfc determine the (historical) discounted cash flow [FinancialModelingPrep] From 6f39a593434f60f26693f01461e1d91d5d07d2b0 Mon Sep 17 00:00:00 2001 From: James Maslek Date: Fri, 3 Mar 2023 13:06:27 -0500 Subject: [PATCH 06/17] Get rid of irritating newline on reset --- openbb_terminal/parent_classes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 8e86486c2994..d3a37dc87e92 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -393,7 +393,7 @@ def switch(self, an_input: str) -> List[str]: self.log_queue() if not self.queue or (self.queue and self.queue[0] not in ("quit", "help")): - console.print() + pass return self.queue From bc24799769d39166b2a4041b1a0863abb33ca881 Mon Sep 17 00:00:00 2001 From: James Maslek Date: Fri, 3 Mar 2023 14:01:00 -0500 Subject: [PATCH 07/17] Trying something small --- .github/workflows/unit-test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index a13798207e03..9895951169ed 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -119,6 +119,12 @@ jobs: source $VENV pytest tests/ --optimization --cov --cov-fail-under=50 --autodoc -n auto --durations=10 --timeout=30 + - name: Upload coverage reports to Codecov + run: | + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + ./codecov -t ${CODECOV_TOKEN} -f coverage.xml + - name: Start Terminal and exit run: | source $VENV From 05dd19037020c4422d82e425ad0ac2850e621869 Mon Sep 17 00:00:00 2001 From: James Maslek Date: Fri, 3 Mar 2023 14:12:21 -0500 Subject: [PATCH 08/17] try something else --- .github/workflows/unit-test.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 9895951169ed..2f7750f2cab9 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -117,13 +117,10 @@ jobs: MPLBACKEND: Agg run: | source $VENV - pytest tests/ --optimization --cov --cov-fail-under=50 --autodoc -n auto --durations=10 --timeout=30 + pytest tests/ --optimization --cov --cov-fail-under=50 --autodoc -n auto --durations=10 --timeout=30 --cov-report=xml - - name: Upload coverage reports to Codecov - run: | - curl -Os https://uploader.codecov.io/latest/linux/codecov - chmod +x codecov - ./codecov -t ${CODECOV_TOKEN} -f coverage.xml + - name: Upload coverage reports to Codecov with GitHub Action + uses: codecov/codecov-action@v3 - name: Start Terminal and exit run: | From 84090a3dc7a9641e115ffcfe640a74feb98187ca Mon Sep 17 00:00:00 2001 From: James Maslek Date: Mon, 6 Mar 2023 10:21:33 -0500 Subject: [PATCH 09/17] linting --- openbb_terminal/stocks/fundamental_analysis/fmp_model.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openbb_terminal/stocks/fundamental_analysis/fmp_model.py b/openbb_terminal/stocks/fundamental_analysis/fmp_model.py index 993303d732d9..f0665470d41d 100644 --- a/openbb_terminal/stocks/fundamental_analysis/fmp_model.py +++ b/openbb_terminal/stocks/fundamental_analysis/fmp_model.py @@ -750,9 +750,11 @@ def get_price_targets(symbol: str) -> pd.DataFrame: pd.DataFrame DataFrame of price targets """ + current_user = get_current_user() + url = ( "https://financialmodelingprep.com/api/v4/price-target?" - f"symbol={symbol}&apikey={cfg.API_KEY_FINANCIALMODELINGPREP}" + f"symbol={symbol}&apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}" ) response = request(url) # Check if response is valid From 5f1877ec715491a68c034860fe160ae62c0645e2 Mon Sep 17 00:00:00 2001 From: James Maslek Date: Mon, 6 Mar 2023 11:26:33 -0500 Subject: [PATCH 10/17] Change cfg to the new user object --- CONTRIBUTING.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee45dfb8375b..abdd445d9cdc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -107,7 +107,7 @@ import logging import pandas as pd -from openbb_terminal import config_terminal as cfg +from openbb_terminal.core.session.current_user import get_current_user from openbb_terminal.decorators import check_api_key, log_start_end from openbb_terminal.helpers import request @@ -128,7 +128,9 @@ def get_price_targets(cls, symbol: str) -> pd.DataFrame: pd.DataFrame DataFrame of price targets """ - url = f"https://financialmodelingprep.com/api/v4/price-target?symbol={symbol}&apikey={cfg.API_KEY_FINANCIALMODELINGPREP}" + current_user = get_current_user() + + url = f"https://financialmodelingprep.com/api/v4/price-target?symbol={symbol}&apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}" response = request(url) # Check if response is valid if response.status_code != 200: @@ -144,6 +146,7 @@ def get_price_targets(cls, symbol: str) -> pd.DataFrame: In this function: +- We import the current user object and preferences using the `get_current_user` function. API keys are stored in `current_user.credentials` - We use the `@log_start_end` decorator to add the function to our logs for debugging purposes. - We add the `check_api_key` decorator to confirm the api key is valid. - We have type hinting and a doctring describing the function. @@ -176,8 +179,9 @@ def get_economy_calendar_events() -> pd.DataFrame: pd.DataFrame Get dataframe with economic calendar events """ + current_user = get_current_user() response = request( - f"https://finnhub.io/api/v1/calendar/economic?token={cfg.API_FINNHUB_KEY}" + f"https://finnhub.io/api/v1/calendar/economic?token={current_user.credentials.API_FINNHUB_KEY}" ) df = pd.DataFrame() @@ -1175,14 +1179,14 @@ def check_polygon_key(show_output: bool = False) -> str: str Status of key set """ - - if cfg.API_POLYGON_KEY == "REPLACE_ME": + current_user = get_current_user() + if current_user.credentials.API_POLYGON_KEY == "REPLACE_ME": logger.info("Polygon key not defined") status = KeyStatus.NOT_DEFINED else: r = request( "https://api.polygon.io/v2/aggs/ticker/AAPL/range/1/day/2020-06-01/2020-06-17" - f"?apiKey={cfg.API_POLYGON_KEY}" + f"?apiKey={current_user.credentials.API_POLYGON_KEY}" ) if r.status_code in [403, 401]: logger.warning("Polygon key defined, test failed") From e06e89bf7f61f01e7be6e25ec5015c00cc1cbeb7 Mon Sep 17 00:00:00 2001 From: James Maslek Date: Tue, 7 Mar 2023 08:30:52 -0500 Subject: [PATCH 11/17] words --- CONTRIBUTING.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index abdd445d9cdc..213f2a8c97f3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -149,12 +149,11 @@ In this function: - We import the current user object and preferences using the `get_current_user` function. API keys are stored in `current_user.credentials` - We use the `@log_start_end` decorator to add the function to our logs for debugging purposes. - We add the `check_api_key` decorator to confirm the api key is valid. -- We have type hinting and a doctring describing the function. -- We use the openbb_terminal helper function `request`, which is an abstracted version of the requests, which allows us to add user agents, timeouts, caches, etc to any request in the terminal. +- We have type hinting and a docstring describing the function. +- We use the openbb_terminal helper function `request`, which is an abstracted version of the requests library, which allows us to add user agents, timeouts, caches, etc to any request in the terminal. - We check for different error messages. This will depend on the API provider and usually requires some trial and error. With the FMP api, if there is an invalid symbol, we get a response code of 200, but the json response has an error message field. Same with an invalid api key. - When an error is caught, we still return an empty dataframe. - We return the json response as a pandas dataframe. Most functions in the terminal should return a datatframe, but if not, make sure that the return type is specified. -- The API key is imported from the config_terminal file. This is important so that runtime import issues are not encountered. Note: @@ -377,6 +376,8 @@ Our new function will be: Here, we make the parser, add the arguments, and then parse the arguments. In order to use the fact that we had a new source, we add the logic to access the correct view function. In this specific menu, we also allow the user to specify the symbol with -t, which is what the first block is doing. +Note that in the `fa` submenu, we allow the function to be run by specifying a ticker, ie `pt -t AAPL`. In this submenu we do a `load` behind the scenes with the ticker selected so that other functions can be run without specifying the ticker. + Now from the terminal, this function can be run as desired: ```bash @@ -408,7 +409,7 @@ Now from the terminal, this function can be run as desired: โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` -If a new menu is being added the code looks like this: +When adding a new menu, the code looks like this: ```python @log_start_end(log=logger) From 5c79c8959517d9ef0daaf059cab18fc6094ae8e0 Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Thu, 9 Mar 2023 10:35:37 +0000 Subject: [PATCH 12/17] minor improvements --- .../stocks/fundamental_analysis/fa_controller.py | 2 +- .../stocks/fundamental_analysis/fmp_model.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/openbb_terminal/stocks/fundamental_analysis/fa_controller.py b/openbb_terminal/stocks/fundamental_analysis/fa_controller.py index 664766decd04..5f81daef6efb 100644 --- a/openbb_terminal/stocks/fundamental_analysis/fa_controller.py +++ b/openbb_terminal/stocks/fundamental_analysis/fa_controller.py @@ -1782,7 +1782,7 @@ def call_pt(self, other_args: List[str]): parser = argparse.ArgumentParser( add_help=False, prog="pt", - description="""Prints price target from analysts. [Source: Business Insider]""", + description="""Prints price target from analysts. [Source: Business Insider and Financial Modeling Prep]""", ) parser.add_argument( "-t", diff --git a/openbb_terminal/stocks/fundamental_analysis/fmp_model.py b/openbb_terminal/stocks/fundamental_analysis/fmp_model.py index c1d56a0c278f..750a80b89173 100644 --- a/openbb_terminal/stocks/fundamental_analysis/fmp_model.py +++ b/openbb_terminal/stocks/fundamental_analysis/fmp_model.py @@ -804,12 +804,14 @@ def get_price_targets(symbol: str) -> pd.DataFrame: ) response = request(url) # Check if response is valid - if response.status_code != 200: - console.print(f"[red]Error, Status Code: {response.status_code}[/red]\n") - return pd.DataFrame() - if "Error Message" in response.json(): - console.print( - f"[red]Error, Message: {response.json()['Error Message']}[/red]\n" + if response.status_code != 200 or "Error Message" in response.json(): + message = f"Error, Status Code: {response.status_code}." + message = ( + message + if "Error Message" not in response.json() + else message + "\n" + response.json()["Error Message"] + ".\n" ) + console.print(message) return pd.DataFrame() + return pd.DataFrame(response.json()) From 77ab0b203f8d6f95fc091c99b0c18cc00e6c54e2 Mon Sep 17 00:00:00 2001 From: hjoaquim Date: Thu, 9 Mar 2023 11:38:24 +0000 Subject: [PATCH 13/17] some adjustments to the contributing guidelines --- CONTRIBUTING.md | 178 ++++++++++++------ .../stocks/fundamental_analysis/fmp_model.py | 1 + 2 files changed, 117 insertions(+), 62 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 213f2a8c97f3..d39023c08231 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,7 +61,7 @@ Use your best judgment, and feel free to propose changes to this document in a p Before implementing a new command we highly recommend that you go through [Understand Code Structure](#understand-code-structure) and [Follow Coding Guidelines](#follow-coding-guidelines). This will allow you to get your PR merged faster and keep consistency of our code base. In the next sections we describe the process to add a new command. -We will be adding a function to get price targets from the Financial Modeling Prep api. Note that there already exists a function to get price targets from the Business Insider website, `stocks/fa/pt`, so we will be adding a new function to get price targets from the Financial Modeling Prep api, and go through adding sources. +We will be adding a function to get price targets from the Financial Modeling Prep API. Note that there already exists a function to get price targets from the Business Insider website, `stocks/fa/pt`, so we will be adding a new function to get price targets from the Financial Modeling Prep API, and go through adding sources. ### Select Feature @@ -69,7 +69,7 @@ We will be adding a function to get price targets from the Financial Modeling Pr - Feel free to discuss what you'll be working on either directly on [the issue](https://github.com/OpenBB-finance/OpenBBTerminal/issues) or on [our Discord](www.openbb.co/discord). - This ensures someone from the team can help you and there isn't duplicated work. -Before writing any code, it is good to understand what the data will look like. In this case, we will be getting the price targets from the Financial Modeling Prep api, and the data will look like this: +Before writing any code, it is good to understand what the data will look like. In this case, we will be getting the price targets from the Financial Modeling Prep API, and the data will look like this: ```json [ @@ -132,26 +132,29 @@ def get_price_targets(cls, symbol: str) -> pd.DataFrame: url = f"https://financialmodelingprep.com/api/v4/price-target?symbol={symbol}&apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}" response = request(url) + # Check if response is valid - if response.status_code != 200: - console.print(f"[red]Error, Status Code: {response.status_code}[/red]\n") - return pd.DataFrame() - if "Error Message" in response.json(): - console.print( - f"[red]Error, Message: {response.json()['Error Message']}[/red]\n" + if response.status_code != 200 or "Error Message" in response.json(): + message = f"Error, Status Code: {response.status_code}." + message = ( + message + if "Error Message" not in response.json() + else message + "\n" + response.json()["Error Message"] + ".\n" ) + console.print(message) return pd.DataFrame() + return pd.DataFrame(response.json()) ``` In this function: -- We import the current user object and preferences using the `get_current_user` function. API keys are stored in `current_user.credentials` +- We import the current user object and, consequently, preferences using the `get_current_user` function. API keys are stored in `current_user.credentials` - We use the `@log_start_end` decorator to add the function to our logs for debugging purposes. -- We add the `check_api_key` decorator to confirm the api key is valid. +- We add the `check_api_key` decorator to confirm the API key is valid. - We have type hinting and a docstring describing the function. -- We use the openbb_terminal helper function `request`, which is an abstracted version of the requests library, which allows us to add user agents, timeouts, caches, etc to any request in the terminal. -- We check for different error messages. This will depend on the API provider and usually requires some trial and error. With the FMP api, if there is an invalid symbol, we get a response code of 200, but the json response has an error message field. Same with an invalid api key. +- We use the openbb_terminal helper function `request`, which is an abstracted version of the requests library, which allows us to add user agents, timeouts, caches, etc. to any HTTP request in the terminal. +- We check for different error messages. This will depend on the API provider and usually requires some trial and error. With the FMP API, if there is an invalid symbol, we get a response code of 200, but the json response has an error message field. Same with an invalid API key. - When an error is caught, we still return an empty dataframe. - We return the json response as a pandas dataframe. Most functions in the terminal should return a datatframe, but if not, make sure that the return type is specified. @@ -159,6 +162,7 @@ Note: 1. 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. 2. If the model require an API key, make sure to handle the error and output relevant message. +3. If the data provider is not yet supported, you'll most likely need to do some extra steps in order to add it to the `keys` menu. See [this section](#external-api-keys) for more details. In the example below, you can see that we explicitly handle 4 important error types: @@ -276,7 +280,7 @@ def display_price_targets( In this function: -- We use the same log and api decorators as in the model. +- We use the same log and API decorators as in the model. - We define the columns we want to show to the user. - We get the data from the fmp_model function - We check if there is data. If something went wrong, we don't want to show it, so we print a message and return. Note that because we have error messages in both the model and view, there will be two print outs. If you wish to just show one, it is better to handle in the model. @@ -331,7 +335,7 @@ Our new function will be: parser = argparse.ArgumentParser( add_help=False, prog="pt", - description="""Prints price target from analysts. [Source: Business Insider]""", + description="""Prints price target from analysts. [Source: Business Insider and Financial Modeling Prep]""", ) parser.add_argument( "-t", @@ -472,18 +476,20 @@ The added line of the file should look like this: ### Open a Pull Request -Once you're happy with what you have, push your branch to remote. E.g. `git push origin feature/AmazingFeature`. Note that we follow gitflow naming convention, so your branch name should be prefixed with `feature/` or `hotfix/` depending on the type of work you are doing. +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. +> Note that we follow gitflow naming convention, so your branch name should be prefixed with `feature/` or `hotfix/` depending on the type of work you are doing. + +A user may create a **Draft Pull Request** when there is the intention to discuss implementation with the team. ### Review Process 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. +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` forbetter visibility, or suggesting a speed optimization improvement. +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. ## Understand Code Structure @@ -573,6 +579,9 @@ With: - Why? It increases code readability and acts as an input example for the functions arguments. This increases the ease of use of the functions through the SDK, but also just generally. + + > Watch out, add default values whenever possible, but take care for not adding mutable default arguments! [More info](https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments) +
@@ -1150,14 +1159,33 @@ It is important to keep a coherent UI/UX throughout the terminal. These are the OpenBB Terminal currently has over 100 different data sources. Most of these require an API key that allows access to some free tier features from the data provider, but also paid ones. -When a new API data source is added to the platform, it must be added through [config_terminal.py](/openbb_terminal/config_terminal.py). E.g. +When a new API data source is added to the platform, it must be added through [credentials_model.py](/openbb_terminal/credentials_model.py) under the section that resonates the most with its functionality, from: `Data providers`, `Socials` or `Brokers`. + +Example (a section from [credentials_model.py](/openbb_terminal/credentials_model.py)): ```python -# https://messari.io/ -API_MESSARI_KEY = os.getenv("OPENBB_API_MESSARI_KEY") or "REPLACE_ME" -``` +# Data providers +API_DATABENTO_KEY = "REPLACE_ME" +API_KEY_ALPHAVANTAGE: str = "REPLACE_ME" +API_KEY_FINANCIALMODELINGPREP: str = "REPLACE_ME" + +... -Note that a `OPENBB_` is added so that the user knows that that environment variable is used by our terminal. +# Socials +API_GITHUB_KEY: str = "REPLACE_ME" +API_REDDIT_CLIENT_ID: str = "REPLACE_ME" +API_REDDIT_CLIENT_SECRET: str = "REPLACE_ME" + +... + +# Brokers or data providers with brokerage services +RH_USERNAME: str = "REPLACE_ME" +RH_PASSWORD: str = "REPLACE_ME" +DG_USERNAME: str = "REPLACE_ME" + +... + +``` ### Setting and checking API key @@ -1180,7 +1208,9 @@ def check_polygon_key(show_output: bool = False) -> str: str Status of key set """ + current_user = get_current_user() + if current_user.credentials.API_POLYGON_KEY == "REPLACE_ME": logger.info("Polygon key not defined") status = KeyStatus.NOT_DEFINED @@ -1216,7 +1246,6 @@ Note: Sometimes the user may have the correct API key but still not have access A function can then be created with the following format to allow the user to change its environment key directly from the terminal. ```python -@log_start_end(log=logger) def call_polygon(self, other_args: List[str]): """Process polygon command""" parser = argparse.ArgumentParser( @@ -1233,7 +1262,7 @@ def call_polygon(self, other_args: List[str]): help="key", ) if not other_args: - console.print("For your API Key, visit: https://polygon.io\n") + console.print("For your API Key, visit: https://polygon.io") return if other_args and "-" not in other_args[0][0]: @@ -1397,54 +1426,75 @@ At that point the user goes into the `dps` menu and runs the command `psi` with In order to help users with a powerful autocomplete, we have implemented our own (which can be found [here](/openbb_terminal/custom_prompt_toolkit.py)). -This **STATIC** list of options is meant to be defined on the `__init__` method of a class as follows. +The list of options for each command is automatically generated, if you're interested take a look at its implementation [here](/openbb_terminal/core/completer/choices.py). + +To leverage this functionality, you need to add the following line to the top of the desired controller: ```python -if session and obbff.USE_PROMPT_TOOLKIT: - self.choices: dict = {c: {} for c in self.controller_choices} - self.choices["overview"] = { - "--type": {c: None for c in self.overview_options}, - "-t": "--type", - } - self.choices["futures"] = { - "--commodity": {c: None for c in self.futures_commodities}, - "-c": "--commodity", - "--sortby": {c: None for c in self.wsj_sortby_cols_dict.keys()}, - "-s": "--sortby", - "--reverse": {}, - "-r": "--reverse", - } - self.choices["map"] = { - "--period": {c: None for c in self.map_period_list}, - "-p": "--period", - "--type": {c: None for c in self.map_filter_list}, - "-t": "--type", - } - self.completer = NestedCompleter.from_nested_dict(self.choices) +CHOICES_GENERATION = True ``` -Important things to note: +Here's an example of how to use it, on the [`forex` controller](/openbb_terminal/forex/forex_controller.py): + +```python +class ForexController(BaseController): + """Forex Controller class.""" + + CHOICES_COMMANDS = [ + "fwd", + "candle", + "load", + "quote", + ] + CHOICES_MENUS = [ + "forecast", + "qa", + "oanda", + "ta", + ] + RESOLUTION = ["i", "d", "w", "m"] + + PATH = "/forex/" + FILE_PATH = os.path.join(os.path.dirname(__file__), "README.md") + CHOICES_GENERATION = True + + def __init__(self, queue: Optional[List[str]] = None): + """Construct Data.""" + super().__init__(queue) -- `self.choices: dict = {c: {} for c in self.controller_choices}`: this allows users to have autocomplete on the command that they are allowed to select in each menu -- `self.choices["overview"]`: this corresponds to the list of choices that the user is allowed to select after specifying `$ overview` -- `"--commodity": {c: None for c in self.futures_commodities}`: this allows the user to select several commodity values after `--commodity` flag -- `"-c": "--commodity"`: this is interpreted as `-c` having the same effect as `--commodity` -- `"--reverse": {}`: corresponds to a boolean flag (does not expect any value after) -- `"--start": None`: corresponds to a flag where the values allowed are not easily discrete due to vast range -- `self.completer = NestedCompleter.from_nested_dict(self.choices)`: from the choices create our custom completer + self.fx_pair = "" + self.from_symbol = "" + self.to_symbol = "" + self.source = get_ordered_list_sources(f"{self.PATH}load")[0] + self.data = pd.DataFrame() + + if session and get_current_user().preferences.USE_PROMPT_TOOLKIT: + choices: dict = self.choices_default + choices["load"].update({c: {} for c in FX_TICKERS}) + + self.completer = NestedCompleter.from_nested_dict(choices) + + + ... +``` In case the user is interested in a **DYNAMIC** list of options which changes based on user's state, then a class method must be defined. -The example below shows the `update_runtime_choices` method being defined in the options controller. +The example below shows the an excerpt from `update_runtime_choices` method in the [`options` controller](/openbb_terminal/stocks/options/options_controller.py). ```python def update_runtime_choices(self): - """Update runtime choices""" - if self.expiry_dates and session and obbff.USE_PROMPT_TOOLKIT: - self.choices["exp"] = {str(c): {} for c in range(len(self.expiry_dates))} - self.choices["exp"]["-d"] = {c: {} for c in self.expiry_dates + [""]} - - self.completer = NestedCompleter.from_nested_dict(self.choices) + """Update runtime choices""" + if session and get_current_user().preferences.USE_PROMPT_TOOLKIT: + if not self.chain.empty: + strike = set(self.chain["strike"]) + + self.choices["hist"]["--strike"] = {str(c): {} for c in strike} + self.choices["grhist"]["-s"] = "--strike" + self.choices["grhist"]["--strike"] = {str(c): {} for c in strike} + self.choices["grhist"]["-s"] = "--strike" + self.choices["binom"]["--strike"] = {str(c): {} for c in strike} + self.choices["binom"]["-s"] = "--strike" ``` This method should only be called when the user's state changes leads to the auto-complete not being accurate. @@ -1493,6 +1543,10 @@ def your_function() -> pd.DataFrame: pass ``` +> Note: if for some reason you don't want your logs to be collected, you can set the `LOG_COLLECTION` user preference to `False`. + +> Disclaimer: all the user paths, names, IPs, credentials and other sensitive information are anonymized, [take a look at how we do it](/openbb_terminal/core/log/generation/formatter_with_exceptions.py). + ### Internationalization WORK IN PROGRESS - The menu can be internationalised BUT we do not support yet help commands`-h` internationalization. diff --git a/openbb_terminal/stocks/fundamental_analysis/fmp_model.py b/openbb_terminal/stocks/fundamental_analysis/fmp_model.py index 750a80b89173..c8fe71b69ada 100644 --- a/openbb_terminal/stocks/fundamental_analysis/fmp_model.py +++ b/openbb_terminal/stocks/fundamental_analysis/fmp_model.py @@ -803,6 +803,7 @@ def get_price_targets(symbol: str) -> pd.DataFrame: f"symbol={symbol}&apikey={current_user.credentials.API_KEY_FINANCIALMODELINGPREP}" ) response = request(url) + # Check if response is valid if response.status_code != 200 or "Error Message" in response.json(): message = f"Error, Status Code: {response.status_code}." From bc8aa0ce14510515daed64f00be2fdb59c3c3334 Mon Sep 17 00:00:00 2001 From: James Maslek Date: Thu, 9 Mar 2023 10:38:26 -0500 Subject: [PATCH 14/17] Another pass at contributing --- CONTRIBUTING.md | 49 +++++++++++++++---------------- openbb_terminal/parent_classes.py | 2 +- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d39023c08231..8342881fe310 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,7 @@ Use your best judgment, and feel free to propose changes to this document in a p - [View](#view) - [Controller](#controller) - [Add SDK endpoint](#add-sdk-endpoint) + - [Add Unit Tests](#add-unit-tests) - [Open a Pull Request](#open-a-pull-request) - [Review Process](#review-process) - [Understand Code Structure](#understand-code-structure) @@ -51,7 +52,6 @@ Use your best judgment, and feel free to propose changes to this document in a p - [Coding](#coding) - [Git Process](#git-process) - [Branch Naming Conventions](#branch-naming-conventions) - - [Add a Test](#add-a-test) - [Installers](#installers) # BASIC @@ -90,14 +90,14 @@ Before writing any code, it is good to understand what the data will look like. ### Model -1. Create a file with the source of data as the name followed by `_model` if it doesn't exist. In this case, the file `openbb_terminal/stocs/fundamental_analysis/fmp_model.py` already exists, so we will add the function to that file. +1. Create a file with the source of data as the name followed by `_model` if it doesn't exist. In this case, the file `openbb_terminal/stocks/fundamental_analysis/fmp_model.py` already exists, so we will add the function to that file. 2. Add the documentation header 3. Do the necessary imports to get the data 4. Define a function starting with `get_` -5. In that function: - 1. Use typing hints - 2. Write a descriptive description where at the end the source is specified - 3. Utilizing a third party API, get and return the data. +5. In this function: + 1. Use type hinting + 2. Write a descriptive description where at the end the source is specified. + 3. Utilize an official API, get and return the data. ```python """ Financial Modeling Prep Model """ @@ -151,7 +151,7 @@ In this function: - We import the current user object and, consequently, preferences using the `get_current_user` function. API keys are stored in `current_user.credentials` - We use the `@log_start_end` decorator to add the function to our logs for debugging purposes. -- We add the `check_api_key` decorator to confirm the API key is valid. +- We add the `@check_api_key` decorator to confirm the API key is valid. - We have type hinting and a docstring describing the function. - We use the openbb_terminal helper function `request`, which is an abstracted version of the requests library, which allows us to add user agents, timeouts, caches, etc. to any HTTP request in the terminal. - We check for different error messages. This will depend on the API provider and usually requires some trial and error. With the FMP API, if there is an invalid symbol, we get a response code of 200, but the json response has an error message field. Same with an invalid API key. @@ -160,8 +160,8 @@ In this function: Note: -1. 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. -2. If the model require an API key, make sure to handle the error and output relevant message. +1. If the function is applicable to many asset classes, it is possible that this file needs to be created under `common/` directory rather than `stocks/`, which means the function should be written in a generic way, i.e. not mentioning stocks or a specific context. +2. If the model requires an API key, make sure to handle the error and output relevant message. 3. If the data provider is not yet supported, you'll most likely need to do some extra steps in order to add it to the `keys` menu. See [this section](#external-api-keys) for more details. In the example below, you can see that we explicitly handle 4 important error types: @@ -474,6 +474,20 @@ The added line of the file should look like this: python generate_sdk.py sort ``` +### Add Unit Tests + +This is a vital part of teh contribution process. We have a set of unit tests that are run on every Pull Request. These tests are located in the `tests` folder. + +Unit tests minimize errors in code and quickly find errors when they do arise. Integration tests are standard usage examples, which are also used to identify errors. + +A thorough introduction on the usage of unit tests and integration tests in OpenBBTerminal can be found on the following page respectively: + +[Unit Test README](tests/README.md) + +[Integration Test README](scripts/README.md) + +Any new features that do not contain unit tests will not be accepted. + ### Open a Pull Request Once you're happy with what you have, push your branch to remote. E.g. `git push origin feature/AmazingFeature`. @@ -1129,8 +1143,7 @@ The following linters are used by our codebase: | codespell | spelling checker | | ruff | a fast python linter | | mypy | static typing checker | -| safety | checks security vulnerabilities | -| pylint | bug and quality checker | +| pylint | static code analysis | | markdownlint | markdown linter | #### Command names @@ -1619,20 +1632,6 @@ The accepted branch naming conventions are: All `feature/feature-name` related branches can only have PRs pointing to `develop` branch. `hotfix/hotfix-name` and `release/2.1.0` or `release/2.1.0rc0` branches can only have PRs pointing to `main` branch. -## Add a Test - -Unit tests minimize errors in code and quickly find errors when they do arise. Integration tests are standard usage examples, which are also used to identify errors. - -A thorough introduction on the usage of unit tests and integration tests in OpenBBTerminal can be found on the following page respectively: - -[Unit Test README](tests/README.md) - -[Integration Test README](scripts/README.md) - -In short: - -- Pytest: is the tool we are using to run our tests, with the command: `pytest tests/` -- Coverage: can be checked like running `coverage run -m pytest` or `coverage html` ## Installers diff --git a/openbb_terminal/parent_classes.py b/openbb_terminal/parent_classes.py index 6786762292f0..bd60472e968d 100644 --- a/openbb_terminal/parent_classes.py +++ b/openbb_terminal/parent_classes.py @@ -394,7 +394,7 @@ def switch(self, an_input: str) -> List[str]: self.log_queue() if not self.queue or (self.queue and self.queue[0] not in ("quit", "help")): - pass + console.print() return self.queue From 1b52f52a4d3d761080e20fdf9796d08c352c793f Mon Sep 17 00:00:00 2001 From: James Maslek Date: Thu, 9 Mar 2023 10:41:18 -0500 Subject: [PATCH 15/17] spelling --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8342881fe310..6cfb554fee7d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -476,7 +476,7 @@ The added line of the file should look like this: ### Add Unit Tests -This is a vital part of teh contribution process. We have a set of unit tests that are run on every Pull Request. These tests are located in the `tests` folder. +This is a vital part of the contribution process. We have a set of unit tests that are run on every Pull Request. These tests are located in the `tests` folder. Unit tests minimize errors in code and quickly find errors when they do arise. Integration tests are standard usage examples, which are also used to identify errors. @@ -486,7 +486,7 @@ A thorough introduction on the usage of unit tests and integration tests in Open [Integration Test README](scripts/README.md) -Any new features that do not contain unit tests will not be accepted. +Any new features that do not contain unit tests will not be accepted. ### Open a Pull Request From 1e678d1cbe3f515573aebf8f55ff0a38433643c9 Mon Sep 17 00:00:00 2001 From: James Maslek Date: Thu, 9 Mar 2023 10:46:59 -0500 Subject: [PATCH 16/17] we need a local markdown linter --- CONTRIBUTING.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6cfb554fee7d..c0353636329b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -593,7 +593,6 @@ With: - Why? It increases code readability and acts as an input example for the functions arguments. This increases the ease of use of the functions through the SDK, but also just generally. - > Watch out, add default values whenever possible, but take care for not adding mutable default arguments! [More info](https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments)
@@ -1556,8 +1555,7 @@ def your_function() -> pd.DataFrame: pass ``` -> Note: if for some reason you don't want your logs to be collected, you can set the `LOG_COLLECTION` user preference to `False`. - +> Note: if you don't want your logs to be collected, you can set the `LOG_COLLECTION` user preference to `False`. > Disclaimer: all the user paths, names, IPs, credentials and other sensitive information are anonymized, [take a look at how we do it](/openbb_terminal/core/log/generation/formatter_with_exceptions.py). ### Internationalization @@ -1632,7 +1630,6 @@ The accepted branch naming conventions are: All `feature/feature-name` related branches can only have PRs pointing to `develop` branch. `hotfix/hotfix-name` and `release/2.1.0` or `release/2.1.0rc0` branches can only have PRs pointing to `main` branch. - ## Installers When implementing a new feature or fixing something within the codebase, it is necessary to ensure that it is working From 6215e29b4b23e11ee4cc4a10d256b0ff6e97e549 Mon Sep 17 00:00:00 2001 From: James Maslek Date: Thu, 9 Mar 2023 11:28:15 -0500 Subject: [PATCH 17/17] why does this pylint disable still fail --- .../cryptocurrency/technical_analysis/ta_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_terminal/cryptocurrency/technical_analysis/ta_controller.py b/openbb_terminal/cryptocurrency/technical_analysis/ta_controller.py index 77ae9e5d8166..9fe9042920a3 100644 --- a/openbb_terminal/cryptocurrency/technical_analysis/ta_controller.py +++ b/openbb_terminal/cryptocurrency/technical_analysis/ta_controller.py @@ -1,6 +1,6 @@ """Crypto Technical Analysis Controller Module""" __docformat__ = "numpy" -# pylint: disable=too-many-lines,R0904,C0201 +# pylint: disable=C0302,R0904,C0201 import argparse import logging