def __init__(self, benchmark_tms: QFSeries, strategy_tms: QFSeries): super().__init__() self.assert_is_qfseries(benchmark_tms) self.assert_is_qfseries(strategy_tms) self.benchmark_tms = benchmark_tms.to_simple_returns() self.strategy_tms = strategy_tms.to_simple_returns()
def __init__(self, benchmark_tms: QFSeries, strategy_tms: QFSeries, tail_plot=False, custom_title=False): super().__init__() self.assert_is_qfseries(benchmark_tms) self.assert_is_qfseries(strategy_tms) self.benchmark_tms = benchmark_tms.to_simple_returns() self.strategy_tms = strategy_tms.to_simple_returns() self.tail_plot = tail_plot self.custom_title = custom_title
def __init__(self, analysed_tms: QFSeries, regressors_df: QFDataFrame, frequency: Frequency, factors_identifier: FactorsIdentifier, is_fit_intercept: bool = True): """ Parameters ---------- analysed_tms must have a set name in order to be displayed properly later on regressors_df must have a set name for each column in order to be displayed properly later on frequency frequency of every series (the same for all) factors_identifier class used for identifying significant factors for the model (picks them up from regressors_df) is_fit_intercept default True; True if the calculated model should include the intercept coefficient """ self.logger = qf_logger.getChild(self.__class__.__name__) self.analysed_tms = analysed_tms.to_simple_returns() self.regressors_df = regressors_df.to_simple_returns() self.frequency = frequency self.factors_identifier = factors_identifier self.is_fit_intercept = is_fit_intercept self.used_regressors_ = None # data frame of regressors used in the model self.used_fund_returns_ = None # analysed timeseries without dates unused in the regression self.coefficients_vector_ = None # vector of coefficients for each regressor used in the model self.intercept_ = None # the independent term in a linear model
def cvar(qf_series: QFSeries, percentage: float) -> float: """ Calculates Conditional Value at Risk for a given percentage. Percentage equal to 0.05 means 5% CVaR. Parameters ---------- qf_series: QFSeries Series of returns/prices percentage: float Percentage defining CVaR (what percentage of worst-case scenarios should be considered" Returns ------- float Conditional value at risk as a number from range (-1,1). Simplifying: means how much money can be lost in the worst "percentage" % of all cases. """ returns_tms = qf_series.to_simple_returns() number_of_returns = len(returns_tms.values) tail_length = round(number_of_returns * percentage) assert tail_length > 0, 'Too few values in the series' sorted_returns = sorted(returns_tms.values) tail_returns = sorted_returns[:tail_length] return np.mean(tail_returns, dtype=np.float64)
def kelly(qf_series: QFSeries) -> float: """ Calculates the value of the Kelly Criterion (the fraction of money that should be invested) for the series of returns/prices. Kelly Criterion assumptions: 1. You trade the same way you traded in the past. 2. Each return corresponds to one trade. 3. Returns are normally distributed (calculated value will be close to the ideal kelly value even for highly skewed returns. Test showed that the difference of up to 10% (relative) might occur for extremely skewed distributions. Parameters ---------- qf_series: QFSeries timeseries of returns/prices. Each return/price must correspond to one trade. Returns ------- float fraction of money that should be invested """ # it is important to convert a series to simple returns and not log returns returns_tms = qf_series.to_simple_returns() # type: SimpleReturnsSeries mean = returns_tms.mean() variance = returns_tms.var() kelly_criterion_value = mean / variance return kelly_criterion_value
def get_factor_return_attribution(cls, fund_tms: QFSeries, fit_tms: QFSeries, regressors_df: QFDataFrame, coefficients: QFSeries, alpha: float) -> Tuple[QFSeries, float]: """ Returns performance attribution for each factor in given regressors and also calculates the unexplained return. """ fund_returns = fund_tms.to_simple_returns() regressors_returns = regressors_df.to_simple_returns() annualised_fund_return = cagr(fund_returns) annualised_fit_return = cagr(fit_tms) total_nav = fit_tms.to_prices(initial_price=1.0) def calc_factors_profit(series) -> float: factor_ret = regressors_returns.loc[:, series.name].values return coefficients.loc[series.name] * (total_nav[:-1].values * factor_ret).sum() factors_profits = regressors_returns.apply(calc_factors_profit) alpha_profit = total_nav[:-1].sum() * alpha total_profit = factors_profits.sum() + alpha_profit regressors_return_attribution = factors_profits * annualised_fit_return / total_profit regressors_return_attribution = cast_series( regressors_return_attribution, QFSeries) unexplained_return = annualised_fund_return - regressors_return_attribution.sum( ) return regressors_return_attribution, unexplained_return
def beta_and_alpha_full_stats( strategy_tms: QFSeries, benchmark_tms: QFSeries) -> Tuple[float, float, float, float, float]: """ Calculates alpha and beta of the series versus the benchmark series. Parameters ---------- strategy_tms Series of portfolio's returns/values benchmark_tms Series of benchmark returns/values Returns ------- beta beta coefficient for the linear fit alpha alpha coefficient for the linear fit (y = alpha * x + beta, where x is the benchmark return and y is the portfolio's return) r_value correlation coefficient. NOTE: this is not r_squared, r_squared = r_value**2 p_value two-sided p-value for a hypothesis test whose null hypothesis is that the slope is zero std_err standard error of the estimate """ strategy_tms = strategy_tms.to_simple_returns() benchmark_tms = benchmark_tms.to_simple_returns() from qf_lib.common.utils.dateutils.get_values_common_dates import get_values_for_common_dates strategy_tms, benchmark_tms = get_values_for_common_dates(strategy_tms, benchmark_tms, remove_nans=True) strategy_returns = strategy_tms.values benchmark_returns = benchmark_tms.values beta, alpha, r_value, p_value, std_err = stats.linregress( benchmark_returns, strategy_returns) return beta, alpha, r_value, p_value, std_err
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 __init__(self, series: QFSeries, frequency: Frequency = Frequency.DAILY): """ Parameters ---------- series series to be volatility managed frequency frequency of the series that is passed """ self.returns_tms = series.to_simple_returns() self.frequency = frequency
def __init__(self, returns_timeseries: QFSeries, frequency: Frequency): super().__init__() self.returns_tms = returns_timeseries.to_simple_returns() # type: SimpleReturnsSeries self.frequency = frequency self.start_date = self.returns_tms.first_valid_index() self.end_date = self.returns_tms.index[-1] # calculate statistics self._calculate_return() self._calculate_volatility() self._calculate_ratios() self._calculate_risk_stats() self._calculate_returns_stats()
def __init__(self, analysed_tms: QFSeries, regressors_df: QFDataFrame, frequency: Frequency, factors_identifier: FactorsIdentifier, is_fit_intercept: bool = True): self.logger = qf_logger.getChild(self.__class__.__name__) self.analysed_tms = analysed_tms.to_simple_returns() self.regressors_df = regressors_df.to_simple_returns() self.frequency = frequency self.factors_identifier = factors_identifier self.is_fit_intercept = is_fit_intercept self.used_regressors_ = None # data frame of regressors used in the model self.used_fund_returns_ = None # analysed timeseries without dates unused in the regression self.coefficients_vector_ = None # vector of coefficients for each regressor used in the model self.intercept_ = None # the independent term in a linear model
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(), fill_value=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, legend_subtitle) chart.add_decorator(legend) title_decorator = TitleDecorator(chart_title, key="title") chart.add_decorator(title_decorator) chart.add_decorator( AxesFormatterDecorator(y_major=PercentageFormatter(".0f"))) fill_decorator = FillBetweenDecorator(diff) chart.add_decorator(fill_decorator) self.document.add_element( ChartElement(chart, figsize=self.full_image_size, dpi=self.dpi))
def minTRL(returns_timeseries: QFSeries, target_sharpe_ratio: float = 1.0, confidence_level: float = 0.95) -> float: """ Computes the Minimum Track Record Length measure. The aim of computing is the possibility of answering the following question: 'How long should a track record be in order to have statistical confidence that its Sharpe ratio is above a given threshold?' The concept of Minimum Track Record Length is presented by Bailey and Prado in 'The Sharpe Ratio Efficient Frontier' """ returns_series = returns_timeseries.to_simple_returns() skewness = returns_series.skew() kurtosis = returns_series.kurt() sharpe_ratio_value = sharpe_ratio(returns_series, frequency=Frequency.DAILY) minTRL_value = 1 + ((1 - skewness * sharpe_ratio_value + (kurtosis - 1) / 4.0) * sharpe_ratio_value ** 2) * \ (stats.norm.ppf(confidence_level) / (sharpe_ratio_value - target_sharpe_ratio)) ** 2 return minTRL_value
def __init__(self, returns_timeseries: QFSeries, frequency: Frequency): """ Parameters ---------- returns_timeseries: QFSeries Analysed timeseries. It should be PriceSeries, SimpleReturnSeries or LogReturnSeries frequency: Frequency Corresponds to the frequency od data samples in the seres. """ super().__init__() self.returns_tms = returns_timeseries.to_simple_returns( ) # type: SimpleReturnsSeries self.frequency = frequency self.start_date = self.returns_tms.first_valid_index() self.end_date = self.returns_tms.index[-1] # calculate statistics self._calculate_return() self._calculate_volatility() self._calculate_ratios() self._calculate_risk_stats() self._calculate_returns_stats()
def create_skewness_chart(series: QFSeries, title: str = None) -> LineChart: """ Creates a new line chart showing the skewness of the distribution. It plots original series together with another series which contains sorted absolute value of the returns Parameters ---------- series ``QFSeries`` to plot on the chart. title title of the graph, specify ``None`` if you don't want the chart to show a title. Returns ------- The constructed ``LineChart``. """ original_price_series = series.to_prices(1) # Construct a series with returns sorted by their amplitude returns_series = series.to_simple_returns() abs_returns_series = returns_series.abs() returns_df = pd.concat([returns_series, abs_returns_series], axis=1, keys=['simple', 'abs']) sorted_returns_df = returns_df.sort_values(by='abs') skewness_series = SimpleReturnsSeries( index=returns_series.index, data=sorted_returns_df['simple'].values) skewed_price_series = skewness_series.to_prices(1) # Create a new Line Chart. line_chart = LineChart(start_x=series.index[0], rotate_x_axis=True) # Add original series to the chart original_series_element = DataElementDecorator(original_price_series) line_chart.add_decorator(original_series_element) skewed_series_element = DataElementDecorator(skewed_price_series) line_chart.add_decorator(skewed_series_element) # Add a point at the end point = (skewed_price_series.index[-1], skewed_price_series[-1]) point_emphasis = PointEmphasisDecorator(skewed_series_element, point, font_size=9) line_chart.add_decorator(point_emphasis) # Create a title. if title is not None: title_decorator = TitleDecorator(title, "title") line_chart.add_decorator(title_decorator) # Add a legend. legend_decorator = LegendDecorator(key='legend') legend_decorator.add_entry(original_series_element, 'Chronological returns') legend_decorator.add_entry(skewed_series_element, 'Returns sorted by magnitude') line_chart.add_decorator(legend_decorator) line_chart.add_decorator(AxesLabelDecorator(y_label="Profit/Loss")) return line_chart
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 get_aggregate_returns(series: QFSeries, convert_to: Frequency, multi_index: bool = False) -> SimpleReturnsSeries: """ Aggregates returns by week, month, or year. Parameters ---------- series Daily returns of the strategy, noncumulative. convert_to Can be 'weekly', 'monthly', or 'yearly'. multi_index Determines whether the grouping multi-index should be preserved. Returns ------- Aggregated returns. """ simple_rets = series.to_simple_returns() grouping = get_grouping_for_frequency(convert_to) # fix for grouping with multi-index (whenever a tuple is identifying a group. # Example: in weekly grouping a group could be identified by a tuple (2014, 52). Then the whole series would be # identified by a multi-level index (dates, dates) which is forbidden (names of levels must be unique). # Ideally each grouping would define names of the levels, e.g. (year, week) but I don't know simple_rets.index.name = None aggregated_series = simple_rets.groupby(grouping).apply( lambda rets: rets.total_cumulative_return()) aggregated_series = cast_series(aggregated_series, SimpleReturnsSeries) if not multi_index: # calculate a simple index based on the grouped MultiIndex if convert_to == Frequency.DAILY: # it is a day index = [ datetime(date[2], date[1], date[0]) for date in aggregated_series.index ] elif convert_to == Frequency.WEEKLY: # it is always Friday index = [ iso_to_gregorian(date[0], date[1], 5) for date in aggregated_series.index ] elif convert_to == Frequency.MONTHLY: # it is the end of the month index = [ datetime(date[0], date[1], monthrange(date[0], date[1])[1]) for date in aggregated_series.index ] elif convert_to == Frequency.YEARLY: # it is the end of the year index = [ datetime(year, 12, 31) for year in aggregated_series.index ] else: assert False aggregated_series = SimpleReturnsSeries(data=aggregated_series.values, index=DatetimeIndex(index)) aggregated_series.sort_index(inplace=True) aggregated_series.name = series.name return aggregated_series
def __init__(self, series: QFSeries, frequency: Frequency = Frequency.DAILY): self.returns_tms = series.to_simple_returns() self.frequency = frequency