mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-29 13:32:08 +00:00
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:
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user