feat(HPO): added run_hpo script (#237)

* feat(HPO): added `run_hpo` script

* fix(Linter): ran

* feat(HPO): removed any reference to sweep (superseeded by optuna)

* fix(HPO): optimize for sharpe

* fix(Config): removed glassnode data, save trials from hpo

* feat(Labelling): added three-balanced method works again

* fix(BetSizing): set the correct class labels

* fix(HPO): powerset should return what's expected, added two new normalization methods

* fix(Linter): ran

* fix(DataLoader): sort the dataframe when fetching data

* fix(Config): only take z-score of other assets
This commit is contained in:
Mark Aron Szulyovszky
2022-03-15 14:43:16 +01:00
committed by GitHub
parent 345b48a67c
commit b5ddee8dce
30 changed files with 126 additions and 115 deletions
+6 -4
View File
@@ -1,4 +1,3 @@
from sklearn.model_selection import TimeSeriesSplit
from .types import Config, RawConfig from .types import Config, RawConfig
from utils.helpers import flatten from utils.helpers import flatten
from feature_extractors.feature_extractor_presets import ( from feature_extractors.feature_extractor_presets import (
@@ -114,9 +113,12 @@ def __preprocess_data_collections_config(data_dict: dict) -> dict:
return data_dict return data_dict
def __preprocess_event_filter_config(data_dict: dict) -> dict: def __preprocess_event_filter_config(config_dict: dict) -> dict:
data_dict["event_filter"] = eventfilters_map[data_dict["event_filter"]] config_dict["event_filter"] = eventfilters_map[config_dict["event_filter"]](
return data_dict config_dict["event_filter_multiplier"]
)
config_dict.pop("event_filter_multiplier")
return config_dict
def __preprocess_event_labeller_config(config_dict: dict) -> dict: def __preprocess_event_labeller_config(config_dict: dict) -> dict:
+4 -3
View File
@@ -1,4 +1,4 @@
from .types import RawConfig, Config from .types import RawConfig
def get_default_config() -> RawConfig: def get_default_config() -> RawConfig:
@@ -22,14 +22,15 @@ def get_default_config() -> RawConfig:
assets=["fivemin_crypto"], assets=["fivemin_crypto"],
target_asset="BTCUSDT", target_asset="BTCUSDT",
other_assets=[], other_assets=[],
exogenous_data=["daily_glassnode"], exogenous_data=[],
load_non_target_asset=True, load_non_target_asset=True,
own_features=["level_2"], own_features=["level_2"],
other_features=["z_score"], other_features=["z_score"],
exogenous_features=["z_score"], exogenous_features=[],
directional_models=classification_models, directional_models=classification_models,
meta_models=meta_models, meta_models=meta_models,
event_filter="cusum_vol", event_filter="cusum_vol",
event_filter_multiplier=3.5,
remove_overlapping_events=False, remove_overlapping_events=False,
labeling="two_class", labeling="two_class",
forecasting_horizon=10, forecasting_horizon=10,
-44
View File
@@ -1,44 +0,0 @@
program: run_sweep.py
method: grid
project: price-forecasting
name: Meta labelling
metric:
goal: maximize
name: sharpe
parameters:
assets:
value: ['daily_crypto']
other_assets:
value: ['daily_etf']
exogenous_data:
value: ['daily_glassnode']
initial_window_size:
value: 380
distribution: categorical
n_features_to_select:
values: [40, 50, 60]
distribution: categorical
dimensionality_reduction_ratio:
value: 0.5
retrain_every:
value: 20
scaler:
value: 'minmax'
no_of_classes:
value: 'two'
load_non_target_asset:
value: True
directional_models:
distribution: categorical
values:
- ["LDA", "LogisticRegression_two_class", "KNN", "SVC", "CART", "NB", "AB", "RFC", "XGB_two_class", "LGBM", "StaticMom"]
- ["LogisticRegression_two_class", "LDA", "NB", "RFC", "XGB_two_class", "LGBM", "StaticMom"]
- ["LogisticRegression_two_class", "LDA", "LGBM", "RFC", "XGB_two_class"]
meta_models:
value: ["LGBM", "LogisticRegression_two_class"]
own_features:
value: ['date_days', 'level_2', 'lags_up_to_5']
other_features:
value: ['level_2', 'lags_up_to_5']
exogenous_features:
value: ['z_score']
+1
View File
@@ -26,6 +26,7 @@ class RawConfig(BaseModel):
other_features: list[str] other_features: list[str]
exogenous_features: list[str] exogenous_features: list[str]
event_filter: Literal["none", "cusum_vol", "cusum_fixed"] event_filter: Literal["none", "cusum_vol", "cusum_fixed"]
event_filter_multiplier: float
remove_overlapping_events: bool remove_overlapping_events: bool
labeling: Literal["two_class", "three_class_balanced", "three_class_imbalanced"] labeling: Literal["two_class", "three_class_balanced", "three_class_imbalanced"]
forecasting_horizon: int forecasting_horizon: int
+1
View File
@@ -25,6 +25,7 @@ dependencies:
- pytest - pytest
- wandb - wandb
- pandas-ta - pandas-ta
- optuna
- pip - pip
- pip: - pip:
- black - black
+6 -5
View File
@@ -7,7 +7,7 @@ from numba.typed import List
class CUSUMVolatilityEventFilter(EventFilter): class CUSUMVolatilityEventFilter(EventFilter):
def __init__(self, vol_period: int, multiplier: float): def __init__(self, multiplier: float, vol_period=100):
self.vol_period = vol_period self.vol_period = vol_period
self.multiplier = multiplier self.multiplier = multiplier
@@ -23,13 +23,14 @@ class CUSUMVolatilityEventFilter(EventFilter):
class CUSUMFixedEventFilter(EventFilter): class CUSUMFixedEventFilter(EventFilter):
def __init__(self, threshold: float): def __init__(self, threshold_multiplier: float):
self.threshold = threshold self.threshold_multiplier = threshold_multiplier
def get_event_start_times(self, returns: ReturnSeries) -> pd.DatetimeIndex: def get_event_start_times(self, returns: ReturnSeries) -> pd.DatetimeIndex:
diffed_returns = returns.diff() diffed_returns = returns.diff()
int_indicies = _process_fixed( int_indicies = _process(
List(diffed_returns.to_list()), abs(returns.mean()) * self.threshold List(diffed_returns.to_list()),
abs(returns.mean()) * self.threshold_multiplier,
) )
return pd.DatetimeIndex([returns.index[i] for i in int_indicies]) return pd.DatetimeIndex([returns.index[i] for i in int_indicies])
+3
View File
@@ -4,5 +4,8 @@ import pandas as pd
class NoEventFilter(EventFilter): class NoEventFilter(EventFilter):
def __init__(self, ignored_threshold_multiplier: float):
pass
def get_event_start_times(self, returns: ReturnSeries) -> pd.DatetimeIndex: def get_event_start_times(self, returns: ReturnSeries) -> pd.DatetimeIndex:
return returns.index return returns.index
+3 -3
View File
@@ -2,7 +2,7 @@ from .event_filters.nofilter import NoEventFilter
from .event_filters.cusum import CUSUMVolatilityEventFilter, CUSUMFixedEventFilter from .event_filters.cusum import CUSUMVolatilityEventFilter, CUSUMFixedEventFilter
eventfilters_map = dict( eventfilters_map = dict(
none=NoEventFilter(), none=NoEventFilter,
cusum_vol=CUSUMVolatilityEventFilter(vol_period=100, multiplier=3.5), cusum_vol=CUSUMVolatilityEventFilter,
cusum_fixed=CUSUMFixedEventFilter(threshold=20), cusum_fixed=CUSUMFixedEventFilter,
) )
@@ -1,7 +1,7 @@
from data_loader.types import ReturnSeries from data_loader.types import ReturnSeries
from ..types import EventLabeller, EventsDataFrame from ..types import EventLabeller, EventsDataFrame
import pandas as pd import pandas as pd
from .utils import create_forward_returns from .utils import create_forward_returns, discretize_threeway_threshold
from typing import Callable from typing import Callable
@@ -58,4 +58,4 @@ class FixedTimeHorionThreeClassBalancedEventLabeller(EventLabeller):
return [-1, 0, 1] return [-1, 0, 1]
def get_discretize_function(self) -> Callable: def get_discretize_function(self) -> Callable:
raise NotImplementedError return discretize_threeway_threshold(0.02)
@@ -57,4 +57,4 @@ class FixedTimeHorionThreeClassImbalancedEventLabeller(EventLabeller):
return [-1, 0, 1] return [-1, 0, 1]
def get_discretize_function(self) -> Callable: def get_discretize_function(self) -> Callable:
raise NotImplementedError return discretize_threeway_threshold(0.02)
-5
View File
@@ -11,7 +11,6 @@ def report_results(
output_weights: WeightsSeries, output_weights: WeightsSeries,
config: Config, config: Config,
wandb, wandb,
sweep: bool,
): ):
# Only send the results of the final model to wandb # Only send the results of the final model to wandb
@@ -44,7 +43,3 @@ def report_results(
"Mean Probabilistic Sharpe ratio for Level-2 (Ensemble) models: ", "Mean Probabilistic Sharpe ratio for Level-2 (Ensemble) models: ",
output_stats["prob_sharpe"], output_stats["prob_sharpe"],
) )
if sweep:
if wandb.run is not None:
wandb.finish()
+1 -9
View File
@@ -1,13 +1,9 @@
import pandas as pd
from config.types import RawConfig from config.types import RawConfig
from typing import Optional from typing import Optional
from utils.helpers import weighted_average
from training.types import Stats from training.types import Stats
def launch_wandb( def launch_wandb(project_name: str, default_config: RawConfig) -> Optional[object]:
project_name: str, default_config: RawConfig, sweep: bool = False
) -> Optional[object]:
from wandb_setup import get_wandb from wandb_setup import get_wandb
wandb = get_wandb() wandb = get_wandb()
@@ -15,10 +11,6 @@ def launch_wandb(
raise Exception( raise Exception(
"Wandb can not be initalized, the environment variable WANDB_API_KEY is missing (can also use .env file)" "Wandb can not be initalized, the environment variable WANDB_API_KEY is missing (can also use .env file)"
) )
elif sweep:
wandb.init(project=project_name, config=vars(default_config))
return wandb
else: else:
wandb.init(project=project_name, config=vars(default_config), reinit=True) wandb.init(project=project_name, config=vars(default_config), reinit=True)
return wandb return wandb
-1
View File
@@ -8,7 +8,6 @@ for index in range(6):
_, _, _, _, results_1, predictions_1, _ = run_pipeline( _, _, _, _, results_1, predictions_1, _ = run_pipeline(
project_name="price-prediction", project_name="price-prediction",
with_wandb=False, with_wandb=False,
sweep=False,
config=get_default_ensemble_config(), config=get_default_ensemble_config(),
) )
all_results.append(results_1) all_results.append(results_1)
+39
View File
@@ -0,0 +1,39 @@
from config.types import RawConfig
from run_pipeline import run_pipeline
from config.presets import get_default_config
import optuna
from utils.helpers import powerset
config = get_default_config()
config.save_models = False
def train(trial):
search_space = {
"n_features_to_select": trial.suggest_int("n_features_to_select", 10, 200),
"forecasting_horizon": trial.suggest_int("forecasting_horizon", 3, 300),
"own_features": trial.suggest_categorical(
"own_features", powerset(["z_score", "level_2", "level_1"])
),
"other_features": trial.suggest_categorical(
"other_features", powerset(["z_score", "level_2", "level_1"])
),
"labeling": trial.suggest_categorical(
"labeling", ["two_class", "three_class_balanced", "three_class_imbalanced"]
),
"scaler": trial.suggest_categorical(
"scaler",
["normalize", "minmax", "standardize", "robust", "box-cox", "quantile"],
),
}
trial_config = RawConfig(**vars(config) | search_space)
outcome, _ = run_pipeline(
project_name="test", with_wandb=False, raw_config=trial_config
)
return 1 - outcome.get_output_stats()["sharpe"]
study = optuna.create_study()
study.optimize(train, n_trials=40)
print(study.best_params)
study.trials_dataframe().to_csv("output/trials.csv")
-1
View File
@@ -20,7 +20,6 @@ def run_inference(preload_models: bool, fallback_raw_config: RawConfig):
pipeline_outcome, config = run_pipeline( pipeline_outcome, config = run_pipeline(
project_name="price-prediction", project_name="price-prediction",
with_wandb=False, with_wandb=False,
sweep=False,
raw_config=fallback_raw_config, raw_config=fallback_raw_config,
) )
-1
View File
@@ -5,6 +5,5 @@ from config.presets import get_dev_config
run_pipeline( run_pipeline(
project_name="price-prediction", project_name="price-prediction",
with_wandb=False, with_wandb=False,
sweep=False,
raw_config=get_dev_config(), raw_config=get_dev_config(),
) )
+4 -6
View File
@@ -6,13 +6,13 @@ from config.presets import get_default_config
def run_multi_asset_pipeline( def run_multi_asset_pipeline(
project_name: str, with_wandb: bool, sweep: bool, raw_config: RawConfig project_name: str, with_wandb: bool, raw_config: RawConfig
): ):
collection = data_collections["fivemin_crypto"] collection = data_collections["fivemin_crypto"]
for asset in collection: for asset in collection:
print(f"# Predicting asset: {asset[1]}\n") print(f"# Predicting asset: {asset.file_name}\n")
raw_config.target_asset = asset[1] raw_config.target_asset = asset.file_name
wandb, config = setup_config(project_name, with_wandb, sweep, raw_config) wandb, config = setup_config(project_name, with_wandb, raw_config)
pipeline_outcome = run_training(config) pipeline_outcome = run_training(config)
report_results( report_results(
pipeline_outcome.directional_training.training.stats, pipeline_outcome.directional_training.training.stats,
@@ -20,7 +20,6 @@ def run_multi_asset_pipeline(
pipeline_outcome.get_output_weights(), pipeline_outcome.get_output_weights(),
config, config,
wandb, wandb,
sweep,
) )
@@ -28,6 +27,5 @@ if __name__ == "__main__":
run_multi_asset_pipeline( run_multi_asset_pipeline(
project_name="price-prediction", project_name="price-prediction",
with_wandb=False, with_wandb=False,
sweep=False,
raw_config=get_default_config(), raw_config=get_default_config(),
) )
+10 -14
View File
@@ -19,31 +19,28 @@ from training.types import PipelineOutcome
def run_pipeline( def run_pipeline(
project_name: str, with_wandb: bool, sweep: bool, raw_config: RawConfig project_name: str, with_wandb: bool, raw_config: RawConfig
) -> tuple[PipelineOutcome, Config]: ) -> tuple[PipelineOutcome, Config]:
wandb, config = setup_config(project_name, with_wandb, sweep, raw_config) wandb, config = setup_config(project_name, with_wandb, raw_config)
pipeline_outcome = run_training(config) outcome = run_training(config)
report_results( report_results(
pipeline_outcome.directional_training.stats, outcome.directional_training.stats,
pipeline_outcome.get_output_stats(), outcome.get_output_stats(),
pipeline_outcome.get_output_weights(), outcome.get_output_weights(),
config, config,
wandb, wandb,
sweep,
) )
if config.save_models: if config.save_models:
save_models(pipeline_outcome, config) save_models(outcome, config)
return pipeline_outcome, config return outcome, config
def setup_config( def setup_config(
project_name: str, with_wandb: bool, sweep: bool, raw_config: RawConfig project_name: str, with_wandb: bool, raw_config: RawConfig
) -> tuple[Optional[object], Config]: ) -> tuple[Optional[object], Config]:
wandb = None wandb = None
if with_wandb: if with_wandb:
wandb = launch_wandb( wandb = launch_wandb(project_name=project_name, default_config=raw_config)
project_name=project_name, default_config=raw_config, sweep=sweep
)
raw_config = override_config_with_wandb_values(wandb, raw_config) raw_config = override_config_with_wandb_values(wandb, raw_config)
config = preprocess_config(raw_config) config = preprocess_config(raw_config)
@@ -108,6 +105,5 @@ if __name__ == "__main__":
run_pipeline( run_pipeline(
project_name="price-prediction", project_name="price-prediction",
with_wandb=False, with_wandb=False,
sweep=False,
raw_config=get_default_config(), raw_config=get_default_config(),
) )
-9
View File
@@ -1,9 +0,0 @@
from run_pipeline import run_pipeline
from config.presets import get_default_ensemble_config
run_pipeline(
project_name="price-prediction",
with_wandb=True,
sweep=True,
raw_config=get_default_ensemble_config(),
)
+1
View File
@@ -84,6 +84,7 @@ def test_evaluation():
expanding_window=False, expanding_window=False,
window_size=window_length, window_size=window_length,
retrain_every=retrain_every, retrain_every=retrain_every,
class_labels=[0, 1],
from_index=None, from_index=None,
) )
+1
View File
@@ -80,6 +80,7 @@ def test_walk_forward_train_test():
expanding_window=False, expanding_window=False,
window_size=window_length, window_size=window_length,
retrain_every=retrain_every, retrain_every=retrain_every,
class_labels=[0, 1],
from_index=None, from_index=None,
) )
+1
View File
@@ -66,6 +66,7 @@ def bet_sizing_with_meta_model(
retrain_every=config.retrain_every, retrain_every=config.retrain_every,
from_index=from_index, from_index=from_index,
level="meta", level="meta",
class_labels=[0, 1],
transformations_over_time=transformations_over_time, transformations_over_time=transformations_over_time,
model_over_time=preloaded_models, model_over_time=preloaded_models,
) )
+1
View File
@@ -48,6 +48,7 @@ def train_directional_model(
retrain_every=config.retrain_every, retrain_every=config.retrain_every,
from_index=from_index, from_index=from_index,
level="primary", level="primary",
class_labels=config.labeling.get_labels(),
transformations_over_time=transformations_over_time, transformations_over_time=transformations_over_time,
model_over_time=preloaded_training_step.model_over_time model_over_time=preloaded_training_step.model_over_time
if preloaded_training_step if preloaded_training_step
+2
View File
@@ -23,6 +23,7 @@ def train_model(
retrain_every: int, retrain_every: int,
from_index: Optional[pd.Timestamp], from_index: Optional[pd.Timestamp],
level: str, level: str,
class_labels: list[int],
transformations_over_time: TransformationsOverTime, transformations_over_time: TransformationsOverTime,
model_over_time: Optional[ModelOverTime], model_over_time: Optional[ModelOverTime],
) -> BaseTrainingOutcome: ) -> BaseTrainingOutcome:
@@ -61,6 +62,7 @@ def train_model(
expanding_window=True, expanding_window=True,
window_size=initial_window_size, window_size=initial_window_size,
retrain_every=retrain_every, retrain_every=retrain_every,
class_labels=class_labels,
from_index=from_index, from_index=from_index,
) )
+4 -1
View File
@@ -20,10 +20,13 @@ def walk_forward_inference(
expanding_window: bool, expanding_window: bool,
window_size: int, window_size: int,
retrain_every: int, retrain_every: int,
class_labels: list[int],
from_index: Optional[pd.Timestamp], from_index: Optional[pd.Timestamp],
) -> tuple[PredictionsSeries, ProbabilitiesDataFrame]: ) -> tuple[PredictionsSeries, ProbabilitiesDataFrame]:
predictions = pd.Series(index=X.index, dtype="object").rename(model_name) predictions = pd.Series(index=X.index, dtype="object").rename(model_name)
probabilities = pd.DataFrame(index=X.index) probabilities = pd.DataFrame(
index=X.index, columns=[str(label) for label in class_labels]
)
inference_from = ( inference_from = (
max( max(
+4 -1
View File
@@ -20,10 +20,13 @@ def walk_forward_inference_batched(
expanding_window: bool, expanding_window: bool,
window_size: int, window_size: int,
retrain_every: int, retrain_every: int,
class_labels: list[int],
from_index: Optional[pd.Timestamp], from_index: Optional[pd.Timestamp],
) -> tuple[PredictionsSeries, ProbabilitiesDataFrame]: ) -> tuple[PredictionsSeries, ProbabilitiesDataFrame]:
predictions = pd.Series(index=X.index, dtype="object").rename(model_name) predictions = pd.Series(index=X.index, dtype="object").rename(model_name)
probabilities = pd.DataFrame(index=X.index, columns=["0", "1"]) probabilities = pd.DataFrame(
index=X.index, columns=[str(label) for label in class_labels]
)
inference_from = ( inference_from = (
max( max(
+4 -1
View File
@@ -22,10 +22,13 @@ def walk_forward_inference(
expanding_window: bool, expanding_window: bool,
window_size: int, window_size: int,
retrain_every: int, retrain_every: int,
class_labels: list[int],
from_index: Optional[pd.Timestamp], from_index: Optional[pd.Timestamp],
) -> tuple[PredictionsSeries, ProbabilitiesDataFrame]: ) -> tuple[PredictionsSeries, ProbabilitiesDataFrame]:
predictions = pd.Series(index=X.index, dtype="object").rename(model_name) predictions = pd.Series(index=X.index, dtype="object").rename(model_name)
probabilities = pd.DataFrame(index=X.index) probabilities = pd.DataFrame(
index=X.index, columns=[str(label) for label in class_labels]
)
inference_from = ( inference_from = (
max( max(
+2 -1
View File
@@ -19,7 +19,8 @@ class PCATransformation(Transformation):
n_components=min( n_components=min(
int(len(X.columns) * self.ratio_components_to_keep), int(len(X.columns) * self.ratio_components_to_keep),
self.initial_window_size, self.initial_window_size,
) ),
# whiten=True,
) )
self.pca.fit(X, y) self.pca.fit(X, y)
+19 -2
View File
@@ -3,9 +3,18 @@ from .pca import PCATransformation
from .sklearn import SKLearnTransformation from .sklearn import SKLearnTransformation
from typing import Literal, Optional from typing import Literal, Optional
from models.model_map import default_feature_selector_classification from models.model_map import default_feature_selector_classification
from sklearn.preprocessing import MinMaxScaler, Normalizer, StandardScaler, RobustScaler from sklearn.preprocessing import (
MinMaxScaler,
Normalizer,
StandardScaler,
RobustScaler,
PowerTransformer,
QuantileTransformer,
)
ScalerTypes = Literal["normalize", "minmax", "standardize", "robust"] ScalerTypes = Literal[
"normalize", "minmax", "standardize", "robust", "box-cox", "quantile"
]
def get_rfe(n_feature_to_select: int) -> Optional[RFETransformation]: def get_rfe(n_feature_to_select: int) -> Optional[RFETransformation]:
@@ -42,5 +51,13 @@ def get_scaler(type: ScalerTypes) -> SKLearnTransformation:
return SKLearnTransformation( return SKLearnTransformation(
RobustScaler(with_centering=False, quantile_range=(0.10, 0.90)) RobustScaler(with_centering=False, quantile_range=(0.10, 0.90))
) )
elif type == "box-cox":
return SKLearnTransformation(
PowerTransformer(method="box-cox", standardize=True)
)
elif type == "quantile":
return SKLearnTransformation(
QuantileTransformer(n_quantiles=100, output_distribution="normal")
)
else: else:
raise Exception("Scaler type not supported") raise Exception("Scaler type not supported")
+6 -1
View File
@@ -3,7 +3,7 @@ import numpy as np
import os import os
import string import string
import random import random
from itertools import dropwhile from itertools import chain, combinations, dropwhile
def get_files_from_dir(path: str) -> list[str]: def get_files_from_dir(path: str) -> list[str]:
@@ -78,3 +78,8 @@ def drop_until_first_valid_index(
get_first_valid_return_index(series), get_first_valid_return_index(series),
) )
return df.iloc[first_valid_index:], series.iloc[first_valid_index:] return df.iloc[first_valid_index:], series.iloc[first_valid_index:]
def powerset(input: list) -> list[list]:
p_set = list(chain.from_iterable(combinations(input, r) for r in range(len(input) + 1))) # type: ignore
return [list(item) for item in p_set if len(item) > 0]