Files
OpenBB/gamestonk_terminal/helper_classes.py
Theodore Aptekarev e1b6022b9c Global plot styles (#1228)
* Add default stylesheets

* Add terminal style helper class and global style initialization in cfg

* Style comments and docstrings

* Load rich terminal theme from config file

* Add application chart styles to candle charts

* Add todos

* Remove explicit color setting for some ta charts

* Add user styles folder to gitignore

* Update default stylesheets

* Add matplotlib font manager support

* Add matplotlib font manager support

* Update docstrings and default style

* Update stocks candle chart formatting (return fig to style title)

* Style common ta overlap view

* Make up and down market colors a part of the style helper

* Update stylesheets

* Style common ta volume view

* Style common ta momentum view

* Style common ta trend indicators view

* Style common ta volatility view

* Style common ta volume view

* Style common ta custom indicators view

* Fix styling bugs and remove the obvious time x lablel

* Style charts in the covid menu

* Set legend position to upper left in the mpl stylesheet

* Add mpl_rcparams configs for parameters not covered by stylesheets

* Remove font configuration files

* Update style class utility functions

* Implement passing external axes and style utility usage in ema & stoch

* Add theme watermark and output helpers

* Rename style to theme

* Update helper usage in ta/ma and ta/stoch

* Update style to theme in sample menus

* Style forex (#1305)

* Make tight layout optional 'cause mplfinance doesn't support it

* Apply global style to the forex menu

* Update code layout in oanda view and black

* Style common TA (#1315)

* Make tight layout optional 'cause mplfinance doesn't support it

* Apply global style to the forex menu

* Add linewidth to theme for use in mpf's addplots

* Add vwap to the stocks notebook api

* Update common/ta overlap to follow charting style

* Apply style on TerminalStyle init

* Enable infrastructure for excluding non-trading days from plots

* Update notebook api to include there and resolve bandit warning

* Update ta/common/overlap to exclude non-trading days

* Enable external ax, style and non-trading days in common/ta/momentum

* Enable external ax, style and non-trading days in common/ta/trend

* Update vwap to the argument naming convention

* Enable external ax, style and non-trading days in common/ta/volatility

* Enable external ax, style and non-trading days in common/ta/volume

* Enable external ax, style and non-trading days in common/ta/custom

* Fix controller tests

* Forgot to disable rewriting of the cassettes ...

* Fix controller errors that came up because a merge conflict

* Fix price label position on fib

* Fix line having wrong x values in fib

Co-authored-by: Colin Delahunty <72827203+colin99d@users.noreply.github.com>

* Style economy (#1308)

* Began converting

* Added alphavan_view

* Added CNN View

* Updated nasdaq view, fixed glitch

* Added fred

* Refactored URL

* Theo's requested changes

* Updated docstrings

* Updated tests

* Fixed pylint

* Fixed tests

* Theo changes

* Econ Fix

* Refactor chart style for Crypto context (#1306)

* Remove mock for gff

* Mock visualize_output helper function

* Refactor

* Fix plot helper

* Update legend loc

* Refactor mplfinance candle plot

* Fix errors in the helper function

* Fix binbook having the wrong call_ function name

* Remove hardcoded style params

* Resolve kwargs future warning from pandas

* Remove warnings import

Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* funds + custom (#1311)

* funds + custom

* cleanup cleanup everybody everywhere

* Fix external axes conditional and a typo

Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* Add external axes mode to covid charts (#1328)

* Add portfolio menu plots (#1318)

* Portfolio view plots (commenting out report stuff)

* PA Menu broken.  Commenting out and fix tests

* portfolio optimization

* comment out commented api line

* Add notes on disabling the pa submenu

Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* Plot updates in common BA (#1335)

* Add external axes support to common/ba/finbrain

* Add external axes support to common/ba/twitter

* Add external axes support to common/ba/google

* Add external axes support to common/ba/sentimentinvestor

* Add sentimentinvestor to the notebooks API

* Fix tests

* Etf refactor (#1323)

* Refactored no ETF

* Fixed gtff import

* Fixed tests

* Fix pie chart style

* Refactored etf/candle

* Added pylint fix

* Fixed tests

* Update candle chart layout

* Update etf controller test

* Remove strange binary file

Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* Expose ETF candle function in the notebooks API

* Common BA and Common QA charts update (#1342)

* Add external axes support to common/ba/finbrain

* Add external axes support to common/ba/twitter

* Add external axes support to common/ba/google

* Add external axes support to common/ba/sentimentinvestor

* Add sentimentinvestor to the notebooks API

* Fix tests

* Update stylesheet files

* Refactor charts for common/qa

* Update the forgotten line plot

* Update tests

* Add missing arg to a docstring

* Remove scientific notation

* Black imports

Co-authored-by: Minh Hoang <nminh.hoang1023@gmail.com>

* Options refactor (#1324)

* Fixed alphaquery_view

* finished options

* Fixed pylint

* Fixed tests

* Fixed tests

* Fixed tests

* update yfinance

* Tradier + Chartexchange

* change mocks from gtff to theme.visualize output

* tests

Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>
Co-authored-by: james <jmaslek11@gmail.com>

* Refactor Stocks menu (#1325)

* Fix backtesting menu

* Refactor comparison analysis

* Refactor Dark pool shorts

* Refactor rest of menu

* Fix test

* Fix tests failing

* Fix tests fail

* Fix test failing

* Remove record mode=none to record new output

* Rewrite test output

* Rewrite test outputs

* Adding more rewritten test output

* Mock plt.show

* Mock missing plt.show

* Missing @pytest.mark.vcr

* Updating tests : common/behavioural_analysis/finbrain

* Improve notebooks API coverage for CA and DPS

* Silence annoying flake8 warning

Co-authored-by: Chavithra PARANA <chavithra@gmail.com>
Co-authored-by: Theodore Aptekarev <aptekarev@gmail.com>

* Charts update for common/pred (#1344)

* Add external axes support to common/ba/finbrain

* Add external axes support to common/ba/twitter

* Add external axes support to common/ba/google

* Add external axes support to common/ba/sentimentinvestor

* Add sentimentinvestor to the notebooks API

* Fix tests

* Update stylesheet files

* Refactor charts for common/qa

* Update the forgotten line plot

* Update tests

* Add missing arg to a docstring

* Style pred helper and controllers

* Update ETS plot

* Update plots in KNN and pred helper

* Update plot and pretty table for arima

* Update plot for common/pred/regression

* Refactor mc_view

* Fix linting

* Fix mypy

* Move plot title to the axis level to make more vertical space

Co-authored-by: Minh Hoang <nminh.hoang1023@gmail.com>
Co-authored-by: jmaslek <jmaslek11@gmail.com>

* linter

* Update common/ba test data

* Change etf candle to match stock candle

* try updating sia test

Co-authored-by: Colin Delahunty <72827203+colin99d@users.noreply.github.com>
Co-authored-by: jmaslek <jmaslek11@gmail.com>
Co-authored-by: minhhoang1023 <40023817+minhhoang1023@users.noreply.github.com>
Co-authored-by: Minh Hoang <nminh.hoang1023@gmail.com>
Co-authored-by: Chavithra PARANA <chavithra@gmail.com>
2022-02-11 20:33:16 -05:00

340 lines
12 KiB
Python

"""Helper classes."""
__docformat__ = "numpy"
import os
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
class LineAnnotateDrawer:
"""Line drawing class."""
def __init__(self, ax: matplotlib.axes = None):
self.ax = ax
def draw_lines_and_annotate(self):
"""Draw lines."""
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():
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.
"""
_STYLES_FOLDER = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "styles")
)
DEFAULT_STYLES_LOCATION = os.path.join(_STYLES_FOLDER, "default")
USER_STYLES_LOCATION = os.path.join(_STYLES_FOLDER, "user")
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 ""
"""
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: str) -> 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
"""
for font_file in os.listdir(folder):
if font_file.endswith(".otf") or font_file.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: str) -> 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
"""
for stf in os.listdir(folder):
if stf.endswith(".mplstyle"):
self.mpl_styles_available[stf.replace(".mplstyle", "")] = os.path.join(
folder, stf
)
elif stf.endswith(".mplrc.json"):
self.mpl_rcparams_available[
stf.replace(".mplrc.json", "")
] = os.path.join(folder, stf)
elif stf.endswith(".mpfstyle.json"):
self.mpf_styles_available[
stf.replace(".mpfstyle.json", "")
] = os.path.join(folder, stf)
elif stf.endswith(".richstyle.json"):
self.console_styles_available[
stf.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"]
self.volume_bar_width = self.mpl_rcparams["volume_bar_width"]
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 = "Gamestonk Terminal"
fig.text(
0.69,
0.0420,
label,
fontsize=12,
color="gray",
alpha=0.5,
)
# pylint: disable=import-outside-toplevel
def visualize_output(self, force_tight_layout: bool = True):
"""Show chart in an interactive widget."""
import gamestonk_terminal.feature_flags as gtff
from gamestonk_terminal.rich_config import console
if gtff.USE_WATERMARK:
self.add_label(plt.gcf())
if force_tight_layout:
plt.tight_layout(pad=self.tight_layout_padding)
if gtff.USE_ION:
plt.ion()
plt.show()
console.print()