def _add_performance_statistics(self): """ For each ticker computes its overall performance (PnL of short positions, PnL of long positions, total PnL). It generates a table containing final PnL values for each of the ticker nad optionally plots the performance throughout the backtest. """ closed_positions = self.backtest_result.portfolio.closed_positions() closed_positions_pnl = QFDataFrame.from_records( data=[(self._ticker_name(p.contract()), p.end_time, p.direction(), p.total_pnl) for p in closed_positions], columns=["Tickers name", "Time", "Direction", "Realised PnL"] ) closed_positions_pnl = closed_positions_pnl.sort_values(by="Time") # Get all open positions history open_positions_history = self.backtest_result.portfolio.positions_history() open_positions_history = open_positions_history.reset_index().melt( id_vars='index', value_vars=open_positions_history.columns, var_name='Contract', value_name='Position summary') open_positions_pnl = QFDataFrame(data={ "Tickers name": open_positions_history["Contract"].apply(lambda contract: self._ticker_name(contract)), "Time": open_positions_history["index"], "Direction": open_positions_history["Position summary"].apply( lambda p: p.direction if isinstance(p, BacktestPositionSummary) else 0), "Total PnL of open position": open_positions_history["Position summary"].apply( lambda p: p.total_pnl if isinstance(p, BacktestPositionSummary) else 0) }) all_positions_pnl = pd.concat([closed_positions_pnl, open_positions_pnl], sort=False) performance_dicts_series = all_positions_pnl.groupby(by=["Tickers name"]).apply( self._performance_series_for_ticker) performance_df = QFDataFrame(performance_dicts_series.tolist(), index=performance_dicts_series.index) self.document.add_element(NewPageElement()) self.document.add_element(HeadingElement(level=2, text="Performance of each asset")) final_performance = performance_df. \ applymap(lambda pnl_series: pnl_series.iloc[-1] if not pnl_series.empty else 0.0). \ sort_values(by="Overall performance", ascending=False). \ applymap(lambda p: '{:,.2f}'.format(p)). \ reset_index() table = DFTable(final_performance, css_classes=['table', 'left-align']) table.add_columns_classes(["Tickers name"], 'wide-column') self.document.add_element(table) # Add performance plots if self.generate_pnl_chart_per_ticker: self.document.add_element(NewPageElement()) self.document.add_element( HeadingElement(level=2, text="Performance of each asset during the whole backtest")) for ticker_name, performance in performance_df.iterrows(): self._plot_ticker_performance(ticker_name, performance)
class ModelController(object): def __init__(self, data=None, index=None, columns=None, dtype=None, copy=False): self.logger = qf_logger.getChild(self.__class__.__name__) # Data Frame containing the table data self.data = QFDataFrame(data, index, columns, dtype, copy) # Dictionary containing a mapping from column names onto ColumnStyles self._columns_styles = { column_name: self.ColumnStyle(column_name) for column_name in self.data.columns.tolist() } # Series containing the styles for each of the rows self._rows_styles = QFSeries( data=[self.RowStyle(loc) for loc in self.data.index], index=self.data.index) # Data Frame containing styles for all cells in the table, based upon columns_styles and rows_styles self._styles = QFDataFrame(data={ column_name: [ self.CellStyle(row_style, column_style) for row_style in self.rows_styles ] for column_name, column_style in self.columns_styles.items() }, index=self.data.index, columns=self.data.columns) self.table_styles = self.Style() def add_columns_styles(self, columns: Union[str, Sequence[str]], styles_dict: Dict[str, str]): if not isinstance(columns, list): columns = [columns] for column_name in columns: self.columns_styles[column_name].add_styles(styles_dict) def add_columns_classes(self, columns: Union[str, Sequence[str]], css_classes: Sequence[str]): if not isinstance(columns, list): columns = [columns] for column_name in columns: self.columns_styles[column_name].add_css_class(css_classes) def remove_columns_classes(self, columns: Union[str, Sequence[str]], css_classes: Sequence[str]): if not isinstance(columns, list): columns = [columns] for column_name in columns: self.columns_styles[column_name].remove_css_class(css_classes) def remove_columns_styles(self, columns: Union[str, Sequence[str]], styles: Union[Dict[str, str], Sequence[str]]): if not isinstance(columns, list): columns = [columns] for column_name in columns: self.columns_styles[column_name].remove_styles(styles) def add_rows_styles(self, loc_indexer: Union[Any, Sequence[Any]], styles_dict: Dict[str, str]): for row in self.rows_styles.loc[loc_indexer]: row.add_styles(styles_dict) def add_rows_classes(self, loc_indexer: Union[Any, Sequence[Any]], css_classes: Sequence[str]): for row in self.rows_styles.loc[loc_indexer]: row.add_css_class(css_classes) def remove_rows_styles(self, loc_indexer: Union[Any, Sequence[Any]], styles: Union[Dict[str, str], Sequence[str]]): for row in self.rows_styles.loc[loc_indexer]: row.remove_styles(styles) def remove_rows_classes(self, loc_indexer: Union[Any, Sequence[Any]], css_classes: Sequence[str]): for row in self.rows_styles.loc[loc_indexer]: row.remove_css_class(css_classes) def add_cells_styles(self, columns: Union[str, Sequence[str]], rows: Union[Any, Sequence[Any]], css_styles: Dict[str, str]): if not isinstance(columns, list): columns = [columns] if not isinstance(rows, list): rows = [rows] for column_name in columns: for row in rows: self.styles.loc[row, column_name].add_styles(css_styles) def add_cells_classes(self, columns: Union[str, Sequence[str]], rows: Union[Any, Sequence[Any]], css_classes: Sequence[str]): if not isinstance(columns, list): columns = [columns] if not isinstance(rows, list): rows = [rows] for column_name in columns: for row in rows: self.styles.loc[row, column_name].add_css_class(css_classes) def remove_cells_styles(self, columns: Union[str, Sequence[str]], rows: Union[Any, Sequence[Any]], styles: Union[Dict[str, str], Sequence[str]]): if not isinstance(columns, list): columns = [columns] if not isinstance(rows, list): rows = [rows] for column_name in columns: for row in rows: self.styles.loc[row, column_name].remove_styles(styles) def remove_cells_classes(self, columns: Union[str, Sequence[str]], rows: Union[Any, Sequence[Any]], css_classes: Sequence[str]): if not isinstance(columns, list): columns = [columns] if not isinstance(rows, list): rows = [rows] for column_name in columns: for row in rows: self.styles.loc[row, column_name].remove_css_class(css_classes) def iterrows(self): return zip(self.data.iterrows(), self.styles.iterrows()) @property def styles(self): def get_cell(row_style, column_style, column_name): try: row_indexer = row_style.label column_indexer = column_name return self._styles.loc[row_indexer, column_indexer] except KeyError: return self.CellStyle(row_style, column_style) self._styles = QFDataFrame(data={ column_name: [ get_cell(row_style, column_style, column_name) for row_style in self.rows_styles ] for column_name, column_style in self.columns_styles.items() }, index=self.data.index, columns=self.data.columns) return self._styles @property def columns_styles(self): if set(self._columns_styles.keys()) != set(self.data.columns): # Delete the unnecessary columns styles self._columns_styles = { column_name: column_style for column_name, column_style in self._columns_styles.items() if column_name in self.data.columns } # Add columns styles, which do not exist yet existing_columns = [ column_name for column_name in self._columns_styles.keys() ] self._columns_styles.update({ column_name: self.ColumnStyle(column_name) for column_name in self.data.columns.tolist() if column_name not in existing_columns }) return self._columns_styles @property def rows_styles(self): if not self._rows_styles.index.equals(self.data.index): # Delete the unnecessary rows styles indices_to_delete = [ index for index in self._rows_styles.index if index not in self.data.index ] self._rows_styles.drop(indices_to_delete, inplace=True) # Add rows styles, which do not exist yet new_indices = [ index for index in self.data.index if index not in self._rows_styles.index ] new_row_styles = QFSeries( [self.RowStyle(loc) for loc in new_indices], index=new_indices) self._rows_styles = self._rows_styles.append(new_row_styles, ignore_index=False) return self._rows_styles class Style(object): def __init__(self, style: Dict[str, str] = None, css_class: str = None): self.style = style if style is not None else dict() self.css_class = css_class.split() if css_class is not None else [] self.logger = qf_logger.getChild(self.__class__.__name__) def add_css_class(self, css_classes: Sequence[str]): self.css_class.extend(css_classes) def remove_css_class(self, css_classes: Sequence[str]): for class_name in css_classes: try: self.css_class.remove(class_name) except ValueError: self.logger.warning( "The css class {} can not be removed, as it does not exist" .format(class_name)) def add_styles(self, styles_dict: Dict[str, str]): self.style.update(styles_dict) def remove_styles(self, styles: Union[Dict[str, str], Sequence[str]]): properties = styles.keys() if type(styles) is dict else styles for property_name in properties: try: del self.style[property_name] except KeyError: self.logger.warning( "The css style for proptyety {} can not be removed, as it does not exist" .format(property_name)) def styles(self): # The function merges into string all styles. This string may be further used as the css styles attributes # value. No spaces between attributes and their values are allowed. def merge_styles(styles_dict: Dict[str, str]) -> str: return "".join([ '%s:%s;' % (key, value) for key, value in styles_dict.items() ]) styles = merge_styles(self.style) styles = '""' if len(styles) == 0 else styles return styles def classes(self): def merge_classes(css_classes_list: Sequence[str]) -> str: return " ".join(css_classes_list) css_classes = merge_classes(self.css_class) return css_classes class ColumnStyle(Style): def __init__(self, column_name: str, style: Dict[str, str] = None, css_class: str = None): super().__init__(style, css_class) self.column_name = column_name class RowStyle(Style): def __init__(self, label: Any, style: Dict[str, str] = None, css_class: str = None): super().__init__(style, css_class) self.label = label class CellStyle(Style): def __init__(self, row_style, column_style, style: Dict[str, str] = None, css_class: str = None): super().__init__(style, css_class) self.row_style = row_style self.column_style = column_style def styles(self): # The function merges into string all styles, corresponding to the row, column and cell. styles = super().styles() styles = self.row_style.styles() + self.column_style.styles( ) + styles styles = '""' if len(styles) == 0 else styles return styles def classes(self): # The function merges into string all styles, corresponding to the row, column and cell. This string may be # further used as the css class attribute value classes = super().classes() css_classes = self.row_style.classes() + self.column_style.classes( ) + classes css_classes = '""' if len(css_classes) == 0 else css_classes return css_classes