Source code for mpl_bsic.plot_trade

from typing import Literal, Union

import matplotlib.pyplot as plt
import pandas as pd
from matplotlib.axes import Axes

from .apply_bsic_logo import apply_bsic_logo
from .apply_bsic_style import apply_bsic_style
from .format_timeseries_axis import format_timeseries_axis


def _plot_xline(ax: Axes, y: float, trade_start: str, color: str):
    bbox_props = dict(boxstyle="round", fc="w", ec="k", lw=1)
    y_min, y_max = ax.get_ylim()

    ax.hlines(
        y=[y],
        xmin=trade_start,
        xmax=ax.get_xbound()[1],
        color=color,
        linewidth=1,
        linestyle="-",
        alpha=0.75,
    )
    ax.annotate(
        str(round(y, 2)),
        xycoords="axes fraction",
        xy=(1.02, (y - y_min) / (y_max - y_min)),
        size=8,
        bbox=bbox_props,
        verticalalignment="center",
    )


def _plot_last_price(ax: Axes, data: pd.Series):
    """Plots the last price of the series on the right side of the plot."""
    # plot last price
    last_price = float(data.iloc[-1])
    y_min, y_max = ax.get_ylim()

    bbox_props = dict(boxstyle="round", fc="w", ec="k", lw=0.75)
    ax.annotate(
        str(int(last_price)),
        xycoords="axes fraction",
        xy=(1.02, (last_price - y_min) / (y_max - y_min)),
        size=8,
        bbox=bbox_props,
        verticalalignment="center",
    )


def _get_dates(
    underlying: pd.Series, pnl: pd.Series, months_offset: int
) -> tuple[pd.DatetimeIndex, pd.Timestamp]:
    """Gets the dates for the trade."""
    if not isinstance(underlying.index, pd.DatetimeIndex):
        raise Exception("Index of underlying must be a DatetimeIndex")

    if not isinstance(pnl.index, pd.DatetimeIndex):
        raise Exception("Index of PnL must be a DatetimeIndex")

    dates = underlying.index

    end_date = dates[-1]
    start_date = end_date - pd.DateOffset(months=months_offset)
    entry_date = pnl.dropna().index[0]

    dates = dates[(dates >= start_date) & (dates <= end_date)]

    if not isinstance(entry_date, pd.Timestamp):
        raise Exception("There was an issue pulling the entry date for the trade.")

    return dates, entry_date


def _plot_entry_point(
    ax: Axes,
    entry_date: pd.Timestamp,
    underlying: pd.Series,
    marker_location: Literal["top", "bottom"],
    marker_size: int,
):
    """Plots the entry point of the trade."""

    offset = 0.075 if marker_location == "top" else -0.075
    marker = "v" if marker_location == "top" else "^"

    price_at_entry = underlying.loc[entry_date]
    marker_loc = price_at_entry * (1 + offset)

    ax.scatter(
        [entry_date.strftime("%Y-%m-%d")],
        [marker_loc],
        color="g",
        marker=marker,
        s=marker_size,
    )


def _preprocess_data(
    underlying: pd.Series, pnl: pd.Series, dates: pd.DatetimeIndex, pnl_type: str
):
    """Preprocesses the data."""
    underlying = underlying.copy().loc[dates]
    pnl = pnl.copy()

    if pnl_type == "nominal":
        pass
    elif pnl_type == "cumulative":
        pnl = pnl.cumsum()
    else:
        raise Exception(
            'pnl type is not supported. Supported are "nominal" and "cumulative".'
        )

    pnl.fillna(0, inplace=True)

    return underlying, pnl


[docs] def plot_trade( underlying: pd.Series, pnl: pd.Series, pnl_type: Literal["nominal", "cumulative"], title: str, underlying_name: str, months_offset: int = 3, sources: Union[str, list[str]] = "BSIC", entry_point_marker_loc: Literal["top", "bottom"] = "top", entry_point_marker_size: int = 10, date_ticks_unit: Literal["Y", "M", "W", "D"] = "W", date_ticks_freq: int = 1, date_ticks_format: str = "%b %d, %Y", ): """Plot a trade performance vs the underlying. Create a figure with two subplots. On the top, the PnL of the trade is plotted, while on the bottom the underlying is plotted. The function automatically applies BSIC Style and Logo through the respective functions. Parameters ---------- underlying : pd.Series Pandas series of the underlying's prices. pnl : pd.Series Pandas series of the pnl of the trade. This must be in nominal terms or in percentage terms, if you want to plot the percentages. You can also plot the cumulative PnL, the function will calculate it for you. pnl_type : Literal['Nominal', 'Cumulative'] The type of PnL you want to plot. Can be "Nominal" or "Cumulative". If "nominal", the function will plot the PnL as is. If "cumulative", the function will plot the cumulative PnL by doing `.cumsum()` on the PnL series. title : str The title of the trade. This will be set as the suptitle of the figure. underlying_name : str The name of the underlying that will be plotted on the bottom subplot, for example "Swap Spread", "EURUSD", etc. months_offset : int, optional The months offset that will be used to calculate the first date plotted, by default 3. Since it is a trade, it is recommended to set a short offset (like 3 as the default), so that you can easily see the path of the underlying after you entered the trade. sources : str | list[str], optional List of sources, by default "BSIC". You can either specify a string (if you have only one source) or a list of strings (multiple sources). Since BSIC is always a source, it will always be included. **NB**: if when calling ``plt.show()`` the text seems cutted out, don't worry. When exporting using ``bbox_inches="tight"``, it will seamlessly fit within the figure. This happens because I want to make sure there is enough space between the plot and the sources text, so I position the text at the very bottom of the figure. entry_point_marker_loc : Literal['top', 'bottom'], optional The location of the marker for the entry point, by default "top". Whether it is better to use "top" or "bottom" depends on the type of trade you are plotting and the shape of the plot. Choose so that the marker is clearly visible and does not overlap with the underlying's plot. entry_point_marker_size : int, optional The size of the entry point market, by default 10. Choosing this size will depend on the final size of the plot. Choose so that the marker is clearly visible and does not overlap with the underlying's plot. date_ticks_unit : Literal['Y', 'M', 'W', 'D'], optional The unit used to segment the datetime index (which is displayed on the bottom axis), by default "W". Since you are probably going to plot a short timespan, it is recommended to use either "W" (weeks) or "D" (days). There should be no reason to use "Y" (years), and you should really question the timeframe you are using if you are choosing "M". date_ticks_freq : int, optional The frequency for the ticks in the unit specified, by default 1. For example, if you chose "W" as the unit, and 1 as the frequency, there will be 1 tick every week. Choose so that the axis is not cluttered. date_ticks_format : str, optional The format used for the date ticks, by default ``"%b %d, %Y"``. I recommend using the default, but you can change it to whatever you find more suitable. Returns ------- tuple[matplotlib.figure.Figure, matplotlib.axes.Axes] A tuple containing the figure and the tuple of two axis just created. You can later use this to save the fig for export, do ``fig.show()``, or further customize the plot. See Also -------- mpl_bsic.apply_bsic_style : Apply the BSIC style to a plot. mpl_bsic.apply_bsic_logo : Apply the BSIC logo to a plot. mpl_bsic.check_figsize : Check if the figsize is valid. Examples -------- Examples will come soon. """ dates, entry_date = _get_dates(underlying, pnl, months_offset) underlying, pnl = _preprocess_data(underlying, pnl, dates, pnl_type) # creates plots fig, axs = plt.subplots( 2, 1, sharex=True, gridspec_kw={"hspace": 0.1, "height_ratios": [1.25, 2]} ) pnl_ax, underlying_ax = axs pnl_ax: Axes underlying_ax: Axes # sets title and applies style if title is not None: fig.suptitle(title) apply_bsic_style(fig, axs, sources) # plot the data underlying_ax.plot(underlying.index, underlying) pnl_ax.plot(pnl.index, pnl) pnl_ax.axhline(0, color="black", linewidth=1, alpha=0.75) # set labels underlying_ax.set_ylabel(underlying_name) pnl_ax.set_ylabel("PnL") # formats dates format_timeseries_axis( underlying_ax, date_ticks_unit, date_ticks_freq, date_ticks_format ) # apply logo apply_bsic_logo(fig, pnl_ax, location="top left") # plot last price _plot_last_price(underlying_ax, underlying) _plot_last_price(pnl_ax, pnl) # plot entry point _plot_entry_point( underlying_ax, entry_date, underlying, entry_point_marker_loc, entry_point_marker_size, ) # plot areas of profit and loss pnl_ax.fill_between( pnl.index, 0, pnl, where=(pnl >= 0), # type: ignore color="g", alpha=0.3, interpolate=True, lw=0, ) pnl_ax.fill_between( pnl.index, 0, pnl, where=(pnl < 0), # type: ignore color="r", alpha=0.3, interpolate=True, ) # # plot stop loss and take profit # trade_start = pnl[pnl["pnl"] > 0].index[-1] # print(trade_start, pnl, pnl[pnl > 0]) # if stop_loss is not None: # _plot_xline(underlying_ax, stop_loss, trade_start=trade_start, color="r") # if take_profit is not None: # _plot_xline(underlying_ax, take_profit, trade_start=trade_start, color="g") # underlying_ax.set_xlim(xlims) fig.subplots_adjust(bottom=0.2) # leave space for the sources return fig, axs