Untitled
unknown
plain_text
a year ago
18 kB
11
Indexable
import pandas as pd
import numpy as np
import warnings
import itertools
# Filter out the SettingWithCopyWarning
warnings.filterwarnings('ignore')
import matplotlib.pyplot as plt
from IPython.display import display
def max_dd(returns):
"""Determines the maximum drawdown of a strategy.
Args:
returns : (pandas.Series) Daily returns of the strategy, noncumulative.
Returns:
(float): Maximum drawdown.
"""
r = returns.add(1).cumprod()
dd = r.div(r.cummax()).sub(1)
mdd = dd.min()
end = dd.idxmin()
start = r.loc[:end].idxmax()
return {"Max Drawdown": round(mdd * 100, 2),
"Start": start.strftime("%m/%d/%Y"),
"End": end.strftime("%m/%d/%Y")}
def period_volatility(x, annualization_factor=252):
"""
Determines the volatility of a strategy.
Args:
x : (pandas.Series) Daily returns of the strategy, noncumulative.
annualization_factor : int(default 252 days), factor used for annualization of values (252 if data is daily,
52 if data is weekly,
12 if data is monthly,
4 of data is quarterly)
Returns:
(float) Volatility.
"""
return np.std(x) * (annualization_factor ** 0.5)
def period_downside_volatility(x, annualization_factor=252):
"""
Determines the downside volatility(using only negative returns) of a strategy.
Args:
x : (pandas.Series) Daily returns of the strategy, noncumulative.
annualization_factor : int(default 252 days), factor used for annualization of values (252 if data is daily,
52 if data is weekly,
12 if data is monthly,
4 of data is quarterly)
Returns:
(float) Downside volatility.
"""
return (np.std(x[x < 0]) * (annualization_factor ** 0.5))
def cumulative_return(x):
"""
Calculates the cumulative return of a strategy.
Args:
x : (pandas.Series) Daily returns of the strategy, noncumulative.
Returns:
(float) Cumulative return.
"""
return (np.prod(1 + x) - 1)
def period_return(x,
annualize=True,
annualization_factor=252):
"""
Determines return of a strategy.
Args:
x : (pandas.Series) Daily returns of the strategy, noncumulative.
annualize: boolean, False if annulization is not used, True otherwise
annualization_factor : int(default 252 days), factor used for annualization of values (252 if data is daily,
52 if data is weekly,
12 if data is monthly,
4 of data is quarterly)
Returns:
(float) Return
"""
if annualize:
ann_factor = len(x) / annualization_factor
return ((1 + cumulative_return(x)) ** (1 / ann_factor)) - 1
else:
return cumulative_return(x)
def period_sharpe_ratio(x,
annualize=True,
annualization_factor=252):
"""
Determines Sharpe ratio of a strategy.
Args:
x : (pandas.Series) Daily returns of the strategy, noncumulative.
annualize: boolean, False if annulization is not used, True otherwise
annualization_factor : int(default 252 days), factor used for annualization of values (252 if data is daily,
52 if data is weekly,
12 if data is monthly,
4 of data is quarterly)
Returns:
(float) Sharpe ratio
"""
a = period_return(x, annualize=annualize, annualization_factor=annualization_factor)
b = period_volatility(x, annualization_factor=annualization_factor)
return a / b
def sortino_ratio(x, annualize=True, annualization_factor=252):
"""
Determines Sortino ratio of a strategy.
Args:
x : (pandas.Series) Daily returns of the strategy, noncumulative.
annualize: boolean, False if annulization is not used, True otherwise
annualization_factor : int(default 252 days), factor used for annualization of values (252 if data is daily,
52 if data is weekly,
12 if data is monthly,
4 of data is quarterly)
Returns:
(float) Sortino ratio
"""
a = period_return(x, annualize=annualize, annualization_factor=annualization_factor)
b = period_downside_volatility(x, annualization_factor=annualization_factor)
return a / b
def calmar_ratio(x, annualize=True, annualization_factor=252):
"""
Determines Calmar ratio of a strategy.
Args:
x : (pandas.Series) Daily returns of the strategy, noncumulative.
annualize: boolean, False if annulization is not used, True otherwise
annualization_factor : int(default 252 days), factor used for annualization of values (252 if data is daily,
52 if data is weekly,
12 if data is monthly,
4 of data is quarterly)
Returns:
(float) Calmar ratio
"""
dd = max_dd(x)
dd = dd["Max Drawdown"]
if dd < 0:
a = period_return(x, annualize=annualize, annualization_factor=annualization_factor) * 100
b = abs(dd)
return a / b
else:
return np.nan
####
def iqr_indicator(x):
q3, q1 = np.percentile(x, [75, 25])
iqr = q3 - q1
return iqr
def qcd_indicator(x):
q3, q1 = np.percentile(x, [75, 25])
qcd = (q3 - q1) / (q3 + q1)
return qcd
def cv_indicator(x):
return np.std(x) / np.mean(x)
def generate_simulation_params_list(param_dict):
# Extract the keys and values from the dictionary
param_names = list(param_dict.keys())
param_values = list(param_dict.values())
# Generate all possible combinations of the parameter values
param_combinations = list(itertools.product(*param_values))
# Convert each combination into a dictionary with the corresponding parameter names
full_list = [dict(zip(param_names, combination)) for combination in param_combinations]
return full_list
def calculate_metrics(x):
return pd.Series({
'Sharpe': period_sharpe_ratio(x),
'CAGR': cumulative_return(x) * 100,
'Calmar': calmar_ratio(x),
'Sortino': sortino_ratio(x),
'MaxDD': max_dd(x)['Max Drawdown'],
'Vol': period_volatility(x) * 100
})
def simulation_results(backtest_results, list_params, eop_freq='Y', weekdays=False):
sim_key = '_'.join([str(x) for x in list_params.values()])
backtest_results['positions'] = backtest_results['quantity'].diff()
backtest_results['positions'].fillna(0, inplace=True)
backtest_results['signal_diff'] = backtest_results['signals'].ffill().fillna(0).astype(int).diff()
n_trades = pd.DataFrame(
backtest_results.groupby(pd.Grouper(freq='Y')).apply(lambda x: len(x[(x['signal_diff'] != 0)])))
n_trades.columns = ['trades']
n_trades['year'] = n_trades.index.year.astype(str)
n_trades['metric'] = f"EOY_Trades"
n_trades = n_trades.set_index(['metric', 'year'])[['trades']].T
n_trades.index = [sim_key]
n_trades[('ITD', 'Trades')] = n_trades.sum(axis=1)
backtest_results['buysell'] = np.where(backtest_results['positions'] > 0, 'BUY', 'SELL')
backtest_results['buysell'] = np.where(backtest_results['positions'] == 0, np.nan, backtest_results['buysell'])
backtest_results['return'] = backtest_results['total_nav'].pct_change()
log_returns = np.log(1 + backtest_results[['return']].copy())
log_returns_daily = log_returns.resample('D').sum()
log_returns_daily['return'] = np.exp(log_returns_daily['return']) - 1
if weekdays == True:
returns_daily = log_returns_daily.copy()
else:
returns_daily = log_returns_daily[log_returns_daily.index.dayofweek < 5]
monthly_returns = pd.DataFrame(
returns_daily.groupby(pd.Grouper(freq='1W', label='right')).apply(lambda x: period_return(x['return'],
annualize=False) * 100).rename(
'return'))
monthly_returns.index = pd.to_datetime(monthly_returns.index)
monthly_returns['month_year'] = monthly_returns.index.month_name().str[
:3] + '_' + monthly_returns.index.year.astype(str)
monthly_returns['metric'] = 'Monthly_RET'
monthly_returns = monthly_returns.set_index(['metric', 'month_year'])[['return']].T
monthly_returns.index = [sim_key]
# Apply the function to each group for period defined in config
metrics_table_eop = returns_daily.groupby(pd.Grouper(freq=eop_freq))['return'].apply(calculate_metrics).reset_index()
metrics_table_eop.columns = ['date', 'metric', 'measure']
metrics_table_eop['year'] = metrics_table_eop['date'].dt.year.astype(str) + '_' + metrics_table_eop['date'].dt.month_name().str[
:3]
metrics_table_eop['metric'] = metrics_table_eop['metric'].apply(lambda x: f"EOP_{x}")
metrics_table_eop = metrics_table_eop.set_index(['metric', 'year'])[['measure']].T
metrics_table_eop.index = [sim_key]
# Apply the function to each group
metrics_table_itd = pd.DataFrame(calculate_metrics(returns_daily['return']), columns=[sim_key]).T
metrics_table_itd.columns = pd.MultiIndex.from_product([['ITD'], metrics_table_itd.columns])
final_df = pd.concat([metrics_table_itd, metrics_table_eop, monthly_returns, n_trades], axis=1)
return final_df, returns_daily['return'].rename(sim_key)
def assign_rankings(simulation_df_results, include_itd =True):
ranking_rules = {'Sharpe': False, 'CAGR': False, 'Calmar': False, 'Sortino': False,
'MaxDD': True, 'Vol': True, 'Trades': False, 'HitRatio':False}
ranking_simulations = []
data = simulation_df_results.loc[:, ('Monthly_RET', slice(None))]
def pct_positive(row):
positive_values = row[row > 0].count()
total_values = row.count()
return (positive_values / total_values) * 100
# Applying the function to each row
data['hit_ratio_monret'] = data.apply(pct_positive, axis=1)
filtered_byhitratio = data[data['hit_ratio_monret'] >= 50]
filtered_byhitratio['monret_mean'] = filtered_byhitratio.loc[:, ('Monthly_RET', slice(None))].mean(axis=1)
filtered_by_mean = filtered_byhitratio[filtered_byhitratio['monret_mean'] > 0]
filtered_by_mean['IQR'] = filtered_by_mean.apply(iqr_indicator, axis=1).rank(ascending=True)
filtered_by_mean['QCD'] = filtered_by_mean.apply(qcd_indicator, axis=1).rank(ascending=True)
filtered_by_mean['CV'] = filtered_by_mean.apply(cv_indicator, axis=1).rank(ascending=True)
ranking_simulations.append(filtered_by_mean['IQR'])
ranking_simulations.append(filtered_by_mean['QCD'])
ranking_simulations.append(filtered_by_mean['CV'])
if include_itd:
for c in simulation_df_results.loc[filtered_by_mean.index, ('ITD', slice(None))].columns:
df_temp = simulation_df_results.loc[filtered_by_mean.index, c]
df_temp = df_temp.rank(ascending=ranking_rules[c[-1]])
print(df_temp.columns)
ranking_simulations.append(df_temp.rename(columns = {c:f"ITD_{c[-1]}"}))
cols_eoy = ['EOP_Sharpe', 'EOP_Vol', 'EOP_CAGR', 'EOP_Calmar', 'EOP_Sortino']
for c in cols_eoy:
name = c.split('_')[-1]
df_temp = simulation_df_results.loc[filtered_by_mean.index, (c, slice(None))]
df_temp[f'IQR_{name}'] = df_temp.apply(iqr_indicator, axis=1) * (1 if ranking_rules[name] else -1)
df_temp[f'Mean_{name}'] = df_temp.mean(axis=1)
df_temp[f"Rank_{name}"] = df_temp[[f'IQR_{name}', f'Mean_{name}']].apply(tuple, axis=1) \
.rank(method='dense', ascending=ranking_rules[name]).astype(int)
ranking_simulations.append(df_temp[f"Rank_{name}"])
df_temp = pd.DataFrame(filtered_by_mean.loc[:, 'hit_ratio_monret'])
df_temp['Rank_Hitratio']= df_temp['hit_ratio_monret'].rank(method='dense', ascending=ranking_rules['HitRatio']).astype(int)
ranking_simulations.append(df_temp["Rank_Hitratio"])
final_ranks=pd.concat(ranking_simulations, axis=1)
print(final_ranks)
#cols_to_rank =['IQR', 'QCD', 'CV', 'ITD_Sharpe', 'ITD_CAGR', 'ITD_Calmar', 'ITD_Sortino', 'ITD_MaxDD', 'ITD_Vol', 'ITD_Trades', 'Rank_Hitratio']
cols_to_rank = [
('ITD', 'Sharpe'), ('ITD', 'CAGR'), ('ITD', 'Calmar'), ('ITD', 'Sortino'),
('ITD', 'MaxDD'), ('ITD', 'Vol'), ('ITD', 'Trades'), 'IQR', 'QCD', 'CV', 'Rank_Hitratio'
]
final_ranks['Final_Rank'] = final_ranks[cols_to_rank].sum(axis=1)
return final_ranks
def selected_by_corr(allrets, top_N, negative_only=False):
if negative_only:
# Filter for negative returns only
allrets = allrets.applymap(lambda x: x if x < 0 else 0)
cor_matrix = allrets.corr()
mean_cor = cor_matrix.mean()
selected_models = mean_cor.nsmallest(top_N).index
return selected_models
def selected_by_cov(allrets, top_N, negative_only=False):
if negative_only:
# Filter for negative returns only
allrets = allrets.applymap(lambda x: x if x < 0 else 0)
cov_matrix = allrets.cov()
mean_cov = cov_matrix.mean()
selected_models = mean_cov.nsmallest(top_N).index
return selected_models
def equal_weight_ensemble(df):
num_models= len(df.columns)
weights = 1 / num_models
equally_weighted_returns = df.apply(lambda x: x * weights).sum(axis=1)
return equally_weighted_returns
def decorralated_portfolio(returns_all,rankings,top_rank=10,bottom_corr=3):
decorralated_models = selected_by_corr(returns_all[rankings.sort_values('Final_Rank').head(top_rank).index], bottom_corr)
return decorralated_models
def performance(
returns,
annualize=True,
annualization_factor=252,
color="brown",
label="LONG",
verbose=True,
plot=True):
"""Calculates basic performance measures on series of returns(Return, Volatility, Sharpe).
Args:
returns: (pandas Series), Daily(period) noncumulative returns of the strategy
annualize: boolean,False if annulization is not used, True otherwise
annualization_factor : int(default 252 days), factor used for annualization of values (252 if data is daily,
52 if data is weekly,
12 if data is monthly,
4 of data is quarterly)
color: the color used for the plot and printing if verbose=True
label: the name for the strategy in returns
verbose: (boolean) True if you want to print the output
plot: (boolean) True if you want to plot cumulative return of the strategy (returns)
Returns:
pandas DataFrame, plot or print of the calculated measures
"""
Volatility = np.round(period_volatility(returns, annualization_factor=annualization_factor) * 100, 2)
Return = np.round(period_return(returns, annualize=annualize, annualization_factor=annualization_factor) * 100, 2)
Sharpe = np.round(period_sharpe_ratio(returns, annualize=annualize, annualization_factor=annualization_factor), 2)
Sortino = np.round(sortino_ratio(returns, annualize=annualize, annualization_factor=annualization_factor), 2)
Calmar = np.round(calmar_ratio(returns, annualize=annualize, annualization_factor=annualization_factor), 2)
max_drawdown = max_dd(returns)
Max_DD = max_drawdown["Max Drawdown"]
start = max_drawdown["Start"]
end = max_drawdown["End"]
stat = {
"Return": [Return],
"Annualized Volatility": [Volatility],
"Sharpe": [Sharpe],
"Sortino ratio": [Sortino],
"Calmar ratio": [Calmar],
"Max Draw-Down": [Max_DD],
"MDD Start": [start],
"MDD End": [end]
}
df_output = pd.DataFrame(stat)
df_to_print = df_output.rename(index={0: label})
if verbose:
pd.options.display.notebook_repr_html = True
def color_b(s):
return np.where(s == label, f"background-color: {color};", "")
display(df_to_print.style.apply_index(color_b))
if plot:
start = returns.index.min()
ptf_cum = pd.Series([1], index=[start])
ptf_cum = pd.concat([ptf_cum, np.cumprod(returns[returns.index > start] + 1)])
ptf_cum.plot(figsize=(12, 5), color=color, label=label)
if label.startswith('_'):
pass
else:
plt.legend()
return df_to_print[["Return", "Annualized Volatility", "Sharpe",
"Sortino ratio", "Calmar ratio",
"Max Draw-Down"]]Editor is loading...
Leave a Comment