2022-03-03 17:40:17 +01:00
|
|
|
from typing import Callable
|
2021-12-23 23:48:59 +01:00
|
|
|
from sklearn.metrics import accuracy_score, f1_score, precision_score, recall_score
|
2021-12-23 17:06:22 +01:00
|
|
|
from quantstats.stats import skew, sortino
|
2021-12-23 23:48:59 +01:00
|
|
|
from utils.metrics import probabilistic_sharpe_ratio, sharpe_ratio
|
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
|
2022-01-29 06:41:40 +01:00
|
|
|
from data_loader.types import ForwardReturnSeries, ySeries
|
|
|
|
|
from training.types import Stats, WeightsSeries
|
2021-12-01 09:28:24 +01:00
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
|
|
|
|
def backtest(
|
2022-03-03 17:40:17 +01:00
|
|
|
returns: pd.Series, signal: pd.Series, transaction_cost: float
|
2022-02-17 19:22:17 +01:00
|
|
|
) -> pd.Series:
|
|
|
|
|
delta_pos = signal.diff(1).abs().fillna(0.0)
|
2021-12-15 21:11:12 +01:00
|
|
|
costs = transaction_cost * delta_pos
|
|
|
|
|
return (signal * returns) - costs
|
2021-12-01 09:28:24 +01:00
|
|
|
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
def __preprocess(
|
|
|
|
|
forward_returns: ForwardReturnSeries,
|
|
|
|
|
y_pred: pd.Series,
|
|
|
|
|
y_true: pd.Series,
|
2022-03-03 17:40:17 +01:00
|
|
|
discretize_func: Callable,
|
|
|
|
|
transaction_costs: float,
|
2022-02-17 19:22:17 +01:00
|
|
|
) -> pd.DataFrame:
|
|
|
|
|
y_pred.name = "y_pred"
|
|
|
|
|
forward_returns.name = "forward_returns"
|
|
|
|
|
df = pd.concat([y_pred, forward_returns], axis=1).dropna()
|
2021-12-14 21:25:43 +01:00
|
|
|
|
2021-12-23 13:24:56 +01:00
|
|
|
# make sure that we evaluate binary/three-way predictions even if the model is a regression
|
2022-03-03 17:40:17 +01:00
|
|
|
df["sign_pred"] = df.y_pred.apply(discretize_func)
|
2022-02-17 19:22:17 +01:00
|
|
|
df["sign_true"] = y_true
|
2021-12-23 13:24:56 +01:00
|
|
|
|
2022-03-03 17:40:17 +01:00
|
|
|
df["result"] = backtest(df.forward_returns, df.y_pred, transaction_costs)
|
2021-12-15 21:11:12 +01:00
|
|
|
|
2021-12-14 21:25:43 +01:00
|
|
|
return df
|
|
|
|
|
|
2022-02-17 19:22:17 +01:00
|
|
|
|
2021-12-17 14:32:17 +01:00
|
|
|
def evaluate_predictions(
|
2022-02-17 19:22:17 +01:00
|
|
|
forward_returns: ForwardReturnSeries,
|
|
|
|
|
y_pred: WeightsSeries,
|
|
|
|
|
y_true: ySeries,
|
2022-03-03 17:40:17 +01:00
|
|
|
discretize_func: Callable,
|
|
|
|
|
labels: list[int],
|
|
|
|
|
transaction_costs: float,
|
2022-02-17 19:22:17 +01:00
|
|
|
) -> Stats:
|
2022-03-13 16:05:16 +01:00
|
|
|
# ignore the predictions until we see a non-zero returns (and definitely skip the first initial_window_size)
|
2022-02-17 19:22:17 +01:00
|
|
|
evaluate_from = max(
|
|
|
|
|
get_first_valid_return_index(forward_returns),
|
|
|
|
|
get_first_valid_return_index(y_pred),
|
|
|
|
|
)
|
|
|
|
|
|
2022-01-26 23:22:43 +01:00
|
|
|
forward_returns = pd.Series(forward_returns[evaluate_from:])
|
2021-12-14 18:16:17 +01:00
|
|
|
y_pred = pd.Series(y_pred[evaluate_from:])
|
|
|
|
|
|
2022-03-03 17:40:17 +01:00
|
|
|
df = __preprocess(
|
|
|
|
|
forward_returns, y_pred, y_true, discretize_func, transaction_costs
|
|
|
|
|
)
|
2022-02-17 19:22:17 +01:00
|
|
|
|
2022-01-29 06:41:40 +01:00
|
|
|
scorecard = dict()
|
2022-02-17 19:22:17 +01:00
|
|
|
|
2021-12-23 13:24:56 +01:00
|
|
|
def count_non_zero(series: pd.Series) -> int:
|
|
|
|
|
return len(series[series != 0])
|
2022-02-17 19:22:17 +01:00
|
|
|
|
2021-12-23 17:06:22 +01:00
|
|
|
no_of_samples = count_non_zero(df.y_pred)
|
2022-02-17 19:22:17 +01:00
|
|
|
scorecard["no_of_samples"] = no_of_samples
|
2022-02-17 16:36:35 +01:00
|
|
|
sharpe = sharpe_ratio(df.result + 1e-20)
|
2022-02-17 19:22:17 +01:00
|
|
|
scorecard["sharpe"] = sharpe
|
2022-01-26 23:22:43 +01:00
|
|
|
benchmark_sharpe = sharpe_ratio(df.forward_returns)
|
2022-02-17 19:22:17 +01:00
|
|
|
scorecard["benchmark_sharpe"] = benchmark_sharpe
|
|
|
|
|
scorecard["prob_sharpe"] = probabilistic_sharpe_ratio(
|
|
|
|
|
sharpe, benchmark_sharpe, no_of_samples
|
|
|
|
|
)
|
|
|
|
|
scorecard["sortino"] = sortino(df.result)
|
|
|
|
|
scorecard["skew"] = skew(df.result)
|
2021-12-15 21:11:12 +01:00
|
|
|
|
2022-03-03 17:40:17 +01:00
|
|
|
avg_type = "weighted" if len(labels) == 2 else "macro"
|
|
|
|
|
|
|
|
|
|
scorecard["accuracy"] = accuracy_score(df.sign_true, df.sign_pred) * 100
|
|
|
|
|
scorecard["recall"] = recall_score(
|
|
|
|
|
df.sign_true, df.sign_pred, labels=labels, average=avg_type
|
|
|
|
|
)
|
|
|
|
|
scorecard["precision"] = precision_score(
|
|
|
|
|
df.sign_true, df.sign_pred, labels=labels, average=avg_type
|
|
|
|
|
)
|
|
|
|
|
scorecard["f1_score"] = f1_score(
|
|
|
|
|
df.sign_true, df.sign_pred, labels=labels, average=avg_type
|
|
|
|
|
)
|
2022-02-17 19:22:17 +01:00
|
|
|
scorecard["edge"] = df.result.mean()
|
|
|
|
|
scorecard["noise"] = df.y_pred.diff().abs().mean()
|
|
|
|
|
scorecard["edge_to_noise"] = scorecard["edge"] / (scorecard["noise"] + 0.00001)
|
|
|
|
|
|
2022-03-03 17:40:17 +01:00
|
|
|
for index, row in df.sign_true.value_counts().iteritems():
|
|
|
|
|
scorecard["sign_true_ratio_" + str(index)] = row / len(df.sign_true)
|
2022-02-17 19:22:17 +01:00
|
|
|
|
2022-03-03 17:40:17 +01:00
|
|
|
for index, row in df.sign_pred.value_counts().iteritems():
|
|
|
|
|
scorecard["sign_pred_ratio_" + str(index)] = row / len(df.sign_pred)
|
2021-12-20 16:38:44 +01:00
|
|
|
|
2022-01-29 06:41:40 +01:00
|
|
|
scorecard = {k: round(float(v), 3) for k, v in scorecard.items()}
|
2022-02-17 19:22:17 +01:00
|
|
|
return scorecard
|