mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
init
This commit is contained in:
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,2 +1,3 @@
|
||||
* linguist-vendored
|
||||
*.py linguist-vendored=false
|
||||
text eol=lf
|
||||
|
||||
@@ -161,6 +161,7 @@ def display_covid_ov(
|
||||
export: str = "",
|
||||
sheet_name: Optional[str] = None,
|
||||
plot: bool = True,
|
||||
external_axes: bool = False,
|
||||
) -> Union[OpenBBFigure, None]:
|
||||
"""Prints table showing historical cases and deaths by country.
|
||||
|
||||
@@ -178,6 +179,8 @@ def display_covid_ov(
|
||||
Format to export data
|
||||
plot: bool
|
||||
Flag to display historical plot
|
||||
external_axes : bool, optional
|
||||
Whether to return the figure object or not, by default False
|
||||
"""
|
||||
fig = OpenBBFigure()
|
||||
|
||||
@@ -207,7 +210,7 @@ def display_covid_ov(
|
||||
fig,
|
||||
)
|
||||
|
||||
return fig.show(external=raw)
|
||||
return fig.show(external=raw or external_axes)
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
|
||||
@@ -266,6 +266,7 @@ def display_bw(
|
||||
>>> df = openbb.stocks.load("AAPL")
|
||||
>>> openbb.qa.bw(data=df, target="Adj Close")
|
||||
"""
|
||||
data = data.copy()
|
||||
start = data[target].index[0]
|
||||
|
||||
color = theme.get_colors()[0]
|
||||
@@ -287,7 +288,10 @@ def display_bw(
|
||||
]
|
||||
|
||||
fig = OpenBBFigure(
|
||||
title=f"{['Monthly','Yearly'][yearly]} box plot of {symbol} {target} from {start.strftime('%Y-%m-%d')}",
|
||||
title=(
|
||||
f"{['Monthly','Yearly'][yearly]} box plot of {symbol.upper()} "
|
||||
f"{target} from {start.strftime('%Y-%m-%d')}"
|
||||
),
|
||||
yaxis_title=target,
|
||||
xaxis_title=["Monthly", "Yearly"][yearly],
|
||||
)
|
||||
|
||||
6
openbb_terminal/core/plots/plotly.html
vendored
6
openbb_terminal/core/plots/plotly.html
vendored
@@ -91,7 +91,11 @@
|
||||
// calling OpenBBMain to avoid errors
|
||||
if (typeof OpenBBMain != "undefined" && window.plotly_figure) {
|
||||
clearInterval(interval);
|
||||
OpenBBMain(window.plotly_figure, document.getElementById("openbb_chart"));
|
||||
CHART_DIV = document.getElementById("openbb_chart");
|
||||
CSV_DIV = document.getElementById("popup_csv");
|
||||
TEXT_DIV = document.getElementById("popup_text");
|
||||
TITLE_DIV = document.getElementById("popup_title");
|
||||
OpenBBMain(window.plotly_figure, CHART_DIV, CSV_DIV, TEXT_DIV, TITLE_DIV);
|
||||
}
|
||||
}, 20);
|
||||
</script>
|
||||
|
||||
@@ -260,6 +260,7 @@ class OpenBBFigure(go.Figure):
|
||||
self.__dict__ = fig.__dict__
|
||||
|
||||
self._has_secondary_y = kwargs.pop("has_secondary_y", False)
|
||||
self._subplots_kwargs: Dict[str, Any] = kwargs.pop("subplots_kwargs", {})
|
||||
self._multi_rows = kwargs.pop("multi_rows", False)
|
||||
self._added_logscale = False
|
||||
self._date_xaxs: dict = {}
|
||||
@@ -284,6 +285,16 @@ class OpenBBFigure(go.Figure):
|
||||
width=plots_backend().WIDTH,
|
||||
)
|
||||
|
||||
@property
|
||||
def subplots_kwargs(self):
|
||||
"""Get subplots kwargs property."""
|
||||
return self._subplots_kwargs
|
||||
|
||||
@subplots_kwargs.setter
|
||||
def subplots_kwargs(self, value):
|
||||
"""Get subplots kwargs setter."""
|
||||
self._subplots_kwargs = value
|
||||
|
||||
@property
|
||||
def has_subplots(self):
|
||||
"""Has subplots property."""
|
||||
@@ -343,7 +354,8 @@ class OpenBBFigure(go.Figure):
|
||||
specs : `List[List[dict]]`, optional
|
||||
Subplot specs, by default `[[{}] * cols] * rows` (all subplots are the same size)
|
||||
"""
|
||||
fig = make_subplots(
|
||||
# We save the original kwargs to store them in the figure for later use
|
||||
subplots_kwargs = dict(
|
||||
rows=rows,
|
||||
cols=cols,
|
||||
shared_xaxes=shared_xaxes,
|
||||
@@ -354,7 +366,13 @@ class OpenBBFigure(go.Figure):
|
||||
specs=specs or [[{}] * cols] * rows,
|
||||
**kwargs,
|
||||
)
|
||||
kwargs = {"multi_rows": rows > 1}
|
||||
|
||||
fig = make_subplots(**subplots_kwargs)
|
||||
|
||||
kwargs = {
|
||||
"multi_rows": rows > 1,
|
||||
"subplots_kwargs": subplots_kwargs,
|
||||
}
|
||||
if specs and any(
|
||||
spec.get("secondary_y", False) for row in specs for spec in row if spec
|
||||
):
|
||||
@@ -362,51 +380,48 @@ class OpenBBFigure(go.Figure):
|
||||
|
||||
return cls(fig, **kwargs)
|
||||
|
||||
def add_trend(self, data: pd.DataFrame) -> None:
|
||||
def add_trend(
|
||||
self,
|
||||
data: pd.DataFrame,
|
||||
row: int = 1,
|
||||
col: int = 1,
|
||||
secondary_y: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Add a trend line to the figure.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : `pd.DataFrame`
|
||||
Data to plot
|
||||
name : `str`
|
||||
Name of the plot
|
||||
row : `int`, optional
|
||||
Row number, by default None
|
||||
col : `int`, optional
|
||||
Column number, by default None
|
||||
secondary_y : `bool`, optional
|
||||
Whether to plot on secondary y axis, by default None
|
||||
"""
|
||||
try:
|
||||
if "OC_High_trend" in data.columns:
|
||||
high_trend = data.loc[
|
||||
data["OC_High_trend"]
|
||||
.idxmin() : data["OC_High_trend"] # noqa: E203
|
||||
.idxmax()
|
||||
]
|
||||
self.add_shape(
|
||||
type="line",
|
||||
name="High Trend",
|
||||
x0=high_trend.index[0],
|
||||
y0=high_trend["OC_High_trend"].iloc[0],
|
||||
x1=high_trend.index[-1],
|
||||
y1=high_trend["OC_High_trend"].iloc[-1],
|
||||
line=dict(color=theme.up_color, width=2),
|
||||
row=1,
|
||||
col=1,
|
||||
)
|
||||
if "OC_Low_trend" in data.columns:
|
||||
low_trend = data.loc[
|
||||
data["OC_Low_trend"]
|
||||
.idxmin() : data["OC_Low_trend"] # noqa: E203
|
||||
.idxmax()
|
||||
]
|
||||
self.add_shape(
|
||||
type="line",
|
||||
name="Low Trend",
|
||||
x0=low_trend.index[0],
|
||||
y0=low_trend["OC_Low_trend"].iloc[0],
|
||||
x1=low_trend.index[-1],
|
||||
y1=low_trend["OC_Low_trend"].iloc[-1],
|
||||
line=dict(color=theme.down_color, width=2),
|
||||
row=1,
|
||||
col=1,
|
||||
)
|
||||
for column, color in zip(
|
||||
["OC_High_trend", "OC_Low_trend"], [theme.up_color, theme.down_color]
|
||||
):
|
||||
if column in data.columns:
|
||||
name = column.split("_")[1].title()
|
||||
trend = data.copy().dropna()
|
||||
self.add_shape(
|
||||
type="line",
|
||||
name=f"{name} Trend",
|
||||
x0=trend.index[0],
|
||||
y0=trend[column].iloc[0],
|
||||
x1=trend.index[-1],
|
||||
y1=trend[column].iloc[-1],
|
||||
line=dict(color=color, width=2),
|
||||
row=row,
|
||||
col=col,
|
||||
secondary_y=secondary_y,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
console.print("[red]Error adding trend line[/red]")
|
||||
|
||||
@@ -787,14 +802,52 @@ class OpenBBFigure(go.Figure):
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def chart_volume_scaling(
|
||||
df_volume: pd.DataFrame, range_x: int = 4
|
||||
) -> Dict[str, list]:
|
||||
"""Takes df_volume and returns volume_ticks, tickvals for chart volume scaling
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df_volume : pd.DataFrame
|
||||
Dataframe of volume (e.g. df_volume = df["Volume"])
|
||||
range_x : int, optional
|
||||
Number to multiply volume, by default 4
|
||||
|
||||
Returns
|
||||
-------
|
||||
Dict[str, list]
|
||||
{"range": volume_range, "ticks": tickvals}
|
||||
"""
|
||||
df_volume = df_volume.apply(lambda x: f"{x:.1f}")
|
||||
df_volume = pd.to_numeric(df_volume.astype(float))
|
||||
volume_ticks = int(df_volume.max().max())
|
||||
round_digits = -3
|
||||
first_val = round(volume_ticks * 0.20, round_digits)
|
||||
|
||||
for x, y in zip([2, 5, 6, 7, 8, 9, 10], [1, 4, 5, 6, 7, 8, 9]):
|
||||
if len(str(volume_ticks)) > x:
|
||||
round_digits = -y
|
||||
first_val = round(volume_ticks * 0.20, round_digits)
|
||||
|
||||
tickvals = [
|
||||
floor(first_val),
|
||||
floor(first_val * 2),
|
||||
floor(first_val * 3),
|
||||
floor(first_val * 4),
|
||||
]
|
||||
volume_range = [0, floor(volume_ticks * range_x)]
|
||||
|
||||
return {"range": volume_range, "ticks": tickvals}
|
||||
|
||||
def add_stock_volume(
|
||||
self,
|
||||
df_stock: pd.DataFrame,
|
||||
close_col: str = "Close",
|
||||
volume_col: str = "Volume",
|
||||
row: int = 2,
|
||||
col: int = 1,
|
||||
secondary_y: bool = False,
|
||||
close_col: Optional[str] = "Close",
|
||||
volume_col: Optional[str] = "Volume",
|
||||
row: Optional[int] = 1,
|
||||
col: Optional[int] = 1,
|
||||
) -> None:
|
||||
"""Add the volume of a stock to the figure.
|
||||
|
||||
@@ -810,21 +863,43 @@ class OpenBBFigure(go.Figure):
|
||||
Row number, by default 2
|
||||
col : `int`, optional
|
||||
Column number, by default 1
|
||||
secondary_y : `bool`, optional
|
||||
Whether to use the secondary y axis, by default False
|
||||
"""
|
||||
colors = [
|
||||
theme.down_color if row.Open < row[close_col] else theme.up_color
|
||||
for _, row in df_stock.iterrows()
|
||||
]
|
||||
vol_scale = self.chart_volume_scaling(df_stock[volume_col])
|
||||
self.add_bar(
|
||||
x=df_stock.index,
|
||||
y=df_stock[volume_col],
|
||||
name="Volume",
|
||||
marker_color=colors,
|
||||
yaxis="y2",
|
||||
row=row,
|
||||
col=col,
|
||||
secondary_y=secondary_y,
|
||||
secondary_y=False,
|
||||
)
|
||||
ticksize = 14 - (self.subplots_kwargs["rows"] // 1.5)
|
||||
self.update_layout(
|
||||
yaxis=dict(
|
||||
fixedrange=True,
|
||||
side="left",
|
||||
nticks=10,
|
||||
range=vol_scale["range"],
|
||||
tickvals=vol_scale["ticks"],
|
||||
showgrid=False,
|
||||
showline=False,
|
||||
zeroline=False,
|
||||
tickfont=dict(size=ticksize),
|
||||
),
|
||||
yaxis2=dict(
|
||||
autorange=True,
|
||||
side="right",
|
||||
fixedrange=False,
|
||||
anchor="x",
|
||||
layer="above traces",
|
||||
overlaying="y",
|
||||
),
|
||||
)
|
||||
|
||||
def add_legend_label(
|
||||
@@ -1062,6 +1137,13 @@ class OpenBBFigure(go.Figure):
|
||||
output: Optional[List[datetime]] = None
|
||||
subplots = self.get_subplots_dict()
|
||||
|
||||
try:
|
||||
false_y = list(self.select_traces(secondary_y=False))
|
||||
true_y = list(self.select_traces(secondary_y=True))
|
||||
except Exception:
|
||||
false_y = []
|
||||
true_y = []
|
||||
|
||||
for trace in self.select_traces():
|
||||
if not hasattr(trace, "xaxis"):
|
||||
continue
|
||||
@@ -1073,9 +1155,15 @@ class OpenBBFigure(go.Figure):
|
||||
if isinstance(x, (datetime, np.datetime64, pd.DatetimeIndex)):
|
||||
output = trace.x
|
||||
name = trace.name if hasattr(trace, "name") else f"{trace}"
|
||||
|
||||
secondary_y: Optional[bool] = trace in true_y
|
||||
if trace not in (false_y + true_y):
|
||||
secondary_y = None
|
||||
|
||||
self._date_xaxs[trace.xaxis] = {
|
||||
"yaxis": trace.yaxis,
|
||||
"name": name,
|
||||
"secondary_y": secondary_y,
|
||||
}
|
||||
self._subplot_xdates.setdefault(row, {}).setdefault(
|
||||
col, []
|
||||
@@ -1146,15 +1234,7 @@ class OpenBBFigure(go.Figure):
|
||||
else:
|
||||
rangebreaks.append(dict(bounds=[15.99, 9.50], pattern="hour"))
|
||||
|
||||
if not self._has_secondary_y:
|
||||
self.update_xaxes(rangebreaks=rangebreaks, row=row, col=col)
|
||||
else:
|
||||
for entry in self._date_xaxs.values():
|
||||
self.update_xaxes(
|
||||
rangebreaks=rangebreaks,
|
||||
type="date",
|
||||
selector=dict(anchor=entry["yaxis"]),
|
||||
)
|
||||
self.update_xaxes(rangebreaks=rangebreaks, row=row, col=col)
|
||||
|
||||
def hide_holidays(self, prepost: bool = False) -> None:
|
||||
"""Add rangebreaks to hide holidays on the xaxis.
|
||||
@@ -1402,7 +1482,7 @@ class OpenBBFigure(go.Figure):
|
||||
return
|
||||
|
||||
margin_add = (
|
||||
[80, 60, 85, 60, 0] if not self._has_secondary_y else [80, 50, 85, 40, 0]
|
||||
[80, 60, 85, 60, 0] if not self._has_secondary_y else [60, 50, 85, 40, 0]
|
||||
)
|
||||
|
||||
# We adjust margins
|
||||
@@ -1498,7 +1578,7 @@ class OpenBBFigure(go.Figure):
|
||||
|
||||
self._feature_flags_applied = True
|
||||
|
||||
def add_logscale_menus(self) -> None:
|
||||
def add_logscale_menus(self, yaxis: str = "yaxis") -> None:
|
||||
"""Set the menus for the figure."""
|
||||
self._added_logscale = True
|
||||
self.update_layout(
|
||||
@@ -1547,12 +1627,12 @@ class OpenBBFigure(go.Figure):
|
||||
dict(
|
||||
label="linear ",
|
||||
method="relayout",
|
||||
args=[{"yaxis.type": "linear"}],
|
||||
args=[{f"{yaxis}.type": "linear"}],
|
||||
),
|
||||
dict(
|
||||
label="log",
|
||||
method="relayout",
|
||||
args=[{"yaxis.type": "log"}],
|
||||
args=[{f"{yaxis}.type": "log"}],
|
||||
),
|
||||
],
|
||||
y=1.07,
|
||||
|
||||
@@ -94,6 +94,7 @@ class PltTA(metaclass=PluginMeta):
|
||||
close_column: Optional[str] = "Close"
|
||||
params: Dict[str, TAIndicator] = {}
|
||||
inchart_colors: List[str] = []
|
||||
show_volume: bool = True
|
||||
|
||||
__static_methods__: list = []
|
||||
__indicators__: List[Indicator] = []
|
||||
|
||||
@@ -39,7 +39,10 @@ class Custom(PltTA):
|
||||
cond4 = df["High"][i - 1] > df["High"][i - 2]
|
||||
return cond1 and cond2 and cond3 and cond4
|
||||
|
||||
df_ta2 = df_ta.copy()
|
||||
df_ta2 = df_ta.copy().loc[
|
||||
(df_ta.index >= datetime.now() - timedelta(days=200))
|
||||
& (df_ta.index < datetime.now())
|
||||
]
|
||||
if df_ta2.index[-2].date() != df_ta2.index[-1].date():
|
||||
interval = 1440
|
||||
else:
|
||||
@@ -82,6 +85,7 @@ class Custom(PltTA):
|
||||
showlegend=False,
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
fig.add_hline(
|
||||
y=lv,
|
||||
@@ -90,6 +94,7 @@ class Custom(PltTA):
|
||||
line_color="rgba(120, 70, 200, 0.70)",
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
elif is_resistance(df_ta2, i):
|
||||
lv = df_ta2["High"][i]
|
||||
@@ -108,6 +113,7 @@ class Custom(PltTA):
|
||||
showlegend=False,
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
fig.add_hline(
|
||||
y=lv,
|
||||
@@ -116,6 +122,7 @@ class Custom(PltTA):
|
||||
line_color="rgba(120, 70, 200, 0.70)",
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
|
||||
return fig
|
||||
@@ -158,6 +165,7 @@ class Custom(PltTA):
|
||||
showlegend=False,
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
df_ta2 = df_ta.copy()
|
||||
interval = 1440
|
||||
@@ -198,6 +206,7 @@ class Custom(PltTA):
|
||||
showlegend=False,
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
@@ -32,6 +32,7 @@ class Momentum(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
fig.add_hrect(
|
||||
@@ -43,6 +44,7 @@ class Momentum(PltTA):
|
||||
line_width=0,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_hrect(
|
||||
y0=-100,
|
||||
@@ -53,6 +55,7 @@ class Momentum(PltTA):
|
||||
line_width=0,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_hline(
|
||||
y=100,
|
||||
@@ -61,6 +64,7 @@ class Momentum(PltTA):
|
||||
line=dict(width=2, color=theme.down_color, dash="dash"),
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_hline(
|
||||
y=-100,
|
||||
@@ -69,11 +73,12 @@ class Momentum(PltTA):
|
||||
line=dict(width=2, color=theme.up_color, dash="dash"),
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>CCI</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -82,7 +87,7 @@ class Momentum(PltTA):
|
||||
font_size=14,
|
||||
font_color="#e0b700",
|
||||
)
|
||||
fig["layout"][f"yaxis{subplot_row}"].update(nticks=5, autorange=True)
|
||||
fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=5, autorange=True)
|
||||
|
||||
return fig, subplot_row + 1
|
||||
|
||||
@@ -99,6 +104,7 @@ class Momentum(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_scatter(
|
||||
name="Signal",
|
||||
@@ -109,10 +115,11 @@ class Momentum(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>CG</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -123,7 +130,7 @@ class Momentum(PltTA):
|
||||
)
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="Signal",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -133,7 +140,7 @@ class Momentum(PltTA):
|
||||
font_size=14,
|
||||
font_color="#ef7d00",
|
||||
)
|
||||
fig["layout"][f"yaxis{subplot_row}"].update(nticks=5, autorange=True)
|
||||
fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=5, autorange=True)
|
||||
|
||||
return fig, subplot_row + 1
|
||||
|
||||
@@ -151,6 +158,7 @@ class Momentum(PltTA):
|
||||
line=dict(color=self.inchart_colors[inchart_index], width=2),
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
fig.add_annotation(
|
||||
xref="paper",
|
||||
@@ -196,6 +204,7 @@ class Momentum(PltTA):
|
||||
textfont=dict(color=theme.down_color),
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
fig.add_scatter(
|
||||
x=high.index,
|
||||
@@ -207,6 +216,7 @@ class Momentum(PltTA):
|
||||
textfont=dict(color=theme.up_color),
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
|
||||
fig.add_annotation(
|
||||
@@ -239,6 +249,7 @@ class Momentum(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_scatter(
|
||||
name="Fisher Signal",
|
||||
@@ -249,13 +260,14 @@ class Momentum(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
dmax = df_ta[fishers_col].max()
|
||||
dmin = df_ta[fishers_col].min()
|
||||
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>FISHER</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -266,7 +278,7 @@ class Momentum(PltTA):
|
||||
)
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>SIGNAL</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -284,6 +296,7 @@ class Momentum(PltTA):
|
||||
line_width=0,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_hrect(
|
||||
y0=-2,
|
||||
@@ -294,6 +307,7 @@ class Momentum(PltTA):
|
||||
line_width=0,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_hline(
|
||||
y=2,
|
||||
@@ -303,6 +317,7 @@ class Momentum(PltTA):
|
||||
line=dict(width=2, color=theme.down_color, dash="dash"),
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_hline(
|
||||
y=-2,
|
||||
@@ -312,8 +327,9 @@ class Momentum(PltTA):
|
||||
line=dict(width=2, color=theme.up_color, dash="dash"),
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig["layout"][f"yaxis{subplot_row}"].update(nticks=5, autorange=True)
|
||||
fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=5, autorange=True)
|
||||
|
||||
return fig, subplot_row + 1
|
||||
|
||||
@@ -329,6 +345,7 @@ class Momentum(PltTA):
|
||||
marker_color="#1a97ea",
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_scatter(
|
||||
name="MACD",
|
||||
@@ -339,6 +356,7 @@ class Momentum(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_scatter(
|
||||
name="MACD Signal",
|
||||
@@ -349,11 +367,12 @@ class Momentum(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>MACD</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -364,7 +383,7 @@ class Momentum(PltTA):
|
||||
)
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>SIGNAL</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -373,7 +392,7 @@ class Momentum(PltTA):
|
||||
xshift=-3,
|
||||
font_color="rgb(7, 166, 52)",
|
||||
)
|
||||
fig["layout"][f"yaxis{subplot_row}"].update(autorange=True, nticks=5)
|
||||
fig["layout"][f"yaxis{subplot_row + 1}"].update(autorange=True, nticks=5)
|
||||
|
||||
return fig, subplot_row + 1
|
||||
|
||||
@@ -390,11 +409,12 @@ class Momentum(PltTA):
|
||||
opacity=1,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>RSI</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -412,6 +432,7 @@ class Momentum(PltTA):
|
||||
line_width=0,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_hrect(
|
||||
y0=0,
|
||||
@@ -422,6 +443,7 @@ class Momentum(PltTA):
|
||||
line_width=0,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_hline(
|
||||
y=70,
|
||||
@@ -431,6 +453,7 @@ class Momentum(PltTA):
|
||||
line=dict(width=2, color=theme.down_color, dash="dash"),
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_hline(
|
||||
y=30,
|
||||
@@ -440,8 +463,9 @@ class Momentum(PltTA):
|
||||
line=dict(width=2, color=theme.up_color, dash="dash"),
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig["layout"][f"yaxis{subplot_row}"].update(tickvals=[0, 30, 70, 100])
|
||||
fig["layout"][f"yaxis{subplot_row + 1}"].update(tickvals=[0, 30, 70, 100])
|
||||
|
||||
return fig, subplot_row + 1
|
||||
|
||||
@@ -457,6 +481,7 @@ class Momentum(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_scatter(
|
||||
name="STOCH %D",
|
||||
@@ -467,11 +492,12 @@ class Momentum(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>STOCH</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -480,6 +506,6 @@ class Momentum(PltTA):
|
||||
font_size=14,
|
||||
font_color="#e0b700",
|
||||
)
|
||||
fig["layout"][f"yaxis{subplot_row}"].update(nticks=5, autorange=True)
|
||||
fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=5, autorange=True)
|
||||
|
||||
return fig, subplot_row + 1
|
||||
|
||||
@@ -42,6 +42,7 @@ class Overlap(PltTA):
|
||||
connectgaps=True,
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
fig.add_annotation(
|
||||
xref="paper",
|
||||
@@ -75,6 +76,7 @@ class Overlap(PltTA):
|
||||
opacity=0.8,
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
fig.add_annotation(
|
||||
xref="paper",
|
||||
|
||||
@@ -25,6 +25,7 @@ class Trend(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_scatter(
|
||||
name="+DI",
|
||||
@@ -35,6 +36,7 @@ class Trend(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_scatter(
|
||||
name="-DI",
|
||||
@@ -45,11 +47,12 @@ class Trend(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>ADX</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -60,7 +63,7 @@ class Trend(PltTA):
|
||||
)
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text=(
|
||||
f"<span style='color: {theme.up_color}'>D+</span><br>"
|
||||
f"<span style='color: {theme.down_color}'>D-</span>"
|
||||
@@ -82,8 +85,9 @@ class Trend(PltTA):
|
||||
line=dict(color="white", dash="dash"),
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig["layout"][f"yaxis{subplot_row}"].update(nticks=5, autorange=True)
|
||||
fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=5, autorange=True)
|
||||
|
||||
return fig, subplot_row + 1
|
||||
|
||||
@@ -102,6 +106,7 @@ class Trend(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_scatter(
|
||||
name="Aroon Down",
|
||||
@@ -112,11 +117,12 @@ class Trend(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>Aroon</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -127,7 +133,7 @@ class Trend(PltTA):
|
||||
)
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text=(
|
||||
f"<span style='color: {theme.up_color}'>↑</span><br>"
|
||||
f"<span style='color: {theme.down_color}'>↓</span>"
|
||||
@@ -148,6 +154,7 @@ class Trend(PltTA):
|
||||
line=dict(color="white", dash="dash"),
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
subplot_row += 1
|
||||
@@ -162,11 +169,12 @@ class Trend(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>Aroon<br>OSC</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -175,7 +183,7 @@ class Trend(PltTA):
|
||||
font_size=14,
|
||||
font_color="#e0b700",
|
||||
)
|
||||
fig["layout"][f"yaxis{subplot_row}"].update(
|
||||
fig["layout"][f"yaxis{subplot_row + 1}"].update(
|
||||
tickvals=[-100, 0, 100],
|
||||
ticktext=["-100", "0", "100"],
|
||||
nticks=5,
|
||||
|
||||
@@ -26,11 +26,12 @@ class Volatility(PltTA):
|
||||
line=dict(width=1, color=theme.get_colors()[1]),
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>ATR</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -58,6 +59,7 @@ class Volatility(PltTA):
|
||||
line=dict(width=1, color=theme.up_color),
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
fig.add_scatter(
|
||||
name=f"{columns_regex(df_ta, 'BBL')[0]}",
|
||||
@@ -68,6 +70,7 @@ class Volatility(PltTA):
|
||||
line=dict(width=1, color=theme.down_color),
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
fig.add_scatter(
|
||||
name=f"{columns_regex(df_ta, 'BBM')[0]}",
|
||||
@@ -78,6 +81,7 @@ class Volatility(PltTA):
|
||||
line=dict(width=1, color=theme.get_colors()[1], dash="dash"),
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
bbands_text = (
|
||||
columns_regex(df_ta, "BBL")[0]
|
||||
@@ -121,6 +125,7 @@ class Volatility(PltTA):
|
||||
line=dict(width=0.3, color="#EF6689"),
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
fig.add_scatter(
|
||||
name=f"{columns_regex(df_ta, 'DCL')[0]}",
|
||||
@@ -133,14 +138,20 @@ class Volatility(PltTA):
|
||||
fillcolor=fillcolor,
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
|
||||
donchian_text = (
|
||||
columns_regex(df_ta, "DCL")[0]
|
||||
.replace("DCL_", "DC")
|
||||
.replace("_", ",")
|
||||
.split(".")[0]
|
||||
)
|
||||
|
||||
fig.add_annotation(
|
||||
xref="paper",
|
||||
yref="paper",
|
||||
text=(
|
||||
f"<b>DC{self.params['donchian'].get_argument_values('upper_length') or ''},"
|
||||
f"{self.params['donchian'].get_argument_values('lower_length') or ''}</b>"
|
||||
),
|
||||
text=f"<b>{donchian_text}</b>",
|
||||
x=0,
|
||||
xanchor="left",
|
||||
yshift=-inchart_index * 18,
|
||||
@@ -173,6 +184,7 @@ class Volatility(PltTA):
|
||||
line=dict(width=0.3, color="#EF6689"),
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
fig.add_scatter(
|
||||
name=f"{columns_regex(df_ta, 'KCL')[0]}",
|
||||
@@ -185,14 +197,18 @@ class Volatility(PltTA):
|
||||
fillcolor=fillcolor,
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
kctext = (
|
||||
columns_regex(df_ta, "KCL")[0]
|
||||
.replace("KCL_", "KC")
|
||||
.replace("_", ",")
|
||||
.split(".")[0]
|
||||
)
|
||||
fig.add_annotation(
|
||||
xref="paper",
|
||||
yref="paper",
|
||||
text=(
|
||||
f"<b>KC{self.params['kc'].get_argument_values('length') or ''},"
|
||||
f"{self.params['kc'].get_argument_values('scalar') or ''}</b>"
|
||||
),
|
||||
text=f"<b>{kctext}</b>",
|
||||
x=0,
|
||||
xanchor="left",
|
||||
yshift=-inchart_index * 18,
|
||||
|
||||
@@ -26,6 +26,7 @@ class Volume(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
fig.add_hline(
|
||||
@@ -36,11 +37,12 @@ class Volume(PltTA):
|
||||
line=dict(color="white", dash="dash", width=2),
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>AD</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -49,7 +51,7 @@ class Volume(PltTA):
|
||||
font_size=14,
|
||||
font_color=theme.get_colors()[1],
|
||||
)
|
||||
fig["layout"][f"yaxis{subplot_row}"].update(nticks=3, autorange=True)
|
||||
fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=3, autorange=True)
|
||||
|
||||
return fig, subplot_row + 1
|
||||
|
||||
@@ -66,11 +68,12 @@ class Volume(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>ADOSC</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -95,11 +98,12 @@ class Volume(PltTA):
|
||||
opacity=0.9,
|
||||
row=subplot_row,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
|
||||
fig.add_annotation(
|
||||
xref=f"x{subplot_row} domain",
|
||||
yref=f"y{subplot_row} domain",
|
||||
yref=f"y{subplot_row + 1} domain",
|
||||
text="<b>OBV</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
@@ -108,6 +112,6 @@ class Volume(PltTA):
|
||||
font_size=14,
|
||||
font_color=theme.get_colors()[1],
|
||||
)
|
||||
fig["layout"][f"yaxis{subplot_row}"].update(nticks=5, autorange=True)
|
||||
fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=5, autorange=True)
|
||||
|
||||
return fig, subplot_row + 1
|
||||
|
||||
@@ -8,12 +8,11 @@ from typing import Any, Dict, List, Optional, Type, Union
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from openbb_terminal import OpenBBFigure
|
||||
from openbb_terminal.base_helpers import console
|
||||
from openbb_terminal import OpenBBFigure, theme
|
||||
from openbb_terminal.common.technical_analysis import ta_helpers
|
||||
from openbb_terminal.config_terminal import theme
|
||||
from openbb_terminal.core.plots.plotly_ta.base import PltTA
|
||||
from openbb_terminal.core.plots.plotly_ta.data_classes import ChartIndicators
|
||||
from openbb_terminal.rich_config import console
|
||||
|
||||
PLUGINS_PATH = Path(__file__).parent / "plugins"
|
||||
PLOTLY_TA: Optional["PlotlyTA"] = None
|
||||
@@ -316,14 +315,13 @@ class PlotlyTA(PltTA):
|
||||
check_active = self.indicators.get_active_ids()
|
||||
subplots = [subplot for subplot in self.subplots if subplot in check_active]
|
||||
|
||||
if self.show_volume:
|
||||
subplots.append("volume")
|
||||
|
||||
check_rows = min(len(self.check_subplots(subplots)), 4)
|
||||
check_rows += 1 if "aroon" in subplots and (check_rows + 1) < 5 else 0
|
||||
|
||||
specs = [[{"secondary_y": True}]] + [[{"secondary_y": False}]] * check_rows
|
||||
|
||||
output = row_params.get(str(check_rows), dict(rows=1, row_width=[1]))
|
||||
output.update(dict(cols=1, vertical_spacing=0.04))
|
||||
output.update(dict(cols=1, vertical_spacing=0.04, specs=specs))
|
||||
|
||||
return output
|
||||
|
||||
@@ -342,14 +340,13 @@ class PlotlyTA(PltTA):
|
||||
fig : OpenBBFigure
|
||||
Plotly figure with candlestick/line chart and volume bar chart (if enabled)
|
||||
"""
|
||||
|
||||
fig = OpenBBFigure.create_subplots(
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
shared_xaxes=True,
|
||||
vertical_spacing=0.06,
|
||||
subplot_titles=[f"{symbol}", "Volume" if self.show_volume else ""],
|
||||
row_width=[0.2, 0.7],
|
||||
row_width=[1],
|
||||
specs=[[{"secondary_y": True}]],
|
||||
)
|
||||
if candles:
|
||||
fig.add_candlestick(
|
||||
@@ -358,10 +355,11 @@ class PlotlyTA(PltTA):
|
||||
high=self.df_stock.High,
|
||||
low=self.df_stock.Low,
|
||||
close=self.df_stock.Close,
|
||||
name="OHLC",
|
||||
name=f"{symbol} OHLC",
|
||||
showlegend=False,
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
else:
|
||||
fig.add_scatter(
|
||||
@@ -371,14 +369,12 @@ class PlotlyTA(PltTA):
|
||||
connectgaps=True,
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
)
|
||||
fig.update_layout(yaxis=dict(nticks=15))
|
||||
self.inchart_colors = theme.get_colors()[1:]
|
||||
|
||||
if self.show_volume:
|
||||
fig.add_stock_volume(self.df_stock, self.close_column, row=2, col=1) # type: ignore
|
||||
|
||||
fig.update_layout(yaxis_title="Price ($)")
|
||||
fig.set_title(symbol, x=0.5, y=0.98, xanchor="center", yanchor="top")
|
||||
return fig
|
||||
|
||||
def plot_fig(
|
||||
@@ -416,15 +412,15 @@ class PlotlyTA(PltTA):
|
||||
|
||||
subplot_row, fig_new = 2, {}
|
||||
inchart_index, ma_done = 0, False
|
||||
subplot_row += (
|
||||
1 if self.show_volume and "Volume" in self.df_stock.columns else 0
|
||||
)
|
||||
|
||||
figure = self.process_fig(figure)
|
||||
|
||||
# Aroon indicator is always plotted first since it has 2 subplot rows
|
||||
plot_indicators = sorted(
|
||||
self.indicators.get_active_ids(), key=lambda x: x in self.subplots
|
||||
self.indicators.get_active_ids(),
|
||||
key=lambda x: 50 if x == "aroon" else 999 if x in self.subplots else 1,
|
||||
)
|
||||
|
||||
for indicator in plot_indicators:
|
||||
try:
|
||||
if indicator in self.subplots:
|
||||
@@ -447,10 +443,14 @@ class PlotlyTA(PltTA):
|
||||
|
||||
fig_new.update(figure.to_plotly_json())
|
||||
|
||||
if subplot_row > 5 and indicator != plot_indicators[-1]:
|
||||
remaining = plot_indicators[plot_indicators.index(indicator) + 1 :]
|
||||
remaining_subplots = list(
|
||||
set(plot_indicators[plot_indicators.index(indicator) + 1 :])
|
||||
- set(self.inchart)
|
||||
)
|
||||
if subplot_row > 5 and remaining_subplots:
|
||||
console.print(
|
||||
f"[bold red]Reached max number of subplots, skipping {', '.join(remaining)}[/]"
|
||||
f"[bold red]Reached max number of subplots. "
|
||||
f"Skipping {', '.join(remaining_subplots)}[/]"
|
||||
)
|
||||
break
|
||||
except Exception as e:
|
||||
@@ -458,6 +458,13 @@ class PlotlyTA(PltTA):
|
||||
continue
|
||||
|
||||
figure.update(fig_new)
|
||||
figure.set_yaxis_title(
|
||||
"Price ($)",
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=self.show_volume,
|
||||
nticks=15 if subplot_row < 3 else 10,
|
||||
)
|
||||
figure.update_traces(
|
||||
selector=dict(type="scatter", mode="lines"), connectgaps=True
|
||||
)
|
||||
@@ -466,9 +473,9 @@ class PlotlyTA(PltTA):
|
||||
|
||||
# We remove xaxis labels from all but bottom subplot, and we make sure
|
||||
# they all match the bottom one
|
||||
xbottom = list(figure.select_xaxes())[-1].anchor
|
||||
xbottom = f"y{subplot_row}"
|
||||
for xa in figure.select_xaxes():
|
||||
if not self.show_volume and subplot_row == 2:
|
||||
if subplot_row == 2:
|
||||
xa.showticklabels = True
|
||||
break
|
||||
if not xa.showticklabels and xa.anchor != xbottom:
|
||||
@@ -516,34 +523,11 @@ class PlotlyTA(PltTA):
|
||||
for trace in fig.select_traces():
|
||||
xref, yref = trace.xaxis, trace.yaxis
|
||||
row, col = subplots[xref][yref][0]
|
||||
if trace.name == "Volume":
|
||||
try:
|
||||
list(fig.select_annotations(selector=dict(text="Volume")))[
|
||||
0
|
||||
].text = ""
|
||||
except IndexError:
|
||||
pass
|
||||
if not self.show_volume:
|
||||
continue
|
||||
fig.add_annotation(
|
||||
xref=f"x{row} domain",
|
||||
yref=f"y{row} domain",
|
||||
text="<b>Volume</b>",
|
||||
x=0,
|
||||
xanchor="right",
|
||||
xshift=-6,
|
||||
y=0.96,
|
||||
font_size=14,
|
||||
font_color="#e0b700",
|
||||
)
|
||||
fig.update_yaxes(nticks=5, row=row, col=col)
|
||||
|
||||
new_subplot.add_trace(trace, row=row, col=col)
|
||||
secondary_y = not row > 1 if self.show_volume else False
|
||||
new_subplot.add_trace(trace, row=row, col=col, secondary_y=secondary_y)
|
||||
|
||||
fig_json = fig.to_plotly_json()["layout"]
|
||||
for layout in fig_json:
|
||||
if layout in ["xaxis2", "yaxis2"] and not self.show_volume:
|
||||
continue
|
||||
if (
|
||||
isinstance(fig_json[layout], dict)
|
||||
and "domain" in fig_json[layout]
|
||||
@@ -556,4 +540,7 @@ class PlotlyTA(PltTA):
|
||||
fig.layout.update({layout: fig_json[layout]})
|
||||
new_subplot.layout.update({layout: fig.layout[layout]})
|
||||
|
||||
if self.show_volume:
|
||||
new_subplot.add_stock_volume(self.df_stock, self.close_column)
|
||||
|
||||
return new_subplot
|
||||
|
||||
169
openbb_terminal/core/plots/web/bar_menus.js
vendored
169
openbb_terminal/core/plots/web/bar_menus.js
vendored
@@ -1,98 +1,131 @@
|
||||
// Custom Menu functions for Plotly charts
|
||||
// import Plotly from 'plotly.js-dist'
|
||||
|
||||
function autoScaling(eventdata, graphs) {
|
||||
try {
|
||||
if (eventdata["xaxis.range[0]"] != undefined) {
|
||||
let log_scale = true
|
||||
? graphs.layout.yaxis != undefined && graphs.layout.yaxis.type == "log"
|
||||
: false;
|
||||
|
||||
let secondary_y = true
|
||||
? graphs.layout.yaxis2 != undefined &&
|
||||
graphs.layout.yaxis2.overlaying == "y"
|
||||
: false;
|
||||
|
||||
let x_min = eventdata["xaxis.range[0]"];
|
||||
let x_max = eventdata["xaxis.range[1]"];
|
||||
|
||||
const x_min = eventdata["xaxis.range[0]"];
|
||||
const x_max = eventdata["xaxis.range[1]"];
|
||||
let to_update = {};
|
||||
let y_min, y_max;
|
||||
let y_done = [];
|
||||
|
||||
for (let trace of graphs.data) {
|
||||
let yaxis = !trace.yaxis
|
||||
? "yaxis"
|
||||
: "yaxis" + trace.yaxis.replace("y", "");
|
||||
const YaxisData = graphs.data.filter((trace) => trace.yaxis != undefined);
|
||||
const yaxis_unique = [
|
||||
...new Set(
|
||||
YaxisData.map(
|
||||
(trace) =>
|
||||
trace.yaxis || trace.y != undefined || trace.type == "candlestick"
|
||||
)
|
||||
),
|
||||
];
|
||||
|
||||
if (trace.x != undefined) {
|
||||
if (y_done.indexOf(yaxis) == -1) {
|
||||
y_done.push(yaxis);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
const get_all_yaxis_traces = (yaxis) => {
|
||||
return graphs.data.filter(
|
||||
(trace) =>
|
||||
trace.yaxis == yaxis && (trace.y || trace.type == "candlestick")
|
||||
);
|
||||
};
|
||||
|
||||
let x = trace.x;
|
||||
let y_low, y_high;
|
||||
yaxis_unique.forEach((unique) => {
|
||||
let yaxis = "yaxis" + unique.replace("y", "");
|
||||
let y_candle = [];
|
||||
let y_values = [];
|
||||
let log_scale = graphs.layout[yaxis].type == "log";
|
||||
|
||||
if (trace.close != undefined) {
|
||||
y_high = trace.high;
|
||||
y_low = trace.low;
|
||||
}
|
||||
get_all_yaxis_traces(unique).forEach((trace2) => {
|
||||
let x = trace2.x;
|
||||
log_scale = graphs.layout[yaxis].type == "log";
|
||||
|
||||
let y = y_low ? y_low : trace.y;
|
||||
let y = trace2.y != undefined ? trace2.y : [];
|
||||
let y_low = trace2.type == "candlestick" ? trace2.low : [];
|
||||
let y_high = trace2.type == "candlestick" ? trace2.high : [];
|
||||
|
||||
if (log_scale) {
|
||||
y = y.map(Math.log10);
|
||||
if (y_high != undefined) {
|
||||
y_high = y_high.map(Math.log10);
|
||||
if (trace2.type == "candlestick") {
|
||||
y_low = trace2.low.map(Math.log10);
|
||||
y_high = trace2.high.map(Math.log10);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < x.length; i++) {
|
||||
if (x[i] >= x_min && x[i] <= x_max) {
|
||||
if (y_min == undefined || y_min > y[i]) {
|
||||
y_min = y[i];
|
||||
}
|
||||
|
||||
let y_high_value = y_high ? y_high[i] : y[i];
|
||||
if (y_max == undefined || y_max < y_high_value) {
|
||||
y_max = y_high_value;
|
||||
let yx_values = x.map((x, i) => {
|
||||
let out = null;
|
||||
if (x >= x_min && x <= x_max) {
|
||||
if (trace2.y != undefined) {
|
||||
console.log(trace2.name, trace2.type, trace2.yaxis);
|
||||
out = y[i];
|
||||
}
|
||||
if (trace2.type == "candlestick") {
|
||||
y_candle.push(y_low[i]);
|
||||
y_candle.push(y_high[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
});
|
||||
|
||||
y_values = y_values.concat(yx_values);
|
||||
});
|
||||
|
||||
y_values = y_values.filter((y2) => y2 != undefined && y2 != null);
|
||||
y_min = Math.min(...y_values);
|
||||
y_max = Math.max(...y_values);
|
||||
|
||||
if (y_candle.length > 0) {
|
||||
y_candle = y_candle.filter((y2) => y2 != undefined && y2 != null);
|
||||
y_min = Math.min(...y_candle);
|
||||
y_max = Math.max(...y_candle);
|
||||
}
|
||||
|
||||
let org_y_max = y_max;
|
||||
let is_volume =
|
||||
graphs.layout[yaxis].fixedrange != undefined &&
|
||||
graphs.layout[yaxis].fixedrange == true;
|
||||
|
||||
if (y_min != undefined && y_max != undefined) {
|
||||
if (yaxis != "yaxis" && log_scale) {
|
||||
y_min = Math.pow(10, y_min);
|
||||
y_max = Math.pow(10, y_max);
|
||||
}
|
||||
|
||||
let y_range = y_max - y_min;
|
||||
|
||||
if (yaxis == "yaxis" && graphs.layout.yaxis.range[0] != 0) {
|
||||
y_min -= y_range * 0.15;
|
||||
}
|
||||
y_max += y_range * 0.15;
|
||||
|
||||
if (yaxis == "yaxis" && secondary_y) {
|
||||
secondary_y = [y_min, y_max];
|
||||
let y_mult = 0.15;
|
||||
if (y_candle.length > 0) {
|
||||
y_mult = 0.3;
|
||||
}
|
||||
|
||||
if (yaxis == "yaxis2" && secondary_y) {
|
||||
let y2_max = secondary_y[1] - secondary_y[0] * 1.5;
|
||||
y_min -= y_range * y_mult;
|
||||
y_max += y_range * y_mult;
|
||||
|
||||
if (is_volume) {
|
||||
if (graphs.layout[yaxis].tickvals != undefined) {
|
||||
const range_x = 4;
|
||||
let volume_ticks = org_y_max;
|
||||
let round_digits = -3;
|
||||
let first_val = Math.round(volume_ticks * 0.2, round_digits);
|
||||
let x_zipped = [2, 5, 6, 7, 8, 9, 10];
|
||||
let y_zipped = [1, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
for (let i = 0; i < x_zipped.length; i++) {
|
||||
if (String(volume_ticks).length > x_zipped[i]) {
|
||||
round_digits = -y_zipped[i];
|
||||
first_val = Math.round(volume_ticks * 0.2, round_digits);
|
||||
}
|
||||
}
|
||||
let tickvals = [
|
||||
Math.floor(first_val),
|
||||
Math.floor(first_val * 2),
|
||||
Math.floor(first_val * 3),
|
||||
Math.floor(first_val * 4),
|
||||
];
|
||||
let volume_range = [0, Math.floor(volume_ticks * range_x)];
|
||||
|
||||
to_update[yaxis + ".tickvals"] = tickvals;
|
||||
to_update[yaxis + ".range"] = volume_range;
|
||||
to_update[yaxis + ".tickformat"] = ".2s";
|
||||
return;
|
||||
}
|
||||
y_min = 0;
|
||||
y_max = secondary_y[1] - secondary_y[0] * 1.5;
|
||||
|
||||
if ((secondary_y[1] - y2_max) / secondary_y[1] > 0.5) {
|
||||
y_max = secondary_y[0] * 1.2;
|
||||
}
|
||||
if ((secondary_y[0] - y2_max) / secondary_y[0] > 0.5) {
|
||||
y_max = secondary_y[1] * 1.2;
|
||||
}
|
||||
y_max = graphs.layout[yaxis].range[1];
|
||||
}
|
||||
|
||||
Plotly.relayout(globals.chartDiv, yaxis + ".range", [y_min, y_max]);
|
||||
to_update[yaxis + ".range"] = [y_min, y_max];
|
||||
}
|
||||
}
|
||||
});
|
||||
Plotly.update(graphs, {}, to_update);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Error in AutoScaling: ${e}`);
|
||||
@@ -161,7 +194,7 @@ function changeColor() {
|
||||
function downloadImage() {
|
||||
const loader = document.getElementById("loader");
|
||||
loader.classList.add("show");
|
||||
Plotly.toImage(globals.chartDiv, {
|
||||
Plotly.toImage(globals.CHART_DIV, {
|
||||
format: "png",
|
||||
height: 627,
|
||||
width: 1200,
|
||||
|
||||
32
openbb_terminal/core/plots/web/helpers.js
vendored
32
openbb_terminal/core/plots/web/helpers.js
vendored
@@ -27,7 +27,7 @@ const ICONS = {
|
||||
};
|
||||
|
||||
function add_annotation(data, yshift, popup_data, current_text = null) {
|
||||
let gd = globals.chartDiv;
|
||||
let gd = globals.CHART_DIV;
|
||||
let x = data.x;
|
||||
let y = data.y;
|
||||
let yref = data.yref;
|
||||
@@ -210,9 +210,8 @@ function openbbFilename(data, csv = false) {
|
||||
|
||||
let date = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
||||
let time = new Date().toISOString().slice(11, 19).replace(/:/g, "");
|
||||
let dateTime = date + "_" + time;
|
||||
|
||||
return filename + "_" + dateTime;
|
||||
return filename + "_" + date + "_" + time;
|
||||
}
|
||||
|
||||
function plot_text(data, popup_data, current_text = null) {
|
||||
@@ -223,13 +222,10 @@ function plot_text(data, popup_data, current_text = null) {
|
||||
// data is the data from the chart
|
||||
|
||||
console.log("plot_text");
|
||||
let gd = globals.chartDiv;
|
||||
let gd = globals.CHART_DIV;
|
||||
let annotations = undefined;
|
||||
let yrange = gd.layout.yaxis.range;
|
||||
|
||||
if (data.yref == "y2") {
|
||||
yrange = gd.layout.yaxis2.range;
|
||||
}
|
||||
let yaxis = data.yref.replace("y", "yaxis");
|
||||
let yrange = gd.layout[yaxis].range;
|
||||
let yshift = (yrange[1] - yrange[0]) * 0.2;
|
||||
|
||||
if (popup_data.yanchor == "below") {
|
||||
@@ -241,18 +237,16 @@ function plot_text(data, popup_data, current_text = null) {
|
||||
} else {
|
||||
annotations = add_annotation(data, yshift, popup_data, current_text);
|
||||
}
|
||||
let to_update = { annotations: annotations, dragmode: "pan" };
|
||||
to_update[yaxis + ".type"] = "linear";
|
||||
|
||||
Plotly.relayout(gd, { annotations: annotations });
|
||||
Plotly.react(gd, gd.data, gd.layout, gd.config);
|
||||
Plotly.relayout(gd, { "yaxis.type": "linear", dragmode: "pan" });
|
||||
Plotly.update(gd, {}, to_update);
|
||||
gd.removeAllListeners("plotly_click");
|
||||
|
||||
TEXT_DIV = document.getElementById("popup_text");
|
||||
TEXT_DIV.querySelectorAll("input").forEach(function (input) {
|
||||
if (!input.name != "addtext_color") {
|
||||
globals.TEXT_DIV = document.getElementById("popup_text");
|
||||
globals.TEXT_DIV.querySelectorAll("input").forEach(function (input) {
|
||||
if (!input.name in ["addtext_color", "addtext_border"]) {
|
||||
input.value = "";
|
||||
} else {
|
||||
input.value = "#ffffff";
|
||||
}
|
||||
});
|
||||
closePopup();
|
||||
@@ -262,7 +256,7 @@ function update_color() {
|
||||
// updates the color of the last added shape
|
||||
// this function is called when the color picker is used
|
||||
let color = globals.color_picker.querySelector("#picked_color").value;
|
||||
let gd = globals.chartDiv;
|
||||
let gd = globals.CHART_DIV;
|
||||
|
||||
// we change last added shape color
|
||||
let shapes = gd.layout.shapes;
|
||||
@@ -272,7 +266,7 @@ function update_color() {
|
||||
}
|
||||
let last_shape = shapes[shapes.length - 1];
|
||||
last_shape.line.color = color;
|
||||
Plotly.relayout(gd, "shapes", shapes);
|
||||
Plotly.update(gd, {}, { shapes: shapes });
|
||||
}
|
||||
|
||||
function button_pressed(title, active = false) {
|
||||
|
||||
171
openbb_terminal/core/plots/web/main.js
vendored
171
openbb_terminal/core/plots/web/main.js
vendored
@@ -1,36 +1,17 @@
|
||||
let globals = { dark_mode: false, modebarHidden: false };
|
||||
let globals = {
|
||||
dark_mode: false,
|
||||
modebarHidden: false,
|
||||
added_traces: [],
|
||||
csv_yaxis_id: null,
|
||||
cmd_src_idx: null,
|
||||
};
|
||||
|
||||
TITLE_DIV = undefined;
|
||||
TEXT_DIV = undefined;
|
||||
CSV_DIV = undefined;
|
||||
CHART_DIV = undefined;
|
||||
|
||||
let check_divs = setInterval(function () {
|
||||
// Wait for the popup divs to be loaded before assigning them to variables
|
||||
let div_ids = ["popup_title", "popup_text", "popup_csv", "openbb_chart"];
|
||||
let divs = div_ids.map(function (id) {
|
||||
return document.getElementById(id);
|
||||
});
|
||||
|
||||
if (
|
||||
divs.every(function (div) {
|
||||
return div != null;
|
||||
})
|
||||
) {
|
||||
TITLE_DIV = document.getElementById("popup_title");
|
||||
TEXT_DIV = document.getElementById("popup_text");
|
||||
CSV_DIV = document.getElementById("popup_csv");
|
||||
CHART_DIV = document.getElementById("openbb_chart");
|
||||
console.log("popup divs found");
|
||||
clearInterval(check_divs);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
|
||||
function OpenBBMain(plotly_figure, chartdiv) {
|
||||
function OpenBBMain(plotly_figure, chartdiv, csvdiv, textdiv, titlediv) {
|
||||
// Main function that plots the graphs and initializes the bar menus
|
||||
CHART_DIV = chartdiv;
|
||||
globals.chartDiv = CHART_DIV;
|
||||
globals.CHART_DIV = chartdiv;
|
||||
globals.TITLE_DIV = titlediv;
|
||||
globals.TEXT_DIV = textdiv;
|
||||
globals.CSV_DIV = csvdiv;
|
||||
console.log("main.js loaded");
|
||||
console.log("plotly_figure", plotly_figure);
|
||||
let graphs = plotly_figure;
|
||||
@@ -44,8 +25,8 @@ function OpenBBMain(plotly_figure, chartdiv) {
|
||||
toImageButtonOptions: {
|
||||
format: "svg",
|
||||
filename: openbbFilename(graphs),
|
||||
height: CHART_DIV.clientHeight,
|
||||
width: CHART_DIV.clientWidth,
|
||||
height: globals.CHART_DIV.clientHeight,
|
||||
width: globals.CHART_DIV.clientWidth,
|
||||
},
|
||||
modeBarButtonsToRemove: ["lasso2d", "select2d"],
|
||||
modeBarButtons: [
|
||||
@@ -119,15 +100,30 @@ function OpenBBMain(plotly_figure, chartdiv) {
|
||||
let active = true;
|
||||
if (button.style.border == "transparent") {
|
||||
active = false;
|
||||
// We add the listener to the plotly_relayout event
|
||||
// to autoscale the graphs
|
||||
CHART_DIV.on("plotly_relayout", function (eventdata) {
|
||||
autoScaling(eventdata, graphs);
|
||||
});
|
||||
|
||||
const autoscale = (func, delay) => {
|
||||
let timeout;
|
||||
return function () {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(context, args), delay);
|
||||
};
|
||||
};
|
||||
|
||||
globals.CHART_DIV.on(
|
||||
"plotly_relayout",
|
||||
autoscale(function (eventdata) {
|
||||
if (eventdata["xaxis.range[0]"] == undefined) {
|
||||
return;
|
||||
}
|
||||
autoScaling(eventdata, globals.CHART_DIV);
|
||||
}, 100)
|
||||
);
|
||||
} else {
|
||||
// If the button is active, we remove the listener so
|
||||
// If the button isn't active, we remove the listener so
|
||||
// the graphs don't autoscale anymore
|
||||
CHART_DIV.removeAllListeners("plotly_relayout");
|
||||
globals.CHART_DIV.removeAllListeners("plotly_relayout");
|
||||
}
|
||||
button_pressed(title, active);
|
||||
},
|
||||
@@ -172,7 +168,7 @@ function OpenBBMain(plotly_figure, chartdiv) {
|
||||
openPopup("popup_title");
|
||||
}
|
||||
if (e.ctrlKey && e.key.toLowerCase() == "s") {
|
||||
downloadData(CHART_DIV);
|
||||
downloadData(globals.CHART_DIV);
|
||||
}
|
||||
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() == "c") {
|
||||
openPopup("popup_csv");
|
||||
@@ -200,8 +196,13 @@ function OpenBBMain(plotly_figure, chartdiv) {
|
||||
};
|
||||
}
|
||||
|
||||
if (annotation.text != undefined) {
|
||||
if (annotation.text[0] == "/") {
|
||||
globals.cmd_src_idx = graphs.layout.annotations.indexOf(annotation);
|
||||
}
|
||||
}
|
||||
annotation.font.size = Math.min(
|
||||
CHART_DIV.clientWidth / 50,
|
||||
globals.CHART_DIV.clientWidth / 50,
|
||||
annotation.font.size
|
||||
);
|
||||
});
|
||||
@@ -218,7 +219,7 @@ function OpenBBMain(plotly_figure, chartdiv) {
|
||||
// Set the default dragmode to pan and set the font size to a reasonable value
|
||||
graphs.layout.dragmode = "pan";
|
||||
graphs.layout.font.size = Math.min(
|
||||
CHART_DIV.clientWidth / 50,
|
||||
globals.CHART_DIV.clientWidth / 50,
|
||||
graphs.layout.font.size
|
||||
);
|
||||
|
||||
@@ -231,7 +232,7 @@ function OpenBBMain(plotly_figure, chartdiv) {
|
||||
|
||||
// We set the plot config and plot the chart
|
||||
Plotly.setPlotConfig(CONFIG);
|
||||
Plotly.newPlot(CHART_DIV, graphs, { responsive: true });
|
||||
Plotly.newPlot(globals.CHART_DIV, graphs, { responsive: true });
|
||||
|
||||
// Create global variables to for use later
|
||||
let modebar = document.getElementsByClassName("modebar-container");
|
||||
@@ -251,41 +252,38 @@ function OpenBBMain(plotly_figure, chartdiv) {
|
||||
// window close interval if exporting plot to image
|
||||
let is_3dmesh = false;
|
||||
|
||||
// We add a listener to the chart div to listen for relayout events
|
||||
// we only care about the yaxis.type event
|
||||
CHART_DIV.on("plotly_relayout", function (eventdata) {
|
||||
if (CHART_DIV.layout.yaxis.type != undefined) {
|
||||
if (
|
||||
eventdata["yaxis.type"] == "log" ||
|
||||
(CHART_DIV.layout.yaxis.type == "log" && !globals.logYaxis)
|
||||
) {
|
||||
console.log("yaxis.type changed to log");
|
||||
globals.logYaxis = true;
|
||||
if (globals.CHART_DIV.layout.yaxis.type != undefined) {
|
||||
if (globals.CHART_DIV.layout.yaxis.type == "log" && !globals.logYaxis) {
|
||||
console.log("yaxis.type changed to log");
|
||||
globals.logYaxis = true;
|
||||
|
||||
// We update the yaxis exponent format to SI,
|
||||
// set the tickformat to '.0s' and the exponentbase to 100
|
||||
Plotly.relayout(CHART_DIV, {
|
||||
"yaxis.exponentformat": "SI",
|
||||
"yaxis.tickformat": ".0s",
|
||||
"yaxis.exponentbase": 100,
|
||||
});
|
||||
}
|
||||
if (eventdata["yaxis.type"] == "linear" && globals.logYaxis) {
|
||||
console.log("yaxis.type changed to linear");
|
||||
globals.logYaxis = false;
|
||||
// We update the yaxis exponent format to SI,
|
||||
// set the tickformat to '.0s' and the exponentbase to 100
|
||||
let layout_update = {
|
||||
"yaxis.exponentformat": "SI",
|
||||
"yaxis.tickformat": ".0s",
|
||||
"yaxis.exponentbase": 100,
|
||||
|
||||
// We update the yaxis exponent format to none,
|
||||
// set the tickformat to null and the exponentbase to 10
|
||||
Plotly.relayout(CHART_DIV, {
|
||||
"yaxis.exponentformat": "none",
|
||||
"yaxis.tickformat": null,
|
||||
"yaxis.exponentbase": 10,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
is_3dmesh = true;
|
||||
Plotly.update(globals.CHART_DIV, layout_update);
|
||||
}
|
||||
});
|
||||
if (globals.CHART_DIV.layout.yaxis.type == "linear" && globals.logYaxis) {
|
||||
console.log("yaxis.type changed to linear");
|
||||
globals.logYaxis = false;
|
||||
|
||||
// We update the yaxis exponent format to none,
|
||||
// set the tickformat to null and the exponentbase to 10
|
||||
let layout_update = {
|
||||
"yaxis.exponentformat": "none",
|
||||
"yaxis.tickformat": null,
|
||||
"yaxis.exponentbase": 10,
|
||||
|
||||
}
|
||||
Plotly.update(globals.CHART_DIV, layout_update);
|
||||
}
|
||||
} else {
|
||||
is_3dmesh = true;
|
||||
}
|
||||
|
||||
// send a relayout event to trigger the initial zoom/bars-resize
|
||||
// check if the xaxis.range is defined
|
||||
@@ -293,28 +291,28 @@ function OpenBBMain(plotly_figure, chartdiv) {
|
||||
graphs.layout.xaxis != undefined &&
|
||||
graphs.layout.xaxis.range != undefined
|
||||
) {
|
||||
Plotly.relayout(CHART_DIV, {
|
||||
Plotly.relayout(globals.CHART_DIV, {
|
||||
"xaxis.range[0]": graphs.layout.xaxis.range[0],
|
||||
"xaxis.range[1]": graphs.layout.xaxis.range[1],
|
||||
});
|
||||
}
|
||||
|
||||
// Just in case the CSV_DIV is undefined, we check for it every 100ms
|
||||
// Just in case the globals.CSV_DIV is undefined, we check for it every 100ms
|
||||
let check_csv = setInterval(function () {
|
||||
if (CSV_DIV != undefined) {
|
||||
console.log("CSV_DIV is defined");
|
||||
if (globals.CSV_DIV != undefined) {
|
||||
console.log("globals.CSV_DIV is defined");
|
||||
// We add the event listeners for csv file/type changes
|
||||
CSV_DIV.querySelector("#csv_file").addEventListener(
|
||||
globals.CSV_DIV.querySelector("#csv_file").addEventListener(
|
||||
"change",
|
||||
function () {
|
||||
checkFile(CSV_DIV);
|
||||
checkFile(globals.CSV_DIV);
|
||||
}
|
||||
);
|
||||
CSV_DIV.querySelector("#csv_trace_type").addEventListener(
|
||||
globals.CSV_DIV.querySelector("#csv_trace_type").addEventListener(
|
||||
"change",
|
||||
function () {
|
||||
console.log("type changed");
|
||||
checkFile(CSV_DIV, true);
|
||||
checkFile(globals.CSV_DIV, true);
|
||||
}
|
||||
);
|
||||
clearInterval(check_csv);
|
||||
@@ -334,16 +332,17 @@ function OpenBBMain(plotly_figure, chartdiv) {
|
||||
|
||||
if (["jpeg", "png", "svg"].includes(extension)) {
|
||||
// We run Plotly.downloadImage to save the chart as an image
|
||||
Plotly.downloadImage(CHART_DIV, {
|
||||
Plotly.downloadImage(globals.CHART_DIV, {
|
||||
format: extension,
|
||||
width: CHART_DIV.clientWidth,
|
||||
height: CHART_DIV.clientHeight,
|
||||
width: globals.CHART_DIV.clientWidth,
|
||||
height: globals.CHART_DIV.clientHeight,
|
||||
filename: window.export_image.split("/").pop(),
|
||||
});
|
||||
}
|
||||
setTimeout(function () {
|
||||
window.close();
|
||||
}, close_interval);
|
||||
// send to console all Plotly functions
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
218
openbb_terminal/core/plots/web/popups.js
vendored
218
openbb_terminal/core/plots/web/popups.js
vendored
@@ -1,20 +1,26 @@
|
||||
// import Plotly from "plotly.js-dist";
|
||||
|
||||
function get_popup(data = null, popup_id = null) {
|
||||
let popup = null;
|
||||
popup_id = popup_id.replace("popup_", "");
|
||||
|
||||
if (popup_id == "title") {
|
||||
data = globals.chartDiv.layout;
|
||||
data = globals.CHART_DIV.layout;
|
||||
let main_trace = globals.CHART_DIV.data[0];
|
||||
let use_yaxis = "yaxis" + main_trace.yaxis.replace("y", "");
|
||||
let use_xaxis = "xaxis" + main_trace.xaxis.replace("x", "");
|
||||
|
||||
let title = "title" in data && "text" in data.title ? data.title.text : "";
|
||||
let xaxis =
|
||||
"title" in data.xaxis && "text" in data.xaxis.title
|
||||
? data.xaxis.title.text
|
||||
"title" in data[use_xaxis] && "text" in data[use_xaxis].title
|
||||
? data[use_xaxis].title.text
|
||||
: "";
|
||||
let yaxis =
|
||||
"title" in data.yaxis && "text" in data.yaxis.title
|
||||
? data.yaxis.title.text
|
||||
"title" in data[use_yaxis] && "text" in data[use_yaxis].title
|
||||
? data[use_yaxis].title.text
|
||||
: "";
|
||||
|
||||
TITLE_DIV.innerHTML = `
|
||||
globals.TITLE_DIV.innerHTML = `
|
||||
<div style="display:flex; flex-direction: column; gap: 6px;">
|
||||
<div>
|
||||
<label for="title_text">Title:</label>
|
||||
@@ -37,9 +43,9 @@ function get_popup(data = null, popup_id = null) {
|
||||
`;
|
||||
|
||||
// when opening the popup, we make sure to focus on the title input
|
||||
TITLE_DIV.style.display = "inline-block";
|
||||
TITLE_DIV.querySelector("#title_text").focus();
|
||||
popup = TITLE_DIV;
|
||||
globals.TITLE_DIV.style.display = "inline-block";
|
||||
globals.TITLE_DIV.querySelector("#title_text").focus();
|
||||
popup = globals.TITLE_DIV;
|
||||
} else if (popup_id == "text") {
|
||||
let has_annotation = false;
|
||||
if (data == undefined) {
|
||||
@@ -59,13 +65,13 @@ function get_popup(data = null, popup_id = null) {
|
||||
data.text = data.text.replace(/<br>/g, "\n");
|
||||
|
||||
let yanchor =
|
||||
TEXT_DIV.querySelector("#addtext_above") == null
|
||||
globals.TEXT_DIV.querySelector("#addtext_above") == null
|
||||
? "above"
|
||||
: TEXT_DIV.querySelector("#addtext_above").checked
|
||||
: globals.TEXT_DIV.querySelector("#addtext_above").checked
|
||||
? "above"
|
||||
: "below";
|
||||
|
||||
TEXT_DIV.innerHTML = `
|
||||
globals.TEXT_DIV.innerHTML = `
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label for="popup_textarea"><b>Text:</b>
|
||||
<div id="popup_textarea_warning" class="popup_warning">Text is required</div></label><br>
|
||||
@@ -105,7 +111,7 @@ function get_popup(data = null, popup_id = null) {
|
||||
`;
|
||||
|
||||
if (has_annotation) {
|
||||
TEXT_DIV.innerHTML += `
|
||||
globals.TEXT_DIV.innerHTML += `
|
||||
<div style="float: right; margin-top: 20px;">
|
||||
<button class="_btn-tertiary" id="addtext_cancel" onclick="closePopup()">Cancel</button>
|
||||
<button class="_btn" id="addtext_delete" onclick="on_delete('text')">Delete</button>
|
||||
@@ -116,29 +122,29 @@ function get_popup(data = null, popup_id = null) {
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
TEXT_DIV.innerHTML += `
|
||||
globals.TEXT_DIV.innerHTML += `
|
||||
<div style="float: right; margin-top: 20px;">
|
||||
<button class="_btn-tertiary" id="addtext_cancel" onclick="closePopup()">Cancel</button>
|
||||
<button class="_btn-tertiary" id="addtext_cancel" onclick="closePopup()">Cancel</button>
|
||||
<button class="_btn" id="addtext_submit" onclick="on_submit('text')">Submit</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (TEXT_DIV.querySelector("#addtext_annotation") != null) {
|
||||
TEXT_DIV.querySelector("#addtext_annotation").remove();
|
||||
if (globals.TEXT_DIV.querySelector("#addtext_annotation") != null) {
|
||||
globals.TEXT_DIV.querySelector("#addtext_annotation").remove();
|
||||
}
|
||||
}
|
||||
|
||||
// when opening the popup, we make sure to focus on the textarea
|
||||
TEXT_DIV.style.display = "inline-block";
|
||||
TEXT_DIV.querySelector("#addtext_textarea").focus();
|
||||
popup = TEXT_DIV;
|
||||
globals.TEXT_DIV.style.display = "inline-block";
|
||||
globals.TEXT_DIV.querySelector("#addtext_textarea").focus();
|
||||
popup = globals.TEXT_DIV;
|
||||
} else if (popup_id == "csv") {
|
||||
CSV_DIV.style.display = "inline-block";
|
||||
popup = CSV_DIV;
|
||||
globals.CSV_DIV.style.display = "inline-block";
|
||||
popup = globals.CSV_DIV;
|
||||
console.log("csv");
|
||||
} else if (popup_id == "upload") {
|
||||
TEXT_DIV.style.display = "inline-block";
|
||||
TEXT_DIV.innerHTML = `
|
||||
globals.TEXT_DIV.style.display = "inline-block";
|
||||
globals.TEXT_DIV.innerHTML = `
|
||||
<div>
|
||||
<p>Media preview (you can share it on twitter):</p>
|
||||
<a style="margin-top: 10px;" href="${data.url}" target="_blank" rel="noreferrer noopener">${data.url}</a>
|
||||
@@ -152,11 +158,11 @@ function get_popup(data = null, popup_id = null) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
popup = TEXT_DIV;
|
||||
popup = globals.TEXT_DIV;
|
||||
console.log("upload");
|
||||
}
|
||||
|
||||
let popup_divs = [TITLE_DIV, TEXT_DIV, CSV_DIV];
|
||||
let popup_divs = [globals.TITLE_DIV, globals.TEXT_DIV, globals.CSV_DIV];
|
||||
popup_divs.forEach(function (div) {
|
||||
if (div.id != popup.id) {
|
||||
div.style.display = "none";
|
||||
@@ -173,23 +179,23 @@ function get_popup_data(popup_id = null) {
|
||||
|
||||
if (popup_id == "title") {
|
||||
data = {
|
||||
title: TITLE_DIV.querySelector("#title_text").value,
|
||||
xaxis: TITLE_DIV.querySelector("#title_xaxis").value,
|
||||
yaxis: TITLE_DIV.querySelector("#title_yaxis").value,
|
||||
title: globals.TITLE_DIV.querySelector("#title_text").value,
|
||||
xaxis: globals.TITLE_DIV.querySelector("#title_xaxis").value,
|
||||
yaxis: globals.TITLE_DIV.querySelector("#title_yaxis").value,
|
||||
};
|
||||
} else if (popup_id == "text") {
|
||||
data = {
|
||||
text: TEXT_DIV.querySelector("#addtext_textarea").value,
|
||||
color: TEXT_DIV.querySelector("#addtext_color").value,
|
||||
size: TEXT_DIV.querySelector("#addtext_size").value,
|
||||
yanchor: TEXT_DIV.querySelector("#addtext_above").checked
|
||||
text: globals.TEXT_DIV.querySelector("#addtext_textarea").value,
|
||||
color: globals.TEXT_DIV.querySelector("#addtext_color").value,
|
||||
size: globals.TEXT_DIV.querySelector("#addtext_size").value,
|
||||
yanchor: globals.TEXT_DIV.querySelector("#addtext_above").checked
|
||||
? "above"
|
||||
: "below",
|
||||
bordercolor: TEXT_DIV.querySelector("#addtext_border").value,
|
||||
bordercolor: globals.TEXT_DIV.querySelector("#addtext_border").value,
|
||||
};
|
||||
if (TEXT_DIV.querySelector("#addtext_annotation") != null) {
|
||||
if (globals.TEXT_DIV.querySelector("#addtext_annotation") != null) {
|
||||
data.annotation = JSON.parse(
|
||||
TEXT_DIV.querySelector("#addtext_annotation").value
|
||||
globals.TEXT_DIV.querySelector("#addtext_annotation").value
|
||||
);
|
||||
}
|
||||
|
||||
@@ -197,27 +203,27 @@ function get_popup_data(popup_id = null) {
|
||||
data.text = data.text.replace(/\n/g, "<br>");
|
||||
console.log(data);
|
||||
} else if (popup_id == "csv") {
|
||||
let trace_type = CSV_DIV.querySelector("#csv_trace_type").value;
|
||||
let trace_type = globals.CSV_DIV.querySelector("#csv_trace_type").value;
|
||||
if (trace_type == "candlestick") {
|
||||
data = {
|
||||
x: CSV_DIV.querySelector("#csv_x").value,
|
||||
open: CSV_DIV.querySelector("#csv_open").value,
|
||||
high: CSV_DIV.querySelector("#csv_high").value,
|
||||
low: CSV_DIV.querySelector("#csv_low").value,
|
||||
close: CSV_DIV.querySelector("#csv_close").value,
|
||||
increasing: CSV_DIV.querySelector("#csv_increasing").value,
|
||||
decreasing: CSV_DIV.querySelector("#csv_decreasing").value,
|
||||
x: globals.CSV_DIV.querySelector("#csv_x").value,
|
||||
open: globals.CSV_DIV.querySelector("#csv_open").value,
|
||||
high: globals.CSV_DIV.querySelector("#csv_high").value,
|
||||
low: globals.CSV_DIV.querySelector("#csv_low").value,
|
||||
close: globals.CSV_DIV.querySelector("#csv_close").value,
|
||||
increasing: globals.CSV_DIV.querySelector("#csv_increasing").value,
|
||||
decreasing: globals.CSV_DIV.querySelector("#csv_decreasing").value,
|
||||
};
|
||||
} else {
|
||||
data = {
|
||||
x: CSV_DIV.querySelector("#csv_x").value,
|
||||
y: CSV_DIV.querySelector("#csv_y").value,
|
||||
color: CSV_DIV.querySelector("#csv_color").value,
|
||||
x: globals.CSV_DIV.querySelector("#csv_x").value,
|
||||
y: globals.CSV_DIV.querySelector("#csv_y").value,
|
||||
color: globals.CSV_DIV.querySelector("#csv_color").value,
|
||||
};
|
||||
console.log(data);
|
||||
}
|
||||
data.name = CSV_DIV.querySelector("#csv_name").value;
|
||||
data.file = CSV_DIV.querySelector("#csv_file");
|
||||
data.name = globals.CSV_DIV.querySelector("#csv_name").value;
|
||||
data.file = globals.CSV_DIV.querySelector("#csv_file");
|
||||
data.trace_type = trace_type;
|
||||
}
|
||||
return data;
|
||||
@@ -227,8 +233,8 @@ function on_submit(popup_id, on_annotation = null) {
|
||||
// popup_id is either 'title', 'text', or 'csv' (for now)
|
||||
// and is used to determine which popup to get data from
|
||||
let popup_data = get_popup_data(popup_id);
|
||||
let gd = globals.chartDiv;
|
||||
Plotly.relayout(gd, "hovermode", "closest");
|
||||
let gd = globals.CHART_DIV;
|
||||
Plotly.update(gd, {}, { hovermode: "closest" });
|
||||
|
||||
if (popup_id == "text") {
|
||||
if (!popup_data.text == "") {
|
||||
@@ -256,7 +262,7 @@ function on_submit(popup_id, on_annotation = null) {
|
||||
};
|
||||
plot_text(data, popup_data, popup_data.annotation.text);
|
||||
}
|
||||
Plotly.relayout(gd, "hovermode", "x");
|
||||
Plotly.update(gd, {}, { hovermode: "x" });
|
||||
});
|
||||
|
||||
let clickHandler = function (eventData) {
|
||||
@@ -281,27 +287,30 @@ function on_submit(popup_id, on_annotation = null) {
|
||||
yref: yaxis,
|
||||
};
|
||||
plot_text(data, popup_data);
|
||||
Plotly.relayout(gd, "hovermode", "x");
|
||||
Plotly.update(gd, {}, { hovermode: "x" });
|
||||
};
|
||||
|
||||
Plotly.relayout(gd, { dragmode: "select" });
|
||||
Plotly.update(gd, {}, { dragmode: "select" });
|
||||
gd.on("plotly_click", clickHandler);
|
||||
} else {
|
||||
let textarea = TEXT_DIV.querySelector("#addtext_textarea");
|
||||
let textarea = globals.TEXT_DIV.querySelector("#addtext_textarea");
|
||||
document.getElementById("popup_textarea_warning").style.display = "block";
|
||||
textarea.style.border = "1px solid red";
|
||||
textarea.focus();
|
||||
|
||||
TEXT_DIV.style.display = "inline-block";
|
||||
globals.TEXT_DIV.style.display = "inline-block";
|
||||
return;
|
||||
}
|
||||
} else if (popup_id == "title") {
|
||||
Plotly.relayout(gd, {
|
||||
title: popup_data.title,
|
||||
"xaxis.title": popup_data.xaxis,
|
||||
"yaxis.title": popup_data.yaxis,
|
||||
"yaxis.type": "linear",
|
||||
});
|
||||
let main_trace = gd.data[0];
|
||||
let yaxis = "yaxis" + main_trace.yaxis.replace("y", "");
|
||||
let xaxis = "xaxis" + main_trace.xaxis.replace("x", "");
|
||||
let to_update = { title: popup_data.title };
|
||||
to_update[yaxis + ".title"] = popup_data.yaxis;
|
||||
to_update[xaxis + ".title"] = popup_data.xaxis;
|
||||
to_update[yaxis + ".type"] = "linear";
|
||||
|
||||
Plotly.update(gd, {}, to_update);
|
||||
} else if (popup_id == "csv") {
|
||||
console.log("got popup file");
|
||||
let popup_file = popup_data.file;
|
||||
@@ -327,8 +336,17 @@ function on_submit(popup_id, on_annotation = null) {
|
||||
}
|
||||
data.push(obj);
|
||||
}
|
||||
|
||||
// We get main plotly trace to get the x/y axis
|
||||
let main_trace = gd.data[0];
|
||||
gd.data.forEach((trace) => {
|
||||
if (globals.added_traces.indexOf(trace.name) == -1) {
|
||||
trace.showlegend = false;
|
||||
}
|
||||
});
|
||||
main_trace.showlegend = true;
|
||||
|
||||
let yaxis_id = main_trace.yaxis;
|
||||
|
||||
if (popup_data.trace_type == "candlestick") {
|
||||
trace = {
|
||||
@@ -351,34 +369,88 @@ function on_submit(popup_id, on_annotation = null) {
|
||||
name: popup_data.name,
|
||||
increasing: { line: { color: popup_data.increasing } },
|
||||
decreasing: { line: { color: popup_data.decreasing } },
|
||||
showlegend: true,
|
||||
};
|
||||
} else {
|
||||
let yaxis;
|
||||
|
||||
if (globals.csv_yaxis_id == null) {
|
||||
let yaxes = Object.keys(gd.layout)
|
||||
.filter((k) => k.startsWith("yaxis"))
|
||||
.map((k) => gd.layout[k].title);
|
||||
|
||||
yaxis = `y${yaxes.length + 1}`;
|
||||
yaxis_id = `yaxis${yaxes.length + 1}`;
|
||||
|
||||
globals.csv_yaxis_id = yaxis_id;
|
||||
globals.csv_yaxis = yaxis;
|
||||
gd.layout.margin.r -= 20;
|
||||
gd.layout.showlegend = true;
|
||||
}
|
||||
|
||||
trace = {
|
||||
x: data.map(function (row) {
|
||||
return row[popup_data.x];
|
||||
}),
|
||||
y: data.map(function (row) {
|
||||
return row[popup_data.y];
|
||||
return (
|
||||
(row[popup_data.y] - data[0][popup_data.y]) /
|
||||
data[0][popup_data.y]
|
||||
);
|
||||
}),
|
||||
type: popup_data.trace_type,
|
||||
mode: "lines",
|
||||
name: popup_data.name,
|
||||
line: { color: popup_data.color },
|
||||
xaxis: main_trace.xaxis,
|
||||
yaxis: main_trace.yaxis,
|
||||
showlegend: true,
|
||||
connectgaps: true,
|
||||
xaxis: main_trace.xaxis,
|
||||
yaxis: globals.csv_yaxis,
|
||||
};
|
||||
|
||||
if (globals.added_traces.length == 0) {
|
||||
gd.layout[yaxis_id] = {
|
||||
overlaying: "y",
|
||||
side: "left",
|
||||
title: {
|
||||
text: "% Change",
|
||||
font: {
|
||||
size: 14,
|
||||
},
|
||||
standoff: 0,
|
||||
},
|
||||
tickfont: { size: 14 },
|
||||
ticksuffix: " ",
|
||||
tickformat: ".0%",
|
||||
tickpadding: 5,
|
||||
showgrid: false,
|
||||
showline: false,
|
||||
showticklabels: true,
|
||||
zeroline: false,
|
||||
anchor: "x",
|
||||
type: "linear",
|
||||
autorange: true,
|
||||
};
|
||||
if (globals.cmd_src_idx != null) {
|
||||
gd.layout.annotations[globals.cmd_src_idx].xshift -= 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(trace);
|
||||
|
||||
globals.added_traces.push(trace.name);
|
||||
|
||||
Plotly.addTraces(gd, trace);
|
||||
Plotly.relayout(gd, { "yaxis.type": "linear" });
|
||||
|
||||
gd.layout[globals.csv_yaxis_id].type = "linear";
|
||||
Plotly.react(gd, gd.data, gd.layout);
|
||||
|
||||
// We empty the fields and innerHTML after the plot is made
|
||||
CSV_DIV.querySelector("#csv_colors").innerHTML = "";
|
||||
CSV_DIV.querySelector("#csv_columns").innerHTML = "";
|
||||
globals.CSV_DIV.querySelector("#csv_colors").innerHTML = "";
|
||||
globals.CSV_DIV.querySelector("#csv_columns").innerHTML = "";
|
||||
|
||||
CSV_DIV.querySelectorAll("input").forEach(function (input) {
|
||||
globals.CSV_DIV.querySelectorAll("input").forEach(function (input) {
|
||||
input.value = "";
|
||||
});
|
||||
};
|
||||
@@ -394,19 +466,19 @@ function on_cancel() {
|
||||
}
|
||||
|
||||
function on_delete(popup_id) {
|
||||
let gd = globals.chartDiv;
|
||||
let gd = globals.CHART_DIV;
|
||||
closePopup();
|
||||
|
||||
if (popup_id == "text") {
|
||||
let annotation = JSON.parse(
|
||||
TEXT_DIV.querySelector("#addtext_annotation").value
|
||||
globals.TEXT_DIV.querySelector("#addtext_annotation").value
|
||||
);
|
||||
gd.layout.annotations.splice(gd.layout.annotations.indexOf(annotation), 1);
|
||||
gd.layout.dragmode = "pan";
|
||||
|
||||
Plotly.react(gd, gd.data, gd.layout);
|
||||
Plotly.relayout(gd, "dragmode", "pan");
|
||||
|
||||
TEXT_DIV.style.display = "none";
|
||||
globals.TEXT_DIV.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -899,11 +899,10 @@ def plot_candles( # pylint: disable=too-many-arguments
|
||||
return data
|
||||
|
||||
fig = OpenBBFigure.create_subplots(
|
||||
rows=1 if not volume else 2,
|
||||
rows=1,
|
||||
cols=1,
|
||||
shared_xaxes=True,
|
||||
vertical_spacing=0,
|
||||
row_width=[1] if not volume else [0.2, 0.7],
|
||||
specs=[[{"secondary_y": True}]],
|
||||
).set_title(f"\n{symbol if title == '' else title}")
|
||||
|
||||
fig.add_candlestick(
|
||||
@@ -912,15 +911,17 @@ def plot_candles( # pylint: disable=too-many-arguments
|
||||
low=data.Low,
|
||||
close=data.Close,
|
||||
x=data.index,
|
||||
name=f"OHLC {symbol}",
|
||||
name=f"{symbol} OHLC",
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=volume,
|
||||
)
|
||||
if volume:
|
||||
fig.add_stock_volume(data)
|
||||
fig.update_yaxes(title="Volume", row=2, col=1)
|
||||
|
||||
fig.update_layout(showlegend=False, yaxis=dict(title="Price", type=yscale))
|
||||
fig.set_yaxis_title("Price", row=1, col=1, secondary_y=volume, type=yscale)
|
||||
fig.update_layout(showlegend=False)
|
||||
|
||||
return fig.show(external=external_axes)
|
||||
|
||||
|
||||
|
||||
@@ -14,10 +14,13 @@ from pathlib import Path
|
||||
from subprocess import PIPE, STDOUT
|
||||
from typing import List, Optional
|
||||
|
||||
import dotenv
|
||||
import numpy as np
|
||||
import psutil
|
||||
|
||||
import openbb_terminal.config_terminal as cfg
|
||||
from openbb_terminal.base_helpers import load_env_vars, strtobool
|
||||
from openbb_terminal.core.config.paths import SETTINGS_ENV_FILE
|
||||
from openbb_terminal.core.plots.backend import plots_backend
|
||||
from openbb_terminal.core.session.current_user import get_current_user
|
||||
from openbb_terminal.custom_prompt_toolkit import NestedCompleter
|
||||
@@ -46,13 +49,15 @@ class DashboardsController(BaseController):
|
||||
"futures",
|
||||
"forecast",
|
||||
"forecasting",
|
||||
"indicators",
|
||||
]
|
||||
PATH = "/dashboards/"
|
||||
|
||||
def __init__(self, queue: Optional[List[str]] = None):
|
||||
"""Constructor"""
|
||||
super().__init__(queue)
|
||||
self.jupyter_token = None
|
||||
self.jupyter_token: Optional[str] = None
|
||||
self.streamlit_url: Optional[str] = None
|
||||
self.processes: List[psutil.Process] = []
|
||||
self.parent_path = (
|
||||
Path(sys.executable).parent if hasattr(sys, "frozen") else Path(os.getcwd())
|
||||
@@ -80,6 +85,7 @@ class DashboardsController(BaseController):
|
||||
mt.add_cmd("forecast")
|
||||
mt.add_raw("\nStreamlit Apps:\n")
|
||||
mt.add_cmd("forecasting")
|
||||
mt.add_cmd("indicators")
|
||||
console.print(text=mt.menu_text, menu="Dashboards")
|
||||
|
||||
@log_start_end(log=logger)
|
||||
@@ -125,7 +131,12 @@ class DashboardsController(BaseController):
|
||||
@log_start_end(log=logger)
|
||||
def call_forecasting(self, other_args: List[str]):
|
||||
"""Process forecasting command"""
|
||||
self.create_call_streamlit(other_args, "forecast")
|
||||
self.create_call_streamlit(other_args, "Forecasting")
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def call_indicators(self, other_args: List[str]):
|
||||
"""Process indicators command"""
|
||||
self.create_call_streamlit(other_args, "Indicators")
|
||||
|
||||
def create_call_voila(
|
||||
self, other_args: List[str], name: str, filename: Optional[str] = None
|
||||
@@ -234,7 +245,8 @@ class DashboardsController(BaseController):
|
||||
"""Kills all processes started by this class."""
|
||||
for process in [p for p in self.processes if p.is_running()]:
|
||||
for child in process.children(recursive=True):
|
||||
child.kill()
|
||||
if child.is_running():
|
||||
child.kill()
|
||||
|
||||
process.kill()
|
||||
|
||||
@@ -299,40 +311,106 @@ class DashboardsController(BaseController):
|
||||
if ns_parser:
|
||||
cmd = "streamlit run"
|
||||
|
||||
filepath = Path(__file__).parent / "stream" / f"{filename}.py"
|
||||
filepath = Path(__file__).parent / "stream" / "Forecasting.py"
|
||||
file = filepath.relative_to(self.parent_path).as_posix()
|
||||
|
||||
if not ns_parser.input:
|
||||
console.print(
|
||||
f"Warning: opens a port on your computer to run a {cmd} server."
|
||||
)
|
||||
response = input("Would you like us to run the server for you [yn]? ")
|
||||
streamlit_warning = is_streamlit_activated()
|
||||
if not streamlit_warning:
|
||||
return
|
||||
|
||||
if ns_parser.input or response.lower() == "y":
|
||||
response = ""
|
||||
if not ns_parser.input and not self.streamlit_url:
|
||||
response = console.input(
|
||||
f"\nWarning: opens a port on your computer to run a {cmd} server.\n"
|
||||
"[green]Would you like us to run the server for you Y/n? [/]"
|
||||
)
|
||||
|
||||
if not self.streamlit_url and ns_parser.input or response.lower() == "y":
|
||||
port = self.get_free_port()
|
||||
self.streamlit_url = f"http://localhost:{port}"
|
||||
os.environ["PYTHONPATH"] = str(self.parent_path)
|
||||
self.processes.append(
|
||||
psutil.Popen(
|
||||
f"{cmd} --server.port {port} {file}",
|
||||
f"{cmd} --server.headless true --server.port {port} {file}",
|
||||
stdout=PIPE,
|
||||
stderr=STDOUT,
|
||||
stdin=PIPE,
|
||||
shell=True, # nosec
|
||||
env=os.environ,
|
||||
cwd=os.getcwd(),
|
||||
)
|
||||
)
|
||||
url = f"http://localhost:{port}"
|
||||
url = f"http://localhost:{port}/{name}"
|
||||
plots_backend().send_url(url=url, title=f"{filename.title()} Dashboard")
|
||||
|
||||
atexit.register(self.kill_processes)
|
||||
|
||||
thread = threading.Thread(
|
||||
target=non_blocking_streamlit,
|
||||
args=(self.processes[-1],),
|
||||
daemon=True,
|
||||
)
|
||||
thread.start()
|
||||
|
||||
if not self.processes[-1].is_running():
|
||||
self.processes.remove(self.processes[-1])
|
||||
self.streamlit_url = ""
|
||||
console.print(
|
||||
"[red]Error: streamlit server failed to start.[/]\n"
|
||||
"It might need to be activated or updated. Try running:\n"
|
||||
"[green] streamlit activate[/] or [green]pip install streamlit --upgrade[/]"
|
||||
)
|
||||
return
|
||||
elif self.streamlit_url:
|
||||
url = f"{self.streamlit_url}/{name}"
|
||||
plots_backend().send_url(url=url, title=f"{filename.title()} Dashboard")
|
||||
else:
|
||||
console.print(f"Type: {cmd} '{file}'\ninto a terminal to run.")
|
||||
console.print(f"\n\nType: {cmd} '{file}'\ninto a terminal to run.")
|
||||
|
||||
|
||||
def is_streamlit_activated() -> bool:
|
||||
def _declined():
|
||||
console.print(
|
||||
"\n[green]You will need to activate streamlit before running this command.[/]\n"
|
||||
"[yellow]Type: streamlit activate into a terminal to activate it.[/]"
|
||||
)
|
||||
return False
|
||||
|
||||
def _set_key():
|
||||
dotenv.set_key(
|
||||
SETTINGS_ENV_FILE,
|
||||
"OPENBB_DISABLE_STREAMLIT_WARNING",
|
||||
"1",
|
||||
)
|
||||
os.environ["OPENBB_DISABLE_STREAMLIT_WARNING"] = "1"
|
||||
|
||||
if load_env_vars("OPENBB_DISABLE_STREAMLIT_WARNING", strtobool, False):
|
||||
return True
|
||||
|
||||
run_activate = console.input(
|
||||
"\n[yellow]If you have not ran streamlit before, you will need to "
|
||||
"activate it first.\n[/]"
|
||||
"[green]If you have already activated streamlit, you can press enter to continue.\n"
|
||||
"Otherwise, would like us to activate streamlit for you. Y/n?[/]"
|
||||
).lower()
|
||||
if run_activate not in ["y", ""]:
|
||||
return _declined()
|
||||
|
||||
if run_activate == "":
|
||||
_set_key()
|
||||
return True
|
||||
|
||||
try:
|
||||
console.print("\n[green]Activating streamlit. This may take a few seconds.[/]")
|
||||
activate = os.system("streamlit activate")
|
||||
if activate == 0:
|
||||
_set_key()
|
||||
return True
|
||||
return _declined()
|
||||
|
||||
except Exception as err:
|
||||
console.print(f"Error: {err}")
|
||||
|
||||
return _declined()
|
||||
|
||||
|
||||
def non_blocking_streamlit(process: psutil.Popen) -> None:
|
||||
|
||||
@@ -11,7 +11,13 @@ from openbb_terminal.forecast import helpers
|
||||
from openbb_terminal.rich_config import console
|
||||
from openbb_terminal.sdk import openbb
|
||||
|
||||
st.set_page_config(layout="wide")
|
||||
st.set_page_config(
|
||||
layout="wide",
|
||||
page_title="Forecasting",
|
||||
page_icon="📈",
|
||||
initial_sidebar_state="collapsed",
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=E1101
|
||||
model_opts = {
|
||||
468
openbb_terminal/dashboards/stream/pages/Indicators.py
Normal file
468
openbb_terminal/dashboards/stream/pages/Indicators.py
Normal file
@@ -0,0 +1,468 @@
|
||||
import asyncio
|
||||
import re
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any, List, Optional, Union
|
||||
from unittest.mock import patch
|
||||
|
||||
import pandas as pd
|
||||
import streamlit as st
|
||||
from streamlit.delta_generator import DeltaGenerator
|
||||
|
||||
from openbb_terminal.common.technical_analysis import ta_helpers
|
||||
from openbb_terminal.core.plots.plotly_helper import OpenBBFigure
|
||||
from openbb_terminal.core.plots.plotly_ta.ta_class import PlotlyTA
|
||||
from openbb_terminal.rich_config import console
|
||||
from openbb_terminal.sdk import openbb
|
||||
|
||||
pd.options.plotting.backend = "plotly"
|
||||
st.set_page_config(
|
||||
layout="wide",
|
||||
page_title="Indicators",
|
||||
initial_sidebar_state="expanded",
|
||||
)
|
||||
|
||||
logger = st.empty()
|
||||
page_title = st.empty()
|
||||
plotly_chart = st.empty()
|
||||
|
||||
|
||||
TA_CLASS = PlotlyTA()
|
||||
indicators_opts = sorted(
|
||||
[c.name.replace("plot_", "") for c in TA_CLASS if c.name != "plot_ma"]
|
||||
+ TA_CLASS.ma_mode
|
||||
)
|
||||
|
||||
interval_opts = ["5m", "15m", "30m", "1d"]
|
||||
source_opts = [
|
||||
"AlphaVantage",
|
||||
"YahooFinance",
|
||||
"EODHD",
|
||||
"Polygon",
|
||||
"Intrinio",
|
||||
"DataBento",
|
||||
]
|
||||
MAIN_LOOP: asyncio.AbstractEventLoop = None # type: ignore
|
||||
|
||||
|
||||
def load_state(name: str, default: Any):
|
||||
if name not in st.session_state:
|
||||
st.session_state[name] = default
|
||||
|
||||
|
||||
def special_st(text: str):
|
||||
rich_color = re.search(r"\[([a-z ]+)\]", text)
|
||||
text = re.sub(r"(\[\/{0,1}[a-z ]+\])|(\[\/\])", "", text)
|
||||
|
||||
async def _special_st():
|
||||
with logger.container():
|
||||
if rich_color:
|
||||
color = rich_color.group(1)
|
||||
if "green" in color:
|
||||
st.success(text, icon="🎉")
|
||||
elif "red" in color:
|
||||
st.error(text, icon="🔥")
|
||||
await asyncio.sleep(5)
|
||||
elif "yellow" in color:
|
||||
st.warning(text, icon="⚠️")
|
||||
else:
|
||||
st.write(text)
|
||||
await asyncio.sleep(4)
|
||||
|
||||
MAIN_LOOP.create_task(_special_st())
|
||||
|
||||
|
||||
async def plot_indicators(
|
||||
data: pd.DataFrame, tickers: List[str], indicators: List[str]
|
||||
) -> OpenBBFigure:
|
||||
main_ticker = tickers[0]
|
||||
tickers = tickers[1:]
|
||||
data = data.copy()
|
||||
|
||||
indicators_dict: dict = {}
|
||||
for indicator in indicators:
|
||||
indicators_dict[indicator] = st.session_state["indicators_args"].get(
|
||||
indicator, {}
|
||||
)
|
||||
|
||||
title = (
|
||||
f"{main_ticker} Technical Analysis"
|
||||
if not tickers
|
||||
else f"{main_ticker} Technical Analysis with {', '.join(tickers)}"
|
||||
)
|
||||
|
||||
fig = TA_CLASS.plot(
|
||||
data,
|
||||
indicators_dict,
|
||||
main_ticker,
|
||||
candles=not tickers,
|
||||
volume=not tickers,
|
||||
)
|
||||
|
||||
fig.update_traces(showlegend=False)
|
||||
rows = fig.subplots_kwargs["rows"]
|
||||
|
||||
for ticker in tickers:
|
||||
ticker = ticker
|
||||
df_ticker = st.session_state["indicators_dfs"][ticker]
|
||||
df_ticker["% Change"] = df_ticker["Close"].apply(
|
||||
lambda x: (x - df_ticker["Close"].iloc[0]) / df_ticker["Close"].iloc[0]
|
||||
)
|
||||
|
||||
fig.add_scatter(
|
||||
x=df_ticker.index,
|
||||
y=df_ticker["% Change"],
|
||||
name=f"{ticker} % Change",
|
||||
customdata=df_ticker["Close"],
|
||||
secondary_y=True,
|
||||
hovertemplate="Close: %{customdata:.2f}<br>%{y:.2%}",
|
||||
yaxis=f"y{rows}",
|
||||
showlegend=True,
|
||||
row=1,
|
||||
col=1,
|
||||
)
|
||||
|
||||
if tickers:
|
||||
fig.update_traces(
|
||||
selector=dict(type="scatter", name=f"{main_ticker} Close"), showlegend=True
|
||||
)
|
||||
fig.update_yaxes(
|
||||
selector=dict(title_text="Price ($)"),
|
||||
title_text=f"{main_ticker} Price ($)",
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.update_yaxes(
|
||||
secondary_y=True,
|
||||
showgrid=False,
|
||||
showticklabels=True,
|
||||
row=1,
|
||||
col=1,
|
||||
showline=False,
|
||||
zeroline=False,
|
||||
title_text="% Change",
|
||||
ticksuffix=f"{'':>20}",
|
||||
side="left",
|
||||
tickformat=".2%",
|
||||
overlaying="y",
|
||||
)
|
||||
|
||||
fig.hide_holidays()
|
||||
|
||||
y_min, y_max = data["Low"].min().min(), data["High"].max().max()
|
||||
y_range = y_max - y_min
|
||||
y_min -= y_range * 0.4
|
||||
y_max += y_range * 0.4
|
||||
|
||||
yaxis = "yaxis" if tickers else "yaxis2"
|
||||
fig.update_layout(
|
||||
{yaxis: dict(range=[y_min, y_max], autorange=False)},
|
||||
title=dict(x=0.5, xanchor="center", yanchor="top", y=0.99, text=title),
|
||||
showlegend=True,
|
||||
margin=dict(t=40),
|
||||
height=650,
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
|
||||
class Handler:
|
||||
def __init__(self, loop: asyncio.AbstractEventLoop):
|
||||
default_args = {
|
||||
"last_tickers": "",
|
||||
"last_interval": "1d",
|
||||
"last_source": "YahooFinance",
|
||||
"interval": "1d",
|
||||
"args": {indicator: {} for indicator in indicators_opts},
|
||||
"dfs": {},
|
||||
}
|
||||
for key, value in default_args.items():
|
||||
load_state(f"indicators_{key}", value)
|
||||
|
||||
default_opts = {
|
||||
"start_date": date.today() - timedelta(days=365),
|
||||
"end_date": date.today(),
|
||||
"indicators_tickers": st.session_state["indicators_last_tickers"],
|
||||
"indicators_interval": st.session_state["indicators_last_interval"],
|
||||
"indicators": [],
|
||||
"source": st.session_state["indicators_last_source"],
|
||||
}
|
||||
load_state("widget_options", default_opts)
|
||||
self.loop = loop
|
||||
|
||||
global MAIN_LOOP
|
||||
MAIN_LOOP = loop
|
||||
|
||||
# pylint: disable=R0913
|
||||
async def handle_changes(
|
||||
self,
|
||||
start: date,
|
||||
end: date,
|
||||
tickers: str,
|
||||
indicators: list,
|
||||
):
|
||||
tickers_l = tickers.split(",")
|
||||
main_ticker = tickers_l[0].upper().strip()
|
||||
if (
|
||||
not tickers_l
|
||||
or st.session_state["indicators_dfs"].get(main_ticker, pd.DataFrame()).empty
|
||||
):
|
||||
with logger.empty():
|
||||
st.error("Please select at least one valid ticker", icon="🔥")
|
||||
return None
|
||||
|
||||
start_n = datetime(start.year, start.month, start.day).date()
|
||||
end_n = datetime(end.year, end.month, end.day).date()
|
||||
|
||||
for ticker in tickers_l:
|
||||
ticker = ticker.upper().strip()
|
||||
st.session_state["indicators_dfs"][ticker] = st.session_state[
|
||||
"indicators_dfs"
|
||||
][ticker].loc[
|
||||
(st.session_state["indicators_dfs"][ticker].index.date >= start_n)
|
||||
& (st.session_state["indicators_dfs"][ticker].index.date <= end_n)
|
||||
]
|
||||
|
||||
result: pd.DataFrame = st.session_state["indicators_dfs"][main_ticker]
|
||||
with st.spinner("Calculating indicators..."):
|
||||
with patch.object(console, "print", special_st):
|
||||
if result.empty:
|
||||
with logger.container():
|
||||
logger.error("There was an error with the data", icon="🔥")
|
||||
await asyncio.sleep(2)
|
||||
return None
|
||||
|
||||
data = pd.DataFrame(result)
|
||||
data.index.name = "date"
|
||||
|
||||
if ta_helpers.check_columns(data) is None:
|
||||
return None
|
||||
|
||||
fig = await plot_indicators(data, tickers_l, indicators)
|
||||
|
||||
with plotly_chart.container():
|
||||
last_day = data.loc[data.index.date == data.index.date[-1]]
|
||||
|
||||
if not last_day.empty:
|
||||
last_day.index = pd.to_datetime(last_day.index).date
|
||||
weeks52 = data.loc[
|
||||
data.index.date >= data.index.date[-1] - timedelta(weeks=52)
|
||||
]
|
||||
stats_df = pd.DataFrame(
|
||||
{
|
||||
"Open": last_day["Open"].head(1).values[0],
|
||||
"High": last_day["High"].max(),
|
||||
"Low": last_day["Low"].min(),
|
||||
"Close": last_day["Close"].tail(1).values[0],
|
||||
"Volume": last_day["Volume"].sum(),
|
||||
"52 week high": weeks52["High"].values.max(),
|
||||
"52 week low": weeks52["Low"].values.min(),
|
||||
},
|
||||
index=[
|
||||
last_day.tail(1).index.values[0].strftime("%Y-%m-%d")
|
||||
],
|
||||
)
|
||||
st.table(
|
||||
stats_df.style.format(
|
||||
{"Volume": "{:,.0f}"}, precision=2, thousands=","
|
||||
)
|
||||
)
|
||||
|
||||
dt_now = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"{dt_now}_{main_ticker}_technical_analysis"
|
||||
fig.show(external=True)
|
||||
|
||||
st.plotly_chart(
|
||||
fig,
|
||||
use_container_width=True,
|
||||
config=dict(
|
||||
scrollZoom=True,
|
||||
displaylogo=False,
|
||||
toImageButtonOptions=dict(
|
||||
format="png",
|
||||
filename=filename,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
async def load_ticker_data(
|
||||
self, ticker: str, interval: str, start: date, end: date, source: str
|
||||
):
|
||||
kwargs = {}
|
||||
if interval == "1d":
|
||||
kwargs.update(dict(start_date=start, end_date=end))
|
||||
else:
|
||||
end + timedelta(days=1)
|
||||
kwargs.update(dict(interval=int(interval.replace("m", "")))) # type: ignore
|
||||
|
||||
with patch.object(console, "print", special_st):
|
||||
df = openbb.stocks.load(ticker, **kwargs, source=source)
|
||||
|
||||
if df.empty:
|
||||
with logger.container():
|
||||
st.error(
|
||||
f"Could not load data for {ticker}. Is it a valid ticker?", icon="❗"
|
||||
)
|
||||
await asyncio.sleep(2)
|
||||
return pd.DataFrame()
|
||||
|
||||
df = df.dropna()
|
||||
return df
|
||||
|
||||
def on_ticker_change(self):
|
||||
self.loop.run_until_complete(self.async_on_ticker_change())
|
||||
|
||||
async def async_on_ticker_change(self):
|
||||
"""Load ticker data when ticker changes"""
|
||||
new_data = False
|
||||
tickers = list(set(st.session_state.indicators_ticker.split(","))) # type: ignore
|
||||
tickers = [ticker.strip().upper() for ticker in tickers if ticker.strip()] # type: ignore
|
||||
|
||||
try:
|
||||
interval = st.session_state.indicators_interval
|
||||
source = st.session_state.indicators_source
|
||||
start = st.session_state.start_date
|
||||
end = st.session_state.end_date
|
||||
except AttributeError:
|
||||
interval = "1d"
|
||||
source = "YahooFinance"
|
||||
start = date.today() - timedelta(days=365)
|
||||
end = date.today()
|
||||
|
||||
check_last = [
|
||||
("indicators_last_tickers", ",".join(tickers)),
|
||||
("indicators_last_interval", interval),
|
||||
("indicators_last_source", source),
|
||||
]
|
||||
new_params = any(
|
||||
[st.session_state.get(key, None) != value for key, value in check_last]
|
||||
)
|
||||
if new_params:
|
||||
st.session_state["indicators_dfs"] = {}
|
||||
for key, value in check_last:
|
||||
st.session_state[key] = value
|
||||
new_data = True
|
||||
|
||||
for ticker in tickers:
|
||||
if ticker not in st.session_state["indicators_dfs"]:
|
||||
st.session_state["indicators_dfs"][
|
||||
ticker
|
||||
] = await self.load_ticker_data(ticker, interval, start, end, source)
|
||||
if st.session_state["indicators_dfs"][ticker].empty:
|
||||
indicators_dfs = st.session_state["indicators_dfs"]
|
||||
del indicators_dfs[ticker]
|
||||
st.session_state["indicators_dfs"] = indicators_dfs
|
||||
tickers.remove(ticker)
|
||||
else:
|
||||
with logger.container():
|
||||
st.success(f"Loaded data for {ticker}", icon="📈")
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
if new_data:
|
||||
st.session_state["indicators_ticker"] = ",".join(tickers)
|
||||
|
||||
def handle_indicators(self, options_col: DeltaGenerator):
|
||||
"""Handle the indicators selection"""
|
||||
indicators = [
|
||||
indicator
|
||||
for indicator in list(
|
||||
set(st.session_state.indicators).intersection(set(TA_CLASS.ma_mode))
|
||||
)
|
||||
if not st.session_state["indicators_args"][indicator].get("length", [])
|
||||
]
|
||||
|
||||
for indicator in st.session_state["indicators_args"]:
|
||||
if indicator not in indicators:
|
||||
st.session_state["indicators_args"][indicator] = {}
|
||||
|
||||
if not indicators:
|
||||
return None
|
||||
with options_col.container().form("indicators_form", clear_on_submit=True):
|
||||
st.write("Select window lengths:")
|
||||
for indicator in indicators:
|
||||
st.multiselect(
|
||||
f"{indicator.upper()} Lengths",
|
||||
options=[7, 10, 20, 50, 100, 200],
|
||||
key=f"{indicator}_length",
|
||||
)
|
||||
st.form_submit_button(
|
||||
"Submit",
|
||||
on_click=self.on_length_submit,
|
||||
args=(indicators,),
|
||||
)
|
||||
|
||||
def on_length_submit(self, indicators: list):
|
||||
"""Handle the length submit"""
|
||||
for indicator in indicators:
|
||||
st.session_state["indicators_args"][indicator] = {
|
||||
"length": st.session_state[f"{indicator}_length"]
|
||||
}
|
||||
|
||||
async def run(self):
|
||||
"""Run the app"""
|
||||
|
||||
page_title.markdown(
|
||||
"<h2 style='text-align: center;'>OpenBB Technical Analysis</h2>",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
ticker = st.sidebar.text_input(
|
||||
"Ticker (multiple possible, separated by comma)",
|
||||
"",
|
||||
key="indicators_ticker",
|
||||
on_change=self.on_ticker_change,
|
||||
)
|
||||
data_opts = st.sidebar.expander("Data options")
|
||||
length_opts = st.sidebar.container()
|
||||
indicators_sideopts = st.sidebar.container()
|
||||
|
||||
with data_opts.container():
|
||||
data_opts.selectbox(
|
||||
"Source",
|
||||
index=1,
|
||||
key="indicators_source",
|
||||
options=source_opts,
|
||||
on_change=self.on_ticker_change,
|
||||
)
|
||||
start_date = data_opts.date_input(
|
||||
"Start date", date.today() - timedelta(days=365), key="start_date"
|
||||
)
|
||||
end_date = data_opts.date_input("End date", date.today(), key="end_date")
|
||||
|
||||
data_opts.selectbox(
|
||||
"Interval",
|
||||
index=3,
|
||||
key="indicators_interval",
|
||||
options=interval_opts,
|
||||
on_change=self.on_ticker_change,
|
||||
)
|
||||
with indicators_sideopts:
|
||||
indicators = indicators_sideopts.multiselect(
|
||||
"Indicators",
|
||||
key="indicators",
|
||||
options=indicators_opts,
|
||||
on_change=self.handle_indicators,
|
||||
kwargs=dict(options_col=length_opts),
|
||||
)
|
||||
|
||||
st.sidebar.markdown("""---""")
|
||||
if st.sidebar.button("Plot Data"):
|
||||
if ticker:
|
||||
await self.handle_changes(
|
||||
start_date,
|
||||
end_date,
|
||||
ticker,
|
||||
indicators,
|
||||
)
|
||||
else:
|
||||
with logger.container():
|
||||
logger.error("Please enter a ticker", icon="❗")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
loop.run_until_complete(Handler(loop).run())
|
||||
@@ -268,10 +268,10 @@ def display_candle(
|
||||
data.name = f"{from_symbol}/{to_symbol}"
|
||||
fig = PlotlyTA.plot(data, dict(rma=dict(length=ma)), volume=has_volume)
|
||||
if add_trend:
|
||||
fig.add_trend(data)
|
||||
fig.add_trend(data, secondary_y=has_volume)
|
||||
|
||||
fig.add_logscale_menus()
|
||||
fig.update_yaxes(type=yscale, row=1, col=1, nticks=20)
|
||||
fig.add_logscale_menus("yaxis2" if has_volume else "yaxis")
|
||||
fig.update_yaxes(type=yscale, row=1, col=1, nticks=20, secondary_y=has_volume)
|
||||
|
||||
return fig.show(external=external_axes)
|
||||
|
||||
|
||||
1
openbb_terminal/miscellaneous/i18n/en.yml
vendored
1
openbb_terminal/miscellaneous/i18n/en.yml
vendored
@@ -1069,6 +1069,7 @@ en:
|
||||
dashboards/stocks: historic stock information
|
||||
dashboards/forecast: forecasting for timeseries datasets
|
||||
dashboards/forecasting: forecasting for timeseries datasets
|
||||
dashboards/indicators: technical analysis indicators
|
||||
dashboards/correlation: stock correlations
|
||||
dashboards/vsurf: options volatility surface
|
||||
dashboards/chains: options chain analysis
|
||||
|
||||
2
openbb_terminal/reports/templates/etf.ipynb
vendored
2
openbb_terminal/reports/templates/etf.ipynb
vendored
@@ -25,7 +25,7 @@
|
||||
"from openbb_terminal.reports import widget_helpers as widgets\n",
|
||||
"from openbb_terminal.sdk import openbb\n",
|
||||
"from openbb_terminal.core.config.paths import REPOSITORY_DIRECTORY\n",
|
||||
"from openbb_terminal import OpenBBFigure\n",
|
||||
"from openbb_terminal import OpenBBFigure, theme\n",
|
||||
"from openbb_terminal.helper_classes import TerminalStyle\n",
|
||||
"\n",
|
||||
"warnings.filterwarnings(\"ignore\")"
|
||||
|
||||
@@ -40,12 +40,10 @@ def plot_chart(
|
||||
"""
|
||||
|
||||
fig = OpenBBFigure.create_subplots(
|
||||
rows=2,
|
||||
rows=1,
|
||||
cols=1,
|
||||
shared_xaxes=True,
|
||||
vertical_spacing=0.06,
|
||||
row_width=[0.3, 0.7],
|
||||
subplot_titles=["", "Volume"],
|
||||
specs=[[{"secondary_y": True}]],
|
||||
)
|
||||
fig.set_title(f"Historical {symbol} {price} {option_type.title()}")
|
||||
|
||||
@@ -55,9 +53,10 @@ def plot_chart(
|
||||
low=df.Low,
|
||||
close=df.Close,
|
||||
x=df.index,
|
||||
name=f"OHLC {symbol}",
|
||||
name=f"{price} {option_type.title()} OHLC",
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=True,
|
||||
)
|
||||
fig.add_stock_volume(df)
|
||||
fig.hide_holidays()
|
||||
|
||||
@@ -72,12 +72,11 @@ def display_historical(
|
||||
df_hist.columns = [x.title() for x in df_hist.columns]
|
||||
|
||||
fig = OpenBBFigure.create_subplots(
|
||||
rows=2,
|
||||
rows=1,
|
||||
cols=1,
|
||||
shared_xaxes=True,
|
||||
specs=[[{"secondary_y": True}]],
|
||||
vertical_spacing=0.03,
|
||||
row_heights=[0.3, 0.7],
|
||||
subplot_titles=[f"{symbol} {strike} {op_type}", "Volume"],
|
||||
subplot_titles=[f"{symbol} {strike} {op_type}"],
|
||||
)
|
||||
|
||||
fig.add_candlestick(
|
||||
@@ -89,8 +88,9 @@ def display_historical(
|
||||
name=f"{symbol} OHLC",
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=True,
|
||||
)
|
||||
fig.add_stock_volume(df_hist, row=2, col=1)
|
||||
fig.add_stock_volume(df_hist)
|
||||
fig.hide_holidays()
|
||||
|
||||
if export:
|
||||
@@ -178,9 +178,7 @@ def view_historical_greeks(
|
||||
export=bool(export),
|
||||
)
|
||||
|
||||
try:
|
||||
greek_df = df[greek.lower()]
|
||||
except KeyError:
|
||||
if greek.lower() not in df.columns:
|
||||
return console.print(f"[red]Could not find greek {greek} in data.[/red]\n")
|
||||
|
||||
fig = OpenBBFigure.create_subplots(
|
||||
@@ -193,16 +191,18 @@ def view_historical_greeks(
|
||||
|
||||
fig.add_scatter(
|
||||
x=df.index,
|
||||
y=df.price,
|
||||
y=df.price.values,
|
||||
name="Stock Price",
|
||||
line=dict(color=theme.down_color),
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_scatter(
|
||||
x=df.index,
|
||||
y=greek_df,
|
||||
y=df[greek.lower()].values,
|
||||
name=greek.title(),
|
||||
line=dict(color=theme.up_color),
|
||||
yaxis="y2",
|
||||
secondary_y=True,
|
||||
)
|
||||
fig.update_layout(
|
||||
yaxis2=dict(
|
||||
|
||||
@@ -177,16 +177,20 @@ def view_historical_greeks(
|
||||
)
|
||||
fig.add_scatter(
|
||||
x=df.index,
|
||||
y=df.price,
|
||||
y=df.price.values,
|
||||
name="Stock Price",
|
||||
line=dict(color=theme.down_color),
|
||||
connectgaps=True,
|
||||
secondary_y=False,
|
||||
)
|
||||
fig.add_scatter(
|
||||
x=df.index,
|
||||
y=df[greek.lower()],
|
||||
y=df[greek.lower()].values,
|
||||
connectgaps=True,
|
||||
name=greek.title(),
|
||||
line=dict(color=theme.up_color),
|
||||
yaxis="y2",
|
||||
secondary_y=True,
|
||||
)
|
||||
fig.update_layout(
|
||||
margin=dict(t=30),
|
||||
@@ -199,7 +203,6 @@ def view_historical_greeks(
|
||||
title=f"{symbol} Price",
|
||||
),
|
||||
)
|
||||
fig.hide_holidays()
|
||||
|
||||
export_data(
|
||||
export,
|
||||
|
||||
@@ -70,16 +70,14 @@ def display_historical(
|
||||
df_hist.columns = [x.title() for x in df_hist.columns]
|
||||
|
||||
fig = OpenBBFigure.create_subplots(
|
||||
rows=2,
|
||||
rows=1,
|
||||
cols=1,
|
||||
shared_xaxes=True,
|
||||
specs=[[{"secondary_y": True}]],
|
||||
vertical_spacing=0.06,
|
||||
row_width=[0.3, 0.7],
|
||||
subplot_titles=["", "Volume"],
|
||||
)
|
||||
fig.set_title(f"Historical {symbol} {strike} {op_type.title()}")
|
||||
|
||||
df_hist.index = df_hist.index.astype("datetime64[ms]")
|
||||
df_hist.index = df_hist.index.astype("datetime64[ns]")
|
||||
|
||||
fig.add_candlestick(
|
||||
open=df_hist["Open"],
|
||||
@@ -90,8 +88,9 @@ def display_historical(
|
||||
name=f"{symbol} OHLC",
|
||||
row=1,
|
||||
col=1,
|
||||
secondary_y=True,
|
||||
)
|
||||
fig.add_stock_volume(df_hist, row=2, col=1)
|
||||
fig.add_stock_volume(df_hist)
|
||||
fig.hide_holidays()
|
||||
|
||||
if export:
|
||||
|
||||
@@ -90,10 +90,8 @@ class StocksController(StockBaseController):
|
||||
|
||||
if session and get_current_user().preferences.USE_PROMPT_TOOLKIT:
|
||||
choices: dict = self.choices_default
|
||||
choices["load"] = {
|
||||
"--file": {c: None for c in self.USER_IMPORT_FILES},
|
||||
"-f": "--file",
|
||||
}
|
||||
choices["load"]["--file"] = {c: None for c in self.USER_IMPORT_FILES}
|
||||
choices["load"]["-f"] = "--file"
|
||||
self.completer = NestedCompleter.from_nested_dict(choices)
|
||||
|
||||
def print_help(self):
|
||||
@@ -380,7 +378,7 @@ class StocksController(StockBaseController):
|
||||
dest="ticker",
|
||||
help="Ticker to analyze",
|
||||
type=str,
|
||||
default=self.ticker,
|
||||
default="",
|
||||
required=not any(x in other_args for x in ["-h", "--help"])
|
||||
and not self.ticker,
|
||||
)
|
||||
|
||||
@@ -401,10 +401,8 @@ def load(
|
||||
if df_stock_candidate.empty:
|
||||
return pd.DataFrame()
|
||||
|
||||
df_stock_candidate.index = (
|
||||
pd.to_datetime(df_stock_candidate.index, utc=True)
|
||||
.tz_convert(pytz.timezone("America/New_York"))
|
||||
.tz_localize(None)
|
||||
df_stock_candidate.index = pd.to_datetime(
|
||||
df_stock_candidate.index, utc=True
|
||||
)
|
||||
|
||||
s_start = (
|
||||
@@ -471,6 +469,10 @@ def load(
|
||||
pytz.utc.localize(s_start_dt) if s_start_dt > start_date else start_date
|
||||
)
|
||||
s_interval = f"{interval}min"
|
||||
|
||||
if not prepost:
|
||||
df_stock_candidate = df_stock_candidate.between_time("9:30", "16:00")
|
||||
|
||||
int_string = "Intraday"
|
||||
|
||||
s_intraday = (f"Intraday {interval}min", int_string)[interval == 1440]
|
||||
@@ -517,11 +519,7 @@ def display_candle(
|
||||
Flag to add high and low trends to chart
|
||||
ma: Tuple[int]
|
||||
Moving averages to add to the candle
|
||||
asset_type_: str
|
||||
String to include in title
|
||||
external_axes : bool, optional
|
||||
Whether to return the figure object or not, by default False
|
||||
asset_type_: str
|
||||
asset_type: str
|
||||
String to include in title
|
||||
start_date: str or datetime, optional
|
||||
Start date to get data from with. - datetime or string format (YYYY-MM-DD)
|
||||
@@ -537,6 +535,8 @@ def display_candle(
|
||||
Flag to get weekly data
|
||||
monthly: bool
|
||||
Flag to get monthly data
|
||||
external_axes : bool, optional
|
||||
Whether to return the figure object or not, by default False
|
||||
raw : bool, optional
|
||||
Flag to display raw data, by default False
|
||||
yscale: str
|
||||
@@ -591,14 +591,18 @@ def display_candle(
|
||||
if ma:
|
||||
kwargs["rma"] = dict(length=ma)
|
||||
|
||||
if data.index[-2].date() == data.index[-1].date():
|
||||
interval = int((data.index[1] - data.index[0]).seconds / 60)
|
||||
|
||||
data.name = f"{asset_type} {symbol}"
|
||||
|
||||
fig = PlotlyTA.plot(data, dict(**kwargs), prepost=prepost)
|
||||
|
||||
if add_trend:
|
||||
fig.add_trend(data)
|
||||
fig.add_trend(data, secondary_y=True)
|
||||
|
||||
fig.update_layout(yaxis=dict(title="Stock Price ($)", type=yscale))
|
||||
fig.add_logscale_menus()
|
||||
fig.update_layout(yaxis2=dict(title="Stock Price ($)", type=yscale))
|
||||
fig.add_logscale_menus("yaxis2")
|
||||
|
||||
return fig.show(external=external_axes)
|
||||
|
||||
@@ -879,10 +883,16 @@ def show_codes_polygon(ticker: str):
|
||||
r_json = r_json["results"]
|
||||
cols = ["cik", "composite_figi", "share_class_figi", "sic_code"]
|
||||
vals = [r_json[col] for col in cols]
|
||||
polygon_df = pd.DataFrame({"codes": [c.upper() for c in cols], "vals": vals})
|
||||
polygon_df = pd.DataFrame(
|
||||
{"codes": [c.upper() for c in cols], "values": vals},
|
||||
columns=["codes", "values"],
|
||||
)
|
||||
polygon_df.codes = polygon_df.codes.apply(lambda x: x.replace("_", " "))
|
||||
print_rich_table(
|
||||
polygon_df, show_index=False, headers=["", ""], title=f"{ticker.upper()} Codes"
|
||||
polygon_df,
|
||||
show_index=False,
|
||||
headers=["code", "value"],
|
||||
title=f"{ticker.upper()} Codes",
|
||||
)
|
||||
|
||||
|
||||
|
||||
20488
poetry.lock
generated
vendored
20488
poetry.lock
generated
vendored
File diff suppressed because it is too large
Load Diff
2
pyproject.toml
vendored
2
pyproject.toml
vendored
@@ -93,7 +93,7 @@ setuptools = "<65.5.0"
|
||||
numpy = "1.23.4"
|
||||
papermill = "2.4"
|
||||
stocksera = "^0.1.21"
|
||||
ipython = "8.5.0"
|
||||
ipython = "8.11.0"
|
||||
protobuf = "3.20.1"
|
||||
pytorch-lightning = {version = "1.6.5", optional = true}
|
||||
u8darts = {extras = ["torch"], version = "0.23.0", optional = true}
|
||||
|
||||
Reference in New Issue
Block a user