def test_get_aggregate_returns_with_simple_returns(self): test_returns = [1, 1, 1, 1] dates = DatetimeIndex( ['2015-12-01', '2016-05-05', '2016-10-01', '2017-01-05']) simple_returns_series = SimpleReturnsSeries(data=test_returns, index=dates) expected_cumulative_returns = [1.0, 3.0, 1.0] expected_result = SimpleReturnsSeries( data=expected_cumulative_returns, index=DatetimeIndex(['2015-12-31', '2016-12-31', '2017-12-31'])) actual_result = get_aggregate_returns(simple_returns_series, convert_to=Frequency.YEARLY) assert_series_equal(expected_result, actual_result) expected_result = SimpleReturnsSeries(data=[1, 1, 1, 1], index=DatetimeIndex([ '2015-12-31', '2016-05-31', '2016-10-31', '2017-01-31' ])) actual_result = get_aggregate_returns(simple_returns_series, convert_to=Frequency.MONTHLY) assert_series_equal(expected_result, actual_result) actual_result = get_aggregate_returns(simple_returns_series, convert_to=Frequency.MONTHLY, multi_index=True) actual_result = actual_result.unstack() self.assertEqual(actual_result[1].values[2], 1.0) self.assertEqual(actual_result[5].values[1], 1.0) self.assertEqual(actual_result[10].values[1], 1.0) self.assertEqual(actual_result[12].values[0], 1.0)
def test_get_aggregate_returns_with_log_returns(self): test_returns = [0.01] * 22 dates = DatetimeIndex([ '2000-01-15', '2000-01-17', '2000-01-19', '2000-01-21', '2000-01-23', '2000-01-25', '2000-01-27', '2000-01-29', '2000-01-31', '2000-02-02', '2000-02-04', '2000-03-05', '2000-04-04', '2000-05-04', '2000-06-03', '2000-07-03', '2001-01-19', '2001-08-07', '2002-02-23', '2002-09-11', '2003-03-30', '2003-10-16' ]) log_returns_series = LogReturnsSeries(data=test_returns, index=dates) expected_result = log_returns_series.to_simple_returns() actual_result = get_aggregate_returns(log_returns_series, convert_to=Frequency.DAILY) assert_series_equal(expected_result, actual_result) expected_cumulative_returns = \ [0.01, 0.04, 0.03, 0.03, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01] expected_dates = DatetimeIndex([ '2000-01-14', '2000-01-21', '2000-01-28', '2000-02-04', '2000-03-03', '2000-04-07', '2000-05-05', '2000-06-02', '2000-07-07', '2001-01-19', '2001-08-10', '2002-02-22', '2002-09-13', '2003-03-28', '2003-10-17' ]) expected_result = LogReturnsSeries( data=expected_cumulative_returns, index=expected_dates).to_simple_returns() actual_result = get_aggregate_returns(log_returns_series, convert_to=Frequency.WEEKLY) assert_series_equal(expected_result, actual_result) expected_cumulative_returns = \ [0.09, 0.02, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01] expected_dates = DatetimeIndex([ '2000-01-31', '2000-02-29', '2000-03-31', '2000-04-30', '2000-05-31', '2000-06-30', '2000-07-31', '2001-01-31', '2001-08-31', '2002-02-28', '2002-09-30', '2003-03-31', '2003-10-31' ]) expected_result = LogReturnsSeries( data=expected_cumulative_returns, index=expected_dates).to_simple_returns() actual_result = get_aggregate_returns(log_returns_series, convert_to=Frequency.MONTHLY) assert_series_equal(expected_result, actual_result) expected_cumulative_returns = [0.16, 0.02, 0.02, 0.02] expected_dates = DatetimeIndex( ['2000-12-31', '2001-12-31', '2002-12-31', '2003-12-31']) expected_result = LogReturnsSeries( data=expected_cumulative_returns, index=expected_dates).to_simple_returns() actual_result = get_aggregate_returns(log_returns_series, convert_to=Frequency.YEARLY) assert_series_equal(expected_result, actual_result)
def _get_simple_quantile_chart(simple_returns): if len(simple_returns) > 0: simple_returns_weekly = get_aggregate_returns(simple_returns, Frequency.WEEKLY, multi_index=True) simple_returns_monthly = get_aggregate_returns(simple_returns, Frequency.MONTHLY, multi_index=True) chart = BoxplotChart([simple_returns, simple_returns_weekly, simple_returns_monthly], linewidth=1) else: chart = BoxplotChart([QFSeries(), QFSeries(), QFSeries()], linewidth=1) tick_decorator = AxisTickLabelsDecorator(labels=["daily", "weekly", "monthly"], axis=Axis.X) return chart, tick_decorator
def create_return_quantiles(returns: QFSeries, live_start_date: datetime = None, x_axis_labels_rotation: int = 20) \ -> BoxplotChart: """ Creates a new return quantiles boxplot chart based on the returns specified. A swarm plot is also rendered on the chart if the ``live_start_date`` is specified. Parameters ---------- returns The returns series to plot on the chart. live_start_date The live start date that will determine whether a swarm plot should be rendered. x_axis_labels_rotation Returns ------- A new ``BoxplotChart`` instance. """ simple_returns = returns.to_simple_returns() # case when we can plot IS together with OOS if live_start_date is not None: oos_returns = simple_returns.loc[simple_returns.index >= live_start_date] if len(oos_returns) > 0: in_sample_returns = simple_returns.loc[simple_returns.index < live_start_date] in_sample_weekly = get_aggregate_returns(in_sample_returns, Frequency.WEEKLY, multi_index=True) in_sample_monthly = get_aggregate_returns(in_sample_returns, Frequency.MONTHLY, multi_index=True) oos_weekly = get_aggregate_returns(oos_returns, Frequency.WEEKLY, multi_index=True) oos_monthly = get_aggregate_returns(oos_returns, Frequency.MONTHLY, multi_index=True) chart = BoxplotChart([in_sample_returns, oos_returns, in_sample_weekly, oos_weekly, in_sample_monthly, oos_monthly], linewidth=1) x_labels = ["daily IS", "daily OOS", "weekly IS", "weekly OOS", "monthly IS", "monthly OOS"] tick_decorator = AxisTickLabelsDecorator(labels=x_labels, axis=Axis.X, rotation=x_axis_labels_rotation) else: chart, tick_decorator = _get_simple_quantile_chart(simple_returns) else: # case where there is only one set of data chart, tick_decorator = _get_simple_quantile_chart(simple_returns) # fixed_format_decorator = AxesFormatterDecorator(x_major=fixed_formatter) chart.add_decorator(tick_decorator) # Set title. title = TitleDecorator("Return Quantiles") chart.add_decorator(title) chart.add_decorator(AxesLabelDecorator(y_label="Returns")) return chart
def plot(self, figsize: Tuple[float, float] = None): self._setup_axes_if_necessary(figsize) ret_table = get_aggregate_returns(self._returns, Frequency.MONTHLY, multi_index=True).unstack().round(3) ret_table = ret_table.sort_index(ascending=False, axis=0) # show most recent year at the top ret_table = ret_table.fillna(0) * 100 ret_table.rename(columns={1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun', 7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec'}, inplace=True) # Format Y axis to make sure we have a tick for each year or 2 years if there is more that 10 years year_step = 1 if len(ret_table.index) > 10: year_step = 2 sns.heatmap( ret_table, annot=True, annot_kws={"size": 6}, alpha=1.0, center=0.0, cbar=False, cmap=cm.Blues, fmt=".1f", ax=self.axes, yticklabels=year_step # use the column names but plot only every year_step label. ) self._adjust_style() self.axes.yaxis.set_tick_params(rotation=0) self.axes.set_xlabel('Month') self.axes.set_ylabel('Year') self.axes.set_title(self.title)
def convert_dataframe_frequency( dataframe: QFDataFrame, frequency: Frequency) -> SimpleReturnsDataFrame: """ Converts each column in the dataframe to the specified frequency. ValueError is raised when a column has a lower frequency than the one we are converting to. """ # Verify that all columns in the dataframe have a lower frequency. data_frequencies = dataframe.get_frequency() for column, col_frequency in data_frequencies.items(): if col_frequency < frequency: raise ValueError( "Column '{}' cannot be converted to '{}' frequency because its frequency is '{}'." .format(column, frequency, col_frequency)) if frequency == Frequency.DAILY: return dataframe.to_simple_returns() filled_df = dataframe.to_prices().fillna(method="ffill") new_columns = {} for column in filled_df: new_columns[column] = get_aggregate_returns(filled_df[column], frequency) return SimpleReturnsDataFrame(new_columns)
def gain_to_pain_ratio(qf_series: QFSeries) -> float: """ Calculates the gain to pain ratio for a given timeseries of returns. gain_to_pain_ratio is calculated for monthly returns gain_to_pain_ratio = sum(all returns) / abs(sum(negative returns) Parameters ---------- qf_series: QFSeries financial series Returns ------- float < 0 is bad > 1 is good > 1.5 is exceptionally good """ aggregated_series = get_aggregate_returns(qf_series, Frequency.MONTHLY, multi_index=True) negative_returns = aggregated_series.loc[aggregated_series < 0] negative_sum = np.abs(negative_returns.sum()) if negative_sum != 0: gain_to_pain = aggregated_series.sum() / negative_sum else: gain_to_pain = float("inf") return gain_to_pain
def information_ratio(portfolio: QFSeries, benchmark: QFSeries) -> float: """ The function calculates information ratio based on monthly returns. Return of higher frequency will be aggregated into monthly. """ portfolio_monthly = get_aggregate_returns(portfolio, Frequency.MONTHLY) benchmark_monthly = get_aggregate_returns(benchmark, Frequency.MONTHLY) portfolio_avg = portfolio_monthly.mean() benchmark_avg = benchmark_monthly.mean() excess_ret = portfolio_monthly - benchmark_monthly tracking_error = excess_ret.std() information_ratio_value = (portfolio_avg - benchmark_avg) / tracking_error return information_ratio_value
def capture_ratio(strategy: QFSeries, benchmark: QFSeries, upside_capture: bool, frequency: Frequency = Frequency.MONTHLY) -> float: """ Upside / Downside Capture Ratio This ratio is a statistical measure of a Fund Manager's overall performance in down or up markets. For example Downside Capture Ratio is used to evaluate how well or poorly the Manager performed relative to a specific index during periods when that index has dropped (had negative returns). The ratio is calculated by dividing the Strategy returns by the returns of the index during the down-market Downside Capture Ratio = sum(Strategy Returns) / sum(Index Returns) A Fund Manager who has a downside capture ratio less than 1 has outperformed the index during the down-market by falling less than the index. For instance, a ratio of 0.75 indicates that the portfolio declined only 75% as much as the index during the period under consideration. Parameters ---------- strategy: series of the strategy that will be evaluated benchmark: series of the benchmark for the strategy upside_capture: bool True - for upside capture ratio False - for downside capture ratio frequency: Frequency Frequency on which the the ratio is evaluated. For example Frequency.MONTHLY will result in evaluating the ration based on Monthly returns. Returns ------- float """ aggregated_strategy = get_aggregate_returns(strategy, frequency) aggregated_benchmark = get_aggregate_returns(benchmark, frequency) if upside_capture: selected_dates = aggregated_benchmark.loc[aggregated_benchmark > 0].index else: selected_dates = aggregated_benchmark.loc[aggregated_benchmark < 0].index selected_strategy_returns = aggregated_strategy.loc[selected_dates] selected_benchmark_returns = aggregated_benchmark.loc[selected_dates] benchmark_sum = selected_benchmark_returns.sum() if benchmark_sum != 0.0 and is_finite_number(benchmark_sum): return selected_strategy_returns.sum() / selected_benchmark_returns.sum() return float('nan')
def create_returns_distribution(returns: QFSeries, frequency: Frequency = Frequency.MONTHLY, title: str = None) -> \ HistogramChart: """ Creates a new returns distribution histogram with the specified frequency. Parameters ---------- returns: QFSeries The returns series to use in the histogram. frequency: Frequency frequency of the returns after aggregation title title of the chart Returns ------- HistogramChart A new ``HistogramChart`` instance. """ colors = Chart.get_axes_colors() aggregate_returns = get_aggregate_returns(returns, frequency, multi_index=True).multiply(100) chart = HistogramChart(aggregate_returns) # Format the x-axis so that its labels are shown as a percentage. x_axis_formatter = FormatStrFormatter("%.0f%%") axes_formatter_decorator = AxesFormatterDecorator(x_major=x_axis_formatter, key="axes_formatter") chart.add_decorator(axes_formatter_decorator) # Only show whole numbers on the y-axis. y_axis_locator = MaxNLocator(integer=True) axes_locator_decorator = AxesLocatorDecorator(y_major=y_axis_locator, key="axes_locator") chart.add_decorator(axes_locator_decorator) # Add an average line. avg_line = VerticalLineDecorator(aggregate_returns.values.mean(), color=colors[1], key="average_line_decorator", linestyle="--", alpha=0.8) chart.add_decorator(avg_line) # Add a legend. legend = LegendDecorator(key="legend_decorator") legend.add_entry(avg_line, "Mean") chart.add_decorator(legend) # Add a title. if title is None: title = "Distribution of " + str(frequency).capitalize() + " Returns" title = TitleDecorator(title, key="title_decorator") chart.add_decorator(title) chart.add_decorator(AxesLabelDecorator("Returns", "Occurrences")) return chart
def create_returns_bar_chart( returns: QFSeries, frequency: Frequency = Frequency.YEARLY) -> BarChart: """ Constructs a new returns bar chart based on the returns specified. By default a new annual returns bar chart will be created. """ colors = Chart.get_axes_colors() # Calculate data. aggregate_returns = get_aggregate_returns(returns, frequency, multi_index=True) data_series = QFSeries(aggregate_returns.sort_index(ascending=True)) chart = BarChart(Orientation.Horizontal, align="center") chart.add_decorator(DataElementDecorator(data_series)) chart.add_decorator(BarValuesDecorator(data_series)) # Format the x-axis so that its labels are shown as a percentage. chart.add_decorator(AxesFormatterDecorator(x_major=PercentageFormatter())) # Format Y axis to make sure we have a tick for each year or 2 years if len(data_series) > 10: y_labels = data_series[data_series.index % 2 == 1].index else: y_labels = data_series.index chart.add_decorator( AxisTickLabelsDecorator(labels=y_labels, axis=Axis.Y, tick_values=y_labels)) # Add an average line. avg_line = VerticalLineDecorator(aggregate_returns.values.mean(), color=colors[1], key="avg_line", linestyle="--", alpha=0.8) chart.add_decorator(avg_line) # Add a legend. legend = LegendDecorator(key="legend_decorator") legend.add_entry(avg_line, "Mean") chart.add_decorator(legend) # Add a title. title = TitleDecorator(str(frequency).capitalize() + " Returns", key="title_decorator") chart.add_decorator(title) chart.add_decorator(AxesLabelDecorator("Returns", "Year")) return chart
def import_data(frequency: Frequency, file_path: str): xlsx = ExcelImporter() df = xlsx.import_container(file_path, 'A1', 'J1888', sheet_name="Data", include_index=True, include_column_names=True) weights = xlsx.import_container(file_path, 'M15', 'N23', sheet_name="Data", include_index=True, include_column_names=False) simple_ret_df = DataFrame() for column in df: prices = PricesSeries(df[column]) simple_returns = get_aggregate_returns(prices, frequency) simple_ret_df[column] = simple_returns return simple_ret_df, weights
def create_returns_similarity(strategy: QFSeries, benchmark: QFSeries, mean_normalization: bool = True, std_normalization: bool = True, frequency: Frequency = None) -> KDEChart: """ Creates a new returns similarity chart. The frequency is determined by the specified returns series. Parameters ---------- strategy: QFSeries The strategy series to plot. benchmark: QFSeries The benchmark series to plot. mean_normalization: bool Whether to perform mean normalization on the series data. std_normalization: bool Whether to perform variance normalization on the series data. frequency: Frequency Returns can be aggregated in to specific frequency before plotting the chart Returns ------- KDEChart A newly created KDEChart instance. """ chart = KDEChart() colors = Chart.get_axes_colors() if frequency is not None: aggregate_strategy = get_aggregate_returns( strategy.to_simple_returns(), frequency) aggregate_benchmark = get_aggregate_returns( benchmark.to_simple_returns(), frequency) else: aggregate_strategy = strategy.to_simple_returns() aggregate_benchmark = benchmark.to_simple_returns() scaled_strategy = preprocessing.scale(aggregate_strategy, with_mean=mean_normalization, with_std=std_normalization) strategy_data_element = DataElementDecorator(scaled_strategy, bw="scott", shade=True, label=strategy.name, color=colors[0]) chart.add_decorator(strategy_data_element) scaled_benchmark = preprocessing.scale(aggregate_benchmark, with_mean=mean_normalization, with_std=std_normalization) benchmark_data_element = DataElementDecorator(scaled_benchmark, bw="scott", shade=True, label=benchmark.name, color=colors[1]) chart.add_decorator(benchmark_data_element) # Add a title. title = _get_title(mean_normalization, std_normalization, frequency) title_decorator = TitleDecorator(title, key="title") chart.add_decorator(title_decorator) chart.add_decorator(AxesLabelDecorator("Returns", "Similarity")) return chart
def agg_series_by_year(series): return get_aggregate_returns(series=series, convert_to=Frequency.YEARLY)
def _prepare_data_to_plot(self): simple_returns_tms = self.strategy_tms.to_simple_returns() annual_returns_tms = get_aggregate_returns(series=simple_returns_tms, convert_to=Frequency.YEARLY) return annual_returns_tms