2021-12-23 17:06:22 +01:00
|
|
|
|
import warnings
|
|
|
|
|
|
import pandas as pd
|
|
|
|
|
|
import scipy.stats as ss
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
|
2021-12-23 17:06:22 +01:00
|
|
|
|
def timing_of_flattening_and_flips(target_positions: pd.Series) -> pd.DatetimeIndex:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Advances in Financial Machine Learning, Snippet 14.1, page 197
|
|
|
|
|
|
Derives the timestamps of flattening or flipping trades from a pandas series
|
|
|
|
|
|
of target positions. Can be used for position changes analysis, such as
|
|
|
|
|
|
frequency and balance of position changes.
|
|
|
|
|
|
Flattenings - times when open position is bing closed (final target position is 0).
|
|
|
|
|
|
Flips - times when positive position is reversed to negative and vice versa.
|
|
|
|
|
|
:param target_positions: (pd.Series) Target position series with timestamps as indices
|
|
|
|
|
|
:return: (pd.DatetimeIndex) Timestamps of trades flattening, flipping and last bet
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
empty_positions = target_positions[
|
|
|
|
|
|
(target_positions == 0)
|
|
|
|
|
|
].index # Empty positions index
|
|
|
|
|
|
previous_positions = target_positions.shift(
|
|
|
|
|
|
1
|
|
|
|
|
|
) # Timestamps pointing at previous positions
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
# Index of positions where previous one wasn't empty
|
|
|
|
|
|
previous_positions = previous_positions[(previous_positions != 0)].index
|
|
|
|
|
|
|
|
|
|
|
|
# FLATTENING - if previous position was open, but current is empty
|
|
|
|
|
|
flattening = empty_positions.intersection(previous_positions)
|
|
|
|
|
|
|
|
|
|
|
|
# Multiplies current position with value of next one
|
|
|
|
|
|
multiplied_posions = target_positions.iloc[1:] * target_positions.iloc[:-1].values
|
|
|
|
|
|
|
|
|
|
|
|
# FLIPS - if current position has another direction compared to the next
|
|
|
|
|
|
flips = multiplied_posions[(multiplied_posions < 0)].index
|
|
|
|
|
|
flips_and_flattenings = flattening.union(flips).sort_values()
|
2022-02-17 19:22:17 +01:00
|
|
|
|
if (
|
|
|
|
|
|
target_positions.index[-1] not in flips_and_flattenings
|
|
|
|
|
|
): # Appending with last bet
|
|
|
|
|
|
flips_and_flattenings = flips_and_flattenings.append(
|
|
|
|
|
|
target_positions.index[-1:]
|
|
|
|
|
|
)
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
return flips_and_flattenings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def average_holding_period(target_positions: pd.Series) -> float:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Advances in Financial Machine Learning, Snippet 14.2, page 197
|
|
|
|
|
|
Estimates the average holding period (in days) of a strategy, given a pandas series
|
|
|
|
|
|
of target positions using average entry time pairing algorithm.
|
|
|
|
|
|
Idea of an algorithm:
|
|
|
|
|
|
* entry_time = (previous_time * weight_of_previous_position + time_since_beginning_of_trade * increase_in_position )
|
|
|
|
|
|
/ weight_of_current_position
|
|
|
|
|
|
* holding_period ['holding_time' = time a position was held, 'weight' = weight of position closed]
|
|
|
|
|
|
* res = weighted average time a trade was held
|
|
|
|
|
|
:param target_positions: (pd.Series) Target position series with timestamps as indices
|
|
|
|
|
|
:return: (float) Estimated average holding period, NaN if zero or unpredicted
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
holding_period = pd.DataFrame(columns=["holding_time", "weight"])
|
2021-12-23 17:06:22 +01:00
|
|
|
|
entry_time = 0
|
|
|
|
|
|
position_difference = target_positions.diff()
|
|
|
|
|
|
|
|
|
|
|
|
# Time elapsed from the starting time for each position
|
2022-02-17 19:22:17 +01:00
|
|
|
|
time_difference = (
|
|
|
|
|
|
target_positions.index - target_positions.index[0]
|
|
|
|
|
|
) / np.timedelta64(1, "D")
|
2021-12-23 17:06:22 +01:00
|
|
|
|
for i in range(1, target_positions.size):
|
|
|
|
|
|
|
|
|
|
|
|
# Increased or unchanged position
|
|
|
|
|
|
if float(position_difference.iloc[i] * target_positions.iloc[i - 1]) >= 0:
|
|
|
|
|
|
if float(target_positions.iloc[i]) != 0: # And not an empty position
|
2022-02-17 19:22:17 +01:00
|
|
|
|
entry_time = (
|
|
|
|
|
|
entry_time * target_positions.iloc[i - 1]
|
|
|
|
|
|
+ time_difference[i] * position_difference.iloc[i]
|
|
|
|
|
|
) / target_positions.iloc[i]
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
# Decreased
|
|
|
|
|
|
if float(position_difference.iloc[i] * target_positions.iloc[i - 1]) < 0:
|
|
|
|
|
|
hold_time = time_difference[i] - entry_time
|
|
|
|
|
|
|
|
|
|
|
|
# Flip of a position
|
|
|
|
|
|
if float(target_positions.iloc[i] * target_positions.iloc[i - 1]) < 0:
|
|
|
|
|
|
weight = abs(target_positions.iloc[i - 1])
|
2022-02-17 19:22:17 +01:00
|
|
|
|
holding_period.loc[
|
|
|
|
|
|
target_positions.index[i], ["holding_time", "weight"]
|
|
|
|
|
|
] = (hold_time, weight)
|
2021-12-23 17:06:22 +01:00
|
|
|
|
entry_time = time_difference[i] # Reset entry time
|
|
|
|
|
|
|
|
|
|
|
|
# Only a part of position is closed
|
|
|
|
|
|
else:
|
|
|
|
|
|
weight = abs(position_difference.iloc[i])
|
2022-02-17 19:22:17 +01:00
|
|
|
|
holding_period.loc[
|
|
|
|
|
|
target_positions.index[i], ["holding_time", "weight"]
|
|
|
|
|
|
] = (hold_time, weight)
|
|
|
|
|
|
|
|
|
|
|
|
if float(holding_period["weight"].sum()) > 0: # If there were closed trades at all
|
|
|
|
|
|
avg_holding_period = float(
|
|
|
|
|
|
(holding_period["holding_time"] * holding_period["weight"]).sum()
|
|
|
|
|
|
/ holding_period["weight"].sum()
|
|
|
|
|
|
)
|
2021-12-23 17:06:22 +01:00
|
|
|
|
else:
|
2022-02-17 19:22:17 +01:00
|
|
|
|
avg_holding_period = float("nan")
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
return avg_holding_period
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bets_concentration(returns: pd.Series) -> float:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Advances in Financial Machine Learning, Snippet 14.3, page 201
|
|
|
|
|
|
Derives the concentration of returns from given pd.Series of returns.
|
|
|
|
|
|
Algorithm is based on Herfindahl-Hirschman Index where return weights
|
|
|
|
|
|
are taken as an input.
|
|
|
|
|
|
:param returns: (pd.Series) Returns from bets
|
|
|
|
|
|
:return: (float) Concentration of returns (nan if less than 3 returns)
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
if returns.size <= 2:
|
2022-02-17 19:22:17 +01:00
|
|
|
|
return float("nan") # If less than 3 bets
|
2021-12-23 17:06:22 +01:00
|
|
|
|
weights = returns / returns.sum() # Weights of each bet
|
2022-02-17 19:22:17 +01:00
|
|
|
|
hhi = (weights**2).sum() # Herfindahl-Hirschman Index for weights
|
2021-12-23 17:06:22 +01:00
|
|
|
|
hhi = float((hhi - returns.size ** (-1)) / (1 - returns.size ** (-1)))
|
|
|
|
|
|
|
|
|
|
|
|
return hhi
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
def all_bets_concentration(returns: pd.Series, frequency: str = "M") -> tuple:
|
2021-12-23 17:06:22 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Advances in Financial Machine Learning, Snippet 14.3, page 201
|
|
|
|
|
|
Given a pd.Series of returns, derives concentration of positive returns, negative returns
|
|
|
|
|
|
and concentration of bets grouped by time intervals (daily, monthly etc.).
|
|
|
|
|
|
If after time grouping less than 3 observations, returns nan.
|
|
|
|
|
|
Properties or results:
|
|
|
|
|
|
* low positive_concentration ⇒ no right fat-tail of returns (desirable)
|
|
|
|
|
|
* low negative_concentration ⇒ no left fat-tail of returns (desirable)
|
|
|
|
|
|
* low time_concentration ⇒ bets are not concentrated in time, or are evenly concentrated (desirable)
|
|
|
|
|
|
* positive_concentration == 0 ⇔ returns are uniform
|
|
|
|
|
|
* positive_concentration == 1 ⇔ only one non-zero return exists
|
|
|
|
|
|
:param returns: (pd.Series) Returns from bets
|
|
|
|
|
|
:param frequency: (str) Desired time grouping frequency from pd.Grouper
|
|
|
|
|
|
:return: (tuple of floats) Concentration of positive, negative and time grouped concentrations
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# Concentration of positive returns per bet
|
|
|
|
|
|
positive_concentration = bets_concentration(returns[returns >= 0])
|
|
|
|
|
|
|
|
|
|
|
|
# Concentration of negative returns per bet
|
|
|
|
|
|
negative_concentration = bets_concentration(returns[returns < 0])
|
|
|
|
|
|
|
|
|
|
|
|
# Concentration of bets/time period (month by default)
|
2022-02-17 19:22:17 +01:00
|
|
|
|
time_concentration = bets_concentration(
|
|
|
|
|
|
returns.groupby(pd.Grouper(freq=frequency)).count()
|
|
|
|
|
|
)
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
return (positive_concentration, negative_concentration, time_concentration)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def drawdown_and_time_under_water(returns: pd.Series, dollars: bool = False) -> tuple:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Advances in Financial Machine Learning, Snippet 14.4, page 201
|
|
|
|
|
|
Calculates drawdowns and time under water for pd.Series of either relative price of a
|
|
|
|
|
|
portfolio or dollar price of a portfolio.
|
|
|
|
|
|
Intuitively, a drawdown is the maximum loss suffered by an investment between two consecutive high-watermarks.
|
|
|
|
|
|
The time under water is the time elapsed between an high watermark and the moment the PnL (profit and loss)
|
|
|
|
|
|
exceeds the previous maximum PnL. We also append the Time under water series with period from the last
|
|
|
|
|
|
high-watermark to the last return observed.
|
|
|
|
|
|
Return details:
|
|
|
|
|
|
* Drawdown series index is the time of a high watermark and the value of a
|
|
|
|
|
|
drawdown after it.
|
|
|
|
|
|
* Time under water index is the time of a high watermark and how much time
|
|
|
|
|
|
passed till the next high watermark in years. Also includes time between
|
|
|
|
|
|
the last high watermark and last observation in returns as the last element.
|
|
|
|
|
|
:param returns: (pd.Series) Returns from bets
|
|
|
|
|
|
:param dollars: (bool) Flag if given dollar performance and not returns.
|
|
|
|
|
|
If dollars, then drawdowns are in dollars, else as a %.
|
|
|
|
|
|
:return: (tuple of pd.Series) Series of drawdowns and time under water
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
frame = returns.to_frame("pnl")
|
|
|
|
|
|
frame["hwm"] = returns.expanding().max() # Adding high watermarks as column
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
# Grouped as min returns by high watermarks
|
2022-02-17 19:22:17 +01:00
|
|
|
|
high_watermarks = frame.groupby("hwm").min().reset_index()
|
|
|
|
|
|
high_watermarks.columns = ["hwm", "min"]
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
# Time high watermark occurred
|
2022-02-17 19:22:17 +01:00
|
|
|
|
high_watermarks.index = frame["hwm"].drop_duplicates(keep="first").index
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
# Picking ones that had a drawdown after high watermark
|
2022-02-17 19:22:17 +01:00
|
|
|
|
high_watermarks = high_watermarks[high_watermarks["hwm"] > high_watermarks["min"]]
|
2021-12-23 17:06:22 +01:00
|
|
|
|
if dollars:
|
2022-02-17 19:22:17 +01:00
|
|
|
|
drawdown = high_watermarks["hwm"] - high_watermarks["min"]
|
2021-12-23 17:06:22 +01:00
|
|
|
|
else:
|
2022-02-17 19:22:17 +01:00
|
|
|
|
drawdown = 1 - high_watermarks["min"] / high_watermarks["hwm"]
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
time_under_water = (
|
|
|
|
|
|
(high_watermarks.index[1:] - high_watermarks.index[:-1])
|
|
|
|
|
|
/ np.timedelta64(1, "Y")
|
|
|
|
|
|
).values
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
# Adding also period from last High watermark to last return observed.
|
2022-02-17 19:22:17 +01:00
|
|
|
|
time_under_water = np.append(
|
|
|
|
|
|
time_under_water,
|
|
|
|
|
|
(returns.index[-1] - high_watermarks.index[-1]) / np.timedelta64(1, "Y"),
|
|
|
|
|
|
)
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
time_under_water = pd.Series(time_under_water, index=high_watermarks.index)
|
|
|
|
|
|
|
|
|
|
|
|
return drawdown, time_under_water
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
def sharpe_ratio(
|
|
|
|
|
|
returns: pd.Series, entries_per_year: int = 252, risk_free_rate: float = 0
|
|
|
|
|
|
) -> float:
|
2021-12-23 17:06:22 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Calculates annualized Sharpe ratio for pd.Series of normal or log returns.
|
|
|
|
|
|
Risk_free_rate should be given for the same period the returns are given.
|
|
|
|
|
|
For example, if the input returns are observed in 3 months, the risk-free
|
|
|
|
|
|
rate given should be the 3-month risk-free rate.
|
|
|
|
|
|
:param returns: (pd.Series) Returns - normal or log
|
|
|
|
|
|
:param entries_per_year: (int) Times returns are recorded per year (252 by default)
|
|
|
|
|
|
:param risk_free_rate: (float) Risk-free rate (0 by default)
|
|
|
|
|
|
:return: (float) Annualized Sharpe ratio
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
sharpe_r = (
|
|
|
|
|
|
(returns.mean() - risk_free_rate)
|
|
|
|
|
|
/ returns.std()
|
|
|
|
|
|
* (entries_per_year) ** (1 / 2)
|
|
|
|
|
|
)
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
return sharpe_r
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
def information_ratio(
|
|
|
|
|
|
returns: pd.Series, benchmark: float = 0, entries_per_year: int = 252
|
|
|
|
|
|
) -> float:
|
2021-12-23 17:06:22 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Calculates annualized information ratio for pd.Series of normal or log returns.
|
|
|
|
|
|
Benchmark should be provided as a return for the same time period as that between
|
|
|
|
|
|
input returns. For example, for the daily observations it should be the
|
|
|
|
|
|
benchmark of daily returns.
|
|
|
|
|
|
It is the annualized ratio between the average excess return and the tracking error.
|
|
|
|
|
|
The excess return is measured as the portfolio’s return in excess of the benchmark’s
|
|
|
|
|
|
return. The tracking error is estimated as the standard deviation of the excess returns.
|
|
|
|
|
|
:param returns: (pd.Series) Returns - normal or log
|
|
|
|
|
|
:param benchmark: (float) Benchmark for performance comparison (0 by default)
|
|
|
|
|
|
:param entries_per_year: (int) Times returns are recorded per year (252 by default)
|
|
|
|
|
|
:return: (float) Annualized information ratio
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
excess_returns = returns - benchmark
|
|
|
|
|
|
information_r = sharpe_ratio(excess_returns, entries_per_year)
|
|
|
|
|
|
|
|
|
|
|
|
return information_r
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
def probabilistic_sharpe_ratio(
|
|
|
|
|
|
observed_sr: float,
|
|
|
|
|
|
benchmark_sr: float,
|
|
|
|
|
|
number_of_returns: int,
|
|
|
|
|
|
skewness_of_returns: float = 0,
|
|
|
|
|
|
kurtosis_of_returns: float = 3,
|
|
|
|
|
|
) -> float:
|
2021-12-23 17:06:22 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Calculates the probabilistic Sharpe ratio (PSR) that provides an adjusted estimate of SR,
|
|
|
|
|
|
by removing the inflationary effect caused by short series with skewed and/or
|
|
|
|
|
|
fat-tailed returns.
|
|
|
|
|
|
Given a user-defined benchmark Sharpe ratio and an observed Sharpe ratio,
|
|
|
|
|
|
PSR estimates the probability that SR ̂is greater than a hypothetical SR.
|
|
|
|
|
|
- It should exceed 0.95, for the standard significance level of 5%.
|
|
|
|
|
|
- It can be computed on absolute or relative returns.
|
|
|
|
|
|
:param observed_sr: (float) Sharpe ratio that is observed
|
|
|
|
|
|
:param benchmark_sr: (float) Sharpe ratio to which observed_SR is tested against
|
|
|
|
|
|
:param number_of_returns: (int) Times returns are recorded for observed_SR
|
|
|
|
|
|
:param skewness_of_returns: (float) Skewness of returns (0 by default)
|
|
|
|
|
|
:param kurtosis_of_returns: (float) Kurtosis of returns (3 by default)
|
|
|
|
|
|
:return: (float) Probabilistic Sharpe ratio
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
test_value = ((observed_sr - benchmark_sr) * np.sqrt(number_of_returns - 1)) / (
|
|
|
|
|
|
(
|
|
|
|
|
|
1
|
|
|
|
|
|
- skewness_of_returns * observed_sr
|
|
|
|
|
|
+ (kurtosis_of_returns - 1) / 4 * observed_sr**2
|
|
|
|
|
|
)
|
|
|
|
|
|
** (1 / 2)
|
|
|
|
|
|
)
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
if np.isnan(test_value):
|
2022-02-17 19:22:17 +01:00
|
|
|
|
warnings.warn("Test value is nan. Please check the input values.", UserWarning)
|
2021-12-23 17:06:22 +01:00
|
|
|
|
return test_value
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(test_value, complex):
|
2022-02-17 19:22:17 +01:00
|
|
|
|
warnings.warn(
|
|
|
|
|
|
"Output is a complex number. You may want to check the input skewness (too high), "
|
|
|
|
|
|
"kurtosis (too low), or observed_sr values.",
|
|
|
|
|
|
UserWarning,
|
|
|
|
|
|
)
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
if np.isinf(test_value):
|
2022-02-17 19:22:17 +01:00
|
|
|
|
warnings.warn(
|
|
|
|
|
|
"Test value is infinite. You may want to check the input skewness, "
|
|
|
|
|
|
"kurtosis, or observed_sr values.",
|
|
|
|
|
|
UserWarning,
|
|
|
|
|
|
)
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
probab_sr = ss.norm.cdf(test_value)
|
|
|
|
|
|
|
|
|
|
|
|
return probab_sr
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
def deflated_sharpe_ratio(
|
|
|
|
|
|
observed_sr: float,
|
|
|
|
|
|
sr_estimates: list,
|
|
|
|
|
|
number_of_returns: int,
|
|
|
|
|
|
skewness_of_returns: float = 0,
|
|
|
|
|
|
kurtosis_of_returns: float = 3,
|
|
|
|
|
|
estimates_param: bool = False,
|
|
|
|
|
|
benchmark_out: bool = False,
|
|
|
|
|
|
) -> float:
|
2021-12-23 17:06:22 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Calculates the deflated Sharpe ratio (DSR) - a PSR where the rejection threshold is
|
|
|
|
|
|
adjusted to reflect the multiplicity of trials. DSR is estimated as PSR[SR∗], where
|
|
|
|
|
|
the benchmark Sharpe ratio, SR∗, is no longer user-defined, but calculated from
|
|
|
|
|
|
SR estimate trails.
|
|
|
|
|
|
DSR corrects SR for inflationary effects caused by non-Normal returns, track record
|
|
|
|
|
|
length, and multiple testing/selection bias.
|
|
|
|
|
|
- It should exceed 0.95, for the standard significance level of 5%.
|
|
|
|
|
|
- It can be computed on absolute or relative returns.
|
|
|
|
|
|
Function allows the calculated SR benchmark output and usage of only
|
|
|
|
|
|
standard deviation and number of SR trails instead of full list of trails.
|
|
|
|
|
|
:param observed_sr: (float) Sharpe ratio that is being tested
|
|
|
|
|
|
:param sr_estimates: (list) Sharpe ratios estimates trials list or
|
|
|
|
|
|
properties list: [Standard deviation of estimates, Number of estimates]
|
|
|
|
|
|
if estimates_param flag is set to True.
|
|
|
|
|
|
:param number_of_returns: (int) Times returns are recorded for observed_SR
|
|
|
|
|
|
:param skewness_of_returns: (float) Skewness of returns (0 by default)
|
|
|
|
|
|
:param kurtosis_of_returns: (float) Kurtosis of returns (3 by default)
|
|
|
|
|
|
:param estimates_param: (bool) Flag to use properties of estimates instead of full list
|
|
|
|
|
|
:param benchmark_out: (bool) Flag to output the calculated benchmark instead of DSR
|
|
|
|
|
|
:return: (float) Deflated Sharpe ratio or Benchmark SR (if benchmark_out)
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# Calculating benchmark_SR from the parameters of estimates
|
|
|
|
|
|
if estimates_param:
|
2022-02-17 19:22:17 +01:00
|
|
|
|
benchmark_sr = sr_estimates[0] * (
|
|
|
|
|
|
(1 - np.euler_gamma) * ss.norm.ppf(1 - 1 / sr_estimates[1])
|
|
|
|
|
|
+ np.euler_gamma * ss.norm.ppf(1 - 1 / sr_estimates[1] * np.e ** (-1))
|
|
|
|
|
|
)
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
# Calculating benchmark_SR from a list of estimates
|
|
|
|
|
|
else:
|
2022-02-17 19:22:17 +01:00
|
|
|
|
benchmark_sr = np.array(sr_estimates).std() * (
|
|
|
|
|
|
(1 - np.euler_gamma) * ss.norm.ppf(1 - 1 / len(sr_estimates))
|
|
|
|
|
|
+ np.euler_gamma * ss.norm.ppf(1 - 1 / len(sr_estimates) * np.e ** (-1))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
deflated_sr = probabilistic_sharpe_ratio(
|
|
|
|
|
|
observed_sr,
|
|
|
|
|
|
benchmark_sr,
|
|
|
|
|
|
number_of_returns,
|
|
|
|
|
|
skewness_of_returns,
|
|
|
|
|
|
kurtosis_of_returns,
|
|
|
|
|
|
)
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
|
|
|
|
|
if benchmark_out:
|
|
|
|
|
|
return benchmark_sr
|
|
|
|
|
|
|
|
|
|
|
|
return deflated_sr
|
|
|
|
|
|
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
def minimum_track_record_length(
|
|
|
|
|
|
observed_sr: float,
|
|
|
|
|
|
benchmark_sr: float,
|
|
|
|
|
|
skewness_of_returns: float = 0,
|
|
|
|
|
|
kurtosis_of_returns: float = 3,
|
|
|
|
|
|
alpha: float = 0.05,
|
|
|
|
|
|
) -> float:
|
2021-12-23 17:06:22 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Calculates the minimum track record length (MinTRL) - "How long should a track
|
|
|
|
|
|
record be in order to have statistical confidence that its Sharpe ratio is above
|
|
|
|
|
|
a given threshold?”
|
|
|
|
|
|
If a track record is shorter than MinTRL, we do not have enough confidence
|
|
|
|
|
|
that the observed Sharpe ratio ̂is above the designated Sharpe ratio threshold.
|
|
|
|
|
|
MinTRLis expressed in terms of number of observations, not annual or calendar terms.
|
|
|
|
|
|
:param observed_sr: (float) Sharpe ratio that is being tested
|
|
|
|
|
|
:param benchmark_sr: (float) Sharpe ratio to which observed_SR is tested against
|
|
|
|
|
|
:param number_of_returns: (int) Times returns are recorded for observed_SR
|
|
|
|
|
|
:param skewness_of_returns: (float) Skewness of returns (0 by default)
|
|
|
|
|
|
:param kurtosis_of_returns: (float) Kurtosis of returns (3 by default)
|
|
|
|
|
|
:param alpha: (float) Desired significance level (0.05 by default)
|
|
|
|
|
|
:return: (float) Minimum number of track records
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
track_rec_length = 1 + (
|
|
|
|
|
|
1
|
|
|
|
|
|
- skewness_of_returns * observed_sr
|
|
|
|
|
|
+ (kurtosis_of_returns - 1) / 4 * observed_sr**2
|
|
|
|
|
|
) * (ss.norm.ppf(1 - alpha) / (observed_sr - benchmark_sr)) ** (2)
|
2021-12-23 17:06:22 +01:00
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
return track_rec_length
|