Files
drift/utils/evaluate.py
T
Mark Aron Szulyovszky 9488e92597 feature(MetaLabeling): replaced previous non-functional Ensembling method with Meta-labeling method available for both lvl1 and lvl2 models (#110)
* feature(MetaLabeling): added hacky prototype

* fix(MetaLabeling): drop index until first valid X & y

* fix(MetaLabeling): transform both X & y before feature selection

* fix(MetaLabeling): got feature selection to work

* fix(MetaLabeling): correct values for meta_y

* feat(MetaLabeling): created predictions multiplied by bet sizes

* feat(Pipeline): print out averaged result

* fix(Evaluation): correctly deal with non-discretized data

* fix(Pipeline): use the right column names

* refactor(Pipeline): move out meta-labeling

* refactor(Pipeline): complete refactoring

* feat(CI): post results to PR

* fix(Pipeline): use the correct filename

* chore(Config): removed now redundant feature_selection flag

* feat(Models): added SVC

* fix(Pipeline): accidentally switched two return values

* feat(Sweep): prepared sweep_meta.yaml, moved report_results() into a separate file

* fix(Pipeline): wrong function name

* fix(Sweep): yaml + run_sweep

* fix(Sweep): typo in name

* fix(Reporting): only save averaged results

* feat(MetaLabeling): use optional meta-labeling step for every lvl1 models, before averaging

* feat(Reporting): print out sharpe improvement in meta-labeling step

* fix(Sweep): adjusted config, defaulted to good defaults

* fix(Sweep): adjusted sweep
2022-01-06 16:36:45 +01:00

128 lines
5.7 KiB
Python

from typing import Literal, Callable
from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score
from quantstats.stats import skew, sortino
from utils.metrics import probabilistic_sharpe_ratio, sharpe_ratio
from utils.helpers import get_first_valid_return_index
import pandas as pd
import numpy as np
def backtest(returns: pd.Series, signal: pd.Series, transaction_cost = 0.00) -> pd.Series:
delta_pos = signal.diff(1).abs().fillna(0.)
costs = transaction_cost * delta_pos
return (signal * returns) - costs
def __preprocess(target_returns: pd.Series, y_pred: pd.Series, y_true: pd.Series, method: Literal['classification', 'regression'], no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced'], discretize: bool) -> pd.DataFrame:
y_pred.name = 'y_pred'
target_returns.name = 'target_returns'
df = pd.concat([y_pred, target_returns],axis=1).dropna()
discretize_func = get_discretize_function(no_of_classes)
# make sure that we evaluate binary/three-way predictions even if the model is a regression
if method == 'regression':
df['sign_pred'] = df.y_pred.apply(discretize_func) if discretize else df.y_pred
df['sign_true'] = df.target_returns.apply(discretize_func)
else:
df['sign_pred'] = df.y_pred.apply(discretize_func) if discretize else df.y_pred
df['sign_true'] = y_true
df['result'] = backtest(df.target_returns, df.sign_pred)
return df
def evaluate_predictions(
model_name: str,
target_returns: pd.Series,
y_pred: pd.Series,
y_true: pd.Series,
method: Literal['classification', 'regression'],
no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced'],
discretize: bool = False,
) -> pd.Series:
# ignore the predictions until we see a non-zero returns (and definitely skip the first sliding_window_size)
first_nonzero_return = max(get_first_valid_return_index(target_returns), get_first_valid_return_index(y_pred))
evaluate_from = first_nonzero_return + 1
target_returns = pd.Series(target_returns[evaluate_from:])
if method == 'regression':
# if there are lots of zeros in the ground truth returns, probably something is wrong, but we can tolerate a couple of days of missing data.
is_zero = target_returns[target_returns == 0]
assert len(is_zero) < 15
y_pred = pd.Series(y_pred[evaluate_from:])
df = __preprocess(target_returns, y_pred, y_true, method, no_of_classes, discretize)
scorecard = pd.Series()
# we probably will not need regression models at all
# if method == 'regression':
# scorecard.loc['RSQ'] = r2_score(df.target_returns, df.y_pred)
# scorecard.loc['MAE'] = mean_absolute_error(df.target_returns, df.y_pred)
# elif method == 'classification':
# scorecard.loc['RSQ'] = 0.
# scorecard.loc['MAE Matrix'] = 0.
def count_non_zero(series: pd.Series) -> int:
return len(series[series != 0])
no_of_samples = count_non_zero(df.y_pred)
scorecard.loc['no_of_samples'] = no_of_samples
sharpe = sharpe_ratio(df.result)
scorecard.loc['sharpe'] = sharpe
benchmark_sharpe = sharpe_ratio(df.target_returns)
scorecard.loc['benchmark_sharpe'] = benchmark_sharpe
scorecard.loc['prob_sharpe'] = probabilistic_sharpe_ratio(sharpe, benchmark_sharpe, no_of_samples)
scorecard.loc['sortino'] = sortino(df.result)
scorecard.loc['skew'] = skew(df.result)
labels = [1, -1] if no_of_classes == 'two' else [1, -1, 0]
avg_type = 'weighted' if no_of_classes == 'two' else 'macro'
if discretize == True:
scorecard.loc['accuracy'] = accuracy_score(df.sign_true, df.sign_pred) * 100
scorecard.loc['recall'] = recall_score(df.sign_true, df.sign_pred, labels = labels, average=avg_type)
scorecard.loc['precision'] = precision_score(df.sign_true, df.sign_pred, labels = labels, average=avg_type)
scorecard.loc['f1_score'] = f1_score(df.sign_true, df.sign_pred, labels = labels, average=avg_type)
scorecard.loc['edge'] = df.result.mean()
scorecard.loc['noise'] = df.y_pred.diff().abs().mean()
scorecard.loc['edge_to_noise'] = scorecard.loc['edge'] / scorecard.loc['noise']
if discretize == True:
for index, row in df.sign_true.value_counts().iteritems():
scorecard.loc['sign_true_ratio_' + str(index)] = row / len(df.sign_true)
for index, row in df.sign_pred.value_counts().iteritems():
scorecard.loc['sign_pred_ratio_' + str(index)] = row / len(df.sign_pred)
# if method == 'regression':
# scorecard.loc['edge_to_mae'] = scorecard.loc['edge'] / scorecard.loc['MAE']
# elif method == 'classification':
# scorecard.loc['edge_to_mae'] = 0.
scorecard = scorecard.round(3)
print("Model name: ", model_name)
print(scorecard)
return scorecard
def __discretize_binary(x): return 1 if x > 0 else -1
def __discretize_threeway(x): return 0 if x == 0 else 1 if x > 0 else -1
def discretize_threeway_threshold(threshold: float) -> Callable:
def discretize(current_value):
lower_threshold = -threshold
upper_threshold = threshold
if np.isnan(current_value):
return np.nan
elif current_value <= lower_threshold:
return -1
elif current_value > lower_threshold and current_value < upper_threshold:
return 0
else:
return 1
return discretize
def get_discretize_function(no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced']) -> Callable:
return __discretize_binary if no_of_classes == 'two' else __discretize_threeway