Source code for qf_lib.analysis.common.abstract_document

#     Copyright 2016-present CERN – European Organization for Nuclear Research
#
#     Licensed under the Apache License, Version 2.0 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.

from abc import abstractmethod, ABCMeta
from os.path import join
from typing import List

import pandas as pd

from qf_lib.analysis.timeseries_analysis.timeseries_analysis import TimeseriesAnalysis
from qf_lib.common.enums.plotting_mode import PlottingMode
from qf_lib.common.utils.logging.qf_parent_logger import qf_logger
from qf_lib.common.utils.returns.drawdown_tms import drawdown_tms
from qf_lib.common.utils.volatility.get_volatility import get_volatility
from qf_lib.containers.series.prices_series import PricesSeries
from qf_lib.containers.series.qf_series import QFSeries
from qf_lib.documents_utils.document_exporting.document import Document
from qf_lib.documents_utils.document_exporting.element.grid import GridElement
from qf_lib.documents_utils.document_exporting.element.page_header import PageHeaderElement
from qf_lib.documents_utils.document_exporting.element.table import Table
from qf_lib.documents_utils.document_exporting.pdf_exporter import PDFExporter
from qf_lib.plotting.charts.line_chart import LineChart
from qf_lib.plotting.decorators.axes_formatter_decorator import AxesFormatterDecorator, PercentageFormatter
from qf_lib.plotting.decorators.axes_position_decorator import AxesPositionDecorator
from qf_lib.plotting.decorators.data_element_decorator import DataElementDecorator
from qf_lib.plotting.decorators.legend_decorator import LegendDecorator
from qf_lib.plotting.decorators.line_decorators import HorizontalLineDecorator
from qf_lib.plotting.decorators.title_decorator import TitleDecorator
from qf_lib.plotting.decorators.underwater_decorator import UnderwaterDecorator
from qf_lib.settings import Settings
from qf_lib.starting_dir import get_starting_dir_abs_path
from qf_lib.plotting.decorators.fill_between_decorator import FillBetweenDecorator
from qf_lib.documents_utils.document_exporting.element.chart import ChartElement


[docs]class AbstractDocument(metaclass=ABCMeta): """ Base class for most PDF documents with charts and tables. Parameters ----------- settings: Settings settings containing all necessary information pdf_exporter: PDFExporter used to create PDF document title: str title of the document """ def __init__(self, settings: Settings, pdf_exporter: PDFExporter, title: str = "Document Title"): self.title = title self.document = Document(title) self.full_image_size = (8, 2.4) # position is linked to the position of axis in tearsheet.mplstyle self.full_image_axis_position = (0.07, 0.1, 0.93, 0.80) # (left, bottom, width, height) self.half_image_size = (4, 2.1) self.dpi = 400 self.settings = settings self.pdf_exporter = pdf_exporter self.logger = qf_logger.getChild(self.__class__.__name__) @abstractmethod def build_document(self): # main function that composes the document pass @abstractmethod def save(self, report_dir: str = ""): # function that saves the document on the disk pass def _get_new_grid(self) -> GridElement: return GridElement(mode=PlottingMode.PDF, figsize=self.half_image_size, dpi=self.dpi) def _add_header(self): logo_path = join(get_starting_dir_abs_path(), self.settings.logo_path) if hasattr(self.settings, "logo_path") \ else None company_name = getattr(self.settings, "company_name", "") if not logo_path: self.logger.warning( f"{self.__class__.__name__} will be generated without a logo in the header. If you would " f"like to include your logo, add 'logo_path' variable to your JSON settings file. " ) if not company_name: self.logger.warning( f"{self.__class__.__name__} will be generated without a company name in the header. If you would " f"like to include your company name, add 'company_name' variable to your JSON settings file. " ) self.document.add_element(PageHeaderElement(logo_path, company_name, self.title)) def _get_underwater_chart(self, series: QFSeries, title="Drawdown", benchmark_series: QFSeries = None, rotate_x_axis: bool = False): underwater_chart = LineChart(start_x=series.index[0], end_x=series.index[-1], log_scale=False, rotate_x_axis=rotate_x_axis) underwater_chart.add_decorator(UnderwaterDecorator(series)) underwater_chart.add_decorator(TitleDecorator(title)) if benchmark_series is not None: legend = LegendDecorator() benchmark_dd = PricesSeries(drawdown_tms(benchmark_series)) benchmark_dd *= -1 benchmark_dd_elem = DataElementDecorator(benchmark_dd, color="black", linewidth=0.5) legend.add_entry(benchmark_dd_elem, "Benchmark DD") underwater_chart.add_decorator(benchmark_dd_elem) underwater_chart.add_decorator(legend) return underwater_chart def _get_large_perf_chart(self, series_list): return self._get_perf_chart(series_list, True) def _get_small_perf_chart(self, series_list): return self._get_perf_chart(series_list, False) def _get_perf_chart(self, series_list, is_large_chart, title="Strategy Performance"): strategy = series_list[0].to_prices(1) # the main strategy should be the first series log_scale = True if strategy[-1] > 10 else False # use log scale for returns above 1 000 % if is_large_chart: chart = LineChart(start_x=strategy.index[0], end_x=strategy.index[-1], log_scale=log_scale) position_decorator = AxesPositionDecorator(*self.full_image_axis_position) chart.add_decorator(position_decorator) else: chart = LineChart(log_scale=log_scale, rotate_x_axis=True) line_decorator = HorizontalLineDecorator(1, key="h_line", linewidth=1) chart.add_decorator(line_decorator) legend = LegendDecorator() for series in series_list: strategy_tms = series.to_prices(1) series_elem = DataElementDecorator(strategy_tms) chart.add_decorator(series_elem) legend.add_entry(series_elem, strategy_tms.name) chart.add_decorator(legend) title_decorator = TitleDecorator(title, key="title") chart.add_decorator(title_decorator) return chart def _get_leverage_chart(self, leverage: QFSeries, rotate_x_axis: bool = False): return self._get_line_chart(leverage, "Leverage over time", rotate_x_axis) def _get_line_chart(self, series: QFSeries, title: str, rotate_x_axis: bool = False): chart = LineChart(rotate_x_axis=rotate_x_axis) series_elem = DataElementDecorator(series) chart.add_decorator(series_elem) title_decorator = TitleDecorator(title, key="title") chart.add_decorator(title_decorator) return chart def _get_rolling_ret_and_vol_chart(self, timeseries): freq = timeseries.get_frequency() rolling_window_len = int(freq.value / 2) # 6M rolling step = round(freq.value / 6) # 2M shift tms = timeseries.to_prices(1) chart = LineChart(start_x=tms.index[0], end_x=tms.index[-1]) line_decorator = HorizontalLineDecorator(0, key="h_line", linewidth=1) chart.add_decorator(line_decorator) legend = LegendDecorator() def tot_return(window): return PricesSeries(window).total_cumulative_return() def volatility(window): return get_volatility(PricesSeries(window), freq) functions = [tot_return, volatility] names = ['Rolling Return', 'Rolling Volatility'] for func, name in zip(functions, names): rolling = tms.rolling_window(rolling_window_len, func, step=step) rolling_element = DataElementDecorator(rolling) chart.add_decorator(rolling_element) legend.add_entry(rolling_element, name) chart.add_decorator(legend) chart.add_decorator(AxesFormatterDecorator(y_major=PercentageFormatter(".0f"))) position_decorator = AxesPositionDecorator(*self.full_image_axis_position) chart.add_decorator(position_decorator) title_str = "Rolling Stats [{} {} samples]".format(rolling_window_len, freq) title_decorator = TitleDecorator(title_str, key="title") chart.add_decorator(title_decorator) return chart def _get_rolling_chart(self, timeseries_list, rolling_function, function_name): freq = timeseries_list[0].get_frequency() timeseries_list = [tms.dropna().to_prices(1) for tms in timeseries_list] df = pd.concat(timeseries_list, axis=1).fillna(method='ffill') rolling_window_len = int(freq.value / 2) # 6M rolling step = round(freq.value / 6) # 2M shift legend = LegendDecorator() chart = LineChart(start_x=df.index[0], end_x=df.index[-1]) line_decorator = HorizontalLineDecorator(0, key="h_line", linewidth=1) chart.add_decorator(line_decorator) for _, tms in df.items(): rolling = tms.rolling_window(rolling_window_len, rolling_function, step=step) rolling_element = DataElementDecorator(rolling) chart.add_decorator(rolling_element) legend.add_entry(rolling_element, tms.name) chart.add_decorator(legend) chart.add_decorator(AxesFormatterDecorator(y_major=PercentageFormatter(".0f"))) position_decorator = AxesPositionDecorator(*self.full_image_axis_position) chart.add_decorator(position_decorator) title_str = "{} - Rolling Stats [{} {} samples]".format(function_name, rolling_window_len, freq) title_decorator = TitleDecorator(title_str, key="title") chart.add_decorator(title_decorator) return chart def _add_statistics_table(self, ta_list: List[TimeseriesAnalysis]): table = Table(css_class="table stats-table") for ta in ta_list: ta.populate_table(table) self.document.add_element(table) def _add_relative_performance_chart(self, strategy_tms: QFSeries, benchmark_tms: QFSeries, chart_title: str = "Relative Performance", legend_subtitle: str = "Strategy - Benchmark"): diff = strategy_tms.to_simple_returns().subtract(benchmark_tms.to_simple_returns()) diff = diff.fillna(0) diff = diff.to_prices(1) - 1 chart = LineChart(start_x=diff.index[0], end_x=diff.index[-1], log_scale=False) position_decorator = AxesPositionDecorator(*self.full_image_axis_position) chart.add_decorator(position_decorator) line_decorator = HorizontalLineDecorator(0, key="h_line", linewidth=1) chart.add_decorator(line_decorator) legend = LegendDecorator() series_elem = DataElementDecorator(diff) chart.add_decorator(series_elem) legend.add_entry(series_elem, f"[{legend_subtitle}] % diff") title_decorator = TitleDecorator(chart_title, key="title") chart.add_decorator(title_decorator) chart.add_decorator(AxesFormatterDecorator(y_major=PercentageFormatter(".0f"))) diff_simple = strategy_tms.to_prices(1).subtract(benchmark_tms.to_prices(1)) diff_simple = diff_simple.ffill() fill_decorator = FillBetweenDecorator(diff_simple) chart.add_decorator(fill_decorator) legend.add_entry(fill_decorator, f"[{legend_subtitle}] $ diff") chart.add_decorator(legend) self.document.add_element(ChartElement(chart, figsize=self.full_image_size, dpi=self.dpi))