diff --git a/config/presets.py b/config/presets.py index 51e3bf2..22a1d5e 100644 --- a/config/presets.py +++ b/config/presets.py @@ -17,7 +17,7 @@ def get_default_config() -> RawConfig: dimensionality_reduction_ratio=0.5, n_features_to_select=50, sliding_window_size=3800, - retrain_every=1000, + retrain_every=2000, scaler="minmax", # 'normalize' 'minmax' 'standardize' 'robust' assets=["fivemin_crypto"], target_asset="BTCUSDT", @@ -29,9 +29,11 @@ def get_default_config() -> RawConfig: exogenous_features=[], directional_models=classification_models, meta_models=meta_models, - event_filter="cusum_fixed", + event_filter="cusum_vol", + remove_overlapping_events=False, labeling="two_class", - forecasting_horizon=50, + forecasting_horizon=10, + transaction_costs=0.002, save_models=True, ensembling_method="voting_soft", ) diff --git a/config/types.py b/config/types.py index a5b0feb..c52ff3f 100644 --- a/config/types.py +++ b/config/types.py @@ -26,8 +26,10 @@ class RawConfig(BaseModel): other_features: list[str] exogenous_features: list[str] event_filter: Literal["none", "cusum_vol", "cusum_fixed"] + remove_overlapping_events: bool labeling: Literal["two_class", "three_class_balanced", "three_class_imbalanced"] forecasting_horizon: int + transaction_costs: float save_models: bool ensembling_method: Literal["voting_soft", "stacking"] @@ -49,8 +51,10 @@ class Config: other_features: list[tuple[str, FeatureExtractor, list[int]]] exogenous_features: list[tuple[str, FeatureExtractor, list[int]]] event_filter: EventFilter + remove_overlapping_events: bool labeling: EventLabeller forecasting_horizon: int + transaction_costs: float no_of_classes: Literal["two", "three-balanced", "three-imbalanced"] save_models: bool diff --git a/labeling/event_filters/cusum.py b/labeling/event_filters/cusum.py index ef42e1f..88856b7 100644 --- a/labeling/event_filters/cusum.py +++ b/labeling/event_filters/cusum.py @@ -7,12 +7,13 @@ from numba.typed import List class CUSUMVolatilityEventFilter(EventFilter): - def __init__(self, vol_period: int): + def __init__(self, vol_period: int, multiplier: float): self.vol_period = vol_period + self.multiplier = multiplier def get_event_start_times(self, returns: ReturnSeries) -> pd.DatetimeIndex: - rolling_vol = returns.rolling(self.vol_period).std().mean() + rolling_vol = returns.rolling(self.vol_period).std() * self.multiplier filtered_indices = [] pos_threshold = 0 diff --git a/labeling/eventfilters_map.py b/labeling/eventfilters_map.py index 8fa67ff..0004fca 100644 --- a/labeling/eventfilters_map.py +++ b/labeling/eventfilters_map.py @@ -3,6 +3,6 @@ from .event_filters.cusum import CUSUMVolatilityEventFilter, CUSUMFixedEventFilt eventfilters_map = dict( none=NoEventFilter(), - cusum_vol=CUSUMVolatilityEventFilter(vol_period=20), - cusum_fixed=CUSUMFixedEventFilter(threshold=70), + cusum_vol=CUSUMVolatilityEventFilter(vol_period=100, multiplier=3.5), + cusum_fixed=CUSUMFixedEventFilter(threshold=20), ) diff --git a/labeling/labellers/fixed_time_three_class_balanced.py b/labeling/labellers/fixed_time_three_class_balanced.py index fb1616a..2be4a62 100644 --- a/labeling/labellers/fixed_time_three_class_balanced.py +++ b/labeling/labellers/fixed_time_three_class_balanced.py @@ -1,4 +1,4 @@ -from data_loader.types import ReturnSeries, ForwardReturnSeries +from data_loader.types import ReturnSeries from ..types import EventLabeller, EventsDataFrame import pandas as pd from .utils import create_forward_returns @@ -13,7 +13,7 @@ class FixedTimeHorionThreeClassBalancedEventLabeller(EventLabeller): def label_events( self, event_start_times: pd.DatetimeIndex, returns: ReturnSeries - ) -> tuple[EventsDataFrame, ForwardReturnSeries]: + ) -> EventsDataFrame: forward_returns = create_forward_returns(returns, self.time_horizon) cutoff_point = returns.index[-self.time_horizon] @@ -43,15 +43,12 @@ class FixedTimeHorionThreeClassBalancedEventLabeller(EventLabeller): return 1 labels = event_candidates.map(map_class_threeway) - - return ( - pd.DataFrame( - { - "start": event_start_times, - "end": event_start_times + pd.Timedelta(days=self.time_horizon), - "label": labels, - "returns": forward_returns[event_start_times], - } - ), - forward_returns[event_start_times], + events = pd.DataFrame( + { + "start": event_start_times, + "end": event_start_times + pd.Timedelta(minutes=self.time_horizon * 5), + "label": labels, + "returns": forward_returns[event_start_times], + } ) + return events diff --git a/labeling/labellers/fixed_time_three_class_imbalanced.py b/labeling/labellers/fixed_time_three_class_imbalanced.py index 5073be5..22c4b76 100644 --- a/labeling/labellers/fixed_time_three_class_imbalanced.py +++ b/labeling/labellers/fixed_time_three_class_imbalanced.py @@ -1,4 +1,4 @@ -from ..types import EventLabeller, EventsDataFrame, ReturnSeries, ForwardReturnSeries +from ..types import EventLabeller, EventsDataFrame, ReturnSeries import pandas as pd from .utils import create_forward_returns @@ -12,7 +12,7 @@ class FixedTimeHorionThreeClassImbalancedEventLabeller(EventLabeller): def label_events( self, event_start_times: pd.DatetimeIndex, returns: ReturnSeries - ) -> tuple[EventsDataFrame, ForwardReturnSeries]: + ) -> EventsDataFrame: forward_returns = create_forward_returns(returns, self.time_horizon) cutoff_point = returns.index[-self.time_horizon] @@ -42,15 +42,12 @@ class FixedTimeHorionThreeClassImbalancedEventLabeller(EventLabeller): return 1 labels = event_candidates.map(map_class_threeway) - - return ( - pd.DataFrame( - { - "start": event_start_times, - "end": event_start_times + pd.Timedelta(days=self.time_horizon), - "label": labels, - "returns": forward_returns[event_start_times], - } - ), - forward_returns[event_start_times], + events = pd.DataFrame( + { + "start": event_start_times, + "end": event_start_times + pd.Timedelta(minutes=self.time_horizon * 5), + "label": labels, + "returns": forward_returns[event_start_times], + } ) + return events diff --git a/labeling/labellers/fixed_time_two_class.py b/labeling/labellers/fixed_time_two_class.py index 5665dc4..25b2574 100644 --- a/labeling/labellers/fixed_time_two_class.py +++ b/labeling/labellers/fixed_time_two_class.py @@ -1,4 +1,4 @@ -from ..types import EventLabeller, EventsDataFrame, ReturnSeries, ForwardReturnSeries +from ..types import EventLabeller, EventsDataFrame, ReturnSeries import pandas as pd from .utils import create_forward_returns @@ -12,7 +12,7 @@ class FixedTimeHorionTwoClassEventLabeller(EventLabeller): def label_events( self, event_start_times: pd.DatetimeIndex, returns: ReturnSeries - ) -> tuple[EventsDataFrame, ForwardReturnSeries]: + ) -> EventsDataFrame: forward_returns = create_forward_returns(returns, self.time_horizon) cutoff_point = returns.index[-self.time_horizon] @@ -23,15 +23,12 @@ class FixedTimeHorionTwoClassEventLabeller(EventLabeller): return -1 if x <= 0.0 else 1 labels = event_candidates.map(get_class_binary) - - return ( - pd.DataFrame( - { - "start": event_start_times, - "end": event_start_times + pd.Timedelta(days=self.time_horizon), - "label": labels, - "returns": forward_returns[event_start_times], - } - ), - forward_returns[event_start_times], + events = pd.DataFrame( + { + "start": event_start_times, + "end": event_start_times + pd.Timedelta(minutes=self.time_horizon * 5), + "label": labels, + "returns": forward_returns[event_start_times], + } ) + return events diff --git a/labeling/labellers/utils.py b/labeling/labellers/utils.py index 867bbe8..ec436d6 100644 --- a/labeling/labellers/utils.py +++ b/labeling/labellers/utils.py @@ -1,5 +1,6 @@ import pandas as pd from data_loader.types import ForwardReturnSeries +from labeling.types import EventsDataFrame def create_forward_returns(series: pd.Series, period: int) -> ForwardReturnSeries: @@ -8,3 +9,16 @@ def create_forward_returns(series: pd.Series, period: int) -> ForwardReturnSerie forward_returns = series.rolling(window=indexer).sum() return forward_returns + + +def purge_overlapping_events(events: EventsDataFrame) -> EventsDataFrame: + events = events.copy() + indicies_to_remove = [] + last_event_end = events.iloc[0]["start"] + for index, row in events.iterrows(): + if row["start"] < last_event_end: + indicies_to_remove.append(index) + else: + last_event_end = row["end"] + events.drop(indicies_to_remove, inplace=True) + return events diff --git a/labeling/process.py b/labeling/process.py index 26f295f..d979b25 100644 --- a/labeling/process.py +++ b/labeling/process.py @@ -1,5 +1,6 @@ from .types import EventFilter, EventLabeller, EventsDataFrame from data_loader.types import ForwardReturnSeries, XDataFrame, ReturnSeries, ySeries +from .labellers.utils import purge_overlapping_events def label_data( @@ -7,6 +8,7 @@ def label_data( event_labeller: EventLabeller, X: XDataFrame, returns: ReturnSeries, + remove_overlapping_events: bool, ) -> tuple[EventsDataFrame, XDataFrame, ySeries, ForwardReturnSeries]: event_start_times = event_filter.get_event_start_times(returns) @@ -16,7 +18,14 @@ def label_data( "% of timestamps", ) - events, forward_returns = event_labeller.label_events(event_start_times, returns) + events = event_labeller.label_events(event_start_times, returns) + if remove_overlapping_events: + events = purge_overlapping_events(events) + print( + "| Purged ", + (1 - (len(events) / len(event_start_times))) * 100, + "% of overlapping events", + ) X = X.filter(items=events.index, axis=0) y = events["label"] diff --git a/labeling/types.py b/labeling/types.py index f56bbd6..a2092d0 100644 --- a/labeling/types.py +++ b/labeling/types.py @@ -25,5 +25,5 @@ class EventLabeller(ABC): @abstractmethod def label_events( self, event_start_times: pd.DatetimeIndex, returns: ReturnSeries - ) -> tuple[EventsDataFrame, ForwardReturnSeries]: + ) -> EventsDataFrame: raise NotImplementedError diff --git a/run_fetch_data_5min.py b/run_fetch_data_5min.py index 7b47275..8f08a82 100644 --- a/run_fetch_data_5min.py +++ b/run_fetch_data_5min.py @@ -1,9 +1,9 @@ -from binance_historical_data import CandleDataDumper +from binance_historical_data import BinanceDataDumper import datetime -data_dumper = CandleDataDumper( +data_dumper = BinanceDataDumper( path_dir_where_to_dump="./data/5min_crypto/", - str_data_frequency="5m", + data_frequency="5m", ) assets = [ @@ -32,7 +32,7 @@ from tqdm import tqdm import pandas as pd for asset in tqdm(assets): - path = f"./data/5min_crypto/{asset}/5m/monthly/" + path = f"./data/5min_crypto/spot/monthly/klines/{asset}/5m/" files = os.listdir(path) def load_df(path): @@ -56,6 +56,7 @@ for asset in tqdm(assets): df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms") df = df[["timestamp", "open", "high", "low", "close", "volume"]] df.set_index("timestamp", inplace=True) + df.sort_index(inplace=True) return df dfs = pd.concat([load_df(path + file) for file in files], axis=0) diff --git a/run_inference.py b/run_inference.py index 2e7c1f1..252104d 100644 --- a/run_inference.py +++ b/run_inference.py @@ -45,7 +45,11 @@ def __inference(config: Config, pipeline_outcome: PipelineOutcome): # 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 + event_filter=config.event_filter, + event_labeller=config.labeling, + X=X, + returns=returns, + remove_overlapping_events=config.remove_overlapping_events, ) inference_from: pd.Timestamp = X.index[len(X.index) - 1] @@ -65,7 +69,7 @@ def __inference(config: Config, pipeline_outcome: PipelineOutcome): # 4. Run bet sizing on primary model's output bet_sizing_outcome = bet_sizing_with_meta_model( X=X, - input_predictions=directional_training_outcome.training.predictions, + input_predictions=directional_training_outcome.predictions, y=y, forward_returns=forward_returns, model=config.meta_model, @@ -73,7 +77,7 @@ def __inference(config: Config, pipeline_outcome: PipelineOutcome): config=config, model_suffix="meta", from_index=inference_from, - transformations_over_time=pipeline_outcome.bet_sizing.meta_transformations, + transformations_over_time=pipeline_outcome.bet_sizing.meta_training.transformations, preloaded_models=pipeline_outcome.bet_sizing.meta_training.model_over_time, ) diff --git a/run_pipeline.py b/run_pipeline.py index a34ada5..753ca66 100644 --- a/run_pipeline.py +++ b/run_pipeline.py @@ -28,7 +28,7 @@ def run_pipeline( wandb, config = setup_config(project_name, with_wandb, sweep, raw_config) pipeline_outcome = run_training(config) report_results( - pipeline_outcome.directional_training.training.stats, + pipeline_outcome.directional_training.stats, pipeline_outcome.get_output_stats(), pipeline_outcome.get_output_weights(), config, @@ -72,7 +72,11 @@ def run_training(config: Config) -> PipelineOutcome: 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 + event_filter=config.event_filter, + event_labeller=config.labeling, + X=X, + returns=returns, + remove_overlapping_events=config.remove_overlapping_events, ) print("---> Train directional models") @@ -90,7 +94,7 @@ def run_training(config: Config) -> PipelineOutcome: print("---> Run bet sizing on directional model's output") bet_sizing_outcomes = bet_sizing_with_meta_model( X, - directional_training_outcome.training.predictions, + directional_training_outcome.predictions, y, forward_returns, config.meta_model, diff --git a/training/bet_sizing.py b/training/bet_sizing.py index b71c150..fdd58ac 100644 --- a/training/bet_sizing.py +++ b/training/bet_sizing.py @@ -7,7 +7,12 @@ from models.base import Model from models.model_map import default_feature_selector_classification from typing import Optional from config.types import Config -from .types import BetSizingWithMetaOutcome, ModelOverTime, TransformationsOverTime +from .types import ( + BetSizingWithMetaOutcome, + ModelOverTime, + TrainingOutcome, + TransformationsOverTime, +) from training.walk_forward import walk_forward_process_transformations from transformations.base import Transformation @@ -64,6 +69,9 @@ def bet_sizing_with_meta_model( transformations_over_time=transformations_over_time, model_over_time=preloaded_models, ) + meta_outcome = TrainingOutcome( + **vars(meta_outcome), transformations=transformations_over_time + ) meta_predictions = meta_outcome.predictions bet_size = meta_outcome.probabilities.iloc[:, 1] @@ -85,7 +93,6 @@ def bet_sizing_with_meta_model( return BetSizingWithMetaOutcome( model_id, meta_outcome, - transformations_over_time, avg_predictions_with_sizing, stats, ) diff --git a/training/directional_training.py b/training/directional_training.py index e53dfd1..f865a26 100644 --- a/training/directional_training.py +++ b/training/directional_training.py @@ -2,7 +2,7 @@ import pandas as pd from transformations.base import Transformation -from .types import DirectionalTrainingOutcome, TrainingOutcome +from .types import TrainingOutcome from training.train_model import train_model from training.walk_forward import walk_forward_process_transformations @@ -19,8 +19,8 @@ def train_directional_model( model: Model, transformations: list[Transformation], from_index: Optional[pd.Timestamp], - preloaded_training_step: Optional[DirectionalTrainingOutcome] = None, -) -> DirectionalTrainingOutcome: + preloaded_training_step: Optional[TrainingOutcome] = None, +) -> TrainingOutcome: if preloaded_training_step is None: print("Preprocess transformations") @@ -49,11 +49,13 @@ def train_directional_model( level="primary", output_stats=config.mode == "training", transformations_over_time=transformations_over_time, - model_over_time=preloaded_training_step.training.model_over_time + model_over_time=preloaded_training_step.model_over_time if preloaded_training_step else None, ) if config.mode == "training": print(training_outcome.stats) - return DirectionalTrainingOutcome(training_outcome, transformations_over_time) + return TrainingOutcome( + **vars(training_outcome), transformations=transformations_over_time + ) diff --git a/training/train_model.py b/training/train_model.py index 7d4fa6b..5beaea6 100644 --- a/training/train_model.py +++ b/training/train_model.py @@ -7,7 +7,11 @@ from training.walk_forward import ( ) from utils.evaluate import evaluate_predictions from models.base import Model -from .types import ModelOverTime, TransformationsOverTime, TrainingOutcome +from .types import ( + ModelOverTime, + TransformationsOverTime, + TrainingOutcomeWithoutTransformations, +) def train_model( @@ -24,7 +28,7 @@ def train_model( output_stats: bool, transformations_over_time: TransformationsOverTime, model_over_time: Optional[ModelOverTime], -) -> TrainingOutcome: +) -> TrainingOutcomeWithoutTransformations: if model_over_time is None: print("Train model") @@ -74,4 +78,6 @@ def train_model( else: stats = None - return TrainingOutcome(model_id, predictions, probabilities, stats, model_over_time) + return TrainingOutcomeWithoutTransformations( + model_id, predictions, probabilities, stats, model_over_time + ) diff --git a/training/types.py b/training/types.py index 94a8b3f..e4bbe45 100644 --- a/training/types.py +++ b/training/types.py @@ -12,7 +12,7 @@ TransformationsOverTime = list[pd.Series] @dataclass -class TrainingOutcome: +class TrainingOutcomeWithoutTransformations: model_id: str predictions: PredictionsSeries probabilities: ProbabilitiesDataFrame @@ -20,24 +20,22 @@ class TrainingOutcome: model_over_time: ModelOverTime +@dataclass +class TrainingOutcome(TrainingOutcomeWithoutTransformations): + transformations: TransformationsOverTime + + @dataclass class BetSizingWithMetaOutcome: model_id: str meta_training: TrainingOutcome - meta_transformations: TransformationsOverTime weights: WeightsSeries stats: Optional[Stats] -@dataclass -class DirectionalTrainingOutcome: - training: TrainingOutcome - transformations: TransformationsOverTime - - @dataclass class PipelineOutcome: - directional_training: DirectionalTrainingOutcome + directional_training: TrainingOutcome bet_sizing: BetSizingWithMetaOutcome def get_output_weights(self) -> WeightsSeries: