from typing import Union
import numpy as np
import matplotlib.pyplot as plt
from MPSPlots.styles import mps
from matplotlib.patches import Patch
from matplotlib.lines import Line2D
from MPSPlots import helper
from TradeTide.binary import position
from TradeTide.binary.interface_portfolio import PORTFOLIO
import TradeTide
Long = position.Long
Short = position.Short
[docs]
class Portfolio(PORTFOLIO):
def __init__(self, position_collection, debug_mode: bool = False):
"""
Initialize the Portfolio with a position collection and optional debug mode.
Parameters
----------
position_collection : PositionCollection
The collection of positions to manage.
"""
super().__init__(
position_collection=position_collection,
debug_mode=TradeTide.debug_mode if TradeTide.debug_mode else debug_mode,
)
self.position_collection = position_collection
[docs]
@helper.pre_plot(nrows=2, ncols=1)
def plot_positions(
self,
axes: plt.Axes,
max_positions: Union[int, float] = np.inf,
) -> plt.Figure:
"""
Plot market bid/ask prices and shade closed positions, using the mps style,
consistent naming of 'position', and a clear legend with distinct colors.
Parameters
----------
axes : matplotlib.axes.Axes, optional
Axes to draw on. If None, a new figure+axes are created.
max_positions : int or float, default=np.inf
Maximum number of positions to draw (in chronological order).
"""
long_list = []
short_list = []
position_list = self.get_positions(max_positions)
for idx, p in enumerate(position_list):
if p.is_long:
long_list.append(p)
else:
short_list.append(p)
if idx > 10:
break
axes[0].sharex(axes[1])
self._plot_long_positions(axes=axes[0], position_list=long_list, show=False)
self._plot_short_positions(
axes=axes[1], position_list=short_list, show=False
)
@helper.pre_plot(nrows=1, ncols=1)
def _plot_long_positions(
self, position_list: list[position.Long], axes: plt.Axes
) -> None:
"""
Plot the long positions in the portfolio.
Parameters
----------
axes : matplotlib.axes.Axes, optional
Axes to draw on. If None, a new figure+axes are created.
"""
color_fill = "lightblue"
sl_color = "#d62728"
tp_color = "#2ca02c"
axes.set_ylabel(f"Bid Price")
self.position_collection.market.plot_bid(axes=axes, show=False)
for position in position_list:
start, end = position.start_date, position.close_date
axes.axvspan(start, end, facecolor=color_fill, edgecolor="black", alpha=0.2)
axes.plot(
position.dates(),
position.stop_loss_prices(),
linestyle="--",
color=sl_color,
linewidth=1,
)
axes.plot(
position.dates(),
position.take_profit_prices(),
linestyle="--",
color=tp_color,
linewidth=1,
)
# Custom legend
legend_handles = [
Line2D([0], [0], color=sl_color, linestyle="--", label="Stop Loss"),
Line2D([0], [0], color=tp_color, linestyle="--", label="Take Profit"),
Patch(facecolor=color_fill, edgecolor="none", label="Long Position"),
]
axes.legend(handles=legend_handles, loc="upper left", framealpha=0.9)
@helper.pre_plot(nrows=1, ncols=1)
def _plot_short_positions(
self, position_list: list[position.Long], axes: plt.Axes
) -> None:
"""
Plot the short positions in the portfolio.
Parameters
----------
axes : matplotlib.axes.Axes, optional
Axes to draw on. If None, a new figure+axes are created.
"""
color_fill = (0.8, 0.2, 0.2, 0.3)
sl_color = "#d62728"
tp_color = "#2ca02c"
axes.set_ylabel(f"Bid Price")
self.position_collection.market.plot_bid(axes=axes, show=False)
for position in position_list:
start, end = position.start_date, position.close_date
axes.axvspan(start, end, facecolor=color_fill, edgecolor="black", alpha=0.2)
axes.plot(
position.dates(),
position.stop_loss_prices(),
linestyle="--",
color=sl_color,
linewidth=1,
)
axes.plot(
position.dates(),
position.take_profit_prices(),
linestyle="--",
color=tp_color,
linewidth=1,
)
# Custom legend
legend_handles = [
Line2D([0], [0], color=sl_color, linestyle="--", label="Stop Loss"),
Line2D([0], [0], color=tp_color, linestyle="--", label="Take Profit"),
Patch(facecolor=color_fill, edgecolor="none", label="Short Position"),
]
axes.legend(handles=legend_handles, loc="upper left", framealpha=0.9)
[docs]
@helper.pre_plot(nrows=1, ncols=1)
def plot_equity(self, axes: plt.Axes) -> None:
"""
Plot the portfolio's equity over time.
Parameters
----------
axes : matplotlib.axes.Axes, optional
Axes to draw on. If None, a new figure+axes are created.
"""
axes.plot(self.record.time, self.record.equity, color="black")
axes.axhline(
self.record.initial_capital,
color="red",
linestyle="--",
linewidth=1,
label="Initial Capital",
)
axes.set_ylabel("Equity")
axes.legend()
[docs]
@helper.pre_plot(nrows=1, ncols=1)
def plot_capital_at_risk(self, axes: plt.Axes) -> None:
"""
Plot the capital at risk over time.
Parameters
----------
axes : matplotlib.axes.Axes, optional
Axes to draw on. If None, a new figure+axes are created.
"""
axes.step(
self.record.time, self.record.capital_at_risk, color="black", where="mid"
)
axes.set_ylabel("Capital at Risk")
[docs]
@helper.pre_plot(nrows=1, ncols=1)
def plot_capital(self, axes: plt.Axes) -> None:
"""
Plot the capital over time.
Parameters
----------
axes : matplotlib.axes.Axes, optional
Axes to draw on. If None, a new figure+axes are created.
"""
axes.step(self.record.time, self.record.capital, color="black", where="mid")
axes.set_ylabel("Capital")
[docs]
@helper.pre_plot(nrows=1, ncols=1)
def plot_number_of_positions(self, axes: plt.Axes) -> None:
"""
Plot the number of open positions over time.
Parameters
----------
axes : matplotlib.axes.Axes, optional
Axes to draw on. If None, a new figure+axes are created.
"""
axes.step(
self.record.time,
self.record.number_of_concurent_positions,
color="black",
where="mid",
)
axes.set_ylabel("Number of open positions")
[docs]
@helper.pre_plot(nrows=1, ncols=1)
def plot_prices(self, axes: plt.Axes) -> None:
"""
Plot the market bid and ask prices over time.
Parameters
----------
axes : matplotlib.axes.Axes, optional
Axes to draw on. If None, a new figure+axes are created.
"""
axes.plot(self.dates, self.market.ask.open, label="Ask-Open", color="C0")
axes.plot(self.dates, self.market.bid.open, label="Bid-Open", color="C1")
axes.ticklabel_format(style="plain", axis="y") # Prevent y-axis offset
# Legend (bottom plot only)
axes.legend(loc="upper left")
axes.set_ylabel("Prices")
[docs]
def plot(self, *plot_type) -> plt.Figure:
"""
Plot the portfolio's performance, including equity, capital at risk, capital,
number of open positions, and market prices.
Returns
-------
None
"""
if len(plot_type) == 0:
plot_type = (
"equity",
"capital_at_risk",
"capital",
"number_of_positions",
"prices",
)
else:
plot_type = plot_type[0] if isinstance(plot_type[0], tuple) else plot_type
if not isinstance(plot_type, tuple):
plot_type = (plot_type,)
n_plots = len(plot_type)
with plt.style.context(mps):
_, axs = plt.subplots(
nrows=n_plots, ncols=1, figsize=(12, 2 * n_plots), sharex=True
)
plot_methods = {
"equity": self.plot_equity,
"capital_at_risk": self.plot_capital_at_risk,
"capital": self.plot_capital,
"number_of_positions": self.plot_number_of_positions,
"prices": self.plot_prices,
}
for ax, plot in zip(axs, plot_type):
plot_methods[plot](ax=ax, show=False)
plt.tight_layout()
plt.show()