feat(Events): added EventFilter, EventLabeller (#186)

This commit is contained in:
Mark Aron Szulyovszky
2022-01-26 23:22:43 +01:00
committed by GitHub
parent 1042c82333
commit 42a1bc59cb
65 changed files with 759 additions and 276571 deletions
+2
View File
@@ -137,3 +137,5 @@ lightning_logs/
results.csv
models/saved/
data/minute_crypto/**
+1 -1
View File
@@ -9,7 +9,7 @@ from pytorch_forecasting.metrics import QuantileLoss
import sys
sys.path.insert(0, '..')
from data_loader.load_data import load_files
from data_loader import load_files
print("success")
+1 -4
View File
@@ -1,5 +1,5 @@
from utils.types import DataCollection
from data_loader.types import DataCollection
def hash_data_config(data_config: dict) -> str:
@@ -12,10 +12,7 @@ def hash_data_config(data_config: dict) -> str:
hash_data_collection(data_config['exogenous_data']),
data_config['target_asset'][0] + data_config['target_asset'][1],
data_config['load_non_target_asset'],
data_config['log_returns'],
data_config['forecasting_horizon'],
hash_feature_extractors(data_config['own_features']),
hash_feature_extractors(data_config['other_features']),
hash_feature_extractors(data_config['exogenous_features']),
data_config['no_of_classes'],
]))
+18 -8
View File
@@ -1,16 +1,20 @@
from config.config import Config, RawConfig
from .types import Config, RawConfig
from utils.helpers import flatten
from feature_extractors.feature_extractor_presets import presets as feature_extractor_presets
from models.model_map import get_model_map
from models.model_map import get_model
from data_loader.collections import data_collections
from labeling.eventfilters_map import eventfilters_map
from labeling.labellers_map import labellers_map
def preprocess_config(raw_config: RawConfig) -> Config:
config_dict = vars(raw_config)
config_dict = __preprocess_model_config(config_dict)
config_dict = __preprocess_feature_extractors_config(config_dict)
config_dict = __preprocess_data_collections_config(config_dict)
config_dict = __preprocess_event_filter_config(config_dict)
config_dict = __preprocess_event_labeller_config(config_dict)
config_dict['no_of_classes'] = 'two'
config = Config(**config_dict)
validate_config(config)
return config
@@ -24,17 +28,15 @@ def __preprocess_feature_extractors_config(data_dict: dict) -> dict:
return data_dict
def __preprocess_model_config(model_config:dict) -> dict:
model_map = get_model_map(model_config)
model_config['primary_models'] = [(model_name, model_map['primary_models'][model_name]) for model_name in model_config['primary_models']]
model_config['primary_models'] = [(model_name, get_model(model_name)) for model_name in model_config['primary_models']]
if len(model_config['meta_labeling_models']) > 0:
model_config['meta_labeling_models'] = [(model_name, model_map['primary_models'][model_name]) for model_name in model_config['meta_labeling_models']]
model_config['meta_labeling_models'] = [(model_name, get_model(model_name)) for model_name in model_config['meta_labeling_models']]
if model_config['ensemble_model'] is not None:
model_config['ensemble_model'] = (model_config['ensemble_model'], model_map['ensemble_models'][model_config['ensemble_model']])
model_config['ensemble_model'] = (model_config['ensemble_model'], get_model(model_config['ensemble_model']))
return model_config
def __preprocess_data_collections_config(data_dict: dict) -> dict:
data_dict = data_dict.copy()
keys = ['assets', 'other_assets', 'exogenous_data']
for key in keys:
preset_names = data_dict[key]
@@ -44,6 +46,14 @@ def __preprocess_data_collections_config(data_dict: dict) -> dict:
data_dict['target_asset'] = target_asset
return data_dict
def __preprocess_event_filter_config(data_dict: dict) -> dict:
data_dict['event_filter'] = eventfilters_map[data_dict['event_filter']]
return data_dict
def __preprocess_event_labeller_config(data_dict: dict) -> dict:
data_dict['labeling'] = labellers_map[data_dict['labeling']]
return data_dict
def validate_config(config: Config):
# We need to make sure there's only one output from the pipeline
+13 -77
View File
@@ -1,68 +1,4 @@
from pydantic import BaseModel
from typing import Literal, Optional
from models.base import Model
from utils.types import DataCollection, DataSource, FeatureExtractor
# RawConfig is needed to ensure we can declare config presets here with static typing, we then convert it to Config
class RawConfig(BaseModel):
primary_models_meta_labeling: bool
dimensionality_reduction: bool
n_features_to_select: int
expanding_window_base: bool
expanding_window_meta_labeling: bool
sliding_window_size_base: int
sliding_window_size_meta_labeling: int
retrain_every: int
scaler: Literal['normalize', 'minmax', 'standardize']
assets: list[str]
target_asset: str
other_assets: list[str]
exogenous_data: list[str]
load_non_target_asset: bool
log_returns: bool
forecasting_horizon: int
own_features: list[str]
other_features: list[str]
exogenous_features: list[str]
no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced']
primary_models: list[str]
meta_labeling_models: list[str]
ensemble_model: Optional[str]
class Config(BaseModel):
primary_models_meta_labeling: bool
dimensionality_reduction: bool
n_features_to_select: int
expanding_window_base: bool
expanding_window_meta_labeling: bool
sliding_window_size_base: int
sliding_window_size_meta_labeling: int
retrain_every: int
scaler: Literal['normalize', 'minmax', 'standardize']
assets: DataCollection
target_asset: DataSource
other_assets: DataCollection
exogenous_data: DataCollection
load_non_target_asset: bool
log_returns: bool
forecasting_horizon: int
own_features: list[tuple[str, FeatureExtractor, list[int]]]
other_features: list[tuple[str, FeatureExtractor, list[int]]]
exogenous_features: list[tuple[str, FeatureExtractor, list[int]]]
no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced']
primary_models: list[tuple[str, Model]]
meta_labeling_models: list[tuple[str, Model]]
ensemble_model: Optional[tuple[str, Model]]
class Config:
arbitrary_types_allowed = True
from .types import RawConfig, Config
def get_dev_config() -> RawConfig:
@@ -85,16 +21,16 @@ def get_dev_config() -> RawConfig:
other_assets = [],
exogenous_data = [],
load_non_target_asset= True,
log_returns= True,
forecasting_horizon = 1,
own_features = ['level_2', 'date_days'],
other_features = ['single_mom'],
exogenous_features = ['z_score'],
no_of_classes= 'two',
primary_models = classification_models,
meta_labeling_models = [],
ensemble_model = None
ensemble_model = None,
event_filter = 'none',
labeling = 'two_class'
)
@@ -121,16 +57,16 @@ def get_default_ensemble_config() -> RawConfig:
other_assets = ['daily_etf'],
exogenous_data = ['daily_glassnode'],
load_non_target_asset= True,
log_returns= True,
forecasting_horizon = 1,
own_features = ['level_2', 'date_days', 'lags_up_to_5'],
other_features = ['level_2', 'lags_up_to_5'],
exogenous_features = ['z_score'],
no_of_classes= 'two',
primary_models = classification_models,
meta_labeling_models = meta_labeling_models,
ensemble_model = ensemble_model
ensemble_model = ensemble_model,
event_filter = 'cusum_vol',
labeling = 'two_class'
)
@@ -158,16 +94,16 @@ def get_lightweight_ensemble_config() -> RawConfig:
other_assets = ['daily_etf'],
exogenous_data = ['daily_glassnode'],
load_non_target_asset= True,
log_returns= True,
forecasting_horizon = 1,
own_features = ['level_2' ],
other_features = ['level_2'],
exogenous_features = ['z_score'],
no_of_classes= 'two',
primary_models = classification_models,
meta_labeling_models = meta_labeling_models,
ensemble_model = ensemble_model
ensemble_model = ensemble_model,
event_filter = 'none',
labeling = 'two_class'
)
-4
View File
@@ -34,12 +34,8 @@ parameters:
value: 'minmax'
no_of_classes:
value: 'two'
forecasting_horizon:
value: 1
load_non_target_asset:
value: True
log_returns:
value: True
primary_models:
distribution: categorical
values:
-4
View File
@@ -39,13 +39,9 @@ parameters:
no_of_classes:
values: ['two', 'three-balanced', 'three-imbalanced']
distribution: categorical
forecasting_horizon:
value: 1
load_non_target_asset:
values: [True, False]
distribution: categorical
log_returns:
value: True
primary_models:
value: ["LogisticRegression_two_class", "LDA", "KNN", "CART", "NB", "AB", "RFC", "StaticMom"]
meta_labeling_models:
-4
View File
@@ -34,12 +34,8 @@ parameters:
value: 'minmax'
no_of_classes:
value: 'two'
forecasting_horizon:
value: 1
load_non_target_asset:
value: True
log_returns:
value: True
primary_models:
values: [['LogisticRegression_two_class'], ['SVC'], ['LDA'], ['KNN'], ['CART'], ['MNB'], ['NB'], ['AB'], ['RFC'], ['XGB_two_class'], ['LGBM']]
distribution: categorical
+68
View File
@@ -0,0 +1,68 @@
from pydantic import BaseModel
from typing import Literal, Optional
from labeling.types import EventFilter
from models.base import Model
from data_loader.types import DataCollection, DataSource
from feature_extractors.types import FeatureExtractor, ScalerTypes
from labeling.types import EventFilter, EventLabeller
# RawConfig is needed to ensure we can declare config presets here with static typing, we then convert it to Config
class RawConfig(BaseModel):
primary_models_meta_labeling: bool
dimensionality_reduction: bool
n_features_to_select: int
expanding_window_base: bool
expanding_window_meta_labeling: bool
sliding_window_size_base: int
sliding_window_size_meta_labeling: int
retrain_every: int
scaler: Literal['normalize', 'minmax', 'standardize']
assets: list[str]
target_asset: str
other_assets: list[str]
exogenous_data: list[str]
load_non_target_asset: bool
own_features: list[str]
other_features: list[str]
exogenous_features: list[str]
event_filter: Literal['none', 'cusum_vol', 'cusum_fixed']
labeling: Literal['two_class', 'three_class_balanced', 'three_class_imbalanced']
primary_models: list[str]
meta_labeling_models: list[str]
ensemble_model: Optional[str]
class Config(BaseModel):
primary_models_meta_labeling: bool
dimensionality_reduction: bool
n_features_to_select: int
expanding_window_base: bool
expanding_window_meta_labeling: bool
sliding_window_size_base: int
sliding_window_size_meta_labeling: int
retrain_every: int
scaler: Literal['normalize', 'minmax', 'standardize']
assets: DataCollection
target_asset: DataSource
other_assets: DataCollection
exogenous_data: DataCollection
load_non_target_asset: bool
own_features: list[tuple[str, FeatureExtractor, list[int]]]
other_features: list[tuple[str, FeatureExtractor, list[int]]]
exogenous_features: list[tuple[str, FeatureExtractor, list[int]]]
event_filter: EventFilter
labeling: EventLabeller
no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced']
primary_models: list[tuple[str, Model]]
meta_labeling_models: list[tuple[str, Model]]
ensemble_model: Optional[tuple[str, Model]]
class Config:
arbitrary_types_allowed = True
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
from .collections import data_collections
from .load import load_data, load_only_returns
+3 -3
View File
@@ -1,4 +1,4 @@
from utils.types import Path, FileName, DataSource, DataCollection
from .types import Path, FileName, DataSource, DataCollection
from utils.helpers import flatten
def transform_to_data_collection(path: str, file_names: list[str]) -> DataCollection:
@@ -10,7 +10,7 @@ __daily_crypto = ["ADA_USD", "BCH_USD", "BNB_USD", "BTC_USD", "DOT_USD", "ETC_US
__daily_crypto_lightweight = ["ADA_USD", "BCH_USD"]
__hourly_crypto = ["BTC_USD", "DASH_USD", "ETC_USD", "ETH_USD", "LTC_USD", "TRX_USD", "XLM_USD", "XMR_USD", "XRP_USD"]
__minute_crypto = ["BTC_USD", "DASH_USD", "ETC_USD", "ETH_USD", "LTC_USD", "TRX_USD", "XLM_USD", "XMR_USD"]
__daily_glassnode = ['rhodl_ratio',
# 'cvdd',
@@ -45,7 +45,7 @@ data_collections = dict(
daily_crypto = transform_to_data_collection("data/daily_crypto", __daily_crypto),
daily_crypto_lightweight = transform_to_data_collection("data/daily_crypto", __daily_crypto_lightweight),
daily_etf = transform_to_data_collection("data/daily_etf", __daily_etf),
hourly_crypto = transform_to_data_collection("data/hourly_crypto", __hourly_crypto),
minute_crypto = transform_to_data_collection("data/minute_crypto", __minute_crypto),
daily_glassnode =transform_to_data_collection("data/daily_glassnode", __daily_glassnode),
)
+151
View File
@@ -0,0 +1,151 @@
import pandas as pd
import numpy as np
from .types import DataSource
from feature_extractors.types import FeatureExtractor
from utils.helpers import drop_columns_if_exist
from data_loader.collections import DataCollection
from typing import Literal
import ray
import os
from config.hashing import hash_data_config
from .types import XDataFrame, ReturnSeries, ForwardReturnSeries
from diskcache import Cache
cache = Cache(".cachedir/data")
def load_data(**kwargs) -> tuple[XDataFrame, ReturnSeries, ForwardReturnSeries]:
hashed = hash_data_config(kwargs)
if hashed in cache:
return cache.get(hashed)
else:
return_value = __load_data(**kwargs)
cache[hashed] = return_value
return return_value
def __load_data(assets: DataCollection,
other_assets: DataCollection,
exogenous_data: DataCollection,
target_asset: DataSource,
load_non_target_asset: bool,
own_features: list[tuple[str, FeatureExtractor, list[int]]],
other_features: list[tuple[str, FeatureExtractor, list[int]]],
exogenous_features: list[tuple[str, FeatureExtractor, list[int]]],
) -> tuple[XDataFrame, ReturnSeries, ForwardReturnSeries]:
"""
Loads asset data from the specified path.
Returns:
- DataFrame `X` with all the training data
- Series `returns` with only the returns
- Series `forward_returns` with the target asset returns shifted by 1 day
"""
target_file = [f for f in assets if f[1].startswith(target_asset[1])]
assert len(target_file) == 1, "There should be exactly one target file"
other_files = [f for f in assets if load_non_target_asset == True and f[1].startswith(target_asset[1]) == False]
files = other_files + other_assets
target_asset_future = [__load_df.remote(
data_source=data_source,
prefix=data_source[1],
returns='log_returns',
feature_extractors=own_features,
) for data_source in target_file]
target_asset_df = ray.get(target_asset_future)
target_asset_only_returns_future = __load_df.remote(
data_source=target_file[0],
prefix=target_file[0][1],
returns='returns',
feature_extractors=[],
)
df_target_asset_only_returns = ray.get(target_asset_only_returns_future)
asset_futures = [__load_df.remote(
data_source=data_source,
prefix=data_source[1],
returns='log_returns',
feature_extractors=other_features,
) for data_source in files]
asset_dfs = ray.get(asset_futures)
exogenous_futures = [__load_df.remote(
data_source=data_source,
prefix=data_source[1],
returns='none',
feature_extractors=exogenous_features,
) for data_source in exogenous_data]
exogenous_dfs = ray.get(exogenous_futures)
X = target_asset_df + asset_dfs + exogenous_dfs
X = pd.concat([df.sort_index().reindex(X[0].index) for df in X], axis=1).fillna(0.)
X.index = pd.DatetimeIndex(X.index)
## Create target
returns = df_target_asset_only_returns[target_asset[1] + '_returns']
returns.index = pd.DatetimeIndex(X.index)
forward_returns = __create_target_cum_forward_returns(returns, 1)
forward_returns.index = pd.DatetimeIndex(X.index)
# we need to null out the last forward returns row, because when doing forward-shifting, we automatically get the first row, which is definitely incorrect
forward_returns[forward_returns.index[-1]] = 0.
return X, returns, forward_returns
@ray.remote
def __load_df(data_source: DataSource,
prefix: str,
returns: Literal['none', 'price', 'returns', 'log_returns'],
feature_extractors: list[tuple[str, FeatureExtractor, list[int]]]) -> pd.DataFrame:
df = pd.read_csv(os.path.join(data_source[0], data_source[1] + '.csv'), header=0, index_col=0).fillna(0)
if returns == 'log_returns':
df['returns'] = np.log(df['close']).diff(1)
elif returns == 'price':
df['returns'] = df['close']
elif returns == 'returns':
df['returns'] = df['close'].pct_change()
df = __apply_feature_extractors(df, feature_extractors = feature_extractors)
df = df.replace([np.inf, -np.inf], 0.)
df = drop_columns_if_exist(df, ['open', 'high', 'low', 'close', 'volume'])
df.columns = [prefix + "_" + c if 'date' not in c else c for c in df.columns]
return df
def __apply_feature_extractors(df: pd.DataFrame, feature_extractors: list[tuple[str, FeatureExtractor, list[int]]]) -> pd.DataFrame:
for name, extractor, periods in feature_extractors:
for period in periods:
features = extractor(df, period)
if type(features) == pd.DataFrame:
df = pd.concat([df, features], axis=1)
elif type(features) == pd.Series:
df[name + '_' + str(period)] = features
else:
assert False, "Feature extractor must return a pd.DataFrame or pd.Series"
return df
def __create_target_cum_forward_returns(series: pd.Series, period: int) -> pd.Series:
assert period > 0
return series.shift(-period).copy()
def load_only_returns(assets: DataCollection, returns: Literal['price', 'returns']) -> pd.DataFrame:
assets_future = [__load_df.remote(
data_source=data_source,
prefix=data_source[1],
returns=returns,
feature_extractors=[],
) for data_source in assets]
dfs = ray.get(assets_future)
dfs = pd.concat(dfs, axis=1)
dfs.index = pd.DatetimeIndex(dfs.index)
return dfs
-229
View File
@@ -1,229 +0,0 @@
import pandas as pd
import numpy as np
from utils.types import DataSource, FeatureExtractor
from utils.helpers import deduplicate_indexes, drop_columns_if_exist
from data_loader.collections import DataCollection
from typing import Literal
import ray
import os
from config.hashing import hash_data_config
from diskcache import Cache
cache = Cache(".cachedir/data")
def load_data(**kwargs) -> tuple[pd.DataFrame, pd.Series, pd.Series]:
hashed = hash_data_config(kwargs)
if hashed in cache:
return cache.get(hashed)
else:
return_value = __load_data(**kwargs)
cache[hashed] = return_value
return return_value
def __load_data(assets: DataCollection,
other_assets: DataCollection,
exogenous_data: DataCollection,
target_asset: DataSource,
load_non_target_asset: bool,
log_returns: bool,
forecasting_horizon: int,
own_features: list[tuple[str, FeatureExtractor, list[int]]],
other_features: list[tuple[str, FeatureExtractor, list[int]]],
exogenous_features: list[tuple[str, FeatureExtractor, list[int]]],
no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced'],
) -> tuple[pd.DataFrame, pd.Series, pd.Series]:
"""
Loads asset data from the specified path.
Returns:
- DataFrame `X` with all the training data
- Series `y` with the target asset returns shifted by 1 day OR if it's a classification problem, the target class)
- Series `forward_returns` with the target asset returns shifted by 1 day
"""
target_file = [f for f in assets if f[1].startswith(target_asset[1])]
assert len(target_file) == 1, "There should be exactly one target file"
other_files = [f for f in assets if load_non_target_asset == True and f[1].startswith(target_asset[1]) == False]
files = other_files + other_assets
target_asset_future = [__load_df.remote(
data_source=data_source,
prefix=data_source[1],
returns='log_returns' if log_returns else 'returns',
feature_extractors=own_features,
) for data_source in target_file]
target_asset_df = ray.get(target_asset_future)
target_asset_only_returns_future = __load_df.remote(
data_source=target_file[0],
prefix=target_file[0][1],
returns='returns',
feature_extractors=[],
)
df_target_asset_only_returns = ray.get(target_asset_only_returns_future)
asset_futures = [__load_df.remote(
data_source=data_source,
prefix=data_source[1],
returns='log_returns' if log_returns else 'returns',
feature_extractors=other_features,
) for data_source in files]
asset_dfs = ray.get(asset_futures)
exogenous_futures = [__load_df.remote(
data_source=data_source,
prefix=data_source[1],
returns='none',
feature_extractors=exogenous_features,
) for data_source in exogenous_data]
exogenous_dfs = ray.get(exogenous_futures)
dfs = target_asset_df + asset_dfs + exogenous_dfs
dfs = [deduplicate_indexes(df) for df in dfs]
target_df = dfs[0]
dfs = pd.concat([df.sort_index().reindex(target_df.index) for df in dfs], axis=1).fillna(0.)
dfs.index = pd.DatetimeIndex(dfs.index)
## Create target
target_col = 'target'
returns_col = target_asset[1] + '_returns'
forward_returns = __create_target_cum_forward_returns(df_target_asset_only_returns, returns_col, forecasting_horizon)
forward_returns.index = pd.DatetimeIndex(dfs.index)
dfs[target_col] = __create_target_classes(dfs, returns_col, forecasting_horizon, no_of_classes)
# we need to drop the last row, because we forward-shift the target (see what happens if you call .shift[-1] on a pd.Series)
dfs = dfs.iloc[:-forecasting_horizon]
forward_returns = forward_returns.iloc[:-forecasting_horizon]
X = dfs.drop(columns=[target_col])
y = dfs[target_col]
return X, y, forward_returns
@ray.remote
def __load_df(data_source: DataSource,
prefix: str,
returns: Literal['none', 'price', 'returns', 'log_returns'],
feature_extractors: list[tuple[str, FeatureExtractor, list[int]]]) -> pd.DataFrame:
df = pd.read_csv(os.path.join(data_source[0], data_source[1] + '.csv'), header=0, index_col=0).fillna(0)
if returns == 'log_returns':
df['returns'] = np.log(df['close']).diff(1)
elif returns == 'price':
df['returns'] = df['close']
elif returns == 'returns':
df['returns'] = df['close'].pct_change()
df = __apply_feature_extractors(df, log_returns=True if returns == 'log_returns' else False, feature_extractors = feature_extractors)
df = df.replace([np.inf, -np.inf], 0.)
df = drop_columns_if_exist(df, ['open', 'high', 'low', 'close', 'volume'])
df.columns = [prefix + "_" + c if 'date' not in c else c for c in df.columns]
return df
def __apply_feature_extractors(df: pd.DataFrame,
log_returns: bool,
feature_extractors: list[tuple[str, FeatureExtractor, list[int]]]) -> pd.DataFrame:
for name, extractor, periods in feature_extractors:
for period in periods:
features = extractor(df, period, log_returns)
if type(features) == pd.DataFrame:
df = pd.concat([df, features], axis=1)
elif type(features) == pd.Series:
df[name + '_' + str(period)] = features
else:
assert False, "Feature extractor must return a pd.DataFrame or pd.Series"
return df
def __create_target_cum_forward_returns(df: pd.DataFrame, source_column: str, period: int) -> pd.Series:
assert period > 0
return df[source_column].shift(-period)
def __create_target_classes(df: pd.DataFrame, source_column: str, period: int, no_of_classes: Literal["two", "three"]) -> pd.Series:
assert period > 0
def get_class_binary(x: float) -> int:
return -1 if x <= 0.0 else 1
def get_class_threeway_balanced(series: pd.Series) -> pd.Series:
def get_bins_threeway(x):
bins = pd.qcut(df[source_column], 3, retbins=True, duplicates = 'drop')[1]
if len(bins) != 4:
# if we don't have enough data for the quantiles, we'll need to add hard-coded values
lower_bound = bins[0]
upper_bound = bins[-1]
bins = [lower_bound] + [-0.02, 0.02] + [upper_bound]
return bins
bins = get_bins_threeway(series)
def map_class_threeway(current_value):
lower_threshold = bins[1]
upper_threshold = bins[2]
if current_value <= lower_threshold:
return -1
elif current_value > lower_threshold and current_value < upper_threshold:
return 0
else:
return 1
return series.map(map_class_threeway)
def get_class_threeway_imbalanced(series: pd.Series) -> pd.Series:
def get_bins_threeway(x):
bins = pd.qcut(df[source_column], 4, retbins=True, duplicates = 'drop')[1]
if len(bins) != 5:
# if we don't have enough data for the quantiles, we'll need to add hard-coded values
lower_bound = bins[0]
upper_bound = bins[-1]
bins = [lower_bound] + [-0.02, 0.0, 0.02] + [upper_bound]
return bins
bins = get_bins_threeway(series)
def map_class_threeway(current_value):
lower_threshold = bins[1]
upper_threshold = bins[3]
if current_value <= lower_threshold:
return -1
elif current_value > lower_threshold and current_value < upper_threshold:
return 0
else:
return 1
return series.map(map_class_threeway)
target_column = df[source_column].shift(-period)
if no_of_classes == "three-balanced":
return get_class_threeway_balanced(target_column)
elif no_of_classes == "three-imbalanced":
return get_class_threeway_imbalanced(target_column)
else:
return target_column.map(get_class_binary)
def datasource_to_file(data_source: DataSource) -> str:
return data_source[0] + '/' + data_source[1] + '.csv'
def load_only_returns(assets: DataCollection, returns: Literal['price', 'returns']) -> pd.DataFrame:
assets_future = [__load_df.remote(
data_source=data_source,
prefix=data_source[1],
returns=returns,
feature_extractors=[],
) for data_source in assets]
target_asset_df = ray.get(assets_future)
dfs = [deduplicate_indexes(df) for df in target_asset_df]
dfs = pd.concat(dfs, axis=1)
dfs.index = pd.DatetimeIndex(dfs.index)
return dfs
+19
View File
@@ -0,0 +1,19 @@
import pandas as pd
from config.types import Config
import warnings
from utils.helpers import get_first_valid_return_index
from data_loader.types import XDataFrame
def check_data(X: XDataFrame, config: Config) -> bool:
""" Returns True if data is valid, else returns False."""
if has_enough_samples_to_train(X, config) == False:
warnings.warn("Not enough samples to train")
return False
return True
def has_enough_samples_to_train(X: XDataFrame, config: Config) -> bool:
first_valid_index = get_first_valid_return_index(X.iloc[:,0])
samples_to_train = len(X) - first_valid_index
return samples_to_train > config.sliding_window_size_base + config.sliding_window_size_meta_labeling + 100
-13
View File
@@ -1,13 +0,0 @@
import pandas as pd
from config.config import Config
from utils.helpers import has_enough_samples_to_train
import warnings
def check_data(X:pd.DataFrame, y:pd.Series, config: Config):
""" Returns True if data is valid, else returns False."""
if has_enough_samples_to_train(X, y, config) == False:
warnings.warn("Not enough samples to train")
return False
return True
+11
View File
@@ -0,0 +1,11 @@
import pandas as pd
Path = str
FileName = str
DataSource = tuple[Path, FileName]
DataCollection = list[DataSource]
ReturnSeries = pd.Series
ForwardReturnSeries = pd.Series
XDataFrame = pd.DataFrame
ySeries = pd.Series
+3
View File
@@ -0,0 +1,3 @@
import pandas as pd
def deduplicate_indexes(df: pd.DataFrame) -> pd.DataFrame: return df[~df.index.duplicated(keep='last')]
+1
View File
@@ -34,4 +34,5 @@ dependencies:
- alphalens-reloaded
- vectorbt
- pydantic
- pandera[mypy]
prefix: /usr/local/anaconda3/envs/quant
+2 -2
View File
@@ -25,7 +25,7 @@
"source": [
"import pandas as pd\n",
"import pandas_ta as ta\n",
"from config.config import get_default_ensemble_config\n",
"from config import get_default_ensemble_config\n",
"from config.preprocess import preprocess_config\n",
"from data_loader.load_data import load_data\n",
"import seaborn as sns\n",
@@ -35,7 +35,7 @@
"model_config, training_config, data_config = preprocess_config(model_config, training_config, data_config)\n",
"\n",
"config.target_asset'] = config.assets'][0]\n",
"X, y, target_returns = load_data(**data_config)"
"X, returns, forward_returns = load_data(**data_config)"
]
},
{
@@ -1,7 +1,7 @@
from feature_extractors.feature_extractors import feature_lag, feature_mom, feature_ROC, feature_RSI, feature_STOD, feature_STOK, feature_expanding_zscore, feature_vol, feature_day_of_month, feature_day_of_week, feature_month, feature_debug_future_lookahead
from utils.types import FeatureExtractorConfig
from utils.helpers import flatten
from feature_extractors.fractional_differentiation import feature_fractional_differentiation, feature_fractional_differentiation_log
from .types import FeatureExtractorConfig
from .feature_extractors import feature_lag, feature_mom, feature_ROC, feature_RSI, feature_STOD, feature_STOK, feature_expanding_zscore, feature_vol, feature_day_of_month, feature_day_of_week, feature_month, feature_debug_future_lookahead
from .fractional_differentiation import feature_fractional_differentiation, feature_fractional_differentiation_log
__presets = dict(
debug_future_lookahead = [('debug_future', feature_debug_future_lookahead, [1])],
+14 -17
View File
@@ -3,46 +3,43 @@ import numpy as np
from feature_extractors.utils import get_close_low_high
from feature_extractors.utils import apply_log_if_necessary_series
def feature_debug_future_lookahead(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.Series:
def feature_debug_future_lookahead(df: pd.DataFrame, period: int) -> pd.Series:
return df['returns'].shift(-period)
def feature_lag(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.Series:
def feature_lag(df: pd.DataFrame, period: int) -> pd.Series:
assert period > 0
return df['returns'].shift(period)
def feature_expanding_zscore(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.Series:
def feature_expanding_zscore(df: pd.DataFrame, period: int) -> pd.Series:
close = df['close']
return (close - close.expanding(period).mean()) / close.expanding(period).std()
def feature_day_of_week(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.DataFrame:
def feature_day_of_week(df: pd.DataFrame, period: int) -> pd.DataFrame:
return pd.get_dummies(pd.DatetimeIndex(df.index).dayofweek, drop_first=True, prefix="date_day_week").set_index(df.index)
def feature_day_of_month(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.DataFrame:
def feature_day_of_month(df: pd.DataFrame, period: int) -> pd.DataFrame:
return pd.get_dummies(pd.DatetimeIndex(df.index).day, drop_first=True, prefix="date_day_month").set_index(df.index)
def feature_month(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.DataFrame:
def feature_month(df: pd.DataFrame, period: int) -> pd.DataFrame:
return pd.get_dummies(pd.DatetimeIndex(df.index).month, drop_first=True, prefix="date_month").set_index(df.index)
def feature_vol(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.Series:
def feature_vol(df: pd.DataFrame, period: int) -> pd.Series:
return df['returns'].rolling(period).std() * (252**0.5)
def feature_mom(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.Series:
if is_log_return:
return np.log(df['close']).diff(period)
else:
return df['close'].pct_change(period)
def feature_mom(df: pd.DataFrame, period: int) -> pd.Series:
return np.log(df['close']).diff(period)
def feature_STOK(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.Series:
def feature_STOK(df: pd.DataFrame, period: int) -> pd.Series:
close, low, high = get_close_low_high(df)
STOK = ((close - low.rolling(period).min()) / (high.rolling(period).max() - low.rolling(period).min())) * 100
return apply_log_if_necessary_series(STOK, "stok")
def feature_STOD(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.Series:
stok = feature_STOK(df, period, is_log_return)
def feature_STOD(df: pd.DataFrame, period: int) -> pd.Series:
stok = feature_STOK(df, period)
return stok.rolling(3).mean()
def feature_RSI(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.Series:
def feature_RSI(df: pd.DataFrame, period: int) -> pd.Series:
returns = df['returns']
delta = returns.diff().dropna()
u=delta*0
@@ -55,7 +52,7 @@ def feature_RSI(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.Series
d.ewm(com=period-1, adjust=False).mean()
return apply_log_if_necessary_series(100-100/(1+rs), "rsi")
def feature_ROC(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.Series:
def feature_ROC(df: pd.DataFrame, period: int) -> pd.Series:
returns = df['returns']
M = returns.diff(period - 1)
N = returns.shift(period - 1)
@@ -3,13 +3,13 @@ import pandas as pd
import numpy as np
from feature_extractors.utils import apply_log_if_necessary_series
def feature_fractional_differentiation(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.Series:
def feature_fractional_differentiation(df: pd.DataFrame, period: int) -> pd.Series:
frac_diff = FracdiffStat(window = period)
input_series = df["close"].to_numpy().reshape(-1, 1)
result = frac_diff.fit_transform(input_series)
return pd.Series(result.squeeze(), index = df.index)
def feature_fractional_differentiation_log(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.Series:
def feature_fractional_differentiation_log(df: pd.DataFrame, period: int) -> pd.Series:
series = feature_fractional_differentiation(df, period, is_log_return)
return apply_log_if_necessary_series(series, "fracdiff")
+1 -1
View File
@@ -2,7 +2,7 @@ import pandas_ta as ta
import pandas as pd
from feature_extractors.utils import get_close_low_high
def feature_EBSW(df: pd.DataFrame, period: int, is_log_return: bool) -> pd.Series:
def feature_EBSW(df: pd.DataFrame, period: int) -> pd.Series:
close, low, high = get_close_low_high(df)
return ta.ebsw(close, period)
@@ -3,12 +3,7 @@ import pandas as pd
Period = int
IsLogReturn = bool
FeatureExtractor = Callable[[pd.DataFrame, Period, IsLogReturn], Union[pd.DataFrame, pd.Series]]
FeatureExtractor = Callable[[pd.DataFrame, Period], Union[pd.DataFrame, pd.Series]]
Name = str
FeatureExtractorConfig = tuple[Name, FeatureExtractor, list[Period]]
Path = str
FileName = str
DataSource = tuple[Path, FileName]
DataCollection = list[DataSource]
ScalerTypes = Literal['normalize', 'minmax', 'standardize', 'none']
ScalerTypes = Literal['normalize', 'minmax', 'standardize', 'none']
+60
View File
@@ -0,0 +1,60 @@
from ..types import EventFilter
from data_loader.types import ReturnSeries
import pandas as pd
class CUSUMVolatilityEventFilter(EventFilter):
def __init__(self, vol_period: int):
self.vol_period = vol_period
def get_event_start_times(self, returns: ReturnSeries) -> pd.DatetimeIndex:
rolling_vol = returns.rolling(self.vol_period).std() * 0.15
filtered_indices = []
pos_threshold = 0
neg_threshold = 0
diff = returns.diff()
for index in diff.index[1:]:
pos_threshold, neg_threshold = (
max(0, pos_threshold + diff.loc[index]),
min(0, neg_threshold + diff.loc[index]),
)
if neg_threshold < -rolling_vol[index]:
neg_threshold = 0
filtered_indices.append(index)
elif pos_threshold > rolling_vol[index]:
pos_threshold = 0
filtered_indices.append(index)
return pd.DatetimeIndex(filtered_indices)
class CUSUMFixedEventFilter(EventFilter):
def __init__(self, threshold: float):
self.threshold = threshold
def get_event_start_times(self, returns: ReturnSeries) -> pd.DatetimeIndex:
filtered_indices = []
pos_threshold = 0
neg_threshold = 0
diff = returns.diff()
for index in diff.index[1:]:
pos_threshold, neg_threshold = (
max(0, pos_threshold + diff.loc[index]),
min(0, neg_threshold + diff.loc[index]),
)
if neg_threshold < -self.threshold:
neg_threshold = 0
filtered_indices.append(index)
elif pos_threshold > self.threshold:
pos_threshold = 0
filtered_indices.append(index)
return pd.DatetimeIndex(filtered_indices)
+9
View File
@@ -0,0 +1,9 @@
from ..types import EventFilter
from data_loader.types import ReturnSeries
import pandas as pd
class NoEventFilter(EventFilter):
def get_event_start_times(self, returns: ReturnSeries) -> pd.DatetimeIndex:
return returns.index
+8
View File
@@ -0,0 +1,8 @@
from .event_filters.nofilter import NoEventFilter
from .event_filters.cusum import CUSUMVolatilityEventFilter, CUSUMFixedEventFilter
eventfilters_map = dict(
none = NoEventFilter(),
cusum_vol = CUSUMVolatilityEventFilter(vol_period = 20),
cusum_fixed = CUSUMFixedEventFilter(threshold = 0.05)
)
@@ -0,0 +1,44 @@
from data_loader.types import ForwardReturnSeries
from ..types import EventLabeller, EventsDataFrame
import pandas as pd
class FixedTimeHorionThreeClassBalancedEventLabeller(EventLabeller):
time_horizon: int
def __init__(self, time_horizon: int = 1):
self.time_horizon = time_horizon
def label_events(self, event_start_times: pd.DatetimeIndex, forward_returns: ForwardReturnSeries) -> EventsDataFrame:
event_candidates = forward_returns[event_start_times]
def get_bins_threeway(x):
bins = pd.qcut(event_candidates, 3, retbins=True, duplicates = 'drop')[1]
if len(bins) != 4:
# if we don't have enough data for the quantiles, we'll need to add hard-coded values
lower_bound = bins[0]
upper_bound = bins[-1]
bins = [lower_bound] + [-0.02, 0.02] + [upper_bound]
return bins
bins = get_bins_threeway(event_candidates)
def map_class_threeway(current_value):
lower_threshold = bins[1]
upper_threshold = bins[2]
if current_value <= lower_threshold:
return -1
elif current_value > lower_threshold and current_value < upper_threshold:
return 0
else:
return 1
labels = event_candidates.map(map_class_threeway)
return pd.DataFrame({
'start': event_start_times,
'end': event_start_times + pd.Timedelta(days=self.time_horizon),
'label': labels,
'returns': forward_returns[event_start_times]
})
@@ -0,0 +1,44 @@
from ..types import EventLabeller, EventsDataFrame, ForwardReturnSeries
import pandas as pd
class FixedTimeHorionThreeClassImbalancedEventLabeller(EventLabeller):
time_horizon: int
def __init__(self, time_horizon: int = 1):
self.time_horizon = time_horizon
def label_events(self, event_start_times: pd.DatetimeIndex, forward_returns: ForwardReturnSeries) -> EventsDataFrame:
event_candidates = forward_returns[event_start_times]
def get_bins_threeway(x):
bins = pd.qcut(event_candidates, 4, retbins=True, duplicates = 'drop')[1]
if len(bins) != 5:
# if we don't have enough data for the quantiles, we'll need to add hard-coded values
lower_bound = bins[0]
upper_bound = bins[-1]
bins = [lower_bound] + [-0.02, 0.0, 0.02] + [upper_bound]
return bins
bins = get_bins_threeway(event_candidates)
def map_class_threeway(current_value):
lower_threshold = bins[1]
upper_threshold = bins[3]
if current_value <= lower_threshold:
return -1
elif current_value > lower_threshold and current_value < upper_threshold:
return 0
else:
return 1
labels = event_candidates.map(map_class_threeway)
return pd.DataFrame({
'start': event_start_times,
'end': event_start_times + pd.Timedelta(days=self.time_horizon),
'label': labels,
'returns': forward_returns[event_start_times]
})
@@ -0,0 +1,29 @@
from ..types import EventLabeller, EventsDataFrame, ForwardReturnSeries
import pandas as pd
class FixedTimeHorionTwoClassEventLabeller(EventLabeller):
time_horizon: int
def __init__(self, time_horizon: int = 1):
self.time_horizon = time_horizon
def label_events(self, event_start_times: pd.DatetimeIndex, forward_returns: ForwardReturnSeries) -> EventsDataFrame:
event_candidates = forward_returns[event_start_times]
def get_class_binary(x: float) -> int:
return -1 if x <= 0.0 else 1
labels = event_candidates.map(get_class_binary)
return pd.DataFrame({
'start': event_start_times,
'end': event_start_times + pd.Timedelta(days=self.time_horizon),
'label': labels,
'returns': forward_returns[event_start_times]
})
+9
View File
@@ -0,0 +1,9 @@
from .labellers.fixed_time_three_class_balanced import FixedTimeHorionThreeClassBalancedEventLabeller
from .labellers.fixed_time_three_class_imbalanced import FixedTimeHorionThreeClassImbalancedEventLabeller
from .labellers.fixed_time_two_class import FixedTimeHorionTwoClassEventLabeller
labellers_map = dict(
two_class = FixedTimeHorionTwoClassEventLabeller(),
three_class_balanced = FixedTimeHorionThreeClassBalancedEventLabeller(),
three_class_imbalanced = FixedTimeHorionThreeClassImbalancedEventLabeller()
)
+17
View File
@@ -0,0 +1,17 @@
from .types import EventFilter, EventLabeller, EventsDataFrame
from data_loader.types import ForwardReturnSeries, XDataFrame, ReturnSeries, ySeries
def label_data(
event_filter: EventFilter,
event_labeller: EventLabeller,
X: XDataFrame,
returns: ReturnSeries,
forward_returns: ForwardReturnSeries) -> tuple[EventsDataFrame, XDataFrame, ySeries, ForwardReturnSeries]:
event_start_times = event_filter.get_event_start_times(returns)
events = event_labeller.label_events(event_start_times, forward_returns)
X = X.filter(items = events.index, axis = 0)
y = events['label']
forward_returns = events['returns']
return events, X, y, forward_returns
+28
View File
@@ -0,0 +1,28 @@
from data_loader.types import ReturnSeries, ForwardReturnSeries
from abc import ABC, abstractmethod
import pandas as pd
import pandera as pa
from pandera.typing import DataFrame, Series
class EventFilter(ABC):
@abstractmethod
def get_event_start_times(self, returns: ReturnSeries) -> pd.DatetimeIndex:
raise NotImplementedError
class EventSchema(pa.SchemaModel):
start: Series[pd.Timestamp]
end: Series[pd.Timestamp]
label: Series[int]
returns: Series[float]
EventsDataFrame = DataFrame[EventSchema]
class EventLabeller(ABC):
@abstractmethod
def label_events(self, event_start_times: pd.DatetimeIndex, forward_returns: ForwardReturnSeries) -> EventsDataFrame:
raise NotImplementedError
-2
View File
@@ -3,8 +3,6 @@ from typing import Literal, Optional, Union
from abc import ABC, abstractmethod
import numpy as np
# import numpy as np
class Model(ABC):
method: Literal["regression", "classification"]
+83 -91
View File
@@ -1,100 +1,92 @@
from models.sklearn import SKLearnModel
from sklearnex.ensemble import RandomForestClassifier
from sklearnex.ensemble import RandomForestRegressor
from .base import Model
default_feature_selector_classification = SKLearnModel(RandomForestClassifier(n_jobs=-1, max_depth=20, random_state=1), 'classification')
default_feature_selector_regression = SKLearnModel(RandomForestRegressor(n_jobs=-1, max_depth=20, random_state=1), 'regression')
def get_model_map(config:dict):
def get_model(model_name: str) -> Model:
model_map = {
"primary_models": dict(),
"ensemble_models": dict(),
}
combined_list = config['primary_models'] + config['meta_labeling_models'] + [config['ensemble_model']]
for model_name in combined_list:
if model_name == 'LinearRegression':
from sklearn.linear_model import LinearRegression
model_map['primary_models']['LR'] = SKLearnModel(LinearRegression(n_jobs=-1), 'regression')
elif model_name == 'Lasso':
from sklearn.linear_model import Lasso
model_map['primary_models']['Lasso'] = SKLearnModel(Lasso(alpha=100, random_state=1), 'regression')
elif model_name == 'Ridge':
from sklearn.linear_model import Ridge
model_map['primary_models']['Ridge'] = SKLearnModel(Ridge(alpha=0.1), 'regression')
elif model_name == 'BayesianRidge':
from sklearn.linear_model import BayesianRidge
model_map['primary_models']['BayesianRidge'] = SKLearnModel(BayesianRidge(), 'regression')
elif model_name == 'KNN':
from sklearnex.neighbors import KNeighborsRegressor
model_map['primary_models']['KNN'] = SKLearnModel(KNeighborsRegressor(n_neighbors=25), 'regression')
elif model_name == 'AB':
from sklearn.ensemble import AdaBoostRegressor
model_map['primary_models']['AB'] = SKLearnModel(AdaBoostRegressor(random_state=1), 'regression')
elif model_name == 'MLP':
from sklearn.neural_network import MLPRegressor
model_map['primary_models']['MLP'] = SKLearnModel(MLPRegressor(hidden_layer_sizes=(100,20), max_iter=1000), 'regression')
elif model_name == 'RFR':
# from sklearn.ensemble import RandomForestRegressor
model_map['primary_models']['RFR'] = SKLearnModel(RandomForestRegressor(n_jobs=-1, max_depth=20, random_state=1), 'regression')
elif model_name == 'SVR':
from sklearnex.svm import SVR
model_map['primary_models']['SVR'] = SKLearnModel(SVR(kernel='rbf', C=1e3, gamma=0.1), 'regression')
elif model_name == 'StaticNaive':
from models.naive import StaticNaiveModel
model_map['primary_models']['StaticNaive'] = StaticNaiveModel()
elif model_name == 'DNN':
from models.neural import LightningNeuralNetModel
from models.pytorch.neural_nets import MultiLayerPerceptron
import torch.nn.functional as F
model_map['primary_models']['DNN'] = LightningNeuralNetModel(
MultiLayerPerceptron(
hidden_layers_ratio = [1.0],
probabilities = False,
loss_function = F.mse_loss),
max_epochs=15
)
elif model_name == 'LogisticRegression_two_class':
from sklearn.linear_model import LogisticRegression
model_map['primary_models']['LogisticRegression_two_class'] = SKLearnModel(LogisticRegression(C=10, random_state=1, solver='liblinear', max_iter=1000), 'classification')
elif model_name == 'LogisticRegression_three_class':
from sklearnex.linear_model import LogisticRegression as LogisticRegression_EX
model_map['primary_models']['LogisticRegression_three_class'] = SKLearnModel(LogisticRegression_EX(C=10, random_state=1, max_iter=1000, n_jobs=-1), 'classification')
elif model_name == 'LDA':
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
model_map['primary_models']['LDA'] = SKLearnModel(LinearDiscriminantAnalysis(), 'classification')
elif model_name == 'KNN':
from sklearn.neighbors import KNeighborsClassifier
model_map['primary_models']['KNN'] = SKLearnModel(KNeighborsClassifier(), 'classification')
elif model_name == 'CART':
from sklearn.tree import DecisionTreeClassifier
model_map['primary_models']['CART'] = SKLearnModel(DecisionTreeClassifier(max_depth=15, random_state=1), 'classification')
elif model_name == 'NB':
from sklearn.naive_bayes import GaussianNB
model_map['primary_models']['NB'] = SKLearnModel(GaussianNB(), 'classification')
elif model_name == 'AB':
from sklearn.ensemble import AdaBoostClassifier
model_map['primary_models']['AB'] = SKLearnModel(AdaBoostClassifier(n_estimators=15), 'classification')
elif model_name == 'RFC':
model_map['primary_models']['RFC'] = SKLearnModel(RandomForestClassifier(n_jobs=-1, max_depth=20, random_state=1), 'classification')
elif model_name == 'SVC':
from sklearn.svm import SVC
model_map['primary_models']['SVC'] = SKLearnModel(SVC(kernel='rbf', C=1e3, probability=True, random_state=1), 'classification')
elif model_name == 'XGB_two_class':
from xgboost import XGBClassifier
from models.xgboost import XGBoostModel
model_map['primary_models']['XGB_two_class'] = XGBoostModel(XGBClassifier(n_jobs=-1, max_depth = 20, random_state=1, objective='binary:logistic', use_label_encoder= False, eval_metric='mlogloss'))
elif model_name == 'LGBM':
from lightgbm import LGBMClassifier
model_map['primary_models']['LGBM'] = SKLearnModel(LGBMClassifier(n_jobs=-1, max_depth=20, random_state=1), 'classification')
elif model_name == 'StaticMom':
from models.momentum import StaticMomentumModel
model_map['primary_models']['StaticMom'] = StaticMomentumModel(allow_short=True)
elif model_name == 'Average':
from models.average import StaticAverageModel
model_map['ensemble_models']['Average'] = StaticAverageModel()
return model_map
if model_name == 'LinearRegression':
from sklearn.linear_model import LinearRegression
return SKLearnModel(LinearRegression(n_jobs=-1), 'regression')
elif model_name == 'Lasso':
from sklearn.linear_model import Lasso
return SKLearnModel(Lasso(alpha=100, random_state=1), 'regression')
elif model_name == 'Ridge':
from sklearn.linear_model import Ridge
return SKLearnModel(Ridge(alpha=0.1), 'regression')
elif model_name == 'BayesianRidge':
from sklearn.linear_model import BayesianRidge
return SKLearnModel(BayesianRidge(), 'regression')
elif model_name == 'KNN':
from sklearnex.neighbors import KNeighborsRegressor
return SKLearnModel(KNeighborsRegressor(n_neighbors=25), 'regression')
elif model_name == 'AB':
from sklearn.ensemble import AdaBoostRegressor
return SKLearnModel(AdaBoostRegressor(random_state=1), 'regression')
elif model_name == 'MLP':
from sklearn.neural_network import MLPRegressor
return SKLearnModel(MLPRegressor(hidden_layer_sizes=(100,20), max_iter=1000), 'regression')
elif model_name == 'RFR':
return SKLearnModel(RandomForestRegressor(n_jobs=-1, max_depth=20, random_state=1), 'regression')
elif model_name == 'SVR':
from sklearnex.svm import SVR
return SKLearnModel(SVR(kernel='rbf', C=1e3, gamma=0.1), 'regression')
elif model_name == 'StaticNaive':
from models.naive import StaticNaiveModel
return StaticNaiveModel()
elif model_name == 'DNN':
from models.neural import LightningNeuralNetModel
from models.pytorch.neural_nets import MultiLayerPerceptron
import torch.nn.functional as F
return LightningNeuralNetModel(
MultiLayerPerceptron(
hidden_layers_ratio = [1.0],
probabilities = False,
loss_function = F.mse_loss),
max_epochs=15
)
elif model_name == 'LogisticRegression_two_class':
from sklearn.linear_model import LogisticRegression
return SKLearnModel(LogisticRegression(C=10, random_state=1, solver='liblinear', max_iter=1000), 'classification')
elif model_name == 'LogisticRegression_three_class':
from sklearnex.linear_model import LogisticRegression as LogisticRegression_EX
return SKLearnModel(LogisticRegression_EX(C=10, random_state=1, max_iter=1000, n_jobs=-1), 'classification')
elif model_name == 'LDA':
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
return SKLearnModel(LinearDiscriminantAnalysis(), 'classification')
elif model_name == 'KNN':
from sklearn.neighbors import KNeighborsClassifier
return SKLearnModel(KNeighborsClassifier(), 'classification')
elif model_name == 'CART':
from sklearn.tree import DecisionTreeClassifier
return SKLearnModel(DecisionTreeClassifier(max_depth=15, random_state=1), 'classification')
elif model_name == 'NB':
from sklearn.naive_bayes import GaussianNB
return SKLearnModel(GaussianNB(), 'classification')
elif model_name == 'AB':
from sklearn.ensemble import AdaBoostClassifier
return SKLearnModel(AdaBoostClassifier(n_estimators=15), 'classification')
elif model_name == 'RFC':
return SKLearnModel(RandomForestClassifier(n_jobs=-1, max_depth=20, random_state=1), 'classification')
elif model_name == 'SVC':
from sklearn.svm import SVC
return SKLearnModel(SVC(kernel='rbf', C=1e3, probability=True, random_state=1), 'classification')
elif model_name == 'XGB_two_class':
from xgboost import XGBClassifier
from models.xgboost import XGBoostModel
return XGBoostModel(XGBClassifier(n_jobs=-1, max_depth = 20, random_state=1, objective='binary:logistic', use_label_encoder= False, eval_metric='mlogloss'))
elif model_name == 'LGBM':
from lightgbm import LGBMClassifier
return SKLearnModel(LGBMClassifier(n_jobs=-1, max_depth=20, random_state=1), 'classification')
elif model_name == 'StaticMom':
from models.momentum import StaticMomentumModel
return StaticMomentumModel(allow_short=True)
elif model_name == 'Average':
from models.average import StaticAverageModel
return StaticAverageModel()
else:
raise Exception(f'Model {model_name} not found')
+1 -1
View File
@@ -1,7 +1,7 @@
from reporting.wandb import send_report_to_wandb
import pandas as pd
from utils.helpers import weighted_average
from config.config import Config
from config.types import Config
def report_results(results:pd.DataFrame, all_predictions:pd.DataFrame, config: Config, wandb, sweep: bool, project_name:str):
+1 -1
View File
@@ -1,6 +1,6 @@
import pickle
import datetime
from config.config import Config
from config.types import Config
from typing import Optional, Union
import os
import warnings
+10 -11
View File
@@ -1,7 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass
import pandas as pd
from models.base import Model
from typing import Optional, Union
class Reporting:
@@ -14,12 +13,11 @@ class Reporting:
def get_results(self) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, Reporting.Asset]:
return self.results, self.all_predictions, self.all_probabilities, self.asset
@dataclass
class Single_Model:
def __init__(self, model_name: str, model_over_time: pd.Series, transformations_over_time: list[pd.Series]):
self.model_name: str = model_name
self.model_over_time: pd.Series = model_over_time
self.transformations_over_time: list[pd.Series] = transformations_over_time
model_name: str
model_over_time: pd.Series
transformations_over_time: list[pd.Series]
class Training_Step:
@@ -37,8 +35,9 @@ class Reporting:
structured_dict[model.model_name] = [(x.model_name, x.model_over_time, x.transformations_over_time) for x in self.metalabeling[i]]
return structured_dict
@dataclass
class Asset():
def __init__(self, ticker: str, primary: Reporting.Training_Step, secondary: Reporting.Training_Step):
self.name: str = ticker
self.primary: Reporting.Training_Step = primary
self.secondary: Reporting.Training_Step = secondary
name: str
primary: Reporting.Training_Step
secondary: Reporting.Training_Step
+1 -1
View File
@@ -1,5 +1,5 @@
import pandas as pd
from config.config import RawConfig
from config.types import RawConfig
from typing import Optional
from utils.helpers import weighted_average
+2 -2
View File
@@ -1,11 +1,11 @@
from run_pipeline import run_pipeline
from config.config import get_default_ensemble_config, get_dev_config
from config import get_default_ensemble_config, get_dev_config
import pandas as pd
all_results = []
all_predictions = []
for index in range(6):
_, _, _, _, results_1, predictions_1, _ = run_pipeline(project_name='price-prediction', with_wandb = False, sweep = False, get_config= get_default_ensemble_config)
_, _, _, _, results_1, predictions_1, _ = run_pipeline(project_name='price-prediction', with_wandb = False, sweep = False, config= get_default_ensemble_config())
all_results.append(results_1)
all_predictions.append(predictions_1)
@@ -1,24 +1,21 @@
import pandas as pd
import ssl
from tqdm import tqdm
from data_loader.utils import deduplicate_indexes
base_url = "https://www.cryptodatadownload.com/cdd/"
exchange_name = "Bitfinex"
period = "minute" # 1h
files_to_download = [
exchange_name + '_TRXUSD_1h.csv',
exchange_name + '_ETHUSD_1h.csv',
exchange_name + '_XLMUSD_1h.csv',
exchange_name + '_XMRUSD_1h.csv',
exchange_name + '_LTCUSD_1h.csv',
# exchange_name + '_FILUSD_1h.csv',
exchange_name + '_DASHUSD_1h.csv',
# exchange_name + '_LINKUSD_1h.csv',
# exchange_name + '_SOLUSD_1h.csv',
exchange_name + '_BTCUSD_1h.csv',
exchange_name + '_ETCUSD_1h.csv',
# exchange_name + '_VETUSD_1h.csv',
# exchange_name + '_DOTUSD_1h.csv'
exchange_name + '_TRXUSD_' + period + '.csv',
exchange_name + '_ETHUSD_' + period + '.csv',
exchange_name + '_XLMUSD_' + period + '.csv',
exchange_name + '_XMRUSD_' + period + '.csv',
exchange_name + '_LTCUSD_' + period + '.csv',
exchange_name + '_DASHUSD_' + period + '.csv',
exchange_name + '_BTCUSD_' + period + '.csv',
exchange_name + '_ETCUSD_' + period + '.csv',
]
@@ -31,4 +28,5 @@ for file in tqdm(files_to_download):
data.rename({'Volume USD': 'volume'}, axis=1, inplace=True)
data.index.rename('time', inplace=True)
target_file = file.split('_')[1].replace('USD', '') + '_USD'
data.to_csv(f"data/hourly_crypto/{target_file}.csv")
data = deduplicate_indexes(data)
data.to_csv(f"data/minute_crypto/{target_file}.csv")
+16 -15
View File
@@ -1,21 +1,22 @@
from data_loader.load_data import load_data
from data_loader.process_data import check_data
from data_loader import load_data
from data_loader.process import check_data
from reporting.saving import load_models
from run_pipeline import run_pipeline
from config.config import Config, get_dev_config, get_default_ensemble_config, get_lightweight_ensemble_config
from config.types import Config, RawConfig
from config.presets import get_dev_config, get_default_ensemble_config, get_lightweight_ensemble_config
from labeling.process import label_data
from typing import Callable, Optional
from reporting.types import Reporting
from training.training_steps import primary_step, secondary_step
import pandas as pd
import warnings
def run_inference(preload_models:bool, get_config:Callable):
def run_inference(preload_models:bool, raw_config: RawConfig):
if preload_models:
all_models, config = load_models(None)
else:
all_models, config, _, _, _ = run_pipeline(project_name='price-prediction', with_wandb = False, sweep = False, get_config=get_config)
all_models, config, _, _, _ = run_pipeline(project_name='price-prediction', with_wandb = False, sweep = False, raw_config=raw_config)
__inference(config, all_models.primary, all_models.secondary)
@@ -25,35 +26,35 @@ def __inference(config: Config, primary_models: Optional[Reporting.Training_Step
asset = config.target_asset
# 1. Load data, check for validity and process data
X, y, target_returns = load_data(
X, returns, forward_returns = load_data(
assets = config.assets,
other_assets = config.other_assets,
exogenous_data = config.exogenous_data,
target_asset = config.target_asset,
load_non_target_asset = config.load_non_target_asset,
log_returns = config.log_returns,
forecasting_horizon = config.forecasting_horizon,
own_features = config.own_features,
other_features = config.other_features,
exogenous_features = config.exogenous_features,
no_of_classes = config.no_of_classes,
)
assert check_data(X, y, config) == True, "Data is not valid. Cancelling Inference."
assert check_data(X, config) == True, "Data is not valid. Cancelling Inference."
events, X, y, forward_returns = label_data(config.event_filter, config.labeling, X, returns, forward_returns)
inference_from: pd.Timestamp = X.index[len(X.index) - 2]
# 2. Train a Primary model with optional metalabeling for each asset
training_step_primary, current_predictions = primary_step(X, y, target_returns, config, reporting, from_index = inference_from, preloaded_training_step = primary_models)
training_step_primary, current_predictions = primary_step(X, y, forward_returns, config, reporting, from_index = inference_from, preloaded_training_step = primary_models)
# 3. Train an Ensemble model with optional metalabeling for each asset
if secondary_models is not None:
warnings.warn("Secondary models are not specified.")
training_step_secondary = secondary_step(X, y, current_predictions, target_returns, config, reporting, from_index = inference_from, preloaded_training_step = secondary_models)
training_step_secondary = secondary_step(X, y, current_predictions, forward_returns, config, reporting, from_index = inference_from, preloaded_training_step = secondary_models)
# 4. Save the models
reporting.asset = Reporting.Asset(ticker=asset[1], primary=training_step_primary, secondary=training_step_secondary)
reporting.asset = Reporting.Asset(name=asset[1], primary=training_step_primary, secondary=training_step_secondary)
return reporting
if __name__ == '__main__':
run_inference(preload_models=True, get_config=get_lightweight_ensemble_config)
run_inference(preload_models=True, raw_config=get_lightweight_ensemble_config())
+2 -2
View File
@@ -1,5 +1,5 @@
from run_pipeline import run_pipeline
from config.config import get_dev_config
from config.presets import get_dev_config
run_pipeline(project_name='price-prediction', with_wandb = False, sweep = False, get_config=get_dev_config)
run_pipeline(project_name='price-prediction', with_wandb = False, sweep = False, raw_config=get_dev_config())
+25 -23
View File
@@ -1,27 +1,29 @@
import pandas as pd
from typing import Callable, Optional
from data_loader.load_data import load_data
from data_loader.process_data import check_data
from config.types import Config, RawConfig
from config.preprocess import preprocess_config, validate_config
from config.presets import get_default_ensemble_config, get_lightweight_ensemble_config
from data_loader.load import load_data
from data_loader.process import check_data
from labeling.process import label_data
from reporting.wandb import launch_wandb, override_config_with_wandb_values
from reporting.reporting import report_results
from reporting.saving import save_models
from config.config import Config, get_default_ensemble_config, get_lightweight_ensemble_config
from config.preprocess import validate_config, preprocess_config
from reporting.types import Reporting
from training.training_steps import primary_step, secondary_step
from reporting.types import Reporting
import ray
ray.init()
def run_pipeline(project_name:str, with_wandb: bool, sweep: bool, get_config: Callable) -> tuple[Reporting.Asset, Config, pd.DataFrame, pd.DataFrame, pd.DataFrame]:
wandb, config = __setup_config(project_name, with_wandb, sweep, get_config)
def run_pipeline(project_name:str, with_wandb: bool, sweep: bool, raw_config: RawConfig) -> tuple[Reporting.Asset, Config, pd.DataFrame, pd.DataFrame, pd.DataFrame]:
wandb, config = __setup_config(project_name, with_wandb, sweep, raw_config)
reporting = __run_training(config)
results, all_predictions, all_probabilities, all_models = reporting.get_results()
report_results(results, all_predictions, config, wandb, sweep, project_name)
@@ -30,8 +32,7 @@ def run_pipeline(project_name:str, with_wandb: bool, sweep: bool, get_config: Ca
return all_models, config, results, all_predictions, all_probabilities
def __setup_config(project_name:str, with_wandb: bool, sweep: bool, get_config: Callable) -> tuple[Optional[object], Config]:
raw_config = get_config()
def __setup_config(project_name:str, with_wandb: bool, sweep: bool, raw_config: RawConfig) -> tuple[Optional[object], Config]:
wandb = None
if with_wandb:
wandb = launch_wandb(project_name=project_name, default_config=raw_config, sweep=sweep)
@@ -48,29 +49,30 @@ def __run_training(config: Config):
reporting = Reporting()
# 1. Load data, check for validity
X, y, target_returns = load_data(
X, returns, forward_returns = load_data(
assets = config.assets,
other_assets = config.other_assets,
exogenous_data = config.exogenous_data,
target_asset = config.target_asset,
load_non_target_asset = config.load_non_target_asset,
log_returns = config.log_returns,
forecasting_horizon = config.forecasting_horizon,
own_features = config.own_features,
other_features = config.other_features,
exogenous_features = config.exogenous_features,
no_of_classes = config.no_of_classes,
)
assert check_data(X, y, config) == True, "Data is not valid."
# 2. Train a Primary model with optional metalabeling for each asset
training_step_primary, current_predictions = primary_step(X, y, target_returns, config, reporting, from_index = None)
assert check_data(X, config) == True, "Data is not valid."
# 2. Filter for significant events when we want to trade, and label data
events, X, y, forward_returns = label_data(config.event_filter, config.labeling, X, returns, forward_returns)
# 3. Train a Primary model with optional metalabeling for each asset
training_step_primary, current_predictions = primary_step(X, y, forward_returns, config, reporting, from_index = None)
# 3. Train an Ensemble model with optional metalabeling for each asset
training_step_secondary = secondary_step(X, y, current_predictions, target_returns, config, reporting, from_index = None)
# 4. Train an Ensemble model with optional metalabeling for each asset
training_step_secondary = secondary_step(X, y, current_predictions, forward_returns, config, reporting, from_index = None)
# 4. Save the models
reporting.asset = Reporting.Asset(ticker= config.target_asset[1], primary=training_step_primary, secondary=training_step_secondary)
# 5. Save the models
reporting.asset = Reporting.Asset(name = config.target_asset[1], primary = training_step_primary, secondary = training_step_secondary)
return reporting
@@ -78,4 +80,4 @@ def __run_training(config: Config):
if __name__ == '__main__':
run_pipeline(project_name='price-prediction', with_wandb = False, sweep = False, get_config=get_default_ensemble_config)
run_pipeline(project_name='price-prediction', with_wandb = False, sweep = False, raw_config=get_default_ensemble_config())
+3 -3
View File
@@ -1,8 +1,7 @@
#%%
import pandas as pd
import numpy as np
from data_loader.load_data import load_only_returns
from data_loader.collections import data_collections
from data_loader import load_only_returns, data_collections
from utils.helpers import get_first_valid_return_index
import alphalens
@@ -109,9 +108,10 @@ first_index = get_first_valid_return_index(predictions[predictions.columns[0]])
predictions = predictions.iloc[first_index:]
close = load_only_returns(data_collections['daily_crypto'], 'price')
close = close.iloc[first_index:-1]
close = close.iloc[first_index:]
close.columns = [col.replace("_returns", "") for col in close.columns]
close = close[predictions.columns]
close = close.filter(items = predictions.index, axis = 0)
availability = close.applymap(lambda x: 0 if x == 0.0 or x == 0 or np.isnan(x) else 1)
+2 -2
View File
@@ -1,4 +1,4 @@
from run_pipeline import run_pipeline
from config.config import get_default_ensemble_config
from config.presets import get_default_ensemble_config
run_pipeline(project_name='price-prediction', with_wandb = True, sweep = True, get_config= get_default_ensemble_config)
run_pipeline(project_name='price-prediction', with_wandb = True, sweep = True, raw_config= get_default_ensemble_config())
+3 -3
View File
@@ -73,7 +73,7 @@ def test_evaluation():
model=model,
X=X,
y=y,
target_returns=y,
forward_returns=y,
expanding_window=False,
window_size=window_length,
retrain_every=10,
@@ -94,12 +94,12 @@ def test_evaluation():
for i in range(window_length+2, no_of_rows):
assert predictions[i] == y[i]
fake_target_returns = y * 0.1
fake_forward_returns = y * 0.1
processed_predictions_to_match_returns = predictions * 0.1
result = evaluate_predictions(
model_name='test',
target_returns=fake_target_returns,
forward_returns=fake_forward_returns,
y_pred=processed_predictions_to_match_returns,
y_true=y,
no_of_classes='two',
+1 -1
View File
@@ -71,7 +71,7 @@ def test_walk_forward_train_test():
model=model,
X=X,
y=y,
target_returns=y,
forward_returns=y,
expanding_window=False,
window_size=window_length,
retrain_every=10,
+4 -4
View File
@@ -5,14 +5,14 @@ import pandas as pd
from models.base import Model
from reporting.types import Reporting
from typing import Union, Optional
from config.config import Config
from config.types import Config
def train_meta_labeling_model(
target_asset: str,
X: pd.DataFrame,
input_predictions: pd.Series,
y: pd.Series,
target_returns: pd.Series,
forward_returns: pd.Series,
models: list[tuple[str, Model]],
config: Config,
model_suffix: str,
@@ -30,7 +30,7 @@ def train_meta_labeling_model(
ticker_to_predict = "prediction_correct",
X = meta_X,
y = meta_y,
target_returns = target_returns,
forward_returns = forward_returns,
models = models,
expanding_window = config.expanding_window_meta_labeling,
sliding_window_size = config.sliding_window_size_meta_labeling,
@@ -52,7 +52,7 @@ def train_meta_labeling_model(
meta_result = evaluate_predictions(
model_name = "Meta",
target_returns = target_returns,
forward_returns = forward_returns,
y_pred = avg_predictions_with_sizing,
y_true = y,
no_of_classes = 'two',
+4 -5
View File
@@ -3,8 +3,7 @@ from typing import Literal, Optional, Union
from training.walk_forward import walk_forward_train, walk_forward_inference
from utils.evaluate import evaluate_predictions
from models.base import Model
from utils.scaler import get_scaler
from utils.types import ScalerTypes
from transformations.scaler import get_scaler, ScalerTypes
from reporting.types import Reporting
from transformations.rfe import RFETransformation
from transformations.pca import PCATransformation
@@ -13,7 +12,7 @@ def train_primary_model(
ticker_to_predict: str,
X: pd.DataFrame,
y: pd.Series,
target_returns: pd.Series,
forward_returns: pd.Series,
models: list[tuple[str, Model]],
expanding_window: bool,
sliding_window_size: int,
@@ -46,7 +45,7 @@ def train_primary_model(
model = model,
X = X,
y = y,
target_returns = target_returns,
forward_returns = forward_returns,
expanding_window = expanding_window,
window_size = sliding_window_size,
retrain_every = retrain_every,
@@ -76,7 +75,7 @@ def train_primary_model(
assert len(preds) == len(y)
result = evaluate_predictions(
model_name = model_name,
target_returns = target_returns,
forward_returns = forward_returns,
y_pred = preds,
y_true = y,
no_of_classes=no_of_classes,
+7 -7
View File
@@ -7,13 +7,13 @@ from training.meta_labeling import train_meta_labeling_model
from reporting.types import Reporting
from typing import Union, Optional
from config.config import Config
from config.types import Config
def primary_step(
X: pd.DataFrame,
y: pd.Series,
target_returns: pd.Series,
forward_returns: pd.Series,
config: Config,
reporting: Reporting,
from_index: Optional[pd.Timestamp],
@@ -26,7 +26,7 @@ def primary_step(
ticker_to_predict = config.target_asset[1],
X = X,
y = y,
target_returns = target_returns,
forward_returns = forward_returns,
models = config.primary_models,
expanding_window = config.expanding_window_base,
sliding_window_size = config.sliding_window_size_base,
@@ -50,7 +50,7 @@ def primary_step(
X = X,
input_predictions= primary_model_predictions,
y = y,
target_returns = target_returns,
forward_returns = forward_returns,
model_suffix = 'meta',
models = config.meta_labeling_models,
config = config,
@@ -75,7 +75,7 @@ def secondary_step(
X:pd.DataFrame,
y:pd.Series,
current_predictions:pd.DataFrame,
target_returns:pd.Series,
forward_returns:pd.Series,
config: Config,
reporting: Reporting,
from_index: Optional[pd.Timestamp],
@@ -89,7 +89,7 @@ def secondary_step(
ticker_to_predict = config.target_asset[1],
X = current_predictions,
y = y,
target_returns = target_returns,
forward_returns = forward_returns,
models = [config.ensemble_model],
expanding_window = False,
sliding_window_size = 1,
@@ -117,7 +117,7 @@ def secondary_step(
X = X,
input_predictions= ensemble_predictions,
y = y,
target_returns = target_returns,
forward_returns = forward_returns,
models = config.meta_labeling_models,
config = config,
model_suffix = 'ensemble',
+2 -2
View File
@@ -11,7 +11,7 @@ def walk_forward_train(
model: Model,
X: pd.DataFrame,
y: pd.Series,
target_returns: pd.Series,
forward_returns: pd.Series,
expanding_window: bool,
window_size: int,
retrain_every: int,
@@ -23,7 +23,7 @@ def walk_forward_train(
models_over_time = pd.Series(index=y.index).rename(model_name)
transformations_over_time = [pd.Series(index=y.index).rename(t.get_name()) for t in transformations]
first_nonzero_return = max(get_first_valid_return_index(target_returns), get_first_valid_return_index(X.iloc[:,0]), get_first_valid_return_index(y))
first_nonzero_return = max(get_first_valid_return_index(forward_returns), get_first_valid_return_index(X.iloc[:,0]), get_first_valid_return_index(y))
train_from = first_nonzero_return + window_size + 1 if from_index is None else X.index.to_list().index(from_index)
train_till = len(y)
iterations_before_retrain = 0
-3
View File
@@ -26,6 +26,3 @@ class Transformation(ABC):
raise NotImplementedError
@@ -1,6 +1,8 @@
from sklearn.preprocessing import MinMaxScaler, Normalizer, StandardScaler
from transformations.sklearn import SKLearnTransformation
from utils.types import ScalerTypes
from .sklearn import SKLearnTransformation
from typing import Literal
ScalerTypes = Literal['normalize', 'minmax', 'standardize']
def get_scaler(type: ScalerTypes) -> SKLearnTransformation:
if type == 'normalize':
+9 -9
View File
@@ -12,23 +12,23 @@ def backtest(returns: pd.Series, signal: pd.Series, transaction_cost = 0.002) ->
return (signal * returns) - costs
def __preprocess(target_returns: pd.Series, y_pred: pd.Series, y_true: pd.Series, no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced'], discretize: bool) -> pd.DataFrame:
def __preprocess(forward_returns: pd.Series, y_pred: pd.Series, y_true: pd.Series, 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()
forward_returns.name = 'forward_returns'
df = pd.concat([y_pred, forward_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
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)
df['result'] = backtest(df.forward_returns, df.sign_pred)
return df
def evaluate_predictions(
model_name: str,
target_returns: pd.Series,
forward_returns: pd.Series,
y_pred: pd.Series,
y_true: pd.Series,
no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced'],
@@ -36,12 +36,12 @@ def evaluate_predictions(
discretize: bool = False,
) -> pd.Series:
# ignore the predictions until we see a non-zero returns (and definitely skip the first sliding_window_size)
evaluate_from = max(get_first_valid_return_index(target_returns), get_first_valid_return_index(y_pred))
evaluate_from = max(get_first_valid_return_index(forward_returns), get_first_valid_return_index(y_pred))
target_returns = pd.Series(target_returns[evaluate_from:])
forward_returns = pd.Series(forward_returns[evaluate_from:])
y_pred = pd.Series(y_pred[evaluate_from:])
df = __preprocess(target_returns, y_pred, y_true, no_of_classes, discretize)
df = __preprocess(forward_returns, y_pred, y_true, no_of_classes, discretize)
scorecard = pd.Series()
@@ -51,7 +51,7 @@ def evaluate_predictions(
scorecard.loc['no_of_samples'] = no_of_samples
sharpe = sharpe_ratio(df.result)
scorecard.loc['sharpe'] = sharpe
benchmark_sharpe = sharpe_ratio(df.target_returns)
benchmark_sharpe = sharpe_ratio(df.forward_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)
+1 -9
View File
@@ -3,9 +3,6 @@ import numpy as np
import os
import string
import random
from typing import Union
from config.config import Config
def get_files_from_dir(path: str) -> list[str]:
return [f for f in os.listdir(path) if os.path.isfile(os.path.join(path,f)) and not f.startswith('.')]
@@ -19,10 +16,7 @@ def get_first_valid_return_index(series: pd.Series) -> int:
return 0
return nested_result[0]
def has_enough_samples_to_train(X: pd.DataFrame, y: pd.Series, config: Config) -> bool:
first_valid_index = get_first_valid_return_index(X.iloc[:,0])
samples_to_train = len(y) - first_valid_index
return samples_to_train > config.sliding_window_size_base + config.sliding_window_size_meta_labeling + 100
def flatten(list_of_lists: list) -> list:
return [item for sublist in list_of_lists for item in sublist]
@@ -39,8 +33,6 @@ def weighted_average(df: pd.DataFrame, weights_source: str) -> pd.Series:
return mean_df
def deduplicate_indexes(df: pd.DataFrame) -> pd.DataFrame: return df[~df.index.duplicated(keep='last')]
def drop_columns_if_exist(df: pd.DataFrame, columns: list) -> pd.DataFrame:
for column in columns:
if column in df.columns: