mirror of
https://github.com/webclinic017/drift.git
synced 2026-08-15 20:08:08 +00:00
feat(Project): use SKLearn models directly, removed custom ensembling, use 5 minute data, batch inference, numba cusum filter (#192)
* feat(Project): use 5 minute data, running training in parallel, sped up cusum filter by 10x with numba * fix(WalkForward): inference mini-batch parallelization * fix(WalkForward): don't use the parallel version of any of the functions * feat(CI): download the data required * fix(Project): 5min_crypto folder added * fix(Evaluate): make sure we have numerical stability in returns * feat(Models): use SKLearn models directly to enable composability * feat(Inference): batched inference now working, added forecasting_horizon * fix(Inference): works again * fix(Inference) * chore(Models): remove unused Ensemble model * fix(Labeller): don't just forward shift returns, also take the sum of the data happened until then * Update test.yml
This commit is contained in:
@@ -19,15 +19,19 @@ jobs:
|
|||||||
shell: bash -l {0}
|
shell: bash -l {0}
|
||||||
run: |
|
run: |
|
||||||
pytest --junit-xml pytest.xml
|
pytest --junit-xml pytest.xml
|
||||||
|
- name: Download data
|
||||||
|
shell: bash -l {0}
|
||||||
|
run: |
|
||||||
|
python run_fetch_data_5min.py
|
||||||
- name: Run pipeline
|
- name: Run pipeline
|
||||||
shell: bash -l {0}
|
shell: bash -l {0}
|
||||||
run: |
|
run: |
|
||||||
export RAY_DISABLE_MEMORY_MONITOR=1
|
export RAY_DISABLE_MEMORY_MONITOR=1
|
||||||
python run_pipeline.py
|
python run_pipeline.py
|
||||||
- name: Run portfolio reporting
|
# - name: Run portfolio reporting
|
||||||
shell: bash -l {0}
|
# shell: bash -l {0}
|
||||||
run: |
|
# run: |
|
||||||
python run_portfolio_reporting.py
|
# python run_portfolio_reporting.py
|
||||||
- name: Run inference
|
- name: Run inference
|
||||||
shell: bash -l {0}
|
shell: bash -l {0}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+1
-1
@@ -138,4 +138,4 @@ lightning_logs/
|
|||||||
results.csv
|
results.csv
|
||||||
models/saved/
|
models/saved/
|
||||||
|
|
||||||
data/minute_crypto/**
|
data/5min_crypto/**
|
||||||
+12
-14
@@ -5,6 +5,8 @@ from models.model_map import get_model
|
|||||||
from data_loader.collections import data_collections
|
from data_loader.collections import data_collections
|
||||||
from labeling.eventfilters_map import eventfilters_map
|
from labeling.eventfilters_map import eventfilters_map
|
||||||
from labeling.labellers_map import labellers_map
|
from labeling.labellers_map import labellers_map
|
||||||
|
from models.sklearn import SKLearnModel
|
||||||
|
from sklearn.ensemble import VotingClassifier
|
||||||
|
|
||||||
def preprocess_config(raw_config: RawConfig) -> Config:
|
def preprocess_config(raw_config: RawConfig) -> Config:
|
||||||
config_dict = vars(raw_config)
|
config_dict = vars(raw_config)
|
||||||
@@ -17,7 +19,6 @@ def preprocess_config(raw_config: RawConfig) -> Config:
|
|||||||
config_dict['no_of_classes'] = 'two'
|
config_dict['no_of_classes'] = 'two'
|
||||||
config_dict['mode'] = 'training'
|
config_dict['mode'] = 'training'
|
||||||
config = Config(**config_dict)
|
config = Config(**config_dict)
|
||||||
validate_config(config)
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def __preprocess_feature_extractors_config(data_dict: dict) -> dict:
|
def __preprocess_feature_extractors_config(data_dict: dict) -> dict:
|
||||||
@@ -28,10 +29,14 @@ def __preprocess_feature_extractors_config(data_dict: dict) -> dict:
|
|||||||
data_dict[key] = flatten([feature_extractor_presets[preset_name] for preset_name in preset_names])
|
data_dict[key] = flatten([feature_extractor_presets[preset_name] for preset_name in preset_names])
|
||||||
return data_dict
|
return data_dict
|
||||||
|
|
||||||
def __preprocess_model_config(model_config:dict) -> dict:
|
def __preprocess_model_config(model_config: dict) -> dict:
|
||||||
model_config['directional_models'] = [get_model(model_name) for model_name in model_config['directional_models']]
|
directional_models = [get_model(model_name) for model_name in model_config['directional_models']]
|
||||||
|
model_config.pop('directional_models')
|
||||||
|
model_config['directional_model'] = SKLearnModel(VotingClassifier([(m.name, m)for m in directional_models], voting ='soft'))
|
||||||
if len(model_config['meta_models']) > 0:
|
if len(model_config['meta_models']) > 0:
|
||||||
model_config['meta_models'] = [get_model(model_name) for model_name in model_config['meta_models']]
|
meta_models = [get_model(model_name) for model_name in model_config['meta_models']]
|
||||||
|
model_config['meta_model'] = SKLearnModel(VotingClassifier([(m.name, m)for m in meta_models], voting ='soft'))
|
||||||
|
model_config.pop('meta_models')
|
||||||
|
|
||||||
return model_config
|
return model_config
|
||||||
|
|
||||||
@@ -49,16 +54,9 @@ def __preprocess_event_filter_config(data_dict: dict) -> dict:
|
|||||||
data_dict['event_filter'] = eventfilters_map[data_dict['event_filter']]
|
data_dict['event_filter'] = eventfilters_map[data_dict['event_filter']]
|
||||||
return data_dict
|
return data_dict
|
||||||
|
|
||||||
def __preprocess_event_labeller_config(data_dict: dict) -> dict:
|
def __preprocess_event_labeller_config(config_dict: dict) -> dict:
|
||||||
data_dict['labeling'] = labellers_map[data_dict['labeling']]
|
config_dict['labeling'] = labellers_map[config_dict['labeling']](config_dict['forecasting_horizon'])
|
||||||
return data_dict
|
return config_dict
|
||||||
|
|
||||||
|
|
||||||
def validate_config(config: Config):
|
|
||||||
# We need to make sure there's only one output from the pipeline
|
|
||||||
# If meta model is there, we need more than one directional models to train
|
|
||||||
if len(config.meta_models) > 1: assert len(config.directional_models) > 0
|
|
||||||
# If there's no level-2 model, we need to have only one level-1 model
|
|
||||||
if len(config.meta_models) == 0: assert len(config.directional_models) == 1
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-20
@@ -2,7 +2,6 @@ from .types import RawConfig, Config
|
|||||||
|
|
||||||
def get_dev_config() -> RawConfig:
|
def get_dev_config() -> RawConfig:
|
||||||
|
|
||||||
regression_models = ["Lasso"]
|
|
||||||
classification_models = ["LogisticRegression_two_class"]
|
classification_models = ["LogisticRegression_two_class"]
|
||||||
|
|
||||||
return RawConfig(
|
return RawConfig(
|
||||||
@@ -29,13 +28,13 @@ def get_dev_config() -> RawConfig:
|
|||||||
meta_models = [],
|
meta_models = [],
|
||||||
|
|
||||||
event_filter = 'none',
|
event_filter = 'none',
|
||||||
labeling = 'two_class'
|
labeling = 'two_class',
|
||||||
|
forecasting_horizon = 100,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_default_ensemble_config() -> RawConfig:
|
def get_default_ensemble_config() -> RawConfig:
|
||||||
|
|
||||||
regression_models = ["Lasso", "KNN", "RFR"]
|
|
||||||
classification_models = ["LogisticRegression_two_class", "LDA", "NB", "RFC", "XGB_two_class", "LGBM", "StaticMom"]
|
classification_models = ["LogisticRegression_two_class", "LDA", "NB", "RFC", "XGB_two_class", "LGBM", "StaticMom"]
|
||||||
meta_models = ['LogisticRegression_two_class', 'LGBM']
|
meta_models = ['LogisticRegression_two_class', 'LGBM']
|
||||||
|
|
||||||
@@ -63,42 +62,43 @@ def get_default_ensemble_config() -> RawConfig:
|
|||||||
meta_models = meta_models,
|
meta_models = meta_models,
|
||||||
|
|
||||||
event_filter = 'cusum_vol',
|
event_filter = 'cusum_vol',
|
||||||
labeling = 'two_class'
|
labeling = 'two_class',
|
||||||
|
forecasting_horizon = 100,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_lightweight_ensemble_config() -> RawConfig:
|
def get_lightweight_ensemble_config() -> RawConfig:
|
||||||
|
|
||||||
regression_models = ["Lasso", "KNN"]
|
classification_models = ['LogisticRegression_two_class', 'LGBM']
|
||||||
classification_models = ['LogisticRegression_two_class', 'SVC']
|
|
||||||
meta_models = ['LogisticRegression_two_class', 'LGBM']
|
meta_models = ['LogisticRegression_two_class', 'LGBM']
|
||||||
|
|
||||||
return RawConfig(
|
return RawConfig(
|
||||||
directional_models_meta = True,
|
directional_models_meta = True,
|
||||||
dimensionality_reduction = True,
|
dimensionality_reduction = True,
|
||||||
n_features_to_select = 30,
|
n_features_to_select = 30,
|
||||||
expanding_window_base = False,
|
expanding_window_base = True,
|
||||||
expanding_window_meta = True,
|
expanding_window_meta = True,
|
||||||
sliding_window_size_base = 380,
|
sliding_window_size_base = 3800,
|
||||||
sliding_window_size_meta = 240,
|
sliding_window_size_meta = 2400,
|
||||||
retrain_every = 40,
|
retrain_every = 1000,
|
||||||
scaler = 'minmax', # 'normalize' 'minmax' 'standardize'
|
scaler = 'minmax', # 'normalize' 'minmax' 'standardize'
|
||||||
|
|
||||||
assets = ['daily_crypto_lightweight'],
|
assets = ['fivemin_crypto'],
|
||||||
target_asset = 'BCH_USD',
|
target_asset = 'BTC_USD',
|
||||||
other_assets = ['daily_etf'],
|
other_assets = [],
|
||||||
exogenous_data = ['daily_glassnode'],
|
exogenous_data = [],
|
||||||
load_non_target_asset= True,
|
load_non_target_asset= False,
|
||||||
own_features = ['level_2' ],
|
own_features = ['level_1'],
|
||||||
other_features = ['level_2'],
|
other_features = [],
|
||||||
exogenous_features = ['z_score'],
|
exogenous_features = [],
|
||||||
|
|
||||||
directional_models = classification_models,
|
directional_models = classification_models,
|
||||||
meta_models = meta_models,
|
meta_models = meta_models,
|
||||||
|
|
||||||
event_filter = 'none',
|
event_filter = 'cusum_fixed',
|
||||||
labeling = 'two_class'
|
labeling = 'two_class',
|
||||||
|
forecasting_horizon = 50,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+25
-7
@@ -1,11 +1,12 @@
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, validator
|
||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
from labeling.types import EventFilter
|
from labeling.types import EventFilter
|
||||||
from models.base import Model
|
from models.base import Model
|
||||||
from data_loader.types import DataCollection, DataSource
|
from data_loader.types import DataCollection, DataSource
|
||||||
from feature_extractors.types import FeatureExtractor, ScalerTypes
|
from feature_extractors.types import FeatureExtractor, ScalerTypes
|
||||||
from labeling.types import EventFilter, EventLabeller
|
from labeling.types import EventFilter, EventLabeller
|
||||||
|
from sklearn.base import BaseEstimator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
# RawConfig is needed to ensure we can declare config presets here with static typing, we then convert it to Config
|
# RawConfig is needed to ensure we can declare config presets here with static typing, we then convert it to Config
|
||||||
class RawConfig(BaseModel):
|
class RawConfig(BaseModel):
|
||||||
@@ -29,12 +30,14 @@ class RawConfig(BaseModel):
|
|||||||
exogenous_features: list[str]
|
exogenous_features: list[str]
|
||||||
event_filter: Literal['none', 'cusum_vol', 'cusum_fixed']
|
event_filter: Literal['none', 'cusum_vol', 'cusum_fixed']
|
||||||
labeling: Literal['two_class', 'three_class_balanced', 'three_class_imbalanced']
|
labeling: Literal['two_class', 'three_class_balanced', 'three_class_imbalanced']
|
||||||
|
forecasting_horizon: int
|
||||||
|
|
||||||
directional_models: list[str]
|
directional_models: list[str]
|
||||||
meta_models: list[str]
|
meta_models: list[str]
|
||||||
|
|
||||||
|
|
||||||
class Config(BaseModel):
|
@dataclass
|
||||||
|
class Config:
|
||||||
directional_models_meta: bool
|
directional_models_meta: bool
|
||||||
dimensionality_reduction: bool
|
dimensionality_reduction: bool
|
||||||
n_features_to_select: int
|
n_features_to_select: int
|
||||||
@@ -55,14 +58,29 @@ class Config(BaseModel):
|
|||||||
exogenous_features: list[tuple[str, FeatureExtractor, list[int]]]
|
exogenous_features: list[tuple[str, FeatureExtractor, list[int]]]
|
||||||
event_filter: EventFilter
|
event_filter: EventFilter
|
||||||
labeling: EventLabeller
|
labeling: EventLabeller
|
||||||
|
forecasting_horizon: int
|
||||||
no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced']
|
no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced']
|
||||||
|
|
||||||
mode: Literal['training', 'inference']
|
mode: Literal['training', 'inference']
|
||||||
|
|
||||||
directional_models: list[Model]
|
directional_model: Model
|
||||||
meta_models: list[Model]
|
meta_model: Model
|
||||||
|
|
||||||
class Config:
|
@validator('directional_model', 'meta_model')
|
||||||
arbitrary_types_allowed = True
|
def check_model(cls, v):
|
||||||
|
assert isinstance(v, BaseEstimator)
|
||||||
|
return v
|
||||||
|
|
||||||
|
@validator('event_filter')
|
||||||
|
def check_event_filter(cls, v):
|
||||||
|
assert isinstance(v, EventFilter)
|
||||||
|
return v
|
||||||
|
|
||||||
|
@validator('labeling')
|
||||||
|
def check_labeling(cls, v):
|
||||||
|
assert isinstance(v, EventLabeller)
|
||||||
|
return v
|
||||||
|
# class Config:
|
||||||
|
# arbitrary_types_allowed = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
__daily_crypto_lightweight = ["ADA_USD", "BCH_USD"]
|
||||||
|
|
||||||
__minute_crypto = ["BTC_USD", "DASH_USD", "ETC_USD", "ETH_USD", "LTC_USD", "TRX_USD", "XLM_USD", "XMR_USD"]
|
__5min_crypto = ["BTC_USD", "DASH_USD", "ETC_USD", "ETH_USD", "LTC_USD", "TRX_USD", "XLM_USD", "XMR_USD"]
|
||||||
|
|
||||||
__daily_glassnode = ['rhodl_ratio',
|
__daily_glassnode = ['rhodl_ratio',
|
||||||
# 'cvdd',
|
# 'cvdd',
|
||||||
@@ -45,7 +45,7 @@ data_collections = dict(
|
|||||||
daily_crypto = transform_to_data_collection("data/daily_crypto", __daily_crypto),
|
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_crypto_lightweight = transform_to_data_collection("data/daily_crypto", __daily_crypto_lightweight),
|
||||||
daily_etf = transform_to_data_collection("data/daily_etf", __daily_etf),
|
daily_etf = transform_to_data_collection("data/daily_etf", __daily_etf),
|
||||||
minute_crypto = transform_to_data_collection("data/minute_crypto", __minute_crypto),
|
fivemin_crypto = transform_to_data_collection("data/5min_crypto", __5min_crypto),
|
||||||
daily_glassnode =transform_to_data_collection("data/daily_glassnode", __daily_glassnode),
|
daily_glassnode =transform_to_data_collection("data/daily_glassnode", __daily_glassnode),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+3
-12
@@ -14,7 +14,7 @@ cache = Cache(".cachedir/data")
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def load_data(**kwargs) -> tuple[XDataFrame, ReturnSeries, ForwardReturnSeries]:
|
def load_data(**kwargs) -> tuple[XDataFrame, ReturnSeries]:
|
||||||
hashed = hash_data_config(kwargs)
|
hashed = hash_data_config(kwargs)
|
||||||
if hashed in cache:
|
if hashed in cache:
|
||||||
return cache.get(hashed)
|
return cache.get(hashed)
|
||||||
@@ -31,7 +31,7 @@ def __load_data(assets: DataCollection,
|
|||||||
own_features: list[tuple[str, FeatureExtractor, list[int]]],
|
own_features: list[tuple[str, FeatureExtractor, list[int]]],
|
||||||
other_features: list[tuple[str, FeatureExtractor, list[int]]],
|
other_features: list[tuple[str, FeatureExtractor, list[int]]],
|
||||||
exogenous_features: list[tuple[str, FeatureExtractor, list[int]]],
|
exogenous_features: list[tuple[str, FeatureExtractor, list[int]]],
|
||||||
) -> tuple[XDataFrame, ReturnSeries, ForwardReturnSeries]:
|
) -> tuple[XDataFrame, ReturnSeries]:
|
||||||
"""
|
"""
|
||||||
Loads asset data from the specified path.
|
Loads asset data from the specified path.
|
||||||
Returns:
|
Returns:
|
||||||
@@ -85,12 +85,8 @@ def __load_data(assets: DataCollection,
|
|||||||
## Create target
|
## Create target
|
||||||
returns = df_target_asset_only_returns[target_asset[1] + '_returns']
|
returns = df_target_asset_only_returns[target_asset[1] + '_returns']
|
||||||
returns.index = pd.DatetimeIndex(X.index)
|
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
|
return X, returns
|
||||||
|
|
||||||
|
|
||||||
@ray.remote
|
@ray.remote
|
||||||
@@ -130,11 +126,6 @@ def __apply_feature_extractors(df: pd.DataFrame, feature_extractors: list[tuple[
|
|||||||
return df
|
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:
|
def load_only_returns(assets: DataCollection, returns: Literal['price', 'returns']) -> pd.DataFrame:
|
||||||
|
|
||||||
assets_future = [__load_df.remote(
|
assets_future = [__load_df.remote(
|
||||||
|
|||||||
+1
-2
@@ -9,8 +9,7 @@ dependencies:
|
|||||||
- python=3.9
|
- python=3.9
|
||||||
- scikit-learn-intelex=2021.4.0
|
- scikit-learn-intelex=2021.4.0
|
||||||
- pip:
|
- pip:
|
||||||
- torch
|
- skorch
|
||||||
- pytorch-lightning
|
|
||||||
- fracdiff
|
- fracdiff
|
||||||
- ray
|
- ray
|
||||||
- diskcache
|
- diskcache
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
|
||||||
|
|
||||||
|
from numpy import float32
|
||||||
from ..types import EventFilter
|
from ..types import EventFilter
|
||||||
from data_loader.types import ReturnSeries
|
from data_loader.types import ReturnSeries
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from numba import njit
|
||||||
|
from numba.typed import List
|
||||||
|
|
||||||
class CUSUMVolatilityEventFilter(EventFilter):
|
class CUSUMVolatilityEventFilter(EventFilter):
|
||||||
|
|
||||||
@@ -11,7 +14,7 @@ class CUSUMVolatilityEventFilter(EventFilter):
|
|||||||
|
|
||||||
def get_event_start_times(self, returns: ReturnSeries) -> pd.DatetimeIndex:
|
def get_event_start_times(self, returns: ReturnSeries) -> pd.DatetimeIndex:
|
||||||
|
|
||||||
rolling_vol = returns.rolling(self.vol_period).std() * 0.15
|
rolling_vol = returns.rolling(self.vol_period).std().mean()
|
||||||
|
|
||||||
filtered_indices = []
|
filtered_indices = []
|
||||||
pos_threshold = 0
|
pos_threshold = 0
|
||||||
@@ -39,22 +42,28 @@ class CUSUMFixedEventFilter(EventFilter):
|
|||||||
self.threshold = threshold
|
self.threshold = threshold
|
||||||
|
|
||||||
def get_event_start_times(self, returns: ReturnSeries) -> pd.DatetimeIndex:
|
def get_event_start_times(self, returns: ReturnSeries) -> pd.DatetimeIndex:
|
||||||
filtered_indices = []
|
diffed_returns = returns.diff()
|
||||||
pos_threshold = 0
|
int_indicies = _process(List(diffed_returns.to_list()), abs(returns.mean()) * self.threshold)
|
||||||
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:
|
return pd.DatetimeIndex([returns.index[i] for i in int_indicies])
|
||||||
neg_threshold = 0
|
|
||||||
filtered_indices.append(index)
|
|
||||||
|
|
||||||
elif pos_threshold > self.threshold:
|
@njit
|
||||||
pos_threshold = 0
|
def _process(diffed_returns: List, threshold: float32) -> List:
|
||||||
filtered_indices.append(index)
|
pos_threshold: float32 = 0.0 # type: ignore
|
||||||
|
neg_threshold: float32 = 0.0 # type: ignore
|
||||||
|
filtered_indicies = List()
|
||||||
|
for index in range(1, len(diffed_returns[1:])):
|
||||||
|
pos_threshold, neg_threshold = ( # type: ignore
|
||||||
|
max(0, pos_threshold + diffed_returns[index]), # type: ignore
|
||||||
|
min(0, neg_threshold + diffed_returns[index]), # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
return pd.DatetimeIndex(filtered_indices)
|
if neg_threshold < -threshold:
|
||||||
|
neg_threshold = 0.0 # type: ignore
|
||||||
|
filtered_indicies.append(index)
|
||||||
|
|
||||||
|
elif pos_threshold > threshold:
|
||||||
|
pos_threshold = 0.0 # type: ignore
|
||||||
|
filtered_indicies.append(index)
|
||||||
|
|
||||||
|
return filtered_indicies
|
||||||
@@ -4,5 +4,5 @@ from .event_filters.cusum import CUSUMVolatilityEventFilter, CUSUMFixedEventFilt
|
|||||||
eventfilters_map = dict(
|
eventfilters_map = dict(
|
||||||
none = NoEventFilter(),
|
none = NoEventFilter(),
|
||||||
cusum_vol = CUSUMVolatilityEventFilter(vol_period = 20),
|
cusum_vol = CUSUMVolatilityEventFilter(vol_period = 20),
|
||||||
cusum_fixed = CUSUMFixedEventFilter(threshold = 0.05)
|
cusum_fixed = CUSUMFixedEventFilter(threshold = 500)
|
||||||
)
|
)
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
from data_loader.types import ForwardReturnSeries
|
from data_loader.types import ReturnSeries, ForwardReturnSeries
|
||||||
from ..types import EventLabeller, EventsDataFrame
|
from ..types import EventLabeller, EventsDataFrame
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from .utils import create_forward_returns
|
||||||
|
|
||||||
class FixedTimeHorionThreeClassBalancedEventLabeller(EventLabeller):
|
class FixedTimeHorionThreeClassBalancedEventLabeller(EventLabeller):
|
||||||
|
|
||||||
time_horizon: int
|
time_horizon: int
|
||||||
|
|
||||||
def __init__(self, time_horizon: int = 1):
|
def __init__(self, time_horizon: int):
|
||||||
self.time_horizon = time_horizon
|
self.time_horizon = time_horizon
|
||||||
|
|
||||||
def label_events(self, event_start_times: pd.DatetimeIndex, forward_returns: ForwardReturnSeries) -> EventsDataFrame:
|
def label_events(self, event_start_times: pd.DatetimeIndex, returns: ReturnSeries) -> tuple[EventsDataFrame, ForwardReturnSeries]:
|
||||||
|
|
||||||
|
forward_returns = create_forward_returns(returns, self.time_horizon)
|
||||||
|
cutoff_point = returns.index[-self.time_horizon]
|
||||||
|
event_start_times[event_start_times < cutoff_point]
|
||||||
event_candidates = forward_returns[event_start_times]
|
event_candidates = forward_returns[event_start_times]
|
||||||
|
|
||||||
def get_bins_threeway(x):
|
def get_bins_threeway(x):
|
||||||
@@ -35,10 +39,10 @@ class FixedTimeHorionThreeClassBalancedEventLabeller(EventLabeller):
|
|||||||
return 1
|
return 1
|
||||||
labels = event_candidates.map(map_class_threeway)
|
labels = event_candidates.map(map_class_threeway)
|
||||||
|
|
||||||
return pd.DataFrame({
|
return (pd.DataFrame({
|
||||||
'start': event_start_times,
|
'start': event_start_times,
|
||||||
'end': event_start_times + pd.Timedelta(days=self.time_horizon),
|
'end': event_start_times + pd.Timedelta(days=self.time_horizon),
|
||||||
'label': labels,
|
'label': labels,
|
||||||
'returns': forward_returns[event_start_times]
|
'returns': forward_returns[event_start_times]
|
||||||
})
|
}), forward_returns[event_start_times])
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
from ..types import EventLabeller, EventsDataFrame, ForwardReturnSeries
|
from ..types import EventLabeller, EventsDataFrame, ReturnSeries, ForwardReturnSeries
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from .utils import create_forward_returns
|
||||||
|
|
||||||
class FixedTimeHorionThreeClassImbalancedEventLabeller(EventLabeller):
|
class FixedTimeHorionThreeClassImbalancedEventLabeller(EventLabeller):
|
||||||
|
|
||||||
time_horizon: int
|
time_horizon: int
|
||||||
|
|
||||||
def __init__(self, time_horizon: int = 1):
|
def __init__(self, time_horizon: int):
|
||||||
self.time_horizon = time_horizon
|
self.time_horizon = time_horizon
|
||||||
|
|
||||||
def label_events(self, event_start_times: pd.DatetimeIndex, forward_returns: ForwardReturnSeries) -> EventsDataFrame:
|
def label_events(self, event_start_times: pd.DatetimeIndex, returns: ReturnSeries) -> tuple[EventsDataFrame, ForwardReturnSeries]:
|
||||||
|
|
||||||
|
forward_returns = create_forward_returns(returns, self.time_horizon)
|
||||||
|
cutoff_point = returns.index[-self.time_horizon]
|
||||||
|
event_start_times[event_start_times < cutoff_point]
|
||||||
event_candidates = forward_returns[event_start_times]
|
event_candidates = forward_returns[event_start_times]
|
||||||
|
|
||||||
def get_bins_threeway(x):
|
def get_bins_threeway(x):
|
||||||
@@ -34,11 +38,11 @@ class FixedTimeHorionThreeClassImbalancedEventLabeller(EventLabeller):
|
|||||||
return 1
|
return 1
|
||||||
labels = event_candidates.map(map_class_threeway)
|
labels = event_candidates.map(map_class_threeway)
|
||||||
|
|
||||||
return pd.DataFrame({
|
return (pd.DataFrame({
|
||||||
'start': event_start_times,
|
'start': event_start_times,
|
||||||
'end': event_start_times + pd.Timedelta(days=self.time_horizon),
|
'end': event_start_times + pd.Timedelta(days=self.time_horizon),
|
||||||
'label': labels,
|
'label': labels,
|
||||||
'returns': forward_returns[event_start_times]
|
'returns': forward_returns[event_start_times]
|
||||||
})
|
}), forward_returns[event_start_times])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
|
from ..types import EventLabeller, EventsDataFrame, ReturnSeries, ForwardReturnSeries
|
||||||
|
|
||||||
from ..types import EventLabeller, EventsDataFrame, ForwardReturnSeries
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from .utils import create_forward_returns
|
||||||
|
|
||||||
class FixedTimeHorionTwoClassEventLabeller(EventLabeller):
|
class FixedTimeHorionTwoClassEventLabeller(EventLabeller):
|
||||||
|
|
||||||
time_horizon: int
|
time_horizon: int
|
||||||
|
|
||||||
def __init__(self, time_horizon: int = 1):
|
def __init__(self, time_horizon: int):
|
||||||
self.time_horizon = time_horizon
|
self.time_horizon = time_horizon
|
||||||
|
|
||||||
def label_events(self, event_start_times: pd.DatetimeIndex, forward_returns: ForwardReturnSeries) -> EventsDataFrame:
|
def label_events(self, event_start_times: pd.DatetimeIndex, returns: ReturnSeries) -> tuple[EventsDataFrame, ForwardReturnSeries]:
|
||||||
|
|
||||||
|
forward_returns = create_forward_returns(returns, self.time_horizon)
|
||||||
|
cutoff_point = returns.index[-self.time_horizon]
|
||||||
|
event_start_times[event_start_times < cutoff_point]
|
||||||
event_candidates = forward_returns[event_start_times]
|
event_candidates = forward_returns[event_start_times]
|
||||||
|
|
||||||
def get_class_binary(x: float) -> int:
|
def get_class_binary(x: float) -> int:
|
||||||
return -1 if x <= 0.0 else 1
|
return -1 if x <= 0.0 else 1
|
||||||
labels = event_candidates.map(get_class_binary)
|
labels = event_candidates.map(get_class_binary)
|
||||||
|
|
||||||
return pd.DataFrame({
|
return (pd.DataFrame({
|
||||||
'start': event_start_times,
|
'start': event_start_times,
|
||||||
'end': event_start_times + pd.Timedelta(days=self.time_horizon),
|
'end': event_start_times + pd.Timedelta(days=self.time_horizon),
|
||||||
'label': labels,
|
'label': labels,
|
||||||
'returns': forward_returns[event_start_times]
|
'returns': forward_returns[event_start_times]
|
||||||
})
|
}), forward_returns[event_start_times])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from data_loader.types import ForwardReturnSeries
|
||||||
|
|
||||||
|
def create_forward_returns(series: pd.Series, period: int) -> ForwardReturnSeries:
|
||||||
|
assert period > 0
|
||||||
|
indexer = pd.api.indexers.FixedForwardWindowIndexer(window_size=period)
|
||||||
|
|
||||||
|
forward_returns = series.rolling(window=indexer).sum()
|
||||||
|
return forward_returns
|
||||||
@@ -3,7 +3,7 @@ from .labellers.fixed_time_three_class_imbalanced import FixedTimeHorionThreeCla
|
|||||||
from .labellers.fixed_time_two_class import FixedTimeHorionTwoClassEventLabeller
|
from .labellers.fixed_time_two_class import FixedTimeHorionTwoClassEventLabeller
|
||||||
|
|
||||||
labellers_map = dict(
|
labellers_map = dict(
|
||||||
two_class = FixedTimeHorionTwoClassEventLabeller(),
|
two_class = FixedTimeHorionTwoClassEventLabeller,
|
||||||
three_class_balanced = FixedTimeHorionThreeClassBalancedEventLabeller(),
|
three_class_balanced = FixedTimeHorionThreeClassBalancedEventLabeller,
|
||||||
three_class_imbalanced = FixedTimeHorionThreeClassImbalancedEventLabeller()
|
three_class_imbalanced = FixedTimeHorionThreeClassImbalancedEventLabeller
|
||||||
)
|
)
|
||||||
+14
-12
@@ -2,16 +2,18 @@ from .types import EventFilter, EventLabeller, EventsDataFrame
|
|||||||
from data_loader.types import ForwardReturnSeries, XDataFrame, ReturnSeries, ySeries
|
from data_loader.types import ForwardReturnSeries, XDataFrame, ReturnSeries, ySeries
|
||||||
|
|
||||||
def label_data(
|
def label_data(
|
||||||
event_filter: EventFilter,
|
event_filter: EventFilter,
|
||||||
event_labeller: EventLabeller,
|
event_labeller: EventLabeller,
|
||||||
X: XDataFrame,
|
X: XDataFrame,
|
||||||
returns: ReturnSeries,
|
returns: ReturnSeries) -> tuple[EventsDataFrame, XDataFrame, ySeries, ForwardReturnSeries]:
|
||||||
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
|
event_start_times = event_filter.get_event_start_times(returns)
|
||||||
|
print("| Filtered out ", (1 - (len(event_start_times) / len(returns))) * 100, "% of timestamps" )
|
||||||
|
|
||||||
|
events, forward_returns = event_labeller.label_events(event_start_times, returns)
|
||||||
|
|
||||||
|
X = X.filter(items = events.index, axis = 0)
|
||||||
|
y = events['label']
|
||||||
|
forward_returns = events['returns']
|
||||||
|
|
||||||
|
return events, X, y, forward_returns
|
||||||
+1
-1
@@ -23,6 +23,6 @@ EventsDataFrame = DataFrame[EventSchema]
|
|||||||
class EventLabeller(ABC):
|
class EventLabeller(ABC):
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def label_events(self, event_start_times: pd.DatetimeIndex, forward_returns: ForwardReturnSeries) -> EventsDataFrame:
|
def label_events(self, event_start_times: pd.DatetimeIndex, returns: ReturnSeries) -> tuple[EventsDataFrame, ForwardReturnSeries]:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|||||||
+2
-11
@@ -6,10 +6,8 @@ import numpy as np
|
|||||||
class Model(ABC):
|
class Model(ABC):
|
||||||
|
|
||||||
name: str = ""
|
name: str = ""
|
||||||
method: Literal["regression", "classification"]
|
|
||||||
data_transformation: Literal["transformed", "original"]
|
data_transformation: Literal["transformed", "original"]
|
||||||
only_column: Optional[str]
|
only_column: Optional[str]
|
||||||
model_type: Literal['ml', 'static']
|
|
||||||
predict_window_size: Literal['single_timestamp', 'window_size']
|
predict_window_size: Literal['single_timestamp', 'window_size']
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -17,17 +15,10 @@ class Model(ABC):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def predict(self, X: np.ndarray) -> tuple[float, np.ndarray]:
|
def predict(self, X: np.ndarray) -> np.ndarray:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def clone(self) -> Model:
|
def predict_proba(self, X: np.ndarray) -> np.ndarray:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def initialize_network(self, input_dim:int, output_dim:int):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+16
-60
@@ -3,7 +3,7 @@ from sklearnex.ensemble import RandomForestClassifier
|
|||||||
from sklearnex.ensemble import RandomForestRegressor
|
from sklearnex.ensemble import RandomForestRegressor
|
||||||
from .base import Model
|
from .base import Model
|
||||||
|
|
||||||
default_feature_selector_classification = SKLearnModel(RandomForestClassifier(n_jobs=-1, max_depth=20, random_state=1), 'classification')
|
default_feature_selector_classification = SKLearnModel(RandomForestClassifier(n_jobs=-1, max_depth=20, random_state=1))
|
||||||
|
|
||||||
def get_model(model_name: str) -> Model:
|
def get_model(model_name: str) -> Model:
|
||||||
|
|
||||||
@@ -11,85 +11,41 @@ def get_model(model_name: str) -> Model:
|
|||||||
model.name = model_name
|
model.name = model_name
|
||||||
return model
|
return model
|
||||||
|
|
||||||
if model_name == 'LinearRegression':
|
if model_name == 'LogisticRegression_two_class':
|
||||||
from sklearn.linear_model import LinearRegression
|
|
||||||
return set_name(SKLearnModel(LinearRegression(n_jobs=-1), 'regression'))
|
|
||||||
elif model_name == 'Lasso':
|
|
||||||
from sklearn.linear_model import Lasso
|
|
||||||
return set_name(SKLearnModel(Lasso(alpha=100, random_state=1), 'regression'))
|
|
||||||
elif model_name == 'Ridge':
|
|
||||||
from sklearn.linear_model import Ridge
|
|
||||||
return set_name(SKLearnModel(Ridge(alpha=0.1), 'regression'))
|
|
||||||
elif model_name == 'BayesianRidge':
|
|
||||||
from sklearn.linear_model import BayesianRidge
|
|
||||||
return set_name(SKLearnModel(BayesianRidge(), 'regression'))
|
|
||||||
elif model_name == 'KNN':
|
|
||||||
from sklearnex.neighbors import KNeighborsRegressor
|
|
||||||
return set_name(SKLearnModel(KNeighborsRegressor(n_neighbors=25), 'regression'))
|
|
||||||
elif model_name == 'AB':
|
|
||||||
from sklearn.ensemble import AdaBoostRegressor
|
|
||||||
return set_name(SKLearnModel(AdaBoostRegressor(random_state=1), 'regression'))
|
|
||||||
elif model_name == 'MLP':
|
|
||||||
from sklearn.neural_network import MLPRegressor
|
|
||||||
return set_name(SKLearnModel(MLPRegressor(hidden_layer_sizes=(100,20), max_iter=1000), 'regression'))
|
|
||||||
elif model_name == 'RFR':
|
|
||||||
return set_name(SKLearnModel(RandomForestRegressor(n_jobs=-1, max_depth=20, random_state=1), 'regression'))
|
|
||||||
elif model_name == 'SVR':
|
|
||||||
from sklearnex.svm import SVR
|
|
||||||
return set_name(SKLearnModel(SVR(kernel='rbf', C=1e3, gamma=0.1), 'regression'))
|
|
||||||
elif model_name == 'StaticNaive':
|
|
||||||
from models.naive import StaticNaiveModel
|
|
||||||
return set_name(StaticNaiveModel())
|
|
||||||
elif model_name == 'DNN':
|
|
||||||
from models.neural import LightningNeuralNetModel
|
|
||||||
from models.pytorch.neural_nets import MultiLayerPerceptron
|
|
||||||
import torch.nn.functional as F
|
|
||||||
return set_name(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
|
from sklearn.linear_model import LogisticRegression
|
||||||
return set_name(SKLearnModel(LogisticRegression(C=10, random_state=1, solver='liblinear', max_iter=1000), 'classification'))
|
return set_name(SKLearnModel(LogisticRegression(C=10, random_state=1, solver='liblinear', max_iter=1000)))
|
||||||
elif model_name == 'LogisticRegression_three_class':
|
elif model_name == 'LogisticRegression_three_class':
|
||||||
from sklearnex.linear_model import LogisticRegression as LogisticRegression_EX
|
from sklearnex.linear_model import LogisticRegression as LogisticRegression_EX
|
||||||
return set_name(SKLearnModel(LogisticRegression_EX(C=10, random_state=1, max_iter=1000, n_jobs=-1), 'classification'))
|
return set_name(SKLearnModel(LogisticRegression_EX(C=10, random_state=1, max_iter=1000, n_jobs=-1)))
|
||||||
elif model_name == 'LDA':
|
elif model_name == 'LDA':
|
||||||
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
|
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
|
||||||
return set_name(SKLearnModel(LinearDiscriminantAnalysis(), 'classification'))
|
return set_name(SKLearnModel(LinearDiscriminantAnalysis()))
|
||||||
elif model_name == 'KNN':
|
elif model_name == 'KNN':
|
||||||
from sklearn.neighbors import KNeighborsClassifier
|
from sklearn.neighbors import KNeighborsClassifier
|
||||||
return set_name(SKLearnModel(KNeighborsClassifier(), 'classification'))
|
return set_name(SKLearnModel(KNeighborsClassifier()))
|
||||||
elif model_name == 'CART':
|
elif model_name == 'CART':
|
||||||
from sklearn.tree import DecisionTreeClassifier
|
from sklearn.tree import DecisionTreeClassifier
|
||||||
return set_name(SKLearnModel(DecisionTreeClassifier(max_depth=15, random_state=1), 'classification'))
|
return set_name(SKLearnModel(DecisionTreeClassifier(max_depth=15, random_state=1)))
|
||||||
elif model_name == 'NB':
|
elif model_name == 'NB':
|
||||||
from sklearn.naive_bayes import GaussianNB
|
from sklearn.naive_bayes import GaussianNB
|
||||||
return set_name(SKLearnModel(GaussianNB(), 'classification'))
|
return set_name(SKLearnModel(GaussianNB()))
|
||||||
elif model_name == 'AB':
|
elif model_name == 'AB':
|
||||||
from sklearn.ensemble import AdaBoostClassifier
|
from sklearn.ensemble import AdaBoostClassifier
|
||||||
return set_name(SKLearnModel(AdaBoostClassifier(n_estimators=15), 'classification'))
|
return set_name(SKLearnModel(AdaBoostClassifier(n_estimators=15)))
|
||||||
elif model_name == 'RFC':
|
elif model_name == 'RFC':
|
||||||
return set_name(SKLearnModel(RandomForestClassifier(n_jobs=-1, max_depth=20, random_state=1), 'classification'))
|
return set_name(SKLearnModel(RandomForestClassifier(n_jobs=-1, max_depth=20, random_state=1)))
|
||||||
elif model_name == 'SVC':
|
elif model_name == 'SVC':
|
||||||
from sklearn.svm import SVC
|
from sklearn.svm import SVC
|
||||||
return set_name(SKLearnModel(SVC(kernel='rbf', C=1e3, probability=True, random_state=1), 'classification'))
|
return set_name(SKLearnModel(SVC(kernel='rbf', C=1e3, probability=True, random_state=1)))
|
||||||
elif model_name == 'XGB_two_class':
|
# elif model_name == 'XGB_two_class':
|
||||||
from xgboost import XGBClassifier
|
# from xgboost import XGBClassifier
|
||||||
from models.xgboost import XGBoostModel
|
# from models.xgboost import XGBoostModel
|
||||||
return set_name(XGBoostModel(XGBClassifier(n_jobs=-1, max_depth = 20, random_state=1, objective='binary:logistic', use_label_encoder= False, eval_metric='mlogloss')))
|
# return set_name(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':
|
elif model_name == 'LGBM':
|
||||||
from lightgbm import LGBMClassifier
|
from lightgbm import LGBMClassifier
|
||||||
return set_name(SKLearnModel(LGBMClassifier(n_jobs=-1, max_depth=20, random_state=1), 'classification'))
|
return set_name(SKLearnModel(LGBMClassifier(n_jobs=-1, max_depth=20, random_state=1)))
|
||||||
elif model_name == 'StaticMom':
|
elif model_name == 'StaticMom':
|
||||||
from models.momentum import StaticMomentumModel
|
from models.momentum import StaticMomentumModel
|
||||||
return set_name(StaticMomentumModel(allow_short=True))
|
return set_name(StaticMomentumModel(allow_short=True))
|
||||||
elif model_name == 'Average':
|
|
||||||
from models.average import StaticAverageModel
|
|
||||||
return set_name(StaticAverageModel())
|
|
||||||
else:
|
else:
|
||||||
raise Exception(f'Model {model_name} not found')
|
raise Exception(f'Model {model_name} not found')
|
||||||
+8
-12
@@ -1,16 +1,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from models.base import Model
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from .base import Model
|
||||||
|
from sklearn.base import BaseEstimator, ClassifierMixin
|
||||||
|
|
||||||
class StaticMomentumModel(Model):
|
class StaticMomentumModel(BaseEstimator, ClassifierMixin, Model):
|
||||||
'''
|
'''
|
||||||
Model that uses only one feature: momentum. It's positive if momentum is greater than 0, otherwise it's negative.
|
Model that uses only one feature: momentum. It's positive if momentum is greater than 0, otherwise it's negative.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
method = 'classification'
|
|
||||||
data_transformation = 'original'
|
data_transformation = 'original'
|
||||||
only_column = 'mom'
|
only_column = 'mom'
|
||||||
model_type = 'static'
|
|
||||||
predict_window_size = 'single_timestamp'
|
predict_window_size = 'single_timestamp'
|
||||||
|
|
||||||
def __init__(self, allow_short: bool) -> None:
|
def __init__(self, allow_short: bool) -> None:
|
||||||
@@ -21,13 +20,10 @@ class StaticMomentumModel(Model):
|
|||||||
# This is a static model, it can' learn anything
|
# This is a static model, it can' learn anything
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def predict(self, X) -> tuple[float, np.ndarray]:
|
def predict(self, X) -> np.ndarray:
|
||||||
negative_class = -1.0 if self.allow_short == True else 0.0
|
negative_class = -1.0 if self.allow_short == True else 0.0
|
||||||
prediction = 1.0 if X[-1][0] > 0 else negative_class
|
prediction = 1.0 if X[-1][0] > 0 else negative_class
|
||||||
return (prediction, np.array([]))
|
return np.array(prediction)
|
||||||
|
|
||||||
def clone(self) -> StaticMomentumModel:
|
def predict_proba(self, X) -> np.ndarray:
|
||||||
return self
|
return np.array([])
|
||||||
|
|
||||||
def initialize_network(self, input_dim:int, output_dim:int):
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
from models.base import Model
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
class StaticNaiveModel(Model):
|
|
||||||
'''
|
|
||||||
Model that carries the last observation (from returns) to the next one, naively.
|
|
||||||
'''
|
|
||||||
|
|
||||||
method = 'regression'
|
|
||||||
data_transformation = 'original'
|
|
||||||
only_column = None
|
|
||||||
model_type = 'static'
|
|
||||||
predict_window_size = 'single_timestamp'
|
|
||||||
|
|
||||||
def fit(self, X: np.ndarray, y: np.ndarray) -> None:
|
|
||||||
# This is a static model, it can' learn anything
|
|
||||||
pass
|
|
||||||
|
|
||||||
def predict(self, X) -> tuple[float, np.ndarray]:
|
|
||||||
return (X[-1][0], np.array([]))
|
|
||||||
|
|
||||||
def clone(self) -> StaticNaiveModel:
|
|
||||||
return self
|
|
||||||
|
|
||||||
def initialize_network(self, input_dim:int, output_dim:int):
|
|
||||||
pass
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
from models.base import Model
|
|
||||||
import numpy as np
|
|
||||||
from models.pytorch.pytorch_dataset import get_dataloader
|
|
||||||
import copy
|
|
||||||
import pytorch_lightning as pl
|
|
||||||
|
|
||||||
class LightningNeuralNetModel(Model):
|
|
||||||
|
|
||||||
method = 'regression'
|
|
||||||
data_transformation = 'transformed'
|
|
||||||
only_column = None
|
|
||||||
model_type = 'ml'
|
|
||||||
|
|
||||||
''' Standard lightning methods '''
|
|
||||||
|
|
||||||
def __init__(self, model, max_epochs=5):
|
|
||||||
self.model = model
|
|
||||||
self.trainer = pl.Trainer(max_epochs=max_epochs)
|
|
||||||
|
|
||||||
def fit(self, X: np.ndarray, y: np.ndarray) -> None:
|
|
||||||
train_dataloader = self.__prepare_data(X.astype(float), y.astype(float))
|
|
||||||
self.trainer.fit(self.model, train_dataloader)
|
|
||||||
|
|
||||||
def predict(self, X: np.ndarray) -> tuple[float, np.ndarray]:
|
|
||||||
return self.model(X)
|
|
||||||
|
|
||||||
def clone(self):
|
|
||||||
model_copy = copy.deepcopy(self.model)
|
|
||||||
return LightningNeuralNetModel(model_copy)
|
|
||||||
|
|
||||||
''' Non-standard lightning methods '''
|
|
||||||
def __prepare_data(self, X:np.ndarray, y:np.ndarray):
|
|
||||||
dataloader = get_dataloader(X, y)
|
|
||||||
return dataloader
|
|
||||||
|
|
||||||
def initialize_network(self, input_dim:int, output_dim:int):
|
|
||||||
self.model.initialize_network(input_dim, output_dim)
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import torch
|
|
||||||
from torch import nn
|
|
||||||
import torch.nn.functional as F
|
|
||||||
from torch.utils.data import DataLoader, random_split
|
|
||||||
import pytorch_lightning as pl
|
|
||||||
import math
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
class MultiLayerPerceptron(pl.LightningModule):
|
|
||||||
def __init__(self, hidden_layers_ratio: list[float] = [2.0, 2.0], probabilities: bool = False, loss_function=F.mse_loss):
|
|
||||||
super().__init__()
|
|
||||||
self.hidden_layers_ratio = hidden_layers_ratio
|
|
||||||
self.probabilities = probabilities
|
|
||||||
self.loss_function = loss_function
|
|
||||||
self.float()
|
|
||||||
|
|
||||||
def initialize_network(self, input_dim: int, output_dim: int) -> None:
|
|
||||||
self.layers = nn.ModuleList()
|
|
||||||
current_dim = input_dim
|
|
||||||
|
|
||||||
for hdim in self.hidden_layers_ratio:
|
|
||||||
hidden_layer_size = int(math.floor(current_dim * hdim))
|
|
||||||
self.layers.append(nn.Linear(current_dim, hidden_layer_size))
|
|
||||||
self.layers.append(nn.ReLU())
|
|
||||||
current_dim = hidden_layer_size
|
|
||||||
|
|
||||||
self.layers.append(nn.Linear(current_dim, output_dim))
|
|
||||||
|
|
||||||
def forward(self, x: torch.Tensor):
|
|
||||||
# in lightning, forward defines the prediction/inference actions
|
|
||||||
x = torch.from_numpy(x).float()
|
|
||||||
for layer in self.layers:
|
|
||||||
x = layer(x)
|
|
||||||
|
|
||||||
if self.probabilities:
|
|
||||||
x = F.softmax(x, dim=1)
|
|
||||||
|
|
||||||
return (x.item(), np.array([]))
|
|
||||||
|
|
||||||
def training_step(self, batch: torch.Tensor, batch_idx):
|
|
||||||
# training_step defined the train loop.
|
|
||||||
# It is independent of forward
|
|
||||||
x, y = batch
|
|
||||||
x = x.view(x.size(0), -1)
|
|
||||||
|
|
||||||
|
|
||||||
loss = 0
|
|
||||||
for layer in self.layers:
|
|
||||||
x = layer(x.float())
|
|
||||||
|
|
||||||
if self.probabilities:
|
|
||||||
p = F.softmax(x, dim=1)
|
|
||||||
loss = F.nll_loss(torch.log(p), y.float())
|
|
||||||
|
|
||||||
loss = self.loss_function(x, y.float())
|
|
||||||
|
|
||||||
return loss
|
|
||||||
|
|
||||||
def configure_optimizers(self):
|
|
||||||
optimizer = torch.optim.Adam(self.parameters(), lr=1e-3)
|
|
||||||
return optimizer
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import os
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
import torch
|
|
||||||
from torch.utils.data import Dataset
|
|
||||||
from torch.utils.data import DataLoader
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
|
|
||||||
class TimeSeriesDataset(Dataset):
|
|
||||||
def __init__(self, X: np.ndarray, y: np.ndarray):
|
|
||||||
self.X = X
|
|
||||||
self.y = y
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.X)
|
|
||||||
|
|
||||||
def __getitem__(self, idx):
|
|
||||||
return self.X[idx].astype(float), self.y[idx].astype(float)
|
|
||||||
|
|
||||||
def get_dataloader(X: np.ndarray, y: np.ndarray, batch_size: int = 32, shuffle: bool = True):
|
|
||||||
training_data = TimeSeriesDataset(X, y)
|
|
||||||
train_dataloader = DataLoader(training_data, batch_size=batch_size, shuffle=shuffle)
|
|
||||||
|
|
||||||
return train_dataloader
|
|
||||||
+13
-23
@@ -1,33 +1,23 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from models.base import Model
|
from .base import Model
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from sklearn.base import clone
|
|
||||||
|
|
||||||
|
|
||||||
class SKLearnModel(Model):
|
|
||||||
|
|
||||||
method: Literal["regression", "classification"]
|
def SKLearnModel(instance) -> Model:
|
||||||
data_transformation = 'transformed'
|
|
||||||
only_column = None
|
|
||||||
model_type = 'ml'
|
|
||||||
predict_window_size = 'single_timestamp'
|
|
||||||
|
|
||||||
def __init__(self, model, method: Literal['regression', 'classification']):
|
instance.data_transformation = 'transformed'
|
||||||
self.model = model
|
instance.only_column = None
|
||||||
self.method = method
|
instance.predict_window_size = 'single_timestamp'
|
||||||
|
instance.name = instance.__class__.__name__
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
def fit(self, X: np.ndarray, y: np.ndarray) -> None:
|
|
||||||
self.model.fit(X, y)
|
|
||||||
|
|
||||||
def predict(self, X) -> tuple[float, np.ndarray]:
|
# def predict(self, X) -> tuple[float, np.ndarray]:
|
||||||
pred = self.model.predict(X).item()
|
# pred = self.model.predict(X).item()
|
||||||
probability = self.model.predict_proba(X).squeeze()
|
# probability = self.model.predict_proba(X).squeeze()
|
||||||
return (pred, probability)
|
# return (pred, probability)
|
||||||
|
|
||||||
def clone(self) -> SKLearnModel:
|
|
||||||
return SKLearnModel(clone(self.model), self.method)
|
|
||||||
|
|
||||||
def initialize_network(self, input_dim:int, output_dim:int):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
from statsmodels.tsa.base.tsa_model import TimeSeriesModel
|
|
||||||
from models.base import Model
|
|
||||||
import numpy as np
|
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
|
|
||||||
class StatsModel(Model):
|
|
||||||
|
|
||||||
# This is work in progress
|
|
||||||
data_transformation = 'transformed'
|
|
||||||
only_column = None
|
|
||||||
model_type = 'ml'
|
|
||||||
predict_window_size = 'single_timestamp'
|
|
||||||
|
|
||||||
def __init__(self, model: TimeSeriesModel):
|
|
||||||
self.model = model
|
|
||||||
|
|
||||||
def fit(self, X: np.ndarray, y: np.ndarray) -> None:
|
|
||||||
self.model.fit(X, y)
|
|
||||||
|
|
||||||
def predict(self, X) -> tuple[float, np.ndarray]:
|
|
||||||
pred = self.model.predict(X).item()
|
|
||||||
return (pred, np.array([0]))
|
|
||||||
|
|
||||||
def clone(self) -> StatsModel:
|
|
||||||
return StatsModel(deepcopy(self.model))
|
|
||||||
|
|
||||||
def initialize_network(self, input_dim:int, output_dim:int):
|
|
||||||
pass
|
|
||||||
|
|
||||||
+18
-29
@@ -1,33 +1,22 @@
|
|||||||
from __future__ import annotations
|
# from __future__ import annotations
|
||||||
from models.base import Model
|
# from models.base import Model
|
||||||
import numpy as np
|
# import numpy as np
|
||||||
from xgboost import XGBClassifier
|
# from xgboost import XGBClassifier
|
||||||
from sklearn.base import clone
|
|
||||||
|
|
||||||
class XGBoostModel(Model):
|
# class XGBoostModel(XGBClassifier):
|
||||||
|
|
||||||
method = 'classification'
|
# method = 'classification'
|
||||||
data_transformation = 'transformed'
|
# data_transformation = 'transformed'
|
||||||
only_column = None
|
# only_column = None
|
||||||
model_type = 'ml'
|
# predict_window_size = 'single_timestamp'
|
||||||
predict_window_size = 'single_timestamp'
|
|
||||||
|
|
||||||
def __init__(self, model: XGBClassifier):
|
|
||||||
self.model = model
|
|
||||||
|
|
||||||
def fit(self, X: np.ndarray, y: np.ndarray) -> None:
|
# def fit(self, X: np.ndarray, y: np.ndarray) -> None:
|
||||||
def map_to_xgb(y): return np.array([1 if i == 1 else 0 for i in y])
|
# def map_to_xgb(y): return np.array([1 if i == 1 else 0 for i in y])
|
||||||
self.model.fit(X, map_to_xgb(y))
|
# self.fit(X, map_to_xgb(y))
|
||||||
|
|
||||||
|
# def predict(self, X) -> tuple[float, np.ndarray]:
|
||||||
|
# pred = self.predict(X).item()
|
||||||
|
# probability = self.predict_proba(X).squeeze()
|
||||||
|
# def map_from_xgb(y): return 1 if y == 1 else -1
|
||||||
|
# return (map_from_xgb(pred), probability)
|
||||||
|
|
||||||
def predict(self, X) -> tuple[float, np.ndarray]:
|
|
||||||
pred = self.model.predict(X).item()
|
|
||||||
probability = self.model.predict_proba(X).squeeze()
|
|
||||||
def map_from_xgb(y): return 1 if y == 1 else -1
|
|
||||||
return (map_from_xgb(pred), probability)
|
|
||||||
|
|
||||||
def clone(self) -> XGBoostModel:
|
|
||||||
return XGBoostModel(clone(self.model))
|
|
||||||
|
|
||||||
def initialize_network(self, input_dim:int, output_dim:int):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ class ReplaceFunctionCommand(VisitorBasedCodemodCommand):
|
|||||||
|
|
||||||
def leave_FunctionDef(self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef) -> cst.FunctionDef:
|
def leave_FunctionDef(self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef) -> cst.FunctionDef:
|
||||||
functions_docstring = updated_node.get_docstring()
|
functions_docstring = updated_node.get_docstring()
|
||||||
docstring_should_be = '"""No docstring here yet."""'
|
docstring_should_be = ''
|
||||||
if functions_docstring is not None:
|
if functions_docstring is not None:
|
||||||
docstring_should_be = '"""\n{}\n\n"""'.format(functions_docstring)
|
docstring_should_be = '"""\n{}\n\n"""'.format(functions_docstring)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from utils.helpers import weighted_average
|
|||||||
from config.types import Config
|
from config.types import Config
|
||||||
from training.types import WeightsSeries, Stats
|
from training.types import WeightsSeries, Stats
|
||||||
|
|
||||||
def report_results(directional_stats: list[Stats], output_stats: Stats, output_weights: WeightsSeries, config: Config, wandb, sweep: bool):
|
def report_results(directional_stats: Stats, output_stats: Stats, output_weights: WeightsSeries, config: Config, wandb, sweep: bool):
|
||||||
|
|
||||||
# Only send the results of the final model to wandb
|
# Only send the results of the final model to wandb
|
||||||
send_report_to_wandb(output_stats, wandb)
|
send_report_to_wandb(output_stats, wandb)
|
||||||
@@ -13,18 +13,16 @@ def report_results(directional_stats: list[Stats], output_stats: Stats, output_w
|
|||||||
output_weights.rename(config.target_asset[1]).to_csv('output/predictions.csv')
|
output_weights.rename(config.target_asset[1]).to_csv('output/predictions.csv')
|
||||||
|
|
||||||
print("\n--------\n")
|
print("\n--------\n")
|
||||||
directional_avg_stats = weighted_average(pd.concat([pd.Series(stat) for stat in directional_stats], axis = 1), 'no_of_samples')
|
|
||||||
|
|
||||||
print("Benchmark buy-and-hold sharpe: ", output_stats['benchmark_sharpe'])
|
print("Benchmark buy-and-hold sharpe: ", output_stats['benchmark_sharpe'])
|
||||||
|
|
||||||
print("Level-1: Number of samples evaluated: ", directional_avg_stats.loc['no_of_samples'].sum())
|
print("Level-1: Number of samples evaluated: ", directional_stats['no_of_samples'])
|
||||||
print("Mean Sharpe ratio for Level-1 models: ", round(directional_avg_stats.loc['sharpe'], 3))
|
print("Mean Sharpe ratio for Level-1 models: ", round(directional_stats['sharpe'], 3))
|
||||||
print("Mean Probabilistic Sharpe ratio for Level-1 models: ", round(directional_avg_stats.loc['prob_sharpe'].mean(), 3))
|
print("Mean Probabilistic Sharpe ratio for Level-1 models: ", round(directional_stats['prob_sharpe'], 3))
|
||||||
|
|
||||||
if len(config.meta_models) > 0:
|
print("Level-2 (Ensemble): Number of samples evaluated: ", output_stats['no_of_samples'])
|
||||||
print("Level-2 (Ensemble): Number of samples evaluated: ", output_stats['no_of_samples'])
|
print("Mean Sharpe ratio for Level-2 (Ensemble) models: ", output_stats['sharpe'])
|
||||||
print("Mean Sharpe ratio for Level-2 (Ensemble) models: ", output_stats['sharpe'])
|
print("Mean Probabilistic Sharpe ratio for Level-2 (Ensemble) models: ", output_stats['prob_sharpe'])
|
||||||
print("Mean Probabilistic Sharpe ratio for Level-2 (Ensemble) models: ", output_stats['prob_sharpe'])
|
|
||||||
|
|
||||||
if sweep:
|
if sweep:
|
||||||
if wandb.run is not None:
|
if wandb.run is not None:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import pandas as pd
|
|||||||
import ssl
|
import ssl
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from data_loader.utils import deduplicate_indexes
|
from data_loader.utils import deduplicate_indexes
|
||||||
|
from utils.resample import resample_ohlc
|
||||||
|
|
||||||
base_url = "https://www.cryptodatadownload.com/cdd/"
|
base_url = "https://www.cryptodatadownload.com/cdd/"
|
||||||
|
|
||||||
@@ -27,6 +28,10 @@ for file in tqdm(files_to_download):
|
|||||||
data.drop(volume_column_to_delete + ['symbol'], axis=1, inplace=True)
|
data.drop(volume_column_to_delete + ['symbol'], axis=1, inplace=True)
|
||||||
data.rename({'Volume USD': 'volume'}, axis=1, inplace=True)
|
data.rename({'Volume USD': 'volume'}, axis=1, inplace=True)
|
||||||
data.index.rename('time', inplace=True)
|
data.index.rename('time', inplace=True)
|
||||||
target_file = file.split('_')[1].replace('USD', '') + '_USD'
|
data.sort_index(inplace=True)
|
||||||
data = deduplicate_indexes(data)
|
data = deduplicate_indexes(data)
|
||||||
data.to_csv(f"data/minute_crypto/{target_file}.csv")
|
data.index = pd.to_datetime(data.index)
|
||||||
|
data = data.resample('1Min').ffill()
|
||||||
|
data = resample_ohlc(data, '5Min')
|
||||||
|
target_file = file.split('_')[1].replace('USD', '') + '_USD'
|
||||||
|
data.to_csv(f"data/5min_crypto/{target_file}.csv")
|
||||||
+8
-17
@@ -8,9 +8,8 @@ from config.presets import get_dev_config, get_default_ensemble_config, get_ligh
|
|||||||
from labeling.process import label_data
|
from labeling.process import label_data
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from training.directional_training import train_directional_models
|
from training.directional_training import train_directional_model
|
||||||
from training.bet_sizing import bet_sizing_with_meta_models
|
from training.bet_sizing import bet_sizing_with_meta_model
|
||||||
from training.ensemble import ensemble_weights
|
|
||||||
from training.types import PipelineOutcome
|
from training.types import PipelineOutcome
|
||||||
|
|
||||||
def run_inference(preload_models:bool, fallback_raw_config: RawConfig):
|
def run_inference(preload_models:bool, fallback_raw_config: RawConfig):
|
||||||
@@ -26,7 +25,7 @@ def run_inference(preload_models:bool, fallback_raw_config: RawConfig):
|
|||||||
def __inference(config: Config, pipeline_outcome: PipelineOutcome):
|
def __inference(config: Config, pipeline_outcome: PipelineOutcome):
|
||||||
|
|
||||||
# 1. Load data, check for validity and process data
|
# 1. Load data, check for validity and process data
|
||||||
X, returns, forward_returns = load_data(
|
X, returns = load_data(
|
||||||
assets = config.assets,
|
assets = config.assets,
|
||||||
other_assets = config.other_assets,
|
other_assets = config.other_assets,
|
||||||
exogenous_data = config.exogenous_data,
|
exogenous_data = config.exogenous_data,
|
||||||
@@ -38,26 +37,18 @@ def __inference(config: Config, pipeline_outcome: PipelineOutcome):
|
|||||||
)
|
)
|
||||||
assert check_data(X, 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)
|
# 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)
|
||||||
|
|
||||||
inference_from: pd.Timestamp = X.index[len(X.index) - 1]
|
inference_from: pd.Timestamp = X.index[len(X.index) - 1]
|
||||||
|
|
||||||
# 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 directional models
|
# 3. Train directional models
|
||||||
directional_training_outcome = train_directional_models(X, y, forward_returns, config, config.directional_models, from_index = inference_from, preloaded_training_step = pipeline_outcome.directional_training)
|
directional_training_outcome = train_directional_model(X, y, forward_returns, config, config.directional_model, from_index = inference_from, preloaded_training_step = pipeline_outcome.directional_training)
|
||||||
|
|
||||||
# 4. Run bet sizing on primary model's output
|
# 4. Run bet sizing on primary model's output
|
||||||
bet_sizing_outcomes = [bet_sizing_with_meta_models(X, training_outcome.predictions, y, forward_returns, config.meta_models, config, 'meta', from_index = inference_from, transformations_over_time = preloaded_outcome.meta_transformations, preloaded_models = [b.model_over_time for b in preloaded_outcome.meta_training]) for training_outcome, preloaded_outcome in zip(directional_training_outcome.training, pipeline_outcome.bet_sizing)]
|
bet_sizing_outcome = bet_sizing_with_meta_model(X, directional_training_outcome.training.predictions, y, forward_returns, config.meta_model, config, 'meta', from_index = inference_from, transformations_over_time = pipeline_outcome.bet_sizing.meta_transformations, preloaded_models = pipeline_outcome.bet_sizing.meta_training.model_over_time)
|
||||||
|
|
||||||
# 4. Ensemble weights
|
return PipelineOutcome(directional_training_outcome, bet_sizing_outcome)
|
||||||
ensemble_outcome = ensemble_weights([o.weights for o in bet_sizing_outcomes], forward_returns, y, config.no_of_classes, config.mode == 'training')
|
|
||||||
|
|
||||||
# 5. (Optional) Additional bet sizing on top of the ensembled weights
|
|
||||||
ensemble_bet_sizing_outcome = bet_sizing_with_meta_models(X, ensemble_outcome.weights, y, forward_returns, config.meta_models, config, 'ensemble', from_index = inference_from, transformations_over_time = pipeline_outcome.secondary_bet_sizing.meta_transformations, preloaded_models= [b.model_over_time for b in pipeline_outcome.secondary_bet_sizing.meta_training]) if len(config.meta_models) > 0 else None
|
|
||||||
|
|
||||||
return PipelineOutcome(directional_training_outcome, bet_sizing_outcomes, ensemble_outcome, ensemble_bet_sizing_outcome)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
+14
-23
@@ -1,7 +1,7 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from config.types import Config, RawConfig
|
from config.types import Config, RawConfig
|
||||||
from config.preprocess import preprocess_config, validate_config
|
from config.preprocess import preprocess_config
|
||||||
from config.presets import get_default_ensemble_config, get_lightweight_ensemble_config
|
from config.presets import get_default_ensemble_config, get_lightweight_ensemble_config
|
||||||
|
|
||||||
from data_loader.load import load_data
|
from data_loader.load import load_data
|
||||||
@@ -13,9 +13,8 @@ from reporting.wandb import launch_wandb, override_config_with_wandb_values
|
|||||||
from reporting.reporting import report_results
|
from reporting.reporting import report_results
|
||||||
from reporting.saving import save_models
|
from reporting.saving import save_models
|
||||||
|
|
||||||
from training.directional_training import train_directional_models
|
from training.directional_training import train_directional_model
|
||||||
from training.bet_sizing import bet_sizing_with_meta_models
|
from training.bet_sizing import bet_sizing_with_meta_model
|
||||||
from training.ensemble import ensemble_weights
|
|
||||||
from training.types import PipelineOutcome
|
from training.types import PipelineOutcome
|
||||||
|
|
||||||
import ray
|
import ray
|
||||||
@@ -25,7 +24,7 @@ ray.init()
|
|||||||
def run_pipeline(project_name:str, with_wandb: bool, sweep: bool, raw_config: RawConfig) -> tuple[PipelineOutcome, Config]:
|
def run_pipeline(project_name:str, with_wandb: bool, sweep: bool, raw_config: RawConfig) -> tuple[PipelineOutcome, Config]:
|
||||||
wandb, config = __setup_config(project_name, with_wandb, sweep, raw_config)
|
wandb, config = __setup_config(project_name, with_wandb, sweep, raw_config)
|
||||||
pipeline_outcome = __run_training(config)
|
pipeline_outcome = __run_training(config)
|
||||||
report_results([s.stats for s in pipeline_outcome.directional_training.training], pipeline_outcome.get_output_stats(), pipeline_outcome.get_output_weights(), config, wandb, sweep)
|
report_results(pipeline_outcome.directional_training.training.stats, pipeline_outcome.get_output_stats(), pipeline_outcome.get_output_weights(), config, wandb, sweep)
|
||||||
save_models(pipeline_outcome, config)
|
save_models(pipeline_outcome, config)
|
||||||
return pipeline_outcome, config
|
return pipeline_outcome, config
|
||||||
|
|
||||||
@@ -42,11 +41,9 @@ def __setup_config(project_name:str, with_wandb: bool, sweep: bool, raw_config:
|
|||||||
|
|
||||||
|
|
||||||
def __run_training(config: Config) -> PipelineOutcome:
|
def __run_training(config: Config) -> PipelineOutcome:
|
||||||
|
|
||||||
validate_config(config)
|
|
||||||
|
|
||||||
# 1. Load data, check for validity
|
print("---> Load data, check for validity")
|
||||||
X, returns, forward_returns = load_data(
|
X, returns = load_data(
|
||||||
assets = config.assets,
|
assets = config.assets,
|
||||||
other_assets = config.other_assets,
|
other_assets = config.other_assets,
|
||||||
exogenous_data = config.exogenous_data,
|
exogenous_data = config.exogenous_data,
|
||||||
@@ -59,22 +56,16 @@ def __run_training(config: Config) -> PipelineOutcome:
|
|||||||
|
|
||||||
assert check_data(X, config) == True, "Data is not valid."
|
assert check_data(X, config) == True, "Data is not valid."
|
||||||
|
|
||||||
# 2. Filter for significant events when we want to trade, and label data
|
print("---> 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)
|
events, X, y, forward_returns = label_data(config.event_filter, config.labeling, X, returns)
|
||||||
|
|
||||||
# 3. Train directional models
|
print("---> Train directional models")
|
||||||
directional_training_outcome = train_directional_models(X, y, forward_returns, config, config.directional_models, from_index = None, preloaded_training_step = None)
|
directional_training_outcome = train_directional_model(X, y, forward_returns, config, config.directional_model, from_index = None, preloaded_training_step = None)
|
||||||
|
|
||||||
# 4. Run bet sizing on primary model's output
|
print("---> Run bet sizing on directional model's output")
|
||||||
bet_sizing_outcomes = [bet_sizing_with_meta_models(X, outcome.predictions, y, forward_returns, config.meta_models, config, 'meta', None, None, None) for outcome in directional_training_outcome.training]
|
bet_sizing_outcomes = bet_sizing_with_meta_model(X, directional_training_outcome.training.predictions, y, forward_returns, config.meta_model, config, 'meta', None, None, None)
|
||||||
|
|
||||||
# 4. Ensemble weights
|
return PipelineOutcome(directional_training_outcome, bet_sizing_outcomes)
|
||||||
ensemble_outcome = ensemble_weights([o.weights for o in bet_sizing_outcomes], forward_returns, y, config.no_of_classes, config.mode == 'training')
|
|
||||||
|
|
||||||
# 5. (Optional) Additional bet sizing on top of the ensembled weights
|
|
||||||
ensemble_bet_sizing_outcome = bet_sizing_with_meta_models(X, ensemble_outcome.weights, y, forward_returns, config.meta_models, config, 'ensemble', None, None, None) if len(config.meta_models) > 0 else None
|
|
||||||
|
|
||||||
return PipelineOutcome(directional_training_outcome, bet_sizing_outcomes, ensemble_outcome, ensemble_bet_sizing_outcome)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
run_pipeline(project_name='price-prediction', with_wandb = False, sweep = False, raw_config=get_default_ensemble_config())
|
run_pipeline(project_name='price-prediction', with_wandb = False, sweep = False, raw_config=get_lightweight_ensemble_config())
|
||||||
@@ -4,6 +4,7 @@ import pandas as pd
|
|||||||
from training.walk_forward import walk_forward_train, walk_forward_inference
|
from training.walk_forward import walk_forward_train, walk_forward_inference
|
||||||
from models.base import Model
|
from models.base import Model
|
||||||
from utils.evaluate import evaluate_predictions
|
from utils.evaluate import evaluate_predictions
|
||||||
|
from sklearn.base import BaseEstimator, ClassifierMixin
|
||||||
|
|
||||||
no_of_rows = 100
|
no_of_rows = 100
|
||||||
|
|
||||||
@@ -29,7 +30,8 @@ def __generate_even_odd_test_data(no_of_rows) -> tuple[pd.DataFrame, pd.Series]:
|
|||||||
|
|
||||||
return X, y
|
return X, y
|
||||||
|
|
||||||
class EvenOddStubModel(Model):
|
|
||||||
|
class EvenOddStubModel(BaseEstimator, ClassifierMixin, Model):
|
||||||
'''
|
'''
|
||||||
A deteministic model that can predict the future with 100% accuracy
|
A deteministic model that can predict the future with 100% accuracy
|
||||||
It verifies that the X[n][any_column] == 1 if n is even,
|
It verifies that the X[n][any_column] == 1 if n is even,
|
||||||
@@ -49,14 +51,10 @@ class EvenOddStubModel(Model):
|
|||||||
assert y[i] == -1 if X[i][0] == 1 else 1
|
assert y[i] == -1 if X[i][0] == 1 else 1
|
||||||
|
|
||||||
def predict(self, X):
|
def predict(self, X):
|
||||||
return (-1 if X[0][0] == 1 else 1, np.array([]))
|
return np.array([-1 if row[0] == 1 else 1 for row in X])
|
||||||
|
|
||||||
def clone(self):
|
def predict_proba(self, X):
|
||||||
return self
|
return np.array([[row[0] + 1, 0] for row in X])
|
||||||
|
|
||||||
|
|
||||||
def initialize_network(self, input_dim: int, output_dim: int):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def test_evaluation():
|
def test_evaluation():
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
from training.walk_forward import walk_forward_train, walk_forward_inference
|
from training.walk_forward import walk_forward_train, walk_forward_inference
|
||||||
from models.base import Model
|
from models.base import Model
|
||||||
|
from sklearn.base import BaseEstimator, ClassifierMixin
|
||||||
|
|
||||||
no_of_rows = 100
|
no_of_rows = 100
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ def __generate_incremental_test_data(no_of_rows) -> tuple[pd.DataFrame, pd.Serie
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class IncrementingStubModel(Model):
|
class IncrementingStubModel(Model, BaseEstimator, ClassifierMixin):
|
||||||
'''
|
'''
|
||||||
A deteministic model that can predict the future with 100% accuracy
|
A deteministic model that can predict the future with 100% accuracy
|
||||||
It verifies that the X[n][any_column]+1 == y[n]
|
It verifies that the X[n][any_column]+1 == y[n]
|
||||||
@@ -48,13 +49,10 @@ class IncrementingStubModel(Model):
|
|||||||
assert X[i][0] + 1 == y[i]
|
assert X[i][0] + 1 == y[i]
|
||||||
|
|
||||||
def predict(self, X):
|
def predict(self, X):
|
||||||
return (X[0][0] + 1, np.array([]))
|
return np.array([row[0] + 1 for row in X])
|
||||||
|
|
||||||
def clone(self):
|
def predict_proba(self, X):
|
||||||
return self
|
return np.array([[row[0] + 1, 0] for row in X])
|
||||||
|
|
||||||
def initialize_network(self, input_dim: int, output_dim: int):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_walk_forward_train_test():
|
def test_walk_forward_train_test():
|
||||||
X, y = __generate_incremental_test_data(no_of_rows)
|
X, y = __generate_incremental_test_data(no_of_rows)
|
||||||
|
|||||||
+10
-15
@@ -1,7 +1,7 @@
|
|||||||
from data_loader.types import ForwardReturnSeries, XDataFrame, ySeries
|
from data_loader.types import ForwardReturnSeries, XDataFrame, ySeries
|
||||||
from utils.evaluate import discretize_threeway_threshold, evaluate_predictions
|
from utils.evaluate import discretize_threeway_threshold, evaluate_predictions
|
||||||
from utils.helpers import equal_except_nan
|
from utils.helpers import equal_except_nan
|
||||||
from .train_model import train_models
|
from .train_model import train_model
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from models.base import Model
|
from models.base import Model
|
||||||
from models.model_map import default_feature_selector_classification
|
from models.model_map import default_feature_selector_classification
|
||||||
@@ -13,17 +13,17 @@ from transformations.scaler import get_scaler
|
|||||||
from transformations.rfe import RFETransformation
|
from transformations.rfe import RFETransformation
|
||||||
from transformations.pca import PCATransformation
|
from transformations.pca import PCATransformation
|
||||||
|
|
||||||
def bet_sizing_with_meta_models(
|
def bet_sizing_with_meta_model(
|
||||||
X: XDataFrame,
|
X: XDataFrame,
|
||||||
input_predictions: pd.Series,
|
input_predictions: pd.Series,
|
||||||
y: ySeries,
|
y: ySeries,
|
||||||
forward_returns: ForwardReturnSeries,
|
forward_returns: ForwardReturnSeries,
|
||||||
models: list[Model],
|
model: Model,
|
||||||
config: Config,
|
config: Config,
|
||||||
model_suffix: str,
|
model_suffix: str,
|
||||||
from_index: Optional[pd.Timestamp],
|
from_index: Optional[pd.Timestamp],
|
||||||
transformations_over_time: Optional[TransformationsOverTime] = None,
|
transformations_over_time: Optional[TransformationsOverTime] = None,
|
||||||
preloaded_models: Optional[list[ModelOverTime]] = None
|
preloaded_models: Optional[ModelOverTime] = None
|
||||||
) -> BetSizingWithMetaOutcome:
|
) -> BetSizingWithMetaOutcome:
|
||||||
|
|
||||||
input_predictions.name = "model_predictions"
|
input_predictions.name = "model_predictions"
|
||||||
@@ -50,12 +50,12 @@ def bet_sizing_with_meta_models(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
meta_outcomes = train_models(
|
meta_outcome = train_model(
|
||||||
ticker_to_predict = "prediction_correct",
|
ticker_to_predict = "prediction_correct",
|
||||||
X = meta_X,
|
X = meta_X,
|
||||||
y = meta_y,
|
y = meta_y,
|
||||||
forward_returns = forward_returns,
|
forward_returns = forward_returns,
|
||||||
models = models,
|
model = model,
|
||||||
expanding_window = config.expanding_window_meta,
|
expanding_window = config.expanding_window_meta,
|
||||||
sliding_window_size = config.sliding_window_size_meta,
|
sliding_window_size = config.sliding_window_size_meta,
|
||||||
retrain_every = config.retrain_every,
|
retrain_every = config.retrain_every,
|
||||||
@@ -64,16 +64,11 @@ def bet_sizing_with_meta_models(
|
|||||||
level = 'meta',
|
level = 'meta',
|
||||||
output_stats = config.mode == 'training',
|
output_stats = config.mode == 'training',
|
||||||
transformations_over_time = transformations_over_time,
|
transformations_over_time = transformations_over_time,
|
||||||
models_over_time = preloaded_models,
|
model_over_time = preloaded_models,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ensemble predictions if necessary
|
meta_predictions = meta_outcome.predictions
|
||||||
if len(models) > 1:
|
bet_size = meta_outcome.probabilities.iloc[:,1]
|
||||||
meta_predictions = pd.concat([outcome.predictions for outcome in meta_outcomes], axis = 1).mean(axis = 1).apply(discretize_threeway_threshold(0.5))
|
|
||||||
bet_size = pd.concat([outcome.probabilities[outcome.probabilities.columns[1::2]] for outcome in meta_outcomes], axis = 1).mean(axis = 1)
|
|
||||||
else:
|
|
||||||
meta_predictions = meta_outcomes[0].predictions
|
|
||||||
bet_size = meta_outcomes[0].probabilities.iloc[:,1]
|
|
||||||
avg_predictions_with_sizing = input_predictions * meta_predictions * bet_size
|
avg_predictions_with_sizing = input_predictions * meta_predictions * bet_size
|
||||||
|
|
||||||
if config.mode == 'training':
|
if config.mode == 'training':
|
||||||
@@ -89,4 +84,4 @@ def bet_sizing_with_meta_models(
|
|||||||
stats = None
|
stats = None
|
||||||
model_id = "model_" + config.target_asset[1] + "_" + model_suffix
|
model_id = "model_" + config.target_asset[1] + "_" + model_suffix
|
||||||
|
|
||||||
return BetSizingWithMetaOutcome(model_id, meta_outcomes, transformations_over_time, avg_predictions_with_sizing, stats)
|
return BetSizingWithMetaOutcome(model_id, meta_outcome, transformations_over_time, avg_predictions_with_sizing, stats)
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ from transformations.scaler import get_scaler
|
|||||||
from transformations.rfe import RFETransformation
|
from transformations.rfe import RFETransformation
|
||||||
from transformations.pca import PCATransformation
|
from transformations.pca import PCATransformation
|
||||||
|
|
||||||
def train_directional_models(
|
def train_directional_model(
|
||||||
X: pd.DataFrame,
|
X: pd.DataFrame,
|
||||||
y: pd.Series,
|
y: pd.Series,
|
||||||
forward_returns: pd.Series,
|
forward_returns: pd.Series,
|
||||||
config: Config,
|
config: Config,
|
||||||
models: list[Model],
|
model: Model,
|
||||||
from_index: Optional[pd.Timestamp],
|
from_index: Optional[pd.Timestamp],
|
||||||
preloaded_training_step: Optional[DirectionalTrainingOutcome] = None,
|
preloaded_training_step: Optional[DirectionalTrainingOutcome] = None,
|
||||||
) -> DirectionalTrainingOutcome:
|
) -> DirectionalTrainingOutcome:
|
||||||
@@ -42,12 +42,7 @@ def train_directional_models(
|
|||||||
else:
|
else:
|
||||||
transformations_over_time = preloaded_training_step.transformations
|
transformations_over_time = preloaded_training_step.transformations
|
||||||
|
|
||||||
def print_stats(outcome: TrainingOutcome) -> TrainingOutcome:
|
training_outcome = train_model(
|
||||||
if config.mode == 'training':
|
|
||||||
print(outcome.stats)
|
|
||||||
return outcome
|
|
||||||
|
|
||||||
training_outcomes = [print_stats(train_model(
|
|
||||||
ticker_to_predict = config.target_asset[1],
|
ticker_to_predict = config.target_asset[1],
|
||||||
X = X,
|
X = X,
|
||||||
y = y,
|
y = y,
|
||||||
@@ -55,13 +50,16 @@ def train_directional_models(
|
|||||||
model = model,
|
model = model,
|
||||||
expanding_window = config.expanding_window_base,
|
expanding_window = config.expanding_window_base,
|
||||||
sliding_window_size = config.sliding_window_size_base,
|
sliding_window_size = config.sliding_window_size_base,
|
||||||
retrain_every = config.retrain_every,
|
retrain_every = config.retrain_every,
|
||||||
from_index = from_index,
|
from_index = from_index,
|
||||||
no_of_classes = config.no_of_classes,
|
no_of_classes = config.no_of_classes,
|
||||||
level = 'primary',
|
level = 'primary',
|
||||||
output_stats= config.mode == 'training',
|
output_stats= config.mode == 'training',
|
||||||
transformations_over_time = transformations_over_time,
|
transformations_over_time = transformations_over_time,
|
||||||
model_over_time = preloaded_training_step.training[index].model_over_time if preloaded_training_step else None
|
model_over_time = preloaded_training_step.training.model_over_time if preloaded_training_step else None
|
||||||
)) for index, model in enumerate(models)]
|
)
|
||||||
return DirectionalTrainingOutcome(training_outcomes, transformations_over_time)
|
if config.mode == 'training':
|
||||||
|
print(training_outcome.stats)
|
||||||
|
|
||||||
|
return DirectionalTrainingOutcome(training_outcome, transformations_over_time)
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
from .types import WeightsSeries, EnsembleOutcome
|
|
||||||
import pandas as pd
|
|
||||||
from utils.evaluate import evaluate_predictions
|
|
||||||
from data_loader.types import ForwardReturnSeries, ySeries
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
def ensemble_weights(
|
|
||||||
input_weights: list[WeightsSeries],
|
|
||||||
forward_returns: ForwardReturnSeries,
|
|
||||||
y: ySeries,
|
|
||||||
no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced'],
|
|
||||||
output_stats: bool
|
|
||||||
) -> EnsembleOutcome:
|
|
||||||
weights = pd.concat(input_weights, axis=1).mean(axis=1)
|
|
||||||
if output_stats:
|
|
||||||
stats = evaluate_predictions(
|
|
||||||
forward_returns = forward_returns,
|
|
||||||
y_pred = weights,
|
|
||||||
y_true = y,
|
|
||||||
no_of_classes = no_of_classes,
|
|
||||||
discretize = True,
|
|
||||||
)
|
|
||||||
print(stats)
|
|
||||||
else:
|
|
||||||
stats = None
|
|
||||||
return EnsembleOutcome(weights, stats)
|
|
||||||
+3
-21
@@ -1,29 +1,10 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
from training.walk_forward import walk_forward_train, walk_forward_inference
|
from training.walk_forward import walk_forward_train, walk_forward_inference, walk_forward_inference_batched
|
||||||
from utils.evaluate import evaluate_predictions
|
from utils.evaluate import evaluate_predictions
|
||||||
from models.base import Model
|
from models.base import Model
|
||||||
from .types import ModelOverTime, TransformationsOverTime, TrainingOutcome
|
from .types import ModelOverTime, TransformationsOverTime, TrainingOutcome
|
||||||
|
|
||||||
def train_models(
|
|
||||||
ticker_to_predict: str,
|
|
||||||
X: pd.DataFrame,
|
|
||||||
y: pd.Series,
|
|
||||||
forward_returns: pd.Series,
|
|
||||||
models: list[Model],
|
|
||||||
expanding_window: bool,
|
|
||||||
sliding_window_size: int,
|
|
||||||
retrain_every: int,
|
|
||||||
from_index: Optional[pd.Timestamp],
|
|
||||||
no_of_classes: Literal['two', 'three-balanced', 'three-imbalanced'],
|
|
||||||
level: str,
|
|
||||||
output_stats: bool,
|
|
||||||
transformations_over_time: TransformationsOverTime,
|
|
||||||
models_over_time: Optional[list[ModelOverTime]]
|
|
||||||
) -> list[TrainingOutcome]:
|
|
||||||
return [train_model(ticker_to_predict, X, y, forward_returns, model, expanding_window, sliding_window_size, retrain_every, from_index, no_of_classes, level, output_stats, transformations_over_time, models_over_time[index] if models_over_time else None) for index, model in enumerate(models)]
|
|
||||||
|
|
||||||
|
|
||||||
def train_model(
|
def train_model(
|
||||||
ticker_to_predict: str,
|
ticker_to_predict: str,
|
||||||
X: pd.DataFrame,
|
X: pd.DataFrame,
|
||||||
@@ -61,7 +42,8 @@ def train_model(
|
|||||||
else:
|
else:
|
||||||
model_id = model_over_time.name
|
model_id = model_over_time.name
|
||||||
|
|
||||||
predictions, probabilities = walk_forward_inference(
|
inference_function = walk_forward_inference if from_index is not None else walk_forward_inference_batched
|
||||||
|
predictions, probabilities = inference_function(
|
||||||
model_name = model_id,
|
model_name = model_id,
|
||||||
model_over_time= model_over_time,
|
model_over_time= model_over_time,
|
||||||
transformations_over_time = transformations_over_time,
|
transformations_over_time = transformations_over_time,
|
||||||
|
|||||||
+6
-13
@@ -19,33 +19,26 @@ class TrainingOutcome:
|
|||||||
stats: Optional[Stats]
|
stats: Optional[Stats]
|
||||||
model_over_time: ModelOverTime
|
model_over_time: ModelOverTime
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class EnsembleOutcome:
|
|
||||||
weights: WeightsSeries
|
|
||||||
stats: Optional[Stats]
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BetSizingWithMetaOutcome:
|
class BetSizingWithMetaOutcome:
|
||||||
model_id: str
|
model_id: str
|
||||||
meta_training: list[TrainingOutcome]
|
meta_training: TrainingOutcome
|
||||||
meta_transformations: TransformationsOverTime
|
meta_transformations: TransformationsOverTime
|
||||||
weights: WeightsSeries
|
weights: WeightsSeries
|
||||||
stats: Optional[Stats]
|
stats: Optional[Stats]
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DirectionalTrainingOutcome:
|
class DirectionalTrainingOutcome:
|
||||||
training: list[TrainingOutcome]
|
training: TrainingOutcome
|
||||||
transformations: TransformationsOverTime
|
transformations: TransformationsOverTime
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PipelineOutcome:
|
class PipelineOutcome:
|
||||||
directional_training: DirectionalTrainingOutcome
|
directional_training: DirectionalTrainingOutcome
|
||||||
bet_sizing: list[BetSizingWithMetaOutcome]
|
bet_sizing: BetSizingWithMetaOutcome
|
||||||
ensemble: EnsembleOutcome
|
|
||||||
secondary_bet_sizing: Optional[BetSizingWithMetaOutcome]
|
|
||||||
|
|
||||||
def get_output_weights(self) -> WeightsSeries:
|
def get_output_weights(self) -> WeightsSeries:
|
||||||
return self.secondary_bet_sizing.weights if self.secondary_bet_sizing else self.ensemble.weights
|
return self.bet_sizing.weights
|
||||||
|
|
||||||
def get_output_stats(self) -> Optional[Stats]:
|
def get_output_stats(self) -> Stats:
|
||||||
return self.secondary_bet_sizing.stats if self.secondary_bet_sizing else self.ensemble.stats
|
return self.bet_sizing.stats
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from .inference_batched import walk_forward_inference_batched
|
||||||
from .inference import walk_forward_inference
|
from .inference import walk_forward_inference
|
||||||
from .train import walk_forward_train
|
from .train import walk_forward_train
|
||||||
from .process_transformations_parallel import walk_forward_process_transformations
|
from .process_transformations import walk_forward_process_transformations
|
||||||
@@ -16,7 +16,7 @@ def walk_forward_inference(
|
|||||||
retrain_every: int,
|
retrain_every: int,
|
||||||
from_index: Optional[pd.Timestamp],
|
from_index: Optional[pd.Timestamp],
|
||||||
) -> tuple[PredictionsSeries, ProbabilitiesDataFrame]:
|
) -> tuple[PredictionsSeries, ProbabilitiesDataFrame]:
|
||||||
predictions = pd.Series(index=X.index).rename(model_name)
|
predictions = pd.Series(index=X.index, dtype='object').rename(model_name)
|
||||||
probabilities = pd.DataFrame(index=X.index)
|
probabilities = pd.DataFrame(index=X.index)
|
||||||
|
|
||||||
inference_from = max(get_first_valid_return_index(model_over_time), get_first_valid_return_index(X.iloc[:,0])) if from_index is None else X.index.to_list().index(from_index)
|
inference_from = max(get_first_valid_return_index(model_over_time), get_first_valid_return_index(X.iloc[:,0])) if from_index is None else X.index.to_list().index(from_index)
|
||||||
@@ -49,7 +49,9 @@ def walk_forward_inference(
|
|||||||
|
|
||||||
next_timestep = next_timestep.to_numpy()
|
next_timestep = next_timestep.to_numpy()
|
||||||
|
|
||||||
prediction, probs = current_model.predict(next_timestep)
|
prediction = current_model.predict(next_timestep)
|
||||||
|
probs = current_model.predict_proba(next_timestep)
|
||||||
|
|
||||||
predictions[X.index[index]] = prediction
|
predictions[X.index[index]] = prediction
|
||||||
if inference_from == index and len(probabilities.columns) != len(probs):
|
if inference_from == index and len(probabilities.columns) != len(probs):
|
||||||
probabilities = probabilities.reindex(columns = ["prob_" + str(num) for num in range(0, len(probs.T))])
|
probabilities = probabilities.reindex(columns = ["prob_" + str(num) for num in range(0, len(probs.T))])
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from training.types import ModelOverTime, TransformationsOverTime, PredictionsSeries, ProbabilitiesDataFrame
|
||||||
|
from utils.helpers import get_first_valid_return_index
|
||||||
|
from tqdm import tqdm
|
||||||
|
from typing import Optional
|
||||||
|
from data_loader.types import XDataFrame
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
def walk_forward_inference_batched(
|
||||||
|
model_name: str,
|
||||||
|
model_over_time: ModelOverTime,
|
||||||
|
transformations_over_time: TransformationsOverTime,
|
||||||
|
X: XDataFrame,
|
||||||
|
expanding_window: bool,
|
||||||
|
window_size: int,
|
||||||
|
retrain_every: int,
|
||||||
|
from_index: Optional[pd.Timestamp],
|
||||||
|
) -> tuple[PredictionsSeries, ProbabilitiesDataFrame]:
|
||||||
|
predictions = pd.Series(index=X.index, dtype='object').rename(model_name)
|
||||||
|
probabilities = pd.DataFrame(index=X.index, columns=['0', '1'])
|
||||||
|
|
||||||
|
inference_from = max(get_first_valid_return_index(model_over_time), get_first_valid_return_index(X.iloc[:,0])) if from_index is None else X.index.to_list().index(from_index)
|
||||||
|
inference_till = X.shape[0]
|
||||||
|
first_model = model_over_time[inference_from]
|
||||||
|
|
||||||
|
if first_model.only_column is not None:
|
||||||
|
X = X[[column for column in X.columns if first_model.only_column in column]]
|
||||||
|
|
||||||
|
if first_model.data_transformation == 'original':
|
||||||
|
transformations_over_time = []
|
||||||
|
|
||||||
|
batch_indices = range(inference_from, inference_till, retrain_every) if inference_till - inference_from > retrain_every else [inference_from]
|
||||||
|
batched_results = [__inference_from_window(index, index + retrain_every, X, model_over_time, transformations_over_time, expanding_window, window_size) for index in tqdm(batch_indices)]
|
||||||
|
for batch in batched_results:
|
||||||
|
for index, prediction, probs in batch:
|
||||||
|
predictions[X.index[index]] = prediction
|
||||||
|
probabilities.loc[X.index[index]] = probs
|
||||||
|
|
||||||
|
return predictions, probabilities
|
||||||
|
|
||||||
|
def __inference_from_window(index_start: int, index_end: int, X: XDataFrame, model_over_time: ModelOverTime, transformations_over_time: TransformationsOverTime, expanding_window: bool, window_size: int) -> list[tuple[int, float, pd.Series]]:
|
||||||
|
current_model = model_over_time[X.index[index_start]]
|
||||||
|
current_transformations = [transformation_over_time[X.index[index_start]] for transformation_over_time in transformations_over_time]
|
||||||
|
|
||||||
|
input_data = X.iloc[index_start:index_end]
|
||||||
|
|
||||||
|
for transformation in current_transformations:
|
||||||
|
input_data = transformation.transform(input_data)
|
||||||
|
|
||||||
|
input_data = input_data.to_numpy()
|
||||||
|
|
||||||
|
predictions = current_model.predict(input_data)
|
||||||
|
probs = current_model.predict_proba(input_data)
|
||||||
|
results = [(index_start + index, predictions[index], probs[index]) for index in range(len(predictions))]
|
||||||
|
|
||||||
|
return results
|
||||||
@@ -18,7 +18,7 @@ def walk_forward_inference(
|
|||||||
retrain_every: int,
|
retrain_every: int,
|
||||||
from_index: Optional[pd.Timestamp],
|
from_index: Optional[pd.Timestamp],
|
||||||
) -> tuple[PredictionsSeries, ProbabilitiesDataFrame]:
|
) -> tuple[PredictionsSeries, ProbabilitiesDataFrame]:
|
||||||
predictions = pd.Series(index=X.index).rename(model_name)
|
predictions = pd.Series(index=X.index, dtype='object').rename(model_name)
|
||||||
probabilities = pd.DataFrame(index=X.index)
|
probabilities = pd.DataFrame(index=X.index)
|
||||||
|
|
||||||
inference_from = max(get_first_valid_return_index(model_over_time), get_first_valid_return_index(X.iloc[:,0])) if from_index is None else X.index.to_list().index(from_index)
|
inference_from = max(get_first_valid_return_index(model_over_time), get_first_valid_return_index(X.iloc[:,0])) if from_index is None else X.index.to_list().index(from_index)
|
||||||
@@ -31,32 +31,38 @@ def walk_forward_inference(
|
|||||||
if first_model.data_transformation == 'original':
|
if first_model.data_transformation == 'original':
|
||||||
transformations_over_time = []
|
transformations_over_time = []
|
||||||
|
|
||||||
results = ray.get([__inference_from_window.remote(index, inference_from, retrain_every, X, model_over_time, transformations_over_time, expanding_window, window_size) for index in range(inference_from, inference_till)])
|
batch_size = int((inference_till - inference_from) / 10)
|
||||||
for index, prediction, probs in results:
|
batched_results = ray.get([__inference_from_window.remote(index, index + batch_size, inference_from, retrain_every, X, model_over_time, transformations_over_time, expanding_window, window_size) for index in range(inference_from, inference_till)])
|
||||||
predictions[X.index[index]] = prediction
|
for batch in batched_results:
|
||||||
probabilities.loc[X.index[index]] = probs
|
for index, prediction, probs in batch:
|
||||||
|
predictions[X.index[index]] = prediction
|
||||||
|
probabilities.loc[X.index[index]] = probs
|
||||||
|
|
||||||
return predictions, probabilities
|
return predictions, probabilities
|
||||||
|
|
||||||
@ray.remote
|
@ray.remote
|
||||||
def __inference_from_window(index: int, inference_from: int, retrain_every: int, X: XDataFrame, model_over_time: ModelOverTime, transformations_over_time: TransformationsOverTime, expanding_window: bool, window_size: int) -> tuple[int, float, pd.Series]:
|
def __inference_from_window(index_start: int, index_end: int, inference_from: int, retrain_every: int, X: XDataFrame, model_over_time: ModelOverTime, transformations_over_time: TransformationsOverTime, expanding_window: bool, window_size: int) -> list[tuple[int, float, pd.Series]]:
|
||||||
|
|
||||||
last_model_index = index - ((index - inference_from) % retrain_every)
|
|
||||||
train_window_start = X.index[inference_from] if expanding_window else X.index[index - window_size - 1]
|
|
||||||
|
|
||||||
current_model = model_over_time[X.index[last_model_index]]
|
|
||||||
current_transformations = [transformation_over_time[X.index[last_model_index]] for transformation_over_time in transformations_over_time]
|
|
||||||
|
|
||||||
if current_model.predict_window_size == 'window_size':
|
|
||||||
next_timestep = X.loc[train_window_start:X.index[index]]
|
|
||||||
else:
|
|
||||||
# we need to get a Dataframe out of it, since the transformation step always expects a 2D array, but it's equivalent to X.iloc[index]
|
|
||||||
next_timestep = X.loc[X.index[index]:X.index[index]]
|
|
||||||
|
|
||||||
for transformation in current_transformations:
|
results = []
|
||||||
next_timestep = transformation.transform(next_timestep)
|
for index in range(index_start, index_end):
|
||||||
|
last_model_index = index - ((index - inference_from) % retrain_every)
|
||||||
|
train_window_start = X.index[inference_from] if expanding_window else X.index[index - window_size - 1]
|
||||||
|
|
||||||
next_timestep = next_timestep.to_numpy()
|
current_model = model_over_time[X.index[last_model_index]]
|
||||||
|
current_transformations = [transformation_over_time[X.index[last_model_index]] for transformation_over_time in transformations_over_time]
|
||||||
|
|
||||||
prediction, probs = current_model.predict(next_timestep)
|
if current_model.predict_window_size == 'window_size':
|
||||||
return index, prediction, probs
|
next_timestep = X.loc[train_window_start:X.index[index]]
|
||||||
|
else:
|
||||||
|
# we need to get a Dataframe out of it, since the transformation step always expects a 2D array, but it's equivalent to X.iloc[index]
|
||||||
|
next_timestep = X.loc[X.index[index]:X.index[index]]
|
||||||
|
|
||||||
|
for transformation in current_transformations:
|
||||||
|
next_timestep = transformation.transform(next_timestep)
|
||||||
|
|
||||||
|
next_timestep = next_timestep.to_numpy()
|
||||||
|
|
||||||
|
prediction, probs = current_model.predict(next_timestep)
|
||||||
|
results.append((index, prediction, probs))
|
||||||
|
|
||||||
|
return results
|
||||||
@@ -17,7 +17,7 @@ def walk_forward_process_transformations(
|
|||||||
from_index: Optional[pd.Timestamp],
|
from_index: Optional[pd.Timestamp],
|
||||||
transformations: list[Transformation],
|
transformations: list[Transformation],
|
||||||
) -> TransformationsOverTime:
|
) -> TransformationsOverTime:
|
||||||
transformations_over_time = [pd.Series(index=y.index).rename(t.get_name()) for t in transformations]
|
transformations_over_time = [pd.Series(index=y.index, dtype='object').rename(t.get_name()) for t in transformations]
|
||||||
|
|
||||||
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))
|
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_from = first_nonzero_return + window_size + 1 if from_index is None else X.index.to_list().index(from_index)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from transformations.base import Transformation
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from data_loader.types import ForwardReturnSeries, XDataFrame, ySeries
|
from data_loader.types import ForwardReturnSeries, XDataFrame, ySeries
|
||||||
import ray
|
import ray
|
||||||
|
from utils.parallel import parallel_compute_with_bar
|
||||||
|
|
||||||
def walk_forward_process_transformations(
|
def walk_forward_process_transformations(
|
||||||
X: XDataFrame,
|
X: XDataFrame,
|
||||||
@@ -23,7 +24,7 @@ def walk_forward_process_transformations(
|
|||||||
train_from = first_nonzero_return + window_size + 1 if from_index is None else X.index.to_list().index(from_index)
|
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)
|
train_till = len(y)
|
||||||
|
|
||||||
processed_transformations = ray.get([preprocess_transformations_window.remote(X, y, expanding_window, window_size, transformations, first_nonzero_return, index) for index in range(train_from, train_till, retrain_every)])
|
processed_transformations = parallel_compute_with_bar([preprocess_transformations_window.remote(X, y, expanding_window, window_size, transformations, first_nonzero_return, index) for index in range(train_from, train_till, retrain_every)])
|
||||||
|
|
||||||
for transformation, index_time in processed_transformations:
|
for transformation, index_time in processed_transformations:
|
||||||
for transformation_index, transformation in enumerate(transformation):
|
for transformation_index, transformation in enumerate(transformation):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from utils.helpers import get_first_valid_return_index
|
|||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from data_loader.types import ForwardReturnSeries, XDataFrame, ySeries
|
from data_loader.types import ForwardReturnSeries, XDataFrame, ySeries
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
def walk_forward_train(
|
def walk_forward_train(
|
||||||
model: Model,
|
model: Model,
|
||||||
@@ -18,7 +18,7 @@ def walk_forward_train(
|
|||||||
from_index: Optional[pd.Timestamp],
|
from_index: Optional[pd.Timestamp],
|
||||||
transformations_over_time: TransformationsOverTime,
|
transformations_over_time: TransformationsOverTime,
|
||||||
) -> ModelOverTime:
|
) -> ModelOverTime:
|
||||||
models_over_time = pd.Series(index=y.index).rename(model.name)
|
models_over_time = pd.Series(index=y.index, dtype='object').rename(model.name)
|
||||||
|
|
||||||
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))
|
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_from = first_nonzero_return + window_size + 1 if from_index is None else X.index.to_list().index(from_index)
|
||||||
@@ -43,13 +43,9 @@ def walk_forward_train(
|
|||||||
X_slice = X_slice.to_numpy()
|
X_slice = X_slice.to_numpy()
|
||||||
y_slice = y[train_window_start:train_window_end].to_numpy()
|
y_slice = y[train_window_start:train_window_end].to_numpy()
|
||||||
|
|
||||||
current_model = model.clone()
|
current_model = deepcopy(model)
|
||||||
|
|
||||||
current_model.initialize_network(input_dim = len(X_slice[0]), output_dim=1)
|
|
||||||
current_model.fit(X_slice, y_slice)
|
current_model.fit(X_slice, y_slice)
|
||||||
|
|
||||||
models_over_time[X.index[index]] = current_model
|
models_over_time[X.index[index]] = current_model
|
||||||
for transformation_index, transformation in enumerate(current_transformations):
|
|
||||||
transformations_over_time[transformation_index][X.index[index]] = transformation
|
|
||||||
|
|
||||||
return models_over_time
|
return models_over_time
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from models.base import Model
|
||||||
|
from training.types import ModelOverTime, TransformationsOverTime
|
||||||
|
from utils.helpers import get_first_valid_return_index
|
||||||
|
from tqdm import tqdm
|
||||||
|
from typing import Optional
|
||||||
|
from data_loader.types import ForwardReturnSeries, XDataFrame, ySeries
|
||||||
|
import ray
|
||||||
|
from utils.parallel import parallel_compute_with_bar
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
def walk_forward_train(
|
||||||
|
model: Model,
|
||||||
|
X: XDataFrame,
|
||||||
|
y: ySeries,
|
||||||
|
forward_returns: ForwardReturnSeries,
|
||||||
|
expanding_window: bool,
|
||||||
|
window_size: int,
|
||||||
|
retrain_every: int,
|
||||||
|
from_index: Optional[pd.Timestamp],
|
||||||
|
transformations_over_time: TransformationsOverTime,
|
||||||
|
) -> ModelOverTime:
|
||||||
|
models_over_time = pd.Series(index=y.index).rename(model.name)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if model.only_column is not None:
|
||||||
|
X = X[[column for column in X.columns if model.only_column in column]]
|
||||||
|
|
||||||
|
if model.data_transformation == 'original':
|
||||||
|
transformations_over_time = []
|
||||||
|
|
||||||
|
models = parallel_compute_with_bar([train_on_window.remote(index, first_nonzero_return, window_size, X, y, model, expanding_window, transformations_over_time) for index in tqdm(range(train_from, train_till, retrain_every))])
|
||||||
|
for index, current_model in models:
|
||||||
|
models_over_time[X.index[index]] = current_model
|
||||||
|
|
||||||
|
return models_over_time
|
||||||
|
|
||||||
|
@ray.remote
|
||||||
|
def train_on_window(index: int, first_nonzero_return: int, window_size: int, X: XDataFrame, y: ySeries, model: Model, expanding_window: bool, transformations_over_time: TransformationsOverTime) -> tuple[int, Model]:
|
||||||
|
train_window_start = X.index[first_nonzero_return] if expanding_window else X.index[index - window_size - 1]
|
||||||
|
|
||||||
|
train_window_end = X.index[index - 1]
|
||||||
|
current_transformations = [transformation_over_time[index] for transformation_over_time in transformations_over_time]
|
||||||
|
X_slice = X[train_window_start:train_window_end]
|
||||||
|
|
||||||
|
for transformation in current_transformations:
|
||||||
|
X_slice = transformation.transform(X_slice)
|
||||||
|
|
||||||
|
X_slice = X_slice.to_numpy()
|
||||||
|
y_slice = y[train_window_start:train_window_end].to_numpy()
|
||||||
|
|
||||||
|
current_model = deepcopy(model)
|
||||||
|
|
||||||
|
current_model.fit(X_slice, y_slice)
|
||||||
|
return index, model
|
||||||
@@ -4,21 +4,17 @@ from typing import Optional
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from sklearn.feature_selection import RFE
|
from sklearn.feature_selection import RFE
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from models.base import Model
|
from models.sklearn import SKLearnModel
|
||||||
from models.model_map import default_feature_selector_classification
|
|
||||||
|
|
||||||
class RFETransformation(Transformation):
|
class RFETransformation(Transformation):
|
||||||
|
|
||||||
rfe: RFE
|
rfe: RFE
|
||||||
n_feature_to_select: int
|
n_feature_to_select: int
|
||||||
|
|
||||||
def __init__(self, n_feature_to_select: int, model: Model, step = 0.1):
|
def __init__(self, n_feature_to_select: int, model: SKLearnModel, step = 0.1):
|
||||||
self.n_feature_to_keep = n_feature_to_select
|
self.n_feature_to_keep = n_feature_to_select
|
||||||
self.model = model
|
self.model = model
|
||||||
if hasattr(self.model, 'model') == False: return
|
self.rfe = RFE(model, n_features_to_select= n_feature_to_select, step=step)
|
||||||
if hasattr(self.model.model, 'feature_importances_') == False and hasattr(self.model.model, 'coef_') == False:
|
|
||||||
model = default_feature_selector_classification
|
|
||||||
self.rfe = RFE(model.model, n_features_to_select= n_feature_to_select, step=step)
|
|
||||||
|
|
||||||
def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None) -> None:
|
def fit(self, X: pd.DataFrame, y: Optional[pd.Series] = None) -> None:
|
||||||
if self.rfe is None: return
|
if self.rfe is None: return
|
||||||
|
|||||||
+1
-1
@@ -49,7 +49,7 @@ def evaluate_predictions(
|
|||||||
return len(series[series != 0])
|
return len(series[series != 0])
|
||||||
no_of_samples = count_non_zero(df.y_pred)
|
no_of_samples = count_non_zero(df.y_pred)
|
||||||
scorecard['no_of_samples'] = no_of_samples
|
scorecard['no_of_samples'] = no_of_samples
|
||||||
sharpe = sharpe_ratio(df.result)
|
sharpe = sharpe_ratio(df.result + 1e-20)
|
||||||
scorecard['sharpe'] = sharpe
|
scorecard['sharpe'] = sharpe
|
||||||
benchmark_sharpe = sharpe_ratio(df.forward_returns)
|
benchmark_sharpe = sharpe_ratio(df.forward_returns)
|
||||||
scorecard['benchmark_sharpe'] = benchmark_sharpe
|
scorecard['benchmark_sharpe'] = benchmark_sharpe
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from tqdm import tqdm
|
||||||
|
import ray
|
||||||
|
|
||||||
|
def parallel_compute_with_bar(computations) -> list:
|
||||||
|
|
||||||
|
def to_iterator(obj_ids):
|
||||||
|
while obj_ids:
|
||||||
|
done, obj_ids = ray.wait(obj_ids)
|
||||||
|
yield ray.get(done[0])
|
||||||
|
|
||||||
|
ret = []
|
||||||
|
for x in tqdm(to_iterator(computations), total=len(computations)):
|
||||||
|
ret.append(x)
|
||||||
|
|
||||||
|
return ret
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
def resample_ohlc(df, period):
|
||||||
|
output = pd.DataFrame()
|
||||||
|
output['open'] = df.open.resample(period).first()
|
||||||
|
output['high'] = df.high.resample(period).max()
|
||||||
|
output['low'] = df.low.resample(period).min()
|
||||||
|
output['close'] = df.close.resample(period).last()
|
||||||
|
return output
|
||||||
Reference in New Issue
Block a user