2021-12-14 21:25:43 +01:00
|
|
|
from typing import Literal
|
2021-12-15 21:11:12 +01:00
|
|
|
from sklearn.metrics import mean_absolute_error, accuracy_score, r2_score, f1_score, precision_score, recall_score
|
|
|
|
|
from quantstats.stats import expected_return, sharpe, skew, sortino
|
2021-12-17 14:32:17 +01:00
|
|
|
from utils.helpers import get_first_valid_return_index
|
2021-12-01 09:28:24 +01:00
|
|
|
import pandas as pd
|
2021-12-14 18:16:17 +01:00
|
|
|
import numpy as np
|
2021-12-01 09:28:24 +01:00
|
|
|
|
2021-12-17 17:41:50 +01:00
|
|
|
def backtest(returns: pd.Series, signal: pd.Series, transaction_cost = 0.00) -> pd.Series:
|
2021-12-15 21:11:12 +01:00
|
|
|
delta_pos = signal.diff(1).abs().fillna(0.)
|
|
|
|
|
costs = transaction_cost * delta_pos
|
|
|
|
|
return (signal * returns) - costs
|
2021-12-01 09:28:24 +01:00
|
|
|
|
|
|
|
|
|
2021-12-23 13:24:56 +01:00
|
|
|
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']) -> pd.DataFrame:
|
2021-12-14 21:25:43 +01:00
|
|
|
y_pred.name = 'y_pred'
|
2021-12-17 14:32:17 +01:00
|
|
|
target_returns.name = 'target_returns'
|
|
|
|
|
df = pd.concat([y_pred, target_returns],axis=1).dropna()
|
2021-12-14 21:25:43 +01:00
|
|
|
|
2021-12-23 13:24:56 +01:00
|
|
|
def categorize_binary(x): return 1 if x > 0 else -1
|
|
|
|
|
def categorize_threeway(x): return 0 if x == 0 else 1 if x > 0 else -1
|
|
|
|
|
categorize = categorize_binary if no_of_classes == 'two' else categorize_threeway
|
|
|
|
|
# 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(categorize)
|
|
|
|
|
df['sign_true'] = df.target_returns.apply(categorize)
|
|
|
|
|
else:
|
|
|
|
|
df['sign_pred'] = df.y_pred.apply(categorize)
|
|
|
|
|
df['sign_true'] = y_true
|
|
|
|
|
|
2021-12-17 14:32:17 +01:00
|
|
|
df['result'] = backtest(df.target_returns, df.sign_pred)
|
2021-12-15 21:11:12 +01:00
|
|
|
|
2021-12-14 21:25:43 +01:00
|
|
|
return df
|
|
|
|
|
|
2021-12-17 14:32:17 +01:00
|
|
|
def evaluate_predictions(
|
|
|
|
|
model_name: str,
|
|
|
|
|
target_returns: pd.Series,
|
|
|
|
|
y_pred: pd.Series,
|
2021-12-23 13:24:56 +01:00
|
|
|
y_true: pd.Series,
|
|
|
|
|
method: Literal['classification', 'regression'],
|
|
|
|
|
no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced'],
|
2021-12-17 14:32:17 +01:00
|
|
|
) -> pd.Series:
|
2021-12-15 21:11:12 +01:00
|
|
|
# ignore the predictions until we see a non-zero returns (and definitely skip the first sliding_window_size)
|
2021-12-17 17:41:50 +01:00
|
|
|
first_nonzero_return = max(get_first_valid_return_index(target_returns), get_first_valid_return_index(y_pred))
|
|
|
|
|
evaluate_from = first_nonzero_return + 1
|
2021-12-15 21:11:12 +01:00
|
|
|
|
2021-12-17 14:32:17 +01:00
|
|
|
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
|
2021-12-17 17:41:50 +01:00
|
|
|
|
2021-12-14 18:16:17 +01:00
|
|
|
y_pred = pd.Series(y_pred[evaluate_from:])
|
|
|
|
|
|
2021-12-23 13:24:56 +01:00
|
|
|
df = __preprocess(target_returns, y_pred, y_true, method, no_of_classes)
|
2021-12-14 18:16:17 +01:00
|
|
|
|
|
|
|
|
scorecard = pd.Series()
|
2021-12-14 21:25:43 +01:00
|
|
|
if method == 'regression':
|
2021-12-17 14:32:17 +01:00
|
|
|
scorecard.loc['RSQ'] = r2_score(df.target_returns, df.y_pred)
|
|
|
|
|
scorecard.loc['MAE'] = mean_absolute_error(df.target_returns, df.y_pred)
|
2021-12-14 21:25:43 +01:00
|
|
|
elif method == 'classification':
|
|
|
|
|
scorecard.loc['RSQ'] = 0.
|
|
|
|
|
scorecard.loc['MAE Matrix'] = 0.
|
2021-12-15 21:11:12 +01:00
|
|
|
sign_true = df.sign_true.astype(int)
|
|
|
|
|
sign_pred = df.sign_pred.astype(int)
|
|
|
|
|
|
2021-12-23 13:24:56 +01:00
|
|
|
def count_non_zero(series: pd.Series) -> int:
|
|
|
|
|
return len(series[series != 0])
|
|
|
|
|
scorecard.loc['no_of_samples'] = count_non_zero(y_pred[evaluate_from:])
|
2021-12-15 21:11:12 +01:00
|
|
|
scorecard.loc['sharpe'] = sharpe(df.result)
|
|
|
|
|
scorecard.loc['sortino'] = sortino(df.result)
|
|
|
|
|
scorecard.loc['skew'] = skew(df.result)
|
|
|
|
|
|
2021-12-23 13:24:56 +01:00
|
|
|
labels = [1, -1] if no_of_classes == 'two' else [1, -1, 0]
|
|
|
|
|
avg_type = 'weighted' if no_of_classes == 'two' else 'macro'
|
|
|
|
|
|
2021-12-15 21:11:12 +01:00
|
|
|
scorecard.loc['accuracy'] = accuracy_score(sign_true, sign_pred) * 100
|
2021-12-23 13:24:56 +01:00
|
|
|
scorecard.loc['recall'] = recall_score(sign_true, sign_pred, labels = labels, average=avg_type)
|
|
|
|
|
scorecard.loc['precision'] = precision_score(sign_true, sign_pred, labels = labels, average=avg_type)
|
|
|
|
|
scorecard.loc['f1_score'] = f1_score(sign_true, sign_pred, labels = labels, average=avg_type)
|
2021-12-14 18:16:17 +01:00
|
|
|
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']
|
2021-12-20 16:38:44 +01:00
|
|
|
|
|
|
|
|
for index, row in sign_true.value_counts().iteritems():
|
|
|
|
|
scorecard.loc['sign_true_ratio_' + str(index)] = row / len(sign_true)
|
|
|
|
|
|
|
|
|
|
for index, row in sign_pred.value_counts().iteritems():
|
|
|
|
|
scorecard.loc['sign_pred_ratio_' + str(index)] = row / len(sign_pred)
|
|
|
|
|
|
|
|
|
|
|
2021-12-14 21:25:43 +01:00
|
|
|
if method == 'regression':
|
|
|
|
|
scorecard.loc['edge_to_mae'] = scorecard.loc['edge'] / scorecard.loc['MAE']
|
|
|
|
|
elif method == 'classification':
|
|
|
|
|
scorecard.loc['edge_to_mae'] = 0.
|
|
|
|
|
|
2021-12-15 21:11:12 +01:00
|
|
|
scorecard = scorecard.round(3)
|
2021-12-14 18:16:17 +01:00
|
|
|
print("Model name: ", model_name)
|
|
|
|
|
print(scorecard)
|
|
|
|
|
return scorecard
|
|
|
|
|
|