initial commit - colin please fix tests (#3309)

* initial commit - colin please fix tests

* left a print

* removed duplicate functions, and fixed matplotlib error when no volume

* pylint fix, removed raw from display_candle

Co-authored-by: teh_coderer <me@tehcoderer.com>
Co-authored-by: James Maslek <jmaslek11@gmail.com>
This commit is contained in:
andrewkenreich
2022-11-09 11:30:19 -05:00
committed by GitHub
parent 721a7d560c
commit 2911908d09
2 changed files with 359 additions and 54 deletions

View File

@@ -10,7 +10,7 @@ from typing import List
import pandas as pd
from openbb_terminal.custom_prompt_toolkit import NestedCompleter
from openbb_terminal.common.quantitative_analysis import qa_view
from openbb_terminal import feature_flags as obbff
from openbb_terminal.decorators import log_start_end
from openbb_terminal.forex import forex_helper, fxempire_view, av_view
@@ -25,9 +25,9 @@ from openbb_terminal.parent_classes import BaseController
from openbb_terminal.rich_config import (
console,
MenuText,
translate,
get_ordered_list_sources,
)
from openbb_terminal.stocks import stocks_helper
from openbb_terminal.decorators import check_api_key
from openbb_terminal.forex.forex_helper import parse_forex_symbol
@@ -71,6 +71,8 @@ class ForexController(BaseController):
self.data = pd.DataFrame()
if session and obbff.USE_PROMPT_TOOLKIT:
one_to_hundred: dict = {str(c): {} for c in range(1, 100)}
choices: dict = {c: {} for c in self.controller_choices}
choices["load"] = {c: {} for c in FX_TICKERS}
choices["load"]["--ticker"] = {c: {} for c in FX_TICKERS}
@@ -86,7 +88,19 @@ class ForexController(BaseController):
choices["quote"]["--source"] = {
c: {} for c in get_ordered_list_sources(f"{self.PATH}quote")
}
choices["candle"]["--ma"] = None
choices["candle"] = {
"--sort": {c: {} for c in forex_helper.CANDLE_SORT},
"--plotly": {},
"-p": "--plotly",
"--descending": {},
"-d": "--descending",
"--raw": {},
"--trend": {},
"-t": "--trend",
"--ma": None,
"--limit": one_to_hundred,
"-l": "--limit",
}
choices["support"] = self.SUPPORT_CHOICES
choices["about"] = self.ABOUT_CHOICES
@@ -225,20 +239,92 @@ class ForexController(BaseController):
prog="candle",
description="Show candle for loaded fx data",
)
parser.add_argument(
"-p",
"--plotly",
dest="plotly",
action="store_false",
default=True,
help="Flag to show interactive plotly chart",
)
parser.add_argument(
"--sort",
choices=forex_helper.CANDLE_SORT,
default="",
type=str.lower,
dest="sort",
help="Choose a column to sort by. Only works when raw data is displayed.",
)
parser.add_argument(
"-d",
"--descending",
action="store_true",
dest="descending",
default=False,
help="Sort selected column descending. Only works when raw data is displayed.",
)
parser.add_argument(
"--raw",
action="store_true",
dest="raw",
default=False,
help="Shows raw data instead of chart.",
)
parser.add_argument(
"-t",
"--trend",
action="store_true",
default=False,
help="Flag to add high and low trends to candle",
dest="trendlines",
)
parser.add_argument(
"--ma",
dest="mov_avg",
type=str,
help=translate(
help=(
"Add moving average in number of days to plot and separate by a comma. "
"Value for ma (moving average) keyword needs to be greater than 1."
),
default=None,
)
ns_parser = self.parse_known_args_and_warn(parser, other_args)
parser.add_argument(
"--log",
help="Plot with y axis on log scale",
action="store_true",
default=False,
dest="logy",
)
ns_parser = self.parse_known_args_and_warn(
parser,
other_args,
EXPORT_ONLY_RAW_DATA_ALLOWED,
limit=20,
)
if ns_parser:
mov_avgs = []
if not self.data.empty:
if not self.to_symbol:
console.print("No ticker loaded. First use 'load <ticker>'")
return
data = stocks_helper.process_candle(self.data)
if ns_parser.raw:
if ns_parser.trendlines:
if (data.index[1] - data.index[0]).total_seconds() >= 86400:
data = stocks_helper.find_trendline(data, "OC_High", "high")
data = stocks_helper.find_trendline(data, "OC_Low", "low")
qa_view.display_raw(
data=data,
sortby=ns_parser.sort,
ascend=not ns_parser.descending,
limit=ns_parser.limit,
)
else:
mov_avgs = []
if ns_parser.mov_avg:
mov_list = (num for num in ns_parser.mov_avg.split(","))
@@ -256,16 +342,22 @@ class ForexController(BaseController):
)
forex_helper.display_candle(
self.data, self.to_symbol, self.from_symbol, mov_avgs
)
else:
logger.error(
"No forex historical data loaded. Load first using <load>."
)
console.print(
"[red]No forex historical data loaded. Load first using <load>.[/red]\n"
to_symbol=self.to_symbol,
from_symbol=self.from_symbol,
data=data,
use_matplotlib=ns_parser.plotly,
add_trend=ns_parser.trendlines,
ma=mov_avgs,
yscale="log" if ns_parser.logy else "linear",
)
export_data(
ns_parser.export,
os.path.dirname(os.path.abspath(__file__)),
f"{self.fx_pair}",
self.data,
)
@log_start_end(log=logger)
def call_quote(self, other_args: List[str]):
"""Process quote command."""

View File

@@ -9,15 +9,31 @@ import re
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.ticker import LogLocator, ScalarFormatter
import mplfinance as mpf
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from openbb_terminal.forex import av_model, polygon_model
from openbb_terminal.rich_config import console
from openbb_terminal.decorators import log_start_end
from openbb_terminal.helper_funcs import plot_autoscale, is_valid_axes_count
from openbb_terminal.config_terminal import theme
import openbb_terminal.config_terminal as cfg
from openbb_terminal.helper_funcs import (
is_valid_axes_count,
plot_autoscale,
lambda_long_number_format_y_axis,
)
from openbb_terminal.stocks import stocks_helper
CANDLE_SORT = [
"adjclose",
"open",
"close",
"high",
"low",
"volume",
"logret",
]
FOREX_SOURCES: Dict = {
"YahooFinance": "YahooFinance",
@@ -207,6 +223,9 @@ def display_candle(
from_symbol: str = "",
ma: Optional[Iterable[int]] = None,
external_axes: Optional[List[plt.Axes]] = None,
use_matplotlib: bool = True,
add_trend: bool = False,
yscale: str = "linear",
):
"""Show candle plot for fx data.
@@ -223,51 +242,245 @@ def display_candle(
external_axes: Optional[List[plt.Axes]]
External axes (1 axis is expected in the list), by default None
"""
candle_chart_kwargs = {
"type": "candle",
"style": theme.mpf_style,
"volume": False,
"xrotation": theme.xticks_rotation,
"scale_padding": {"left": 0.3, "right": 1, "top": 0.8, "bottom": 0.8},
"update_width_config": {
"candle_linewidth": 0.6,
"candle_width": 0.8,
"volume_linewidth": 0.8,
"volume_width": 0.8,
},
"warn_too_much_data": 20000,
}
if ma:
candle_chart_kwargs["mav"] = ma
# This plot has 1 axis
if not external_axes:
candle_chart_kwargs["returnfig"] = True
candle_chart_kwargs["figratio"] = (10, 7)
candle_chart_kwargs["figscale"] = 1.10
candle_chart_kwargs["figsize"] = plot_autoscale()
fig, ax = mpf.plot(data, **candle_chart_kwargs)
if from_symbol and to_symbol:
# We check if there's Volume data to avoid errors and empty subplots
has_volume = bool(data["Volume"].sum() > 0)
if add_trend:
if (data.index[1] - data.index[0]).total_seconds() >= 86400:
data = stocks_helper.find_trendline(data, "OC_High", "high")
data = stocks_helper.find_trendline(data, "OC_Low", "low")
if use_matplotlib:
ap0 = []
if add_trend:
if "OC_High_trend" in data.columns:
ap0.append(
mpf.make_addplot(
data["OC_High_trend"],
color=cfg.theme.up_color,
secondary_y=False,
),
)
if "OC_Low_trend" in data.columns:
ap0.append(
mpf.make_addplot(
data["OC_Low_trend"],
color=cfg.theme.down_color,
secondary_y=False,
),
)
candle_chart_kwargs = {
"type": "candle",
"style": cfg.theme.mpf_style,
"volume": has_volume,
"addplot": ap0,
"xrotation": cfg.theme.xticks_rotation,
"scale_padding": {"left": 0.3, "right": 1, "top": 0.8, "bottom": 0.8},
"update_width_config": {
"candle_linewidth": 0.6,
"candle_width": 0.8,
"volume_linewidth": 0.8,
"volume_width": 0.8,
},
"warn_too_much_data": 10000,
"yscale": yscale,
}
if ma:
candle_chart_kwargs["mav"] = ma
if external_axes is None:
candle_chart_kwargs["returnfig"] = True
candle_chart_kwargs["figratio"] = (10, 7)
candle_chart_kwargs["figscale"] = 1.10
candle_chart_kwargs["figsize"] = plot_autoscale()
candle_chart_kwargs["warn_too_much_data"] = 100_000
fig, ax = mpf.plot(data, **candle_chart_kwargs)
if has_volume:
lambda_long_number_format_y_axis(data, "Volume", ax)
fig.suptitle(
f"{from_symbol}/{to_symbol}",
x=0.055,
y=0.965,
horizontalalignment="left",
)
if ma:
# Manually construct the chart legend
colors = [cfg.theme.get_colors()[i] for i, _ in enumerate(ma)]
lines = [Line2D([0], [0], color=c) for c in colors]
labels = ["MA " + str(label) for label in ma]
ax[0].legend(lines, labels)
if yscale == "log":
ax[0].yaxis.set_major_formatter(ScalarFormatter())
ax[0].yaxis.set_major_locator(
LogLocator(base=100, subs=[1.0, 2.0, 5.0, 10.0])
)
ax[0].ticklabel_format(style="plain", axis="y")
cfg.theme.visualize_output(force_tight_layout=False)
elif is_valid_axes_count(external_axes, 2):
ax1, ax2 = external_axes
candle_chart_kwargs["ax"] = ax1
if has_volume:
candle_chart_kwargs["volume"] = ax2
mpf.plot(data, **candle_chart_kwargs)
if not use_matplotlib:
fig = make_subplots(
rows=2 if has_volume else 1,
cols=1,
shared_xaxes=True,
vertical_spacing=0.06,
subplot_titles=(
f"{from_symbol}/{to_symbol}",
"Volume" if has_volume else None,
),
row_width=[0.2, 0.7] if has_volume else [1],
)
fig.add_trace(
go.Candlestick(
x=data.index,
open=data.Open,
high=data.High,
low=data.Low,
close=data.Close,
name="OHLC",
),
row=1,
col=1,
)
if ma:
# Manually construct the chart legend
colors = [theme.get_colors()[i] for i, _ in enumerate(ma)]
plotly_colors = [
"black",
"teal",
"blue",
"purple",
"orange",
"gray",
"deepskyblue",
]
for idx, ma_val in enumerate(ma):
temp = data["Adj Close"].copy()
temp[f"ma{ma_val}"] = data["Adj Close"].rolling(ma_val).mean()
temp = temp.dropna()
fig.add_trace(
go.Scatter(
x=temp.index,
y=temp[f"ma{ma_val}"],
name=f"MA{ma_val}",
mode="lines",
line=go.scatter.Line(
color=plotly_colors[np.mod(idx, len(plotly_colors))]
),
),
row=1,
col=1,
)
lines = [Line2D([0], [0], color=c) for c in colors]
labels = ["MA " + str(label) for label in ma]
ax[0].legend(lines, labels)
theme.visualize_output(force_tight_layout=False)
if add_trend:
if "OC_High_trend" in data.columns:
fig.add_trace(
go.Scatter(
x=data.index,
y=data["OC_High_trend"],
name="High Trend",
mode="lines",
line=go.scatter.Line(color="green"),
),
row=1,
col=1,
)
if "OC_Low_trend" in data.columns:
fig.add_trace(
go.Scatter(
x=data.index,
y=data["OC_Low_trend"],
name="Low Trend",
mode="lines",
line=go.scatter.Line(color="red"),
),
row=1,
col=1,
)
elif is_valid_axes_count(external_axes, 1):
(ax1,) = external_axes
candle_chart_kwargs["ax"] = ax1
mpf.plot(data, **candle_chart_kwargs)
else:
return
if has_volume:
colors = [
"red" if row.Open < row["Adj Close"] else "green"
for _, row in data.iterrows()
]
fig.add_trace(
go.Bar(
x=data.index,
y=data.Volume,
name="Volume",
marker_color=colors,
),
row=2,
col=1,
)
fig.update_layout(
yaxis_title="Stock Price ($)",
xaxis=dict(
rangeselector=dict(
buttons=list(
[
dict(
count=1,
label="1m",
step="month",
stepmode="backward",
),
dict(
count=3,
label="3m",
step="month",
stepmode="backward",
),
dict(count=1, label="YTD", step="year", stepmode="todate"),
dict(
count=1,
label="1y",
step="year",
stepmode="backward",
),
dict(step="all"),
]
)
),
rangeslider=dict(visible=False),
type="date",
),
)
fig.update_layout(
updatemenus=[
dict(
buttons=[
dict(
label="linear",
method="relayout",
args=[{"yaxis.type": "linear"}],
),
dict(
label="log",
method="relayout",
args=[{"yaxis.type": "log"}],
),
]
)
]
)
fig.show(config=dict({"scrollZoom": True}))
return
@log_start_end(log=logger)