This commit is contained in:
teh_coderer
2023-03-13 09:09:56 -04:00
parent cbdf33603c
commit 6e27fcb53a
32 changed files with 11515 additions and 10709 deletions

1
.gitattributes vendored
View File

@@ -1,2 +1,3 @@
* linguist-vendored
*.py linguist-vendored=false
text eol=lf

View File

@@ -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)

View File

@@ -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],
)

View File

@@ -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>

View File

@@ -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,

View File

@@ -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] = []

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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";
}
}

View File

@@ -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)

View File

@@ -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:

View File

@@ -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 = {

View 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())

View File

@@ -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)

View File

@@ -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

View File

@@ -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\")"

View File

@@ -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()

View File

@@ -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(

View File

@@ -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,

View File

@@ -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:

View File

@@ -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,
)

View File

@@ -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

File diff suppressed because it is too large Load Diff

2
pyproject.toml vendored
View File

@@ -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}