Ejemplo n.º 1
0
    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()
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
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)
Ejemplo n.º 5
0
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
Ejemplo n.º 6
0
    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
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
 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
Ejemplo n.º 10
0
    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()
Ejemplo n.º 11
0
    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
Ejemplo n.º 12
0
    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))
Ejemplo n.º 13
0
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
Ejemplo n.º 14
0
    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()
Ejemplo n.º 15
0
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
Ejemplo n.º 16
0
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
Ejemplo n.º 17
0
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
Ejemplo n.º 18
0
 def __init__(self,
              series: QFSeries,
              frequency: Frequency = Frequency.DAILY):
     self.returns_tms = series.to_simple_returns()
     self.frequency = frequency