mirror of
https://github.com/webclinic017/drift.git
synced 2026-07-27 18:57:55 +00:00
feat(Events): added EventFilter, EventLabeller (#186)
This commit is contained in:
committed by
GitHub
parent
1042c82333
commit
42a1bc59cb
@@ -137,3 +137,5 @@ lightning_logs/
|
||||
|
||||
results.csv
|
||||
models/saved/
|
||||
|
||||
data/minute_crypto/**
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -0,0 +1,2 @@
|
||||
from .collections import data_collections
|
||||
from .load import load_data, load_only_returns
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
import pandas as pd
|
||||
|
||||
def deduplicate_indexes(df: pd.DataFrame) -> pd.DataFrame: return df[~df.index.duplicated(keep='last')]
|
||||
@@ -34,4 +34,5 @@ dependencies:
|
||||
- alphalens-reloaded
|
||||
- vectorbt
|
||||
- pydantic
|
||||
- pandera[mypy]
|
||||
prefix: /usr/local/anaconda3/envs/quant
|
||||
|
||||
+2
-2
@@ -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])],
|
||||
|
||||
@@ -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:
|
||||
def feature_mom(df: pd.DataFrame, period: int) -> pd.Series:
|
||||
return np.log(df['close']).diff(period)
|
||||
else:
|
||||
return df['close'].pct_change(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")
|
||||
|
||||
@@ -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']
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
+28
-36
@@ -1,54 +1,47 @@
|
||||
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')
|
||||
return 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')
|
||||
return 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')
|
||||
return SKLearnModel(Ridge(alpha=0.1), 'regression')
|
||||
elif model_name == 'BayesianRidge':
|
||||
from sklearn.linear_model import BayesianRidge
|
||||
model_map['primary_models']['BayesianRidge'] = SKLearnModel(BayesianRidge(), 'regression')
|
||||
return SKLearnModel(BayesianRidge(), 'regression')
|
||||
elif model_name == 'KNN':
|
||||
from sklearnex.neighbors import KNeighborsRegressor
|
||||
model_map['primary_models']['KNN'] = SKLearnModel(KNeighborsRegressor(n_neighbors=25), 'regression')
|
||||
return 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')
|
||||
return 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')
|
||||
return 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')
|
||||
return 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')
|
||||
return 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()
|
||||
return 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(
|
||||
return LightningNeuralNetModel(
|
||||
MultiLayerPerceptron(
|
||||
hidden_layers_ratio = [1.0],
|
||||
probabilities = False,
|
||||
@@ -58,43 +51,42 @@ def get_model_map(config:dict):
|
||||
|
||||
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')
|
||||
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
|
||||
model_map['primary_models']['LogisticRegression_three_class'] = SKLearnModel(LogisticRegression_EX(C=10, random_state=1, max_iter=1000, n_jobs=-1), 'classification')
|
||||
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
|
||||
model_map['primary_models']['LDA'] = SKLearnModel(LinearDiscriminantAnalysis(), 'classification')
|
||||
return SKLearnModel(LinearDiscriminantAnalysis(), 'classification')
|
||||
elif model_name == 'KNN':
|
||||
from sklearn.neighbors import KNeighborsClassifier
|
||||
model_map['primary_models']['KNN'] = SKLearnModel(KNeighborsClassifier(), 'classification')
|
||||
return 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')
|
||||
return 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')
|
||||
return SKLearnModel(GaussianNB(), 'classification')
|
||||
elif model_name == 'AB':
|
||||
from sklearn.ensemble import AdaBoostClassifier
|
||||
model_map['primary_models']['AB'] = SKLearnModel(AdaBoostClassifier(n_estimators=15), 'classification')
|
||||
return 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')
|
||||
return 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')
|
||||
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
|
||||
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'))
|
||||
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
|
||||
model_map['primary_models']['LGBM'] = SKLearnModel(LGBMClassifier(n_jobs=-1, max_depth=20, random_state=1), 'classification')
|
||||
return 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)
|
||||
return StaticMomentumModel(allow_short=True)
|
||||
elif model_name == 'Average':
|
||||
from models.average import StaticAverageModel
|
||||
model_map['ensemble_models']['Average'] = StaticAverageModel()
|
||||
|
||||
return model_map
|
||||
|
||||
return StaticAverageModel()
|
||||
else:
|
||||
raise Exception(f'Model {model_name} not found')
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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."
|
||||
|
||||
# 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)
|
||||
# 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)
|
||||
|
||||
# 4. Save the models
|
||||
reporting.asset = Reporting.Asset(ticker= config.target_asset[1], primary=training_step_primary, secondary=training_step_secondary)
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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())
|
||||
@@ -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
@@ -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())
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user