Files
OpenBB/openbb_terminal/helper_classes.py
Chavithra 72b0a9f1ee Next release : reports on steroids (#2349)
* fix gov tests

* refactor insider

* new virtual path extraction

* removed some symbol default params as they're considered critical

* little adjustments

* portfolio refactor

* merge API factory

* add helpers, stocks, crypto, forex

* minor forex changes

* include forex api paths

* add 2 missing forex funcs

* portfolio brokers refactor

* display help on api func call

* add econometrics virtual paths to api

* add api unit test

* fixed report for the new api

* minor portfolio refactorings

* added gdapps

* anchor_yield path

* some more crypto path fixes

* small change

* fixed wrong param

* minor fixes

* wip - inital commit for forex report

* add bw as a model, we'll get better solution afterwards

* added ema with dummy model as it adds great functionality to the report

* minor fixes

* wip - added functions to forex report

* add feedparser news path

* add new virtual paths to api

* adding commands to equity report

* revert to old paths, new ones were breaking

* Add in very basic ETF report

* Add candle chart to ETF report

* add etf load

* allow use of candle without data

* add raw to candle

* added forex report

* ongoing equity report

* equity report change

* fix some portfolio bugs and add docstrings

* include portfolio paths and coin class

* add crypto paths

* change event dates to str

* starting economy report

* window for limit

* equity report and refactor newsapi

* add helper to api

* update on economy report

* equity report

* update economy report

* refactor some docstrings

* change maturities helper

* refactor newsapi

* refactor futures command

* add some sauce to ycrv plot

* black

* update report

* refactor alphavantage

* refactor wsj

* update economy report

* ycrv tenor

* map avaiable_indices

* map economy helpers

* fix econdb docstring

* add plots on economy report

* minor fixes

* wip - crypto report

* update economy report

* added same default args as view

* added view to explicity use chart=True when suing the api

* adjustments - removed rich tables to use only df

* final version economy report

* change report name

* equity report for review

* linting

* add etf symbols endpoint

* incorporate feedback economy report

* fix reports launch by adding tag to economy report

* fix equity bug

* remove analyst name

* fix

* fix news

* make links hyperlinks for equity

* click links

* fixed arg name

* improved news

* small improves

* Fix light terminal stylesheet that would prevent using it in notebooks (#2473)

* improved report

* run reports in installer

* fix #2209

* minor ycrv refactoring

* refactor portfolio/holdv virtual path

* refactor benchmark trades

* fix events args

* adapt economy report to changes

* fix portfolio controller bug

* holdv refactor

* refactor perf command

* start portfolio report

* remove perf view

* refactor holp

* add textwrap3 to poetry (doesn't solve the error)

* fix equity after merge

* add some rolling commands

* fix equity after save button

* improved crypto report, plus minor fixes

* minor fixes on the reports

* add maxdd and distr

* refactor qa

* var command

* refactor qa expected shortfall

* add es command

* add es command

* fix qa percentile bug

* fix economy rendering

* refactor qa omega

* add om command

* add summary command

* add dret command

* add mret command

* add yret command

* add metrics

* add allocs to report

* remove bro and po commands, add later

* fixed some tests

* adjustments to crypto report

* Fix docstring for VSCode

Added a note about installing Jupyter PowerToys extension for optimal API usage in Jupyter VSCode, in the API_README.md.

* minor adjustment

* remove nft calendar model virtual paths

* Add in Portfolio report

* fix external axes portfolio view

* Update portfolio report with rolling plots

* Details for ETF and Portfolio

* fix economy report

* change analyst to openbb

* floppy

* fixed unmatched axis in reports

* Speed up tests

* fix file and load on po

* get_news output

* add some po paths

* Add integration tests for Reports menu

* refactor maxsharpe

* open maxsharpe

* open minrisk

* open maxutil

* open maxret

* Added fixes

* black

* remove useless views

* Fixed small issue

* refactor ef

* open ef api

* portfolio optimization report

* Added fixes

* unblock api loading

* add more endpoints

* update po report

* unblock api loading

* update po report

* expose herc

* expose property endpoint

* Added fixes

* More api fixes

* flake8

* Fixed some mypy

* news api model

* flake8

* mypy fix

* mypy

* black

* pylint

* fix tests

* markdown

* markdown

* Added fixes

* fix economy report

* merge

* fix economy report

* remove empty notebook

* expose nco

* remove jupyter notebook

* expose plot endpoint

* remove po report, just used for tests

* api v paths plot

* remove api_old

* change loading msg

Co-authored-by: montezdesousa <montezdesousa@gmail.com>
Co-authored-by: hjoaquim <h.joaquim@campus.fct.unl.pt>
Co-authored-by: montezdesousa <79287829+montezdesousa@users.noreply.github.com>
Co-authored-by: Om Gupta <guptagom@gmail.com>
Co-authored-by: minhhoang1023 <40023817+minhhoang1023@users.noreply.github.com>
Co-authored-by: JerBouma <jer.bouma@gmail.com>
Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>
Co-authored-by: Om Gupta <85685255+soggyomelette@users.noreply.github.com>
Co-authored-by: Diogo Sousa <diogosousa@Diogos-MBP.lan>
Co-authored-by: Colin Delahunty <72827203+colin99d@users.noreply.github.com>
Co-authored-by: northern-64bit <75195383+northern-64bit@users.noreply.github.com>
Co-authored-by: colin99d <colin99delahunty@gmail.com>
Co-authored-by: Minh Hoang <nminh.hoang1023@gmail.com>
2022-09-20 15:07:58 +01:00

399 lines
13 KiB
Python

"""Helper classes."""
__docformat__ = "numpy"
import os
from pathlib import Path
import argparse
import json
from importlib import machinery, util
from typing import Union, List, Dict, Optional
import matplotlib
import matplotlib.pyplot as plt
from matplotlib import font_manager, ticker
from openbb_terminal.core.config.paths import REPOSITORY_DIRECTORY, USER_DATA_DIRECTORY
class LineAnnotateDrawer:
"""Line drawing class."""
def __init__(self, ax: matplotlib.axes = None):
self.ax = ax
# pylint: disable=import-outside-toplevel
def draw_lines_and_annotate(self):
"""Draw lines."""
from openbb_terminal.rich_config import console
console.print(
"Click twice for annotation.\nClose window to keep using terminal.\n"
)
while True:
xy = plt.ginput(2)
# Check whether the user has closed the window or not
if not plt.get_fignums():
console.print("")
return
if len(xy) == 2:
x = [p[0] for p in xy]
y = [p[1] for p in xy]
if (x[0] == x[1]) and (y[0] == y[1]):
txt = input("Annotation: ")
self.ax.annotate(txt, (x[0], y[1]), ha="center", va="center")
else:
self.ax.plot(x, y)
self.ax.figure.canvas.draw()
# pylint: disable=too-few-public-methods
class ModelsNamespace:
"""A namespace placeholder for the menu models.
This class is used in all api wrappers to create a `models` namespace and import
all the model functions.
"""
def __init__(self, folders: Union[str, List[str]]) -> None:
"""Import all menu models into the models namespace.
Instantiation of the namespace requires either a path to the folder that
contains model files or a list of such folders.
Parameters
----------
folders : Union[str, List[str]]
a folder or a list of folders to import models from
"""
if isinstance(folders, str):
folders = [folders]
for folder in folders:
menu_models = [
(
f.replace("_model.py", ""),
os.path.abspath(os.path.join(folder, f)),
)
for f in os.listdir(folder)
if f.endswith("_model.py")
]
for model_name, model_file in menu_models:
loader = machinery.SourceFileLoader(model_name, model_file)
spec = util.spec_from_loader(model_name, loader)
if spec is not None:
setattr(self, model_name, util.module_from_spec(spec))
loader.exec_module(getattr(self, model_name))
else:
pass
# pylint: disable=R0902
class TerminalStyle:
"""The class that helps with handling of style configurations.
It serves styles for 3 libraries. For `Matplotlib` this class serves absolute paths
to the .mplstyle files. For `Matplotlib Finance` and `Rich` this class serves custom
styles as python dictionaries.
"""
DEFAULT_STYLES_LOCATION = REPOSITORY_DIRECTORY / "styles" / "default"
USER_STYLES_LOCATION = USER_DATA_DIRECTORY / "styles"
mpl_styles_available: Dict[str, str] = {}
mpl_style: str = ""
mpl_rcparams_available: Dict[str, str] = {}
mpl_rcparams: Dict = {}
mpf_styles_available: Dict[str, str] = {}
mpf_style: Dict = {}
console_styles_available: Dict[str, str] = {}
console_style: Dict[str, str] = {}
down_color: str = ""
up_color: str = ""
xticks_rotation: str = ""
tight_layout_padding: int = 0
pie_wedgeprops: Dict = {}
pie_startangle: int = 0
line_width: float = 1.5
volume_bar_width: float = 0.8
def __init__(
self,
mpl_style: Optional[str] = "",
mpf_style: Optional[str] = "",
console_style: Optional[str] = "",
) -> None:
"""Instantiate a terminal style class
The stylesheet files should be placed to the `styles/default` or `styles/user`
folders. The parameters required for class instantiation are stylesheet names
without extensions (following matplotlib convention).
Ex. `styles/default/boring.mplstyle` should be passed as `boring`.
Parameters
----------
mpl_style : str, optional
Style name without extension, by default ""
mpf_style : str, optional
Style name without extension, by default ""
console_style : str, optional
Style name without extension, by default ""
"""
# To import all styles from terminal repo folder to user data
for folder in [self.DEFAULT_STYLES_LOCATION, self.USER_STYLES_LOCATION]:
self.load_available_styles_from_folder(folder)
self.load_custom_fonts_from_folder(folder)
if mpl_style in self.mpl_styles_available:
self.mpl_style = self.mpl_styles_available[mpl_style]
else:
self.mpl_style = self.mpl_styles_available["dark"]
if mpl_style in self.mpl_rcparams_available:
with open(self.mpl_rcparams_available[mpl_style]) as stylesheet:
self.mpl_rcparams = json.load(stylesheet)
else:
with open(self.mpl_rcparams_available["dark"]) as stylesheet:
self.mpl_rcparams = json.load(stylesheet)
if mpf_style in self.mpf_styles_available:
with open(self.mpf_styles_available[mpf_style]) as stylesheet:
self.mpf_style = json.load(stylesheet)
self.mpf_style["base_mpl_style"] = self.mpl_style
else:
with open(self.mpf_styles_available["dark"]) as stylesheet:
self.mpf_style = json.load(stylesheet)
self.mpf_style["base_mpl_style"] = self.mpl_style
if console_style in self.console_styles_available:
with open(self.console_styles_available[console_style]) as stylesheet:
self.console_style = json.load(stylesheet)
else:
with open(self.console_styles_available["dark"]) as stylesheet:
self.console_style = json.load(stylesheet)
self.applyMPLstyle()
def load_custom_fonts_from_folder(self, folder: Path) -> None:
"""Load custom fonts form folder.
TTF and OTF fonts are loaded into the mpl font manager and are available for
selection in mpl by their name (for example "Consolas" or "Hack").
Parameters
----------
folder : str
Path to the folder containing the fonts
"""
if not folder.exists():
return
for font_file in folder.iterdir():
if not font_file.is_file():
continue
if font_file.name.endswith(".otf") or font_file.name.endswith(".ttf"):
font_path = os.path.abspath(os.path.join(folder, font_file))
font_manager.fontManager.addfont(font_path)
def load_available_styles_from_folder(self, folder: Path) -> None:
"""Load custom styles from folder.
Parses the styles/default and styles/user folders and loads style files.
To be recognized files need to follow a naming convention:
*.mplstyle - matplotlib stylesheets
*.mplrc.json - matplotlib rc stylesheets that are not handled by mplstyle
*.mpfstyle.json - matplotlib finance stylesheets
*.richstyle.json - rich stylesheets
Parameters
----------
folder : str
Path to the folder containing the stylesheets
"""
if not folder.exists():
return
for stf in folder.iterdir():
if not stf.is_file():
continue
if stf.name.endswith(".mplstyle"):
self.mpl_styles_available[
stf.name.replace(".mplstyle", "")
] = os.path.join(folder, stf)
elif stf.name.endswith(".mplrc.json"):
self.mpl_rcparams_available[
stf.name.replace(".mplrc.json", "")
] = os.path.join(folder, stf)
elif stf.name.endswith(".mpfstyle.json"):
self.mpf_styles_available[
stf.name.replace(".mpfstyle.json", "")
] = os.path.join(folder, stf)
elif stf.name.endswith(".richstyle.json"):
self.console_styles_available[
stf.name.replace(".richstyle.json", "")
] = os.path.join(folder, stf)
def applyMPLstyle(self):
"""Apply style to the current matplotlib context."""
plt.style.use(self.mpl_style)
self.xticks_rotation = self.mpl_rcparams["xticks_rotation"]
self.tight_layout_padding = self.mpl_rcparams["tight_layout_padding"]
self.pie_wedgeprops = self.mpl_rcparams["pie_wedgeprops"]
self.pie_startangle = self.mpl_rcparams["pie_startangle"]
self.mpf_style["mavcolors"] = plt.rcParams["axes.prop_cycle"].by_key()["color"]
self.down_color = self.mpf_style["marketcolors"]["volume"]["down"]
self.up_color = self.mpf_style["marketcolors"]["volume"]["up"]
self.line_width = plt.rcParams["lines.linewidth"]
try:
self.volume_bar_width = self.mpl_rcparams["volume_bar_width"]
except Exception():
pass
def get_colors(self, reverse: bool = False) -> List:
"""Get hex color sequence from the stylesheet."""
plt.style.use(self.mpl_style)
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
if reverse:
colors.reverse()
return colors
def style_primary_axis(
self,
ax: plt.Axes,
data_index: Optional[List[int]] = None,
tick_labels: Optional[List[str]] = None,
):
"""Apply styling to a primary axis.
Parameters
----------
ax : plt.Axes
A matplolib axis
"""
ax.yaxis.set_label_position("right")
ax.grid(axis="both", visible=True, zorder=0)
if (
all([data_index, tick_labels])
and isinstance(data_index, list)
and isinstance(tick_labels, list)
):
ax.xaxis.set_major_formatter(
ticker.FuncFormatter(
lambda value, _: tick_labels[int(value)]
if int(value) in data_index
else ""
)
)
ax.xaxis.set_major_locator(ticker.MaxNLocator(6, integer=True))
ax.tick_params(axis="x", labelrotation=self.xticks_rotation)
def style_twin_axis(self, ax: plt.Axes):
"""Apply styling to a twin axis.
Parameters
----------
ax : plt.Axes
A matplolib axis
"""
ax.yaxis.set_label_position("left")
def style_twin_axes(self, ax1: plt.Axes, ax2: plt.Axes):
"""Apply styling to a twin axes
Parameters
----------
ax1 : plt.Axes
Primary matplolib axis
ax2 : plt.Axes
Twinx matplolib axis
"""
ax1.tick_params(axis="x", labelrotation=self.xticks_rotation)
ax1.grid(axis="both", visible=True, zorder=0)
ax2.grid(visible=False)
def add_label(
self,
fig: plt.figure,
):
"""Add a text label to a figure in a funny position.
Parameters
----------
fig : plt.figure
A matplotlib figure
"""
label = "OpenBB Terminal"
fig.text(
0.99,
0.0420,
label,
fontsize=12,
color="gray",
alpha=0.5,
horizontalalignment="right",
)
# pylint: disable=import-outside-toplevel
def add_cmd_source(
self,
fig: plt.figure,
):
"""Add a text label to a figure in a funny position.
Parameters
----------
fig : plt.figure
A matplotlib figure
"""
from openbb_terminal.helper_funcs import command_location
if command_location:
fig.text(
0.01,
0.5,
command_location,
rotation=90,
fontsize=12,
color="gray",
alpha=0.5,
verticalalignment="center",
)
# pylint: disable=import-outside-toplevel
def visualize_output(self, force_tight_layout: bool = True):
"""Show chart in an interactive widget."""
import openbb_terminal.feature_flags as obbff
from openbb_terminal.rich_config import console
if obbff.USE_CMD_LOCATION_FIGURE:
self.add_cmd_source(plt.gcf())
if obbff.USE_WATERMARK:
self.add_label(plt.gcf())
if force_tight_layout:
plt.tight_layout(pad=self.tight_layout_padding)
if obbff.USE_ION:
plt.ion()
plt.show()
console.print()
class AllowArgsWithWhiteSpace(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
setattr(namespace, self.dest, " ".join(values))