"""Bull/bear market regime filter visualisation.
- Visualise discreet bull/bear market filter by showing coloured regions over price chart
"""
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from tradeexecutor.analysis.regime import Regime, get_regime_signal_regions
#: Different regime colours
DEFAULT_COLOUR_MAP = {
Regime.bull: "green",
Regime.bear: "red",
}
[docs]def visualise_market_regime_filter(
price: pd.Series,
signal: pd.Series,
title="Regime filter",
height=800,
colour_map=DEFAULT_COLOUR_MAP,
opacity=0.1,
region_data: list | None=None,
) -> go.Figure:
"""Visualise a bull/bear market regime filter on the top of the price action chart.
- Draw price
- Under price colour regime filter regions. Bull regime green, bear regime read.
- Crab market does not have colour
:param price_series:
Price on an asset
:param signal:
The market regime filter signal.
See :py:class:`Regime`
+1 for bull
0 = crab
-1 for bear
:param title:
Chart title
:param region_data:
Export individua region data for inspection
:return:
A chart with price action.
Sections that are in bull market are coloured green.
Sections that are in bear market are coloured red.
Crab market sections are blue.
"""
assert isinstance(price, pd.Series), f"Price expected to be a continous series: {price}"
assert isinstance(signal, pd.Series), f"Signal expected to be a continous series: {signal}"
assert len(price) > 0
assert len(signal) > 0
# TODO: Complications
# assert len(price) == len(signal), f"Price and signal are different series. Price length: {len(price)}, signal length: {len(signal)}"
signal_unique = signal.unique()
for uniq_val in signal_unique:
if not pd.isna(uniq_val):
assert uniq_val in (-1, 0, 1, None), f"Got unknown market regime value: {uniq_val}"
# Fill the area between close price and SMA indicator
# See https://plotly.com/python/filled-area-plots/#interior-filling-for-area-chart
# See also https://stackoverflow.com/a/64743166/315168
fig = go.Figure(
layout={
"title": title,
"height": height,
}
)
# We need to use an invisible trace so we can reset "next y"
for region in get_regime_signal_regions(signal):
# https://stackoverflow.com/questions/55062965/python-how-to-make-shaded-areas-or-alternating-background-color-using-plotly
colour = colour_map.get(region.regime)
if region_data is not None:
region_data.append(region)
if colour:
fig.add_vrect(
x0=region.start,
x1=region.end,
fillcolor=colour_map.get(region.regime),
opacity=opacity,
line_width=0,
)
# for the red area indicator
fig.add_trace(
go.Scatter(
x=price.index,
y=price,
line_color="black",
showlegend=False,
)
)
return fig
[docs]def visualise_raw_market_regime_indicator(
price: pd.Series,
indicator: pd.DataFrame,
title="Raw market regime indicator data",
height=800,
indicator_height=200,
):
"""Draw a raw underlying market regime filter indicator data.
- Visualise ADX indicator or similar under a price chart
:param price:
Close price series
:param indicator:
ADX indicator data or similar
:return:
Plotly figure
"""
assert isinstance(price, pd.Series)
assert isinstance(indicator, pd.DataFrame), f"Expected DataFrame, got {type(indicator)}"
indicator_count = len(indicator.columns)
rows = 1 + indicator_count
row_heights = [height] + [indicator_height] * indicator_count
total_height = sum(row_heights)
fig = make_subplots(
rows=rows,
shared_xaxes=True,
row_heights=row_heights
)
fig.add_trace(
go.Scatter(
x=price.index,
y=price,
line_color="black",
showlegend=False,
),
row=1,
col=1,
)
fig.update_yaxes(
title_text='Price',
row=1,
col=1,
)
row = 2
for name, indicator in indicator.items():
fig.add_scatter(
x=indicator.index,
y=indicator,
row=row,
col=1,
)
fig.update_yaxes(
title_text=name,
row=row,
col=1,
)
row += 1
fig.update_layout(
xaxis_rangeslider_visible=False,
title=title,
showlegend=False,
hovermode="x unified",
height=total_height,
)
# Unified cross hair
# https://community.plotly.com/t/unified-hovermode-with-sublots/37606/2
fig.update_traces(xaxis='x1')
return fig